dual-brain 0.1.16 → 0.1.18
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/bin/dual-brain.mjs +197 -9
- package/package.json +1 -1
- package/src/detect.mjs +1 -0
package/bin/dual-brain.mjs
CHANGED
|
@@ -36,6 +36,24 @@ import { loadSession, saveSession, formatSessionCard, importReplitSessions, getS
|
|
|
36
36
|
|
|
37
37
|
import { box, bar, badge, menu, separator } from '../src/tui.mjs';
|
|
38
38
|
|
|
39
|
+
// ─── Dynamic imports for receipts + failure memory ───────────────────────────
|
|
40
|
+
|
|
41
|
+
let _receipt = null;
|
|
42
|
+
async function getReceipt() {
|
|
43
|
+
if (!_receipt) {
|
|
44
|
+
try { _receipt = await import('../src/receipt.mjs'); } catch { _receipt = {}; }
|
|
45
|
+
}
|
|
46
|
+
return _receipt;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
let _failureMem = null;
|
|
50
|
+
async function getFailureMem() {
|
|
51
|
+
if (!_failureMem) {
|
|
52
|
+
try { _failureMem = await import('../src/failure-memory.mjs'); } catch { _failureMem = {}; }
|
|
53
|
+
}
|
|
54
|
+
return _failureMem;
|
|
55
|
+
}
|
|
56
|
+
|
|
39
57
|
// ─── Helpers ─────────────────────────────────────────────────────────────────
|
|
40
58
|
|
|
41
59
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
@@ -158,20 +176,26 @@ function printHelp() {
|
|
|
158
176
|
dual-brain <command> [options]
|
|
159
177
|
|
|
160
178
|
Commands:
|
|
179
|
+
plan "task" Scope and plan without executing (dry-run)
|
|
180
|
+
do "task" Implement — detect, route, execute, verify
|
|
181
|
+
review Challenge current changes with dual-brain
|
|
182
|
+
ship Test, commit, and prepare to ship
|
|
183
|
+
|
|
161
184
|
init First-time setup → flows into interactive REPL
|
|
162
185
|
auth Show provider login and plan status
|
|
163
186
|
install Install Claude Code hooks into the current project
|
|
164
|
-
go "task description" Detect → decide → dispatch
|
|
187
|
+
go "task description" Detect → decide → dispatch (alias for do)
|
|
165
188
|
--dry-run Show routing decision without executing
|
|
166
189
|
--files a.mjs,b.mjs Provide file context for risk classification
|
|
167
190
|
--verbose, -v Print routing trace (intent, risk, health, model selection)
|
|
191
|
+
think "question" Multi-round architecture decision with dual-brain
|
|
168
192
|
status Provider health, session stats, available models
|
|
169
193
|
--verbose, -v Also print profile file path and raw profile object
|
|
170
194
|
hot <provider> Manually mark all model classes for provider as hot
|
|
171
195
|
cool <provider> Manually clear hot state for a provider
|
|
172
196
|
remember "preference" Save a project-scoped preference
|
|
173
197
|
forget "preference" Remove a preference by fuzzy match
|
|
174
|
-
search "keyword"
|
|
198
|
+
search "keyword" Search across all sessions
|
|
175
199
|
specialists List available specialist agents with descriptions
|
|
176
200
|
python "task" Force Python specialist for the task
|
|
177
201
|
typescript "task" Force TypeScript specialist for the task
|
|
@@ -316,8 +340,8 @@ async function cmdAuth(subArgs = []) {
|
|
|
316
340
|
}
|
|
317
341
|
}
|
|
318
342
|
|
|
319
|
-
async function cmdGo(args) {
|
|
320
|
-
const dryRun = args.includes('--dry-run');
|
|
343
|
+
async function cmdGo(args, opts = {}) {
|
|
344
|
+
const dryRun = opts.dryRun || args.includes('--dry-run');
|
|
321
345
|
const verbose = args.includes('--verbose') || args.includes('-v');
|
|
322
346
|
const filesRaw = flag(args, '--files');
|
|
323
347
|
const files = filesRaw && typeof filesRaw === 'string'
|
|
@@ -333,6 +357,17 @@ async function cmdGo(args) {
|
|
|
333
357
|
|
|
334
358
|
if (verbose) console.log('\nDispatching...');
|
|
335
359
|
|
|
360
|
+
// ── Failure memory: check history before dispatching ──────────────────────
|
|
361
|
+
const failureMem = await getFailureMem();
|
|
362
|
+
if (failureMem.checkFailureHistory && failureMem.formatEscalation) {
|
|
363
|
+
try {
|
|
364
|
+
const failureHistory = await failureMem.checkFailureHistory(prompt, files, cwd);
|
|
365
|
+
if (failureHistory?.escalation?.recommended) {
|
|
366
|
+
console.log(failureMem.formatEscalation(failureHistory.escalation));
|
|
367
|
+
}
|
|
368
|
+
} catch { /* non-fatal */ }
|
|
369
|
+
}
|
|
370
|
+
|
|
336
371
|
const { plan, result } = await runPipeline('go', prompt, {
|
|
337
372
|
files,
|
|
338
373
|
cwd,
|
|
@@ -353,6 +388,16 @@ async function cmdGo(args) {
|
|
|
353
388
|
console.log(`\nConsensus: ${result.consensus}`);
|
|
354
389
|
if (result.claude?.summary) console.log(`Claude : ${result.claude.summary}`);
|
|
355
390
|
if (result.openai?.summary) console.log(`OpenAI : ${result.openai.summary}`);
|
|
391
|
+
|
|
392
|
+
// Receipt
|
|
393
|
+
const receipt = await getReceipt();
|
|
394
|
+
if (receipt.buildReceipt && receipt.formatReceipt) {
|
|
395
|
+
try {
|
|
396
|
+
const r = receipt.buildReceipt(result, plan, null);
|
|
397
|
+
console.log(receipt.formatReceipt(r));
|
|
398
|
+
} catch { /* non-fatal */ }
|
|
399
|
+
}
|
|
400
|
+
|
|
356
401
|
saveSession({
|
|
357
402
|
objective: prompt,
|
|
358
403
|
branch: null,
|
|
@@ -362,6 +407,12 @@ async function cmdGo(args) {
|
|
|
362
407
|
provider: plan?._decision?.provider ?? 'claude',
|
|
363
408
|
nextAction: null,
|
|
364
409
|
}, cwd);
|
|
410
|
+
|
|
411
|
+
// Clear failure memory on success
|
|
412
|
+
if (failureMem.clearFailures) {
|
|
413
|
+
try { await failureMem.clearFailures(prompt, cwd); } catch { /* non-fatal */ }
|
|
414
|
+
}
|
|
415
|
+
|
|
365
416
|
// ── Next steps suggestions (dual-brain consensus path) ──────────────────
|
|
366
417
|
try {
|
|
367
418
|
const { suggestNextSteps, formatNextSteps } = await import('../src/nextstep.mjs');
|
|
@@ -375,23 +426,52 @@ async function cmdGo(args) {
|
|
|
375
426
|
}
|
|
376
427
|
} catch { /* non-fatal */ }
|
|
377
428
|
} else {
|
|
378
|
-
const
|
|
429
|
+
const succeeded = result.status === 'completed';
|
|
430
|
+
const statusLine = succeeded ? 'Done' : `Failed (exit ${result.exitCode})`;
|
|
379
431
|
console.log(`\n${statusLine}${result.durationMs != null ? ` in ${(result.durationMs / 1000).toFixed(1)}s` : ''}`);
|
|
380
432
|
if (result.summary) console.log(result.summary);
|
|
381
433
|
if (result.error) process.stderr.write(`${result.error}\n`);
|
|
434
|
+
|
|
435
|
+
// Receipt
|
|
436
|
+
const receipt = await getReceipt();
|
|
437
|
+
if (succeeded && receipt.buildReceipt && receipt.formatReceipt) {
|
|
438
|
+
try {
|
|
439
|
+
const r = receipt.buildReceipt(result, plan, null);
|
|
440
|
+
console.log(receipt.formatReceipt(r));
|
|
441
|
+
} catch { /* non-fatal */ }
|
|
442
|
+
} else if (!succeeded && receipt.buildReceipt && receipt.formatFailureReceipt) {
|
|
443
|
+
try {
|
|
444
|
+
const r = receipt.buildReceipt(result, plan, null);
|
|
445
|
+
console.log(receipt.formatFailureReceipt(r, { error: result.error }));
|
|
446
|
+
} catch { /* non-fatal */ }
|
|
447
|
+
}
|
|
448
|
+
|
|
382
449
|
saveSession({
|
|
383
450
|
objective: prompt,
|
|
384
451
|
branch: null,
|
|
385
452
|
filesChanged: files,
|
|
386
453
|
commandsRun: [`dual-brain go "${prompt}"`],
|
|
387
454
|
lastResult: {
|
|
388
|
-
status:
|
|
389
|
-
summary: result.summary || (
|
|
455
|
+
status: succeeded ? 'success' : 'failure',
|
|
456
|
+
summary: result.summary || (succeeded ? 'completed' : `exit ${result.exitCode}`),
|
|
390
457
|
},
|
|
391
458
|
provider: plan?._decision?.provider ?? 'claude',
|
|
392
459
|
nextAction: null,
|
|
393
460
|
}, cwd);
|
|
394
|
-
|
|
461
|
+
|
|
462
|
+
if (!succeeded) {
|
|
463
|
+
// Record failure memory
|
|
464
|
+
if (failureMem.recordFailure) {
|
|
465
|
+
try { await failureMem.recordFailure(prompt, plan, result.error, cwd); } catch { /* non-fatal */ }
|
|
466
|
+
}
|
|
467
|
+
process.exit(1);
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
// Clear failure memory on success
|
|
471
|
+
if (failureMem.clearFailures) {
|
|
472
|
+
try { await failureMem.clearFailures(prompt, cwd); } catch { /* non-fatal */ }
|
|
473
|
+
}
|
|
474
|
+
|
|
395
475
|
await offerAutoCommit(cwd);
|
|
396
476
|
// ── Next steps suggestions ──────────────────────────────────────────────
|
|
397
477
|
try {
|
|
@@ -473,6 +553,111 @@ async function cmdReview(_args) {
|
|
|
473
553
|
}
|
|
474
554
|
}
|
|
475
555
|
|
|
556
|
+
async function cmdShip() {
|
|
557
|
+
const cwd = process.cwd();
|
|
558
|
+
|
|
559
|
+
console.log('\n── ship: finalizing ──────────────────────────────────────\n');
|
|
560
|
+
|
|
561
|
+
// 1. Check for test script
|
|
562
|
+
let hasTests = false;
|
|
563
|
+
let testScript = null;
|
|
564
|
+
try {
|
|
565
|
+
const pkg = JSON.parse(readFileSync(join(cwd, 'package.json'), 'utf8'));
|
|
566
|
+
testScript = pkg?.scripts?.test;
|
|
567
|
+
hasTests = Boolean(testScript && !testScript.includes('echo'));
|
|
568
|
+
} catch { /* no package.json */ }
|
|
569
|
+
|
|
570
|
+
// 2. Run tests if available
|
|
571
|
+
let testsPassed = null;
|
|
572
|
+
if (hasTests) {
|
|
573
|
+
console.log('Running tests...\n');
|
|
574
|
+
const testResult = _spawnSyncTop('npm', ['test'], {
|
|
575
|
+
cwd,
|
|
576
|
+
encoding: 'utf8',
|
|
577
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
578
|
+
timeout: 60000,
|
|
579
|
+
});
|
|
580
|
+
const testOut = (testResult.stdout || '') + (testResult.stderr || '');
|
|
581
|
+
if (testOut) console.log(testOut.slice(0, 3000));
|
|
582
|
+
testsPassed = testResult.status === 0;
|
|
583
|
+
console.log(testsPassed ? 'Tests: PASS' : 'Tests: FAIL');
|
|
584
|
+
} else {
|
|
585
|
+
console.log('(no test script found in package.json — skipping tests)');
|
|
586
|
+
testsPassed = null;
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
// 3. Git status
|
|
590
|
+
let changedFiles = [];
|
|
591
|
+
let currentBranch = 'unknown';
|
|
592
|
+
try {
|
|
593
|
+
const statusResult = _spawnSyncTop('git', ['status', '--porcelain'], {
|
|
594
|
+
cwd, encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'], timeout: 5000,
|
|
595
|
+
});
|
|
596
|
+
changedFiles = (statusResult.stdout || '').trim().split('\n').filter(Boolean);
|
|
597
|
+
const branchResult = _spawnSyncTop('git', ['rev-parse', '--abbrev-ref', 'HEAD'], {
|
|
598
|
+
cwd, encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'], timeout: 3000,
|
|
599
|
+
});
|
|
600
|
+
currentBranch = (branchResult.stdout || '').trim() || 'unknown';
|
|
601
|
+
} catch { /* non-fatal */ }
|
|
602
|
+
|
|
603
|
+
if (changedFiles.length > 0) {
|
|
604
|
+
console.log('\nChanged files:');
|
|
605
|
+
changedFiles.slice(0, 20).forEach(f => console.log(` ${f}`));
|
|
606
|
+
if (changedFiles.length > 20) console.log(` ... and ${changedFiles.length - 20} more`);
|
|
607
|
+
} else {
|
|
608
|
+
console.log('\nNo uncommitted changes.');
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
// 4. Generate commit message suggestion
|
|
612
|
+
let commitMsg = null;
|
|
613
|
+
try {
|
|
614
|
+
const diffResult = _spawnSyncTop('git', ['diff', '--name-only', 'HEAD'], {
|
|
615
|
+
cwd, encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'], timeout: 5000,
|
|
616
|
+
});
|
|
617
|
+
const diffFiles = (diffResult.stdout || '').trim().split('\n').filter(Boolean);
|
|
618
|
+
if (diffFiles.length > 0) {
|
|
619
|
+
const fileList = diffFiles.slice(0, 5).map(f => basename(f)).join(', ');
|
|
620
|
+
const suffix = diffFiles.length > 5 ? ` and ${diffFiles.length - 5} more` : '';
|
|
621
|
+
commitMsg = `update ${fileList}${suffix}`;
|
|
622
|
+
}
|
|
623
|
+
} catch { /* non-fatal */ }
|
|
624
|
+
|
|
625
|
+
// 5. Show suggested actions
|
|
626
|
+
console.log('\n── suggested actions ─────────────────────────────────────\n');
|
|
627
|
+
|
|
628
|
+
if (testsPassed === false) {
|
|
629
|
+
console.log(' ⚠ Tests failed — fix before committing.');
|
|
630
|
+
} else {
|
|
631
|
+
if (changedFiles.length > 0 && commitMsg) {
|
|
632
|
+
console.log(` Commit: git add -p && git commit -m "${commitMsg}"`);
|
|
633
|
+
} else if (changedFiles.length === 0) {
|
|
634
|
+
console.log(' Nothing to commit — working tree clean.');
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
const isMain = currentBranch === 'main' || currentBranch === 'master';
|
|
638
|
+
if (!isMain && currentBranch !== 'unknown') {
|
|
639
|
+
console.log(` PR: gh pr create --title "${commitMsg || 'update'}" --body "..."`);}
|
|
640
|
+
else if (isMain) {
|
|
641
|
+
console.log(' (on main — consider branching before PR)');
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
// 6. Receipt
|
|
646
|
+
const receipt = await getReceipt();
|
|
647
|
+
if (receipt.buildReceiptFromOutcome && receipt.formatReceipt) {
|
|
648
|
+
try {
|
|
649
|
+
const r = receipt.buildReceiptFromOutcome({
|
|
650
|
+
command: 'ship',
|
|
651
|
+
branch: currentBranch,
|
|
652
|
+
filesChanged: changedFiles.length,
|
|
653
|
+
testsPassed,
|
|
654
|
+
});
|
|
655
|
+
console.log('\n' + receipt.formatReceipt(r));
|
|
656
|
+
} catch { /* non-fatal */ }
|
|
657
|
+
} else {
|
|
658
|
+
console.log(`\nReceipt: branch=${currentBranch} files=${changedFiles.length} tests=${testsPassed === null ? 'skipped' : testsPassed ? 'pass' : 'fail'}`);
|
|
659
|
+
}
|
|
660
|
+
}
|
|
476
661
|
|
|
477
662
|
async function cmdStatus(args = []) {
|
|
478
663
|
const verbose = args.includes('--verbose') || args.includes('-v');
|
|
@@ -4443,9 +4628,12 @@ async function main() {
|
|
|
4443
4628
|
await cmdAuth(args.slice(1));
|
|
4444
4629
|
return;
|
|
4445
4630
|
}
|
|
4631
|
+
if (cmd === 'plan') { await cmdGo(args.slice(1), { dryRun: true }); return; }
|
|
4632
|
+
if (cmd === 'do') { await cmdGo(args.slice(1)); return; }
|
|
4446
4633
|
if (cmd === 'go') { await cmdGo(args.slice(1)); return; }
|
|
4447
4634
|
if (cmd === 'think') { await cmdThink(args.slice(1)); return; }
|
|
4448
4635
|
if (cmd === 'review') { await cmdReview(args.slice(1)); return; }
|
|
4636
|
+
if (cmd === 'ship') { await cmdShip(); return; }
|
|
4449
4637
|
if (cmd === 'status') { await cmdStatus(args.slice(1)); return; }
|
|
4450
4638
|
if (cmd === 'hot') { cmdHot(args[1]); return; }
|
|
4451
4639
|
if (cmd === 'cool') { cmdCool(args[1]); return; }
|
|
@@ -4510,7 +4698,7 @@ fi
|
|
|
4510
4698
|
// If cmd is not a recognized subcommand, treat the entire arg list as a task.
|
|
4511
4699
|
// e.g. `dual-brain fix failing tests` → same as `dual-brain go "fix failing tests"`
|
|
4512
4700
|
const KNOWN_COMMANDS = new Set([
|
|
4513
|
-
'init', 'install', 'auth', 'go', 'status', 'hot', 'cool',
|
|
4701
|
+
'init', 'install', 'auth', 'go', 'do', 'plan', 'ship', 'think', 'review', 'status', 'hot', 'cool',
|
|
4514
4702
|
'remember', 'forget', 'break-glass', 'specialists', 'search', 'shell-hook', 'watch',
|
|
4515
4703
|
'--help', '-h', '--version', '-v',
|
|
4516
4704
|
...Object.keys(loadSpecialistRegistry()),
|
package/package.json
CHANGED
package/src/detect.mjs
CHANGED
|
@@ -369,6 +369,7 @@ const DEFAULT_SPECIALISTS = {
|
|
|
369
369
|
html: { triggers: { extensions: ['.html', '.css', '.scss', '.svg'], keywords: ['html', 'css', 'accessibility', 'a11y', 'aria', 'responsive', 'tailwind'] } },
|
|
370
370
|
linux: { triggers: { extensions: ['.sh', '.bash', '.conf', '.service', '.dockerfile'], keywords: ['linux', 'bash', 'shell', 'systemd', 'nginx', 'docker', 'ssh', 'deploy'] } },
|
|
371
371
|
security: { triggers: { extensions: [], keywords: ['auth', 'oauth', 'jwt', 'credential', 'secret', 'encrypt', 'vulnerability', 'vulnerabilities', 'audit', 'owasp', 'xss', 'csrf'] }, tier_bias: 'think' },
|
|
372
|
+
doctor: { triggers: { extensions: [], keywords: ['doctor', 'health', 'diagnose', 'diagnosis', 'checkup', 'drift', 'completeness', 'broken', 'regression', 'audit health', 'package health', 'health check', 'health report', 'health-manifest'] }, tier_bias: 'think' },
|
|
372
373
|
};
|
|
373
374
|
|
|
374
375
|
function loadSpecialistRegistry() {
|