dual-brain 7.1.23 → 7.1.25

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,14 @@ 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
+
57
65
  // ─── Helpers ─────────────────────────────────────────────────────────────────
58
66
 
59
67
  const __dirname = dirname(fileURLToPath(import.meta.url));
@@ -355,6 +363,12 @@ async function cmdGo(args, opts = {}) {
355
363
  const cwd = process.cwd();
356
364
  await ensureProfile(cwd);
357
365
 
366
+ // ── Living docs: ensure .dual-brain/ exists on session start ─────────────
367
+ try {
368
+ const ld = await getLivingDocs();
369
+ if (ld.initLivingDocs) ld.initLivingDocs(cwd);
370
+ } catch { /* non-fatal */ }
371
+
358
372
  if (verbose) console.log('\nDispatching...');
359
373
 
360
374
  // ── Failure memory: check history before dispatching ──────────────────────
@@ -408,6 +422,16 @@ async function cmdGo(args, opts = {}) {
408
422
  nextAction: null,
409
423
  }, cwd);
410
424
 
425
+ // ── Living docs: record completed session action ───────────────────────
426
+ try {
427
+ const ld = await getLivingDocs();
428
+ if (ld.appendAction) ld.appendAction({
429
+ type: 'task', intent: prompt, status: 'completed',
430
+ owner: plan?._decision?.provider ?? 'claude',
431
+ files, result: result.consensus || 'dual-brain complete',
432
+ }, cwd);
433
+ } catch { /* non-fatal */ }
434
+
411
435
  // Clear failure memory on success
412
436
  if (failureMem.clearFailures) {
413
437
  try { await failureMem.clearFailures(prompt, cwd); } catch { /* non-fatal */ }
@@ -459,6 +483,17 @@ async function cmdGo(args, opts = {}) {
459
483
  nextAction: null,
460
484
  }, cwd);
461
485
 
486
+ // ── Living docs: record completed session action ───────────────────────
487
+ try {
488
+ const ld = await getLivingDocs();
489
+ if (ld.appendAction) ld.appendAction({
490
+ type: 'task', intent: prompt, status: succeeded ? 'completed' : 'failed',
491
+ owner: plan?._decision?.provider ?? 'claude',
492
+ files: result.filesChanged || files,
493
+ result: result.summary || (succeeded ? 'completed' : `exit ${result.exitCode}`),
494
+ }, cwd);
495
+ } catch { /* non-fatal */ }
496
+
462
497
  if (!succeeded) {
463
498
  // Record failure memory
464
499
  if (failureMem.recordFailure) {
@@ -1007,14 +1042,14 @@ async function welcomeScreen(rl, ask) {
1007
1042
  }
1008
1043
  console.log('');
1009
1044
 
1010
- // --- Detect data-tools / replit-tools sessions ---
1045
+ // --- Detect replit-tools sessions ---
1011
1046
  const env = detectEnvironment();
1012
1047
  const existingSessions = importReplitSessions(cwd);
1013
1048
  if (env.hasReplitTools) {
1014
- detectedLines.push(` data-tools detected`);
1049
+ detectedLines.push(` replit-tools detected`);
1015
1050
  }
1016
1051
  if (existingSessions.length > 0) {
1017
- detectedLines.push(` ${existingSessions.length} session${existingSessions.length !== 1 ? 's' : ''} found from data-tools`);
1052
+ detectedLines.push(` ${existingSessions.length} session${existingSessions.length !== 1 ? 's' : ''} found from replit-tools`);
1018
1053
  }
1019
1054
 
1020
1055
  // --- Detect replit-tools ---
@@ -1054,7 +1089,7 @@ async function welcomeScreen(rl, ask) {
1054
1089
  console.log(' [Enter] Save and go');
1055
1090
  console.log(' [c] Customize work style');
1056
1091
  if (existingSessions.length > 0) {
1057
- console.log(` [i] Import ${existingSessions.length} session${existingSessions.length !== 1 ? 's' : ''} from data-tools`);
1092
+ console.log(` [i] Import ${existingSessions.length} session${existingSessions.length !== 1 ? 's' : ''} from replit-tools`);
1058
1093
  }
1059
1094
  if (!rt.installed) {
1060
1095
  console.log('');
@@ -1066,7 +1101,7 @@ async function welcomeScreen(rl, ask) {
1066
1101
  const choice = (await ask(' Choice: ')).trim().toLowerCase();
1067
1102
 
1068
1103
  if (choice === 'i' && existingSessions.length > 0) {
1069
- console.log(`\n Importing ${existingSessions.length} sessions from data-tools...\n`);
1104
+ console.log(`\n Importing ${existingSessions.length} sessions from replit-tools...\n`);
1070
1105
  const recent = existingSessions.slice(0, 5);
1071
1106
  for (const sess of recent) {
1072
1107
  console.log(` ${sess.age.padEnd(6)} ${sess.name}`);
@@ -1483,9 +1518,9 @@ function detectInterruptedWork(sessions, cwd) {
1483
1518
  * Shows: "● Claude ● OpenAI ⚖️ Balanced"
1484
1519
  * Uses ANSI color codes for the dots — no dollar amounts or usage bars.
1485
1520
  */
1486
- function buildProviderStatusLine(profile, auth, maxWidth = 54) {
1487
- const GREEN = '●';
1488
- const RED = '●';
1521
+ function buildProviderStatusLine(profile, auth) {
1522
+ const GREEN = '\x1b[32m●\x1b[0m';
1523
+ const RED = '\x1b[31m●\x1b[0m';
1489
1524
 
1490
1525
  const claudeDot = auth.claude.found ? GREEN : RED;
1491
1526
  const openaiDot = auth.openai.found ? GREEN : RED;
@@ -1498,30 +1533,11 @@ function buildProviderStatusLine(profile, auth, maxWidth = 54) {
1498
1533
  'solo-claude': '⚡ Fast',
1499
1534
  'solo-openai': '⚡ Fast',
1500
1535
  };
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
1536
  const bias = profile?.bias || profile?.mode || 'balanced';
1510
1537
  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
1538
 
1521
- const suffix = tip ? ` — ${tip}` : '';
1522
- return `${claudeDot} Claude ${openaiDot} OpenAI ${label}${suffix}`;
1539
+ return `${claudeDot} Claude ${openaiDot} OpenAI ${label}`;
1523
1540
  }
1524
-
1525
1541
  /**
1526
1542
  * Render a box row padded to inner width W (stripping ANSI for length calculation).
1527
1543
  * Returns a string like: "│ content padded to W │"
@@ -1591,7 +1607,7 @@ async function mainScreen(rl, ask) {
1591
1607
  return ageMs >= 7 * 86400000;
1592
1608
  }).length;
1593
1609
 
1594
- // Detect data-tools version
1610
+ // Detect replit-tools version
1595
1611
  const rtMain = detectReplitTools(cwd);
1596
1612
  const dtVersion = (rtMain.installed && rtMain.version) ? rtMain.version : null;
1597
1613
 
@@ -1609,25 +1625,6 @@ async function mainScreen(rl, ask) {
1609
1625
 
1610
1626
  const row = (content) => makeBoxRow(content, W);
1611
1627
 
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
1628
  // ── Continuation card (interrupted work) ─────────────────────────────────
1632
1629
  if (interrupted) {
1633
1630
  const ctop = `┌${'─'.repeat(boxW - 2)}┐`;
@@ -1706,15 +1703,74 @@ async function mainScreen(rl, ask) {
1706
1703
  // 's' → fall through to normal dashboard
1707
1704
  }
1708
1705
 
1709
- // ── Status section ────────────────────────────────────────────────────────
1710
- const providerLine = buildProviderStatusLine(profile, auth, W);
1706
+ // ── Box 1 — Header row data ─────────────────────────────────────────────
1707
+ const providerLine = buildProviderStatusLine(profile, auth);
1711
1708
 
1712
- const statusRows = [row(providerLine)];
1713
- if (dtVersion) {
1714
- statusRows.push(row(`\x1b[2m📦 replit-tools v${dtVersion}\x1b[0m`));
1709
+ // ── Box 2 — Workspace: gather git data ───────────────────────────────────
1710
+ let gitBranch = 'unknown';
1711
+ let gitUncommitted = 0;
1712
+ let gitAheadCount = 0;
1713
+ let gitLastMsg = '';
1714
+ let gitLastAgo = '';
1715
+
1716
+ try {
1717
+ gitBranch = execSync('git rev-parse --abbrev-ref HEAD 2>/dev/null', {
1718
+ cwd, encoding: 'utf8', timeout: 2000, stdio: 'pipe',
1719
+ }).trim() || 'unknown';
1720
+ } catch {}
1721
+
1722
+ try {
1723
+ const status = execSync('git status --porcelain 2>/dev/null', {
1724
+ cwd, encoding: 'utf8', timeout: 2000, stdio: 'pipe',
1725
+ });
1726
+ gitUncommitted = status.trim().split('\n').filter(Boolean).length;
1727
+ } catch {}
1728
+
1729
+ try {
1730
+ const aheadOut = execSync('git rev-list @{u}..HEAD 2>/dev/null | wc -l', {
1731
+ cwd, encoding: 'utf8', timeout: 2000, stdio: 'pipe',
1732
+ });
1733
+ gitAheadCount = parseInt(aheadOut.trim(), 10) || 0;
1734
+ } catch {}
1735
+
1736
+ try {
1737
+ const logOut = execSync('git log -1 --format="%s|%ct" 2>/dev/null', {
1738
+ cwd, encoding: 'utf8', timeout: 2000, stdio: 'pipe',
1739
+ }).trim();
1740
+ if (logOut) {
1741
+ const [msg, ts] = logOut.split('|');
1742
+ gitLastMsg = (msg || '').slice(0, 38);
1743
+ const ageMs = Date.now() - (parseInt(ts, 10) * 1000);
1744
+ const ageMin = Math.floor(ageMs / 60000);
1745
+ if (ageMin < 60) gitLastAgo = `${ageMin}m ago`;
1746
+ else if (ageMin < 1440) gitLastAgo = `${Math.floor(ageMin / 60)}h ago`;
1747
+ else gitLastAgo = `${Math.floor(ageMin / 1440)}d ago`;
1748
+ }
1749
+ } catch {}
1750
+
1751
+ // ── Box 2 rows ────────────────────────────────────────────────────────────
1752
+ const uncommittedPart = gitUncommitted > 0 ? ` · ${gitUncommitted} uncommitted` : '';
1753
+ const aheadPart = gitAheadCount > 0 ? ` · ${gitAheadCount} ahead` : '';
1754
+ const workspaceLine1 = `${gitBranch}${uncommittedPart}${aheadPart}`;
1755
+ const workspaceLine2 = gitLastMsg
1756
+ ? `Last: ${gitLastMsg} (${gitLastAgo})`
1757
+ : '';
1758
+
1759
+ // Open PRs
1760
+ const repoState = detectRepoState(cwd);
1761
+ const openPRs = await detectOpenPRs(cwd);
1762
+
1763
+ const workspaceRows = [row(workspaceLine1)];
1764
+ if (workspaceLine2) workspaceRows.push(row(workspaceLine2));
1765
+ if (openPRs.length > 0) {
1766
+ workspaceRows.push(row(`${openPRs.length} open PR${openPRs.length === 1 ? '' : 's'}`));
1715
1767
  }
1716
1768
 
1717
- // ── Observer observations (top 2, high priority first) ───────────────────
1769
+ // ── Box 3 Awareness: observer + roadmap + risk ──────────────────────────
1770
+ let awarenessLine1 = '\x1b[2m💡\x1b[0m Ready to work';
1771
+ let awarenessLine2 = '\x1b[2m📋 No roadmap yet\x1b[0m';
1772
+ let awarenessLine3 = '\x1b[32m✓\x1b[0m No risk flags';
1773
+
1718
1774
  let quickObservations = [];
1719
1775
  try {
1720
1776
  const observerMod = await import('../src/observer.mjs');
@@ -1724,64 +1780,39 @@ async function mainScreen(rl, ask) {
1724
1780
  const sorted = [...quickState.observations].sort(
1725
1781
  (a, b) => (PRIO[a.priority] ?? 2) - (PRIO[b.priority] ?? 2)
1726
1782
  );
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}`));
1783
+ quickObservations = sorted.slice(0, 3);
1784
+ const top = quickObservations[0];
1785
+ if (top) {
1786
+ const prefix = top.priority === 'high' ? '🔴' : top.priority === 'medium' ? '🟡' : '\x1b[2m💡\x1b[0m';
1787
+ awarenessLine1 = `${prefix} ${top.message}`;
1788
+ }
1789
+ const hasHighRisk = quickObservations.some(o => o.priority === 'high');
1790
+ if (hasHighRisk) {
1791
+ awarenessLine3 = '\x1b[31m⚠\x1b[0m Risk flags detected — run: dual-brain review';
1734
1792
  }
1735
1793
  }
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}`));
1748
- }
1794
+ } catch { /* non-fatal — observer may not exist */ }
1749
1795
 
1750
- // ── Related sessions hint (only when no continuation card is showing) ─────
1751
- if (!interrupted && recentSessions.length > 0) {
1752
- 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}`));
1796
+ // Try roadmap file
1797
+ try {
1798
+ const roadmapPath = join(cwd, '.dual-brain', 'roadmap.md');
1799
+ if (existsSync(roadmapPath)) {
1800
+ const roadmapText = readFileSync(roadmapPath, 'utf8');
1801
+ const lines = roadmapText.split('\n').filter(Boolean);
1802
+ // Skip heading lines, grab first non-heading line
1803
+ const firstItem = lines.find(l => !l.startsWith('#') && l.trim().length > 0);
1804
+ if (firstItem) {
1805
+ const clean = firstItem.replace(/^[-*>]+\s*/, '').trim().slice(0, 45);
1806
+ awarenessLine2 = `\x1b[2m📋\x1b[0m ${clean}`;
1781
1807
  }
1782
- } catch { /* non-fatal */ }
1783
- }
1784
- // ── End related sessions hint ─────────────────────────────────────────────
1808
+ }
1809
+ } catch { /* non-fatal */ }
1810
+
1811
+ const awarenessRows = [
1812
+ row(awarenessLine1),
1813
+ row(awarenessLine2),
1814
+ row(awarenessLine3),
1815
+ ];
1785
1816
 
1786
1817
  // ── Sessions section ──────────────────────────────────────────────────────
1787
1818
  const sessionRows = [];
@@ -1837,23 +1868,28 @@ async function mainScreen(rl, ask) {
1837
1868
  });
1838
1869
  }
1839
1870
 
1840
- // ── Actions barnavigation only (pipeline verbs are internal stages, not menu items) ─
1841
- const actionsContent = 'n New session / Search q Quit';
1871
+ // ── Box 5Input bar ──────────────────────────────────────────────────
1872
+ const actionsContent = '> type anything... [s] settings [t] team [q] quit';
1842
1873
  const actionsRow = row(actionsContent);
1843
1874
 
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');
1875
+ // ── Print the full 5-box layout ───────────────────────────────────────────
1876
+ // Box 1: header (title + provider dots + work style)
1877
+ // Box 2: workspace (branch · uncommitted · ahead, last commit, open PRs)
1878
+ // Box 3: awareness (observer, roadmap, risk)
1879
+ // Box 4: sessions
1880
+ // Box 5: input bar
1847
1881
  const lines = [
1848
1882
  top,
1849
- ...statusRows,
1850
- ...(actionRows.length > 0 ? [sep, ...actionRows] : []),
1883
+ row(`🧠 dual-brain v${version}`),
1884
+ row(providerLine),
1885
+ sep,
1886
+ ...workspaceRows,
1887
+ sep,
1888
+ ...awarenessRows,
1851
1889
  sep,
1852
1890
  ...sessionRows,
1853
1891
  sep,
1854
1892
  actionsRow,
1855
- sep,
1856
- poweredByRow,
1857
1893
  bot,
1858
1894
  ];
1859
1895
  // ── Stale session hint ──────────────────────────────────────────────────
@@ -1948,7 +1984,7 @@ async function mainScreen(rl, ask) {
1948
1984
  // Single-key commands only fire when buffer is empty
1949
1985
  if (taskBuffer.length === 0) {
1950
1986
  const lower = str.toLowerCase();
1951
- const singleKeySet = new Set(['n', 's', 'q', '/', 'i']);
1987
+ const singleKeySet = new Set(['n', 's', 't', 'q', '/', 'i']);
1952
1988
  if (singleKeySet.has(lower)) {
1953
1989
  cleanup();
1954
1990
  process.stdout.write('\n');
@@ -2054,6 +2090,7 @@ async function mainScreen(rl, ask) {
2054
2090
  }
2055
2091
 
2056
2092
  if (choice === 's') { return { next: 'settings' }; }
2093
+ if (choice === 't') { return { next: 'team' }; }
2057
2094
  if (choice === 'i') { return { next: 'import-picker' }; }
2058
2095
  if (choice === 'q' || choice === 'exit') { return { next: 'exit' }; }
2059
2096
 
@@ -2078,7 +2115,7 @@ async function newSessionScreen(rl, ask) {
2078
2115
  async function importPickerScreen() {
2079
2116
  const cwd = process.cwd();
2080
2117
 
2081
- // Load all available sessions from data-tools
2118
+ // Load all available sessions from replit-tools
2082
2119
  const allSessions = importReplitSessions(cwd);
2083
2120
 
2084
2121
  // Load existing session meta to filter already-imported ones
@@ -2124,9 +2161,9 @@ async function importPickerScreen() {
2124
2161
  if (allSessions.length === 0) {
2125
2162
  process.stdout.write('\n');
2126
2163
  process.stdout.write(top + '\n');
2127
- process.stdout.write(row('Import from data-tools') + '\n');
2164
+ process.stdout.write(row('Import from replit-tools') + '\n');
2128
2165
  process.stdout.write(sep + '\n');
2129
- process.stdout.write(row('No data-tools sessions found.') + '\n');
2166
+ process.stdout.write(row('No replit-tools sessions found.') + '\n');
2130
2167
  process.stdout.write(row('Install replit-tools: npm i -g replit-tools') + '\n');
2131
2168
  process.stdout.write(sep + '\n');
2132
2169
  process.stdout.write(row('Press any key to go back...') + '\n');
@@ -2138,7 +2175,7 @@ async function importPickerScreen() {
2138
2175
  if (candidates.length === 0) {
2139
2176
  process.stdout.write('\n');
2140
2177
  process.stdout.write(top + '\n');
2141
- process.stdout.write(row('Import from data-tools') + '\n');
2178
+ process.stdout.write(row('Import from replit-tools') + '\n');
2142
2179
  process.stdout.write(sep + '\n');
2143
2180
  process.stdout.write(row(`All ${allSessions.length} sessions already imported.`) + '\n');
2144
2181
  process.stdout.write(sep + '\n');
@@ -2161,7 +2198,7 @@ async function importPickerScreen() {
2161
2198
  const renderPicker = () => {
2162
2199
  process.stdout.write('\x1b[2J\x1b[H'); // clear screen
2163
2200
 
2164
- const headerTitle = 'Import from data-tools';
2201
+ const headerTitle = 'Import from replit-tools';
2165
2202
  const footerLine = '↑↓ Navigate Space Toggle Enter Import q Back';
2166
2203
 
2167
2204
  process.stdout.write('\n');
@@ -2298,7 +2335,7 @@ async function importPickerScreen() {
2298
2335
  }
2299
2336
  saveSessionMeta(updatedMeta, cwd);
2300
2337
 
2301
- process.stdout.write(`✓ Imported ${importCount} session${importCount !== 1 ? 's' : ''} from data-tools\n\n`);
2338
+ process.stdout.write(`✓ Imported ${importCount} session${importCount !== 1 ? 's' : ''} from replit-tools\n\n`);
2302
2339
 
2303
2340
  return { next: 'main' };
2304
2341
  }
@@ -2560,22 +2597,60 @@ async function settingsScreen(rl, ask) {
2560
2597
  'balanced': '⚖️ Balanced',
2561
2598
  'quality-first': '🔥 Full Power',
2562
2599
  };
2563
- const workStyleLabel = WORK_STYLE_DISPLAY[currentBias] || '⚖️ Balanced';
2600
+
2601
+ // Work style current markers
2602
+ const _stIsFast = ['cost-saver', 'auto', 'solo-claude', 'solo-openai'].includes(currentBias);
2603
+ const _stIsBal = currentBias === 'balanced';
2604
+ const _stIsFull = currentBias === 'quality-first';
2605
+ const _stMark = (active) => active ? ' ← current' : '';
2606
+
2607
+ // Provider status dots
2608
+ const _stAuth = await detectAuth();
2609
+ const _stGDOT = '\x1b[32m●\x1b[0m';
2610
+ const _stRDOT = '\x1b[31m●\x1b[0m';
2611
+ const _stClDot = _stAuth.claude.found ? _stGDOT : _stRDOT;
2612
+ const _stOaDot = _stAuth.openai.found ? _stGDOT : _stRDOT;
2613
+ const _stClStatus = _stAuth.claude.found ? 'connected' : 'not connected';
2614
+ const _stOaStatus = _stAuth.openai.found ? 'connected' : 'not connected';
2615
+
2616
+ // Calibration from project.json
2617
+ let _stCal = { specificity: 3, corrections: 3, autonomy: 3 };
2618
+ let _stLevel = 'intermediate';
2619
+ let _stStyle = 'normal';
2620
+ try {
2621
+ const _stLd = await import('../src/living-docs.mjs');
2622
+ const _stCm = await import('../src/calibration.mjs');
2623
+ const _stPs = _stLd.getProjectState(cwd);
2624
+ if (_stPs?.project?.userCalibration) _stCal = _stPs.project.userCalibration;
2625
+ const _stAd = _stCm.getAdaptation(_stCal);
2626
+ _stLevel = _stAd.userLevel;
2627
+ _stStyle = _stAd.responseStyle;
2628
+ } catch { /* non-fatal */ }
2629
+
2630
+ const _stS = typeof _stCal.specificity === 'number' ? _stCal.specificity.toFixed(1) : String(_stCal.specificity ?? 3);
2631
+ const _stC = typeof _stCal.corrections === 'number' ? _stCal.corrections.toFixed(1) : String(_stCal.corrections ?? 3);
2632
+ const _stA = typeof _stCal.autonomy === 'number' ? _stCal.autonomy.toFixed(1) : String(_stCal.autonomy ?? 3);
2564
2633
 
2565
2634
  const lines = [
2566
2635
  top,
2567
2636
  row('Settings'),
2568
2637
  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'),
2638
+ row('Work Style'),
2639
+ row(` [1] Fast — speed over caution${_stMark(_stIsFast)}`),
2640
+ row(` [2] Balanced — smart routing, reviews on important${_stMark(_stIsBal)}`),
2641
+ row(` [3] Full Power — dual-brain everything, max quality${_stMark(_stIsFull)}`),
2642
+ sep,
2643
+ row('Providers'),
2644
+ row(` Claude: ${_stClDot} ${_stClStatus}`),
2645
+ row(` OpenAI: ${_stOaDot} ${_stOaStatus}`),
2646
+ sep,
2647
+ row('User Calibration'),
2648
+ row(` Specificity: ${_stS} Corrections: ${_stC} Autonomy: ${_stA}`),
2649
+ row(` Level: ${_stLevel} · Style: ${_stStyle}`),
2650
+ sep,
2651
+ row('[1-3] change style [r] reset calibration [b] back'),
2652
+ row('[m] subscriptions [e] sessions [x] diagnostics'),
2576
2653
  ...(settingsPRs.length > 0 ? [row(`[p] PR triage (${settingsPRs.length} open)`)] : []),
2577
- row(''),
2578
- row('[Esc/b] Back to dashboard'),
2579
2654
  bot,
2580
2655
  ];
2581
2656
  process.stdout.write('\n' + lines.join('\n') + '\n\n');
@@ -2583,45 +2658,10 @@ async function settingsScreen(rl, ask) {
2583
2658
  const raw = (await ask(' Choice: ')).trim();
2584
2659
  const choice = raw.toLowerCase();
2585
2660
 
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];
2661
+ // Direct work style keys 1/2/3
2662
+ if (choice === '1' || choice === '2' || choice === '3') {
2663
+ const _stWsMap = { '1': 'cost-saver', '2': 'balanced', '3': 'quality-first' };
2664
+ const newBias = _stWsMap[choice];
2625
2665
  if (newBias && newBias !== currentBias) {
2626
2666
  profile.bias = newBias;
2627
2667
  const enabledCount = [
@@ -2631,12 +2671,23 @@ async function settingsScreen(rl, ask) {
2631
2671
  if (enabledCount >= 2) profile.mode = newBias;
2632
2672
  saveProfile(profile, { cwd });
2633
2673
  const newLabel = WORK_STYLE_DISPLAY[newBias] || newBias;
2634
- console.log(`\n Work style set to ${newLabel}\n`);
2674
+ process.stdout.write(`\n Work style set to ${newLabel}\n\n`);
2635
2675
  await ask(' Press Enter to continue...');
2636
2676
  }
2637
2677
  return { next: 'settings' };
2638
2678
  }
2639
2679
 
2680
+ // Reset calibration to defaults
2681
+ if (choice === 'r') {
2682
+ try {
2683
+ const _stLdReset = await import('../src/living-docs.mjs');
2684
+ _stLdReset.updateProject({ userCalibration: { specificity: 3, corrections: 3, autonomy: 3 } }, cwd);
2685
+ process.stdout.write('\n Calibration reset to defaults.\n\n');
2686
+ await ask(' Press Enter to continue...');
2687
+ } catch { /* non-fatal */ }
2688
+ return { next: 'settings' };
2689
+ }
2690
+
2640
2691
  if (choice === 'm') { return { next: 'subscriptions' }; }
2641
2692
 
2642
2693
  if (choice === 'e') { return { next: 'sessions' }; }
@@ -2655,7 +2706,7 @@ async function settingsScreen(rl, ask) {
2655
2706
  if (which.status === 0) {
2656
2707
  spawnSync('claude-menu', { stdio: 'inherit' });
2657
2708
  } else {
2658
- process.stdout.write('\n data-tools not found — install with: npm i -g replit-tools\n\n');
2709
+ process.stdout.write('\n replit-tools not found — install with: npm i -g replit-tools\n\n');
2659
2710
  await ask(' Press Enter to continue...');
2660
2711
  }
2661
2712
  return { next: 'settings' };
@@ -2689,6 +2740,105 @@ async function settingsScreen(rl, ask) {
2689
2740
  return { next: 'main' };
2690
2741
  }
2691
2742
 
2743
+ // ─── Screen: teamScreen ───────────────────────────────────────────────────────
2744
+
2745
+ async function teamScreen(rl, ask) {
2746
+ const cwd = process.cwd();
2747
+
2748
+ // Box layout matching dashboard
2749
+ const termW = process.stdout.columns || 60;
2750
+ const boxW = Math.min(termW - 2, 60);
2751
+ const W = boxW - 4;
2752
+
2753
+ const top = `┌${'─'.repeat(boxW - 2)}┐`;
2754
+ const sep = `├${'─'.repeat(boxW - 2)}┤`;
2755
+ const bot = `└${'─'.repeat(boxW - 2)}┘`;
2756
+ const row = (content) => makeBoxRow(content, W);
2757
+
2758
+ // Load team from project.json
2759
+ let team = [];
2760
+ let sharedSessions = 0;
2761
+ let teamDecisions = 0;
2762
+ try {
2763
+ const _tmLd = await import('../src/living-docs.mjs');
2764
+ const _tmPs = _tmLd.getProjectState(cwd);
2765
+ if (Array.isArray(_tmPs?.project?.team)) {
2766
+ team = _tmPs.project.team;
2767
+ }
2768
+ // Count decisions with more than one participant as team decisions
2769
+ if (Array.isArray(_tmPs?.recentDecisions)) {
2770
+ teamDecisions = _tmPs.recentDecisions.filter(
2771
+ d => Array.isArray(d?.participants) && d.participants.length > 1
2772
+ ).length;
2773
+ }
2774
+ } catch { /* non-fatal */ }
2775
+
2776
+ // Fall back to git user if no team configured
2777
+ let ownerName = '(you)';
2778
+ if (team.length === 0) {
2779
+ try {
2780
+ const { execSync: _tmExec } = await import('node:child_process');
2781
+ const gitUser = _tmExec('git config user.name 2>/dev/null', {
2782
+ encoding: 'utf8', timeout: 2000, stdio: 'pipe',
2783
+ }).trim();
2784
+ if (gitUser) ownerName = gitUser;
2785
+ } catch { /* non-fatal */ }
2786
+ }
2787
+
2788
+ const memberRows = [];
2789
+ if (team.length === 0) {
2790
+ memberRows.push(row(` ${ownerName} (owner)`));
2791
+ } else {
2792
+ for (const member of team) {
2793
+ const role = member.role || 'member';
2794
+ memberRows.push(row(` ${member.name} (${role})`));
2795
+ }
2796
+ }
2797
+
2798
+ const lines = [
2799
+ top,
2800
+ row('Team'),
2801
+ sep,
2802
+ row('Members'),
2803
+ ...memberRows,
2804
+ sep,
2805
+ row(`Shared Sessions: ${sharedSessions}`),
2806
+ row(`Team decisions: ${teamDecisions}`),
2807
+ sep,
2808
+ row('[a] add member [b] back'),
2809
+ bot,
2810
+ ];
2811
+ process.stdout.write('\n' + lines.join('\n') + '\n\n');
2812
+
2813
+ const raw = (await ask(' Choice: ')).trim();
2814
+ const choice = raw.toLowerCase();
2815
+
2816
+ if (choice === 'a') {
2817
+ const name = (await ask(' Member name: ')).trim();
2818
+ if (name) {
2819
+ try {
2820
+ const _tmLdAdd = await import('../src/living-docs.mjs');
2821
+ const _tmCur = _tmLdAdd.getProjectState(cwd);
2822
+ const _tmTeam = Array.isArray(_tmCur?.project?.team) ? [..._tmCur.project.team] : [];
2823
+ _tmTeam.push({ name, role: 'member', addedAt: new Date().toISOString() });
2824
+ _tmLdAdd.updateProject({ team: _tmTeam }, cwd);
2825
+ process.stdout.write(`\n Added ${name} to team.\n\n`);
2826
+ await ask(' Press Enter to continue...');
2827
+ } catch {
2828
+ process.stdout.write('\n Could not save team member.\n\n');
2829
+ await ask(' Press Enter to continue...');
2830
+ }
2831
+ }
2832
+ return { next: 'team' };
2833
+ }
2834
+
2835
+ if (choice === 'b' || choice === 'back' || choice === 'q' || raw === '\x1b') {
2836
+ return { next: 'main' };
2837
+ }
2838
+
2839
+ return { next: 'main' };
2840
+ }
2841
+
2692
2842
 
2693
2843
  // ─── Helper: aggregatePlans ───────────────────────────────────────────────────
2694
2844
 
@@ -4026,6 +4176,7 @@ const SCREENS = {
4026
4176
  main: mainScreen,
4027
4177
  'new-session': newSessionScreen,
4028
4178
  settings: settingsScreen,
4179
+ team: teamScreen,
4029
4180
  'import-picker': importPickerScreen,
4030
4181
  'pr-triage': prTriageScreen,
4031
4182
  subscriptions: subscriptionsScreen,