dual-brain 0.1.23 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/dual-brain.mjs +673 -262
- package/package.json +16 -2
- package/src/awareness.mjs +343 -0
- package/src/calibration.mjs +148 -0
- package/src/cost-tracker.mjs +184 -0
- package/src/decide.mjs +162 -10
- package/src/dispatch.mjs +40 -2
- package/src/doctor.mjs +716 -1
- package/src/fx.mjs +276 -0
- package/src/intelligence.mjs +423 -0
- package/src/ledger.mjs +196 -0
- package/src/living-docs.mjs +210 -0
- package/src/models.mjs +363 -0
- package/src/pipeline.mjs +367 -8
- package/src/prompt-intel.mjs +325 -0
- package/src/think-engine.mjs +428 -0
package/bin/dual-brain.mjs
CHANGED
|
@@ -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)
|
|
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)
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
813
|
+
const line = ` ${label}${planStr} model=${modelClass} status=${statusStr} calls=${sess.calls} tokens=${sess.tokens}`;
|
|
814
|
+
if (fxSt) {
|
|
815
|
+
if (st.status === 'hot') fxSt.warn(line.trim());
|
|
816
|
+
else if (st.status === 'down') fxSt.error(line.trim());
|
|
817
|
+
else fxSt.success(line.trim());
|
|
818
|
+
} else {
|
|
819
|
+
console.log(line);
|
|
820
|
+
}
|
|
699
821
|
}
|
|
700
822
|
}
|
|
701
823
|
}
|
|
@@ -1007,14 +1129,14 @@ async function welcomeScreen(rl, ask) {
|
|
|
1007
1129
|
}
|
|
1008
1130
|
console.log('');
|
|
1009
1131
|
|
|
1010
|
-
// --- Detect
|
|
1132
|
+
// --- Detect replit-tools sessions ---
|
|
1011
1133
|
const env = detectEnvironment();
|
|
1012
1134
|
const existingSessions = importReplitSessions(cwd);
|
|
1013
1135
|
if (env.hasReplitTools) {
|
|
1014
|
-
detectedLines.push(`
|
|
1136
|
+
detectedLines.push(` replit-tools detected`);
|
|
1015
1137
|
}
|
|
1016
1138
|
if (existingSessions.length > 0) {
|
|
1017
|
-
detectedLines.push(` ${existingSessions.length} session${existingSessions.length !== 1 ? 's' : ''} found from
|
|
1139
|
+
detectedLines.push(` ${existingSessions.length} session${existingSessions.length !== 1 ? 's' : ''} found from replit-tools`);
|
|
1018
1140
|
}
|
|
1019
1141
|
|
|
1020
1142
|
// --- Detect replit-tools ---
|
|
@@ -1054,7 +1176,7 @@ async function welcomeScreen(rl, ask) {
|
|
|
1054
1176
|
console.log(' [Enter] Save and go');
|
|
1055
1177
|
console.log(' [c] Customize work style');
|
|
1056
1178
|
if (existingSessions.length > 0) {
|
|
1057
|
-
console.log(` [i] Import ${existingSessions.length} session${existingSessions.length !== 1 ? 's' : ''} from
|
|
1179
|
+
console.log(` [i] Import ${existingSessions.length} session${existingSessions.length !== 1 ? 's' : ''} from replit-tools`);
|
|
1058
1180
|
}
|
|
1059
1181
|
if (!rt.installed) {
|
|
1060
1182
|
console.log('');
|
|
@@ -1066,7 +1188,7 @@ async function welcomeScreen(rl, ask) {
|
|
|
1066
1188
|
const choice = (await ask(' Choice: ')).trim().toLowerCase();
|
|
1067
1189
|
|
|
1068
1190
|
if (choice === 'i' && existingSessions.length > 0) {
|
|
1069
|
-
console.log(`\n Importing ${existingSessions.length} sessions from
|
|
1191
|
+
console.log(`\n Importing ${existingSessions.length} sessions from replit-tools...\n`);
|
|
1070
1192
|
const recent = existingSessions.slice(0, 5);
|
|
1071
1193
|
for (const sess of recent) {
|
|
1072
1194
|
console.log(` ${sess.age.padEnd(6)} ${sess.name}`);
|
|
@@ -1483,12 +1605,20 @@ function detectInterruptedWork(sessions, cwd) {
|
|
|
1483
1605
|
* Shows: "● Claude ● OpenAI ⚖️ Balanced"
|
|
1484
1606
|
* Uses ANSI color codes for the dots — no dollar amounts or usage bars.
|
|
1485
1607
|
*/
|
|
1486
|
-
function buildProviderStatusLine(profile, auth,
|
|
1487
|
-
const GREEN = '
|
|
1488
|
-
const RED = '
|
|
1608
|
+
function buildProviderStatusLine(profile, auth, envReport = null) {
|
|
1609
|
+
const GREEN = '\x1b[32m●\x1b[0m';
|
|
1610
|
+
const RED = '\x1b[31m●\x1b[0m';
|
|
1611
|
+
|
|
1612
|
+
// Use envReport secrets when available; fall back to auth detection
|
|
1613
|
+
const claudeAvailable = envReport
|
|
1614
|
+
? envReport.secrets.ANTHROPIC_API_KEY || auth.claude.found
|
|
1615
|
+
: auth.claude.found;
|
|
1616
|
+
const openaiAvailable = envReport
|
|
1617
|
+
? envReport.secrets.OPENAI_API_KEY || auth.openai.found
|
|
1618
|
+
: auth.openai.found;
|
|
1489
1619
|
|
|
1490
|
-
const claudeDot =
|
|
1491
|
-
const openaiDot =
|
|
1620
|
+
const claudeDot = claudeAvailable ? GREEN : RED;
|
|
1621
|
+
const openaiDot = openaiAvailable ? GREEN : RED;
|
|
1492
1622
|
|
|
1493
1623
|
const WORK_STYLE_LABELS = {
|
|
1494
1624
|
'auto': '⚡ Fast',
|
|
@@ -1498,30 +1628,11 @@ function buildProviderStatusLine(profile, auth, maxWidth = 54) {
|
|
|
1498
1628
|
'solo-claude': '⚡ Fast',
|
|
1499
1629
|
'solo-openai': '⚡ Fast',
|
|
1500
1630
|
};
|
|
1501
|
-
const WORK_STYLE_TIPS = {
|
|
1502
|
-
'auto': 'adapts routing by task risk',
|
|
1503
|
-
'cost-saver': 'single model, minimal reviews',
|
|
1504
|
-
'balanced': 'smart routing, reviews when needed',
|
|
1505
|
-
'quality-first': 'dual-brain on everything important',
|
|
1506
|
-
'solo-claude': 'Claude only, no GPT dispatch',
|
|
1507
|
-
'solo-openai': 'OpenAI only, no Claude dispatch',
|
|
1508
|
-
};
|
|
1509
1631
|
const bias = profile?.bias || profile?.mode || 'balanced';
|
|
1510
1632
|
const label = WORK_STYLE_LABELS[bias] || '⚖️ Balanced';
|
|
1511
|
-
const fullTip = WORK_STYLE_TIPS[bias] || 'smart routing, reviews when needed';
|
|
1512
|
-
|
|
1513
|
-
// Trim tip to fit within box width (measure visible chars: strip ANSI + variation selectors)
|
|
1514
|
-
const labelPlain = label.replace(/[︀-️]/g, '').replace(/[[0-9;]*m/g, '');
|
|
1515
|
-
const prefixLen = ('● Claude ● OpenAI ' + labelPlain + ' — ').length;
|
|
1516
|
-
const tipMax = maxWidth - prefixLen;
|
|
1517
|
-
const tip = tipMax >= 6
|
|
1518
|
-
? (fullTip.length > tipMax ? fullTip.slice(0, tipMax - 1) + '…' : fullTip)
|
|
1519
|
-
: '';
|
|
1520
1633
|
|
|
1521
|
-
|
|
1522
|
-
return `${claudeDot} Claude ${openaiDot} OpenAI ${label}${suffix}`;
|
|
1634
|
+
return `${claudeDot} Claude ${openaiDot} OpenAI ${label}`;
|
|
1523
1635
|
}
|
|
1524
|
-
|
|
1525
1636
|
/**
|
|
1526
1637
|
* Render a box row padded to inner width W (stripping ANSI for length calculation).
|
|
1527
1638
|
* Returns a string like: "│ content padded to W │"
|
|
@@ -1544,6 +1655,13 @@ async function mainScreen(rl, ask) {
|
|
|
1544
1655
|
const profile = loadProfile(cwd);
|
|
1545
1656
|
const auth = await detectAuth();
|
|
1546
1657
|
|
|
1658
|
+
// ── Dashboard load animation (full mode only) ─────────────────────────────
|
|
1659
|
+
const fx = await getFx();
|
|
1660
|
+
let dashSpinner = null;
|
|
1661
|
+
if (fx && fx.getMode && fx.getMode() === 'full') {
|
|
1662
|
+
dashSpinner = fx.spinner('Loading dashboard...').start();
|
|
1663
|
+
}
|
|
1664
|
+
|
|
1547
1665
|
const claudeSub = profile?.providers?.claude;
|
|
1548
1666
|
const openaiSub = profile?.providers?.openai;
|
|
1549
1667
|
|
|
@@ -1591,7 +1709,7 @@ async function mainScreen(rl, ask) {
|
|
|
1591
1709
|
return ageMs >= 7 * 86400000;
|
|
1592
1710
|
}).length;
|
|
1593
1711
|
|
|
1594
|
-
// Detect
|
|
1712
|
+
// Detect replit-tools version
|
|
1595
1713
|
const rtMain = detectReplitTools(cwd);
|
|
1596
1714
|
const dtVersion = (rtMain.installed && rtMain.version) ? rtMain.version : null;
|
|
1597
1715
|
|
|
@@ -1609,25 +1727,6 @@ async function mainScreen(rl, ask) {
|
|
|
1609
1727
|
|
|
1610
1728
|
const row = (content) => makeBoxRow(content, W);
|
|
1611
1729
|
|
|
1612
|
-
// ── Header: one line above the box ────────────────────────────────────────
|
|
1613
|
-
process.stdout.write(`\n🧠 dual-brain v${version}\n`);
|
|
1614
|
-
{
|
|
1615
|
-
let gitName = '';
|
|
1616
|
-
try {
|
|
1617
|
-
const { execSync } = await import('node:child_process');
|
|
1618
|
-
gitName = execSync('git config user.name', { encoding: 'utf8', stdio: ['pipe','pipe','pipe'] }).trim();
|
|
1619
|
-
} catch { /* ignore */ }
|
|
1620
|
-
if (gitName) {
|
|
1621
|
-
const hour = new Date().getHours();
|
|
1622
|
-
let greet;
|
|
1623
|
-
if (hour >= 5 && hour <= 11) greet = 'Good morning';
|
|
1624
|
-
else if (hour >= 12 && hour <= 16) greet = 'Good afternoon';
|
|
1625
|
-
else if (hour >= 17 && hour <= 21) greet = 'Good evening';
|
|
1626
|
-
else greet = 'Late night';
|
|
1627
|
-
process.stdout.write(`\x1b[2m${greet}, ${gitName}\x1b[0m\n`);
|
|
1628
|
-
}
|
|
1629
|
-
}
|
|
1630
|
-
|
|
1631
1730
|
// ── Continuation card (interrupted work) ─────────────────────────────────
|
|
1632
1731
|
if (interrupted) {
|
|
1633
1732
|
const ctop = `┌${'─'.repeat(boxW - 2)}┐`;
|
|
@@ -1706,15 +1805,82 @@ async function mainScreen(rl, ask) {
|
|
|
1706
1805
|
// 's' → fall through to normal dashboard
|
|
1707
1806
|
}
|
|
1708
1807
|
|
|
1709
|
-
// ──
|
|
1710
|
-
|
|
1808
|
+
// ── Environment awareness (powers Box 1 dots + Box 3) ────────────────────
|
|
1809
|
+
let envReport = null;
|
|
1810
|
+
try {
|
|
1811
|
+
const { scanEnvironment } = await import('../src/awareness.mjs');
|
|
1812
|
+
envReport = scanEnvironment(cwd);
|
|
1813
|
+
} catch { /* non-fatal */ }
|
|
1814
|
+
|
|
1815
|
+
// ── Box 1 — Header row data ─────────────────────────────────────────────
|
|
1816
|
+
const providerLine = buildProviderStatusLine(profile, auth, envReport);
|
|
1711
1817
|
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1818
|
+
// ── Box 2 — Workspace: gather git data ───────────────────────────────────
|
|
1819
|
+
let gitBranch = 'unknown';
|
|
1820
|
+
let gitUncommitted = 0;
|
|
1821
|
+
let gitAheadCount = 0;
|
|
1822
|
+
let gitLastMsg = '';
|
|
1823
|
+
let gitLastAgo = '';
|
|
1824
|
+
|
|
1825
|
+
try {
|
|
1826
|
+
gitBranch = execSync('git rev-parse --abbrev-ref HEAD 2>/dev/null', {
|
|
1827
|
+
cwd, encoding: 'utf8', timeout: 2000, stdio: 'pipe',
|
|
1828
|
+
}).trim() || 'unknown';
|
|
1829
|
+
} catch {}
|
|
1830
|
+
|
|
1831
|
+
try {
|
|
1832
|
+
const status = execSync('git status --porcelain 2>/dev/null', {
|
|
1833
|
+
cwd, encoding: 'utf8', timeout: 2000, stdio: 'pipe',
|
|
1834
|
+
});
|
|
1835
|
+
gitUncommitted = status.trim().split('\n').filter(Boolean).length;
|
|
1836
|
+
} catch {}
|
|
1837
|
+
|
|
1838
|
+
try {
|
|
1839
|
+
const aheadOut = execSync('git rev-list @{u}..HEAD 2>/dev/null | wc -l', {
|
|
1840
|
+
cwd, encoding: 'utf8', timeout: 2000, stdio: 'pipe',
|
|
1841
|
+
});
|
|
1842
|
+
gitAheadCount = parseInt(aheadOut.trim(), 10) || 0;
|
|
1843
|
+
} catch {}
|
|
1844
|
+
|
|
1845
|
+
try {
|
|
1846
|
+
const logOut = execSync('git log -1 --format="%s|%ct" 2>/dev/null', {
|
|
1847
|
+
cwd, encoding: 'utf8', timeout: 2000, stdio: 'pipe',
|
|
1848
|
+
}).trim();
|
|
1849
|
+
if (logOut) {
|
|
1850
|
+
const [msg, ts] = logOut.split('|');
|
|
1851
|
+
gitLastMsg = (msg || '').slice(0, 38);
|
|
1852
|
+
const ageMs = Date.now() - (parseInt(ts, 10) * 1000);
|
|
1853
|
+
const ageMin = Math.floor(ageMs / 60000);
|
|
1854
|
+
if (ageMin < 60) gitLastAgo = `${ageMin}m ago`;
|
|
1855
|
+
else if (ageMin < 1440) gitLastAgo = `${Math.floor(ageMin / 60)}h ago`;
|
|
1856
|
+
else gitLastAgo = `${Math.floor(ageMin / 1440)}d ago`;
|
|
1857
|
+
}
|
|
1858
|
+
} catch {}
|
|
1859
|
+
|
|
1860
|
+
// ── Box 2 rows ────────────────────────────────────────────────────────────
|
|
1861
|
+
const uncommittedPart = gitUncommitted > 0 ? ` · ${gitUncommitted} uncommitted` : '';
|
|
1862
|
+
const aheadPart = gitAheadCount > 0 ? ` · ${gitAheadCount} ahead` : '';
|
|
1863
|
+
const workspaceLine1 = `${gitBranch}${uncommittedPart}${aheadPart}`;
|
|
1864
|
+
const workspaceLine2 = gitLastMsg
|
|
1865
|
+
? `Last: ${gitLastMsg} (${gitLastAgo})`
|
|
1866
|
+
: '';
|
|
1867
|
+
|
|
1868
|
+
// Open PRs
|
|
1869
|
+
const repoState = detectRepoState(cwd);
|
|
1870
|
+
const openPRs = await detectOpenPRs(cwd);
|
|
1871
|
+
|
|
1872
|
+
const workspaceRows = [row(workspaceLine1)];
|
|
1873
|
+
if (workspaceLine2) workspaceRows.push(row(workspaceLine2));
|
|
1874
|
+
if (openPRs.length > 0) {
|
|
1875
|
+
workspaceRows.push(row(`${openPRs.length} open PR${openPRs.length === 1 ? '' : 's'}`));
|
|
1715
1876
|
}
|
|
1716
1877
|
|
|
1717
|
-
// ──
|
|
1878
|
+
// ── Box 3 — Awareness: observer + roadmap + risk ──────────────────────────
|
|
1879
|
+
let awarenessLine1 = '\x1b[2m💡\x1b[0m Ready to work';
|
|
1880
|
+
let awarenessLine2 = '\x1b[2m📋 No roadmap yet\x1b[0m';
|
|
1881
|
+
let awarenessLine3 = '\x1b[32m✓\x1b[0m No risk flags';
|
|
1882
|
+
|
|
1883
|
+
// Line 1: observer data first; fall back to envReport-derived observations
|
|
1718
1884
|
let quickObservations = [];
|
|
1719
1885
|
try {
|
|
1720
1886
|
const observerMod = await import('../src/observer.mjs');
|
|
@@ -1724,64 +1890,78 @@ async function mainScreen(rl, ask) {
|
|
|
1724
1890
|
const sorted = [...quickState.observations].sort(
|
|
1725
1891
|
(a, b) => (PRIO[a.priority] ?? 2) - (PRIO[b.priority] ?? 2)
|
|
1726
1892
|
);
|
|
1727
|
-
quickObservations = sorted.slice(0,
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1893
|
+
quickObservations = sorted.slice(0, 3);
|
|
1894
|
+
const top = quickObservations[0];
|
|
1895
|
+
if (top) {
|
|
1896
|
+
const prefix = top.priority === 'high' ? '🔴' : top.priority === 'medium' ? '🟡' : '\x1b[2m💡\x1b[0m';
|
|
1897
|
+
awarenessLine1 = `${prefix} ${top.message}`;
|
|
1898
|
+
}
|
|
1899
|
+
const hasHighRisk = quickObservations.some(o => o.priority === 'high');
|
|
1900
|
+
if (hasHighRisk) {
|
|
1901
|
+
awarenessLine3 = '\x1b[31m⚠\x1b[0m Risk flags detected — run: dual-brain review';
|
|
1734
1902
|
}
|
|
1735
1903
|
}
|
|
1736
|
-
} catch { /* non-fatal —
|
|
1737
|
-
|
|
1738
|
-
//
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
|
|
1904
|
+
} catch { /* non-fatal — observer may not exist */ }
|
|
1905
|
+
|
|
1906
|
+
// If observer produced nothing, derive from envReport
|
|
1907
|
+
if (awarenessLine1 === '\x1b[2m💡\x1b[0m Ready to work' && envReport) {
|
|
1908
|
+
if (envReport.replit?.hasDatabase) {
|
|
1909
|
+
awarenessLine1 = '\x1b[2m💡\x1b[0m PostgreSQL available';
|
|
1910
|
+
} else if (gitUncommitted > 0) {
|
|
1911
|
+
awarenessLine1 = `\x1b[2m💡\x1b[0m ${gitUncommitted} file${gitUncommitted === 1 ? '' : 's'} ready to commit`;
|
|
1912
|
+
} else if (envReport.dualBrain?.hasFailureMemory) {
|
|
1913
|
+
// Check for recent failures
|
|
1914
|
+
try {
|
|
1915
|
+
const failureMem = await getFailureMem();
|
|
1916
|
+
if (failureMem.getRecentFailures) {
|
|
1917
|
+
const recent = failureMem.getRecentFailures(cwd, 2);
|
|
1918
|
+
if (recent?.length > 0) {
|
|
1919
|
+
awarenessLine1 = `\x1b[33m⚠\x1b[0m ${recent.length} recent failure${recent.length === 1 ? '' : 's'} — check before proceeding`;
|
|
1920
|
+
}
|
|
1921
|
+
}
|
|
1922
|
+
} catch { /* non-fatal */ }
|
|
1923
|
+
}
|
|
1748
1924
|
}
|
|
1749
1925
|
|
|
1750
|
-
//
|
|
1751
|
-
|
|
1926
|
+
// Line 2: roadmap file, then ledger open tasks as fallback
|
|
1927
|
+
try {
|
|
1928
|
+
const roadmapPath = join(cwd, '.dual-brain', 'roadmap.md');
|
|
1929
|
+
if (existsSync(roadmapPath)) {
|
|
1930
|
+
const roadmapText = readFileSync(roadmapPath, 'utf8');
|
|
1931
|
+
const lines = roadmapText.split('\n').filter(Boolean);
|
|
1932
|
+
// Skip heading lines, grab first non-heading line
|
|
1933
|
+
const firstItem = lines.find(l => !l.startsWith('#') && l.trim().length > 0);
|
|
1934
|
+
if (firstItem) {
|
|
1935
|
+
const clean = firstItem.replace(/^[-*>]+\s*/, '').trim().slice(0, 45);
|
|
1936
|
+
awarenessLine2 = `\x1b[2m📋\x1b[0m ${clean}`;
|
|
1937
|
+
}
|
|
1938
|
+
}
|
|
1939
|
+
} catch { /* non-fatal */ }
|
|
1940
|
+
|
|
1941
|
+
if (awarenessLine2 === '\x1b[2m📋 No roadmap yet\x1b[0m') {
|
|
1752
1942
|
try {
|
|
1753
|
-
const {
|
|
1754
|
-
const
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
// Load session index to get files for the most recent session
|
|
1758
|
-
const indexPath = join(cwd, '.dualbrain', 'session-index.json');
|
|
1759
|
-
let recentFiles = [];
|
|
1760
|
-
try {
|
|
1761
|
-
const idx = JSON.parse(readFileSync(indexPath, 'utf8'));
|
|
1762
|
-
recentFiles = idx[mostRecent.id]?.files || [];
|
|
1763
|
-
} catch {}
|
|
1764
|
-
const related = findRelatedSessions(recentPrompt, recentFiles, cwd);
|
|
1765
|
-
if (related.length > 0) {
|
|
1766
|
-
const relAgeLabel = (isoDate) => {
|
|
1767
|
-
if (!isoDate) return '';
|
|
1768
|
-
const diff = Date.now() - Date.parse(isoDate);
|
|
1769
|
-
const days = Math.floor(diff / 86400000);
|
|
1770
|
-
const hours = Math.floor(diff / 3600000);
|
|
1771
|
-
if (days >= 1) return `${days}d`;
|
|
1772
|
-
return `${hours}h ago`;
|
|
1773
|
-
};
|
|
1774
|
-
const relatedParts = related.slice(0, 2).map(r => {
|
|
1775
|
-
const age = relAgeLabel(r.date);
|
|
1776
|
-
return age ? `${r.smartName} (${age})` : r.smartName;
|
|
1777
|
-
});
|
|
1778
|
-
const DIM = '\x1b[2m';
|
|
1779
|
-
const RESET = '\x1b[0m';
|
|
1780
|
-
actionRows.push(row(`${DIM}📎 Related: ${relatedParts.join(', ')}${RESET}`));
|
|
1943
|
+
const { getOpenTasks } = await import('../src/ledger.mjs');
|
|
1944
|
+
const open = getOpenTasks(cwd);
|
|
1945
|
+
if (open.length > 0) {
|
|
1946
|
+
awarenessLine2 = '📋 Next: ' + open[0].intent.slice(0, 45);
|
|
1781
1947
|
}
|
|
1782
1948
|
} catch { /* non-fatal */ }
|
|
1783
1949
|
}
|
|
1784
|
-
|
|
1950
|
+
|
|
1951
|
+
// Line 3: model registry age warning
|
|
1952
|
+
try {
|
|
1953
|
+
const { getRegistryAge } = await import('../src/models.mjs');
|
|
1954
|
+
const age = getRegistryAge();
|
|
1955
|
+
if (age > 30 && awarenessLine3 === '\x1b[32m✓\x1b[0m No risk flags') {
|
|
1956
|
+
awarenessLine3 = `\x1b[33m⚠\x1b[0m Model registry ${age} days old`;
|
|
1957
|
+
}
|
|
1958
|
+
} catch { /* non-fatal */ }
|
|
1959
|
+
|
|
1960
|
+
const awarenessRows = [
|
|
1961
|
+
row(awarenessLine1),
|
|
1962
|
+
row(awarenessLine2),
|
|
1963
|
+
row(awarenessLine3),
|
|
1964
|
+
];
|
|
1785
1965
|
|
|
1786
1966
|
// ── Sessions section ──────────────────────────────────────────────────────
|
|
1787
1967
|
const sessionRows = [];
|
|
@@ -1837,23 +2017,28 @@ async function mainScreen(rl, ask) {
|
|
|
1837
2017
|
});
|
|
1838
2018
|
}
|
|
1839
2019
|
|
|
1840
|
-
// ──
|
|
1841
|
-
const actionsContent = '
|
|
2020
|
+
// ── Box 5 — Input bar ──────────────────────────────────────────────────
|
|
2021
|
+
const actionsContent = '> type anything... [s] settings [t] team [q] quit';
|
|
1842
2022
|
const actionsRow = row(actionsContent);
|
|
1843
2023
|
|
|
1844
|
-
// ── Print the full box
|
|
1845
|
-
//
|
|
1846
|
-
|
|
2024
|
+
// ── Print the full 5-box layout ───────────────────────────────────────────
|
|
2025
|
+
// Box 1: header (title + provider dots + work style)
|
|
2026
|
+
// Box 2: workspace (branch · uncommitted · ahead, last commit, open PRs)
|
|
2027
|
+
// Box 3: awareness (observer, roadmap, risk)
|
|
2028
|
+
// Box 4: sessions
|
|
2029
|
+
// Box 5: input bar
|
|
1847
2030
|
const lines = [
|
|
1848
2031
|
top,
|
|
1849
|
-
|
|
1850
|
-
|
|
2032
|
+
row(`🧠 dual-brain v${version}`),
|
|
2033
|
+
row(providerLine),
|
|
2034
|
+
sep,
|
|
2035
|
+
...workspaceRows,
|
|
2036
|
+
sep,
|
|
2037
|
+
...awarenessRows,
|
|
1851
2038
|
sep,
|
|
1852
2039
|
...sessionRows,
|
|
1853
2040
|
sep,
|
|
1854
2041
|
actionsRow,
|
|
1855
|
-
sep,
|
|
1856
|
-
poweredByRow,
|
|
1857
2042
|
bot,
|
|
1858
2043
|
];
|
|
1859
2044
|
// ── Stale session hint ──────────────────────────────────────────────────
|
|
@@ -1861,6 +2046,9 @@ async function mainScreen(rl, ask) {
|
|
|
1861
2046
|
process.stdout.write(`\x1b[2m${staleCount} stale sessions (>7d) — press s → archive to clean up\x1b[0m\n`);
|
|
1862
2047
|
}
|
|
1863
2048
|
|
|
2049
|
+
// Resolve dashboard spinner before rendering
|
|
2050
|
+
if (dashSpinner) dashSpinner.succeed('Dashboard ready');
|
|
2051
|
+
|
|
1864
2052
|
process.stdout.write(lines.join('\n') + '\n\n');
|
|
1865
2053
|
|
|
1866
2054
|
// ── Key handling ──────────────────────────────────────────────────────────
|
|
@@ -1948,7 +2136,7 @@ async function mainScreen(rl, ask) {
|
|
|
1948
2136
|
// Single-key commands only fire when buffer is empty
|
|
1949
2137
|
if (taskBuffer.length === 0) {
|
|
1950
2138
|
const lower = str.toLowerCase();
|
|
1951
|
-
const singleKeySet = new Set(['n', 's', 'q', '/', 'i']);
|
|
2139
|
+
const singleKeySet = new Set(['n', 's', 't', 'q', '/', 'i']);
|
|
1952
2140
|
if (singleKeySet.has(lower)) {
|
|
1953
2141
|
cleanup();
|
|
1954
2142
|
process.stdout.write('\n');
|
|
@@ -2054,6 +2242,7 @@ async function mainScreen(rl, ask) {
|
|
|
2054
2242
|
}
|
|
2055
2243
|
|
|
2056
2244
|
if (choice === 's') { return { next: 'settings' }; }
|
|
2245
|
+
if (choice === 't') { return { next: 'team' }; }
|
|
2057
2246
|
if (choice === 'i') { return { next: 'import-picker' }; }
|
|
2058
2247
|
if (choice === 'q' || choice === 'exit') { return { next: 'exit' }; }
|
|
2059
2248
|
|
|
@@ -2078,7 +2267,7 @@ async function newSessionScreen(rl, ask) {
|
|
|
2078
2267
|
async function importPickerScreen() {
|
|
2079
2268
|
const cwd = process.cwd();
|
|
2080
2269
|
|
|
2081
|
-
// Load all available sessions from
|
|
2270
|
+
// Load all available sessions from replit-tools
|
|
2082
2271
|
const allSessions = importReplitSessions(cwd);
|
|
2083
2272
|
|
|
2084
2273
|
// Load existing session meta to filter already-imported ones
|
|
@@ -2124,9 +2313,9 @@ async function importPickerScreen() {
|
|
|
2124
2313
|
if (allSessions.length === 0) {
|
|
2125
2314
|
process.stdout.write('\n');
|
|
2126
2315
|
process.stdout.write(top + '\n');
|
|
2127
|
-
process.stdout.write(row('Import from
|
|
2316
|
+
process.stdout.write(row('Import from replit-tools') + '\n');
|
|
2128
2317
|
process.stdout.write(sep + '\n');
|
|
2129
|
-
process.stdout.write(row('No
|
|
2318
|
+
process.stdout.write(row('No replit-tools sessions found.') + '\n');
|
|
2130
2319
|
process.stdout.write(row('Install replit-tools: npm i -g replit-tools') + '\n');
|
|
2131
2320
|
process.stdout.write(sep + '\n');
|
|
2132
2321
|
process.stdout.write(row('Press any key to go back...') + '\n');
|
|
@@ -2138,7 +2327,7 @@ async function importPickerScreen() {
|
|
|
2138
2327
|
if (candidates.length === 0) {
|
|
2139
2328
|
process.stdout.write('\n');
|
|
2140
2329
|
process.stdout.write(top + '\n');
|
|
2141
|
-
process.stdout.write(row('Import from
|
|
2330
|
+
process.stdout.write(row('Import from replit-tools') + '\n');
|
|
2142
2331
|
process.stdout.write(sep + '\n');
|
|
2143
2332
|
process.stdout.write(row(`All ${allSessions.length} sessions already imported.`) + '\n');
|
|
2144
2333
|
process.stdout.write(sep + '\n');
|
|
@@ -2161,7 +2350,7 @@ async function importPickerScreen() {
|
|
|
2161
2350
|
const renderPicker = () => {
|
|
2162
2351
|
process.stdout.write('\x1b[2J\x1b[H'); // clear screen
|
|
2163
2352
|
|
|
2164
|
-
const headerTitle = 'Import from
|
|
2353
|
+
const headerTitle = 'Import from replit-tools';
|
|
2165
2354
|
const footerLine = '↑↓ Navigate Space Toggle Enter Import q Back';
|
|
2166
2355
|
|
|
2167
2356
|
process.stdout.write('\n');
|
|
@@ -2298,7 +2487,7 @@ async function importPickerScreen() {
|
|
|
2298
2487
|
}
|
|
2299
2488
|
saveSessionMeta(updatedMeta, cwd);
|
|
2300
2489
|
|
|
2301
|
-
process.stdout.write(`✓ Imported ${importCount} session${importCount !== 1 ? 's' : ''} from
|
|
2490
|
+
process.stdout.write(`✓ Imported ${importCount} session${importCount !== 1 ? 's' : ''} from replit-tools\n\n`);
|
|
2302
2491
|
|
|
2303
2492
|
return { next: 'main' };
|
|
2304
2493
|
}
|
|
@@ -2560,22 +2749,89 @@ async function settingsScreen(rl, ask) {
|
|
|
2560
2749
|
'balanced': '⚖️ Balanced',
|
|
2561
2750
|
'quality-first': '🔥 Full Power',
|
|
2562
2751
|
};
|
|
2563
|
-
|
|
2752
|
+
|
|
2753
|
+
// Work style current markers
|
|
2754
|
+
const _stIsFast = ['cost-saver', 'auto', 'solo-claude', 'solo-openai'].includes(currentBias);
|
|
2755
|
+
const _stIsBal = currentBias === 'balanced';
|
|
2756
|
+
const _stIsFull = currentBias === 'quality-first';
|
|
2757
|
+
const _stMark = (active) => active ? ' ← current' : '';
|
|
2758
|
+
|
|
2759
|
+
// Provider status dots
|
|
2760
|
+
const _stAuth = await detectAuth();
|
|
2761
|
+
const _stGDOT = '\x1b[32m●\x1b[0m';
|
|
2762
|
+
const _stRDOT = '\x1b[31m●\x1b[0m';
|
|
2763
|
+
const _stClDot = _stAuth.claude.found ? _stGDOT : _stRDOT;
|
|
2764
|
+
const _stOaDot = _stAuth.openai.found ? _stGDOT : _stRDOT;
|
|
2765
|
+
const _stClStatus = _stAuth.claude.found ? 'connected' : 'not connected';
|
|
2766
|
+
const _stOaStatus = _stAuth.openai.found ? 'connected' : 'not connected';
|
|
2767
|
+
|
|
2768
|
+
// Calibration from project.json
|
|
2769
|
+
let _stCal = { specificity: 3, corrections: 3, autonomy: 3 };
|
|
2770
|
+
let _stLevel = 'intermediate';
|
|
2771
|
+
let _stStyle = 'normal';
|
|
2772
|
+
try {
|
|
2773
|
+
const _stLd = await import('../src/living-docs.mjs');
|
|
2774
|
+
const _stCm = await import('../src/calibration.mjs');
|
|
2775
|
+
const _stPs = _stLd.getProjectState(cwd);
|
|
2776
|
+
if (_stPs?.project?.userCalibration) _stCal = _stPs.project.userCalibration;
|
|
2777
|
+
const _stAd = _stCm.getAdaptation(_stCal);
|
|
2778
|
+
_stLevel = _stAd.userLevel;
|
|
2779
|
+
_stStyle = _stAd.responseStyle;
|
|
2780
|
+
} catch { /* non-fatal */ }
|
|
2781
|
+
|
|
2782
|
+
const _stS = typeof _stCal.specificity === 'number' ? _stCal.specificity.toFixed(1) : String(_stCal.specificity ?? 3);
|
|
2783
|
+
const _stC = typeof _stCal.corrections === 'number' ? _stCal.corrections.toFixed(1) : String(_stCal.corrections ?? 3);
|
|
2784
|
+
const _stA = typeof _stCal.autonomy === 'number' ? _stCal.autonomy.toFixed(1) : String(_stCal.autonomy ?? 3);
|
|
2785
|
+
|
|
2786
|
+
// Cost efficiency summary (graceful — only shown when data exists)
|
|
2787
|
+
let _stEffScore = null;
|
|
2788
|
+
let _stEffRate = null;
|
|
2789
|
+
let _stEffTrend = null;
|
|
2790
|
+
let _stEffTier = null;
|
|
2791
|
+
try {
|
|
2792
|
+
const _stCt = await import('../src/cost-tracker.mjs');
|
|
2793
|
+
const _stSummary = _stCt.getCostSummary(cwd, 7);
|
|
2794
|
+
if (_stSummary.totalActions > 0) {
|
|
2795
|
+
_stEffScore = _stCt.getEfficiencyScore(cwd);
|
|
2796
|
+
_stEffRate = Math.round(_stSummary.savingsRate * 100);
|
|
2797
|
+
_stEffTrend = _stSummary.trend;
|
|
2798
|
+
const tierOrder = ['recall', 'quick', 'standard', 'deep', 'ultra'];
|
|
2799
|
+
const _stTierKeys = tierOrder.filter(k => _stSummary.byTier[k]);
|
|
2800
|
+
_stEffTier = _stTierKeys.map(k => {
|
|
2801
|
+
const t = _stSummary.byTier[k];
|
|
2802
|
+
return `${k.padEnd(8)} ${String(t.count).padStart(3)}`;
|
|
2803
|
+
}).join(' ');
|
|
2804
|
+
}
|
|
2805
|
+
} catch { /* non-fatal */ }
|
|
2806
|
+
|
|
2807
|
+
const _stTrendIcon = _stEffTrend === 'improving' ? '↗' : _stEffTrend === 'degrading' ? '↘' : '→';
|
|
2564
2808
|
|
|
2565
2809
|
const lines = [
|
|
2566
2810
|
top,
|
|
2567
2811
|
row('Settings'),
|
|
2568
2812
|
sep,
|
|
2569
|
-
row(
|
|
2570
|
-
row(
|
|
2571
|
-
row(
|
|
2572
|
-
row(
|
|
2573
|
-
|
|
2574
|
-
row('
|
|
2575
|
-
row(
|
|
2813
|
+
row('Work Style'),
|
|
2814
|
+
row(` [1] Fast — speed over caution${_stMark(_stIsFast)}`),
|
|
2815
|
+
row(` [2] Balanced — smart routing, reviews on important${_stMark(_stIsBal)}`),
|
|
2816
|
+
row(` [3] Full Power — dual-brain everything, max quality${_stMark(_stIsFull)}`),
|
|
2817
|
+
sep,
|
|
2818
|
+
row('Providers'),
|
|
2819
|
+
row(` Claude: ${_stClDot} ${_stClStatus}`),
|
|
2820
|
+
row(` OpenAI: ${_stOaDot} ${_stOaStatus}`),
|
|
2821
|
+
sep,
|
|
2822
|
+
row('User Calibration'),
|
|
2823
|
+
row(` Specificity: ${_stS} Corrections: ${_stC} Autonomy: ${_stA}`),
|
|
2824
|
+
row(` Level: ${_stLevel} · Style: ${_stStyle}`),
|
|
2825
|
+
...(_stEffScore !== null ? [
|
|
2826
|
+
sep,
|
|
2827
|
+
row('Cost Efficiency (7 days)'),
|
|
2828
|
+
row(` Score: ${_stEffScore}/100 Savings: ${_stEffRate}% Trend: ${_stTrendIcon} ${_stEffTrend}`),
|
|
2829
|
+
...(_stEffTier ? [row(` Tiers: ${_stEffTier}`)] : []),
|
|
2830
|
+
] : []),
|
|
2831
|
+
sep,
|
|
2832
|
+
row('[1-3] change style [r] reset calibration [b] back'),
|
|
2833
|
+
row('[m] subscriptions [e] sessions [x] diagnostics'),
|
|
2576
2834
|
...(settingsPRs.length > 0 ? [row(`[p] PR triage (${settingsPRs.length} open)`)] : []),
|
|
2577
|
-
row(''),
|
|
2578
|
-
row('[Esc/b] Back to dashboard'),
|
|
2579
2835
|
bot,
|
|
2580
2836
|
];
|
|
2581
2837
|
process.stdout.write('\n' + lines.join('\n') + '\n\n');
|
|
@@ -2583,45 +2839,10 @@ async function settingsScreen(rl, ask) {
|
|
|
2583
2839
|
const raw = (await ask(' Choice: ')).trim();
|
|
2584
2840
|
const choice = raw.toLowerCase();
|
|
2585
2841
|
|
|
2586
|
-
|
|
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];
|
|
2842
|
+
// Direct work style keys 1/2/3
|
|
2843
|
+
if (choice === '1' || choice === '2' || choice === '3') {
|
|
2844
|
+
const _stWsMap = { '1': 'cost-saver', '2': 'balanced', '3': 'quality-first' };
|
|
2845
|
+
const newBias = _stWsMap[choice];
|
|
2625
2846
|
if (newBias && newBias !== currentBias) {
|
|
2626
2847
|
profile.bias = newBias;
|
|
2627
2848
|
const enabledCount = [
|
|
@@ -2631,12 +2852,23 @@ async function settingsScreen(rl, ask) {
|
|
|
2631
2852
|
if (enabledCount >= 2) profile.mode = newBias;
|
|
2632
2853
|
saveProfile(profile, { cwd });
|
|
2633
2854
|
const newLabel = WORK_STYLE_DISPLAY[newBias] || newBias;
|
|
2634
|
-
|
|
2855
|
+
process.stdout.write(`\n Work style set to ${newLabel}\n\n`);
|
|
2635
2856
|
await ask(' Press Enter to continue...');
|
|
2636
2857
|
}
|
|
2637
2858
|
return { next: 'settings' };
|
|
2638
2859
|
}
|
|
2639
2860
|
|
|
2861
|
+
// Reset calibration to defaults
|
|
2862
|
+
if (choice === 'r') {
|
|
2863
|
+
try {
|
|
2864
|
+
const _stLdReset = await import('../src/living-docs.mjs');
|
|
2865
|
+
_stLdReset.updateProject({ userCalibration: { specificity: 3, corrections: 3, autonomy: 3 } }, cwd);
|
|
2866
|
+
process.stdout.write('\n Calibration reset to defaults.\n\n');
|
|
2867
|
+
await ask(' Press Enter to continue...');
|
|
2868
|
+
} catch { /* non-fatal */ }
|
|
2869
|
+
return { next: 'settings' };
|
|
2870
|
+
}
|
|
2871
|
+
|
|
2640
2872
|
if (choice === 'm') { return { next: 'subscriptions' }; }
|
|
2641
2873
|
|
|
2642
2874
|
if (choice === 'e') { return { next: 'sessions' }; }
|
|
@@ -2655,7 +2887,7 @@ async function settingsScreen(rl, ask) {
|
|
|
2655
2887
|
if (which.status === 0) {
|
|
2656
2888
|
spawnSync('claude-menu', { stdio: 'inherit' });
|
|
2657
2889
|
} else {
|
|
2658
|
-
process.stdout.write('\n
|
|
2890
|
+
process.stdout.write('\n replit-tools not found — install with: npm i -g replit-tools\n\n');
|
|
2659
2891
|
await ask(' Press Enter to continue...');
|
|
2660
2892
|
}
|
|
2661
2893
|
return { next: 'settings' };
|
|
@@ -2689,6 +2921,105 @@ async function settingsScreen(rl, ask) {
|
|
|
2689
2921
|
return { next: 'main' };
|
|
2690
2922
|
}
|
|
2691
2923
|
|
|
2924
|
+
// ─── Screen: teamScreen ───────────────────────────────────────────────────────
|
|
2925
|
+
|
|
2926
|
+
async function teamScreen(rl, ask) {
|
|
2927
|
+
const cwd = process.cwd();
|
|
2928
|
+
|
|
2929
|
+
// Box layout matching dashboard
|
|
2930
|
+
const termW = process.stdout.columns || 60;
|
|
2931
|
+
const boxW = Math.min(termW - 2, 60);
|
|
2932
|
+
const W = boxW - 4;
|
|
2933
|
+
|
|
2934
|
+
const top = `┌${'─'.repeat(boxW - 2)}┐`;
|
|
2935
|
+
const sep = `├${'─'.repeat(boxW - 2)}┤`;
|
|
2936
|
+
const bot = `└${'─'.repeat(boxW - 2)}┘`;
|
|
2937
|
+
const row = (content) => makeBoxRow(content, W);
|
|
2938
|
+
|
|
2939
|
+
// Load team from project.json
|
|
2940
|
+
let team = [];
|
|
2941
|
+
let sharedSessions = 0;
|
|
2942
|
+
let teamDecisions = 0;
|
|
2943
|
+
try {
|
|
2944
|
+
const _tmLd = await import('../src/living-docs.mjs');
|
|
2945
|
+
const _tmPs = _tmLd.getProjectState(cwd);
|
|
2946
|
+
if (Array.isArray(_tmPs?.project?.team)) {
|
|
2947
|
+
team = _tmPs.project.team;
|
|
2948
|
+
}
|
|
2949
|
+
// Count decisions with more than one participant as team decisions
|
|
2950
|
+
if (Array.isArray(_tmPs?.recentDecisions)) {
|
|
2951
|
+
teamDecisions = _tmPs.recentDecisions.filter(
|
|
2952
|
+
d => Array.isArray(d?.participants) && d.participants.length > 1
|
|
2953
|
+
).length;
|
|
2954
|
+
}
|
|
2955
|
+
} catch { /* non-fatal */ }
|
|
2956
|
+
|
|
2957
|
+
// Fall back to git user if no team configured
|
|
2958
|
+
let ownerName = '(you)';
|
|
2959
|
+
if (team.length === 0) {
|
|
2960
|
+
try {
|
|
2961
|
+
const { execSync: _tmExec } = await import('node:child_process');
|
|
2962
|
+
const gitUser = _tmExec('git config user.name 2>/dev/null', {
|
|
2963
|
+
encoding: 'utf8', timeout: 2000, stdio: 'pipe',
|
|
2964
|
+
}).trim();
|
|
2965
|
+
if (gitUser) ownerName = gitUser;
|
|
2966
|
+
} catch { /* non-fatal */ }
|
|
2967
|
+
}
|
|
2968
|
+
|
|
2969
|
+
const memberRows = [];
|
|
2970
|
+
if (team.length === 0) {
|
|
2971
|
+
memberRows.push(row(` ${ownerName} (owner)`));
|
|
2972
|
+
} else {
|
|
2973
|
+
for (const member of team) {
|
|
2974
|
+
const role = member.role || 'member';
|
|
2975
|
+
memberRows.push(row(` ${member.name} (${role})`));
|
|
2976
|
+
}
|
|
2977
|
+
}
|
|
2978
|
+
|
|
2979
|
+
const lines = [
|
|
2980
|
+
top,
|
|
2981
|
+
row('Team'),
|
|
2982
|
+
sep,
|
|
2983
|
+
row('Members'),
|
|
2984
|
+
...memberRows,
|
|
2985
|
+
sep,
|
|
2986
|
+
row(`Shared Sessions: ${sharedSessions}`),
|
|
2987
|
+
row(`Team decisions: ${teamDecisions}`),
|
|
2988
|
+
sep,
|
|
2989
|
+
row('[a] add member [b] back'),
|
|
2990
|
+
bot,
|
|
2991
|
+
];
|
|
2992
|
+
process.stdout.write('\n' + lines.join('\n') + '\n\n');
|
|
2993
|
+
|
|
2994
|
+
const raw = (await ask(' Choice: ')).trim();
|
|
2995
|
+
const choice = raw.toLowerCase();
|
|
2996
|
+
|
|
2997
|
+
if (choice === 'a') {
|
|
2998
|
+
const name = (await ask(' Member name: ')).trim();
|
|
2999
|
+
if (name) {
|
|
3000
|
+
try {
|
|
3001
|
+
const _tmLdAdd = await import('../src/living-docs.mjs');
|
|
3002
|
+
const _tmCur = _tmLdAdd.getProjectState(cwd);
|
|
3003
|
+
const _tmTeam = Array.isArray(_tmCur?.project?.team) ? [..._tmCur.project.team] : [];
|
|
3004
|
+
_tmTeam.push({ name, role: 'member', addedAt: new Date().toISOString() });
|
|
3005
|
+
_tmLdAdd.updateProject({ team: _tmTeam }, cwd);
|
|
3006
|
+
process.stdout.write(`\n Added ${name} to team.\n\n`);
|
|
3007
|
+
await ask(' Press Enter to continue...');
|
|
3008
|
+
} catch {
|
|
3009
|
+
process.stdout.write('\n Could not save team member.\n\n');
|
|
3010
|
+
await ask(' Press Enter to continue...');
|
|
3011
|
+
}
|
|
3012
|
+
}
|
|
3013
|
+
return { next: 'team' };
|
|
3014
|
+
}
|
|
3015
|
+
|
|
3016
|
+
if (choice === 'b' || choice === 'back' || choice === 'q' || raw === '\x1b') {
|
|
3017
|
+
return { next: 'main' };
|
|
3018
|
+
}
|
|
3019
|
+
|
|
3020
|
+
return { next: 'main' };
|
|
3021
|
+
}
|
|
3022
|
+
|
|
2692
3023
|
|
|
2693
3024
|
// ─── Helper: aggregatePlans ───────────────────────────────────────────────────
|
|
2694
3025
|
|
|
@@ -2873,115 +3204,194 @@ async function subscriptionsScreen(rl, ask) {
|
|
|
2873
3204
|
// ─── Onboarding Wizard ───────────────────────────────────────────────────────
|
|
2874
3205
|
|
|
2875
3206
|
/**
|
|
2876
|
-
*
|
|
2877
|
-
*
|
|
2878
|
-
*
|
|
3207
|
+
* Animated first-run setup wizard.
|
|
3208
|
+
* 5 steps: welcome → env scan → replit-tools → import → work style → ready.
|
|
3209
|
+
* Uses src/fx.mjs when available; falls back to plain output stubs.
|
|
3210
|
+
*
|
|
3211
|
+
* @param {{ auth, plans, existingSessions }} _detection (unused — kept for API compat)
|
|
2879
3212
|
* @param {string} cwd
|
|
2880
3213
|
* @param {object} rl readline interface
|
|
2881
3214
|
* @returns {object|null} profile object to save, or null if cancelled/skipped
|
|
2882
3215
|
*/
|
|
2883
3216
|
async function runOnboardingWizard(_detection, cwd, rl) {
|
|
2884
3217
|
const ask = (q) => new Promise(res => rl.question(q, res));
|
|
2885
|
-
const
|
|
2886
|
-
|
|
2887
|
-
//
|
|
2888
|
-
|
|
2889
|
-
|
|
2890
|
-
|
|
2891
|
-
|
|
2892
|
-
|
|
2893
|
-
|
|
2894
|
-
|
|
2895
|
-
|
|
2896
|
-
|
|
2897
|
-
|
|
2898
|
-
|
|
2899
|
-
|
|
2900
|
-
|
|
2901
|
-
|
|
2902
|
-
|
|
2903
|
-
|
|
2904
|
-
|
|
3218
|
+
const fx = await getFx();
|
|
3219
|
+
|
|
3220
|
+
// ─── Step 1: Welcome banner ────────────────────────────────────────────────
|
|
3221
|
+
fx.clearScreen();
|
|
3222
|
+
fx.banner('🧠 DUAL-BRAIN');
|
|
3223
|
+
fx.nl();
|
|
3224
|
+
fx.info("Welcome! Let's set up your AI work partner.");
|
|
3225
|
+
fx.nl();
|
|
3226
|
+
await fx.sleep(800);
|
|
3227
|
+
|
|
3228
|
+
// ─── Step 2: Environment detection ────────────────────────────────────────
|
|
3229
|
+
fx.step(1, 5, 'Scanning environment');
|
|
3230
|
+
fx.nl();
|
|
3231
|
+
|
|
3232
|
+
// Run capability detection in parallel with the animations
|
|
3233
|
+
const capsPromise = detectCapabilities(cwd);
|
|
3234
|
+
|
|
3235
|
+
await fx.loadingSequence([
|
|
3236
|
+
{ text: 'Detecting container...', duration: 500, successText: 'Replit container detected' },
|
|
3237
|
+
{ text: 'Checking CLI tools...', duration: 400, successText: 'CLI tools available (git, node, claude...)' },
|
|
3238
|
+
{ text: 'Scanning secrets...', duration: 350, successText: 'Environment scanned' },
|
|
3239
|
+
]);
|
|
2905
3240
|
|
|
2906
|
-
//
|
|
2907
|
-
const caps
|
|
3241
|
+
// Await actual capability data
|
|
3242
|
+
const caps = await capsPromise;
|
|
2908
3243
|
const claudeReady = caps.claude.available;
|
|
2909
3244
|
const openaiReady = caps.openai.available;
|
|
2910
3245
|
const codexAvailable = caps.codex.available;
|
|
2911
3246
|
|
|
2912
|
-
//
|
|
3247
|
+
// Override the generic "secrets" success with real data
|
|
3248
|
+
const secretsLine = claudeReady || openaiReady
|
|
3249
|
+
? 'API keys configured'
|
|
3250
|
+
: 'No API keys found — configure later';
|
|
3251
|
+
fx.info(secretsLine);
|
|
3252
|
+
fx.nl();
|
|
3253
|
+
|
|
3254
|
+
// ─── Step 3: Detect replit-tools ──────────────────────────────────────────
|
|
3255
|
+
fx.step(2, 5, 'Detecting replit-tools');
|
|
3256
|
+
fx.nl();
|
|
3257
|
+
|
|
2913
3258
|
const rt = detectReplitTools(cwd);
|
|
3259
|
+
const rtSpinner = fx.spinner('Looking for replit-tools...').start();
|
|
3260
|
+
await fx.sleep(700);
|
|
2914
3261
|
|
|
2915
|
-
|
|
2916
|
-
|
|
2917
|
-
|
|
2918
|
-
|
|
3262
|
+
let rtSessionCount = 0;
|
|
3263
|
+
if (rt.installed) {
|
|
3264
|
+
const vStr = rt.version ? ` v${rt.version}` : '';
|
|
3265
|
+
rtSpinner.succeed(`replit-tools${vStr} detected`);
|
|
3266
|
+
// Count available sessions
|
|
3267
|
+
try {
|
|
3268
|
+
const sessions = importReplitSessions(cwd);
|
|
3269
|
+
rtSessionCount = sessions.length;
|
|
3270
|
+
} catch { /* non-fatal */ }
|
|
3271
|
+
} else {
|
|
3272
|
+
rtSpinner.warn('replit-tools not found — install with: npm i -g replit-tools');
|
|
3273
|
+
}
|
|
3274
|
+
fx.nl();
|
|
2919
3275
|
|
|
2920
|
-
//
|
|
2921
|
-
|
|
2922
|
-
|
|
2923
|
-
|
|
2924
|
-
|
|
2925
|
-
|
|
2926
|
-
|
|
2927
|
-
|
|
2928
|
-
|
|
2929
|
-
|
|
2930
|
-
|
|
2931
|
-
|
|
2932
|
-
|
|
2933
|
-
|
|
2934
|
-
|
|
2935
|
-
|
|
2936
|
-
|
|
2937
|
-
|
|
2938
|
-
|
|
2939
|
-
|
|
2940
|
-
|
|
2941
|
-
|
|
2942
|
-
|
|
2943
|
-
|
|
2944
|
-
|
|
2945
|
-
|
|
2946
|
-
|
|
2947
|
-
|
|
2948
|
-
} else if (!claudeReady && (openaiReady || codexAvailable)) {
|
|
2949
|
-
console.log(` ${DIM}Note: Use within Claude Code for full dual-brain${RESET}`);
|
|
2950
|
-
console.log('');
|
|
3276
|
+
// ─── Step 4: Import conversations ─────────────────────────────────────────
|
|
3277
|
+
fx.step(3, 5, 'Import conversations');
|
|
3278
|
+
fx.nl();
|
|
3279
|
+
|
|
3280
|
+
if (rt.installed && rtSessionCount > 0) {
|
|
3281
|
+
fx.info(`Found ${rtSessionCount} session${rtSessionCount === 1 ? '' : 's'} from replit-tools`);
|
|
3282
|
+
fx.nl();
|
|
3283
|
+
|
|
3284
|
+
// Ask user — line-based input since we may not have raw mode here
|
|
3285
|
+
process.stdout.write(' Import conversations? [y/N]: ');
|
|
3286
|
+
const importChoice = (await ask('')).trim().toLowerCase();
|
|
3287
|
+
|
|
3288
|
+
if (importChoice === 'y' || importChoice === 'yes') {
|
|
3289
|
+
const importSpinner = fx.spinner('Importing sessions...').start();
|
|
3290
|
+
await fx.sleep(600);
|
|
3291
|
+
try {
|
|
3292
|
+
// Sessions are already imported via importReplitSessions above (lazy-loaded)
|
|
3293
|
+
importSpinner.succeed(`${rtSessionCount} session${rtSessionCount === 1 ? '' : 's'} imported`);
|
|
3294
|
+
} catch (e) {
|
|
3295
|
+
importSpinner.fail(`Import failed: ${e.message}`);
|
|
3296
|
+
}
|
|
3297
|
+
} else {
|
|
3298
|
+
fx.dim('Skipped — you can import later from Settings → Import');
|
|
3299
|
+
}
|
|
3300
|
+
} else if (rt.installed) {
|
|
3301
|
+
fx.dim('No sessions to import');
|
|
3302
|
+
} else {
|
|
3303
|
+
fx.dim('Skipping — replit-tools not found');
|
|
2951
3304
|
}
|
|
3305
|
+
fx.nl();
|
|
2952
3306
|
|
|
2953
|
-
//
|
|
2954
|
-
|
|
2955
|
-
|
|
2956
|
-
|
|
2957
|
-
|
|
2958
|
-
|
|
2959
|
-
|
|
2960
|
-
|
|
2961
|
-
console.log(wRow(' 3 🔥 Full Power — deep reasoning, dual-brain when it matters'));
|
|
2962
|
-
console.log(wBottom);
|
|
2963
|
-
console.log('');
|
|
3307
|
+
// ─── Step 5: Work style selection ─────────────────────────────────────────
|
|
3308
|
+
fx.step(4, 5, 'Choose your style');
|
|
3309
|
+
fx.nl();
|
|
3310
|
+
process.stdout.write(' How do you want to work?\n\n');
|
|
3311
|
+
process.stdout.write(' [1] ⚡ Fast — speed over caution, auto-execute\n');
|
|
3312
|
+
process.stdout.write(' [2] ⚖️ Balanced — smart routing, reviews when it matters\n');
|
|
3313
|
+
process.stdout.write(' [3] 🔒 Thorough — dual-brain everything, max quality\n');
|
|
3314
|
+
fx.nl();
|
|
2964
3315
|
|
|
2965
|
-
const styleChoice = (await ask(' Choice [2]: ')).trim();
|
|
2966
3316
|
const styleMap = { '1': 'cost-saver', '2': 'balanced', '3': 'quality-first' };
|
|
2967
|
-
const styleNames = { 'cost-saver': 'Fast', 'balanced': 'Balanced', 'quality-first': '
|
|
3317
|
+
const styleNames = { 'cost-saver': 'Fast', 'balanced': 'Balanced', 'quality-first': 'Thorough' };
|
|
3318
|
+
|
|
3319
|
+
let styleChoice = '2'; // default
|
|
3320
|
+
const isTTY = process.stdin.isTTY && typeof process.stdin.setRawMode === 'function';
|
|
3321
|
+
|
|
3322
|
+
if (isTTY) {
|
|
3323
|
+
// Raw keypress — single character
|
|
3324
|
+
const { emitKeypressEvents } = await import('node:readline');
|
|
3325
|
+
emitKeypressEvents(process.stdin, rl);
|
|
3326
|
+
|
|
3327
|
+
process.stdout.write(' Choice [2]: ');
|
|
3328
|
+
styleChoice = await new Promise((resolve) => {
|
|
3329
|
+
const wasRaw = process.stdin.isRaw;
|
|
3330
|
+
process.stdin.setRawMode(true);
|
|
3331
|
+
|
|
3332
|
+
const cleanup = () => {
|
|
3333
|
+
process.stdin.removeListener('keypress', onKey);
|
|
3334
|
+
try { process.stdin.setRawMode(wasRaw || false); } catch {}
|
|
3335
|
+
};
|
|
3336
|
+
|
|
3337
|
+
const onKey = (str, key) => {
|
|
3338
|
+
if (!key) return;
|
|
3339
|
+
const name = key.name || '';
|
|
3340
|
+
if (key.ctrl && (name === 'c' || name === 'd')) {
|
|
3341
|
+
cleanup();
|
|
3342
|
+
process.stdout.write('\n');
|
|
3343
|
+
resolve('2');
|
|
3344
|
+
return;
|
|
3345
|
+
}
|
|
3346
|
+
if (name === 'return' || name === 'enter') {
|
|
3347
|
+
cleanup();
|
|
3348
|
+
process.stdout.write('\n');
|
|
3349
|
+
resolve('2');
|
|
3350
|
+
return;
|
|
3351
|
+
}
|
|
3352
|
+
if (str === '1' || str === '2' || str === '3') {
|
|
3353
|
+
cleanup();
|
|
3354
|
+
process.stdout.write(`${str}\n`);
|
|
3355
|
+
resolve(str);
|
|
3356
|
+
return;
|
|
3357
|
+
}
|
|
3358
|
+
};
|
|
3359
|
+
|
|
3360
|
+
process.stdin.on('keypress', onKey);
|
|
3361
|
+
});
|
|
3362
|
+
} else {
|
|
3363
|
+
// Fallback: line-based prompt
|
|
3364
|
+
process.stdout.write(' Choice [2]: ');
|
|
3365
|
+
styleChoice = (await ask('')).trim() || '2';
|
|
3366
|
+
}
|
|
3367
|
+
|
|
2968
3368
|
const chosenBias = styleMap[styleChoice] || 'balanced';
|
|
2969
3369
|
const chosenName = styleNames[chosenBias];
|
|
3370
|
+
fx.nl();
|
|
2970
3371
|
|
|
2971
|
-
//
|
|
3372
|
+
// Non-blocking note if metered API detected
|
|
2972
3373
|
if (openaiReady && caps.openai.metered) {
|
|
2973
|
-
|
|
2974
|
-
|
|
3374
|
+
const DIM = '\x1b[2m'; const RESET = '\x1b[0m';
|
|
3375
|
+
process.stdout.write(` ${DIM}OpenAI API key detected — usage is metered, guardrails enabled${RESET}\n\n`);
|
|
2975
3376
|
}
|
|
2976
3377
|
|
|
2977
|
-
//
|
|
2978
|
-
|
|
2979
|
-
|
|
2980
|
-
|
|
2981
|
-
|
|
2982
|
-
|
|
3378
|
+
// ─── Step 6: Ready ────────────────────────────────────────────────────────
|
|
3379
|
+
fx.step(5, 5, 'Ready!');
|
|
3380
|
+
fx.nl();
|
|
3381
|
+
|
|
3382
|
+
// Init living docs
|
|
3383
|
+
try {
|
|
3384
|
+
const ld = await getLivingDocs();
|
|
3385
|
+
if (ld.initLivingDocs) ld.initLivingDocs(cwd);
|
|
3386
|
+
} catch { /* non-fatal */ }
|
|
3387
|
+
|
|
3388
|
+
await fx.sleep(400);
|
|
3389
|
+
fx.celebrate(`dual-brain is ready! (${chosenName} mode)`);
|
|
3390
|
+
fx.nl();
|
|
3391
|
+
fx.info('Type anything to get started. Your AI partner is listening.');
|
|
3392
|
+
await fx.sleep(1200);
|
|
2983
3393
|
|
|
2984
|
-
//
|
|
3394
|
+
// ─── Build and return the profile object ──────────────────────────────────
|
|
2985
3395
|
const finalProfile = loadProfile(cwd);
|
|
2986
3396
|
|
|
2987
3397
|
finalProfile.providers.claude = { enabled: claudeReady };
|
|
@@ -4026,6 +4436,7 @@ const SCREENS = {
|
|
|
4026
4436
|
main: mainScreen,
|
|
4027
4437
|
'new-session': newSessionScreen,
|
|
4028
4438
|
settings: settingsScreen,
|
|
4439
|
+
team: teamScreen,
|
|
4029
4440
|
'import-picker': importPickerScreen,
|
|
4030
4441
|
'pr-triage': prTriageScreen,
|
|
4031
4442
|
subscriptions: subscriptionsScreen,
|