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.
@@ -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 a task
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" Search across all sessions
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 statusLine = result.status === 'completed' ? 'Done' : `Failed (exit ${result.exitCode})`;
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: result.status === 'completed' ? 'success' : 'failure',
389
- summary: result.summary || (result.status === 'completed' ? 'completed' : `exit ${result.exitCode}`),
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
- if (result.status !== 'completed') process.exit(1);
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dual-brain",
3
- "version": "0.1.16",
3
+ "version": "0.1.18",
4
4
  "description": "AI orchestration across Claude + OpenAI subscriptions — smart routing, budget awareness, and dual-brain collaboration",
5
5
  "type": "module",
6
6
  "bin": {
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() {