dual-brain 4.6.0 → 4.7.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,21 +3,12 @@
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 Claude provides its own independent analysis, and both
7
- * perspectives are synthesized into a final recommendation.
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.
8
8
  *
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
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?"
21
12
  *
22
13
  * Usage as module:
23
14
  * import { dualThink } from './dual-brain-think.mjs';
@@ -28,7 +19,7 @@
28
19
  * });
29
20
  */
30
21
 
31
- import { execSync, spawnSync } from 'child_process';
22
+ import { spawnSync } from 'child_process';
32
23
  import { appendFileSync } from 'fs';
33
24
  import { dirname, join } from 'path';
34
25
  import { fileURLToPath } from 'url';
@@ -36,7 +27,6 @@ import { fileURLToPath } from 'url';
36
27
  const __dirname = dirname(fileURLToPath(import.meta.url));
37
28
 
38
29
  const CODEX_TIMEOUT_MS = 120_000;
39
- const CLAUDE_TIMEOUT_MS = 60_000;
40
30
  const MODEL = 'gpt-5.5';
41
31
 
42
32
  // ---------------------------------------------------------------------------
@@ -66,28 +56,11 @@ function findCodex() {
66
56
  return null;
67
57
  }
68
58
 
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;
59
+ function isCodexAuthenticated(result) {
60
+ const out = ((result?.stdout || '') + (result?.stderr || '')).toLowerCase();
61
+ if (/\b(not\s+logged\s+in|unauthenticated|logged\s+out|no\s+auth)\b/.test(out)) return false;
62
+ return result?.status === 0 ||
63
+ /\b(logged\s+in|authenticated|signed\s+in)\b/.test(out);
91
64
  }
92
65
 
93
66
  // ---------------------------------------------------------------------------
@@ -195,102 +168,6 @@ function runGptAnalysis(codexBin, prompt) {
195
168
  };
196
169
  }
197
170
 
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
-
294
171
  // ---------------------------------------------------------------------------
295
172
  // Usage logger — matches schema_version: 2 used across the orchestrator
296
173
  // ---------------------------------------------------------------------------
@@ -340,13 +217,12 @@ export async function dualThink({ question, context, files, round, claudePerspec
340
217
  };
341
218
  }
342
219
 
343
- try {
344
- execSync(`${codexBin} login status`, {
345
- encoding: 'utf8',
346
- stdio: ['pipe', 'pipe', 'pipe'],
347
- timeout: 5000,
348
- });
349
- } catch {
220
+ const login = spawnSync(codexBin, ['login', 'status'], {
221
+ encoding: 'utf8',
222
+ stdio: ['pipe', 'pipe', 'pipe'],
223
+ timeout: 5000,
224
+ });
225
+ if (!isCodexAuthenticated(login)) {
350
226
  return {
351
227
  gpt: null,
352
228
  error: 'Codex CLI not authenticated — run `codex login`',
@@ -406,131 +282,6 @@ export async function dualThink({ question, context, files, round, claudePerspec
406
282
  };
407
283
  }
408
284
 
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
-
534
285
  // ---------------------------------------------------------------------------
535
286
  // CLI argument parser
536
287
  // ---------------------------------------------------------------------------
@@ -567,7 +318,7 @@ function parseArgs(argv) {
567
318
  }
568
319
 
569
320
  // ---------------------------------------------------------------------------
570
- // CLI output formatter (manual mode)
321
+ // CLI output formatter
571
322
  // ---------------------------------------------------------------------------
572
323
 
573
324
  function printResult(result, question) {
@@ -578,23 +329,23 @@ function printResult(result, question) {
578
329
  const roundLabel = result.round === 2 ? 'Round 2 — Rebuttal' : 'Round 1 — Initial';
579
330
 
580
331
  console.log(TOP);
581
- console.log(`║ Dual-Brain Think · ${roundLabel}`.padEnd(51) + '║');
332
+ console.log(`║ 🧠 Dual-Brain Think · ${roundLabel}`.padEnd(51) + '║');
582
333
  console.log(BAR);
583
334
  const q = question.length > 44 ? question.slice(0, 41) + '...' : question;
584
335
  console.log(`║ Question: ${q.padEnd(38)} ║`);
585
336
  console.log(BAR);
586
337
 
587
338
  if (!result.gpt) {
588
- console.log(`║ ${(result.error || 'Unknown error').padEnd(46)} ║`);
339
+ console.log(`║${(result.error || 'Unknown error').padEnd(45)} ║`);
589
340
  console.log(BAR);
590
- console.log(`║ ${(result.fallback || '').padEnd(46)} ║`);
341
+ console.log(`║ ↩️ ${(result.fallback || '').padEnd(45)} ║`);
591
342
  console.log(BOT);
592
343
  return;
593
344
  }
594
345
 
595
346
  const gptData = result.gpt;
596
347
  const durSec = (gptData.durationMs / 1000).toFixed(1);
597
- console.log(`║ GPT-5.5 (${durSec}s):`.padEnd(51) + '║');
348
+ console.log(`║ 🤖 GPT-5.5 (${durSec}s):`.padEnd(51) + '║');
598
349
  console.log(BAR);
599
350
  console.log('');
600
351
  console.log(gptData.recommendation || gptData.rebuttal);
@@ -602,13 +353,13 @@ function printResult(result, question) {
602
353
  console.log(BAR);
603
354
 
604
355
  if (result.round === 2) {
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. ║');
356
+ console.log('║ 🔄 Synthesize both rounds into final decision. ║');
357
+ console.log('║ Where you agree → high confidence. ║');
358
+ console.log('║ Where you disagree → state what would resolve it.║');
608
359
  } else {
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. ║');
360
+ console.log('║ 📝 Your turn: analyze independently, then call ║');
361
+ console.log('║ Round 2 with --round 2 --claude-says "..." ║');
362
+ console.log('║ for GPT\'s rebuttal to your analysis. ║');
612
363
  }
613
364
  console.log(BOT);
614
365
  }
@@ -623,32 +374,18 @@ if (import.meta.url === `file://${process.argv[1]}`) {
623
374
  if (!args.question) {
624
375
  console.error(
625
376
  'Usage: node dual-brain-think.mjs --question "<question>" [--context "<ctx>"] [--files f1,f2]\n' +
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)'
377
+ ' node dual-brain-think.mjs --question "<question>" --round 2 --claude-says "<analysis>"'
628
378
  );
629
379
  process.exit(1);
630
380
  }
631
381
 
632
- const hasExplicitRound = args.round !== undefined;
633
- const isManual = args.manual === true || hasExplicitRound;
382
+ const result = await dualThink({
383
+ question: args.question,
384
+ context: args.context,
385
+ files: args.files,
386
+ round: args.round ? parseInt(args.round, 10) : 1,
387
+ claudePerspective: args['claude-says'] || null,
388
+ });
634
389
 
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
- }
390
+ printResult(result, args.question);
654
391
  }