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.
- package/CLAUDE.md +130 -35
- package/README.md +171 -44
- package/hooks/agent-chains.mjs +369 -0
- package/hooks/agent-templates.mjs +441 -0
- package/hooks/atomic-write.mjs +5 -3
- package/hooks/config-validator.mjs +156 -0
- package/hooks/confirmation-policy.mjs +167 -0
- package/hooks/cost-logger.mjs +32 -12
- package/hooks/cost-report.mjs +60 -114
- package/hooks/decision-ledger.mjs +3 -2
- package/hooks/dual-brain-review.mjs +249 -2
- package/hooks/dual-brain-think.mjs +294 -25
- package/hooks/enforce-tier.mjs +246 -87
- package/hooks/error-channel.mjs +68 -0
- package/hooks/failure-detector.mjs +2 -1
- package/hooks/health-check.mjs +16 -17
- package/hooks/risk-classifier.mjs +135 -2
- package/hooks/session-report.mjs +41 -71
- package/hooks/ship-captain.mjs +1176 -0
- package/hooks/ship-gate.mjs +971 -0
- package/hooks/summary-checkpoint.mjs +31 -4
- package/hooks/test-orchestrator.mjs +1975 -11
- package/install.mjs +1064 -31
- package/orchestrator.json +73 -96
- package/package.json +7 -2
|
@@ -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
|
-
*
|
|
12
|
-
*
|
|
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
|
}
|