dual-brain 0.1.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.
Files changed (68) hide show
  1. package/AGENTS.md +97 -0
  2. package/CLAUDE.md +147 -0
  3. package/LICENSE +21 -0
  4. package/README.md +197 -0
  5. package/agents/implementer.md +22 -0
  6. package/agents/researcher.md +25 -0
  7. package/agents/verifier.md +30 -0
  8. package/bin/dual-brain.mjs +2868 -0
  9. package/hooks/auto-update-wrapper.mjs +102 -0
  10. package/hooks/auto-update.sh +67 -0
  11. package/hooks/budget-balancer.mjs +679 -0
  12. package/hooks/control-panel.mjs +1195 -0
  13. package/hooks/cost-logger.mjs +286 -0
  14. package/hooks/cost-report.mjs +351 -0
  15. package/hooks/decision-ledger.mjs +299 -0
  16. package/hooks/dual-brain-review.mjs +404 -0
  17. package/hooks/dual-brain-think.mjs +393 -0
  18. package/hooks/enforce-tier.mjs +469 -0
  19. package/hooks/failure-detector.mjs +138 -0
  20. package/hooks/gpt-work-dispatcher.mjs +512 -0
  21. package/hooks/head-guard.mjs +105 -0
  22. package/hooks/health-check.mjs +444 -0
  23. package/hooks/install-git-hooks.mjs +106 -0
  24. package/hooks/model-registry.mjs +859 -0
  25. package/hooks/plan-generator.mjs +544 -0
  26. package/hooks/profiles.mjs +254 -0
  27. package/hooks/quality-gate.mjs +355 -0
  28. package/hooks/risk-classifier.mjs +41 -0
  29. package/hooks/session-report.mjs +514 -0
  30. package/hooks/setup-wizard.mjs +130 -0
  31. package/hooks/summary-checkpoint.mjs +432 -0
  32. package/hooks/task-classifier.mjs +328 -0
  33. package/hooks/test-orchestrator.mjs +1077 -0
  34. package/hooks/vibe-memory.mjs +463 -0
  35. package/hooks/vibe-router.mjs +387 -0
  36. package/hooks/wave-orchestrator.mjs +1397 -0
  37. package/install.mjs +1541 -0
  38. package/mcp-server/README.md +81 -0
  39. package/mcp-server/index.mjs +388 -0
  40. package/orchestrator.json +215 -0
  41. package/package.json +108 -0
  42. package/playbooks/debug.json +49 -0
  43. package/playbooks/refactor.json +57 -0
  44. package/playbooks/security-audit.json +57 -0
  45. package/playbooks/security.json +38 -0
  46. package/playbooks/test-gen.json +48 -0
  47. package/plugin.json +22 -0
  48. package/review-rules.md +17 -0
  49. package/shell-hook.sh +26 -0
  50. package/skills/go.md +22 -0
  51. package/skills/review.md +19 -0
  52. package/skills/status.md +13 -0
  53. package/skills/think.md +22 -0
  54. package/src/brief.mjs +266 -0
  55. package/src/decide.mjs +635 -0
  56. package/src/decompose.mjs +331 -0
  57. package/src/detect.mjs +345 -0
  58. package/src/dispatch.mjs +942 -0
  59. package/src/health.mjs +253 -0
  60. package/src/index.mjs +44 -0
  61. package/src/install-hooks.mjs +100 -0
  62. package/src/playbook.mjs +257 -0
  63. package/src/profile.mjs +990 -0
  64. package/src/redact.mjs +192 -0
  65. package/src/repo.mjs +292 -0
  66. package/src/session.mjs +1036 -0
  67. package/src/tui.mjs +197 -0
  68. package/src/update-check.mjs +35 -0
@@ -0,0 +1,393 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * dual-brain-think.mjs
4
+ *
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.
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?"
12
+ *
13
+ * Usage as module:
14
+ * import { dualThink } from './dual-brain-think.mjs';
15
+ * const result = await dualThink({
16
+ * question: "Should we use queues or direct calls?",
17
+ * context: "Building a notification system that handles ~1000 events/min",
18
+ * files: ['src/notifications/'],
19
+ * });
20
+ */
21
+
22
+ import { spawnSync } from 'child_process';
23
+ import { appendFileSync } from 'fs';
24
+ import { dirname, join } from 'path';
25
+ import { fileURLToPath } from 'url';
26
+
27
+ const __dirname = dirname(fileURLToPath(import.meta.url));
28
+ const IS_REPLIT = !!(process.env.REPL_ID || process.env.REPL_SLUG);
29
+ const SANDBOX = IS_REPLIT ? 'danger-full-access' : 'read-only';
30
+
31
+ const CODEX_TIMEOUT_MS = 120_000;
32
+ const MODEL = 'gpt-5.5';
33
+
34
+ // ---------------------------------------------------------------------------
35
+ // Codex discovery — same pattern as dual-brain-review.mjs
36
+ // ---------------------------------------------------------------------------
37
+
38
+ function findCodex() {
39
+ const candidates = [
40
+ process.env.CODEX_BIN,
41
+ ].filter(Boolean);
42
+ for (const c of candidates) {
43
+ try { spawnSync(c, ['--version'], { stdio: 'pipe', timeout: 3000 }); return c; } catch {}
44
+ }
45
+ try {
46
+ const which = spawnSync('which', ['codex'], { encoding: 'utf8', stdio: 'pipe', timeout: 3000 });
47
+ if (which.status === 0 && which.stdout.trim()) return which.stdout.trim();
48
+ } catch {}
49
+ const home = process.env.HOME || process.env.USERPROFILE || '';
50
+ const fallbacks = [
51
+ join(home, '.local', 'bin', 'codex'),
52
+ join(home, 'bin', 'codex'),
53
+ '/usr/local/bin/codex',
54
+ ];
55
+ for (const p of fallbacks) {
56
+ try { spawnSync(p, ['--version'], { stdio: 'pipe', timeout: 3000 }); return p; } catch {}
57
+ }
58
+ return null;
59
+ }
60
+
61
+ function isCodexAuthenticated(result) {
62
+ const out = ((result?.stdout || '') + (result?.stderr || '')).toLowerCase();
63
+ if (/\b(not\s+logged\s+in|unauthenticated|logged\s+out|no\s+auth)\b/.test(out)) return false;
64
+ return result?.status === 0 ||
65
+ /\b(logged\s+in|authenticated|signed\s+in)\b/.test(out);
66
+ }
67
+
68
+ // ---------------------------------------------------------------------------
69
+ // Prompt builder
70
+ // ---------------------------------------------------------------------------
71
+
72
+ function buildGptPrompt({ question, context, files, round, claudePerspective }) {
73
+ if (round === 2 && claudePerspective) {
74
+ return `You are GPT-5.5 in a collaborative architectural discussion with Claude (Opus).
75
+ You gave your initial analysis on a question. Claude has now provided its independent perspective.
76
+ This is a professional dialogue — two experts refining a decision together.
77
+
78
+ Original question: ${question}
79
+ ${context ? `\nContext: ${context}` : ''}
80
+
81
+ Claude's perspective:
82
+ ${claudePerspective}
83
+
84
+ Now respond as a colleague, not a critic. Structure your response:
85
+ 1. AGREEMENTS: Where Claude's analysis strengthens or confirms your thinking
86
+ 2. PUSHBACK: Where you disagree — be specific about WHY with evidence or reasoning
87
+ 3. NEW INSIGHTS: Anything Claude's perspective surfaced that you missed
88
+ 4. REFINED RECOMMENDATION: Your updated recommendation incorporating both perspectives
89
+ 5. REMAINING CONCERNS: Open questions neither of you fully resolved
90
+ 6. CONFIDENCE DELTA: Has your confidence changed? Why?
91
+
92
+ Be direct and substantive. If Claude is right about something you got wrong, say so.
93
+ If you still disagree after considering their points, explain what specific evidence would change your mind.`;
94
+ }
95
+
96
+ return `You are GPT-5.5, providing an independent architectural perspective.
97
+ This is Round 1 of a dual-brain analysis — Claude (Opus) will independently analyze the same question,
98
+ then send you their perspective for a collaborative discussion in Round 2.
99
+
100
+ Question: ${question}
101
+ ${context ? `\nContext: ${context}` : ''}
102
+ ${files?.length ? `\nRelevant files: ${files.join(', ')}` : ''}
103
+
104
+ Provide your analysis in this structure:
105
+ 1. RECOMMENDATION: Your clear recommendation (1-2 sentences)
106
+ 2. RATIONALE: Why this is the best approach (3-5 points)
107
+ 3. ALTERNATIVES: What you considered and rejected
108
+ 4. RISKS: What could go wrong with your recommendation
109
+ 5. CONFIDENCE: low/medium/high and why
110
+ 6. VERIFICATION: How to validate this decision is correct`;
111
+ }
112
+
113
+ // ---------------------------------------------------------------------------
114
+ // Codex executor
115
+ // ---------------------------------------------------------------------------
116
+
117
+ function runGptAnalysis(codexBin, prompt) {
118
+ const startTime = Date.now();
119
+
120
+ const proc = spawnSync(codexBin, [
121
+ 'exec', '--json', '--ephemeral',
122
+ '-m', MODEL,
123
+ '-s', SANDBOX,
124
+ prompt,
125
+ ], {
126
+ encoding: 'utf8',
127
+ stdio: ['pipe', 'pipe', 'pipe'],
128
+ timeout: CODEX_TIMEOUT_MS,
129
+ });
130
+
131
+ const durationMs = Date.now() - startTime;
132
+
133
+ // Parse JSONL output
134
+ const messages = (proc.stdout || '')
135
+ .split('\n')
136
+ .filter(l => l.trim())
137
+ .map(l => { try { return JSON.parse(l); } catch { return null; } })
138
+ .filter(Boolean);
139
+
140
+ const agentMessages = messages
141
+ .filter(m => m.type === 'item.completed' && m.item?.type === 'agent_message')
142
+ .map(m => m.item.text);
143
+
144
+ const usage = messages.find(m => m.type === 'turn.completed')?.usage ?? null;
145
+ const errors = messages.filter(m => m.type === 'error' || m.type === 'turn.failed');
146
+
147
+ if (agentMessages.length > 0) {
148
+ return {
149
+ success: true,
150
+ text: agentMessages.join('\n\n'),
151
+ durationMs,
152
+ usage,
153
+ };
154
+ }
155
+
156
+ if (errors.length > 0) {
157
+ return {
158
+ success: false,
159
+ error: errors[0].message || errors[0].error?.message || 'unknown codex error',
160
+ durationMs,
161
+ usage: null,
162
+ };
163
+ }
164
+
165
+ return {
166
+ success: false,
167
+ error: 'No agent messages returned from Codex',
168
+ durationMs,
169
+ usage: null,
170
+ };
171
+ }
172
+
173
+ // ---------------------------------------------------------------------------
174
+ // Usage logger — matches schema_version: 2 used across the orchestrator
175
+ // ---------------------------------------------------------------------------
176
+
177
+ function logUsage({ durationMs, usage, success }) {
178
+ const logFile = join(__dirname, `usage-${new Date().toISOString().slice(0, 10)}.jsonl`);
179
+ const entry = JSON.stringify({
180
+ schema_version: 2,
181
+ timestamp: new Date().toISOString(),
182
+ provider: 'openai',
183
+ tier: 'think',
184
+ tool: 'dual-brain-think',
185
+ model: MODEL,
186
+ dispatcher: 'dual-brain-think',
187
+ status: success ? 'ok' : 'error',
188
+ durationMs: durationMs ?? null,
189
+ input_tokens: usage?.input_tokens ?? null,
190
+ output_tokens: usage?.output_tokens ?? null,
191
+ session_id: process.env.CLAUDE_SESSION_ID || null,
192
+ });
193
+ try {
194
+ appendFileSync(logFile, entry + '\n');
195
+ } catch {}
196
+ }
197
+
198
+ // ---------------------------------------------------------------------------
199
+ // Core exported function
200
+ // ---------------------------------------------------------------------------
201
+
202
+ export async function dualThink({ question, context, files, round, claudePerspective } = {}) {
203
+ if (!question) {
204
+ return {
205
+ gpt: null,
206
+ error: 'No question provided',
207
+ fallback: 'Proceed with single-brain analysis on Claude Opus',
208
+ };
209
+ }
210
+
211
+ const effectiveRound = (round === 2 && claudePerspective) ? 2 : 1;
212
+
213
+ const codexBin = findCodex();
214
+ if (!codexBin) {
215
+ return {
216
+ gpt: null,
217
+ error: 'Codex CLI not available',
218
+ fallback: 'Proceed with single-brain analysis on Claude Opus',
219
+ };
220
+ }
221
+
222
+ const login = spawnSync(codexBin, ['login', 'status'], {
223
+ encoding: 'utf8',
224
+ stdio: ['pipe', 'pipe', 'pipe'],
225
+ timeout: 5000,
226
+ });
227
+ if (!isCodexAuthenticated(login)) {
228
+ return {
229
+ gpt: null,
230
+ error: 'Codex CLI not authenticated — run `codex login`',
231
+ fallback: 'Proceed with single-brain analysis on Claude Opus',
232
+ };
233
+ }
234
+
235
+ const prompt = buildGptPrompt({ question, context, files, round: effectiveRound, claudePerspective });
236
+ const raw = runGptAnalysis(codexBin, prompt);
237
+
238
+ logUsage({ durationMs: raw.durationMs, usage: raw.usage, success: raw.success });
239
+
240
+ if (!raw.success) {
241
+ return {
242
+ gpt: null,
243
+ error: raw.error || 'GPT analysis failed',
244
+ fallback: effectiveRound === 2
245
+ ? 'GPT rebuttal unavailable — synthesize from Round 1 analysis alone'
246
+ : 'Proceed with single-brain analysis on Claude Opus',
247
+ };
248
+ }
249
+
250
+ if (effectiveRound === 2) {
251
+ return {
252
+ round: 2,
253
+ gpt: {
254
+ rebuttal: raw.text,
255
+ model: MODEL,
256
+ durationMs: raw.durationMs,
257
+ tokens: raw.usage,
258
+ },
259
+ instructions: `GPT has responded to your analysis. Now synthesize both rounds into a FINAL DECISION:
260
+ 1. Where you both agree → high confidence, proceed
261
+ 2. Where GPT pushed back on your points → re-evaluate honestly
262
+ 3. Where you still disagree → state why and what evidence would resolve it
263
+ 4. Final recommendation with combined confidence level`,
264
+ question,
265
+ };
266
+ }
267
+
268
+ return {
269
+ round: 1,
270
+ gpt: {
271
+ recommendation: raw.text,
272
+ model: MODEL,
273
+ durationMs: raw.durationMs,
274
+ tokens: raw.usage,
275
+ },
276
+ instructions: `Round 1 complete. Now:
277
+ 1. Provide YOUR independent analysis of the same question (same structure: recommendation, rationale, alternatives, risks, confidence, verification)
278
+ 2. Then call Round 2 to send your perspective back to GPT:
279
+ node .claude/hooks/dual-brain-think.mjs --question "<same question>" --round 2 --claude-says "<your analysis summary>"
280
+ 3. GPT will respond to your specific points — agreements, pushback, and refined recommendation
281
+ 4. You then synthesize both rounds into the final decision`,
282
+ question,
283
+ context: context || null,
284
+ };
285
+ }
286
+
287
+ // ---------------------------------------------------------------------------
288
+ // CLI argument parser
289
+ // ---------------------------------------------------------------------------
290
+
291
+ function parseArgs(argv) {
292
+ const args = {};
293
+ let i = 0;
294
+ while (i < argv.length) {
295
+ const arg = argv[i];
296
+ if (arg.startsWith('--')) {
297
+ const eqIdx = arg.indexOf('=');
298
+ if (eqIdx !== -1) {
299
+ args[arg.slice(2, eqIdx)] = arg.slice(eqIdx + 1);
300
+ } else {
301
+ const key = arg.slice(2);
302
+ const next = argv[i + 1];
303
+ if (next !== undefined && !next.startsWith('--')) {
304
+ args[key] = next;
305
+ i++;
306
+ } else {
307
+ args[key] = true;
308
+ }
309
+ }
310
+ }
311
+ i++;
312
+ }
313
+
314
+ // Normalize files to an array
315
+ if (typeof args.files === 'string') {
316
+ args.files = args.files.split(',').map(f => f.trim()).filter(Boolean);
317
+ }
318
+
319
+ return args;
320
+ }
321
+
322
+ // ---------------------------------------------------------------------------
323
+ // CLI output formatter
324
+ // ---------------------------------------------------------------------------
325
+
326
+ function printResult(result, question) {
327
+ const BAR = '╠══════════════════════════════════════════════════╣';
328
+ const TOP = '╔══════════════════════════════════════════════════╗';
329
+ const BOT = '╚══════════════════════════════════════════════════╝';
330
+
331
+ const roundLabel = result.round === 2 ? 'Round 2 — Rebuttal' : 'Round 1 — Initial';
332
+
333
+ console.log(TOP);
334
+ console.log(`║ 🧠 Dual-Brain Think · ${roundLabel}`.padEnd(51) + '║');
335
+ console.log(BAR);
336
+ const q = question.length > 44 ? question.slice(0, 41) + '...' : question;
337
+ console.log(`║ Question: ${q.padEnd(38)} ║`);
338
+ console.log(BAR);
339
+
340
+ if (!result.gpt) {
341
+ console.log(`║ ❌ ${(result.error || 'Unknown error').padEnd(45)} ║`);
342
+ console.log(BAR);
343
+ console.log(`║ ↩️ ${(result.fallback || '').padEnd(45)} ║`);
344
+ console.log(BOT);
345
+ return;
346
+ }
347
+
348
+ const gptData = result.gpt;
349
+ const durSec = (gptData.durationMs / 1000).toFixed(1);
350
+ console.log(`║ 🤖 GPT-5.5 (${durSec}s):`.padEnd(51) + '║');
351
+ console.log(BAR);
352
+ console.log('');
353
+ console.log(gptData.recommendation || gptData.rebuttal);
354
+ console.log('');
355
+ console.log(BAR);
356
+
357
+ if (result.round === 2) {
358
+ console.log('║ 🔄 Synthesize both rounds into final decision. ║');
359
+ console.log('║ Where you agree → high confidence. ║');
360
+ console.log('║ Where you disagree → state what would resolve it.║');
361
+ } else {
362
+ console.log('║ 📝 Your turn: analyze independently, then call ║');
363
+ console.log('║ Round 2 with --round 2 --claude-says "..." ║');
364
+ console.log('║ for GPT\'s rebuttal to your analysis. ║');
365
+ }
366
+ console.log(BOT);
367
+ }
368
+
369
+ // ---------------------------------------------------------------------------
370
+ // CLI entry point
371
+ // ---------------------------------------------------------------------------
372
+
373
+ if (import.meta.url === `file://${process.argv[1]}`) {
374
+ const args = parseArgs(process.argv.slice(2));
375
+
376
+ if (!args.question) {
377
+ console.error(
378
+ 'Usage: node dual-brain-think.mjs --question "<question>" [--context "<ctx>"] [--files f1,f2]\n' +
379
+ ' node dual-brain-think.mjs --question "<question>" --round 2 --claude-says "<analysis>"'
380
+ );
381
+ process.exit(1);
382
+ }
383
+
384
+ const result = await dualThink({
385
+ question: args.question,
386
+ context: args.context,
387
+ files: args.files,
388
+ round: args.round ? parseInt(args.round, 10) : 1,
389
+ claudePerspective: args['claude-says'] || null,
390
+ });
391
+
392
+ printResult(result, args.question);
393
+ }