dual-brain 0.1.23 → 0.2.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.
@@ -54,6 +54,63 @@ async function getFailureMem() {
54
54
  return _failureMem;
55
55
  }
56
56
 
57
+ let _livingDocs = null;
58
+ async function getLivingDocs() {
59
+ if (!_livingDocs) {
60
+ try { _livingDocs = await import('../src/living-docs.mjs'); } catch { _livingDocs = {}; }
61
+ }
62
+ return _livingDocs;
63
+ }
64
+
65
+ let _fx = null;
66
+ async function getFx() {
67
+ if (_fx !== null) return _fx;
68
+ try {
69
+ _fx = await import('../src/fx.mjs');
70
+ } catch {
71
+ // Fallback stubs when fx.mjs is not yet present
72
+ const _noop = () => {};
73
+ const _spinnerStub = (text) => {
74
+ let _t = text;
75
+ const _o = {
76
+ start() { process.stdout.write(` … ${_t}\n`); return _o; },
77
+ succeed(msg) { process.stdout.write(` ✓ ${msg || _t}\n`); return _o; },
78
+ fail(msg) { process.stdout.write(` ✗ ${msg || _t}\n`); return _o; },
79
+ warn(msg) { process.stdout.write(` ⚠ ${msg || _t}\n`); return _o; },
80
+ stop() { return _o; },
81
+ update(t) { _t = t; return _o; },
82
+ };
83
+ return _o;
84
+ };
85
+ _fx = {
86
+ spinner: _spinnerStub,
87
+ success: (t) => process.stdout.write(` ✓ ${t}\n`),
88
+ error: (t) => process.stdout.write(` ✗ ${t}\n`),
89
+ warn: (t) => process.stdout.write(` ⚠ ${t}\n`),
90
+ info: (t) => process.stdout.write(` ${t}\n`),
91
+ dim: (t) => process.stdout.write(` ${t}\n`),
92
+ step: (cur, tot, t) => process.stdout.write(`\n [${cur}/${tot}] ${t}\n`),
93
+ banner: (t) => process.stdout.write(`\n ═══ ${t} ═══\n\n`),
94
+ box: (content) => process.stdout.write(`${content}\n`),
95
+ celebrate: (t) => process.stdout.write(` ✨ ${t}\n`),
96
+ loadingSequence: async (steps) => {
97
+ for (const s of steps) {
98
+ process.stdout.write(` … ${s.text}\n`);
99
+ await new Promise(r => setTimeout(r, Math.min(s.duration || 300, 300)));
100
+ process.stdout.write(` ✓ ${s.successText || s.text}\n`);
101
+ }
102
+ },
103
+ gradient: (t) => t,
104
+ sleep: (ms) => new Promise(r => setTimeout(r, ms)),
105
+ clearScreen: _noop,
106
+ nl: () => process.stdout.write('\n'),
107
+ getMode: () => 'plain',
108
+ colors: {},
109
+ };
110
+ }
111
+ return _fx;
112
+ }
113
+
57
114
  // ─── Helpers ─────────────────────────────────────────────────────────────────
58
115
 
59
116
  const __dirname = dirname(fileURLToPath(import.meta.url));
@@ -355,6 +412,12 @@ async function cmdGo(args, opts = {}) {
355
412
  const cwd = process.cwd();
356
413
  await ensureProfile(cwd);
357
414
 
415
+ // ── Living docs: ensure .dual-brain/ exists on session start ─────────────
416
+ try {
417
+ const ld = await getLivingDocs();
418
+ if (ld.initLivingDocs) ld.initLivingDocs(cwd);
419
+ } catch { /* non-fatal */ }
420
+
358
421
  if (verbose) console.log('\nDispatching...');
359
422
 
360
423
  // ── Failure memory: check history before dispatching ──────────────────────
@@ -368,6 +431,13 @@ async function cmdGo(args, opts = {}) {
368
431
  } catch { /* non-fatal */ }
369
432
  }
370
433
 
434
+ // ── Dispatch visualization ─────────────────────────────────────────────────
435
+ const fxGo = await getFx();
436
+ let dispatchSpinner = null;
437
+ if (fxGo) {
438
+ dispatchSpinner = fxGo.spinner(`Dispatching agent...`).start();
439
+ }
440
+
371
441
  const { plan, result } = await runPipeline('go', prompt, {
372
442
  files,
373
443
  cwd,
@@ -375,6 +445,11 @@ async function cmdGo(args, opts = {}) {
375
445
  dryRun,
376
446
  });
377
447
 
448
+ if (dispatchSpinner) {
449
+ const model = plan?._decision?.model || plan?._decision?.provider || 'agent';
450
+ dispatchSpinner.succeed(`Agent dispatched: ${prompt.slice(0, 50)}`);
451
+ }
452
+
378
453
  if (dryRun) {
379
454
  // formatExecutionPlan already printed by pipeline when verbose/dryRun=true
380
455
  console.log('\n(dry-run — not executing)');
@@ -385,6 +460,7 @@ async function cmdGo(args, opts = {}) {
385
460
 
386
461
  // Display result — dual-brain vs single-provider
387
462
  if (result.consensus) {
463
+ if (fxGo) fxGo.celebrate('Task complete!');
388
464
  console.log(`\nConsensus: ${result.consensus}`);
389
465
  if (result.claude?.summary) console.log(`Claude : ${result.claude.summary}`);
390
466
  if (result.openai?.summary) console.log(`OpenAI : ${result.openai.summary}`);
@@ -408,6 +484,16 @@ async function cmdGo(args, opts = {}) {
408
484
  nextAction: null,
409
485
  }, cwd);
410
486
 
487
+ // ── Living docs: record completed session action ───────────────────────
488
+ try {
489
+ const ld = await getLivingDocs();
490
+ if (ld.appendAction) ld.appendAction({
491
+ type: 'task', intent: prompt, status: 'completed',
492
+ owner: plan?._decision?.provider ?? 'claude',
493
+ files, result: result.consensus || 'dual-brain complete',
494
+ }, cwd);
495
+ } catch { /* non-fatal */ }
496
+
411
497
  // Clear failure memory on success
412
498
  if (failureMem.clearFailures) {
413
499
  try { await failureMem.clearFailures(prompt, cwd); } catch { /* non-fatal */ }
@@ -428,9 +514,15 @@ async function cmdGo(args, opts = {}) {
428
514
  } else {
429
515
  const succeeded = result.status === 'completed';
430
516
  const statusLine = succeeded ? 'Done' : `Failed (exit ${result.exitCode})`;
517
+ if (succeeded && fxGo) {
518
+ fxGo.celebrate('Task complete!');
519
+ }
431
520
  console.log(`\n${statusLine}${result.durationMs != null ? ` in ${(result.durationMs / 1000).toFixed(1)}s` : ''}`);
432
521
  if (result.summary) console.log(result.summary);
433
- if (result.error) process.stderr.write(`${result.error}\n`);
522
+ if (result.error) {
523
+ if (fxGo) fxGo.error(result.error);
524
+ else process.stderr.write(`${result.error}\n`);
525
+ }
434
526
 
435
527
  // Receipt
436
528
  const receipt = await getReceipt();
@@ -459,6 +551,17 @@ async function cmdGo(args, opts = {}) {
459
551
  nextAction: null,
460
552
  }, cwd);
461
553
 
554
+ // ── Living docs: record completed session action ───────────────────────
555
+ try {
556
+ const ld = await getLivingDocs();
557
+ if (ld.appendAction) ld.appendAction({
558
+ type: 'task', intent: prompt, status: succeeded ? 'completed' : 'failed',
559
+ owner: plan?._decision?.provider ?? 'claude',
560
+ files: result.filesChanged || files,
561
+ result: result.summary || (succeeded ? 'completed' : `exit ${result.exitCode}`),
562
+ }, cwd);
563
+ } catch { /* non-fatal */ }
564
+
462
565
  if (!succeeded) {
463
566
  // Record failure memory
464
567
  if (failureMem.recordFailure) {
@@ -505,6 +608,9 @@ async function cmdThink(args) {
505
608
  const cwd = process.cwd();
506
609
  await ensureProfile(cwd);
507
610
 
611
+ const fxThink = await getFx();
612
+ if (fxThink) fxThink.info('Round 1: GPT analyzing...');
613
+
508
614
  const { result, verification } = await runPipeline('think', question, {
509
615
  cwd,
510
616
  verbose: true,
@@ -513,12 +619,17 @@ async function cmdThink(args) {
513
619
  if (!result) return;
514
620
 
515
621
  if (result.consensus) {
622
+ if (fxThink) fxThink.success('Round 1 complete');
516
623
  console.log(`\nConsensus: ${result.consensus}`);
517
624
  if (result.claude?.summary) console.log(`Claude : ${result.claude.summary}`);
518
625
  if (result.openai?.summary) console.log(`OpenAI : ${result.openai.summary}`);
519
626
  } else {
627
+ if (fxThink) fxThink.success('Round 1 complete');
520
628
  if (result.summary) console.log(`\n${result.summary}`);
521
- if (result.error) process.stderr.write(`${result.error}\n`);
629
+ if (result.error) {
630
+ if (fxThink) fxThink.error(result.error);
631
+ else process.stderr.write(`${result.error}\n`);
632
+ }
522
633
  if (result.status && result.status !== 'completed') process.exit(1);
523
634
  }
524
635
 
@@ -670,12 +781,15 @@ async function cmdStatus(args = []) {
670
781
  const { states } = getHealth(cwd);
671
782
  const sessionStats = getSessionStats(cwd);
672
783
 
784
+ const fxSt = await getFx();
785
+
673
786
  console.log('=== Dual-Brain Status ===\n');
674
787
 
675
788
  // Providers + health
676
789
  console.log('Providers:');
677
790
  if (providers.length === 0) {
678
- console.log(' (none configured — run: dual-brain init)');
791
+ if (fxSt) fxSt.warn('(none configured — run: dual-brain init)');
792
+ else console.log(' (none configured — run: dual-brain init)');
679
793
  } else {
680
794
  for (const p of providers) {
681
795
  const label = p.name === 'claude' ? 'Claude' : 'OpenAI';
@@ -686,7 +800,8 @@ async function cmdStatus(args = []) {
686
800
 
687
801
  const planStr = p.plan ? ` plan=${p.plan}` : '';
688
802
  if (provStates.length === 0) {
689
- console.log(` ${label}${planStr} status=healthy calls=${sess.calls} tokens=${sess.tokens}`);
803
+ const line = ` ${label}${planStr} status=healthy calls=${sess.calls} tokens=${sess.tokens}`;
804
+ if (fxSt) fxSt.success(line.trim()); else console.log(line);
690
805
  } else {
691
806
  for (const [k, st] of provStates) {
692
807
  const modelClass = k.split(':').slice(1).join(':');
@@ -695,7 +810,14 @@ async function cmdStatus(args = []) {
695
810
  const remaining = remainingCooldownMinutes(p.name, modelClass, cwd);
696
811
  statusStr = remaining > 0 ? `hot (retry in ${remaining}m)` : 'hot (cooling)';
697
812
  }
698
- console.log(` ${label}${planStr} model=${modelClass} status=${statusStr} calls=${sess.calls} tokens=${sess.tokens}`);
813
+ const line = ` ${label}${planStr} model=${modelClass} status=${statusStr} calls=${sess.calls} tokens=${sess.tokens}`;
814
+ if (fxSt) {
815
+ if (st.status === 'hot') fxSt.warn(line.trim());
816
+ else if (st.status === 'down') fxSt.error(line.trim());
817
+ else fxSt.success(line.trim());
818
+ } else {
819
+ console.log(line);
820
+ }
699
821
  }
700
822
  }
701
823
  }
@@ -1007,14 +1129,14 @@ async function welcomeScreen(rl, ask) {
1007
1129
  }
1008
1130
  console.log('');
1009
1131
 
1010
- // --- Detect data-tools / replit-tools sessions ---
1132
+ // --- Detect replit-tools sessions ---
1011
1133
  const env = detectEnvironment();
1012
1134
  const existingSessions = importReplitSessions(cwd);
1013
1135
  if (env.hasReplitTools) {
1014
- detectedLines.push(` data-tools detected`);
1136
+ detectedLines.push(` replit-tools detected`);
1015
1137
  }
1016
1138
  if (existingSessions.length > 0) {
1017
- detectedLines.push(` ${existingSessions.length} session${existingSessions.length !== 1 ? 's' : ''} found from data-tools`);
1139
+ detectedLines.push(` ${existingSessions.length} session${existingSessions.length !== 1 ? 's' : ''} found from replit-tools`);
1018
1140
  }
1019
1141
 
1020
1142
  // --- Detect replit-tools ---
@@ -1054,7 +1176,7 @@ async function welcomeScreen(rl, ask) {
1054
1176
  console.log(' [Enter] Save and go');
1055
1177
  console.log(' [c] Customize work style');
1056
1178
  if (existingSessions.length > 0) {
1057
- console.log(` [i] Import ${existingSessions.length} session${existingSessions.length !== 1 ? 's' : ''} from data-tools`);
1179
+ console.log(` [i] Import ${existingSessions.length} session${existingSessions.length !== 1 ? 's' : ''} from replit-tools`);
1058
1180
  }
1059
1181
  if (!rt.installed) {
1060
1182
  console.log('');
@@ -1066,7 +1188,7 @@ async function welcomeScreen(rl, ask) {
1066
1188
  const choice = (await ask(' Choice: ')).trim().toLowerCase();
1067
1189
 
1068
1190
  if (choice === 'i' && existingSessions.length > 0) {
1069
- console.log(`\n Importing ${existingSessions.length} sessions from data-tools...\n`);
1191
+ console.log(`\n Importing ${existingSessions.length} sessions from replit-tools...\n`);
1070
1192
  const recent = existingSessions.slice(0, 5);
1071
1193
  for (const sess of recent) {
1072
1194
  console.log(` ${sess.age.padEnd(6)} ${sess.name}`);
@@ -1483,12 +1605,20 @@ function detectInterruptedWork(sessions, cwd) {
1483
1605
  * Shows: "● Claude ● OpenAI ⚖️ Balanced"
1484
1606
  * Uses ANSI color codes for the dots — no dollar amounts or usage bars.
1485
1607
  */
1486
- function buildProviderStatusLine(profile, auth, maxWidth = 54) {
1487
- const GREEN = '●';
1488
- const RED = '●';
1608
+ function buildProviderStatusLine(profile, auth, envReport = null) {
1609
+ const GREEN = '\x1b[32m●\x1b[0m';
1610
+ const RED = '\x1b[31m●\x1b[0m';
1611
+
1612
+ // Use envReport secrets when available; fall back to auth detection
1613
+ const claudeAvailable = envReport
1614
+ ? envReport.secrets.ANTHROPIC_API_KEY || auth.claude.found
1615
+ : auth.claude.found;
1616
+ const openaiAvailable = envReport
1617
+ ? envReport.secrets.OPENAI_API_KEY || auth.openai.found
1618
+ : auth.openai.found;
1489
1619
 
1490
- const claudeDot = auth.claude.found ? GREEN : RED;
1491
- const openaiDot = auth.openai.found ? GREEN : RED;
1620
+ const claudeDot = claudeAvailable ? GREEN : RED;
1621
+ const openaiDot = openaiAvailable ? GREEN : RED;
1492
1622
 
1493
1623
  const WORK_STYLE_LABELS = {
1494
1624
  'auto': '⚡ Fast',
@@ -1498,30 +1628,11 @@ function buildProviderStatusLine(profile, auth, maxWidth = 54) {
1498
1628
  'solo-claude': '⚡ Fast',
1499
1629
  'solo-openai': '⚡ Fast',
1500
1630
  };
1501
- const WORK_STYLE_TIPS = {
1502
- 'auto': 'adapts routing by task risk',
1503
- 'cost-saver': 'single model, minimal reviews',
1504
- 'balanced': 'smart routing, reviews when needed',
1505
- 'quality-first': 'dual-brain on everything important',
1506
- 'solo-claude': 'Claude only, no GPT dispatch',
1507
- 'solo-openai': 'OpenAI only, no Claude dispatch',
1508
- };
1509
1631
  const bias = profile?.bias || profile?.mode || 'balanced';
1510
1632
  const label = WORK_STYLE_LABELS[bias] || '⚖️ Balanced';
1511
- const fullTip = WORK_STYLE_TIPS[bias] || 'smart routing, reviews when needed';
1512
-
1513
- // Trim tip to fit within box width (measure visible chars: strip ANSI + variation selectors)
1514
- const labelPlain = label.replace(/[︀-️]/g, '').replace(/[[0-9;]*m/g, '');
1515
- const prefixLen = ('● Claude ● OpenAI ' + labelPlain + ' — ').length;
1516
- const tipMax = maxWidth - prefixLen;
1517
- const tip = tipMax >= 6
1518
- ? (fullTip.length > tipMax ? fullTip.slice(0, tipMax - 1) + '…' : fullTip)
1519
- : '';
1520
1633
 
1521
- const suffix = tip ? ` — ${tip}` : '';
1522
- return `${claudeDot} Claude ${openaiDot} OpenAI ${label}${suffix}`;
1634
+ return `${claudeDot} Claude ${openaiDot} OpenAI ${label}`;
1523
1635
  }
1524
-
1525
1636
  /**
1526
1637
  * Render a box row padded to inner width W (stripping ANSI for length calculation).
1527
1638
  * Returns a string like: "│ content padded to W │"
@@ -1544,6 +1655,13 @@ async function mainScreen(rl, ask) {
1544
1655
  const profile = loadProfile(cwd);
1545
1656
  const auth = await detectAuth();
1546
1657
 
1658
+ // ── Dashboard load animation (full mode only) ─────────────────────────────
1659
+ const fx = await getFx();
1660
+ let dashSpinner = null;
1661
+ if (fx && fx.getMode && fx.getMode() === 'full') {
1662
+ dashSpinner = fx.spinner('Loading dashboard...').start();
1663
+ }
1664
+
1547
1665
  const claudeSub = profile?.providers?.claude;
1548
1666
  const openaiSub = profile?.providers?.openai;
1549
1667
 
@@ -1591,7 +1709,7 @@ async function mainScreen(rl, ask) {
1591
1709
  return ageMs >= 7 * 86400000;
1592
1710
  }).length;
1593
1711
 
1594
- // Detect data-tools version
1712
+ // Detect replit-tools version
1595
1713
  const rtMain = detectReplitTools(cwd);
1596
1714
  const dtVersion = (rtMain.installed && rtMain.version) ? rtMain.version : null;
1597
1715
 
@@ -1609,25 +1727,6 @@ async function mainScreen(rl, ask) {
1609
1727
 
1610
1728
  const row = (content) => makeBoxRow(content, W);
1611
1729
 
1612
- // ── Header: one line above the box ────────────────────────────────────────
1613
- process.stdout.write(`\n🧠 dual-brain v${version}\n`);
1614
- {
1615
- let gitName = '';
1616
- try {
1617
- const { execSync } = await import('node:child_process');
1618
- gitName = execSync('git config user.name', { encoding: 'utf8', stdio: ['pipe','pipe','pipe'] }).trim();
1619
- } catch { /* ignore */ }
1620
- if (gitName) {
1621
- const hour = new Date().getHours();
1622
- let greet;
1623
- if (hour >= 5 && hour <= 11) greet = 'Good morning';
1624
- else if (hour >= 12 && hour <= 16) greet = 'Good afternoon';
1625
- else if (hour >= 17 && hour <= 21) greet = 'Good evening';
1626
- else greet = 'Late night';
1627
- process.stdout.write(`\x1b[2m${greet}, ${gitName}\x1b[0m\n`);
1628
- }
1629
- }
1630
-
1631
1730
  // ── Continuation card (interrupted work) ─────────────────────────────────
1632
1731
  if (interrupted) {
1633
1732
  const ctop = `┌${'─'.repeat(boxW - 2)}┐`;
@@ -1706,15 +1805,82 @@ async function mainScreen(rl, ask) {
1706
1805
  // 's' → fall through to normal dashboard
1707
1806
  }
1708
1807
 
1709
- // ── Status section ────────────────────────────────────────────────────────
1710
- const providerLine = buildProviderStatusLine(profile, auth, W);
1808
+ // ── Environment awareness (powers Box 1 dots + Box 3) ────────────────────
1809
+ let envReport = null;
1810
+ try {
1811
+ const { scanEnvironment } = await import('../src/awareness.mjs');
1812
+ envReport = scanEnvironment(cwd);
1813
+ } catch { /* non-fatal */ }
1814
+
1815
+ // ── Box 1 — Header row data ─────────────────────────────────────────────
1816
+ const providerLine = buildProviderStatusLine(profile, auth, envReport);
1711
1817
 
1712
- const statusRows = [row(providerLine)];
1713
- if (dtVersion) {
1714
- statusRows.push(row(`\x1b[2m📦 replit-tools v${dtVersion}\x1b[0m`));
1818
+ // ── Box 2 — Workspace: gather git data ───────────────────────────────────
1819
+ let gitBranch = 'unknown';
1820
+ let gitUncommitted = 0;
1821
+ let gitAheadCount = 0;
1822
+ let gitLastMsg = '';
1823
+ let gitLastAgo = '';
1824
+
1825
+ try {
1826
+ gitBranch = execSync('git rev-parse --abbrev-ref HEAD 2>/dev/null', {
1827
+ cwd, encoding: 'utf8', timeout: 2000, stdio: 'pipe',
1828
+ }).trim() || 'unknown';
1829
+ } catch {}
1830
+
1831
+ try {
1832
+ const status = execSync('git status --porcelain 2>/dev/null', {
1833
+ cwd, encoding: 'utf8', timeout: 2000, stdio: 'pipe',
1834
+ });
1835
+ gitUncommitted = status.trim().split('\n').filter(Boolean).length;
1836
+ } catch {}
1837
+
1838
+ try {
1839
+ const aheadOut = execSync('git rev-list @{u}..HEAD 2>/dev/null | wc -l', {
1840
+ cwd, encoding: 'utf8', timeout: 2000, stdio: 'pipe',
1841
+ });
1842
+ gitAheadCount = parseInt(aheadOut.trim(), 10) || 0;
1843
+ } catch {}
1844
+
1845
+ try {
1846
+ const logOut = execSync('git log -1 --format="%s|%ct" 2>/dev/null', {
1847
+ cwd, encoding: 'utf8', timeout: 2000, stdio: 'pipe',
1848
+ }).trim();
1849
+ if (logOut) {
1850
+ const [msg, ts] = logOut.split('|');
1851
+ gitLastMsg = (msg || '').slice(0, 38);
1852
+ const ageMs = Date.now() - (parseInt(ts, 10) * 1000);
1853
+ const ageMin = Math.floor(ageMs / 60000);
1854
+ if (ageMin < 60) gitLastAgo = `${ageMin}m ago`;
1855
+ else if (ageMin < 1440) gitLastAgo = `${Math.floor(ageMin / 60)}h ago`;
1856
+ else gitLastAgo = `${Math.floor(ageMin / 1440)}d ago`;
1857
+ }
1858
+ } catch {}
1859
+
1860
+ // ── Box 2 rows ────────────────────────────────────────────────────────────
1861
+ const uncommittedPart = gitUncommitted > 0 ? ` · ${gitUncommitted} uncommitted` : '';
1862
+ const aheadPart = gitAheadCount > 0 ? ` · ${gitAheadCount} ahead` : '';
1863
+ const workspaceLine1 = `${gitBranch}${uncommittedPart}${aheadPart}`;
1864
+ const workspaceLine2 = gitLastMsg
1865
+ ? `Last: ${gitLastMsg} (${gitLastAgo})`
1866
+ : '';
1867
+
1868
+ // Open PRs
1869
+ const repoState = detectRepoState(cwd);
1870
+ const openPRs = await detectOpenPRs(cwd);
1871
+
1872
+ const workspaceRows = [row(workspaceLine1)];
1873
+ if (workspaceLine2) workspaceRows.push(row(workspaceLine2));
1874
+ if (openPRs.length > 0) {
1875
+ workspaceRows.push(row(`${openPRs.length} open PR${openPRs.length === 1 ? '' : 's'}`));
1715
1876
  }
1716
1877
 
1717
- // ── Observer observations (top 2, high priority first) ───────────────────
1878
+ // ── Box 3 Awareness: observer + roadmap + risk ──────────────────────────
1879
+ let awarenessLine1 = '\x1b[2m💡\x1b[0m Ready to work';
1880
+ let awarenessLine2 = '\x1b[2m📋 No roadmap yet\x1b[0m';
1881
+ let awarenessLine3 = '\x1b[32m✓\x1b[0m No risk flags';
1882
+
1883
+ // Line 1: observer data first; fall back to envReport-derived observations
1718
1884
  let quickObservations = [];
1719
1885
  try {
1720
1886
  const observerMod = await import('../src/observer.mjs');
@@ -1724,64 +1890,78 @@ async function mainScreen(rl, ask) {
1724
1890
  const sorted = [...quickState.observations].sort(
1725
1891
  (a, b) => (PRIO[a.priority] ?? 2) - (PRIO[b.priority] ?? 2)
1726
1892
  );
1727
- quickObservations = sorted.slice(0, 2);
1728
- for (const obs of quickObservations) {
1729
- let prefix;
1730
- if (obs.priority === 'high') prefix = '🔴';
1731
- else if (obs.priority === 'medium') prefix = '🟡';
1732
- else prefix = '\x1b[2m💡\x1b[0m';
1733
- statusRows.push(row(`${prefix} ${obs.message}`));
1893
+ quickObservations = sorted.slice(0, 3);
1894
+ const top = quickObservations[0];
1895
+ if (top) {
1896
+ const prefix = top.priority === 'high' ? '🔴' : top.priority === 'medium' ? '🟡' : '\x1b[2m💡\x1b[0m';
1897
+ awarenessLine1 = `${prefix} ${top.message}`;
1898
+ }
1899
+ const hasHighRisk = quickObservations.some(o => o.priority === 'high');
1900
+ if (hasHighRisk) {
1901
+ awarenessLine3 = '\x1b[31m⚠\x1b[0m Risk flags detected — run: dual-brain review';
1734
1902
  }
1735
1903
  }
1736
- } catch { /* non-fatal — module may not exist yet */ }
1737
-
1738
- // ── Action cards (git state + open PRs) ──────────────────────────────────
1739
- const repoState = detectRepoState(cwd);
1740
- const openPRs = await detectOpenPRs(cwd);
1741
- const actionRows = buildActionRows(repoState, row, openPRs);
1742
-
1743
- // ── High-priority observer action cards ───────────────────────────────────
1744
- if (quickObservations.some(o => o.priority === 'high')) {
1745
- const DIM = '\x1b[2m';
1746
- const RESET = '\x1b[0m';
1747
- actionRows.push(row(`${DIM}[r] Security review [t] Run tests [c] Commit${RESET}`));
1904
+ } catch { /* non-fatal — observer may not exist */ }
1905
+
1906
+ // If observer produced nothing, derive from envReport
1907
+ if (awarenessLine1 === '\x1b[2m💡\x1b[0m Ready to work' && envReport) {
1908
+ if (envReport.replit?.hasDatabase) {
1909
+ awarenessLine1 = '\x1b[2m💡\x1b[0m PostgreSQL available';
1910
+ } else if (gitUncommitted > 0) {
1911
+ awarenessLine1 = `\x1b[2m💡\x1b[0m ${gitUncommitted} file${gitUncommitted === 1 ? '' : 's'} ready to commit`;
1912
+ } else if (envReport.dualBrain?.hasFailureMemory) {
1913
+ // Check for recent failures
1914
+ try {
1915
+ const failureMem = await getFailureMem();
1916
+ if (failureMem.getRecentFailures) {
1917
+ const recent = failureMem.getRecentFailures(cwd, 2);
1918
+ if (recent?.length > 0) {
1919
+ awarenessLine1 = `\x1b[33m⚠\x1b[0m ${recent.length} recent failure${recent.length === 1 ? '' : 's'} — check before proceeding`;
1920
+ }
1921
+ }
1922
+ } catch { /* non-fatal */ }
1923
+ }
1748
1924
  }
1749
1925
 
1750
- // ── Related sessions hint (only when no continuation card is showing) ─────
1751
- if (!interrupted && recentSessions.length > 0) {
1926
+ // Line 2: roadmap file, then ledger open tasks as fallback
1927
+ try {
1928
+ const roadmapPath = join(cwd, '.dual-brain', 'roadmap.md');
1929
+ if (existsSync(roadmapPath)) {
1930
+ const roadmapText = readFileSync(roadmapPath, 'utf8');
1931
+ const lines = roadmapText.split('\n').filter(Boolean);
1932
+ // Skip heading lines, grab first non-heading line
1933
+ const firstItem = lines.find(l => !l.startsWith('#') && l.trim().length > 0);
1934
+ if (firstItem) {
1935
+ const clean = firstItem.replace(/^[-*>]+\s*/, '').trim().slice(0, 45);
1936
+ awarenessLine2 = `\x1b[2m📋\x1b[0m ${clean}`;
1937
+ }
1938
+ }
1939
+ } catch { /* non-fatal */ }
1940
+
1941
+ if (awarenessLine2 === '\x1b[2m📋 No roadmap yet\x1b[0m') {
1752
1942
  try {
1753
- const { findRelatedSessions } = await import('../src/session.mjs');
1754
- const mostRecent = recentSessions[0];
1755
- // Build a pseudo-prompt from the most recent session's name/objective
1756
- const recentPrompt = mostRecent.name || '';
1757
- // Load session index to get files for the most recent session
1758
- const indexPath = join(cwd, '.dualbrain', 'session-index.json');
1759
- let recentFiles = [];
1760
- try {
1761
- const idx = JSON.parse(readFileSync(indexPath, 'utf8'));
1762
- recentFiles = idx[mostRecent.id]?.files || [];
1763
- } catch {}
1764
- const related = findRelatedSessions(recentPrompt, recentFiles, cwd);
1765
- if (related.length > 0) {
1766
- const relAgeLabel = (isoDate) => {
1767
- if (!isoDate) return '';
1768
- const diff = Date.now() - Date.parse(isoDate);
1769
- const days = Math.floor(diff / 86400000);
1770
- const hours = Math.floor(diff / 3600000);
1771
- if (days >= 1) return `${days}d`;
1772
- return `${hours}h ago`;
1773
- };
1774
- const relatedParts = related.slice(0, 2).map(r => {
1775
- const age = relAgeLabel(r.date);
1776
- return age ? `${r.smartName} (${age})` : r.smartName;
1777
- });
1778
- const DIM = '\x1b[2m';
1779
- const RESET = '\x1b[0m';
1780
- actionRows.push(row(`${DIM}📎 Related: ${relatedParts.join(', ')}${RESET}`));
1943
+ const { getOpenTasks } = await import('../src/ledger.mjs');
1944
+ const open = getOpenTasks(cwd);
1945
+ if (open.length > 0) {
1946
+ awarenessLine2 = '📋 Next: ' + open[0].intent.slice(0, 45);
1781
1947
  }
1782
1948
  } catch { /* non-fatal */ }
1783
1949
  }
1784
- // ── End related sessions hint ─────────────────────────────────────────────
1950
+
1951
+ // Line 3: model registry age warning
1952
+ try {
1953
+ const { getRegistryAge } = await import('../src/models.mjs');
1954
+ const age = getRegistryAge();
1955
+ if (age > 30 && awarenessLine3 === '\x1b[32m✓\x1b[0m No risk flags') {
1956
+ awarenessLine3 = `\x1b[33m⚠\x1b[0m Model registry ${age} days old`;
1957
+ }
1958
+ } catch { /* non-fatal */ }
1959
+
1960
+ const awarenessRows = [
1961
+ row(awarenessLine1),
1962
+ row(awarenessLine2),
1963
+ row(awarenessLine3),
1964
+ ];
1785
1965
 
1786
1966
  // ── Sessions section ──────────────────────────────────────────────────────
1787
1967
  const sessionRows = [];
@@ -1837,23 +2017,28 @@ async function mainScreen(rl, ask) {
1837
2017
  });
1838
2018
  }
1839
2019
 
1840
- // ── Actions barnavigation only (pipeline verbs are internal stages, not menu items) ─
1841
- const actionsContent = 'n New session / Search q Quit';
2020
+ // ── Box 5Input bar ──────────────────────────────────────────────────
2021
+ const actionsContent = '> type anything... [s] settings [t] team [q] quit';
1842
2022
  const actionsRow = row(actionsContent);
1843
2023
 
1844
- // ── Print the full box ────────────────────────────────────────────────────
1845
- // Include action cards between status and sessions (with separators only when non-empty)
1846
- const poweredByRow = row('\x1b[2mPowered by dual-brain\x1b[0m');
2024
+ // ── Print the full 5-box layout ───────────────────────────────────────────
2025
+ // Box 1: header (title + provider dots + work style)
2026
+ // Box 2: workspace (branch · uncommitted · ahead, last commit, open PRs)
2027
+ // Box 3: awareness (observer, roadmap, risk)
2028
+ // Box 4: sessions
2029
+ // Box 5: input bar
1847
2030
  const lines = [
1848
2031
  top,
1849
- ...statusRows,
1850
- ...(actionRows.length > 0 ? [sep, ...actionRows] : []),
2032
+ row(`🧠 dual-brain v${version}`),
2033
+ row(providerLine),
2034
+ sep,
2035
+ ...workspaceRows,
2036
+ sep,
2037
+ ...awarenessRows,
1851
2038
  sep,
1852
2039
  ...sessionRows,
1853
2040
  sep,
1854
2041
  actionsRow,
1855
- sep,
1856
- poweredByRow,
1857
2042
  bot,
1858
2043
  ];
1859
2044
  // ── Stale session hint ──────────────────────────────────────────────────
@@ -1861,6 +2046,9 @@ async function mainScreen(rl, ask) {
1861
2046
  process.stdout.write(`\x1b[2m${staleCount} stale sessions (>7d) — press s → archive to clean up\x1b[0m\n`);
1862
2047
  }
1863
2048
 
2049
+ // Resolve dashboard spinner before rendering
2050
+ if (dashSpinner) dashSpinner.succeed('Dashboard ready');
2051
+
1864
2052
  process.stdout.write(lines.join('\n') + '\n\n');
1865
2053
 
1866
2054
  // ── Key handling ──────────────────────────────────────────────────────────
@@ -1948,7 +2136,7 @@ async function mainScreen(rl, ask) {
1948
2136
  // Single-key commands only fire when buffer is empty
1949
2137
  if (taskBuffer.length === 0) {
1950
2138
  const lower = str.toLowerCase();
1951
- const singleKeySet = new Set(['n', 's', 'q', '/', 'i']);
2139
+ const singleKeySet = new Set(['n', 's', 't', 'q', '/', 'i']);
1952
2140
  if (singleKeySet.has(lower)) {
1953
2141
  cleanup();
1954
2142
  process.stdout.write('\n');
@@ -2054,6 +2242,7 @@ async function mainScreen(rl, ask) {
2054
2242
  }
2055
2243
 
2056
2244
  if (choice === 's') { return { next: 'settings' }; }
2245
+ if (choice === 't') { return { next: 'team' }; }
2057
2246
  if (choice === 'i') { return { next: 'import-picker' }; }
2058
2247
  if (choice === 'q' || choice === 'exit') { return { next: 'exit' }; }
2059
2248
 
@@ -2078,7 +2267,7 @@ async function newSessionScreen(rl, ask) {
2078
2267
  async function importPickerScreen() {
2079
2268
  const cwd = process.cwd();
2080
2269
 
2081
- // Load all available sessions from data-tools
2270
+ // Load all available sessions from replit-tools
2082
2271
  const allSessions = importReplitSessions(cwd);
2083
2272
 
2084
2273
  // Load existing session meta to filter already-imported ones
@@ -2124,9 +2313,9 @@ async function importPickerScreen() {
2124
2313
  if (allSessions.length === 0) {
2125
2314
  process.stdout.write('\n');
2126
2315
  process.stdout.write(top + '\n');
2127
- process.stdout.write(row('Import from data-tools') + '\n');
2316
+ process.stdout.write(row('Import from replit-tools') + '\n');
2128
2317
  process.stdout.write(sep + '\n');
2129
- process.stdout.write(row('No data-tools sessions found.') + '\n');
2318
+ process.stdout.write(row('No replit-tools sessions found.') + '\n');
2130
2319
  process.stdout.write(row('Install replit-tools: npm i -g replit-tools') + '\n');
2131
2320
  process.stdout.write(sep + '\n');
2132
2321
  process.stdout.write(row('Press any key to go back...') + '\n');
@@ -2138,7 +2327,7 @@ async function importPickerScreen() {
2138
2327
  if (candidates.length === 0) {
2139
2328
  process.stdout.write('\n');
2140
2329
  process.stdout.write(top + '\n');
2141
- process.stdout.write(row('Import from data-tools') + '\n');
2330
+ process.stdout.write(row('Import from replit-tools') + '\n');
2142
2331
  process.stdout.write(sep + '\n');
2143
2332
  process.stdout.write(row(`All ${allSessions.length} sessions already imported.`) + '\n');
2144
2333
  process.stdout.write(sep + '\n');
@@ -2161,7 +2350,7 @@ async function importPickerScreen() {
2161
2350
  const renderPicker = () => {
2162
2351
  process.stdout.write('\x1b[2J\x1b[H'); // clear screen
2163
2352
 
2164
- const headerTitle = 'Import from data-tools';
2353
+ const headerTitle = 'Import from replit-tools';
2165
2354
  const footerLine = '↑↓ Navigate Space Toggle Enter Import q Back';
2166
2355
 
2167
2356
  process.stdout.write('\n');
@@ -2298,7 +2487,7 @@ async function importPickerScreen() {
2298
2487
  }
2299
2488
  saveSessionMeta(updatedMeta, cwd);
2300
2489
 
2301
- process.stdout.write(`✓ Imported ${importCount} session${importCount !== 1 ? 's' : ''} from data-tools\n\n`);
2490
+ process.stdout.write(`✓ Imported ${importCount} session${importCount !== 1 ? 's' : ''} from replit-tools\n\n`);
2302
2491
 
2303
2492
  return { next: 'main' };
2304
2493
  }
@@ -2560,22 +2749,89 @@ async function settingsScreen(rl, ask) {
2560
2749
  'balanced': '⚖️ Balanced',
2561
2750
  'quality-first': '🔥 Full Power',
2562
2751
  };
2563
- const workStyleLabel = WORK_STYLE_DISPLAY[currentBias] || '⚖️ Balanced';
2752
+
2753
+ // Work style current markers
2754
+ const _stIsFast = ['cost-saver', 'auto', 'solo-claude', 'solo-openai'].includes(currentBias);
2755
+ const _stIsBal = currentBias === 'balanced';
2756
+ const _stIsFull = currentBias === 'quality-first';
2757
+ const _stMark = (active) => active ? ' ← current' : '';
2758
+
2759
+ // Provider status dots
2760
+ const _stAuth = await detectAuth();
2761
+ const _stGDOT = '\x1b[32m●\x1b[0m';
2762
+ const _stRDOT = '\x1b[31m●\x1b[0m';
2763
+ const _stClDot = _stAuth.claude.found ? _stGDOT : _stRDOT;
2764
+ const _stOaDot = _stAuth.openai.found ? _stGDOT : _stRDOT;
2765
+ const _stClStatus = _stAuth.claude.found ? 'connected' : 'not connected';
2766
+ const _stOaStatus = _stAuth.openai.found ? 'connected' : 'not connected';
2767
+
2768
+ // Calibration from project.json
2769
+ let _stCal = { specificity: 3, corrections: 3, autonomy: 3 };
2770
+ let _stLevel = 'intermediate';
2771
+ let _stStyle = 'normal';
2772
+ try {
2773
+ const _stLd = await import('../src/living-docs.mjs');
2774
+ const _stCm = await import('../src/calibration.mjs');
2775
+ const _stPs = _stLd.getProjectState(cwd);
2776
+ if (_stPs?.project?.userCalibration) _stCal = _stPs.project.userCalibration;
2777
+ const _stAd = _stCm.getAdaptation(_stCal);
2778
+ _stLevel = _stAd.userLevel;
2779
+ _stStyle = _stAd.responseStyle;
2780
+ } catch { /* non-fatal */ }
2781
+
2782
+ const _stS = typeof _stCal.specificity === 'number' ? _stCal.specificity.toFixed(1) : String(_stCal.specificity ?? 3);
2783
+ const _stC = typeof _stCal.corrections === 'number' ? _stCal.corrections.toFixed(1) : String(_stCal.corrections ?? 3);
2784
+ const _stA = typeof _stCal.autonomy === 'number' ? _stCal.autonomy.toFixed(1) : String(_stCal.autonomy ?? 3);
2785
+
2786
+ // Cost efficiency summary (graceful — only shown when data exists)
2787
+ let _stEffScore = null;
2788
+ let _stEffRate = null;
2789
+ let _stEffTrend = null;
2790
+ let _stEffTier = null;
2791
+ try {
2792
+ const _stCt = await import('../src/cost-tracker.mjs');
2793
+ const _stSummary = _stCt.getCostSummary(cwd, 7);
2794
+ if (_stSummary.totalActions > 0) {
2795
+ _stEffScore = _stCt.getEfficiencyScore(cwd);
2796
+ _stEffRate = Math.round(_stSummary.savingsRate * 100);
2797
+ _stEffTrend = _stSummary.trend;
2798
+ const tierOrder = ['recall', 'quick', 'standard', 'deep', 'ultra'];
2799
+ const _stTierKeys = tierOrder.filter(k => _stSummary.byTier[k]);
2800
+ _stEffTier = _stTierKeys.map(k => {
2801
+ const t = _stSummary.byTier[k];
2802
+ return `${k.padEnd(8)} ${String(t.count).padStart(3)}`;
2803
+ }).join(' ');
2804
+ }
2805
+ } catch { /* non-fatal */ }
2806
+
2807
+ const _stTrendIcon = _stEffTrend === 'improving' ? '↗' : _stEffTrend === 'degrading' ? '↘' : '→';
2564
2808
 
2565
2809
  const lines = [
2566
2810
  top,
2567
2811
  row('Settings'),
2568
2812
  sep,
2569
- row(`[w] Work Style: ${workStyleLabel}`),
2570
- row('[m] Manage subscriptions'),
2571
- row('[e] Manage sessions'),
2572
- row('[i] Import from replit-tools'),
2573
- row('[d] Switch to data-tools'),
2574
- row('[?] Help & shortcuts'),
2575
- row('[x] Diagnostics'),
2813
+ row('Work Style'),
2814
+ row(` [1] Fast — speed over caution${_stMark(_stIsFast)}`),
2815
+ row(` [2] Balanced — smart routing, reviews on important${_stMark(_stIsBal)}`),
2816
+ row(` [3] Full Power — dual-brain everything, max quality${_stMark(_stIsFull)}`),
2817
+ sep,
2818
+ row('Providers'),
2819
+ row(` Claude: ${_stClDot} ${_stClStatus}`),
2820
+ row(` OpenAI: ${_stOaDot} ${_stOaStatus}`),
2821
+ sep,
2822
+ row('User Calibration'),
2823
+ row(` Specificity: ${_stS} Corrections: ${_stC} Autonomy: ${_stA}`),
2824
+ row(` Level: ${_stLevel} · Style: ${_stStyle}`),
2825
+ ...(_stEffScore !== null ? [
2826
+ sep,
2827
+ row('Cost Efficiency (7 days)'),
2828
+ row(` Score: ${_stEffScore}/100 Savings: ${_stEffRate}% Trend: ${_stTrendIcon} ${_stEffTrend}`),
2829
+ ...(_stEffTier ? [row(` Tiers: ${_stEffTier}`)] : []),
2830
+ ] : []),
2831
+ sep,
2832
+ row('[1-3] change style [r] reset calibration [b] back'),
2833
+ row('[m] subscriptions [e] sessions [x] diagnostics'),
2576
2834
  ...(settingsPRs.length > 0 ? [row(`[p] PR triage (${settingsPRs.length} open)`)] : []),
2577
- row(''),
2578
- row('[Esc/b] Back to dashboard'),
2579
2835
  bot,
2580
2836
  ];
2581
2837
  process.stdout.write('\n' + lines.join('\n') + '\n\n');
@@ -2583,45 +2839,10 @@ async function settingsScreen(rl, ask) {
2583
2839
  const raw = (await ask(' Choice: ')).trim();
2584
2840
  const choice = raw.toLowerCase();
2585
2841
 
2586
- if (choice === 'w') {
2587
- // Work style picker
2588
- const wsTop = ` ┌${''.repeat(51)}┐`;
2589
- const wsSep = ` ├${'─'.repeat(51)}┤`;
2590
- const wsBot = ` └${'─'.repeat(51)}┘`;
2591
- const wsPad = (s) => {
2592
- const plain = s.replace(/\x1b\[[0-9;]*m/g, '');
2593
- let vlen = 0;
2594
- for (const ch of plain) {
2595
- const cp = ch.codePointAt(0);
2596
- if (
2597
- (cp >= 0x1f300 && cp <= 0x1faff) ||
2598
- (cp >= 0x2600 && cp <= 0x27bf) ||
2599
- cp === 0xfe0f || cp === 0x20e3
2600
- ) { vlen += 2; } else { vlen += 1; }
2601
- }
2602
- return s + ' '.repeat(Math.max(0, 51 - vlen));
2603
- };
2604
- const wsRow = (s) => ` │ ${wsPad(s)}│`;
2605
-
2606
- const isFast = currentBias === 'cost-saver' || currentBias === 'auto' || currentBias === 'solo-claude' || currentBias === 'solo-openai';
2607
- const isBal = currentBias === 'balanced';
2608
- const isFull = currentBias === 'quality-first';
2609
-
2610
- console.log('');
2611
- console.log(wsTop);
2612
- console.log(wsRow('Work Style'));
2613
- console.log(wsSep);
2614
- console.log(wsRow(` 1. ⚡ Fast — quick, single model${isFast ? ' ← current' : ''}`));
2615
- console.log(wsRow(` 2. ⚖️ Balanced — smart routing${isBal ? ' ← current' : ''}`));
2616
- console.log(wsRow(` 3. 🔥 Full Power — dual-brain everything${isFull ? ' ← current' : ''}`));
2617
- console.log(wsSep);
2618
- console.log(wsRow('[Enter] Keep current'));
2619
- console.log(wsBot);
2620
- console.log('');
2621
-
2622
- const wsChoice = (await ask(' Choice [1/2/3/Enter]: ')).trim();
2623
- const wsMap = { '1': 'cost-saver', '2': 'balanced', '3': 'quality-first' };
2624
- const newBias = wsMap[wsChoice];
2842
+ // Direct work style keys 1/2/3
2843
+ if (choice === '1' || choice === '2' || choice === '3') {
2844
+ const _stWsMap = { '1': 'cost-saver', '2': 'balanced', '3': 'quality-first' };
2845
+ const newBias = _stWsMap[choice];
2625
2846
  if (newBias && newBias !== currentBias) {
2626
2847
  profile.bias = newBias;
2627
2848
  const enabledCount = [
@@ -2631,12 +2852,23 @@ async function settingsScreen(rl, ask) {
2631
2852
  if (enabledCount >= 2) profile.mode = newBias;
2632
2853
  saveProfile(profile, { cwd });
2633
2854
  const newLabel = WORK_STYLE_DISPLAY[newBias] || newBias;
2634
- console.log(`\n Work style set to ${newLabel}\n`);
2855
+ process.stdout.write(`\n Work style set to ${newLabel}\n\n`);
2635
2856
  await ask(' Press Enter to continue...');
2636
2857
  }
2637
2858
  return { next: 'settings' };
2638
2859
  }
2639
2860
 
2861
+ // Reset calibration to defaults
2862
+ if (choice === 'r') {
2863
+ try {
2864
+ const _stLdReset = await import('../src/living-docs.mjs');
2865
+ _stLdReset.updateProject({ userCalibration: { specificity: 3, corrections: 3, autonomy: 3 } }, cwd);
2866
+ process.stdout.write('\n Calibration reset to defaults.\n\n');
2867
+ await ask(' Press Enter to continue...');
2868
+ } catch { /* non-fatal */ }
2869
+ return { next: 'settings' };
2870
+ }
2871
+
2640
2872
  if (choice === 'm') { return { next: 'subscriptions' }; }
2641
2873
 
2642
2874
  if (choice === 'e') { return { next: 'sessions' }; }
@@ -2655,7 +2887,7 @@ async function settingsScreen(rl, ask) {
2655
2887
  if (which.status === 0) {
2656
2888
  spawnSync('claude-menu', { stdio: 'inherit' });
2657
2889
  } else {
2658
- process.stdout.write('\n data-tools not found — install with: npm i -g replit-tools\n\n');
2890
+ process.stdout.write('\n replit-tools not found — install with: npm i -g replit-tools\n\n');
2659
2891
  await ask(' Press Enter to continue...');
2660
2892
  }
2661
2893
  return { next: 'settings' };
@@ -2689,6 +2921,105 @@ async function settingsScreen(rl, ask) {
2689
2921
  return { next: 'main' };
2690
2922
  }
2691
2923
 
2924
+ // ─── Screen: teamScreen ───────────────────────────────────────────────────────
2925
+
2926
+ async function teamScreen(rl, ask) {
2927
+ const cwd = process.cwd();
2928
+
2929
+ // Box layout matching dashboard
2930
+ const termW = process.stdout.columns || 60;
2931
+ const boxW = Math.min(termW - 2, 60);
2932
+ const W = boxW - 4;
2933
+
2934
+ const top = `┌${'─'.repeat(boxW - 2)}┐`;
2935
+ const sep = `├${'─'.repeat(boxW - 2)}┤`;
2936
+ const bot = `└${'─'.repeat(boxW - 2)}┘`;
2937
+ const row = (content) => makeBoxRow(content, W);
2938
+
2939
+ // Load team from project.json
2940
+ let team = [];
2941
+ let sharedSessions = 0;
2942
+ let teamDecisions = 0;
2943
+ try {
2944
+ const _tmLd = await import('../src/living-docs.mjs');
2945
+ const _tmPs = _tmLd.getProjectState(cwd);
2946
+ if (Array.isArray(_tmPs?.project?.team)) {
2947
+ team = _tmPs.project.team;
2948
+ }
2949
+ // Count decisions with more than one participant as team decisions
2950
+ if (Array.isArray(_tmPs?.recentDecisions)) {
2951
+ teamDecisions = _tmPs.recentDecisions.filter(
2952
+ d => Array.isArray(d?.participants) && d.participants.length > 1
2953
+ ).length;
2954
+ }
2955
+ } catch { /* non-fatal */ }
2956
+
2957
+ // Fall back to git user if no team configured
2958
+ let ownerName = '(you)';
2959
+ if (team.length === 0) {
2960
+ try {
2961
+ const { execSync: _tmExec } = await import('node:child_process');
2962
+ const gitUser = _tmExec('git config user.name 2>/dev/null', {
2963
+ encoding: 'utf8', timeout: 2000, stdio: 'pipe',
2964
+ }).trim();
2965
+ if (gitUser) ownerName = gitUser;
2966
+ } catch { /* non-fatal */ }
2967
+ }
2968
+
2969
+ const memberRows = [];
2970
+ if (team.length === 0) {
2971
+ memberRows.push(row(` ${ownerName} (owner)`));
2972
+ } else {
2973
+ for (const member of team) {
2974
+ const role = member.role || 'member';
2975
+ memberRows.push(row(` ${member.name} (${role})`));
2976
+ }
2977
+ }
2978
+
2979
+ const lines = [
2980
+ top,
2981
+ row('Team'),
2982
+ sep,
2983
+ row('Members'),
2984
+ ...memberRows,
2985
+ sep,
2986
+ row(`Shared Sessions: ${sharedSessions}`),
2987
+ row(`Team decisions: ${teamDecisions}`),
2988
+ sep,
2989
+ row('[a] add member [b] back'),
2990
+ bot,
2991
+ ];
2992
+ process.stdout.write('\n' + lines.join('\n') + '\n\n');
2993
+
2994
+ const raw = (await ask(' Choice: ')).trim();
2995
+ const choice = raw.toLowerCase();
2996
+
2997
+ if (choice === 'a') {
2998
+ const name = (await ask(' Member name: ')).trim();
2999
+ if (name) {
3000
+ try {
3001
+ const _tmLdAdd = await import('../src/living-docs.mjs');
3002
+ const _tmCur = _tmLdAdd.getProjectState(cwd);
3003
+ const _tmTeam = Array.isArray(_tmCur?.project?.team) ? [..._tmCur.project.team] : [];
3004
+ _tmTeam.push({ name, role: 'member', addedAt: new Date().toISOString() });
3005
+ _tmLdAdd.updateProject({ team: _tmTeam }, cwd);
3006
+ process.stdout.write(`\n Added ${name} to team.\n\n`);
3007
+ await ask(' Press Enter to continue...');
3008
+ } catch {
3009
+ process.stdout.write('\n Could not save team member.\n\n');
3010
+ await ask(' Press Enter to continue...');
3011
+ }
3012
+ }
3013
+ return { next: 'team' };
3014
+ }
3015
+
3016
+ if (choice === 'b' || choice === 'back' || choice === 'q' || raw === '\x1b') {
3017
+ return { next: 'main' };
3018
+ }
3019
+
3020
+ return { next: 'main' };
3021
+ }
3022
+
2692
3023
 
2693
3024
  // ─── Helper: aggregatePlans ───────────────────────────────────────────────────
2694
3025
 
@@ -2873,115 +3204,194 @@ async function subscriptionsScreen(rl, ask) {
2873
3204
  // ─── Onboarding Wizard ───────────────────────────────────────────────────────
2874
3205
 
2875
3206
  /**
2876
- * Streamlined onboarding: auto-detect capabilities, ask ONE question (work style).
2877
- * Replaces the old 5-step wizard with a ~5-second, one-choice flow.
2878
- * @param {{ auth, plans, existingSessions }} detection
3207
+ * Animated first-run setup wizard.
3208
+ * 5 steps: welcome env scan replit-tools import → work style → ready.
3209
+ * Uses src/fx.mjs when available; falls back to plain output stubs.
3210
+ *
3211
+ * @param {{ auth, plans, existingSessions }} _detection (unused — kept for API compat)
2879
3212
  * @param {string} cwd
2880
3213
  * @param {object} rl readline interface
2881
3214
  * @returns {object|null} profile object to save, or null if cancelled/skipped
2882
3215
  */
2883
3216
  async function runOnboardingWizard(_detection, cwd, rl) {
2884
3217
  const ask = (q) => new Promise(res => rl.question(q, res));
2885
- const version = readVersion();
2886
-
2887
- // ── Rounded box helpers (matching mainScreen style) ────────────────────────
2888
- const W = 51;
2889
- const wTop = ` ┌${'─'.repeat(W)}┐`;
2890
- const wBottom = ` └${'─'.repeat(W)}┘`;
2891
- const wPad = (s) => {
2892
- const plain = s.replace(/\x1b\[[0-9;]*m/g, '');
2893
- let vlen = 0;
2894
- for (const ch of plain) {
2895
- const cp = ch.codePointAt(0);
2896
- if (
2897
- (cp >= 0x1f300 && cp <= 0x1faff) ||
2898
- (cp >= 0x2600 && cp <= 0x27bf) ||
2899
- cp === 0xfe0f || cp === 0x20e3
2900
- ) { vlen += 2; } else { vlen += 1; }
2901
- }
2902
- return s + ' '.repeat(Math.max(0, W - vlen));
2903
- };
2904
- const wRow = (s) => ` │ ${wPad(s)}│`;
3218
+ const fx = await getFx();
3219
+
3220
+ // ─── Step 1: Welcome banner ────────────────────────────────────────────────
3221
+ fx.clearScreen();
3222
+ fx.banner('🧠 DUAL-BRAIN');
3223
+ fx.nl();
3224
+ fx.info("Welcome! Let's set up your AI work partner.");
3225
+ fx.nl();
3226
+ await fx.sleep(800);
3227
+
3228
+ // ─── Step 2: Environment detection ────────────────────────────────────────
3229
+ fx.step(1, 5, 'Scanning environment');
3230
+ fx.nl();
3231
+
3232
+ // Run capability detection in parallel with the animations
3233
+ const capsPromise = detectCapabilities(cwd);
3234
+
3235
+ await fx.loadingSequence([
3236
+ { text: 'Detecting container...', duration: 500, successText: 'Replit container detected' },
3237
+ { text: 'Checking CLI tools...', duration: 400, successText: 'CLI tools available (git, node, claude...)' },
3238
+ { text: 'Scanning secrets...', duration: 350, successText: 'Environment scanned' },
3239
+ ]);
2905
3240
 
2906
- // ── Use detectCapabilities for broad detection (env vars, ~/.claude, CLI) ──
2907
- const caps = await detectCapabilities(cwd);
3241
+ // Await actual capability data
3242
+ const caps = await capsPromise;
2908
3243
  const claudeReady = caps.claude.available;
2909
3244
  const openaiReady = caps.openai.available;
2910
3245
  const codexAvailable = caps.codex.available;
2911
3246
 
2912
- // ── Detect replit-tools ────────────────────────────────────────────────────
3247
+ // Override the generic "secrets" success with real data
3248
+ const secretsLine = claudeReady || openaiReady
3249
+ ? 'API keys configured'
3250
+ : 'No API keys found — configure later';
3251
+ fx.info(secretsLine);
3252
+ fx.nl();
3253
+
3254
+ // ─── Step 3: Detect replit-tools ──────────────────────────────────────────
3255
+ fx.step(2, 5, 'Detecting replit-tools');
3256
+ fx.nl();
3257
+
2913
3258
  const rt = detectReplitTools(cwd);
3259
+ const rtSpinner = fx.spinner('Looking for replit-tools...').start();
3260
+ await fx.sleep(700);
2914
3261
 
2915
- const GREEN = '\x1b[32m✓\x1b[0m';
2916
- const RED = '\x1b[31m✗\x1b[0m';
2917
- const DIM = '\x1b[2m';
2918
- const RESET = '\x1b[0m';
3262
+ let rtSessionCount = 0;
3263
+ if (rt.installed) {
3264
+ const vStr = rt.version ? ` v${rt.version}` : '';
3265
+ rtSpinner.succeed(`replit-tools${vStr} detected`);
3266
+ // Count available sessions
3267
+ try {
3268
+ const sessions = importReplitSessions(cwd);
3269
+ rtSessionCount = sessions.length;
3270
+ } catch { /* non-fatal */ }
3271
+ } else {
3272
+ rtSpinner.warn('replit-tools not found — install with: npm i -g replit-tools');
3273
+ }
3274
+ fx.nl();
2919
3275
 
2920
- // ══════════════════════════════════════════════════════════════════════════
2921
- // Step 1 — Auto-detect capabilities (instant, no spinner)
2922
- // ══════════════════════════════════════════════════════════════════════════
2923
- console.log('');
2924
- console.log(wTop);
2925
- console.log(wRow(`🧠 Dual-Brain v${version} First-time Setup`));
2926
- console.log(wRow(claudeReady
2927
- ? `${GREEN} Claude Code`
2928
- : `${RED} Claude Code — not found`));
2929
- console.log(wRow(openaiReady
2930
- ? `${GREEN} OpenAI API`
2931
- : codexAvailable
2932
- ? `${GREEN} OpenAI / Codex CLI`
2933
- : `${DIM}○ OpenAI not configured${RESET}`));
2934
- console.log(wRow(rt.installed
2935
- ? `${GREEN} replit-tools`
2936
- : `${DIM}○ replit-tools not found${RESET}`));
2937
- console.log(wBottom);
2938
-
2939
- // ── Edge cases: communicate honestly, but always let them proceed ──────────
2940
- console.log('');
2941
- if (!claudeReady && !openaiReady && !codexAvailable) {
2942
- console.log(' No AI providers detected configure OPENAI_API_KEY or use');
2943
- console.log(' within Claude Code. You can still continue and set up later.');
2944
- console.log('');
2945
- } else if (claudeReady && !openaiReady && !codexAvailable) {
2946
- console.log(` ${DIM}Tip: Add OPENAI_API_KEY for dual-brain collaboration${RESET}`);
2947
- console.log('');
2948
- } else if (!claudeReady && (openaiReady || codexAvailable)) {
2949
- console.log(` ${DIM}Note: Use within Claude Code for full dual-brain${RESET}`);
2950
- console.log('');
3276
+ // ─── Step 4: Import conversations ─────────────────────────────────────────
3277
+ fx.step(3, 5, 'Import conversations');
3278
+ fx.nl();
3279
+
3280
+ if (rt.installed && rtSessionCount > 0) {
3281
+ fx.info(`Found ${rtSessionCount} session${rtSessionCount === 1 ? '' : 's'} from replit-tools`);
3282
+ fx.nl();
3283
+
3284
+ // Ask userline-based input since we may not have raw mode here
3285
+ process.stdout.write(' Import conversations? [y/N]: ');
3286
+ const importChoice = (await ask('')).trim().toLowerCase();
3287
+
3288
+ if (importChoice === 'y' || importChoice === 'yes') {
3289
+ const importSpinner = fx.spinner('Importing sessions...').start();
3290
+ await fx.sleep(600);
3291
+ try {
3292
+ // Sessions are already imported via importReplitSessions above (lazy-loaded)
3293
+ importSpinner.succeed(`${rtSessionCount} session${rtSessionCount === 1 ? '' : 's'} imported`);
3294
+ } catch (e) {
3295
+ importSpinner.fail(`Import failed: ${e.message}`);
3296
+ }
3297
+ } else {
3298
+ fx.dim('Skipped you can import later from Settings → Import');
3299
+ }
3300
+ } else if (rt.installed) {
3301
+ fx.dim('No sessions to import');
3302
+ } else {
3303
+ fx.dim('Skipping — replit-tools not found');
2951
3304
  }
3305
+ fx.nl();
2952
3306
 
2953
- // ══════════════════════════════════════════════════════════════════════════
2954
- // Step 2 ONE question: work style
2955
- // ══════════════════════════════════════════════════════════════════════════
2956
- console.log(wTop);
2957
- console.log(wRow('How do you want to work?'));
2958
- console.log(wRow(''));
2959
- console.log(wRow(' 1 ⚡ Fast single model, quick tasks, skip reviews'));
2960
- console.log(wRow(' 2 ⚖️ Balanced — smart routing, reviews on important changes'));
2961
- console.log(wRow(' 3 🔥 Full Power — deep reasoning, dual-brain when it matters'));
2962
- console.log(wBottom);
2963
- console.log('');
3307
+ // ─── Step 5: Work style selection ─────────────────────────────────────────
3308
+ fx.step(4, 5, 'Choose your style');
3309
+ fx.nl();
3310
+ process.stdout.write(' How do you want to work?\n\n');
3311
+ process.stdout.write(' [1] Fast — speed over caution, auto-execute\n');
3312
+ process.stdout.write(' [2] ⚖️ Balanced — smart routing, reviews when it matters\n');
3313
+ process.stdout.write(' [3] 🔒 Thoroughdual-brain everything, max quality\n');
3314
+ fx.nl();
2964
3315
 
2965
- const styleChoice = (await ask(' Choice [2]: ')).trim();
2966
3316
  const styleMap = { '1': 'cost-saver', '2': 'balanced', '3': 'quality-first' };
2967
- const styleNames = { 'cost-saver': 'Fast', 'balanced': 'Balanced', 'quality-first': 'Full Power' };
3317
+ const styleNames = { 'cost-saver': 'Fast', 'balanced': 'Balanced', 'quality-first': 'Thorough' };
3318
+
3319
+ let styleChoice = '2'; // default
3320
+ const isTTY = process.stdin.isTTY && typeof process.stdin.setRawMode === 'function';
3321
+
3322
+ if (isTTY) {
3323
+ // Raw keypress — single character
3324
+ const { emitKeypressEvents } = await import('node:readline');
3325
+ emitKeypressEvents(process.stdin, rl);
3326
+
3327
+ process.stdout.write(' Choice [2]: ');
3328
+ styleChoice = await new Promise((resolve) => {
3329
+ const wasRaw = process.stdin.isRaw;
3330
+ process.stdin.setRawMode(true);
3331
+
3332
+ const cleanup = () => {
3333
+ process.stdin.removeListener('keypress', onKey);
3334
+ try { process.stdin.setRawMode(wasRaw || false); } catch {}
3335
+ };
3336
+
3337
+ const onKey = (str, key) => {
3338
+ if (!key) return;
3339
+ const name = key.name || '';
3340
+ if (key.ctrl && (name === 'c' || name === 'd')) {
3341
+ cleanup();
3342
+ process.stdout.write('\n');
3343
+ resolve('2');
3344
+ return;
3345
+ }
3346
+ if (name === 'return' || name === 'enter') {
3347
+ cleanup();
3348
+ process.stdout.write('\n');
3349
+ resolve('2');
3350
+ return;
3351
+ }
3352
+ if (str === '1' || str === '2' || str === '3') {
3353
+ cleanup();
3354
+ process.stdout.write(`${str}\n`);
3355
+ resolve(str);
3356
+ return;
3357
+ }
3358
+ };
3359
+
3360
+ process.stdin.on('keypress', onKey);
3361
+ });
3362
+ } else {
3363
+ // Fallback: line-based prompt
3364
+ process.stdout.write(' Choice [2]: ');
3365
+ styleChoice = (await ask('')).trim() || '2';
3366
+ }
3367
+
2968
3368
  const chosenBias = styleMap[styleChoice] || 'balanced';
2969
3369
  const chosenName = styleNames[chosenBias];
3370
+ fx.nl();
2970
3371
 
2971
- // ── Non-blocking note if metered API detected ──────────────────────────────
3372
+ // Non-blocking note if metered API detected
2972
3373
  if (openaiReady && caps.openai.metered) {
2973
- console.log(` ${DIM}OpenAI API key detected usage is metered, guardrails enabled${RESET}`);
2974
- console.log('');
3374
+ const DIM = '\x1b[2m'; const RESET = '\x1b[0m';
3375
+ process.stdout.write(` ${DIM}OpenAI API key detected — usage is metered, guardrails enabled${RESET}\n\n`);
2975
3376
  }
2976
3377
 
2977
- // ── Done ───────────────────────────────────────────────────────────────────
2978
- console.log(wTop);
2979
- console.log(wRow(`${GREEN} Ready — ${chosenName} mode`));
2980
- console.log(wRow(` Type a task to start, or press Enter for dashboard`));
2981
- console.log(wBottom);
2982
- console.log('');
3378
+ // ─── Step 6: Ready ────────────────────────────────────────────────────────
3379
+ fx.step(5, 5, 'Ready!');
3380
+ fx.nl();
3381
+
3382
+ // Init living docs
3383
+ try {
3384
+ const ld = await getLivingDocs();
3385
+ if (ld.initLivingDocs) ld.initLivingDocs(cwd);
3386
+ } catch { /* non-fatal */ }
3387
+
3388
+ await fx.sleep(400);
3389
+ fx.celebrate(`dual-brain is ready! (${chosenName} mode)`);
3390
+ fx.nl();
3391
+ fx.info('Type anything to get started. Your AI partner is listening.');
3392
+ await fx.sleep(1200);
2983
3393
 
2984
- // ── Build and return the profile object ────────────────────────────────────
3394
+ // ─── Build and return the profile object ──────────────────────────────────
2985
3395
  const finalProfile = loadProfile(cwd);
2986
3396
 
2987
3397
  finalProfile.providers.claude = { enabled: claudeReady };
@@ -4026,6 +4436,7 @@ const SCREENS = {
4026
4436
  main: mainScreen,
4027
4437
  'new-session': newSessionScreen,
4028
4438
  settings: settingsScreen,
4439
+ team: teamScreen,
4029
4440
  'import-picker': importPickerScreen,
4030
4441
  'pr-triage': prTriageScreen,
4031
4442
  subscriptions: subscriptionsScreen,