dual-brain 0.1.23 → 0.2.1

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
  }
@@ -780,6 +902,27 @@ async function cmdStatus(args = []) {
780
902
  console.log(' unknown (could not read .claude/settings.json)');
781
903
  }
782
904
 
905
+ // Replit section
906
+ try {
907
+ const replit = await import('../src/replit.mjs');
908
+ const env = replit.detectReplitEnvironment(cwd);
909
+ if (env.isReplit) {
910
+ console.log('\nReplit:');
911
+ const tools = replit.inspectReplitTools(cwd);
912
+ const verStr = tools.version ? `v${tools.version}` : 'unknown';
913
+ const capsCount = Array.isArray(tools.capabilities) ? tools.capabilities.length : 0;
914
+ console.log(` replit-tools : ${tools.installed ? `${verStr} (${capsCount} capabilities)` : 'not installed'}`);
915
+ const authStatus = replit.getAuthStatus(cwd);
916
+ console.log(` auth : ${authStatus.authenticated ? 'authenticated' : 'not authenticated'}${authStatus.method ? ` (${authStatus.method})` : ''}`);
917
+ const archive = replit.getSessionArchive(cwd);
918
+ const archiveCount = Array.isArray(archive) ? archive.length : (archive?.count ?? 0);
919
+ console.log(` session archive: ${archiveCount} session${archiveCount !== 1 ? 's' : ''}`);
920
+ const openaiPresent = replit.hasSecret('OPENAI_API_KEY');
921
+ const anthropicPresent = replit.hasSecret('ANTHROPIC_API_KEY');
922
+ console.log(` secrets : OPENAI_API_KEY=${openaiPresent ? 'set' : 'unset'} ANTHROPIC_API_KEY=${anthropicPresent ? 'set' : 'unset'}`);
923
+ }
924
+ } catch { /* replit.mjs not available or not in Replit — skip silently */ }
925
+
783
926
  // Update check
784
927
  try {
785
928
  const localVer = readVersion();
@@ -1007,14 +1150,14 @@ async function welcomeScreen(rl, ask) {
1007
1150
  }
1008
1151
  console.log('');
1009
1152
 
1010
- // --- Detect data-tools / replit-tools sessions ---
1153
+ // --- Detect replit-tools sessions ---
1011
1154
  const env = detectEnvironment();
1012
1155
  const existingSessions = importReplitSessions(cwd);
1013
1156
  if (env.hasReplitTools) {
1014
- detectedLines.push(` data-tools detected`);
1157
+ detectedLines.push(` replit-tools detected`);
1015
1158
  }
1016
1159
  if (existingSessions.length > 0) {
1017
- detectedLines.push(` ${existingSessions.length} session${existingSessions.length !== 1 ? 's' : ''} found from data-tools`);
1160
+ detectedLines.push(` ${existingSessions.length} session${existingSessions.length !== 1 ? 's' : ''} found from replit-tools`);
1018
1161
  }
1019
1162
 
1020
1163
  // --- Detect replit-tools ---
@@ -1054,7 +1197,7 @@ async function welcomeScreen(rl, ask) {
1054
1197
  console.log(' [Enter] Save and go');
1055
1198
  console.log(' [c] Customize work style');
1056
1199
  if (existingSessions.length > 0) {
1057
- console.log(` [i] Import ${existingSessions.length} session${existingSessions.length !== 1 ? 's' : ''} from data-tools`);
1200
+ console.log(` [i] Import ${existingSessions.length} session${existingSessions.length !== 1 ? 's' : ''} from replit-tools`);
1058
1201
  }
1059
1202
  if (!rt.installed) {
1060
1203
  console.log('');
@@ -1066,7 +1209,7 @@ async function welcomeScreen(rl, ask) {
1066
1209
  const choice = (await ask(' Choice: ')).trim().toLowerCase();
1067
1210
 
1068
1211
  if (choice === 'i' && existingSessions.length > 0) {
1069
- console.log(`\n Importing ${existingSessions.length} sessions from data-tools...\n`);
1212
+ console.log(`\n Importing ${existingSessions.length} sessions from replit-tools...\n`);
1070
1213
  const recent = existingSessions.slice(0, 5);
1071
1214
  for (const sess of recent) {
1072
1215
  console.log(` ${sess.age.padEnd(6)} ${sess.name}`);
@@ -1483,12 +1626,20 @@ function detectInterruptedWork(sessions, cwd) {
1483
1626
  * Shows: "● Claude ● OpenAI ⚖️ Balanced"
1484
1627
  * Uses ANSI color codes for the dots — no dollar amounts or usage bars.
1485
1628
  */
1486
- function buildProviderStatusLine(profile, auth, maxWidth = 54) {
1487
- const GREEN = '●';
1488
- const RED = '●';
1629
+ function buildProviderStatusLine(profile, auth, envReport = null) {
1630
+ const GREEN = '\x1b[32m●\x1b[0m';
1631
+ const RED = '\x1b[31m●\x1b[0m';
1489
1632
 
1490
- const claudeDot = auth.claude.found ? GREEN : RED;
1491
- const openaiDot = auth.openai.found ? GREEN : RED;
1633
+ // Use envReport secrets when available; fall back to auth detection
1634
+ const claudeAvailable = envReport
1635
+ ? envReport.secrets.ANTHROPIC_API_KEY || auth.claude.found
1636
+ : auth.claude.found;
1637
+ const openaiAvailable = envReport
1638
+ ? envReport.secrets.OPENAI_API_KEY || auth.openai.found
1639
+ : auth.openai.found;
1640
+
1641
+ const claudeDot = claudeAvailable ? GREEN : RED;
1642
+ const openaiDot = openaiAvailable ? GREEN : RED;
1492
1643
 
1493
1644
  const WORK_STYLE_LABELS = {
1494
1645
  'auto': '⚡ Fast',
@@ -1498,30 +1649,11 @@ function buildProviderStatusLine(profile, auth, maxWidth = 54) {
1498
1649
  'solo-claude': '⚡ Fast',
1499
1650
  'solo-openai': '⚡ Fast',
1500
1651
  };
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
1652
  const bias = profile?.bias || profile?.mode || 'balanced';
1510
1653
  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
1654
 
1521
- const suffix = tip ? ` — ${tip}` : '';
1522
- return `${claudeDot} Claude ${openaiDot} OpenAI ${label}${suffix}`;
1655
+ return `${claudeDot} Claude ${openaiDot} OpenAI ${label}`;
1523
1656
  }
1524
-
1525
1657
  /**
1526
1658
  * Render a box row padded to inner width W (stripping ANSI for length calculation).
1527
1659
  * Returns a string like: "│ content padded to W │"
@@ -1544,6 +1676,13 @@ async function mainScreen(rl, ask) {
1544
1676
  const profile = loadProfile(cwd);
1545
1677
  const auth = await detectAuth();
1546
1678
 
1679
+ // ── Dashboard load animation (full mode only) ─────────────────────────────
1680
+ const fx = await getFx();
1681
+ let dashSpinner = null;
1682
+ if (fx && fx.getMode && fx.getMode() === 'full') {
1683
+ dashSpinner = fx.spinner('Loading dashboard...').start();
1684
+ }
1685
+
1547
1686
  const claudeSub = profile?.providers?.claude;
1548
1687
  const openaiSub = profile?.providers?.openai;
1549
1688
 
@@ -1591,7 +1730,7 @@ async function mainScreen(rl, ask) {
1591
1730
  return ageMs >= 7 * 86400000;
1592
1731
  }).length;
1593
1732
 
1594
- // Detect data-tools version
1733
+ // Detect replit-tools version
1595
1734
  const rtMain = detectReplitTools(cwd);
1596
1735
  const dtVersion = (rtMain.installed && rtMain.version) ? rtMain.version : null;
1597
1736
 
@@ -1609,25 +1748,6 @@ async function mainScreen(rl, ask) {
1609
1748
 
1610
1749
  const row = (content) => makeBoxRow(content, W);
1611
1750
 
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
1751
  // ── Continuation card (interrupted work) ─────────────────────────────────
1632
1752
  if (interrupted) {
1633
1753
  const ctop = `┌${'─'.repeat(boxW - 2)}┐`;
@@ -1706,15 +1826,82 @@ async function mainScreen(rl, ask) {
1706
1826
  // 's' → fall through to normal dashboard
1707
1827
  }
1708
1828
 
1709
- // ── Status section ────────────────────────────────────────────────────────
1710
- const providerLine = buildProviderStatusLine(profile, auth, W);
1829
+ // ── Environment awareness (powers Box 1 dots + Box 3) ────────────────────
1830
+ let envReport = null;
1831
+ try {
1832
+ const { scanEnvironment } = await import('../src/awareness.mjs');
1833
+ envReport = scanEnvironment(cwd);
1834
+ } catch { /* non-fatal */ }
1835
+
1836
+ // ── Box 1 — Header row data ─────────────────────────────────────────────
1837
+ const providerLine = buildProviderStatusLine(profile, auth, envReport);
1711
1838
 
1712
- const statusRows = [row(providerLine)];
1713
- if (dtVersion) {
1714
- statusRows.push(row(`\x1b[2m📦 replit-tools v${dtVersion}\x1b[0m`));
1839
+ // ── Box 2 — Workspace: gather git data ───────────────────────────────────
1840
+ let gitBranch = 'unknown';
1841
+ let gitUncommitted = 0;
1842
+ let gitAheadCount = 0;
1843
+ let gitLastMsg = '';
1844
+ let gitLastAgo = '';
1845
+
1846
+ try {
1847
+ gitBranch = execSync('git rev-parse --abbrev-ref HEAD 2>/dev/null', {
1848
+ cwd, encoding: 'utf8', timeout: 2000, stdio: 'pipe',
1849
+ }).trim() || 'unknown';
1850
+ } catch {}
1851
+
1852
+ try {
1853
+ const status = execSync('git status --porcelain 2>/dev/null', {
1854
+ cwd, encoding: 'utf8', timeout: 2000, stdio: 'pipe',
1855
+ });
1856
+ gitUncommitted = status.trim().split('\n').filter(Boolean).length;
1857
+ } catch {}
1858
+
1859
+ try {
1860
+ const aheadOut = execSync('git rev-list @{u}..HEAD 2>/dev/null | wc -l', {
1861
+ cwd, encoding: 'utf8', timeout: 2000, stdio: 'pipe',
1862
+ });
1863
+ gitAheadCount = parseInt(aheadOut.trim(), 10) || 0;
1864
+ } catch {}
1865
+
1866
+ try {
1867
+ const logOut = execSync('git log -1 --format="%s|%ct" 2>/dev/null', {
1868
+ cwd, encoding: 'utf8', timeout: 2000, stdio: 'pipe',
1869
+ }).trim();
1870
+ if (logOut) {
1871
+ const [msg, ts] = logOut.split('|');
1872
+ gitLastMsg = (msg || '').slice(0, 38);
1873
+ const ageMs = Date.now() - (parseInt(ts, 10) * 1000);
1874
+ const ageMin = Math.floor(ageMs / 60000);
1875
+ if (ageMin < 60) gitLastAgo = `${ageMin}m ago`;
1876
+ else if (ageMin < 1440) gitLastAgo = `${Math.floor(ageMin / 60)}h ago`;
1877
+ else gitLastAgo = `${Math.floor(ageMin / 1440)}d ago`;
1878
+ }
1879
+ } catch {}
1880
+
1881
+ // ── Box 2 rows ────────────────────────────────────────────────────────────
1882
+ const uncommittedPart = gitUncommitted > 0 ? ` · ${gitUncommitted} uncommitted` : '';
1883
+ const aheadPart = gitAheadCount > 0 ? ` · ${gitAheadCount} ahead` : '';
1884
+ const workspaceLine1 = `${gitBranch}${uncommittedPart}${aheadPart}`;
1885
+ const workspaceLine2 = gitLastMsg
1886
+ ? `Last: ${gitLastMsg} (${gitLastAgo})`
1887
+ : '';
1888
+
1889
+ // Open PRs
1890
+ const repoState = detectRepoState(cwd);
1891
+ const openPRs = await detectOpenPRs(cwd);
1892
+
1893
+ const workspaceRows = [row(workspaceLine1)];
1894
+ if (workspaceLine2) workspaceRows.push(row(workspaceLine2));
1895
+ if (openPRs.length > 0) {
1896
+ workspaceRows.push(row(`${openPRs.length} open PR${openPRs.length === 1 ? '' : 's'}`));
1715
1897
  }
1716
1898
 
1717
- // ── Observer observations (top 2, high priority first) ───────────────────
1899
+ // ── Box 3 Awareness: observer + roadmap + risk ──────────────────────────
1900
+ let awarenessLine1 = '\x1b[2m💡\x1b[0m Ready to work';
1901
+ let awarenessLine2 = '\x1b[2m📋 No roadmap yet\x1b[0m';
1902
+ let awarenessLine3 = '\x1b[32m✓\x1b[0m No risk flags';
1903
+
1904
+ // Line 1: observer data first; fall back to envReport-derived observations
1718
1905
  let quickObservations = [];
1719
1906
  try {
1720
1907
  const observerMod = await import('../src/observer.mjs');
@@ -1724,64 +1911,98 @@ async function mainScreen(rl, ask) {
1724
1911
  const sorted = [...quickState.observations].sort(
1725
1912
  (a, b) => (PRIO[a.priority] ?? 2) - (PRIO[b.priority] ?? 2)
1726
1913
  );
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}`));
1914
+ quickObservations = sorted.slice(0, 3);
1915
+ const top = quickObservations[0];
1916
+ if (top) {
1917
+ const prefix = top.priority === 'high' ? '🔴' : top.priority === 'medium' ? '🟡' : '\x1b[2m💡\x1b[0m';
1918
+ awarenessLine1 = `${prefix} ${top.message}`;
1919
+ }
1920
+ const hasHighRisk = quickObservations.some(o => o.priority === 'high');
1921
+ if (hasHighRisk) {
1922
+ awarenessLine3 = '\x1b[31m⚠\x1b[0m Risk flags detected — run: dual-brain review';
1734
1923
  }
1735
1924
  }
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}`));
1925
+ } catch { /* non-fatal — observer may not exist */ }
1926
+
1927
+ // If observer produced nothing, derive from envReport
1928
+ if (awarenessLine1 === '\x1b[2m💡\x1b[0m Ready to work' && envReport) {
1929
+ if (envReport.replit?.hasDatabase) {
1930
+ awarenessLine1 = '\x1b[2m💡\x1b[0m PostgreSQL available';
1931
+ } else if (gitUncommitted > 0) {
1932
+ awarenessLine1 = `\x1b[2m💡\x1b[0m ${gitUncommitted} file${gitUncommitted === 1 ? '' : 's'} ready to commit`;
1933
+ } else if (envReport.dualBrain?.hasFailureMemory) {
1934
+ // Check for recent failures
1935
+ try {
1936
+ const failureMem = await getFailureMem();
1937
+ if (failureMem.getRecentFailures) {
1938
+ const recent = failureMem.getRecentFailures(cwd, 2);
1939
+ if (recent?.length > 0) {
1940
+ awarenessLine1 = `\x1b[33m⚠\x1b[0m ${recent.length} recent failure${recent.length === 1 ? '' : 's'} — check before proceeding`;
1941
+ }
1942
+ }
1943
+ } catch { /* non-fatal */ }
1944
+ }
1748
1945
  }
1749
1946
 
1750
- // ── Related sessions hint (only when no continuation card is showing) ─────
1751
- if (!interrupted && recentSessions.length > 0) {
1947
+ // Line 2: roadmap file, then ledger open tasks as fallback
1948
+ try {
1949
+ const roadmapPath = join(cwd, '.dual-brain', 'roadmap.md');
1950
+ if (existsSync(roadmapPath)) {
1951
+ const roadmapText = readFileSync(roadmapPath, 'utf8');
1952
+ const lines = roadmapText.split('\n').filter(Boolean);
1953
+ // Skip heading lines, grab first non-heading line
1954
+ const firstItem = lines.find(l => !l.startsWith('#') && l.trim().length > 0);
1955
+ if (firstItem) {
1956
+ const clean = firstItem.replace(/^[-*>]+\s*/, '').trim().slice(0, 45);
1957
+ awarenessLine2 = `\x1b[2m📋\x1b[0m ${clean}`;
1958
+ }
1959
+ }
1960
+ } catch { /* non-fatal */ }
1961
+
1962
+ if (awarenessLine2 === '\x1b[2m📋 No roadmap yet\x1b[0m') {
1752
1963
  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}`));
1964
+ const { getOpenTasks } = await import('../src/ledger.mjs');
1965
+ const open = getOpenTasks(cwd);
1966
+ if (open.length > 0) {
1967
+ awarenessLine2 = '📋 Next: ' + open[0].intent.slice(0, 45);
1781
1968
  }
1782
1969
  } catch { /* non-fatal */ }
1783
1970
  }
1784
- // ── End related sessions hint ─────────────────────────────────────────────
1971
+
1972
+ // Line 3: model registry age warning
1973
+ try {
1974
+ const { getRegistryAge } = await import('../src/models.mjs');
1975
+ const age = getRegistryAge();
1976
+ if (age > 30 && awarenessLine3 === '\x1b[32m✓\x1b[0m No risk flags') {
1977
+ awarenessLine3 = `\x1b[33m⚠\x1b[0m Model registry ${age} days old`;
1978
+ }
1979
+ } catch { /* non-fatal */ }
1980
+
1981
+ // Replit awareness rows (shown only when running in Replit, max 2-3 lines)
1982
+ const replitAwarenessRows = [];
1983
+ try {
1984
+ const replitMod = await import('../src/replit.mjs');
1985
+ const replitEnv = replitMod.detectReplitEnvironment(cwd);
1986
+ if (replitEnv.isReplit) {
1987
+ const rtInfo = replitMod.inspectReplitTools(cwd);
1988
+ const authInfo = replitMod.getAuthStatus(cwd);
1989
+ const archive = replitMod.getSessionArchive(cwd);
1990
+ const archCount = Array.isArray(archive) ? archive.length : (archive?.count ?? 0);
1991
+ const secretNames = replitMod.listSecretNames();
1992
+ const secretCount = Array.isArray(secretNames) ? secretNames.length : 0;
1993
+ const verStr = rtInfo.version ? `v${rtInfo.version}` : (rtInfo.installed ? 'installed' : 'not installed');
1994
+ const authStr = authInfo.authenticated ? '\x1b[32m✓\x1b[0m auth' : '\x1b[2mno auth\x1b[0m';
1995
+ replitAwarenessRows.push(row(`\x1b[2m🔧\x1b[0m Replit replit-tools ${verStr} ${authStr}`));
1996
+ replitAwarenessRows.push(row(`\x1b[2m \x1b[0m ${archCount} archived session${archCount !== 1 ? 's' : ''} ${secretCount} secret${secretCount !== 1 ? 's' : ''}`));
1997
+ }
1998
+ } catch { /* replit.mjs not available — skip */ }
1999
+
2000
+ const awarenessRows = [
2001
+ row(awarenessLine1),
2002
+ row(awarenessLine2),
2003
+ row(awarenessLine3),
2004
+ ...replitAwarenessRows,
2005
+ ];
1785
2006
 
1786
2007
  // ── Sessions section ──────────────────────────────────────────────────────
1787
2008
  const sessionRows = [];
@@ -1837,23 +2058,28 @@ async function mainScreen(rl, ask) {
1837
2058
  });
1838
2059
  }
1839
2060
 
1840
- // ── Actions barnavigation only (pipeline verbs are internal stages, not menu items) ─
1841
- const actionsContent = 'n New session / Search q Quit';
2061
+ // ── Box 5Input bar ──────────────────────────────────────────────────
2062
+ const actionsContent = '> type anything... [s] settings [t] team [q] quit';
1842
2063
  const actionsRow = row(actionsContent);
1843
2064
 
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');
2065
+ // ── Print the full 5-box layout ───────────────────────────────────────────
2066
+ // Box 1: header (title + provider dots + work style)
2067
+ // Box 2: workspace (branch · uncommitted · ahead, last commit, open PRs)
2068
+ // Box 3: awareness (observer, roadmap, risk)
2069
+ // Box 4: sessions
2070
+ // Box 5: input bar
1847
2071
  const lines = [
1848
2072
  top,
1849
- ...statusRows,
1850
- ...(actionRows.length > 0 ? [sep, ...actionRows] : []),
2073
+ row(`🧠 dual-brain v${version}`),
2074
+ row(providerLine),
2075
+ sep,
2076
+ ...workspaceRows,
2077
+ sep,
2078
+ ...awarenessRows,
1851
2079
  sep,
1852
2080
  ...sessionRows,
1853
2081
  sep,
1854
2082
  actionsRow,
1855
- sep,
1856
- poweredByRow,
1857
2083
  bot,
1858
2084
  ];
1859
2085
  // ── Stale session hint ──────────────────────────────────────────────────
@@ -1861,6 +2087,9 @@ async function mainScreen(rl, ask) {
1861
2087
  process.stdout.write(`\x1b[2m${staleCount} stale sessions (>7d) — press s → archive to clean up\x1b[0m\n`);
1862
2088
  }
1863
2089
 
2090
+ // Resolve dashboard spinner before rendering
2091
+ if (dashSpinner) dashSpinner.succeed('Dashboard ready');
2092
+
1864
2093
  process.stdout.write(lines.join('\n') + '\n\n');
1865
2094
 
1866
2095
  // ── Key handling ──────────────────────────────────────────────────────────
@@ -1948,7 +2177,7 @@ async function mainScreen(rl, ask) {
1948
2177
  // Single-key commands only fire when buffer is empty
1949
2178
  if (taskBuffer.length === 0) {
1950
2179
  const lower = str.toLowerCase();
1951
- const singleKeySet = new Set(['n', 's', 'q', '/', 'i']);
2180
+ const singleKeySet = new Set(['n', 's', 't', 'q', '/', 'i']);
1952
2181
  if (singleKeySet.has(lower)) {
1953
2182
  cleanup();
1954
2183
  process.stdout.write('\n');
@@ -2054,6 +2283,7 @@ async function mainScreen(rl, ask) {
2054
2283
  }
2055
2284
 
2056
2285
  if (choice === 's') { return { next: 'settings' }; }
2286
+ if (choice === 't') { return { next: 'team' }; }
2057
2287
  if (choice === 'i') { return { next: 'import-picker' }; }
2058
2288
  if (choice === 'q' || choice === 'exit') { return { next: 'exit' }; }
2059
2289
 
@@ -2078,7 +2308,7 @@ async function newSessionScreen(rl, ask) {
2078
2308
  async function importPickerScreen() {
2079
2309
  const cwd = process.cwd();
2080
2310
 
2081
- // Load all available sessions from data-tools
2311
+ // Load all available sessions from replit-tools
2082
2312
  const allSessions = importReplitSessions(cwd);
2083
2313
 
2084
2314
  // Load existing session meta to filter already-imported ones
@@ -2124,9 +2354,9 @@ async function importPickerScreen() {
2124
2354
  if (allSessions.length === 0) {
2125
2355
  process.stdout.write('\n');
2126
2356
  process.stdout.write(top + '\n');
2127
- process.stdout.write(row('Import from data-tools') + '\n');
2357
+ process.stdout.write(row('Import from replit-tools') + '\n');
2128
2358
  process.stdout.write(sep + '\n');
2129
- process.stdout.write(row('No data-tools sessions found.') + '\n');
2359
+ process.stdout.write(row('No replit-tools sessions found.') + '\n');
2130
2360
  process.stdout.write(row('Install replit-tools: npm i -g replit-tools') + '\n');
2131
2361
  process.stdout.write(sep + '\n');
2132
2362
  process.stdout.write(row('Press any key to go back...') + '\n');
@@ -2138,7 +2368,7 @@ async function importPickerScreen() {
2138
2368
  if (candidates.length === 0) {
2139
2369
  process.stdout.write('\n');
2140
2370
  process.stdout.write(top + '\n');
2141
- process.stdout.write(row('Import from data-tools') + '\n');
2371
+ process.stdout.write(row('Import from replit-tools') + '\n');
2142
2372
  process.stdout.write(sep + '\n');
2143
2373
  process.stdout.write(row(`All ${allSessions.length} sessions already imported.`) + '\n');
2144
2374
  process.stdout.write(sep + '\n');
@@ -2161,7 +2391,7 @@ async function importPickerScreen() {
2161
2391
  const renderPicker = () => {
2162
2392
  process.stdout.write('\x1b[2J\x1b[H'); // clear screen
2163
2393
 
2164
- const headerTitle = 'Import from data-tools';
2394
+ const headerTitle = 'Import from replit-tools';
2165
2395
  const footerLine = '↑↓ Navigate Space Toggle Enter Import q Back';
2166
2396
 
2167
2397
  process.stdout.write('\n');
@@ -2298,7 +2528,7 @@ async function importPickerScreen() {
2298
2528
  }
2299
2529
  saveSessionMeta(updatedMeta, cwd);
2300
2530
 
2301
- process.stdout.write(`✓ Imported ${importCount} session${importCount !== 1 ? 's' : ''} from data-tools\n\n`);
2531
+ process.stdout.write(`✓ Imported ${importCount} session${importCount !== 1 ? 's' : ''} from replit-tools\n\n`);
2302
2532
 
2303
2533
  return { next: 'main' };
2304
2534
  }
@@ -2560,22 +2790,89 @@ async function settingsScreen(rl, ask) {
2560
2790
  'balanced': '⚖️ Balanced',
2561
2791
  'quality-first': '🔥 Full Power',
2562
2792
  };
2563
- const workStyleLabel = WORK_STYLE_DISPLAY[currentBias] || '⚖️ Balanced';
2793
+
2794
+ // Work style current markers
2795
+ const _stIsFast = ['cost-saver', 'auto', 'solo-claude', 'solo-openai'].includes(currentBias);
2796
+ const _stIsBal = currentBias === 'balanced';
2797
+ const _stIsFull = currentBias === 'quality-first';
2798
+ const _stMark = (active) => active ? ' ← current' : '';
2799
+
2800
+ // Provider status dots
2801
+ const _stAuth = await detectAuth();
2802
+ const _stGDOT = '\x1b[32m●\x1b[0m';
2803
+ const _stRDOT = '\x1b[31m●\x1b[0m';
2804
+ const _stClDot = _stAuth.claude.found ? _stGDOT : _stRDOT;
2805
+ const _stOaDot = _stAuth.openai.found ? _stGDOT : _stRDOT;
2806
+ const _stClStatus = _stAuth.claude.found ? 'connected' : 'not connected';
2807
+ const _stOaStatus = _stAuth.openai.found ? 'connected' : 'not connected';
2808
+
2809
+ // Calibration from project.json
2810
+ let _stCal = { specificity: 3, corrections: 3, autonomy: 3 };
2811
+ let _stLevel = 'intermediate';
2812
+ let _stStyle = 'normal';
2813
+ try {
2814
+ const _stLd = await import('../src/living-docs.mjs');
2815
+ const _stCm = await import('../src/calibration.mjs');
2816
+ const _stPs = _stLd.getProjectState(cwd);
2817
+ if (_stPs?.project?.userCalibration) _stCal = _stPs.project.userCalibration;
2818
+ const _stAd = _stCm.getAdaptation(_stCal);
2819
+ _stLevel = _stAd.userLevel;
2820
+ _stStyle = _stAd.responseStyle;
2821
+ } catch { /* non-fatal */ }
2822
+
2823
+ const _stS = typeof _stCal.specificity === 'number' ? _stCal.specificity.toFixed(1) : String(_stCal.specificity ?? 3);
2824
+ const _stC = typeof _stCal.corrections === 'number' ? _stCal.corrections.toFixed(1) : String(_stCal.corrections ?? 3);
2825
+ const _stA = typeof _stCal.autonomy === 'number' ? _stCal.autonomy.toFixed(1) : String(_stCal.autonomy ?? 3);
2826
+
2827
+ // Cost efficiency summary (graceful — only shown when data exists)
2828
+ let _stEffScore = null;
2829
+ let _stEffRate = null;
2830
+ let _stEffTrend = null;
2831
+ let _stEffTier = null;
2832
+ try {
2833
+ const _stCt = await import('../src/cost-tracker.mjs');
2834
+ const _stSummary = _stCt.getCostSummary(cwd, 7);
2835
+ if (_stSummary.totalActions > 0) {
2836
+ _stEffScore = _stCt.getEfficiencyScore(cwd);
2837
+ _stEffRate = Math.round(_stSummary.savingsRate * 100);
2838
+ _stEffTrend = _stSummary.trend;
2839
+ const tierOrder = ['recall', 'quick', 'standard', 'deep', 'ultra'];
2840
+ const _stTierKeys = tierOrder.filter(k => _stSummary.byTier[k]);
2841
+ _stEffTier = _stTierKeys.map(k => {
2842
+ const t = _stSummary.byTier[k];
2843
+ return `${k.padEnd(8)} ${String(t.count).padStart(3)}`;
2844
+ }).join(' ');
2845
+ }
2846
+ } catch { /* non-fatal */ }
2847
+
2848
+ const _stTrendIcon = _stEffTrend === 'improving' ? '↗' : _stEffTrend === 'degrading' ? '↘' : '→';
2564
2849
 
2565
2850
  const lines = [
2566
2851
  top,
2567
2852
  row('Settings'),
2568
2853
  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'),
2854
+ row('Work Style'),
2855
+ row(` [1] Fast — speed over caution${_stMark(_stIsFast)}`),
2856
+ row(` [2] Balanced — smart routing, reviews on important${_stMark(_stIsBal)}`),
2857
+ row(` [3] Full Power — dual-brain everything, max quality${_stMark(_stIsFull)}`),
2858
+ sep,
2859
+ row('Providers'),
2860
+ row(` Claude: ${_stClDot} ${_stClStatus}`),
2861
+ row(` OpenAI: ${_stOaDot} ${_stOaStatus}`),
2862
+ sep,
2863
+ row('User Calibration'),
2864
+ row(` Specificity: ${_stS} Corrections: ${_stC} Autonomy: ${_stA}`),
2865
+ row(` Level: ${_stLevel} · Style: ${_stStyle}`),
2866
+ ...(_stEffScore !== null ? [
2867
+ sep,
2868
+ row('Cost Efficiency (7 days)'),
2869
+ row(` Score: ${_stEffScore}/100 Savings: ${_stEffRate}% Trend: ${_stTrendIcon} ${_stEffTrend}`),
2870
+ ...(_stEffTier ? [row(` Tiers: ${_stEffTier}`)] : []),
2871
+ ] : []),
2872
+ sep,
2873
+ row('[1-3] change style [r] reset calibration [b] back'),
2874
+ row('[m] subscriptions [e] sessions [x] diagnostics'),
2576
2875
  ...(settingsPRs.length > 0 ? [row(`[p] PR triage (${settingsPRs.length} open)`)] : []),
2577
- row(''),
2578
- row('[Esc/b] Back to dashboard'),
2579
2876
  bot,
2580
2877
  ];
2581
2878
  process.stdout.write('\n' + lines.join('\n') + '\n\n');
@@ -2583,45 +2880,10 @@ async function settingsScreen(rl, ask) {
2583
2880
  const raw = (await ask(' Choice: ')).trim();
2584
2881
  const choice = raw.toLowerCase();
2585
2882
 
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];
2883
+ // Direct work style keys 1/2/3
2884
+ if (choice === '1' || choice === '2' || choice === '3') {
2885
+ const _stWsMap = { '1': 'cost-saver', '2': 'balanced', '3': 'quality-first' };
2886
+ const newBias = _stWsMap[choice];
2625
2887
  if (newBias && newBias !== currentBias) {
2626
2888
  profile.bias = newBias;
2627
2889
  const enabledCount = [
@@ -2631,12 +2893,23 @@ async function settingsScreen(rl, ask) {
2631
2893
  if (enabledCount >= 2) profile.mode = newBias;
2632
2894
  saveProfile(profile, { cwd });
2633
2895
  const newLabel = WORK_STYLE_DISPLAY[newBias] || newBias;
2634
- console.log(`\n Work style set to ${newLabel}\n`);
2896
+ process.stdout.write(`\n Work style set to ${newLabel}\n\n`);
2635
2897
  await ask(' Press Enter to continue...');
2636
2898
  }
2637
2899
  return { next: 'settings' };
2638
2900
  }
2639
2901
 
2902
+ // Reset calibration to defaults
2903
+ if (choice === 'r') {
2904
+ try {
2905
+ const _stLdReset = await import('../src/living-docs.mjs');
2906
+ _stLdReset.updateProject({ userCalibration: { specificity: 3, corrections: 3, autonomy: 3 } }, cwd);
2907
+ process.stdout.write('\n Calibration reset to defaults.\n\n');
2908
+ await ask(' Press Enter to continue...');
2909
+ } catch { /* non-fatal */ }
2910
+ return { next: 'settings' };
2911
+ }
2912
+
2640
2913
  if (choice === 'm') { return { next: 'subscriptions' }; }
2641
2914
 
2642
2915
  if (choice === 'e') { return { next: 'sessions' }; }
@@ -2655,7 +2928,7 @@ async function settingsScreen(rl, ask) {
2655
2928
  if (which.status === 0) {
2656
2929
  spawnSync('claude-menu', { stdio: 'inherit' });
2657
2930
  } else {
2658
- process.stdout.write('\n data-tools not found — install with: npm i -g replit-tools\n\n');
2931
+ process.stdout.write('\n replit-tools not found — install with: npm i -g replit-tools\n\n');
2659
2932
  await ask(' Press Enter to continue...');
2660
2933
  }
2661
2934
  return { next: 'settings' };
@@ -2689,6 +2962,105 @@ async function settingsScreen(rl, ask) {
2689
2962
  return { next: 'main' };
2690
2963
  }
2691
2964
 
2965
+ // ─── Screen: teamScreen ───────────────────────────────────────────────────────
2966
+
2967
+ async function teamScreen(rl, ask) {
2968
+ const cwd = process.cwd();
2969
+
2970
+ // Box layout matching dashboard
2971
+ const termW = process.stdout.columns || 60;
2972
+ const boxW = Math.min(termW - 2, 60);
2973
+ const W = boxW - 4;
2974
+
2975
+ const top = `┌${'─'.repeat(boxW - 2)}┐`;
2976
+ const sep = `├${'─'.repeat(boxW - 2)}┤`;
2977
+ const bot = `└${'─'.repeat(boxW - 2)}┘`;
2978
+ const row = (content) => makeBoxRow(content, W);
2979
+
2980
+ // Load team from project.json
2981
+ let team = [];
2982
+ let sharedSessions = 0;
2983
+ let teamDecisions = 0;
2984
+ try {
2985
+ const _tmLd = await import('../src/living-docs.mjs');
2986
+ const _tmPs = _tmLd.getProjectState(cwd);
2987
+ if (Array.isArray(_tmPs?.project?.team)) {
2988
+ team = _tmPs.project.team;
2989
+ }
2990
+ // Count decisions with more than one participant as team decisions
2991
+ if (Array.isArray(_tmPs?.recentDecisions)) {
2992
+ teamDecisions = _tmPs.recentDecisions.filter(
2993
+ d => Array.isArray(d?.participants) && d.participants.length > 1
2994
+ ).length;
2995
+ }
2996
+ } catch { /* non-fatal */ }
2997
+
2998
+ // Fall back to git user if no team configured
2999
+ let ownerName = '(you)';
3000
+ if (team.length === 0) {
3001
+ try {
3002
+ const { execSync: _tmExec } = await import('node:child_process');
3003
+ const gitUser = _tmExec('git config user.name 2>/dev/null', {
3004
+ encoding: 'utf8', timeout: 2000, stdio: 'pipe',
3005
+ }).trim();
3006
+ if (gitUser) ownerName = gitUser;
3007
+ } catch { /* non-fatal */ }
3008
+ }
3009
+
3010
+ const memberRows = [];
3011
+ if (team.length === 0) {
3012
+ memberRows.push(row(` ${ownerName} (owner)`));
3013
+ } else {
3014
+ for (const member of team) {
3015
+ const role = member.role || 'member';
3016
+ memberRows.push(row(` ${member.name} (${role})`));
3017
+ }
3018
+ }
3019
+
3020
+ const lines = [
3021
+ top,
3022
+ row('Team'),
3023
+ sep,
3024
+ row('Members'),
3025
+ ...memberRows,
3026
+ sep,
3027
+ row(`Shared Sessions: ${sharedSessions}`),
3028
+ row(`Team decisions: ${teamDecisions}`),
3029
+ sep,
3030
+ row('[a] add member [b] back'),
3031
+ bot,
3032
+ ];
3033
+ process.stdout.write('\n' + lines.join('\n') + '\n\n');
3034
+
3035
+ const raw = (await ask(' Choice: ')).trim();
3036
+ const choice = raw.toLowerCase();
3037
+
3038
+ if (choice === 'a') {
3039
+ const name = (await ask(' Member name: ')).trim();
3040
+ if (name) {
3041
+ try {
3042
+ const _tmLdAdd = await import('../src/living-docs.mjs');
3043
+ const _tmCur = _tmLdAdd.getProjectState(cwd);
3044
+ const _tmTeam = Array.isArray(_tmCur?.project?.team) ? [..._tmCur.project.team] : [];
3045
+ _tmTeam.push({ name, role: 'member', addedAt: new Date().toISOString() });
3046
+ _tmLdAdd.updateProject({ team: _tmTeam }, cwd);
3047
+ process.stdout.write(`\n Added ${name} to team.\n\n`);
3048
+ await ask(' Press Enter to continue...');
3049
+ } catch {
3050
+ process.stdout.write('\n Could not save team member.\n\n');
3051
+ await ask(' Press Enter to continue...');
3052
+ }
3053
+ }
3054
+ return { next: 'team' };
3055
+ }
3056
+
3057
+ if (choice === 'b' || choice === 'back' || choice === 'q' || raw === '\x1b') {
3058
+ return { next: 'main' };
3059
+ }
3060
+
3061
+ return { next: 'main' };
3062
+ }
3063
+
2692
3064
 
2693
3065
  // ─── Helper: aggregatePlans ───────────────────────────────────────────────────
2694
3066
 
@@ -2873,115 +3245,194 @@ async function subscriptionsScreen(rl, ask) {
2873
3245
  // ─── Onboarding Wizard ───────────────────────────────────────────────────────
2874
3246
 
2875
3247
  /**
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
3248
+ * Animated first-run setup wizard.
3249
+ * 5 steps: welcome env scan replit-tools import → work style → ready.
3250
+ * Uses src/fx.mjs when available; falls back to plain output stubs.
3251
+ *
3252
+ * @param {{ auth, plans, existingSessions }} _detection (unused — kept for API compat)
2879
3253
  * @param {string} cwd
2880
3254
  * @param {object} rl readline interface
2881
3255
  * @returns {object|null} profile object to save, or null if cancelled/skipped
2882
3256
  */
2883
3257
  async function runOnboardingWizard(_detection, cwd, rl) {
2884
3258
  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)}│`;
3259
+ const fx = await getFx();
3260
+
3261
+ // ─── Step 1: Welcome banner ────────────────────────────────────────────────
3262
+ fx.clearScreen();
3263
+ fx.banner('🧠 DUAL-BRAIN');
3264
+ fx.nl();
3265
+ fx.info("Welcome! Let's set up your AI work partner.");
3266
+ fx.nl();
3267
+ await fx.sleep(800);
3268
+
3269
+ // ─── Step 2: Environment detection ────────────────────────────────────────
3270
+ fx.step(1, 5, 'Scanning environment');
3271
+ fx.nl();
3272
+
3273
+ // Run capability detection in parallel with the animations
3274
+ const capsPromise = detectCapabilities(cwd);
3275
+
3276
+ await fx.loadingSequence([
3277
+ { text: 'Detecting container...', duration: 500, successText: 'Replit container detected' },
3278
+ { text: 'Checking CLI tools...', duration: 400, successText: 'CLI tools available (git, node, claude...)' },
3279
+ { text: 'Scanning secrets...', duration: 350, successText: 'Environment scanned' },
3280
+ ]);
2905
3281
 
2906
- // ── Use detectCapabilities for broad detection (env vars, ~/.claude, CLI) ──
2907
- const caps = await detectCapabilities(cwd);
3282
+ // Await actual capability data
3283
+ const caps = await capsPromise;
2908
3284
  const claudeReady = caps.claude.available;
2909
3285
  const openaiReady = caps.openai.available;
2910
3286
  const codexAvailable = caps.codex.available;
2911
3287
 
2912
- // ── Detect replit-tools ────────────────────────────────────────────────────
3288
+ // Override the generic "secrets" success with real data
3289
+ const secretsLine = claudeReady || openaiReady
3290
+ ? 'API keys configured'
3291
+ : 'No API keys found — configure later';
3292
+ fx.info(secretsLine);
3293
+ fx.nl();
3294
+
3295
+ // ─── Step 3: Detect replit-tools ──────────────────────────────────────────
3296
+ fx.step(2, 5, 'Detecting replit-tools');
3297
+ fx.nl();
3298
+
2913
3299
  const rt = detectReplitTools(cwd);
3300
+ const rtSpinner = fx.spinner('Looking for replit-tools...').start();
3301
+ await fx.sleep(700);
3302
+
3303
+ let rtSessionCount = 0;
3304
+ if (rt.installed) {
3305
+ const vStr = rt.version ? ` v${rt.version}` : '';
3306
+ rtSpinner.succeed(`replit-tools${vStr} detected`);
3307
+ // Count available sessions
3308
+ try {
3309
+ const sessions = importReplitSessions(cwd);
3310
+ rtSessionCount = sessions.length;
3311
+ } catch { /* non-fatal */ }
3312
+ } else {
3313
+ rtSpinner.warn('replit-tools not found — install with: npm i -g replit-tools');
3314
+ }
3315
+ fx.nl();
2914
3316
 
2915
- const GREEN = '\x1b[32m✓\x1b[0m';
2916
- const RED = '\x1b[31m✗\x1b[0m';
2917
- const DIM = '\x1b[2m';
2918
- const RESET = '\x1b[0m';
3317
+ // ─── Step 4: Import conversations ─────────────────────────────────────────
3318
+ fx.step(3, 5, 'Import conversations');
3319
+ fx.nl();
2919
3320
 
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('');
3321
+ if (rt.installed && rtSessionCount > 0) {
3322
+ fx.info(`Found ${rtSessionCount} session${rtSessionCount === 1 ? '' : 's'} from replit-tools`);
3323
+ fx.nl();
3324
+
3325
+ // Ask user — line-based input since we may not have raw mode here
3326
+ process.stdout.write(' Import conversations? [y/N]: ');
3327
+ const importChoice = (await ask('')).trim().toLowerCase();
3328
+
3329
+ if (importChoice === 'y' || importChoice === 'yes') {
3330
+ const importSpinner = fx.spinner('Importing sessions...').start();
3331
+ await fx.sleep(600);
3332
+ try {
3333
+ // Sessions are already imported via importReplitSessions above (lazy-loaded)
3334
+ importSpinner.succeed(`${rtSessionCount} session${rtSessionCount === 1 ? '' : 's'} imported`);
3335
+ } catch (e) {
3336
+ importSpinner.fail(`Import failed: ${e.message}`);
3337
+ }
3338
+ } else {
3339
+ fx.dim('Skipped — you can import later from Settings → Import');
3340
+ }
3341
+ } else if (rt.installed) {
3342
+ fx.dim('No sessions to import');
3343
+ } else {
3344
+ fx.dim('Skipping replit-tools not found');
2951
3345
  }
3346
+ fx.nl();
2952
3347
 
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('');
3348
+ // ─── Step 5: Work style selection ─────────────────────────────────────────
3349
+ fx.step(4, 5, 'Choose your style');
3350
+ fx.nl();
3351
+ process.stdout.write(' How do you want to work?\n\n');
3352
+ process.stdout.write(' [1] Fast — speed over caution, auto-execute\n');
3353
+ process.stdout.write(' [2] ⚖️ Balanced — smart routing, reviews when it matters\n');
3354
+ process.stdout.write(' [3] 🔒 Thoroughdual-brain everything, max quality\n');
3355
+ fx.nl();
2964
3356
 
2965
- const styleChoice = (await ask(' Choice [2]: ')).trim();
2966
3357
  const styleMap = { '1': 'cost-saver', '2': 'balanced', '3': 'quality-first' };
2967
- const styleNames = { 'cost-saver': 'Fast', 'balanced': 'Balanced', 'quality-first': 'Full Power' };
3358
+ const styleNames = { 'cost-saver': 'Fast', 'balanced': 'Balanced', 'quality-first': 'Thorough' };
3359
+
3360
+ let styleChoice = '2'; // default
3361
+ const isTTY = process.stdin.isTTY && typeof process.stdin.setRawMode === 'function';
3362
+
3363
+ if (isTTY) {
3364
+ // Raw keypress — single character
3365
+ const { emitKeypressEvents } = await import('node:readline');
3366
+ emitKeypressEvents(process.stdin, rl);
3367
+
3368
+ process.stdout.write(' Choice [2]: ');
3369
+ styleChoice = await new Promise((resolve) => {
3370
+ const wasRaw = process.stdin.isRaw;
3371
+ process.stdin.setRawMode(true);
3372
+
3373
+ const cleanup = () => {
3374
+ process.stdin.removeListener('keypress', onKey);
3375
+ try { process.stdin.setRawMode(wasRaw || false); } catch {}
3376
+ };
3377
+
3378
+ const onKey = (str, key) => {
3379
+ if (!key) return;
3380
+ const name = key.name || '';
3381
+ if (key.ctrl && (name === 'c' || name === 'd')) {
3382
+ cleanup();
3383
+ process.stdout.write('\n');
3384
+ resolve('2');
3385
+ return;
3386
+ }
3387
+ if (name === 'return' || name === 'enter') {
3388
+ cleanup();
3389
+ process.stdout.write('\n');
3390
+ resolve('2');
3391
+ return;
3392
+ }
3393
+ if (str === '1' || str === '2' || str === '3') {
3394
+ cleanup();
3395
+ process.stdout.write(`${str}\n`);
3396
+ resolve(str);
3397
+ return;
3398
+ }
3399
+ };
3400
+
3401
+ process.stdin.on('keypress', onKey);
3402
+ });
3403
+ } else {
3404
+ // Fallback: line-based prompt
3405
+ process.stdout.write(' Choice [2]: ');
3406
+ styleChoice = (await ask('')).trim() || '2';
3407
+ }
3408
+
2968
3409
  const chosenBias = styleMap[styleChoice] || 'balanced';
2969
3410
  const chosenName = styleNames[chosenBias];
3411
+ fx.nl();
2970
3412
 
2971
- // ── Non-blocking note if metered API detected ──────────────────────────────
3413
+ // Non-blocking note if metered API detected
2972
3414
  if (openaiReady && caps.openai.metered) {
2973
- console.log(` ${DIM}OpenAI API key detected usage is metered, guardrails enabled${RESET}`);
2974
- console.log('');
3415
+ const DIM = '\x1b[2m'; const RESET = '\x1b[0m';
3416
+ process.stdout.write(` ${DIM}OpenAI API key detected — usage is metered, guardrails enabled${RESET}\n\n`);
2975
3417
  }
2976
3418
 
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('');
3419
+ // ─── Step 6: Ready ────────────────────────────────────────────────────────
3420
+ fx.step(5, 5, 'Ready!');
3421
+ fx.nl();
3422
+
3423
+ // Init living docs
3424
+ try {
3425
+ const ld = await getLivingDocs();
3426
+ if (ld.initLivingDocs) ld.initLivingDocs(cwd);
3427
+ } catch { /* non-fatal */ }
2983
3428
 
2984
- // ── Build and return the profile object ────────────────────────────────────
3429
+ await fx.sleep(400);
3430
+ fx.celebrate(`dual-brain is ready! (${chosenName} mode)`);
3431
+ fx.nl();
3432
+ fx.info('Type anything to get started. Your AI partner is listening.');
3433
+ await fx.sleep(1200);
3434
+
3435
+ // ─── Build and return the profile object ──────────────────────────────────
2985
3436
  const finalProfile = loadProfile(cwd);
2986
3437
 
2987
3438
  finalProfile.providers.claude = { enabled: claudeReady };
@@ -4026,6 +4477,7 @@ const SCREENS = {
4026
4477
  main: mainScreen,
4027
4478
  'new-session': newSessionScreen,
4028
4479
  settings: settingsScreen,
4480
+ team: teamScreen,
4029
4481
  'import-picker': importPickerScreen,
4030
4482
  'pr-triage': prTriageScreen,
4031
4483
  subscriptions: subscriptionsScreen,
@@ -4590,6 +5042,21 @@ async function main() {
4590
5042
  }
4591
5043
 
4592
5044
  if (cmd === 'init') {
5045
+ // init --replit: run Replit-specific integration setup
5046
+ if (args.includes('--replit')) {
5047
+ const cwd = process.cwd();
5048
+ const dryRun = args.includes('--dry-run');
5049
+ try {
5050
+ const replit = await import('../src/replit.mjs');
5051
+ const report = await replit.initReplitIntegration({ dryRun, cwd });
5052
+ console.log(replit.formatReplitReport(report));
5053
+ } catch (e) {
5054
+ console.error('replit.mjs not available yet — skipping Replit init');
5055
+ if (process.env.DEBUG) console.error(e.message);
5056
+ }
5057
+ return;
5058
+ }
5059
+
4593
5060
  if (isInteractive) {
4594
5061
  // Run onboarding wizard then main screen
4595
5062
  const cwd = process.cwd();