dual-brain 0.3.25 → 0.3.27
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 +605 -79
- package/dist/src/auto-handoff.d.ts +2 -0
- package/dist/src/auto-handoff.js +20 -5
- package/dist/src/auto-handoff.js.map +1 -1
- package/dist/src/decide.d.ts +21 -0
- package/dist/src/decide.js +145 -23
- package/dist/src/decide.js.map +1 -1
- package/dist/src/install-hooks.js +1 -0
- package/dist/src/install-hooks.js.map +1 -1
- package/dist/src/provider-enforcement.js +2 -0
- package/dist/src/provider-enforcement.js.map +1 -1
- package/dist/src/setup-flow.d.ts +4 -0
- package/dist/src/setup-flow.js +22 -9
- package/dist/src/setup-flow.js.map +1 -1
- package/hooks/command-risk.mjs +137 -0
- package/hooks/head-guard.mjs +26 -40
- package/install.mjs +1 -0
- package/package.json +1 -1
package/bin/dual-brain.mjs
CHANGED
|
@@ -28,7 +28,12 @@ function _claudeResumeArgs(sessionId, cwd) {
|
|
|
28
28
|
const args = ['--resume', sessionId];
|
|
29
29
|
const workspace = cwd || process.cwd();
|
|
30
30
|
if (getEffectiveBypassPermissions(workspace)) args.push('--dangerously-skip-permissions');
|
|
31
|
-
else if (getEffectiveAutomode(loadProfile(workspace), workspace))
|
|
31
|
+
else if (getEffectiveAutomode(loadProfile(workspace), workspace)) {
|
|
32
|
+
const settings = loadSessionSettings(workspace);
|
|
33
|
+
if (settings.headModel) args.push('--model', settings.headModel);
|
|
34
|
+
if (settings.effort) args.push('--effort', settings.effort);
|
|
35
|
+
args.push('--permission-mode', 'auto');
|
|
36
|
+
}
|
|
32
37
|
return args;
|
|
33
38
|
}
|
|
34
39
|
|
|
@@ -36,7 +41,12 @@ function _claudeNewArgs(cwd) {
|
|
|
36
41
|
const args = [];
|
|
37
42
|
const workspace = cwd || process.cwd();
|
|
38
43
|
if (getEffectiveBypassPermissions(workspace)) args.push('--dangerously-skip-permissions');
|
|
39
|
-
else if (getEffectiveAutomode(loadProfile(workspace), workspace))
|
|
44
|
+
else if (getEffectiveAutomode(loadProfile(workspace), workspace)) {
|
|
45
|
+
const settings = loadSessionSettings(workspace);
|
|
46
|
+
if (settings.headModel) args.push('--model', settings.headModel);
|
|
47
|
+
if (settings.effort) args.push('--effort', settings.effort);
|
|
48
|
+
args.push('--permission-mode', 'auto');
|
|
49
|
+
}
|
|
40
50
|
return args;
|
|
41
51
|
}
|
|
42
52
|
|
|
@@ -46,7 +56,26 @@ function _codexResumeArgs(sessionId, cwd) {
|
|
|
46
56
|
return ['--dangerously-bypass-approvals-and-sandbox', 'resume', sessionId];
|
|
47
57
|
}
|
|
48
58
|
const approvalMode = getEffectiveAutomode(loadProfile(workspace), workspace) ? 'never' : 'on-request';
|
|
49
|
-
return [
|
|
59
|
+
return [..._codexApprovalArgs(workspace, approvalMode), 'resume', sessionId];
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function _isReplitWorkspace(cwd) {
|
|
63
|
+
const workspace = cwd || process.cwd();
|
|
64
|
+
return !!(
|
|
65
|
+
process.env.REPL_ID ||
|
|
66
|
+
process.env.REPL_SLUG ||
|
|
67
|
+
process.env.REPLIT_CLUSTER ||
|
|
68
|
+
existsSync(join(workspace, '.replit')) ||
|
|
69
|
+
existsSync(join(workspace, '.replit-tools'))
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function _codexApprovalArgs(cwd, approvalMode = 'on-request') {
|
|
74
|
+
// Codex workspace-write depends on bubblewrap/user namespaces. Replit's
|
|
75
|
+
// container frequently blocks that, so use the Replit container boundary and
|
|
76
|
+
// Codex approvals instead of launching a HEAD that cannot run any command.
|
|
77
|
+
const sandboxMode = _isReplitWorkspace(cwd) ? 'danger-full-access' : 'workspace-write';
|
|
78
|
+
return ['--sandbox', sandboxMode, '--ask-for-approval', approvalMode];
|
|
50
79
|
}
|
|
51
80
|
|
|
52
81
|
function _sessionTool(session) {
|
|
@@ -88,7 +117,7 @@ async function _primeRegistryCache() {
|
|
|
88
117
|
}
|
|
89
118
|
|
|
90
119
|
import {
|
|
91
|
-
decideRoute, getAvailableModels,
|
|
120
|
+
decideRoute, getAvailableModels, recommendHeadModel,
|
|
92
121
|
} from '../dist/src/decide.js';
|
|
93
122
|
|
|
94
123
|
import {
|
|
@@ -1047,6 +1076,14 @@ async function cmdStatus(args = []) {
|
|
|
1047
1076
|
console.log(`\nHead model : ${getHeadModel(profile)}`);
|
|
1048
1077
|
console.log(`Mode : ${profile.mode}`);
|
|
1049
1078
|
console.log(`Solo brain : ${isSoloBrain(profile) ? 'yes' : 'no'}`);
|
|
1079
|
+
try {
|
|
1080
|
+
const headRec = recommendHeadModel(profile);
|
|
1081
|
+
const effort = headRec.effort ? ` (${headRec.effort})` : '';
|
|
1082
|
+
console.log(`Recommended: ${headRec.provider}:${headRec.model}${effort} · ${headRec.confidence} confidence`);
|
|
1083
|
+
console.log(` ${headRec.reason}`);
|
|
1084
|
+
} catch {
|
|
1085
|
+
// Recommendation is advisory only; status should never fail because model probing failed.
|
|
1086
|
+
}
|
|
1050
1087
|
|
|
1051
1088
|
// Runtime
|
|
1052
1089
|
console.log('\nRuntime:');
|
|
@@ -1229,11 +1266,15 @@ async function cmdHandoff(args = []) {
|
|
|
1229
1266
|
console.log(` ⚡ Switching to ${target}...`);
|
|
1230
1267
|
console.log('');
|
|
1231
1268
|
const { spawnHandoff } = autoHandoff;
|
|
1269
|
+
const effectiveAutomode = getEffectiveAutomode(profile, cwd);
|
|
1270
|
+
const effectiveBypass = getEffectiveBypassPermissions(cwd);
|
|
1232
1271
|
const result = spawnHandoff({
|
|
1233
1272
|
fromProvider,
|
|
1234
1273
|
cwd,
|
|
1235
1274
|
auto: true,
|
|
1236
1275
|
force: true,
|
|
1276
|
+
automode: effectiveAutomode,
|
|
1277
|
+
bypassPermissions: effectiveBypass,
|
|
1237
1278
|
interactive: true,
|
|
1238
1279
|
taskBrief: typeof taskBrief === 'string' ? taskBrief : undefined,
|
|
1239
1280
|
});
|
|
@@ -1252,14 +1293,28 @@ async function cmdHandoff(args = []) {
|
|
|
1252
1293
|
fxH.info(ux.text);
|
|
1253
1294
|
console.log('');
|
|
1254
1295
|
const { spawnHandoff } = autoHandoff;
|
|
1255
|
-
const result = spawnHandoff({
|
|
1296
|
+
const result = spawnHandoff({
|
|
1297
|
+
fromProvider: 'anthropic',
|
|
1298
|
+
cwd,
|
|
1299
|
+
auto: true,
|
|
1300
|
+
automode: getEffectiveAutomode(profile, cwd),
|
|
1301
|
+
bypassPermissions: getEffectiveBypassPermissions(cwd),
|
|
1302
|
+
interactive: true,
|
|
1303
|
+
});
|
|
1256
1304
|
if (!result.success) fxH.error(result.message);
|
|
1257
1305
|
} else if (openaiStatus.limited && openaiStatus.otherAvailable) {
|
|
1258
1306
|
const ux = getHandoffUX(openaiStatus);
|
|
1259
1307
|
fxH.info(ux.text);
|
|
1260
1308
|
console.log('');
|
|
1261
1309
|
const { spawnHandoff } = autoHandoff;
|
|
1262
|
-
const result = spawnHandoff({
|
|
1310
|
+
const result = spawnHandoff({
|
|
1311
|
+
fromProvider: 'openai',
|
|
1312
|
+
cwd,
|
|
1313
|
+
auto: true,
|
|
1314
|
+
automode: getEffectiveAutomode(profile, cwd),
|
|
1315
|
+
bypassPermissions: getEffectiveBypassPermissions(cwd),
|
|
1316
|
+
interactive: true,
|
|
1317
|
+
});
|
|
1263
1318
|
if (!result.success) fxH.error(result.message);
|
|
1264
1319
|
} else {
|
|
1265
1320
|
fxH.success('No provider is currently limited. No handoff needed.');
|
|
@@ -1278,6 +1333,78 @@ async function cmdSwitch(args = []) {
|
|
|
1278
1333
|
await cmdHandoff(handoffArgs);
|
|
1279
1334
|
}
|
|
1280
1335
|
|
|
1336
|
+
async function cmdRuntimeSwitch(args = []) {
|
|
1337
|
+
const cwd = process.cwd();
|
|
1338
|
+
let sessions = [];
|
|
1339
|
+
try { sessions = enrichSessions(importReplitSessions(cwd), cwd); } catch {}
|
|
1340
|
+
const sess = sessions[0];
|
|
1341
|
+
if (!sess) {
|
|
1342
|
+
console.log('No resumable session found.');
|
|
1343
|
+
return;
|
|
1344
|
+
}
|
|
1345
|
+
|
|
1346
|
+
const profile = loadProfile(cwd);
|
|
1347
|
+
const settings = loadSessionSettings(cwd);
|
|
1348
|
+
let provider = _sessionTool(sess);
|
|
1349
|
+
let reason = 'head-runtime-switch';
|
|
1350
|
+
const confirmed = args.includes('--confirm') || args.includes('--go');
|
|
1351
|
+
|
|
1352
|
+
for (let i = 0; i < args.length; i += 1) {
|
|
1353
|
+
const a = args[i];
|
|
1354
|
+
if ((a === '--provider' || a === '--to') && args[i + 1]) {
|
|
1355
|
+
const next = args[i + 1].toLowerCase();
|
|
1356
|
+
if (next === 'gpt' || next === 'openai') provider = 'codex';
|
|
1357
|
+
else if (next === 'codex' || next === 'claude') provider = next;
|
|
1358
|
+
}
|
|
1359
|
+
if ((a === '--model' || a === '--head-model') && args[i + 1]) {
|
|
1360
|
+
settings.headModel = args[i + 1];
|
|
1361
|
+
}
|
|
1362
|
+
if (a === '--effort' && args[i + 1]) {
|
|
1363
|
+
settings.effort = args[i + 1];
|
|
1364
|
+
}
|
|
1365
|
+
if (a === '--reason' && args[i + 1]) reason = args[i + 1];
|
|
1366
|
+
}
|
|
1367
|
+
|
|
1368
|
+
if (args.includes('--smart-auto') || args.includes('--automode') || args.includes('--auto')) {
|
|
1369
|
+
settings.automode = true;
|
|
1370
|
+
settings.bypassPermissions = false;
|
|
1371
|
+
}
|
|
1372
|
+
if (args.includes('--manual')) {
|
|
1373
|
+
settings.automode = false;
|
|
1374
|
+
settings.bypassPermissions = false;
|
|
1375
|
+
}
|
|
1376
|
+
if (args.includes('--bypass')) {
|
|
1377
|
+
settings.automode = true;
|
|
1378
|
+
settings.bypassPermissions = true;
|
|
1379
|
+
}
|
|
1380
|
+
saveSessionSettings(cwd, settings);
|
|
1381
|
+
|
|
1382
|
+
const pending = writePendingRuntimeSwitch(cwd, sess, {
|
|
1383
|
+
provider,
|
|
1384
|
+
automode: getEffectiveAutomode(profile, cwd),
|
|
1385
|
+
bypassPermissions: getEffectiveBypassPermissions(cwd),
|
|
1386
|
+
model: settings.headModel || null,
|
|
1387
|
+
effort: settings.effort || null,
|
|
1388
|
+
confirmed,
|
|
1389
|
+
reason,
|
|
1390
|
+
});
|
|
1391
|
+
|
|
1392
|
+
const launchArgs = provider === _sessionTool(sess)
|
|
1393
|
+
? _sessionLaunchArgs(sess, cwd)
|
|
1394
|
+
: ['handoff', '--to', provider];
|
|
1395
|
+
|
|
1396
|
+
console.log('');
|
|
1397
|
+
console.log(`Runtime switch ${confirmed ? 'confirmed' : 'prepared'} for ${pending?.sessionName || sess.id}`);
|
|
1398
|
+
console.log(`Provider: ${provider}`);
|
|
1399
|
+
console.log(`Mode: ${getEffectiveConversationMode(profile, cwd)}`);
|
|
1400
|
+
console.log(`Launch: ${provider} ${launchArgs.join(' ')}`);
|
|
1401
|
+
console.log('');
|
|
1402
|
+
|
|
1403
|
+
if (args.includes('--apply')) {
|
|
1404
|
+
await processPendingRuntimeSwitch(cwd);
|
|
1405
|
+
}
|
|
1406
|
+
}
|
|
1407
|
+
|
|
1281
1408
|
async function cmdUpdate() {
|
|
1282
1409
|
const cwd = process.cwd();
|
|
1283
1410
|
console.log(' Updating Dual Brain...');
|
|
@@ -2121,6 +2248,15 @@ function getEffectiveBypassPermissions(cwd) {
|
|
|
2121
2248
|
return !!profile.bypassPermissions;
|
|
2122
2249
|
}
|
|
2123
2250
|
|
|
2251
|
+
function getEffectiveConversationMode(profile, cwd) {
|
|
2252
|
+
const workspace = cwd || process.cwd();
|
|
2253
|
+
const automode = getEffectiveAutomode(profile || loadProfile(workspace), workspace);
|
|
2254
|
+
const bypassPermissions = getEffectiveBypassPermissions(workspace);
|
|
2255
|
+
if (bypassPermissions) return 'bypass';
|
|
2256
|
+
if (!automode) return 'manual';
|
|
2257
|
+
return 'smart-auto';
|
|
2258
|
+
}
|
|
2259
|
+
|
|
2124
2260
|
function parseModeCommand(input) {
|
|
2125
2261
|
const text = input.trim().toLowerCase().replace(/\s+/g, ' ');
|
|
2126
2262
|
const wantsSession = /\b(this|current)\s+(session|conversation|terminal|chat)\b|\bfor this\b|\bfor current\b/.test(text);
|
|
@@ -2128,7 +2264,8 @@ function parseModeCommand(input) {
|
|
|
2128
2264
|
const scope = wantsGlobal && !wantsSession ? 'profile' : 'session';
|
|
2129
2265
|
|
|
2130
2266
|
let key = null;
|
|
2131
|
-
if (/\b(
|
|
2267
|
+
if (/\b(manual|ask me|ask before|approve every|approval every)\b/.test(text)) key = 'manualMode';
|
|
2268
|
+
if (/\b(smart auto|auto|automode|auto mode)\b/.test(text)) key = 'automode';
|
|
2132
2269
|
if (/\b(bypass|permission|permissions|approval|approvals|sandbox|safe mode)\b/.test(text)) key = 'bypassPermissions';
|
|
2133
2270
|
if (!key) return null;
|
|
2134
2271
|
|
|
@@ -2148,18 +2285,78 @@ function applyModeCommand(cmd, cwd) {
|
|
|
2148
2285
|
const profile = loadProfile(cwd);
|
|
2149
2286
|
if (cmd.key === 'automode') {
|
|
2150
2287
|
profile.automode = cmd.value;
|
|
2288
|
+
if (cmd.value) profile.bypassPermissions = false;
|
|
2151
2289
|
profile.settings = { ...(profile.settings || {}), automode: cmd.value };
|
|
2290
|
+
} else if (cmd.key === 'manualMode') {
|
|
2291
|
+
profile.automode = !cmd.value;
|
|
2292
|
+
if (cmd.value) profile.bypassPermissions = false;
|
|
2293
|
+
profile.settings = { ...(profile.settings || {}), automode: !cmd.value };
|
|
2152
2294
|
} else {
|
|
2153
2295
|
profile.bypassPermissions = cmd.value;
|
|
2296
|
+
if (cmd.value) {
|
|
2297
|
+
profile.automode = true;
|
|
2298
|
+
profile.settings = { ...(profile.settings || {}), automode: true };
|
|
2299
|
+
}
|
|
2154
2300
|
}
|
|
2155
2301
|
saveProfile(profile, { cwd });
|
|
2156
|
-
return { scope: 'default', value: cmd.value };
|
|
2302
|
+
return { scope: 'default', value: cmd.value, key: cmd.key };
|
|
2157
2303
|
}
|
|
2158
2304
|
|
|
2159
2305
|
const settings = loadSessionSettings(cwd);
|
|
2160
|
-
|
|
2306
|
+
if (cmd.key === 'automode') {
|
|
2307
|
+
settings.automode = cmd.value;
|
|
2308
|
+
if (cmd.value) settings.bypassPermissions = false;
|
|
2309
|
+
} else if (cmd.key === 'manualMode') {
|
|
2310
|
+
settings.automode = !cmd.value;
|
|
2311
|
+
if (cmd.value) settings.bypassPermissions = false;
|
|
2312
|
+
} else {
|
|
2313
|
+
settings.bypassPermissions = cmd.value;
|
|
2314
|
+
if (cmd.value) settings.automode = true;
|
|
2315
|
+
}
|
|
2161
2316
|
saveSessionSettings(cwd, settings);
|
|
2162
|
-
return { scope: 'this session', value: cmd.value };
|
|
2317
|
+
return { scope: 'this session', value: cmd.value, key: cmd.key };
|
|
2318
|
+
}
|
|
2319
|
+
|
|
2320
|
+
async function offerRuntimeReloadAfterModeChange(cwd, ask, recentSessions, label = 'runtime settings') {
|
|
2321
|
+
const cyan = '\x1b[36m';
|
|
2322
|
+
const reset = '\x1b[0m';
|
|
2323
|
+
let candidates = Array.isArray(recentSessions) ? recentSessions : [];
|
|
2324
|
+
if (candidates.length === 0) {
|
|
2325
|
+
try { candidates = enrichSessions(importReplitSessions(cwd), cwd).slice(0, 3); } catch {}
|
|
2326
|
+
}
|
|
2327
|
+
const sess = candidates[0] || null;
|
|
2328
|
+
process.stdout.write(' Provider approval flags are applied when Claude/Codex starts.\n');
|
|
2329
|
+
if (!sess) {
|
|
2330
|
+
process.stdout.write(` ${label} will apply to the next session you launch through dual-brain.\n\n`);
|
|
2331
|
+
await ask(' Press Enter to continue...');
|
|
2332
|
+
return { next: 'main' };
|
|
2333
|
+
}
|
|
2334
|
+
|
|
2335
|
+
const tool = _sessionTool(sess);
|
|
2336
|
+
const launchArgs = _sessionLaunchArgs(sess, cwd);
|
|
2337
|
+
writePendingRuntimeSwitch(cwd, sess, {
|
|
2338
|
+
provider: tool,
|
|
2339
|
+
automode: getEffectiveAutomode(loadProfile(cwd), cwd),
|
|
2340
|
+
bypassPermissions: getEffectiveBypassPermissions(cwd),
|
|
2341
|
+
reason: `${label}-reload`,
|
|
2342
|
+
});
|
|
2343
|
+
process.stdout.write(` Reload needed: ${tool} must restart with: ${launchArgs.join(' ')}\n\n`);
|
|
2344
|
+
process.stdout.write(` ${cyan}Enter${reset} reload last session now ${cyan}n${reset} later\n\n`);
|
|
2345
|
+
const choice = (await ask(' Choice: ')).trim().toLowerCase();
|
|
2346
|
+
if (choice === 'n' || choice === 'no' || choice === 'later') {
|
|
2347
|
+
process.stdout.write('\n Saved. It will apply on the next dual-brain resume/switch.\n\n');
|
|
2348
|
+
await ask(' Press Enter to continue...');
|
|
2349
|
+
return { next: 'main' };
|
|
2350
|
+
}
|
|
2351
|
+
writePendingRuntimeSwitch(cwd, sess, {
|
|
2352
|
+
provider: tool,
|
|
2353
|
+
automode: getEffectiveAutomode(loadProfile(cwd), cwd),
|
|
2354
|
+
bypassPermissions: getEffectiveBypassPermissions(cwd),
|
|
2355
|
+
reason: `${label}-reload`,
|
|
2356
|
+
confirmed: true,
|
|
2357
|
+
});
|
|
2358
|
+
const launched = await processPendingRuntimeSwitch(cwd);
|
|
2359
|
+
return launched ? { next: 'main' } : await launchSessionWithLease(sess, cwd, ask);
|
|
2163
2360
|
}
|
|
2164
2361
|
|
|
2165
2362
|
function pidAlive(pid) {
|
|
@@ -2174,6 +2371,69 @@ function activeConversationPath(cwd) {
|
|
|
2174
2371
|
return join(cwd, '.dualbrain', 'active-conversation.json');
|
|
2175
2372
|
}
|
|
2176
2373
|
|
|
2374
|
+
function pendingRuntimeSwitchPath(cwd) {
|
|
2375
|
+
return join(cwd, '.dualbrain', 'pending-runtime-switch.json');
|
|
2376
|
+
}
|
|
2377
|
+
|
|
2378
|
+
function writePendingRuntimeSwitch(cwd, session, updates = {}) {
|
|
2379
|
+
if (!session?.id) return null;
|
|
2380
|
+
const dir = join(cwd, '.dualbrain');
|
|
2381
|
+
const tool = updates.provider || _sessionTool(session);
|
|
2382
|
+
const pending = {
|
|
2383
|
+
id: `runtime-switch-${Date.now()}`,
|
|
2384
|
+
status: updates.confirmed ? 'confirmed' : 'prepared',
|
|
2385
|
+
createdAt: new Date().toISOString(),
|
|
2386
|
+
sessionId: session.id,
|
|
2387
|
+
sessionName: session.smartName || session.name || session.prompts?.first || session.firstPrompt || session.id,
|
|
2388
|
+
fromProvider: _sessionTool(session),
|
|
2389
|
+
provider: tool,
|
|
2390
|
+
model: updates.model || null,
|
|
2391
|
+
effort: updates.effort || null,
|
|
2392
|
+
automode: typeof updates.automode === 'boolean' ? updates.automode : null,
|
|
2393
|
+
bypassPermissions: typeof updates.bypassPermissions === 'boolean' ? updates.bypassPermissions : null,
|
|
2394
|
+
reason: updates.reason || 'runtime-settings-change',
|
|
2395
|
+
autoLaunch: updates.autoLaunch !== false,
|
|
2396
|
+
handoffBrief: updates.handoffBrief || _sessionBrief(session, tool),
|
|
2397
|
+
};
|
|
2398
|
+
mkdirSync(dir, { recursive: true });
|
|
2399
|
+
writeFileSync(pendingRuntimeSwitchPath(cwd), JSON.stringify(pending, null, 2) + '\n');
|
|
2400
|
+
return pending;
|
|
2401
|
+
}
|
|
2402
|
+
|
|
2403
|
+
function readPendingRuntimeSwitch(cwd) {
|
|
2404
|
+
try {
|
|
2405
|
+
const pending = JSON.parse(readFileSync(pendingRuntimeSwitchPath(cwd), 'utf8'));
|
|
2406
|
+
return pending?.sessionId ? pending : null;
|
|
2407
|
+
} catch { return null; }
|
|
2408
|
+
}
|
|
2409
|
+
|
|
2410
|
+
function clearPendingRuntimeSwitch(cwd) {
|
|
2411
|
+
try { unlinkSync(pendingRuntimeSwitchPath(cwd)); } catch {}
|
|
2412
|
+
}
|
|
2413
|
+
|
|
2414
|
+
function runtimeLaunchArgsForPending(session, cwd, pending) {
|
|
2415
|
+
const tool = pending?.provider === 'codex' || pending?.provider === 'claude'
|
|
2416
|
+
? pending.provider
|
|
2417
|
+
: _sessionTool(session);
|
|
2418
|
+
if (tool === 'codex') {
|
|
2419
|
+
if (pending?.bypassPermissions) return ['--dangerously-bypass-approvals-and-sandbox', 'resume', session.id];
|
|
2420
|
+
return [
|
|
2421
|
+
..._codexApprovalArgs(cwd, pending?.automode ? 'never' : 'on-request'),
|
|
2422
|
+
'resume', session.id,
|
|
2423
|
+
];
|
|
2424
|
+
}
|
|
2425
|
+
|
|
2426
|
+
const args = ['--resume', session.id];
|
|
2427
|
+
if (pending?.bypassPermissions) {
|
|
2428
|
+
args.push('--dangerously-skip-permissions');
|
|
2429
|
+
} else if (pending?.automode) {
|
|
2430
|
+
if (pending.model) args.push('--model', pending.model);
|
|
2431
|
+
if (pending.effort) args.push('--effort', pending.effort);
|
|
2432
|
+
args.push('--permission-mode', 'auto');
|
|
2433
|
+
}
|
|
2434
|
+
return args;
|
|
2435
|
+
}
|
|
2436
|
+
|
|
2177
2437
|
function readActiveConversation(cwd) {
|
|
2178
2438
|
try {
|
|
2179
2439
|
const lease = JSON.parse(readFileSync(activeConversationPath(cwd), 'utf8'));
|
|
@@ -2183,7 +2443,46 @@ function readActiveConversation(cwd) {
|
|
|
2183
2443
|
} catch { return null; }
|
|
2184
2444
|
}
|
|
2185
2445
|
|
|
2186
|
-
function
|
|
2446
|
+
async function processPendingRuntimeSwitch(cwd) {
|
|
2447
|
+
const pending = readPendingRuntimeSwitch(cwd);
|
|
2448
|
+
if (!pending || pending.status !== 'confirmed' || pending.autoLaunch === false) return false;
|
|
2449
|
+
|
|
2450
|
+
let sessions = [];
|
|
2451
|
+
try { sessions = enrichSessions(importReplitSessions(cwd), cwd); } catch {}
|
|
2452
|
+
const sess = sessions.find(s => s.id === pending.sessionId) || sessions[0];
|
|
2453
|
+
if (!sess) {
|
|
2454
|
+
clearPendingRuntimeSwitch(cwd);
|
|
2455
|
+
return false;
|
|
2456
|
+
}
|
|
2457
|
+
|
|
2458
|
+
const currentTool = _sessionTool(sess);
|
|
2459
|
+
const targetTool = pending.provider === 'codex' || pending.provider === 'claude'
|
|
2460
|
+
? pending.provider
|
|
2461
|
+
: currentTool;
|
|
2462
|
+
|
|
2463
|
+
process.stdout.write(`\n Reloading HEAD with ${pending.automode ? 'Smart Auto' : 'updated settings'}...\n`);
|
|
2464
|
+
clearPendingRuntimeSwitch(cwd);
|
|
2465
|
+
|
|
2466
|
+
if (targetTool !== currentTool) {
|
|
2467
|
+
const brief = pending.handoffBrief || _sessionBrief(sess, targetTool);
|
|
2468
|
+
writeHandoffConversationLease(cwd, sess, currentTool, targetTool, brief);
|
|
2469
|
+
markSessionSuperseded(cwd, sess, targetTool, pending.reason || 'runtime-switch');
|
|
2470
|
+
await launchSupervisedHandoff(sess, cwd, currentTool, targetTool, brief, pending);
|
|
2471
|
+
return true;
|
|
2472
|
+
}
|
|
2473
|
+
|
|
2474
|
+
const launchArgs = runtimeLaunchArgsForPending(sess, cwd, pending);
|
|
2475
|
+
process.stdout.write(` Launching: ${targetTool} ${launchArgs.join(' ')}\n\n`);
|
|
2476
|
+
writeActiveConversation(cwd, sess, targetTool);
|
|
2477
|
+
try {
|
|
2478
|
+
await launchSupervisedHead(targetTool, launchArgs, cwd, sess);
|
|
2479
|
+
} finally {
|
|
2480
|
+
clearActiveConversation(cwd, sess.id);
|
|
2481
|
+
}
|
|
2482
|
+
return true;
|
|
2483
|
+
}
|
|
2484
|
+
|
|
2485
|
+
function writeActiveConversation(cwd, session, tool, extra = {}) {
|
|
2187
2486
|
const dir = join(cwd, '.dualbrain');
|
|
2188
2487
|
const terminalId = getTerminalId();
|
|
2189
2488
|
const lease = {
|
|
@@ -2192,6 +2491,7 @@ function writeActiveConversation(cwd, session, tool) {
|
|
|
2192
2491
|
provider: tool,
|
|
2193
2492
|
terminalId,
|
|
2194
2493
|
ownerPid: process.pid,
|
|
2494
|
+
childPid: extra.childPid || null,
|
|
2195
2495
|
startedAt: new Date().toISOString(),
|
|
2196
2496
|
lastHeartbeat: new Date().toISOString(),
|
|
2197
2497
|
mode: 'active-head',
|
|
@@ -2201,6 +2501,85 @@ function writeActiveConversation(cwd, session, tool) {
|
|
|
2201
2501
|
return lease;
|
|
2202
2502
|
}
|
|
2203
2503
|
|
|
2504
|
+
function updateActiveConversation(cwd, sessionId, updates = {}) {
|
|
2505
|
+
try {
|
|
2506
|
+
const lease = JSON.parse(readFileSync(activeConversationPath(cwd), 'utf8'));
|
|
2507
|
+
if (sessionId && lease.sessionId !== sessionId) return;
|
|
2508
|
+
writeFileSync(activeConversationPath(cwd), JSON.stringify({
|
|
2509
|
+
...lease,
|
|
2510
|
+
...updates,
|
|
2511
|
+
lastHeartbeat: new Date().toISOString(),
|
|
2512
|
+
}, null, 2) + '\n');
|
|
2513
|
+
} catch {}
|
|
2514
|
+
}
|
|
2515
|
+
|
|
2516
|
+
function writeHandoffConversationLease(cwd, session, fromTool, targetTool, taskBrief = '') {
|
|
2517
|
+
const dir = join(cwd, '.dualbrain');
|
|
2518
|
+
const terminalId = getTerminalId();
|
|
2519
|
+
const lease = {
|
|
2520
|
+
conversationId: session.id,
|
|
2521
|
+
sessionId: session.id,
|
|
2522
|
+
provider: targetTool,
|
|
2523
|
+
previousProvider: fromTool,
|
|
2524
|
+
terminalId,
|
|
2525
|
+
ownerPid: process.pid,
|
|
2526
|
+
startedAt: new Date().toISOString(),
|
|
2527
|
+
lastHeartbeat: new Date().toISOString(),
|
|
2528
|
+
mode: 'handoff-head',
|
|
2529
|
+
taskBrief: taskBrief || null,
|
|
2530
|
+
};
|
|
2531
|
+
mkdirSync(dir, { recursive: true });
|
|
2532
|
+
writeFileSync(activeConversationPath(cwd), JSON.stringify(lease, null, 2) + '\n');
|
|
2533
|
+
return lease;
|
|
2534
|
+
}
|
|
2535
|
+
|
|
2536
|
+
function markSessionSuperseded(cwd, session, targetTool, reason = 'provider-switch') {
|
|
2537
|
+
if (!session?.id) return;
|
|
2538
|
+
try {
|
|
2539
|
+
const meta = getSessionMeta(cwd);
|
|
2540
|
+
const existing = meta[session.id] || {};
|
|
2541
|
+
meta[session.id] = {
|
|
2542
|
+
...existing,
|
|
2543
|
+
id: session.id,
|
|
2544
|
+
tool: session.tool || _sessionTool(session),
|
|
2545
|
+
status: 'superseded',
|
|
2546
|
+
supersededByProvider: targetTool,
|
|
2547
|
+
supersededReason: reason,
|
|
2548
|
+
supersededAt: new Date().toISOString(),
|
|
2549
|
+
terminalId: getTerminalId(),
|
|
2550
|
+
createdAt: existing.createdAt || session.date || new Date().toISOString(),
|
|
2551
|
+
};
|
|
2552
|
+
saveSessionMeta(meta, cwd);
|
|
2553
|
+
} catch {}
|
|
2554
|
+
}
|
|
2555
|
+
|
|
2556
|
+
function setSessionResumeProvider(cwd, session, targetTool, reason = 'user-preference') {
|
|
2557
|
+
if (!session?.id || !targetTool) return;
|
|
2558
|
+
try {
|
|
2559
|
+
const meta = getSessionMeta(cwd);
|
|
2560
|
+
const existing = meta[session.id] || {};
|
|
2561
|
+
meta[session.id] = {
|
|
2562
|
+
...existing,
|
|
2563
|
+
id: session.id,
|
|
2564
|
+
tool: session.tool || _sessionTool(session),
|
|
2565
|
+
resumeProvider: targetTool,
|
|
2566
|
+
resumeProviderReason: reason,
|
|
2567
|
+
resumeProviderSetAt: new Date().toISOString(),
|
|
2568
|
+
createdAt: existing.createdAt || session.date || new Date().toISOString(),
|
|
2569
|
+
};
|
|
2570
|
+
saveSessionMeta(meta, cwd);
|
|
2571
|
+
} catch {}
|
|
2572
|
+
}
|
|
2573
|
+
|
|
2574
|
+
function getSessionResumeProvider(cwd, session) {
|
|
2575
|
+
if (!session?.id) return null;
|
|
2576
|
+
try {
|
|
2577
|
+
const meta = getSessionMeta(cwd);
|
|
2578
|
+
const provider = meta[session.id]?.resumeProvider;
|
|
2579
|
+
return provider === 'claude' || provider === 'codex' ? provider : null;
|
|
2580
|
+
} catch { return null; }
|
|
2581
|
+
}
|
|
2582
|
+
|
|
2204
2583
|
function clearActiveConversation(cwd, sessionId) {
|
|
2205
2584
|
try {
|
|
2206
2585
|
const lease = JSON.parse(readFileSync(activeConversationPath(cwd), 'utf8'));
|
|
@@ -2210,6 +2589,124 @@ function clearActiveConversation(cwd, sessionId) {
|
|
|
2210
2589
|
} catch {}
|
|
2211
2590
|
}
|
|
2212
2591
|
|
|
2592
|
+
function pendingSwitchMatchesSession(pending, session) {
|
|
2593
|
+
if (!pending || pending.status !== 'confirmed' || pending.autoLaunch === false) return false;
|
|
2594
|
+
if (!session?.id) return true;
|
|
2595
|
+
return pending.sessionId === session.id;
|
|
2596
|
+
}
|
|
2597
|
+
|
|
2598
|
+
async function launchSupervisedHead(tool, launchArgs, cwd, session) {
|
|
2599
|
+
const { spawn } = await import('node:child_process');
|
|
2600
|
+
const env = tool === 'codex' && (!process.env.TERM || process.env.TERM === 'dumb')
|
|
2601
|
+
? { ...process.env, TERM: 'xterm-256color' }
|
|
2602
|
+
: process.env;
|
|
2603
|
+
let switching = false;
|
|
2604
|
+
let forceTimer = null;
|
|
2605
|
+
|
|
2606
|
+
return await new Promise((resolve) => {
|
|
2607
|
+
const child = spawn(tool, launchArgs, { stdio: 'inherit', cwd, env });
|
|
2608
|
+
updateActiveConversation(cwd, session?.id, { childPid: child.pid, provider: tool });
|
|
2609
|
+
|
|
2610
|
+
const stopChildForSwitch = () => {
|
|
2611
|
+
if (switching) return;
|
|
2612
|
+
switching = true;
|
|
2613
|
+
process.stdout.write('\n Switch confirmed — restarting HEAD with updated settings...\n');
|
|
2614
|
+
try { child.kill('SIGTERM'); } catch {}
|
|
2615
|
+
forceTimer = setTimeout(() => {
|
|
2616
|
+
try {
|
|
2617
|
+
if (!child.killed) child.kill('SIGKILL');
|
|
2618
|
+
} catch {}
|
|
2619
|
+
}, 2500);
|
|
2620
|
+
};
|
|
2621
|
+
|
|
2622
|
+
const watcher = setInterval(() => {
|
|
2623
|
+
const pending = readPendingRuntimeSwitch(cwd);
|
|
2624
|
+
if (pendingSwitchMatchesSession(pending, session)) stopChildForSwitch();
|
|
2625
|
+
}, 500);
|
|
2626
|
+
|
|
2627
|
+
child.on('exit', async (code, signal) => {
|
|
2628
|
+
clearInterval(watcher);
|
|
2629
|
+
if (forceTimer) clearTimeout(forceTimer);
|
|
2630
|
+
saveTerminalState(cwd, getTerminalId(), session?.id, session?.tool || tool);
|
|
2631
|
+
|
|
2632
|
+
if (switching) {
|
|
2633
|
+
const launched = await processPendingRuntimeSwitch(cwd);
|
|
2634
|
+
resolve({ switched: launched, code, signal });
|
|
2635
|
+
return;
|
|
2636
|
+
}
|
|
2637
|
+
|
|
2638
|
+
clearActiveConversation(cwd, session?.id);
|
|
2639
|
+
resolve({ switched: false, code, signal });
|
|
2640
|
+
});
|
|
2641
|
+
|
|
2642
|
+
child.on('error', (err) => {
|
|
2643
|
+
clearInterval(watcher);
|
|
2644
|
+
if (forceTimer) clearTimeout(forceTimer);
|
|
2645
|
+
process.stderr.write(`\n Could not launch ${tool}: ${err.message}\n`);
|
|
2646
|
+
clearActiveConversation(cwd, session?.id);
|
|
2647
|
+
resolve({ switched: false, error: err });
|
|
2648
|
+
});
|
|
2649
|
+
});
|
|
2650
|
+
}
|
|
2651
|
+
|
|
2652
|
+
async function launchSupervisedHandoff(session, cwd, currentTool, targetTool, brief, pending = {}) {
|
|
2653
|
+
let autoHandoff;
|
|
2654
|
+
try {
|
|
2655
|
+
autoHandoff = await import('../dist/src/auto-handoff.js');
|
|
2656
|
+
} catch (e) {
|
|
2657
|
+
process.stderr.write(`\n Could not load auto-handoff module: ${e.message}\n`);
|
|
2658
|
+
return { switched: false };
|
|
2659
|
+
}
|
|
2660
|
+
|
|
2661
|
+
const fromProvider = currentTool === 'codex' ? 'openai' : 'anthropic';
|
|
2662
|
+
const result = autoHandoff.executeHandoff({
|
|
2663
|
+
fromProvider,
|
|
2664
|
+
cwd,
|
|
2665
|
+
auto: !!pending.automode,
|
|
2666
|
+
force: true,
|
|
2667
|
+
taskBrief: brief,
|
|
2668
|
+
});
|
|
2669
|
+
if (!result.success || !result.contextFile) {
|
|
2670
|
+
process.stderr.write(`\n ${result.message || 'Handoff failed.'}\n`);
|
|
2671
|
+
return { switched: false };
|
|
2672
|
+
}
|
|
2673
|
+
|
|
2674
|
+
let prompt = '';
|
|
2675
|
+
try {
|
|
2676
|
+
const data = JSON.parse(readFileSync(result.contextFile, 'utf8'));
|
|
2677
|
+
prompt = data?.prompt || '';
|
|
2678
|
+
} catch {}
|
|
2679
|
+
if (!prompt) {
|
|
2680
|
+
process.stderr.write('\n Handoff context was created, but no prompt was available to launch the target provider.\n');
|
|
2681
|
+
return { switched: false };
|
|
2682
|
+
}
|
|
2683
|
+
|
|
2684
|
+
let launchArgs;
|
|
2685
|
+
if (targetTool === 'codex') {
|
|
2686
|
+
launchArgs = pending?.bypassPermissions
|
|
2687
|
+
? ['--dangerously-bypass-approvals-and-sandbox', prompt.slice(0, 4000)]
|
|
2688
|
+
: [
|
|
2689
|
+
..._codexApprovalArgs(cwd, pending?.automode ? 'never' : 'on-request'),
|
|
2690
|
+
prompt.slice(0, 4000),
|
|
2691
|
+
];
|
|
2692
|
+
} else {
|
|
2693
|
+
const permissionArgs = pending?.bypassPermissions
|
|
2694
|
+
? ['--dangerously-skip-permissions']
|
|
2695
|
+
: pending?.automode
|
|
2696
|
+
? ['--permission-mode', 'auto']
|
|
2697
|
+
: [];
|
|
2698
|
+
launchArgs = [...permissionArgs, prompt.slice(0, 4000)];
|
|
2699
|
+
}
|
|
2700
|
+
|
|
2701
|
+
process.stdout.write(` Launching: ${targetTool} ${launchArgs.slice(0, -1).join(' ')} [handoff context]\n\n`);
|
|
2702
|
+
writeActiveConversation(cwd, session, targetTool);
|
|
2703
|
+
try {
|
|
2704
|
+
return await launchSupervisedHead(targetTool, launchArgs, cwd, { ...session, tool: targetTool });
|
|
2705
|
+
} finally {
|
|
2706
|
+
clearActiveConversation(cwd, session?.id);
|
|
2707
|
+
}
|
|
2708
|
+
}
|
|
2709
|
+
|
|
2213
2710
|
async function confirmConversationTakeover(sess, cwd, ask) {
|
|
2214
2711
|
const active = readActiveConversation(cwd);
|
|
2215
2712
|
if (!active || active.sessionId !== sess.id || active.ownerPid === process.pid) return 'launch';
|
|
@@ -2234,6 +2731,15 @@ async function launchSessionWithLease(sess, cwd, ask = null) {
|
|
|
2234
2731
|
|
|
2235
2732
|
const { spawnSync } = await import('node:child_process');
|
|
2236
2733
|
const tool = _sessionTool(sess);
|
|
2734
|
+
const preferredTool = getSessionResumeProvider(cwd, sess);
|
|
2735
|
+
if (preferredTool && preferredTool !== tool) {
|
|
2736
|
+
const brief = _sessionBrief(sess, preferredTool);
|
|
2737
|
+
writeHandoffConversationLease(cwd, sess, tool, preferredTool, brief);
|
|
2738
|
+
markSessionSuperseded(cwd, sess, preferredTool, 'resume-provider-preference');
|
|
2739
|
+
process.stdout.write(`\n Continuing in ${preferredTool === 'codex' ? 'Codex/GPT' : 'Claude'} based on this conversation's setting.\n\n`);
|
|
2740
|
+
await cmdSwitch([preferredTool, brief]);
|
|
2741
|
+
return { next: 'main' };
|
|
2742
|
+
}
|
|
2237
2743
|
const launchArgs = _sessionLaunchArgs(sess, cwd);
|
|
2238
2744
|
if (decision === 'takeover') {
|
|
2239
2745
|
process.stdout.write(' Taking over active conversation in this terminal.\n');
|
|
@@ -2241,9 +2747,8 @@ async function launchSessionWithLease(sess, cwd, ask = null) {
|
|
|
2241
2747
|
writeActiveConversation(cwd, sess, tool);
|
|
2242
2748
|
process.stdout.write(`\n Launching: ${tool} ${launchArgs.join(' ')}\n\n`);
|
|
2243
2749
|
try {
|
|
2244
|
-
|
|
2750
|
+
await launchSupervisedHead(tool, launchArgs, cwd, sess);
|
|
2245
2751
|
} finally {
|
|
2246
|
-
saveTerminalState(cwd, getTerminalId(), sess.id, sess.tool || tool);
|
|
2247
2752
|
clearActiveConversation(cwd, sess.id);
|
|
2248
2753
|
}
|
|
2249
2754
|
return { next: 'main' };
|
|
@@ -3341,7 +3846,7 @@ async function mainScreen(rl, ask) {
|
|
|
3341
3846
|
[`s`, 'settings & profiles'],
|
|
3342
3847
|
[`d`, 'doctor — diagnose issues'],
|
|
3343
3848
|
[`t`, 'team settings'],
|
|
3344
|
-
[`a`, getEffectiveAutomode(profile, cwd) ? 'auto
|
|
3849
|
+
[`a`, getEffectiveAutomode(profile, cwd) && !getEffectiveBypassPermissions(cwd) ? 'smart auto (on)' : 'smart auto (off)'],
|
|
3345
3850
|
[`q`, 'quit'],
|
|
3346
3851
|
];
|
|
3347
3852
|
for (const [key, label] of shortcuts) {
|
|
@@ -3359,7 +3864,7 @@ async function mainScreen(rl, ask) {
|
|
|
3359
3864
|
|
|
3360
3865
|
// ── Key handling ──────────────────────────────────────────────────────────
|
|
3361
3866
|
// Use raw keypress mode so we can show a live type-to-start buffer.
|
|
3362
|
-
//
|
|
3867
|
+
// Shortcuts resolve on Enter; printable keys should never preempt natural text.
|
|
3363
3868
|
let taskBuffer = '';
|
|
3364
3869
|
|
|
3365
3870
|
const readline = await import('node:readline');
|
|
@@ -3410,8 +3915,15 @@ async function mainScreen(rl, ask) {
|
|
|
3410
3915
|
if (name === 'return' || name === 'enter' || seq === '\r' || seq === '\n') {
|
|
3411
3916
|
cleanup();
|
|
3412
3917
|
if (taskBuffer.length > 0) {
|
|
3918
|
+
const shortcut = taskBuffer.trim().toLowerCase();
|
|
3919
|
+
const singleKeySet = new Set(['n', 'g', 's', 't', 'q', '/', 'i', '?', 'h', 'd', 'a']);
|
|
3920
|
+
const digit = parseInt(shortcut, 10);
|
|
3413
3921
|
process.stdout.write('\n');
|
|
3414
|
-
|
|
3922
|
+
if (singleKeySet.has(shortcut) || (!isNaN(digit) && digit >= 1 && digit <= 9)) {
|
|
3923
|
+
resolve(shortcut);
|
|
3924
|
+
} else {
|
|
3925
|
+
resolve(`__task__:${taskBuffer}`);
|
|
3926
|
+
}
|
|
3415
3927
|
} else {
|
|
3416
3928
|
resolve('');
|
|
3417
3929
|
}
|
|
@@ -3439,25 +3951,6 @@ async function mainScreen(rl, ask) {
|
|
|
3439
3951
|
const code = str.codePointAt(0);
|
|
3440
3952
|
if (code < 32 || code === 127) return;
|
|
3441
3953
|
|
|
3442
|
-
// Single-key commands only fire when buffer is empty
|
|
3443
|
-
if (taskBuffer.length === 0) {
|
|
3444
|
-
const lower = str.toLowerCase();
|
|
3445
|
-
const singleKeySet = new Set(['n', 'g', 's', 't', 'q', '/', 'i', '?', 'h', 'd', 'a']);
|
|
3446
|
-
if (singleKeySet.has(lower)) {
|
|
3447
|
-
cleanup();
|
|
3448
|
-
process.stdout.write('\n');
|
|
3449
|
-
resolve(lower);
|
|
3450
|
-
return;
|
|
3451
|
-
}
|
|
3452
|
-
const digit = parseInt(str, 10);
|
|
3453
|
-
if (!isNaN(digit) && digit >= 1 && digit <= 9) {
|
|
3454
|
-
cleanup();
|
|
3455
|
-
process.stdout.write('\n');
|
|
3456
|
-
resolve(str);
|
|
3457
|
-
return;
|
|
3458
|
-
}
|
|
3459
|
-
}
|
|
3460
|
-
|
|
3461
3954
|
// Accumulate into buffer
|
|
3462
3955
|
taskBuffer += str;
|
|
3463
3956
|
renderBuffer(taskBuffer);
|
|
@@ -3482,11 +3975,17 @@ async function mainScreen(rl, ask) {
|
|
|
3482
3975
|
|
|
3483
3976
|
if (cmd === 'mode') {
|
|
3484
3977
|
const result = applyModeCommand(classified.modeCommand, cwd);
|
|
3485
|
-
const keyLabel = classified.modeCommand.key === '
|
|
3978
|
+
const keyLabel = classified.modeCommand.key === 'manualMode'
|
|
3979
|
+
? 'Manual mode'
|
|
3980
|
+
: classified.modeCommand.key === 'automode'
|
|
3981
|
+
? 'Smart Auto'
|
|
3982
|
+
: 'Bypass mode';
|
|
3486
3983
|
const state = result.value ? '\x1b[32mON\x1b[0m' : '\x1b[2mOFF\x1b[0m';
|
|
3487
|
-
|
|
3488
|
-
|
|
3489
|
-
|
|
3984
|
+
const profileNow = loadProfile(cwd);
|
|
3985
|
+
const modeNow = getEffectiveConversationMode(profileNow, cwd);
|
|
3986
|
+
process.stdout.write(`\n ${keyLabel}: ${state} \x1b[2m(${result.scope})\x1b[0m\n`);
|
|
3987
|
+
process.stdout.write(` Effective mode: ${modeNow === 'bypass' ? '\x1b[31mBypass\x1b[0m' : modeNow === 'manual' ? '\x1b[2mManual\x1b[0m' : '\x1b[32mSmart Auto\x1b[0m'}\n\n`);
|
|
3988
|
+
return await offerRuntimeReloadAfterModeChange(cwd, ask, recentSessions, keyLabel);
|
|
3490
3989
|
}
|
|
3491
3990
|
|
|
3492
3991
|
if (cmd === 'resume' || cmd === 'r') {
|
|
@@ -3569,12 +4068,12 @@ async function mainScreen(rl, ask) {
|
|
|
3569
4068
|
const nextAuto = !getEffectiveAutomode(prof, cwd2);
|
|
3570
4069
|
const settings = loadSessionSettings(cwd2);
|
|
3571
4070
|
settings.automode = nextAuto;
|
|
4071
|
+
if (nextAuto) settings.bypassPermissions = false;
|
|
3572
4072
|
saveSessionSettings(cwd2, settings);
|
|
3573
4073
|
const state = nextAuto ? '\x1b[32mON\x1b[0m' : '\x1b[2mOFF\x1b[0m';
|
|
3574
|
-
process.stdout.write(`\n
|
|
3575
|
-
process.stdout.write(` ${nextAuto ? '
|
|
3576
|
-
await
|
|
3577
|
-
return { next: 'main' };
|
|
4074
|
+
process.stdout.write(`\n Smart Auto: ${state} \x1b[2m(this session)\x1b[0m\n`);
|
|
4075
|
+
process.stdout.write(` ${nextAuto ? 'Safe tasks dispatch immediately; critical findings still interrupt.' : 'Manual mode: tasks require confirmation.'}\n\n`);
|
|
4076
|
+
return await offerRuntimeReloadAfterModeChange(cwd, ask, recentSessions, 'Smart Auto');
|
|
3578
4077
|
}
|
|
3579
4078
|
if (cmd === 'init --replit') {
|
|
3580
4079
|
await cmdInit(rl);
|
|
@@ -3764,10 +4263,10 @@ async function mainScreen(rl, ask) {
|
|
|
3764
4263
|
const nextAuto = !getEffectiveAutomode(prof, cwd);
|
|
3765
4264
|
const settings = loadSessionSettings(cwd);
|
|
3766
4265
|
settings.automode = nextAuto;
|
|
4266
|
+
if (nextAuto) settings.bypassPermissions = false;
|
|
3767
4267
|
saveSessionSettings(cwd, settings);
|
|
3768
|
-
process.stdout.write(`\n
|
|
3769
|
-
await
|
|
3770
|
-
return { next: 'main' };
|
|
4268
|
+
process.stdout.write(`\n Smart Auto: ${nextAuto ? '\x1b[32mON\x1b[0m' : '\x1b[2mOFF\x1b[0m'} \x1b[2m(this session)\x1b[0m\n\n`);
|
|
4269
|
+
return await offerRuntimeReloadAfterModeChange(cwd, ask, recentSessions, 'Smart Auto');
|
|
3771
4270
|
}
|
|
3772
4271
|
if (choice === 'q' || choice === 'exit') { return { next: 'exit' }; }
|
|
3773
4272
|
|
|
@@ -3802,11 +4301,20 @@ async function switchProviderScreen(rl, ask, ctx = {}) {
|
|
|
3802
4301
|
process.stdout.write('\n');
|
|
3803
4302
|
process.stdout.write(` Continue in ${target === 'codex' ? 'Codex/GPT' : 'Claude'}\n`);
|
|
3804
4303
|
process.stdout.write(` From: ${currentTool} · ${String(label || '').replace(/\s+/g, ' ').slice(0, 80)}\n\n`);
|
|
3805
|
-
process.stdout.write(` \x1b[36mEnter\x1b[0m switch now \x1b[36mb\x1b[0m back\n\n`);
|
|
4304
|
+
process.stdout.write(` \x1b[36mEnter\x1b[0m switch now \x1b[36mn\x1b[0m next resume \x1b[36mb\x1b[0m back\n\n`);
|
|
3806
4305
|
|
|
3807
4306
|
const choice = (await ask(' Choice: ')).trim().toLowerCase();
|
|
3808
|
-
if (choice === '
|
|
4307
|
+
if (choice === 'n' || choice === 'next') {
|
|
4308
|
+
setSessionResumeProvider(cwd, session, target, 'user-next-resume');
|
|
4309
|
+
process.stdout.write(`\n Next resume will continue this conversation in ${target === 'codex' ? 'Codex/GPT' : 'Claude'}.\n\n`);
|
|
4310
|
+
await ask(' Press Enter to continue...');
|
|
4311
|
+
return { next: 'main' };
|
|
4312
|
+
}
|
|
4313
|
+
if (choice === 'b' || choice === 'q') return { next: 'main' };
|
|
3809
4314
|
|
|
4315
|
+
setSessionResumeProvider(cwd, session, target, 'user-switch-now');
|
|
4316
|
+
writeHandoffConversationLease(cwd, session, currentTool, target, brief);
|
|
4317
|
+
markSessionSuperseded(cwd, session, target, 'manual-provider-switch');
|
|
3810
4318
|
await cmdSwitch([target, brief]);
|
|
3811
4319
|
return { next: 'main' };
|
|
3812
4320
|
}
|
|
@@ -3827,7 +4335,7 @@ async function paletteHelpScreen(rl, ask) {
|
|
|
3827
4335
|
const CYAN = '\x1b[36m';
|
|
3828
4336
|
const lines = [
|
|
3829
4337
|
top,
|
|
3830
|
-
row(`${CYAN}Keyboard Shortcuts${RESET} (
|
|
4338
|
+
row(`${CYAN}Keyboard Shortcuts${RESET} (type key, then Enter)`),
|
|
3831
4339
|
sep,
|
|
3832
4340
|
row(`${CYAN}Enter${RESET} Resume last session`),
|
|
3833
4341
|
row(`${CYAN}n${RESET} New coding session`),
|
|
@@ -4345,6 +4853,7 @@ async function settingsScreen(rl, ask) {
|
|
|
4345
4853
|
const automode = getEffectiveAutomode(profile, cwd);
|
|
4346
4854
|
const sessionSettings = loadSessionSettings(cwd);
|
|
4347
4855
|
const bypassPermissions = getEffectiveBypassPermissions(cwd);
|
|
4856
|
+
const conversationMode = getEffectiveConversationMode(profile, cwd);
|
|
4348
4857
|
const autoScope = typeof sessionSettings.automode === 'boolean' ? 'session' : 'default';
|
|
4349
4858
|
const permissionScope = typeof sessionSettings.bypassPermissions === 'boolean' ? 'session' : 'default';
|
|
4350
4859
|
|
|
@@ -4393,10 +4902,17 @@ async function settingsScreen(rl, ask) {
|
|
|
4393
4902
|
const permMode = bypassPermissions
|
|
4394
4903
|
? `${RED}bypass approvals and sandbox${RESET}`
|
|
4395
4904
|
: `${GREEN}safe approvals + workspace sandbox${RESET}`;
|
|
4905
|
+
const modeLabel = conversationMode === 'bypass'
|
|
4906
|
+
? `${RED}Bypass${RESET}`
|
|
4907
|
+
: conversationMode === 'manual'
|
|
4908
|
+
? `${DIM}Manual${RESET}`
|
|
4909
|
+
: `${GREEN}Smart Auto${RESET}`;
|
|
4396
4910
|
const convLines = [
|
|
4397
|
-
` ${DIM}
|
|
4398
|
-
` ${DIM}
|
|
4399
|
-
` ${DIM}
|
|
4911
|
+
` ${DIM}Mode${RESET} ${modeLabel}`,
|
|
4912
|
+
` ${DIM}Manual${RESET} ${automode ? xmark + ' off' : chk + ' on'} ${DIM}[${autoScope}]${RESET}`,
|
|
4913
|
+
` ${DIM}Smart Auto${RESET} ${autoMark} ${automode ? 'safe tasks run immediately' : 'disabled'} ${DIM}[${autoScope}]${RESET}`,
|
|
4914
|
+
` ${DIM}Safety${RESET} ${permMark} ${permMode} ${DIM}[${permissionScope}]${RESET}`,
|
|
4915
|
+
` ${DIM}Claude resume${RESET} ${bypassPermissions ? '--dangerously-skip-permissions' : automode ? '--permission-mode auto' : 'default permissions'}`,
|
|
4400
4916
|
` ${DIM}Codex resume${RESET} ${bypassPermissions ? '--dangerously-bypass-approvals-and-sandbox' : `workspace-write + ${automode ? 'never ask' : 'on-request'}`}`,
|
|
4401
4917
|
];
|
|
4402
4918
|
|
|
@@ -4473,7 +4989,7 @@ async function settingsScreen(rl, ask) {
|
|
|
4473
4989
|
const convContent = [
|
|
4474
4990
|
...convLines.map(l => l.replace(/^ /, '')),
|
|
4475
4991
|
'',
|
|
4476
|
-
signalLine('info', `${DIM}[o] auto
|
|
4992
|
+
signalLine('info', `${DIM}[o] smart auto [m] manual [v] bypass/safety${RESET}`),
|
|
4477
4993
|
];
|
|
4478
4994
|
|
|
4479
4995
|
const sysContent = [
|
|
@@ -4518,23 +5034,34 @@ async function settingsScreen(rl, ask) {
|
|
|
4518
5034
|
|
|
4519
5035
|
// Conversation behavior toggles
|
|
4520
5036
|
if (choice === 'o') {
|
|
4521
|
-
const nextAuto = !automode;
|
|
4522
5037
|
const settings = loadSessionSettings(cwd);
|
|
4523
|
-
settings.automode =
|
|
5038
|
+
settings.automode = true;
|
|
5039
|
+
settings.bypassPermissions = false;
|
|
4524
5040
|
saveSessionSettings(cwd, settings);
|
|
4525
|
-
process.stdout.write(`\n
|
|
4526
|
-
await
|
|
4527
|
-
return { next: 'settings' };
|
|
5041
|
+
process.stdout.write(`\n Mode: ${GREEN}Smart Auto${RESET} ${DIM}(this session)${RESET}\n\n`);
|
|
5042
|
+
const reload = await offerRuntimeReloadAfterModeChange(cwd, ask, [], 'Smart Auto');
|
|
5043
|
+
return reload?.next === 'main' ? { next: 'settings' } : reload;
|
|
5044
|
+
}
|
|
5045
|
+
|
|
5046
|
+
if (choice === 'm') {
|
|
5047
|
+
const settings = loadSessionSettings(cwd);
|
|
5048
|
+
settings.automode = false;
|
|
5049
|
+
settings.bypassPermissions = false;
|
|
5050
|
+
saveSessionSettings(cwd, settings);
|
|
5051
|
+
process.stdout.write(`\n Mode: ${DIM}Manual${RESET} ${DIM}(this session)${RESET}\n\n`);
|
|
5052
|
+
const reload = await offerRuntimeReloadAfterModeChange(cwd, ask, [], 'Manual mode');
|
|
5053
|
+
return reload?.next === 'main' ? { next: 'settings' } : reload;
|
|
4528
5054
|
}
|
|
4529
5055
|
|
|
4530
5056
|
if (choice === 'v') {
|
|
4531
5057
|
if (bypassPermissions) {
|
|
4532
5058
|
const settings = loadSessionSettings(cwd);
|
|
4533
5059
|
settings.bypassPermissions = false;
|
|
5060
|
+
settings.automode = true;
|
|
4534
5061
|
saveSessionSettings(cwd, settings);
|
|
4535
|
-
process.stdout.write(`\n
|
|
4536
|
-
await
|
|
4537
|
-
return { next: 'settings' };
|
|
5062
|
+
process.stdout.write(`\n Mode: ${GREEN}Smart Auto${RESET}; safety boundary restored ${DIM}(this session)${RESET}\n\n`);
|
|
5063
|
+
const reload = await offerRuntimeReloadAfterModeChange(cwd, ask, [], 'Smart Auto');
|
|
5064
|
+
return reload?.next === 'main' ? { next: 'settings' } : reload;
|
|
4538
5065
|
}
|
|
4539
5066
|
|
|
4540
5067
|
process.stdout.write(`\n ${RED}Bypass mode disables provider approval prompts and sandboxing.${RESET}\n`);
|
|
@@ -4543,8 +5070,11 @@ async function settingsScreen(rl, ask) {
|
|
|
4543
5070
|
if (confirm === 'YES') {
|
|
4544
5071
|
const settings = loadSessionSettings(cwd);
|
|
4545
5072
|
settings.bypassPermissions = true;
|
|
5073
|
+
settings.automode = true;
|
|
4546
5074
|
saveSessionSettings(cwd, settings);
|
|
4547
|
-
process.stdout.write(`\n
|
|
5075
|
+
process.stdout.write(`\n Mode: ${RED}Bypass${RESET} ${DIM}(this session)${RESET}\n\n`);
|
|
5076
|
+
const reload = await offerRuntimeReloadAfterModeChange(cwd, ask, [], 'Bypass mode');
|
|
5077
|
+
return reload?.next === 'main' ? { next: 'settings' } : reload;
|
|
4548
5078
|
} else {
|
|
4549
5079
|
process.stdout.write('\n Permission mode unchanged.\n\n');
|
|
4550
5080
|
}
|
|
@@ -5846,16 +6376,7 @@ async function sessionDetailScreen(rl, ask, ctx = {}) {
|
|
|
5846
6376
|
const choice = (await ask(' Choice: ')).trim().toLowerCase();
|
|
5847
6377
|
|
|
5848
6378
|
if (choice === 'c' || choice === 'r') {
|
|
5849
|
-
|
|
5850
|
-
const launchArgs = _sessionLaunchArgs(sess, cwd);
|
|
5851
|
-
console.log(`\n Launching: ${tool} ${launchArgs.join(' ')}\n`);
|
|
5852
|
-
try {
|
|
5853
|
-
const { spawnSync } = await import('node:child_process');
|
|
5854
|
-
spawnSync(tool, launchArgs, { stdio: 'inherit' });
|
|
5855
|
-
} catch {
|
|
5856
|
-
console.log(` Could not launch ${tool} CLI.`);
|
|
5857
|
-
}
|
|
5858
|
-
return { next: 'dashboard' };
|
|
6379
|
+
return await launchSessionWithLease(sess, cwd, ask);
|
|
5859
6380
|
}
|
|
5860
6381
|
|
|
5861
6382
|
if (choice === 'g') {
|
|
@@ -6156,12 +6677,7 @@ async function sessionManageScreen(rl, ask, ctx = {}) {
|
|
|
6156
6677
|
}
|
|
6157
6678
|
|
|
6158
6679
|
if (choice === 'o') {
|
|
6159
|
-
|
|
6160
|
-
const tool = _sessionTool(sess);
|
|
6161
|
-
const launchArgs = _sessionLaunchArgs(sess, cwd);
|
|
6162
|
-
console.log(`\n Launching: ${tool} ${launchArgs.join(' ')}\n`);
|
|
6163
|
-
spawnSync(tool, launchArgs, { stdio: 'inherit' });
|
|
6164
|
-
return { next: 'sessions' };
|
|
6680
|
+
return await launchSessionWithLease(sess, cwd, ask);
|
|
6165
6681
|
}
|
|
6166
6682
|
|
|
6167
6683
|
if (choice === 'g') {
|
|
@@ -6454,6 +6970,15 @@ async function runScreens(startScreen = 'dashboard') {
|
|
|
6454
6970
|
let current = startScreen;
|
|
6455
6971
|
let ctx = {};
|
|
6456
6972
|
while (current && current !== 'exit') {
|
|
6973
|
+
if (current === 'main' || current === 'dashboard') {
|
|
6974
|
+
const launchedPending = await processPendingRuntimeSwitch(process.cwd());
|
|
6975
|
+
if (launchedPending) {
|
|
6976
|
+
current = 'main';
|
|
6977
|
+
ctx = {};
|
|
6978
|
+
continue;
|
|
6979
|
+
}
|
|
6980
|
+
}
|
|
6981
|
+
|
|
6457
6982
|
// Handle type-to-start dispatch from mainScreen — all work routes through pipeline.
|
|
6458
6983
|
if (current === 'go' && ctx.prompt) {
|
|
6459
6984
|
const prompt = ctx.prompt;
|
|
@@ -7228,6 +7753,7 @@ async function main() {
|
|
|
7228
7753
|
if (cmd === 'status') { await cmdStatus(args.slice(1)); return; }
|
|
7229
7754
|
if (cmd === 'handoff') { await cmdHandoff(args.slice(1)); return; }
|
|
7230
7755
|
if (cmd === 'switch') { await cmdSwitch(args.slice(1)); return; }
|
|
7756
|
+
if (cmd === 'runtime-switch') { await cmdRuntimeSwitch(args.slice(1)); return; }
|
|
7231
7757
|
if (cmd === 'update' || cmd === 'upgrade') { await cmdUpdate(); return; }
|
|
7232
7758
|
if (cmd === 'hot') { cmdHot(args[1]); return; }
|
|
7233
7759
|
if (cmd === 'cool') { cmdCool(args[1]); return; }
|
|
@@ -7292,7 +7818,7 @@ fi
|
|
|
7292
7818
|
// If cmd is not a recognized subcommand, treat the entire arg list as a task.
|
|
7293
7819
|
// e.g. `dual-brain fix failing tests` → same as `dual-brain go "fix failing tests"`
|
|
7294
7820
|
const KNOWN_COMMANDS = new Set([
|
|
7295
|
-
'menu', 'init', 'install', 'uninstall', 'auth', 'go', 'do', 'plan', 'ship', 'think', 'review', 'pr', 'status', 'handoff', 'switch', 'hot', 'cool',
|
|
7821
|
+
'menu', 'init', 'install', 'uninstall', 'auth', 'go', 'do', 'plan', 'ship', 'think', 'review', 'pr', 'status', 'handoff', 'switch', 'runtime-switch', 'hot', 'cool',
|
|
7296
7822
|
'remember', 'forget', 'break-glass', 'specialists', 'search', 'shell-hook', 'watch', 'update', 'upgrade',
|
|
7297
7823
|
'--help', '-h', '--version', '-v',
|
|
7298
7824
|
...Object.keys(loadSpecialistRegistry()),
|