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.
- package/bin/dual-brain.mjs +326 -175
- package/package.json +6 -2
- package/src/calibration.mjs +148 -0
- package/src/ledger.mjs +196 -0
- package/src/living-docs.mjs +210 -0
- package/src/pipeline.mjs +95 -0
package/bin/dual-brain.mjs
CHANGED
|
@@ -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
|
|
1045
|
+
// --- Detect replit-tools sessions ---
|
|
1011
1046
|
const env = detectEnvironment();
|
|
1012
1047
|
const existingSessions = importReplitSessions(cwd);
|
|
1013
1048
|
if (env.hasReplitTools) {
|
|
1014
|
-
detectedLines.push(`
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
// ──
|
|
1710
|
-
const providerLine = buildProviderStatusLine(profile, auth
|
|
1706
|
+
// ── Box 1 — Header row data ─────────────────────────────────────────────
|
|
1707
|
+
const providerLine = buildProviderStatusLine(profile, auth);
|
|
1711
1708
|
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
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
|
-
// ──
|
|
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,
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
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 —
|
|
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
|
-
//
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
const
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
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
|
-
}
|
|
1783
|
-
}
|
|
1784
|
-
|
|
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
|
-
// ──
|
|
1841
|
-
const actionsContent = '
|
|
1871
|
+
// ── Box 5 — Input 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
|
-
//
|
|
1846
|
-
|
|
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
|
-
|
|
1850
|
-
|
|
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
|
|
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
|
|
2164
|
+
process.stdout.write(row('Import from replit-tools') + '\n');
|
|
2128
2165
|
process.stdout.write(sep + '\n');
|
|
2129
|
-
process.stdout.write(row('No
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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(
|
|
2570
|
-
row(
|
|
2571
|
-
row(
|
|
2572
|
-
row(
|
|
2573
|
-
|
|
2574
|
-
row('
|
|
2575
|
-
row(
|
|
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
|
-
|
|
2587
|
-
|
|
2588
|
-
const
|
|
2589
|
-
const
|
|
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
|
-
|
|
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
|
|
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,
|