dual-brain 7.1.15 → 7.1.17

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.
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  // dual-brain — CLI entry point. Commands: init, go, status, remember, forget
3
3
 
4
- import { existsSync, readFileSync, mkdirSync, writeFileSync, statSync, readdirSync, unlinkSync } from 'node:fs';
4
+ import { appendFileSync, existsSync, readFileSync, mkdirSync, writeFileSync, statSync, readdirSync, unlinkSync } from 'node:fs';
5
5
  import { join, dirname } from 'node:path';
6
6
  import { fileURLToPath } from 'node:url';
7
7
  import { execSync, spawnSync as _spawnSyncTop } from 'node:child_process';
@@ -169,6 +169,14 @@ Commands:
169
169
  remember "preference" Save a project-scoped preference
170
170
  forget "preference" Remove a preference by fuzzy match
171
171
  search "keyword" Search across all sessions
172
+ specialists List available specialist agents with descriptions
173
+ python "task" Force Python specialist for the task
174
+ typescript "task" Force TypeScript specialist for the task
175
+ html "task" Force HTML/CSS specialist for the task
176
+ linux "task" Force Linux/DevOps specialist for the task
177
+ security "task" Force Security specialist for the task
178
+ --dry-run (specialist commands) Show routing without executing
179
+ --files a,b (specialist commands) Provide file context
172
180
  shell-hook Output bash snippet to add dual-brain to your shell
173
181
  Usage: dual-brain shell-hook >> ~/.bashrc
174
182
 
@@ -391,27 +399,6 @@ async function cmdGo(args) {
391
399
  }, cwd);
392
400
  } else {
393
401
  result = await dispatch({ decision, prompt, files, cwd });
394
- if (result.status === 'completed' && result.type === 'native-agent') {
395
- const nd = result.nativeDispatch || {};
396
- const promptPreview = (nd.prompt || prompt).slice(0, 100);
397
- const promptSuffix = (nd.prompt || prompt).length > 100 ? '...' : '';
398
- console.log(`\nRouted: ${decision.provider}/${nd.model || decision.model} (${decision.tier})`);
399
- console.log('To dispatch, use the Agent tool with:');
400
- console.log(` model: ${nd.model || decision.model}`);
401
- console.log(` prompt: ${promptPreview}${promptSuffix}`);
402
- if (nd.isolation) console.log(` isolation: ${nd.isolation}`);
403
- if (nd.maxTurns) console.log(` maxTurns: ${nd.maxTurns}`);
404
- saveSession({
405
- objective: prompt,
406
- branch: null,
407
- filesChanged: files,
408
- commandsRun: [`dual-brain go "${prompt}"`],
409
- lastResult: { status: 'success', summary: `native-agent routed to ${nd.model || decision.model}` },
410
- provider: decision.provider,
411
- nextAction: null,
412
- }, cwd);
413
- return;
414
- }
415
402
  const statusLine = result.status === 'completed' ? 'Done' : `Failed (exit ${result.exitCode})`;
416
403
  console.log(`\n${statusLine} in ${(result.durationMs / 1000).toFixed(1)}s`);
417
404
  if (result.summary) console.log(result.summary);
@@ -632,6 +619,53 @@ function cmdForget(text) {
632
619
  console.log('Preference removed (if matched).');
633
620
  }
634
621
 
622
+ function cmdBreakGlass(reason) {
623
+ if (!reason) err('Usage: dual-brain break-glass "reason"');
624
+ const cwd = process.cwd();
625
+ const dualbrain = join(cwd, '.dualbrain');
626
+ const tokenPath = join(dualbrain, 'break-glass.json');
627
+ const auditDir = join(dualbrain, 'audit');
628
+ const auditFile = join(auditDir, 'head-audit.jsonl');
629
+ const TTL_MINUTES = 5;
630
+
631
+ mkdirSync(dualbrain, { recursive: true });
632
+ mkdirSync(auditDir, { recursive: true });
633
+
634
+ const token = {
635
+ createdAt: Date.now(),
636
+ ttlMinutes: TTL_MINUTES,
637
+ reason,
638
+ };
639
+ writeFileSync(tokenPath, JSON.stringify(token, null, 2));
640
+
641
+ // Audit entry
642
+ const auditEntry = {
643
+ ts: new Date().toISOString(),
644
+ event: 'break-glass-activated',
645
+ reason,
646
+ ttlMinutes: TTL_MINUTES,
647
+ expiresAt: new Date(token.createdAt + TTL_MINUTES * 60 * 1000).toISOString(),
648
+ };
649
+ try {
650
+ appendFileSync(auditFile, JSON.stringify(auditEntry) + '\n');
651
+ } catch { /* non-fatal */ }
652
+
653
+ const width = 51;
654
+ const inner = width - 2;
655
+ const pad = (s) => ' ' + s + ' '.repeat(inner - 1 - s.length);
656
+ const reasonLine = `Reason: ${reason}`;
657
+ const expiresLine = `Expires: ${TTL_MINUTES} minutes`;
658
+ const auditLine = 'All tool calls logged to audit.';
659
+
660
+ console.log('┌' + '─'.repeat(inner) + '┐');
661
+ console.log('│' + pad('🔓 Break-Glass Activated') + '│');
662
+ console.log('├' + '─'.repeat(inner) + '┤');
663
+ console.log('│' + pad(reasonLine) + '│');
664
+ console.log('│' + pad(expiresLine) + '│');
665
+ console.log('│' + pad(auditLine) + '│');
666
+ console.log('└' + '─'.repeat(inner) + '┘');
667
+ }
668
+
635
669
  // ─── Screen helpers ───────────────────────────────────────────────────────────
636
670
 
637
671
  /**
@@ -1603,6 +1637,282 @@ async function subscriptionsScreen(rl, ask) {
1603
1637
  return { next: 'main' };
1604
1638
  }
1605
1639
 
1640
+ // ─── Onboarding Wizard ───────────────────────────────────────────────────────
1641
+
1642
+ /**
1643
+ * 5-step onboarding wizard shown on first run (no .dualbrain/profile.json).
1644
+ * Matches the rounded ┌─┐ box style used in mainScreen / renderHeader.
1645
+ * @param {{ auth, plans, existingSessions }} detection
1646
+ * @param {string} cwd
1647
+ * @param {object} rl readline interface
1648
+ * @returns {object|null} profile object to save, or null if cancelled/skipped
1649
+ */
1650
+ async function runOnboardingWizard(detection, cwd, rl) {
1651
+ const ask = (q) => new Promise(res => rl.question(q, res));
1652
+ const version = readVersion();
1653
+
1654
+ // ── Rounded box helpers (matching mainScreen style) ────────────────────────
1655
+ const W = 51;
1656
+ const wTop = ` ┌${'─'.repeat(W)}┐`;
1657
+ const wSep = ` ├${'─'.repeat(W)}┤`;
1658
+ const wBottom = ` └${'─'.repeat(W)}┘`;
1659
+ const wPad = (s) => {
1660
+ const plain = s.replace(/\x1b\[[0-9;]*m/g, '');
1661
+ let vlen = 0;
1662
+ for (const ch of plain) {
1663
+ const cp = ch.codePointAt(0);
1664
+ if (
1665
+ (cp >= 0x1f300 && cp <= 0x1faff) ||
1666
+ (cp >= 0x2600 && cp <= 0x27bf) ||
1667
+ cp === 0xfe0f || cp === 0x20e3
1668
+ ) { vlen += 2; } else { vlen += 1; }
1669
+ }
1670
+ return s + ' '.repeat(Math.max(0, W - vlen));
1671
+ };
1672
+ const wRow = (s) => ` │ ${wPad(s)}│`;
1673
+
1674
+ // ── Collected wizard state ─────────────────────────────────────────────────
1675
+ const state = {
1676
+ claudePlan: null,
1677
+ openaiPlan: null,
1678
+ headModel: null,
1679
+ importSessions: false,
1680
+ profile: 'auto',
1681
+ };
1682
+
1683
+ const { auth, plans, existingSessions } = detection;
1684
+ const claudeReady = auth.claude.found;
1685
+ const openaiReady = auth.openai.found;
1686
+
1687
+ // ══════════════════════════════════════════════════════════════════════════
1688
+ // Step 1 — Welcome & provider detection
1689
+ // ══════════════════════════════════════════════════════════════════════════
1690
+ console.log('');
1691
+ console.log(wTop);
1692
+ console.log(wRow(`🧠 Dual-Brain v${version} — First-time Setup`));
1693
+ console.log(wSep);
1694
+ console.log(wRow(`Step 1 of 5: Detected providers`));
1695
+ console.log(wSep);
1696
+
1697
+ const claudePlanLabel = claudeReady
1698
+ ? (CLAUDE_PLAN_LABELS[plans.claude] ?? plans.claude ?? 'plan unknown')
1699
+ : null;
1700
+ const openaiPlanLabel = openaiReady
1701
+ ? (OPENAI_PLAN_LABELS[plans.openai] ?? plans.openai ?? 'plan unknown')
1702
+ : null;
1703
+
1704
+ console.log(wRow(claudeReady
1705
+ ? `✓ Claude CLI ${claudePlanLabel ? `(${claudePlanLabel})` : ''}`
1706
+ : `✗ Claude CLI not logged in`));
1707
+ console.log(wRow(openaiReady
1708
+ ? `✓ Codex CLI ${openaiPlanLabel ? `(${openaiPlanLabel})` : ''}`
1709
+ : `✗ Codex CLI not logged in`));
1710
+ if (existingSessions.length > 0) {
1711
+ console.log(wRow(`✓ ${existingSessions.length} data-tools session${existingSessions.length !== 1 ? 's' : ''} found`));
1712
+ }
1713
+ console.log(wSep);
1714
+ console.log(wRow(`[Enter] Continue setup [s] Skip wizard`));
1715
+ console.log(wBottom);
1716
+ console.log('');
1717
+
1718
+ if (!claudeReady && !openaiReady) {
1719
+ console.log(' No AI provider found. Log in first:');
1720
+ console.log(' claude auth login — for Claude');
1721
+ console.log(' codex login — for OpenAI/Codex');
1722
+ console.log(' Then re-run: dual-brain init\n');
1723
+ return null;
1724
+ }
1725
+
1726
+ const step1 = (await ask(' > ')).trim().toLowerCase();
1727
+ if (step1 === 's') {
1728
+ // Skip: auto-save detected plans and proceed directly
1729
+ const skippedProfile = loadProfile(cwd);
1730
+ if (claudeReady) skippedProfile.providers.claude = { enabled: true, plan: plans.claude || 'pro' };
1731
+ if (openaiReady) skippedProfile.providers.openai = { enabled: true, plan: plans.openai || 'plus' };
1732
+ const enabledCount = [claudeReady, openaiReady].filter(Boolean).length;
1733
+ skippedProfile.mode = enabledCount >= 2 ? 'auto' : claudeReady ? 'solo-claude' : 'solo-openai';
1734
+ return skippedProfile;
1735
+ }
1736
+
1737
+ // ══════════════════════════════════════════════════════════════════════════
1738
+ // Step 2 — Budget / plan selection
1739
+ // ══════════════════════════════════════════════════════════════════════════
1740
+ console.log('');
1741
+ console.log(wTop);
1742
+ console.log(wRow(`🧠 Dual-Brain v${version} — First-time Setup`));
1743
+ console.log(wSep);
1744
+ console.log(wRow(`Step 2 of 5: Subscription plans`));
1745
+ console.log(wSep);
1746
+
1747
+ if (claudeReady) {
1748
+ const detectedClaudePlan = plans.claude || 'pro';
1749
+ const detectedClaudeLabel = CLAUDE_PLAN_LABELS[detectedClaudePlan] ?? detectedClaudePlan;
1750
+ console.log(wRow(`Claude — detected: ${detectedClaudeLabel}`));
1751
+ console.log(wRow(` [1] Pro ($20/mo)`));
1752
+ console.log(wRow(` [2] Max x5 ($100/mo)`));
1753
+ console.log(wRow(` [3] Max x20 ($200/mo)`));
1754
+ console.log(wRow(` [Enter] Keep detected (${detectedClaudeLabel})`));
1755
+ console.log(wSep);
1756
+ const claudeChoice = (await ask(' Claude plan [1/2/3/Enter]: ')).trim();
1757
+ const claudePlanMap = { '1': 'pro', '2': 'max5', '3': 'max20' };
1758
+ state.claudePlan = claudePlanMap[claudeChoice] || detectedClaudePlan;
1759
+ }
1760
+
1761
+ if (openaiReady) {
1762
+ const detectedOpenaiPlan = plans.openai || 'plus';
1763
+ const detectedOpenaiLabel = OPENAI_PLAN_LABELS[detectedOpenaiPlan] ?? detectedOpenaiPlan;
1764
+ console.log(wRow(`OpenAI — detected: ${detectedOpenaiLabel}`));
1765
+ console.log(wRow(` [1] Plus ($20/mo)`));
1766
+ console.log(wRow(` [2] Pro ($100/mo)`));
1767
+ console.log(wRow(` [3] Pro ($200/mo higher limits)`));
1768
+ console.log(wRow(` [Enter] Keep detected (${detectedOpenaiLabel})`));
1769
+ console.log(wSep);
1770
+ const openaiChoice = (await ask(' OpenAI plan [1/2/3/Enter]: ')).trim();
1771
+ const openaiPlanMap = { '1': 'plus', '2': 'pro', '3': 'pro200' };
1772
+ state.openaiPlan = openaiPlanMap[openaiChoice] || detectedOpenaiPlan;
1773
+ }
1774
+
1775
+ console.log(wBottom);
1776
+
1777
+ // ══════════════════════════════════════════════════════════════════════════
1778
+ // Step 3 — HEAD model selection
1779
+ // ══════════════════════════════════════════════════════════════════════════
1780
+ const hasBigPlan = state.claudePlan === 'max5' || state.claudePlan === 'max20';
1781
+ const recommendedModel = hasBigPlan ? 'claude-opus-4-5' : 'claude-sonnet-4-5';
1782
+ const recommendedLabel = hasBigPlan
1783
+ ? 'Opus (Max plan — best quality)'
1784
+ : 'Sonnet (Pro plan — balanced speed/quality)';
1785
+
1786
+ console.log('');
1787
+ console.log(wTop);
1788
+ console.log(wRow(`🧠 Dual-Brain v${version} — First-time Setup`));
1789
+ console.log(wSep);
1790
+ console.log(wRow(`Step 3 of 5: HEAD model (think-tier)`));
1791
+ console.log(wSep);
1792
+ console.log(wRow(`Recommended: ${recommendedLabel}`));
1793
+ console.log(wSep);
1794
+ console.log(wRow(` [1] Haiku — fastest, lowest cost`));
1795
+ console.log(wRow(` [2] Sonnet — balanced (recommended for Pro)`));
1796
+ console.log(wRow(` [3] Opus — best quality (recommended for Max)`));
1797
+ console.log(wRow(` [Enter] Use recommended`));
1798
+ console.log(wBottom);
1799
+ console.log('');
1800
+
1801
+ const step3 = (await ask(' HEAD model [1/2/3/Enter]: ')).trim();
1802
+ const modelMap = {
1803
+ '1': 'claude-haiku-4-5',
1804
+ '2': 'claude-sonnet-4-5',
1805
+ '3': 'claude-opus-4-5',
1806
+ };
1807
+ state.headModel = modelMap[step3] || recommendedModel;
1808
+
1809
+ // ══════════════════════════════════════════════════════════════════════════
1810
+ // Step 4 — Import sessions + profile selection
1811
+ // ══════════════════════════════════════════════════════════════════════════
1812
+ console.log('');
1813
+ console.log(wTop);
1814
+ console.log(wRow(`🧠 Dual-Brain v${version} — First-time Setup`));
1815
+ console.log(wSep);
1816
+ console.log(wRow(`Step 4 of 5: Sessions & routing profile`));
1817
+ console.log(wSep);
1818
+
1819
+ if (existingSessions.length > 0) {
1820
+ console.log(wRow(`Import ${existingSessions.length} data-tools session${existingSessions.length !== 1 ? 's' : ''}?`));
1821
+ console.log(wRow(` [y] Yes [Enter/n] Skip`));
1822
+ console.log(wSep);
1823
+ const importChoice = (await ask(' Import sessions [y/Enter]: ')).trim().toLowerCase();
1824
+ state.importSessions = importChoice === 'y';
1825
+ if (state.importSessions) {
1826
+ console.log('');
1827
+ console.log(` Importing ${existingSessions.length} sessions...`);
1828
+ const recent = existingSessions.slice(0, 5);
1829
+ for (const sess of recent) {
1830
+ console.log(` ${sess.age.padEnd(6)} ${sess.name}`);
1831
+ }
1832
+ if (existingSessions.length > 5) {
1833
+ console.log(` ... and ${existingSessions.length - 5} more`);
1834
+ }
1835
+ }
1836
+ console.log(wSep);
1837
+ }
1838
+
1839
+ console.log(wRow(`Routing profile:`));
1840
+ console.log(wRow(` [1] auto — adapts based on task risk & outcomes`));
1841
+ console.log(wRow(` [2] balanced — best model per tier, normal budgets`));
1842
+ console.log(wRow(` [3] cost-saver — prefer cheaper models, skip GPT`));
1843
+ console.log(wRow(` [4] quality-first — dual-brain for medium+ risk`));
1844
+ console.log(wRow(` [Enter] auto (recommended)`));
1845
+ console.log(wBottom);
1846
+ console.log('');
1847
+
1848
+ const step4 = (await ask(' Profile [1/2/3/4/Enter]: ')).trim();
1849
+ const profileMap = { '1': 'auto', '2': 'balanced', '3': 'cost-saver', '4': 'quality-first' };
1850
+ state.profile = profileMap[step4] || 'auto';
1851
+
1852
+ // ══════════════════════════════════════════════════════════════════════════
1853
+ // Step 5 — Summary & confirm
1854
+ // ══════════════════════════════════════════════════════════════════════════
1855
+ const claudeSummary = state.claudePlan
1856
+ ? `Claude: ${CLAUDE_PLAN_LABELS[state.claudePlan] ?? state.claudePlan}`
1857
+ : `Claude: not configured`;
1858
+ const openaiSummary = state.openaiPlan
1859
+ ? `OpenAI: ${OPENAI_PLAN_LABELS[state.openaiPlan] ?? state.openaiPlan}`
1860
+ : `OpenAI: not configured`;
1861
+ const modelSummary = `HEAD model: ${state.headModel}`;
1862
+ const profileSummary = `Profile: ${state.profile}`;
1863
+ const sessionSummary = existingSessions.length > 0
1864
+ ? `Sessions: ${state.importSessions ? `${existingSessions.length} imported` : 'skipped'}`
1865
+ : null;
1866
+
1867
+ console.log('');
1868
+ console.log(wTop);
1869
+ console.log(wRow(`🧠 Dual-Brain v${version} — First-time Setup`));
1870
+ console.log(wSep);
1871
+ console.log(wRow(`Step 5 of 5: Summary`));
1872
+ console.log(wSep);
1873
+ console.log(wRow(claudeSummary));
1874
+ console.log(wRow(openaiSummary));
1875
+ console.log(wRow(modelSummary));
1876
+ console.log(wRow(profileSummary));
1877
+ if (sessionSummary) console.log(wRow(sessionSummary));
1878
+ console.log(wSep);
1879
+ console.log(wRow(`[Enter] Save and start [q] Quit without saving`));
1880
+ console.log(wBottom);
1881
+ console.log('');
1882
+
1883
+ const step5 = (await ask(' > ')).trim().toLowerCase();
1884
+ if (step5 === 'q') {
1885
+ console.log('\n Setup cancelled.\n');
1886
+ return null;
1887
+ }
1888
+
1889
+ // ── Build and return the profile object ────────────────────────────────────
1890
+ const finalProfile = loadProfile(cwd);
1891
+
1892
+ if (state.claudePlan) {
1893
+ finalProfile.providers.claude = { enabled: true, plan: state.claudePlan };
1894
+ } else if (claudeReady) {
1895
+ finalProfile.providers.claude = { enabled: true, plan: plans.claude || 'pro' };
1896
+ }
1897
+
1898
+ if (state.openaiPlan) {
1899
+ finalProfile.providers.openai = { enabled: true, plan: state.openaiPlan };
1900
+ } else if (openaiReady) {
1901
+ finalProfile.providers.openai = { enabled: true, plan: plans.openai || 'plus' };
1902
+ }
1903
+
1904
+ const enabledCount = [
1905
+ finalProfile.providers?.claude?.enabled,
1906
+ finalProfile.providers?.openai?.enabled,
1907
+ ].filter(Boolean).length;
1908
+
1909
+ finalProfile.mode = enabledCount >= 2 ? state.profile : claudeReady ? 'solo-claude' : 'solo-openai';
1910
+ finalProfile.headModel = state.headModel;
1911
+ finalProfile.bias = state.profile;
1912
+
1913
+ return finalProfile;
1914
+ }
1915
+
1606
1916
  // ─── Screen: dashboardScreen (kept for internal reference, unreachable) ───────
1607
1917
 
1608
1918
  async function dashboardScreen(rl, ask) {
@@ -2244,6 +2554,151 @@ async function runScreens(startScreen = 'dashboard') {
2244
2554
  rl.close();
2245
2555
  }
2246
2556
 
2557
+ // ─── Specialist commands ──────────────────────────────────────────────────────
2558
+
2559
+ const SPECIALIST_DEFAULTS = {
2560
+ python: { name: 'Python', description: 'Python stdlib, typing, async' },
2561
+ typescript: { name: 'TypeScript', description: 'TS type system, React, Node' },
2562
+ html: { name: 'HTML/CSS', description: 'Semantic HTML, CSS, accessibility' },
2563
+ linux: { name: 'Linux/DevOps', description: 'Sysadmin, Docker, nginx, shell' },
2564
+ security: { name: 'Security', description: 'Auth, crypto, OWASP, threat model' },
2565
+ };
2566
+
2567
+ function loadSpecialistRegistry() {
2568
+ const regPath = join(__dirname, '..', 'agents', 'specialists', 'registry.json');
2569
+ try {
2570
+ const raw = JSON.parse(readFileSync(regPath, 'utf8'));
2571
+ const out = {};
2572
+ for (const [key, val] of Object.entries(raw.specialists || {})) {
2573
+ out[key] = { name: val.name || key, description: val.description || '' };
2574
+ }
2575
+ return out;
2576
+ } catch {
2577
+ return SPECIALIST_DEFAULTS;
2578
+ }
2579
+ }
2580
+
2581
+ function cmdSpecialists() {
2582
+ const registry = loadSpecialistRegistry();
2583
+ const entries = Object.entries(registry);
2584
+
2585
+ // Build padded rows
2586
+ const rows = entries.map(([key, val]) => {
2587
+ const k = key.padEnd(12);
2588
+ const d = val.description;
2589
+ return `│ ${k}${d}`;
2590
+ });
2591
+
2592
+ // Find longest row for width
2593
+ const inner = Math.max(
2594
+ ...rows.map(r => r.length),
2595
+ '│ Usage: dual-brain python "task description" │'.length - 2,
2596
+ );
2597
+ const width = inner + 1; // account for trailing │
2598
+
2599
+ function pad(str) {
2600
+ return str + ' '.repeat(Math.max(0, width - str.length - 1)) + '│';
2601
+ }
2602
+
2603
+ const top = '┌' + '─'.repeat(width - 1) + '┐';
2604
+ const title = pad('│ 🎯 Available Specialists');
2605
+ const divTop = '├' + '─'.repeat(width - 1) + '┤';
2606
+ const divBot = '├' + '─'.repeat(width - 1) + '┤';
2607
+ const bot = '└' + '─'.repeat(width - 1) + '┘';
2608
+
2609
+ console.log(top);
2610
+ console.log(title);
2611
+ console.log(divTop);
2612
+ for (const row of rows) console.log(pad(row));
2613
+ console.log(divBot);
2614
+ console.log(pad('│ Usage: dual-brain python "task description"'));
2615
+ console.log(pad('│ Auto-routing: off (use dual-brain go for auto)'));
2616
+ console.log(bot);
2617
+ }
2618
+
2619
+ async function cmdSpecialistGo(specialist, args) {
2620
+ const dryRun = args.includes('--dry-run');
2621
+ const verbose = args.includes('--verbose') || args.includes('-v');
2622
+ const filesRaw = flag(args, '--files');
2623
+ const files = filesRaw && typeof filesRaw === 'string'
2624
+ ? filesRaw.split(',').map(f => f.trim()).filter(Boolean)
2625
+ : [];
2626
+
2627
+ const prompt = args.find(a => !a.startsWith('--') && !a.startsWith('-') && a !== (filesRaw ?? ''));
2628
+ if (!prompt) err(`Usage: dual-brain ${specialist} "task description" [--dry-run] [--files a,b]`);
2629
+
2630
+ const cwd = process.cwd();
2631
+ const profile = await ensureProfile(cwd);
2632
+ const detection = detectTask({ prompt, files });
2633
+
2634
+ // Override specialist, preserve everything else
2635
+ detection.specialist = specialist;
2636
+
2637
+ console.log(`[specialist: ${specialist}] ${detection.explanation}`);
2638
+
2639
+ if (verbose) {
2640
+ vtrace(`Intent: ${detection.intent} | Risk: ${detection.risk} | Complexity: ${detection.complexity} | Effort: ${detection.effort ?? 'n/a'}`);
2641
+ vtrace(`Tier: ${detection.tier} | Specialist override: ${specialist}`);
2642
+ }
2643
+
2644
+ const decision = decideRoute({ profile, detection, cwd });
2645
+
2646
+ if (verbose) {
2647
+ const modelLabel = decision.effort ? `${decision.model} (${decision.effort})` : decision.model;
2648
+ vtrace(`Model selection: ${modelLabel}`);
2649
+ vtrace(`Dual-brain: ${decision.dualBrain ? 'yes' : 'no'}`);
2650
+ }
2651
+
2652
+ console.log(` specialist : ${specialist}`);
2653
+ console.log(` provider : ${decision.provider}`);
2654
+ console.log(` model : ${decision.model}${decision.effort ? ' (' + decision.effort + ')' : ''}`);
2655
+ console.log(` tier : ${decision.tier}`);
2656
+ console.log(` dual-brain : ${decision.dualBrain ? 'yes' : 'no'}`);
2657
+ console.log(` reason : ${decision.explanation}`);
2658
+
2659
+ if (dryRun) {
2660
+ console.log('\n(dry-run — not executing)');
2661
+ return;
2662
+ }
2663
+
2664
+ console.log('\nDispatching...');
2665
+ let result;
2666
+ if (decision.dualBrain) {
2667
+ result = await dispatchDualBrain({ decision, prompt, files, cwd });
2668
+ console.log(`\nConsensus: ${result.consensus}`);
2669
+ if (result.claude?.summary) console.log(`Claude : ${result.claude.summary}`);
2670
+ if (result.openai?.summary) console.log(`OpenAI : ${result.openai.summary}`);
2671
+ saveSession({
2672
+ objective: prompt,
2673
+ branch: null,
2674
+ filesChanged: files,
2675
+ commandsRun: [`dual-brain ${specialist} "${prompt}"`],
2676
+ lastResult: { status: 'success', summary: result.consensus || 'dual-brain complete' },
2677
+ provider: decision.provider,
2678
+ nextAction: null,
2679
+ }, cwd);
2680
+ } else {
2681
+ result = await dispatch({ decision, prompt, files, cwd });
2682
+ const statusLine = result.status === 'completed' ? 'Done' : `Failed (exit ${result.exitCode})`;
2683
+ console.log(`\n${statusLine} in ${(result.durationMs / 1000).toFixed(1)}s`);
2684
+ if (result.summary) console.log(result.summary);
2685
+ if (result.error) process.stderr.write(`${result.error}\n`);
2686
+ saveSession({
2687
+ objective: prompt,
2688
+ branch: null,
2689
+ filesChanged: files,
2690
+ commandsRun: [`dual-brain ${specialist} "${prompt}"`],
2691
+ lastResult: {
2692
+ status: result.status === 'completed' ? 'success' : 'failure',
2693
+ summary: result.summary || (result.status === 'completed' ? 'completed' : `exit ${result.exitCode}`),
2694
+ },
2695
+ provider: decision.provider,
2696
+ nextAction: null,
2697
+ }, cwd);
2698
+ if (result.status !== 'completed') process.exit(1);
2699
+ }
2700
+ }
2701
+
2247
2702
  // ─── Entry point ─────────────────────────────────────────────────────────────
2248
2703
 
2249
2704
  async function main() {
@@ -2267,9 +2722,20 @@ async function main() {
2267
2722
  if (profileExists(cwd)) {
2268
2723
  await runScreens('main');
2269
2724
  } else {
2270
- // First run: welcomeScreen handles auto-setup detection internally,
2271
- // then falls through to manual wizard if needed.
2272
- await runScreens('welcome');
2725
+ // First run: run the 5-step onboarding wizard, then go to main.
2726
+ process.stdout.write(`\ndual-brain v${readVersion()} Setup\n\nDetecting your setup...\n`);
2727
+ const auth = await detectAuth();
2728
+ const plans = detectPlans();
2729
+ const existingSessions = importReplitSessions(cwd);
2730
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
2731
+ const wizardProfile = await runOnboardingWizard({ auth, plans, existingSessions }, cwd, rl);
2732
+ if (wizardProfile) {
2733
+ saveProfile(wizardProfile, { cwd });
2734
+ await cmdInstall(cwd);
2735
+ console.log('\n ✅ Setup complete! Starting dual-brain...\n');
2736
+ }
2737
+ rl.close();
2738
+ await runScreens('main');
2273
2739
  }
2274
2740
  } else {
2275
2741
  // Non-TTY: print status card and exit
@@ -2285,8 +2751,21 @@ async function main() {
2285
2751
 
2286
2752
  if (cmd === 'init') {
2287
2753
  if (isInteractive) {
2288
- // Run welcome wizard then dashboard
2289
- await runScreens('welcome');
2754
+ // Run 5-step onboarding wizard then main screen
2755
+ const cwd = process.cwd();
2756
+ process.stdout.write(`\ndual-brain v${readVersion()} — Setup\n\nDetecting your setup...\n`);
2757
+ const auth = await detectAuth();
2758
+ const plans = detectPlans();
2759
+ const existingSessions = importReplitSessions(cwd);
2760
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
2761
+ const wizardProfile = await runOnboardingWizard({ auth, plans, existingSessions }, cwd, rl);
2762
+ if (wizardProfile) {
2763
+ saveProfile(wizardProfile, { cwd });
2764
+ await cmdInstall(cwd);
2765
+ console.log('\n ✅ Setup complete! Starting dual-brain...\n');
2766
+ }
2767
+ rl.close();
2768
+ await runScreens('main');
2290
2769
  } else {
2291
2770
  await cmdInit();
2292
2771
  }
@@ -2303,8 +2782,14 @@ async function main() {
2303
2782
  if (cmd === 'status') { await cmdStatus(args.slice(1)); return; }
2304
2783
  if (cmd === 'hot') { cmdHot(args[1]); return; }
2305
2784
  if (cmd === 'cool') { cmdCool(args[1]); return; }
2306
- if (cmd === 'remember') { cmdRemember(args[1]); return; }
2307
- if (cmd === 'forget') { cmdForget(args[1]); return; }
2785
+ if (cmd === 'remember') { cmdRemember(args[1]); return; }
2786
+ if (cmd === 'forget') { cmdForget(args[1]); return; }
2787
+ if (cmd === 'break-glass') { cmdBreakGlass(args.slice(1).join(' ')); return; }
2788
+
2789
+ if (cmd === 'specialists') { cmdSpecialists(); return; }
2790
+
2791
+ const SPECIALIST_CMDS = new Set(Object.keys(loadSpecialistRegistry()));
2792
+ if (SPECIALIST_CMDS.has(cmd)) { await cmdSpecialistGo(cmd, args.slice(1)); return; }
2308
2793
 
2309
2794
  if (cmd === 'search') {
2310
2795
  const query = args.slice(1).filter(a => !a.startsWith('--')).join(' ');
package/install.mjs CHANGED
@@ -14,6 +14,7 @@ import { createInterface } from 'readline';
14
14
  import { dirname, join, resolve } from 'path';
15
15
  import { fileURLToPath } from 'url';
16
16
  import { spawnSync } from 'child_process';
17
+ import { createHash } from 'crypto';
17
18
 
18
19
  // Skip hook installation during global npm install — hooks are installed
19
20
  // when the user runs 'dual-brain install' in their project directory.
@@ -863,6 +864,39 @@ function generateGitignoreEntries(workspace) {
863
864
  return { existing, needed };
864
865
  }
865
866
 
867
+ // ─── Hook Manifest ──────────────────────────────────────────────────────────
868
+
869
+ function hashString(s) {
870
+ return createHash('sha256').update(s).digest('hex');
871
+ }
872
+
873
+ function generateHookManifest(settings) {
874
+ const hooks = settings.hooks || {};
875
+ const preHooks = (hooks.PreToolUse || []).flatMap(entry =>
876
+ (entry.hooks || []).map(h => hashString(h.command || ''))
877
+ );
878
+ const postHooks = (hooks.PostToolUse || []).flatMap(entry =>
879
+ (entry.hooks || []).map(h => hashString(h.command || ''))
880
+ );
881
+ const settingsHash = hashString(JSON.stringify(hooks));
882
+ return {
883
+ generatedAt: new Date().toISOString(),
884
+ expectedHooks: {
885
+ PreToolUse: preHooks,
886
+ PostToolUse: postHooks,
887
+ },
888
+ settingsHash,
889
+ };
890
+ }
891
+
892
+ function writeHookManifest(workspace, settings) {
893
+ const dualbrain = join(workspace, '.dualbrain');
894
+ mkdirSync(dualbrain, { recursive: true });
895
+ const manifest = generateHookManifest(settings);
896
+ writeFileSync(join(dualbrain, 'hook-manifest.json'), JSON.stringify(manifest, null, 2) + '\n');
897
+ return manifest;
898
+ }
899
+
866
900
  // ─── Installation ───────────────────────────────────────────────────────────
867
901
 
868
902
  function install(workspace, env, mode) {
@@ -915,6 +949,9 @@ function install(workspace, env, mode) {
915
949
  writeFileSync(join(target, 'settings.json'), JSON.stringify(settings, null, 2) + '\n');
916
950
  actions.push('✓ settings.json (hooks registered)');
917
951
 
952
+ writeHookManifest(workspace, settings);
953
+ actions.push('✓ .dualbrain/hook-manifest.json (integrity manifest)');
954
+
918
955
  const claudeMd = generateClaudeMd(mode);
919
956
  writeClaudeMd(join(target, 'CLAUDE.md'), claudeMd);
920
957
  actions.push('✓ CLAUDE.md (session instructions)');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dual-brain",
3
- "version": "7.1.15",
3
+ "version": "7.1.17",
4
4
  "description": "AI orchestration across Claude + OpenAI subscriptions — smart routing, budget awareness, and dual-brain collaboration",
5
5
  "type": "module",
6
6
  "bin": {
@@ -38,7 +38,7 @@
38
38
  "url": "https://github.com/1xmint/dual-brain.git"
39
39
  },
40
40
  "scripts": {
41
- "test": "node hooks/test-orchestrator.mjs",
41
+ "test": "node .claude/hooks/test-orchestrator.mjs",
42
42
  "test:core": "node --test src/test.mjs",
43
43
  "postinstall": "echo 'dual-brain installed. Run: dual-brain install (in your project) to set up hooks.'"
44
44
  },