dual-brain 0.1.19 → 0.1.20

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.
Files changed (2) hide show
  1. package/bin/dual-brain.mjs +69 -20
  2. package/package.json +1 -1
@@ -1498,10 +1498,19 @@ function buildProviderStatusLine(profile, auth) {
1498
1498
  'solo-claude': '⚡ Fast',
1499
1499
  'solo-openai': '⚡ Fast',
1500
1500
  };
1501
+ const WORK_STYLE_TIPS = {
1502
+ 'auto': 'adapts routing based on task risk',
1503
+ 'cost-saver': 'cheaper models, skips GPT for non-critical',
1504
+ 'balanced': 'smart routing, reviews on important changes',
1505
+ 'quality-first': 'dual-brain for medium+ risk, stricter reviews',
1506
+ 'solo-claude': 'Claude only, no GPT dispatch',
1507
+ 'solo-openai': 'OpenAI only, no Claude dispatch',
1508
+ };
1501
1509
  const bias = profile?.bias || profile?.mode || 'balanced';
1502
1510
  const label = WORK_STYLE_LABELS[bias] || '⚖️ Balanced';
1511
+ const tip = WORK_STYLE_TIPS[bias] || 'smart routing, reviews on important changes';
1503
1512
 
1504
- return `${claudeDot} Claude ${openaiDot} OpenAI ${label}`;
1513
+ return `${claudeDot} Claude ${openaiDot} OpenAI ${label} — ${tip}`;
1505
1514
  }
1506
1515
 
1507
1516
  /**
@@ -1509,7 +1518,11 @@ function buildProviderStatusLine(profile, auth) {
1509
1518
  * Returns a string like: "│ content padded to W │"
1510
1519
  */
1511
1520
  function makeBoxRow(content, W) {
1512
- const plain = content.replace(/\x1b\[[0-9;]*m/g, '');
1521
+ // Strip ANSI codes, then strip zero-width variation selectors (U+FE0F etc.)
1522
+ // so that emoji like ⚖️ (U+2696+U+FE0F) don't inflate the measured length.
1523
+ const plain = content
1524
+ .replace(/\x1b\[[0-9;]*m/g, '') // ANSI color codes
1525
+ .replace(/[\uFE00-\uFE0F]/g, ''); // variation selectors (zero-width)
1513
1526
  const padding = Math.max(0, W - plain.length);
1514
1527
  return `│ ${content}${' '.repeat(padding)} │`;
1515
1528
  }
@@ -1793,32 +1806,39 @@ async function mainScreen(rl, ask) {
1793
1806
  badgeVisible.push('[stale]'.length);
1794
1807
  }
1795
1808
  const msgCount = sess.messageCount ?? sess.promptCount ?? 0;
1796
- const msgBadge = `\x1b[2m(${msgCount})\x1b[0m`;
1797
- const msgBadgeW = `(${msgCount})`.length;
1809
+ // Human-readable: "4 tasks" instead of "(4)"
1810
+ const taskLabel = msgCount === 1 ? '1 task' : `${msgCount} tasks`;
1811
+ const taskBadge = `\x1b[2m${taskLabel}\x1b[0m`;
1812
+ const taskBadgeW = taskLabel.length;
1798
1813
 
1799
1814
  const badgeStr = badges.join('');
1800
1815
  const badgesW = badgeVisible.reduce((s, n) => s + n, 0);
1801
1816
 
1802
- // Layout: "{num} {name...}{badges} {age} {msg}"
1817
+ // Layout: "{num} {name...}{badges} {age} {tasks}"
1818
+ // Use basename for name — strip full paths for readability
1819
+ const displayName = rawName.startsWith('/')
1820
+ ? rawName.split('/').filter(Boolean).pop() || rawName
1821
+ : rawName;
1822
+
1803
1823
  const numStr = String(i + 1);
1804
1824
  const ageStr = sess.age || '';
1805
- // Available for name: W minus fixed chrome, badge widths, and msg badge
1806
- const nameMax = W - numStr.length - 2 - badgesW - 2 - ageStr.length - 2 - msgBadgeW;
1807
- const truncName = rawName.length > nameMax
1808
- ? rawName.slice(0, Math.max(0, nameMax - 3)) + '...'
1809
- : rawName.padEnd(nameMax);
1810
- const content = `${numStr} ${truncName}${badgeStr} ${ageStr} ${msgBadge}`;
1825
+ // Available for name: W minus fixed chrome, badge widths, and task badge
1826
+ const nameMax = W - numStr.length - 2 - badgesW - 2 - ageStr.length - 2 - taskBadgeW;
1827
+ const truncName = displayName.length > nameMax
1828
+ ? displayName.slice(0, Math.max(0, nameMax - 3)) + '...'
1829
+ : displayName.padEnd(nameMax);
1830
+ const content = `${numStr} ${truncName}${badgeStr} ${ageStr} ${taskBadge}`;
1811
1831
  sessionRows.push(row(content));
1812
1832
  });
1813
1833
  }
1814
1834
 
1815
- // ── Actions bar ───────────────────────────────────────────────────────────
1816
- const actionsBase = ' Resume n New / Search i Import s Settings q Quit';
1817
- const actionsContent = openPRs.length > 0 ? `${actionsBase} p PRs` : actionsBase;
1835
+ // ── Actions bar — four product verbs first, then navigation ────────────────
1836
+ const actionsContent = 'd Do p Plan r Review s Ship │ n New / Search q Quit';
1818
1837
  const actionsRow = row(actionsContent);
1819
1838
 
1820
1839
  // ── Print the full box ────────────────────────────────────────────────────
1821
1840
  // Include action cards between status and sessions (with separators only when non-empty)
1841
+ const poweredByRow = row('\x1b[2mPowered by data-tools · Steve Moraco\x1b[0m');
1822
1842
  const lines = [
1823
1843
  top,
1824
1844
  ...statusRows,
@@ -1827,6 +1847,8 @@ async function mainScreen(rl, ask) {
1827
1847
  ...sessionRows,
1828
1848
  sep,
1829
1849
  actionsRow,
1850
+ sep,
1851
+ poweredByRow,
1830
1852
  bot,
1831
1853
  ];
1832
1854
  // ── Stale session hint ──────────────────────────────────────────────────
@@ -1834,8 +1856,7 @@ async function mainScreen(rl, ask) {
1834
1856
  process.stdout.write(`\x1b[2m${staleCount} stale sessions (>7d) — press s → archive to clean up\x1b[0m\n`);
1835
1857
  }
1836
1858
 
1837
- process.stdout.write(lines.join('\n') + '\n');
1838
- process.stdout.write(`\x1b[2mPowered by data-tools · Steve Moraco\x1b[0m\n\n`);
1859
+ process.stdout.write(lines.join('\n') + '\n\n');
1839
1860
 
1840
1861
  // ── Key handling ──────────────────────────────────────────────────────────
1841
1862
  // Use raw keypress mode so we can show a live type-to-start buffer.
@@ -1922,8 +1943,7 @@ async function mainScreen(rl, ask) {
1922
1943
  // Single-key commands only fire when buffer is empty
1923
1944
  if (taskBuffer.length === 0) {
1924
1945
  const lower = str.toLowerCase();
1925
- const singleKeySet = new Set(['n', 's', 'q', '/', 'i']);
1926
- if (lower === 'p' && openPRs.length > 0) singleKeySet.add('p');
1946
+ const singleKeySet = new Set(['n', 's', 'q', '/', 'i', 'd', 'p', 'r']);
1927
1947
  if (singleKeySet.has(lower)) {
1928
1948
  cleanup();
1929
1949
  process.stdout.write('\n');
@@ -1992,6 +2012,37 @@ async function mainScreen(rl, ask) {
1992
2012
 
1993
2013
  if (choice === 'n') { return { next: 'new-session' }; }
1994
2014
 
2015
+ // Four product verbs
2016
+ if (choice === 'd') {
2017
+ // "Do" — prompt user for a task description, then dispatch
2018
+ const prompt = (await ask(' What do you want to do? ')).trim();
2019
+ if (!prompt) return { next: 'main' };
2020
+ return { next: 'go', prompt };
2021
+ }
2022
+
2023
+ if (choice === 'p') {
2024
+ // "Plan" — dry-run routing for a task
2025
+ const prompt = (await ask(' Describe the task to plan: ')).trim();
2026
+ if (!prompt) return { next: 'main' };
2027
+ return { next: 'go', prompt, dryRun: true };
2028
+ }
2029
+
2030
+ if (choice === 'r') {
2031
+ // "Review" — dual-brain review current diff
2032
+ const { spawnSync } = await import('node:child_process');
2033
+ process.stdout.write('\n Running dual-brain review...\n\n');
2034
+ spawnSync('node', ['.claude/hooks/dual-brain-review.mjs'], { stdio: 'inherit', cwd });
2035
+ return { next: 'main' };
2036
+ }
2037
+
2038
+ if (choice === 's') {
2039
+ // "Ship" — run quality gate then prompt for commit/PR
2040
+ const { spawnSync } = await import('node:child_process');
2041
+ process.stdout.write('\n Running quality gate + ship flow...\n\n');
2042
+ spawnSync('node', ['.claude/hooks/quality-gate.mjs'], { stdio: 'inherit', cwd });
2043
+ return { next: 'main' };
2044
+ }
2045
+
1995
2046
  if (choice === '/') {
1996
2047
  const query = (await ask(' Search: ')).trim();
1997
2048
  if (!query) return { next: 'main' };
@@ -2028,9 +2079,7 @@ async function mainScreen(rl, ask) {
2028
2079
  return { next: 'main' };
2029
2080
  }
2030
2081
 
2031
- if (choice === 's') { return { next: 'settings' }; }
2032
2082
  if (choice === 'i') { return { next: 'import-picker' }; }
2033
- if (choice === 'p' && openPRs.length > 0) { return { next: 'pr-triage', openPRs }; }
2034
2083
  if (choice === 'q' || choice === 'exit') { return { next: 'exit' }; }
2035
2084
 
2036
2085
  return { next: 'main' };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dual-brain",
3
- "version": "0.1.19",
3
+ "version": "0.1.20",
4
4
  "description": "AI orchestration across Claude + OpenAI subscriptions — smart routing, budget awareness, and dual-brain collaboration",
5
5
  "type": "module",
6
6
  "bin": {