dual-brain 0.3.25 → 0.3.26
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 +455 -59
- 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
|
|
|
@@ -88,7 +98,7 @@ async function _primeRegistryCache() {
|
|
|
88
98
|
}
|
|
89
99
|
|
|
90
100
|
import {
|
|
91
|
-
decideRoute, getAvailableModels,
|
|
101
|
+
decideRoute, getAvailableModels, recommendHeadModel,
|
|
92
102
|
} from '../dist/src/decide.js';
|
|
93
103
|
|
|
94
104
|
import {
|
|
@@ -1047,6 +1057,14 @@ async function cmdStatus(args = []) {
|
|
|
1047
1057
|
console.log(`\nHead model : ${getHeadModel(profile)}`);
|
|
1048
1058
|
console.log(`Mode : ${profile.mode}`);
|
|
1049
1059
|
console.log(`Solo brain : ${isSoloBrain(profile) ? 'yes' : 'no'}`);
|
|
1060
|
+
try {
|
|
1061
|
+
const headRec = recommendHeadModel(profile);
|
|
1062
|
+
const effort = headRec.effort ? ` (${headRec.effort})` : '';
|
|
1063
|
+
console.log(`Recommended: ${headRec.provider}:${headRec.model}${effort} · ${headRec.confidence} confidence`);
|
|
1064
|
+
console.log(` ${headRec.reason}`);
|
|
1065
|
+
} catch {
|
|
1066
|
+
// Recommendation is advisory only; status should never fail because model probing failed.
|
|
1067
|
+
}
|
|
1050
1068
|
|
|
1051
1069
|
// Runtime
|
|
1052
1070
|
console.log('\nRuntime:');
|
|
@@ -1229,11 +1247,15 @@ async function cmdHandoff(args = []) {
|
|
|
1229
1247
|
console.log(` ⚡ Switching to ${target}...`);
|
|
1230
1248
|
console.log('');
|
|
1231
1249
|
const { spawnHandoff } = autoHandoff;
|
|
1250
|
+
const effectiveAutomode = getEffectiveAutomode(profile, cwd);
|
|
1251
|
+
const effectiveBypass = getEffectiveBypassPermissions(cwd);
|
|
1232
1252
|
const result = spawnHandoff({
|
|
1233
1253
|
fromProvider,
|
|
1234
1254
|
cwd,
|
|
1235
1255
|
auto: true,
|
|
1236
1256
|
force: true,
|
|
1257
|
+
automode: effectiveAutomode,
|
|
1258
|
+
bypassPermissions: effectiveBypass,
|
|
1237
1259
|
interactive: true,
|
|
1238
1260
|
taskBrief: typeof taskBrief === 'string' ? taskBrief : undefined,
|
|
1239
1261
|
});
|
|
@@ -1252,14 +1274,28 @@ async function cmdHandoff(args = []) {
|
|
|
1252
1274
|
fxH.info(ux.text);
|
|
1253
1275
|
console.log('');
|
|
1254
1276
|
const { spawnHandoff } = autoHandoff;
|
|
1255
|
-
const result = spawnHandoff({
|
|
1277
|
+
const result = spawnHandoff({
|
|
1278
|
+
fromProvider: 'anthropic',
|
|
1279
|
+
cwd,
|
|
1280
|
+
auto: true,
|
|
1281
|
+
automode: getEffectiveAutomode(profile, cwd),
|
|
1282
|
+
bypassPermissions: getEffectiveBypassPermissions(cwd),
|
|
1283
|
+
interactive: true,
|
|
1284
|
+
});
|
|
1256
1285
|
if (!result.success) fxH.error(result.message);
|
|
1257
1286
|
} else if (openaiStatus.limited && openaiStatus.otherAvailable) {
|
|
1258
1287
|
const ux = getHandoffUX(openaiStatus);
|
|
1259
1288
|
fxH.info(ux.text);
|
|
1260
1289
|
console.log('');
|
|
1261
1290
|
const { spawnHandoff } = autoHandoff;
|
|
1262
|
-
const result = spawnHandoff({
|
|
1291
|
+
const result = spawnHandoff({
|
|
1292
|
+
fromProvider: 'openai',
|
|
1293
|
+
cwd,
|
|
1294
|
+
auto: true,
|
|
1295
|
+
automode: getEffectiveAutomode(profile, cwd),
|
|
1296
|
+
bypassPermissions: getEffectiveBypassPermissions(cwd),
|
|
1297
|
+
interactive: true,
|
|
1298
|
+
});
|
|
1263
1299
|
if (!result.success) fxH.error(result.message);
|
|
1264
1300
|
} else {
|
|
1265
1301
|
fxH.success('No provider is currently limited. No handoff needed.');
|
|
@@ -1278,6 +1314,78 @@ async function cmdSwitch(args = []) {
|
|
|
1278
1314
|
await cmdHandoff(handoffArgs);
|
|
1279
1315
|
}
|
|
1280
1316
|
|
|
1317
|
+
async function cmdRuntimeSwitch(args = []) {
|
|
1318
|
+
const cwd = process.cwd();
|
|
1319
|
+
let sessions = [];
|
|
1320
|
+
try { sessions = enrichSessions(importReplitSessions(cwd), cwd); } catch {}
|
|
1321
|
+
const sess = sessions[0];
|
|
1322
|
+
if (!sess) {
|
|
1323
|
+
console.log('No resumable session found.');
|
|
1324
|
+
return;
|
|
1325
|
+
}
|
|
1326
|
+
|
|
1327
|
+
const profile = loadProfile(cwd);
|
|
1328
|
+
const settings = loadSessionSettings(cwd);
|
|
1329
|
+
let provider = _sessionTool(sess);
|
|
1330
|
+
let reason = 'head-runtime-switch';
|
|
1331
|
+
const confirmed = args.includes('--confirm') || args.includes('--go');
|
|
1332
|
+
|
|
1333
|
+
for (let i = 0; i < args.length; i += 1) {
|
|
1334
|
+
const a = args[i];
|
|
1335
|
+
if ((a === '--provider' || a === '--to') && args[i + 1]) {
|
|
1336
|
+
const next = args[i + 1].toLowerCase();
|
|
1337
|
+
if (next === 'gpt' || next === 'openai') provider = 'codex';
|
|
1338
|
+
else if (next === 'codex' || next === 'claude') provider = next;
|
|
1339
|
+
}
|
|
1340
|
+
if ((a === '--model' || a === '--head-model') && args[i + 1]) {
|
|
1341
|
+
settings.headModel = args[i + 1];
|
|
1342
|
+
}
|
|
1343
|
+
if (a === '--effort' && args[i + 1]) {
|
|
1344
|
+
settings.effort = args[i + 1];
|
|
1345
|
+
}
|
|
1346
|
+
if (a === '--reason' && args[i + 1]) reason = args[i + 1];
|
|
1347
|
+
}
|
|
1348
|
+
|
|
1349
|
+
if (args.includes('--smart-auto') || args.includes('--automode') || args.includes('--auto')) {
|
|
1350
|
+
settings.automode = true;
|
|
1351
|
+
settings.bypassPermissions = false;
|
|
1352
|
+
}
|
|
1353
|
+
if (args.includes('--manual')) {
|
|
1354
|
+
settings.automode = false;
|
|
1355
|
+
settings.bypassPermissions = false;
|
|
1356
|
+
}
|
|
1357
|
+
if (args.includes('--bypass')) {
|
|
1358
|
+
settings.automode = true;
|
|
1359
|
+
settings.bypassPermissions = true;
|
|
1360
|
+
}
|
|
1361
|
+
saveSessionSettings(cwd, settings);
|
|
1362
|
+
|
|
1363
|
+
const pending = writePendingRuntimeSwitch(cwd, sess, {
|
|
1364
|
+
provider,
|
|
1365
|
+
automode: getEffectiveAutomode(profile, cwd),
|
|
1366
|
+
bypassPermissions: getEffectiveBypassPermissions(cwd),
|
|
1367
|
+
model: settings.headModel || null,
|
|
1368
|
+
effort: settings.effort || null,
|
|
1369
|
+
confirmed,
|
|
1370
|
+
reason,
|
|
1371
|
+
});
|
|
1372
|
+
|
|
1373
|
+
const launchArgs = provider === _sessionTool(sess)
|
|
1374
|
+
? _sessionLaunchArgs(sess, cwd)
|
|
1375
|
+
: ['handoff', '--to', provider];
|
|
1376
|
+
|
|
1377
|
+
console.log('');
|
|
1378
|
+
console.log(`Runtime switch ${confirmed ? 'confirmed' : 'prepared'} for ${pending?.sessionName || sess.id}`);
|
|
1379
|
+
console.log(`Provider: ${provider}`);
|
|
1380
|
+
console.log(`Mode: ${getEffectiveConversationMode(profile, cwd)}`);
|
|
1381
|
+
console.log(`Launch: ${provider} ${launchArgs.join(' ')}`);
|
|
1382
|
+
console.log('');
|
|
1383
|
+
|
|
1384
|
+
if (args.includes('--apply')) {
|
|
1385
|
+
await processPendingRuntimeSwitch(cwd);
|
|
1386
|
+
}
|
|
1387
|
+
}
|
|
1388
|
+
|
|
1281
1389
|
async function cmdUpdate() {
|
|
1282
1390
|
const cwd = process.cwd();
|
|
1283
1391
|
console.log(' Updating Dual Brain...');
|
|
@@ -2121,6 +2229,15 @@ function getEffectiveBypassPermissions(cwd) {
|
|
|
2121
2229
|
return !!profile.bypassPermissions;
|
|
2122
2230
|
}
|
|
2123
2231
|
|
|
2232
|
+
function getEffectiveConversationMode(profile, cwd) {
|
|
2233
|
+
const workspace = cwd || process.cwd();
|
|
2234
|
+
const automode = getEffectiveAutomode(profile || loadProfile(workspace), workspace);
|
|
2235
|
+
const bypassPermissions = getEffectiveBypassPermissions(workspace);
|
|
2236
|
+
if (bypassPermissions) return 'bypass';
|
|
2237
|
+
if (!automode) return 'manual';
|
|
2238
|
+
return 'smart-auto';
|
|
2239
|
+
}
|
|
2240
|
+
|
|
2124
2241
|
function parseModeCommand(input) {
|
|
2125
2242
|
const text = input.trim().toLowerCase().replace(/\s+/g, ' ');
|
|
2126
2243
|
const wantsSession = /\b(this|current)\s+(session|conversation|terminal|chat)\b|\bfor this\b|\bfor current\b/.test(text);
|
|
@@ -2128,7 +2245,8 @@ function parseModeCommand(input) {
|
|
|
2128
2245
|
const scope = wantsGlobal && !wantsSession ? 'profile' : 'session';
|
|
2129
2246
|
|
|
2130
2247
|
let key = null;
|
|
2131
|
-
if (/\b(
|
|
2248
|
+
if (/\b(manual|ask me|ask before|approve every|approval every)\b/.test(text)) key = 'manualMode';
|
|
2249
|
+
if (/\b(smart auto|auto|automode|auto mode)\b/.test(text)) key = 'automode';
|
|
2132
2250
|
if (/\b(bypass|permission|permissions|approval|approvals|sandbox|safe mode)\b/.test(text)) key = 'bypassPermissions';
|
|
2133
2251
|
if (!key) return null;
|
|
2134
2252
|
|
|
@@ -2148,18 +2266,78 @@ function applyModeCommand(cmd, cwd) {
|
|
|
2148
2266
|
const profile = loadProfile(cwd);
|
|
2149
2267
|
if (cmd.key === 'automode') {
|
|
2150
2268
|
profile.automode = cmd.value;
|
|
2269
|
+
if (cmd.value) profile.bypassPermissions = false;
|
|
2151
2270
|
profile.settings = { ...(profile.settings || {}), automode: cmd.value };
|
|
2271
|
+
} else if (cmd.key === 'manualMode') {
|
|
2272
|
+
profile.automode = !cmd.value;
|
|
2273
|
+
if (cmd.value) profile.bypassPermissions = false;
|
|
2274
|
+
profile.settings = { ...(profile.settings || {}), automode: !cmd.value };
|
|
2152
2275
|
} else {
|
|
2153
2276
|
profile.bypassPermissions = cmd.value;
|
|
2277
|
+
if (cmd.value) {
|
|
2278
|
+
profile.automode = true;
|
|
2279
|
+
profile.settings = { ...(profile.settings || {}), automode: true };
|
|
2280
|
+
}
|
|
2154
2281
|
}
|
|
2155
2282
|
saveProfile(profile, { cwd });
|
|
2156
|
-
return { scope: 'default', value: cmd.value };
|
|
2283
|
+
return { scope: 'default', value: cmd.value, key: cmd.key };
|
|
2157
2284
|
}
|
|
2158
2285
|
|
|
2159
2286
|
const settings = loadSessionSettings(cwd);
|
|
2160
|
-
|
|
2287
|
+
if (cmd.key === 'automode') {
|
|
2288
|
+
settings.automode = cmd.value;
|
|
2289
|
+
if (cmd.value) settings.bypassPermissions = false;
|
|
2290
|
+
} else if (cmd.key === 'manualMode') {
|
|
2291
|
+
settings.automode = !cmd.value;
|
|
2292
|
+
if (cmd.value) settings.bypassPermissions = false;
|
|
2293
|
+
} else {
|
|
2294
|
+
settings.bypassPermissions = cmd.value;
|
|
2295
|
+
if (cmd.value) settings.automode = true;
|
|
2296
|
+
}
|
|
2161
2297
|
saveSessionSettings(cwd, settings);
|
|
2162
|
-
return { scope: 'this session', value: cmd.value };
|
|
2298
|
+
return { scope: 'this session', value: cmd.value, key: cmd.key };
|
|
2299
|
+
}
|
|
2300
|
+
|
|
2301
|
+
async function offerRuntimeReloadAfterModeChange(cwd, ask, recentSessions, label = 'runtime settings') {
|
|
2302
|
+
const cyan = '\x1b[36m';
|
|
2303
|
+
const reset = '\x1b[0m';
|
|
2304
|
+
let candidates = Array.isArray(recentSessions) ? recentSessions : [];
|
|
2305
|
+
if (candidates.length === 0) {
|
|
2306
|
+
try { candidates = enrichSessions(importReplitSessions(cwd), cwd).slice(0, 3); } catch {}
|
|
2307
|
+
}
|
|
2308
|
+
const sess = candidates[0] || null;
|
|
2309
|
+
process.stdout.write(' Provider approval flags are applied when Claude/Codex starts.\n');
|
|
2310
|
+
if (!sess) {
|
|
2311
|
+
process.stdout.write(` ${label} will apply to the next session you launch through dual-brain.\n\n`);
|
|
2312
|
+
await ask(' Press Enter to continue...');
|
|
2313
|
+
return { next: 'main' };
|
|
2314
|
+
}
|
|
2315
|
+
|
|
2316
|
+
const tool = _sessionTool(sess);
|
|
2317
|
+
const launchArgs = _sessionLaunchArgs(sess, cwd);
|
|
2318
|
+
writePendingRuntimeSwitch(cwd, sess, {
|
|
2319
|
+
provider: tool,
|
|
2320
|
+
automode: getEffectiveAutomode(loadProfile(cwd), cwd),
|
|
2321
|
+
bypassPermissions: getEffectiveBypassPermissions(cwd),
|
|
2322
|
+
reason: `${label}-reload`,
|
|
2323
|
+
});
|
|
2324
|
+
process.stdout.write(` Reload needed: ${tool} must restart with: ${launchArgs.join(' ')}\n\n`);
|
|
2325
|
+
process.stdout.write(` ${cyan}Enter${reset} reload last session now ${cyan}n${reset} later\n\n`);
|
|
2326
|
+
const choice = (await ask(' Choice: ')).trim().toLowerCase();
|
|
2327
|
+
if (choice === 'n' || choice === 'no' || choice === 'later') {
|
|
2328
|
+
process.stdout.write('\n Saved. It will apply on the next dual-brain resume/switch.\n\n');
|
|
2329
|
+
await ask(' Press Enter to continue...');
|
|
2330
|
+
return { next: 'main' };
|
|
2331
|
+
}
|
|
2332
|
+
writePendingRuntimeSwitch(cwd, sess, {
|
|
2333
|
+
provider: tool,
|
|
2334
|
+
automode: getEffectiveAutomode(loadProfile(cwd), cwd),
|
|
2335
|
+
bypassPermissions: getEffectiveBypassPermissions(cwd),
|
|
2336
|
+
reason: `${label}-reload`,
|
|
2337
|
+
confirmed: true,
|
|
2338
|
+
});
|
|
2339
|
+
const launched = await processPendingRuntimeSwitch(cwd);
|
|
2340
|
+
return launched ? { next: 'main' } : await launchSessionWithLease(sess, cwd, ask);
|
|
2163
2341
|
}
|
|
2164
2342
|
|
|
2165
2343
|
function pidAlive(pid) {
|
|
@@ -2174,6 +2352,70 @@ function activeConversationPath(cwd) {
|
|
|
2174
2352
|
return join(cwd, '.dualbrain', 'active-conversation.json');
|
|
2175
2353
|
}
|
|
2176
2354
|
|
|
2355
|
+
function pendingRuntimeSwitchPath(cwd) {
|
|
2356
|
+
return join(cwd, '.dualbrain', 'pending-runtime-switch.json');
|
|
2357
|
+
}
|
|
2358
|
+
|
|
2359
|
+
function writePendingRuntimeSwitch(cwd, session, updates = {}) {
|
|
2360
|
+
if (!session?.id) return null;
|
|
2361
|
+
const dir = join(cwd, '.dualbrain');
|
|
2362
|
+
const tool = updates.provider || _sessionTool(session);
|
|
2363
|
+
const pending = {
|
|
2364
|
+
id: `runtime-switch-${Date.now()}`,
|
|
2365
|
+
status: updates.confirmed ? 'confirmed' : 'prepared',
|
|
2366
|
+
createdAt: new Date().toISOString(),
|
|
2367
|
+
sessionId: session.id,
|
|
2368
|
+
sessionName: session.smartName || session.name || session.prompts?.first || session.firstPrompt || session.id,
|
|
2369
|
+
fromProvider: _sessionTool(session),
|
|
2370
|
+
provider: tool,
|
|
2371
|
+
model: updates.model || null,
|
|
2372
|
+
effort: updates.effort || null,
|
|
2373
|
+
automode: typeof updates.automode === 'boolean' ? updates.automode : null,
|
|
2374
|
+
bypassPermissions: typeof updates.bypassPermissions === 'boolean' ? updates.bypassPermissions : null,
|
|
2375
|
+
reason: updates.reason || 'runtime-settings-change',
|
|
2376
|
+
autoLaunch: updates.autoLaunch !== false,
|
|
2377
|
+
handoffBrief: updates.handoffBrief || _sessionBrief(session, tool),
|
|
2378
|
+
};
|
|
2379
|
+
mkdirSync(dir, { recursive: true });
|
|
2380
|
+
writeFileSync(pendingRuntimeSwitchPath(cwd), JSON.stringify(pending, null, 2) + '\n');
|
|
2381
|
+
return pending;
|
|
2382
|
+
}
|
|
2383
|
+
|
|
2384
|
+
function readPendingRuntimeSwitch(cwd) {
|
|
2385
|
+
try {
|
|
2386
|
+
const pending = JSON.parse(readFileSync(pendingRuntimeSwitchPath(cwd), 'utf8'));
|
|
2387
|
+
return pending?.sessionId ? pending : null;
|
|
2388
|
+
} catch { return null; }
|
|
2389
|
+
}
|
|
2390
|
+
|
|
2391
|
+
function clearPendingRuntimeSwitch(cwd) {
|
|
2392
|
+
try { unlinkSync(pendingRuntimeSwitchPath(cwd)); } catch {}
|
|
2393
|
+
}
|
|
2394
|
+
|
|
2395
|
+
function runtimeLaunchArgsForPending(session, cwd, pending) {
|
|
2396
|
+
const tool = pending?.provider === 'codex' || pending?.provider === 'claude'
|
|
2397
|
+
? pending.provider
|
|
2398
|
+
: _sessionTool(session);
|
|
2399
|
+
if (tool === 'codex') {
|
|
2400
|
+
if (pending?.bypassPermissions) return ['--dangerously-bypass-approvals-and-sandbox', 'resume', session.id];
|
|
2401
|
+
return [
|
|
2402
|
+
'--sandbox', 'workspace-write',
|
|
2403
|
+
'--ask-for-approval', pending?.automode ? 'never' : 'on-request',
|
|
2404
|
+
'resume', session.id,
|
|
2405
|
+
];
|
|
2406
|
+
}
|
|
2407
|
+
|
|
2408
|
+
const args = ['--resume', session.id];
|
|
2409
|
+
if (pending?.bypassPermissions) {
|
|
2410
|
+
args.push('--dangerously-skip-permissions');
|
|
2411
|
+
} else if (pending?.automode) {
|
|
2412
|
+
if (pending.model) args.push('--model', pending.model);
|
|
2413
|
+
if (pending.effort) args.push('--effort', pending.effort);
|
|
2414
|
+
args.push('--permission-mode', 'auto');
|
|
2415
|
+
}
|
|
2416
|
+
return args;
|
|
2417
|
+
}
|
|
2418
|
+
|
|
2177
2419
|
function readActiveConversation(cwd) {
|
|
2178
2420
|
try {
|
|
2179
2421
|
const lease = JSON.parse(readFileSync(activeConversationPath(cwd), 'utf8'));
|
|
@@ -2183,6 +2425,49 @@ function readActiveConversation(cwd) {
|
|
|
2183
2425
|
} catch { return null; }
|
|
2184
2426
|
}
|
|
2185
2427
|
|
|
2428
|
+
async function processPendingRuntimeSwitch(cwd) {
|
|
2429
|
+
const pending = readPendingRuntimeSwitch(cwd);
|
|
2430
|
+
if (!pending || pending.status !== 'confirmed' || pending.autoLaunch === false) return false;
|
|
2431
|
+
|
|
2432
|
+
let sessions = [];
|
|
2433
|
+
try { sessions = enrichSessions(importReplitSessions(cwd), cwd); } catch {}
|
|
2434
|
+
const sess = sessions.find(s => s.id === pending.sessionId) || sessions[0];
|
|
2435
|
+
if (!sess) {
|
|
2436
|
+
clearPendingRuntimeSwitch(cwd);
|
|
2437
|
+
return false;
|
|
2438
|
+
}
|
|
2439
|
+
|
|
2440
|
+
const currentTool = _sessionTool(sess);
|
|
2441
|
+
const targetTool = pending.provider === 'codex' || pending.provider === 'claude'
|
|
2442
|
+
? pending.provider
|
|
2443
|
+
: currentTool;
|
|
2444
|
+
|
|
2445
|
+
process.stdout.write(`\n Reloading HEAD with ${pending.automode ? 'Smart Auto' : 'updated settings'}...\n`);
|
|
2446
|
+
clearPendingRuntimeSwitch(cwd);
|
|
2447
|
+
|
|
2448
|
+
if (targetTool !== currentTool) {
|
|
2449
|
+
writeHandoffConversationLease(cwd, sess, currentTool, targetTool, pending.handoffBrief || _sessionBrief(sess, targetTool));
|
|
2450
|
+
markSessionSuperseded(cwd, sess, targetTool, pending.reason || 'runtime-switch');
|
|
2451
|
+
await cmdSwitch([targetTool, pending.handoffBrief || _sessionBrief(sess, targetTool)]);
|
|
2452
|
+
return true;
|
|
2453
|
+
}
|
|
2454
|
+
|
|
2455
|
+
const { spawnSync } = await import('node:child_process');
|
|
2456
|
+
const launchArgs = runtimeLaunchArgsForPending(sess, cwd, pending);
|
|
2457
|
+
process.stdout.write(` Launching: ${targetTool} ${launchArgs.join(' ')}\n\n`);
|
|
2458
|
+
writeActiveConversation(cwd, sess, targetTool);
|
|
2459
|
+
const env = targetTool === 'codex' && (!process.env.TERM || process.env.TERM === 'dumb')
|
|
2460
|
+
? { ...process.env, TERM: 'xterm-256color' }
|
|
2461
|
+
: process.env;
|
|
2462
|
+
try {
|
|
2463
|
+
spawnSync(targetTool, launchArgs, { stdio: 'inherit', cwd, env });
|
|
2464
|
+
} finally {
|
|
2465
|
+
saveTerminalState(cwd, getTerminalId(), sess.id, sess.tool || targetTool);
|
|
2466
|
+
clearActiveConversation(cwd, sess.id);
|
|
2467
|
+
}
|
|
2468
|
+
return true;
|
|
2469
|
+
}
|
|
2470
|
+
|
|
2186
2471
|
function writeActiveConversation(cwd, session, tool) {
|
|
2187
2472
|
const dir = join(cwd, '.dualbrain');
|
|
2188
2473
|
const terminalId = getTerminalId();
|
|
@@ -2201,6 +2486,73 @@ function writeActiveConversation(cwd, session, tool) {
|
|
|
2201
2486
|
return lease;
|
|
2202
2487
|
}
|
|
2203
2488
|
|
|
2489
|
+
function writeHandoffConversationLease(cwd, session, fromTool, targetTool, taskBrief = '') {
|
|
2490
|
+
const dir = join(cwd, '.dualbrain');
|
|
2491
|
+
const terminalId = getTerminalId();
|
|
2492
|
+
const lease = {
|
|
2493
|
+
conversationId: session.id,
|
|
2494
|
+
sessionId: session.id,
|
|
2495
|
+
provider: targetTool,
|
|
2496
|
+
previousProvider: fromTool,
|
|
2497
|
+
terminalId,
|
|
2498
|
+
ownerPid: process.pid,
|
|
2499
|
+
startedAt: new Date().toISOString(),
|
|
2500
|
+
lastHeartbeat: new Date().toISOString(),
|
|
2501
|
+
mode: 'handoff-head',
|
|
2502
|
+
taskBrief: taskBrief || null,
|
|
2503
|
+
};
|
|
2504
|
+
mkdirSync(dir, { recursive: true });
|
|
2505
|
+
writeFileSync(activeConversationPath(cwd), JSON.stringify(lease, null, 2) + '\n');
|
|
2506
|
+
return lease;
|
|
2507
|
+
}
|
|
2508
|
+
|
|
2509
|
+
function markSessionSuperseded(cwd, session, targetTool, reason = 'provider-switch') {
|
|
2510
|
+
if (!session?.id) return;
|
|
2511
|
+
try {
|
|
2512
|
+
const meta = getSessionMeta(cwd);
|
|
2513
|
+
const existing = meta[session.id] || {};
|
|
2514
|
+
meta[session.id] = {
|
|
2515
|
+
...existing,
|
|
2516
|
+
id: session.id,
|
|
2517
|
+
tool: session.tool || _sessionTool(session),
|
|
2518
|
+
status: 'superseded',
|
|
2519
|
+
supersededByProvider: targetTool,
|
|
2520
|
+
supersededReason: reason,
|
|
2521
|
+
supersededAt: new Date().toISOString(),
|
|
2522
|
+
terminalId: getTerminalId(),
|
|
2523
|
+
createdAt: existing.createdAt || session.date || new Date().toISOString(),
|
|
2524
|
+
};
|
|
2525
|
+
saveSessionMeta(meta, cwd);
|
|
2526
|
+
} catch {}
|
|
2527
|
+
}
|
|
2528
|
+
|
|
2529
|
+
function setSessionResumeProvider(cwd, session, targetTool, reason = 'user-preference') {
|
|
2530
|
+
if (!session?.id || !targetTool) return;
|
|
2531
|
+
try {
|
|
2532
|
+
const meta = getSessionMeta(cwd);
|
|
2533
|
+
const existing = meta[session.id] || {};
|
|
2534
|
+
meta[session.id] = {
|
|
2535
|
+
...existing,
|
|
2536
|
+
id: session.id,
|
|
2537
|
+
tool: session.tool || _sessionTool(session),
|
|
2538
|
+
resumeProvider: targetTool,
|
|
2539
|
+
resumeProviderReason: reason,
|
|
2540
|
+
resumeProviderSetAt: new Date().toISOString(),
|
|
2541
|
+
createdAt: existing.createdAt || session.date || new Date().toISOString(),
|
|
2542
|
+
};
|
|
2543
|
+
saveSessionMeta(meta, cwd);
|
|
2544
|
+
} catch {}
|
|
2545
|
+
}
|
|
2546
|
+
|
|
2547
|
+
function getSessionResumeProvider(cwd, session) {
|
|
2548
|
+
if (!session?.id) return null;
|
|
2549
|
+
try {
|
|
2550
|
+
const meta = getSessionMeta(cwd);
|
|
2551
|
+
const provider = meta[session.id]?.resumeProvider;
|
|
2552
|
+
return provider === 'claude' || provider === 'codex' ? provider : null;
|
|
2553
|
+
} catch { return null; }
|
|
2554
|
+
}
|
|
2555
|
+
|
|
2204
2556
|
function clearActiveConversation(cwd, sessionId) {
|
|
2205
2557
|
try {
|
|
2206
2558
|
const lease = JSON.parse(readFileSync(activeConversationPath(cwd), 'utf8'));
|
|
@@ -2234,6 +2586,15 @@ async function launchSessionWithLease(sess, cwd, ask = null) {
|
|
|
2234
2586
|
|
|
2235
2587
|
const { spawnSync } = await import('node:child_process');
|
|
2236
2588
|
const tool = _sessionTool(sess);
|
|
2589
|
+
const preferredTool = getSessionResumeProvider(cwd, sess);
|
|
2590
|
+
if (preferredTool && preferredTool !== tool) {
|
|
2591
|
+
const brief = _sessionBrief(sess, preferredTool);
|
|
2592
|
+
writeHandoffConversationLease(cwd, sess, tool, preferredTool, brief);
|
|
2593
|
+
markSessionSuperseded(cwd, sess, preferredTool, 'resume-provider-preference');
|
|
2594
|
+
process.stdout.write(`\n Continuing in ${preferredTool === 'codex' ? 'Codex/GPT' : 'Claude'} based on this conversation's setting.\n\n`);
|
|
2595
|
+
await cmdSwitch([preferredTool, brief]);
|
|
2596
|
+
return { next: 'main' };
|
|
2597
|
+
}
|
|
2237
2598
|
const launchArgs = _sessionLaunchArgs(sess, cwd);
|
|
2238
2599
|
if (decision === 'takeover') {
|
|
2239
2600
|
process.stdout.write(' Taking over active conversation in this terminal.\n');
|
|
@@ -3341,7 +3702,7 @@ async function mainScreen(rl, ask) {
|
|
|
3341
3702
|
[`s`, 'settings & profiles'],
|
|
3342
3703
|
[`d`, 'doctor — diagnose issues'],
|
|
3343
3704
|
[`t`, 'team settings'],
|
|
3344
|
-
[`a`, getEffectiveAutomode(profile, cwd) ? 'auto
|
|
3705
|
+
[`a`, getEffectiveAutomode(profile, cwd) && !getEffectiveBypassPermissions(cwd) ? 'smart auto (on)' : 'smart auto (off)'],
|
|
3345
3706
|
[`q`, 'quit'],
|
|
3346
3707
|
];
|
|
3347
3708
|
for (const [key, label] of shortcuts) {
|
|
@@ -3359,7 +3720,7 @@ async function mainScreen(rl, ask) {
|
|
|
3359
3720
|
|
|
3360
3721
|
// ── Key handling ──────────────────────────────────────────────────────────
|
|
3361
3722
|
// Use raw keypress mode so we can show a live type-to-start buffer.
|
|
3362
|
-
//
|
|
3723
|
+
// Shortcuts resolve on Enter; printable keys should never preempt natural text.
|
|
3363
3724
|
let taskBuffer = '';
|
|
3364
3725
|
|
|
3365
3726
|
const readline = await import('node:readline');
|
|
@@ -3410,8 +3771,15 @@ async function mainScreen(rl, ask) {
|
|
|
3410
3771
|
if (name === 'return' || name === 'enter' || seq === '\r' || seq === '\n') {
|
|
3411
3772
|
cleanup();
|
|
3412
3773
|
if (taskBuffer.length > 0) {
|
|
3774
|
+
const shortcut = taskBuffer.trim().toLowerCase();
|
|
3775
|
+
const singleKeySet = new Set(['n', 'g', 's', 't', 'q', '/', 'i', '?', 'h', 'd', 'a']);
|
|
3776
|
+
const digit = parseInt(shortcut, 10);
|
|
3413
3777
|
process.stdout.write('\n');
|
|
3414
|
-
|
|
3778
|
+
if (singleKeySet.has(shortcut) || (!isNaN(digit) && digit >= 1 && digit <= 9)) {
|
|
3779
|
+
resolve(shortcut);
|
|
3780
|
+
} else {
|
|
3781
|
+
resolve(`__task__:${taskBuffer}`);
|
|
3782
|
+
}
|
|
3415
3783
|
} else {
|
|
3416
3784
|
resolve('');
|
|
3417
3785
|
}
|
|
@@ -3439,25 +3807,6 @@ async function mainScreen(rl, ask) {
|
|
|
3439
3807
|
const code = str.codePointAt(0);
|
|
3440
3808
|
if (code < 32 || code === 127) return;
|
|
3441
3809
|
|
|
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
3810
|
// Accumulate into buffer
|
|
3462
3811
|
taskBuffer += str;
|
|
3463
3812
|
renderBuffer(taskBuffer);
|
|
@@ -3482,11 +3831,17 @@ async function mainScreen(rl, ask) {
|
|
|
3482
3831
|
|
|
3483
3832
|
if (cmd === 'mode') {
|
|
3484
3833
|
const result = applyModeCommand(classified.modeCommand, cwd);
|
|
3485
|
-
const keyLabel = classified.modeCommand.key === '
|
|
3834
|
+
const keyLabel = classified.modeCommand.key === 'manualMode'
|
|
3835
|
+
? 'Manual mode'
|
|
3836
|
+
: classified.modeCommand.key === 'automode'
|
|
3837
|
+
? 'Smart Auto'
|
|
3838
|
+
: 'Bypass mode';
|
|
3486
3839
|
const state = result.value ? '\x1b[32mON\x1b[0m' : '\x1b[2mOFF\x1b[0m';
|
|
3487
|
-
|
|
3488
|
-
|
|
3489
|
-
|
|
3840
|
+
const profileNow = loadProfile(cwd);
|
|
3841
|
+
const modeNow = getEffectiveConversationMode(profileNow, cwd);
|
|
3842
|
+
process.stdout.write(`\n ${keyLabel}: ${state} \x1b[2m(${result.scope})\x1b[0m\n`);
|
|
3843
|
+
process.stdout.write(` Effective mode: ${modeNow === 'bypass' ? '\x1b[31mBypass\x1b[0m' : modeNow === 'manual' ? '\x1b[2mManual\x1b[0m' : '\x1b[32mSmart Auto\x1b[0m'}\n\n`);
|
|
3844
|
+
return await offerRuntimeReloadAfterModeChange(cwd, ask, recentSessions, keyLabel);
|
|
3490
3845
|
}
|
|
3491
3846
|
|
|
3492
3847
|
if (cmd === 'resume' || cmd === 'r') {
|
|
@@ -3569,12 +3924,12 @@ async function mainScreen(rl, ask) {
|
|
|
3569
3924
|
const nextAuto = !getEffectiveAutomode(prof, cwd2);
|
|
3570
3925
|
const settings = loadSessionSettings(cwd2);
|
|
3571
3926
|
settings.automode = nextAuto;
|
|
3927
|
+
if (nextAuto) settings.bypassPermissions = false;
|
|
3572
3928
|
saveSessionSettings(cwd2, settings);
|
|
3573
3929
|
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' };
|
|
3930
|
+
process.stdout.write(`\n Smart Auto: ${state} \x1b[2m(this session)\x1b[0m\n`);
|
|
3931
|
+
process.stdout.write(` ${nextAuto ? 'Safe tasks dispatch immediately; critical findings still interrupt.' : 'Manual mode: tasks require confirmation.'}\n\n`);
|
|
3932
|
+
return await offerRuntimeReloadAfterModeChange(cwd, ask, recentSessions, 'Smart Auto');
|
|
3578
3933
|
}
|
|
3579
3934
|
if (cmd === 'init --replit') {
|
|
3580
3935
|
await cmdInit(rl);
|
|
@@ -3764,10 +4119,10 @@ async function mainScreen(rl, ask) {
|
|
|
3764
4119
|
const nextAuto = !getEffectiveAutomode(prof, cwd);
|
|
3765
4120
|
const settings = loadSessionSettings(cwd);
|
|
3766
4121
|
settings.automode = nextAuto;
|
|
4122
|
+
if (nextAuto) settings.bypassPermissions = false;
|
|
3767
4123
|
saveSessionSettings(cwd, settings);
|
|
3768
|
-
process.stdout.write(`\n
|
|
3769
|
-
await
|
|
3770
|
-
return { next: 'main' };
|
|
4124
|
+
process.stdout.write(`\n Smart Auto: ${nextAuto ? '\x1b[32mON\x1b[0m' : '\x1b[2mOFF\x1b[0m'} \x1b[2m(this session)\x1b[0m\n\n`);
|
|
4125
|
+
return await offerRuntimeReloadAfterModeChange(cwd, ask, recentSessions, 'Smart Auto');
|
|
3771
4126
|
}
|
|
3772
4127
|
if (choice === 'q' || choice === 'exit') { return { next: 'exit' }; }
|
|
3773
4128
|
|
|
@@ -3802,11 +4157,20 @@ async function switchProviderScreen(rl, ask, ctx = {}) {
|
|
|
3802
4157
|
process.stdout.write('\n');
|
|
3803
4158
|
process.stdout.write(` Continue in ${target === 'codex' ? 'Codex/GPT' : 'Claude'}\n`);
|
|
3804
4159
|
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`);
|
|
4160
|
+
process.stdout.write(` \x1b[36mEnter\x1b[0m switch now \x1b[36mn\x1b[0m next resume \x1b[36mb\x1b[0m back\n\n`);
|
|
3806
4161
|
|
|
3807
4162
|
const choice = (await ask(' Choice: ')).trim().toLowerCase();
|
|
3808
|
-
if (choice === '
|
|
4163
|
+
if (choice === 'n' || choice === 'next') {
|
|
4164
|
+
setSessionResumeProvider(cwd, session, target, 'user-next-resume');
|
|
4165
|
+
process.stdout.write(`\n Next resume will continue this conversation in ${target === 'codex' ? 'Codex/GPT' : 'Claude'}.\n\n`);
|
|
4166
|
+
await ask(' Press Enter to continue...');
|
|
4167
|
+
return { next: 'main' };
|
|
4168
|
+
}
|
|
4169
|
+
if (choice === 'b' || choice === 'q') return { next: 'main' };
|
|
3809
4170
|
|
|
4171
|
+
setSessionResumeProvider(cwd, session, target, 'user-switch-now');
|
|
4172
|
+
writeHandoffConversationLease(cwd, session, currentTool, target, brief);
|
|
4173
|
+
markSessionSuperseded(cwd, session, target, 'manual-provider-switch');
|
|
3810
4174
|
await cmdSwitch([target, brief]);
|
|
3811
4175
|
return { next: 'main' };
|
|
3812
4176
|
}
|
|
@@ -3827,7 +4191,7 @@ async function paletteHelpScreen(rl, ask) {
|
|
|
3827
4191
|
const CYAN = '\x1b[36m';
|
|
3828
4192
|
const lines = [
|
|
3829
4193
|
top,
|
|
3830
|
-
row(`${CYAN}Keyboard Shortcuts${RESET} (
|
|
4194
|
+
row(`${CYAN}Keyboard Shortcuts${RESET} (type key, then Enter)`),
|
|
3831
4195
|
sep,
|
|
3832
4196
|
row(`${CYAN}Enter${RESET} Resume last session`),
|
|
3833
4197
|
row(`${CYAN}n${RESET} New coding session`),
|
|
@@ -4345,6 +4709,7 @@ async function settingsScreen(rl, ask) {
|
|
|
4345
4709
|
const automode = getEffectiveAutomode(profile, cwd);
|
|
4346
4710
|
const sessionSettings = loadSessionSettings(cwd);
|
|
4347
4711
|
const bypassPermissions = getEffectiveBypassPermissions(cwd);
|
|
4712
|
+
const conversationMode = getEffectiveConversationMode(profile, cwd);
|
|
4348
4713
|
const autoScope = typeof sessionSettings.automode === 'boolean' ? 'session' : 'default';
|
|
4349
4714
|
const permissionScope = typeof sessionSettings.bypassPermissions === 'boolean' ? 'session' : 'default';
|
|
4350
4715
|
|
|
@@ -4393,10 +4758,17 @@ async function settingsScreen(rl, ask) {
|
|
|
4393
4758
|
const permMode = bypassPermissions
|
|
4394
4759
|
? `${RED}bypass approvals and sandbox${RESET}`
|
|
4395
4760
|
: `${GREEN}safe approvals + workspace sandbox${RESET}`;
|
|
4761
|
+
const modeLabel = conversationMode === 'bypass'
|
|
4762
|
+
? `${RED}Bypass${RESET}`
|
|
4763
|
+
: conversationMode === 'manual'
|
|
4764
|
+
? `${DIM}Manual${RESET}`
|
|
4765
|
+
: `${GREEN}Smart Auto${RESET}`;
|
|
4396
4766
|
const convLines = [
|
|
4397
|
-
` ${DIM}
|
|
4398
|
-
` ${DIM}
|
|
4399
|
-
` ${DIM}
|
|
4767
|
+
` ${DIM}Mode${RESET} ${modeLabel}`,
|
|
4768
|
+
` ${DIM}Manual${RESET} ${automode ? xmark + ' off' : chk + ' on'} ${DIM}[${autoScope}]${RESET}`,
|
|
4769
|
+
` ${DIM}Smart Auto${RESET} ${autoMark} ${automode ? 'safe tasks run immediately' : 'disabled'} ${DIM}[${autoScope}]${RESET}`,
|
|
4770
|
+
` ${DIM}Safety${RESET} ${permMark} ${permMode} ${DIM}[${permissionScope}]${RESET}`,
|
|
4771
|
+
` ${DIM}Claude resume${RESET} ${bypassPermissions ? '--dangerously-skip-permissions' : automode ? '--permission-mode auto' : 'default permissions'}`,
|
|
4400
4772
|
` ${DIM}Codex resume${RESET} ${bypassPermissions ? '--dangerously-bypass-approvals-and-sandbox' : `workspace-write + ${automode ? 'never ask' : 'on-request'}`}`,
|
|
4401
4773
|
];
|
|
4402
4774
|
|
|
@@ -4473,7 +4845,7 @@ async function settingsScreen(rl, ask) {
|
|
|
4473
4845
|
const convContent = [
|
|
4474
4846
|
...convLines.map(l => l.replace(/^ /, '')),
|
|
4475
4847
|
'',
|
|
4476
|
-
signalLine('info', `${DIM}[o] auto
|
|
4848
|
+
signalLine('info', `${DIM}[o] smart auto [m] manual [v] bypass/safety${RESET}`),
|
|
4477
4849
|
];
|
|
4478
4850
|
|
|
4479
4851
|
const sysContent = [
|
|
@@ -4518,23 +4890,34 @@ async function settingsScreen(rl, ask) {
|
|
|
4518
4890
|
|
|
4519
4891
|
// Conversation behavior toggles
|
|
4520
4892
|
if (choice === 'o') {
|
|
4521
|
-
const nextAuto = !automode;
|
|
4522
4893
|
const settings = loadSessionSettings(cwd);
|
|
4523
|
-
settings.automode =
|
|
4894
|
+
settings.automode = true;
|
|
4895
|
+
settings.bypassPermissions = false;
|
|
4524
4896
|
saveSessionSettings(cwd, settings);
|
|
4525
|
-
process.stdout.write(`\n
|
|
4526
|
-
await
|
|
4527
|
-
return { next: 'settings' };
|
|
4897
|
+
process.stdout.write(`\n Mode: ${GREEN}Smart Auto${RESET} ${DIM}(this session)${RESET}\n\n`);
|
|
4898
|
+
const reload = await offerRuntimeReloadAfterModeChange(cwd, ask, [], 'Smart Auto');
|
|
4899
|
+
return reload?.next === 'main' ? { next: 'settings' } : reload;
|
|
4900
|
+
}
|
|
4901
|
+
|
|
4902
|
+
if (choice === 'm') {
|
|
4903
|
+
const settings = loadSessionSettings(cwd);
|
|
4904
|
+
settings.automode = false;
|
|
4905
|
+
settings.bypassPermissions = false;
|
|
4906
|
+
saveSessionSettings(cwd, settings);
|
|
4907
|
+
process.stdout.write(`\n Mode: ${DIM}Manual${RESET} ${DIM}(this session)${RESET}\n\n`);
|
|
4908
|
+
const reload = await offerRuntimeReloadAfterModeChange(cwd, ask, [], 'Manual mode');
|
|
4909
|
+
return reload?.next === 'main' ? { next: 'settings' } : reload;
|
|
4528
4910
|
}
|
|
4529
4911
|
|
|
4530
4912
|
if (choice === 'v') {
|
|
4531
4913
|
if (bypassPermissions) {
|
|
4532
4914
|
const settings = loadSessionSettings(cwd);
|
|
4533
4915
|
settings.bypassPermissions = false;
|
|
4916
|
+
settings.automode = true;
|
|
4534
4917
|
saveSessionSettings(cwd, settings);
|
|
4535
|
-
process.stdout.write(`\n
|
|
4536
|
-
await
|
|
4537
|
-
return { next: 'settings' };
|
|
4918
|
+
process.stdout.write(`\n Mode: ${GREEN}Smart Auto${RESET}; safety boundary restored ${DIM}(this session)${RESET}\n\n`);
|
|
4919
|
+
const reload = await offerRuntimeReloadAfterModeChange(cwd, ask, [], 'Smart Auto');
|
|
4920
|
+
return reload?.next === 'main' ? { next: 'settings' } : reload;
|
|
4538
4921
|
}
|
|
4539
4922
|
|
|
4540
4923
|
process.stdout.write(`\n ${RED}Bypass mode disables provider approval prompts and sandboxing.${RESET}\n`);
|
|
@@ -4543,8 +4926,11 @@ async function settingsScreen(rl, ask) {
|
|
|
4543
4926
|
if (confirm === 'YES') {
|
|
4544
4927
|
const settings = loadSessionSettings(cwd);
|
|
4545
4928
|
settings.bypassPermissions = true;
|
|
4929
|
+
settings.automode = true;
|
|
4546
4930
|
saveSessionSettings(cwd, settings);
|
|
4547
|
-
process.stdout.write(`\n
|
|
4931
|
+
process.stdout.write(`\n Mode: ${RED}Bypass${RESET} ${DIM}(this session)${RESET}\n\n`);
|
|
4932
|
+
const reload = await offerRuntimeReloadAfterModeChange(cwd, ask, [], 'Bypass mode');
|
|
4933
|
+
return reload?.next === 'main' ? { next: 'settings' } : reload;
|
|
4548
4934
|
} else {
|
|
4549
4935
|
process.stdout.write('\n Permission mode unchanged.\n\n');
|
|
4550
4936
|
}
|
|
@@ -6454,6 +6840,15 @@ async function runScreens(startScreen = 'dashboard') {
|
|
|
6454
6840
|
let current = startScreen;
|
|
6455
6841
|
let ctx = {};
|
|
6456
6842
|
while (current && current !== 'exit') {
|
|
6843
|
+
if (current === 'main' || current === 'dashboard') {
|
|
6844
|
+
const launchedPending = await processPendingRuntimeSwitch(process.cwd());
|
|
6845
|
+
if (launchedPending) {
|
|
6846
|
+
current = 'main';
|
|
6847
|
+
ctx = {};
|
|
6848
|
+
continue;
|
|
6849
|
+
}
|
|
6850
|
+
}
|
|
6851
|
+
|
|
6457
6852
|
// Handle type-to-start dispatch from mainScreen — all work routes through pipeline.
|
|
6458
6853
|
if (current === 'go' && ctx.prompt) {
|
|
6459
6854
|
const prompt = ctx.prompt;
|
|
@@ -7228,6 +7623,7 @@ async function main() {
|
|
|
7228
7623
|
if (cmd === 'status') { await cmdStatus(args.slice(1)); return; }
|
|
7229
7624
|
if (cmd === 'handoff') { await cmdHandoff(args.slice(1)); return; }
|
|
7230
7625
|
if (cmd === 'switch') { await cmdSwitch(args.slice(1)); return; }
|
|
7626
|
+
if (cmd === 'runtime-switch') { await cmdRuntimeSwitch(args.slice(1)); return; }
|
|
7231
7627
|
if (cmd === 'update' || cmd === 'upgrade') { await cmdUpdate(); return; }
|
|
7232
7628
|
if (cmd === 'hot') { cmdHot(args[1]); return; }
|
|
7233
7629
|
if (cmd === 'cool') { cmdCool(args[1]); return; }
|
|
@@ -7292,7 +7688,7 @@ fi
|
|
|
7292
7688
|
// If cmd is not a recognized subcommand, treat the entire arg list as a task.
|
|
7293
7689
|
// e.g. `dual-brain fix failing tests` → same as `dual-brain go "fix failing tests"`
|
|
7294
7690
|
const KNOWN_COMMANDS = new Set([
|
|
7295
|
-
'menu', 'init', 'install', 'uninstall', 'auth', 'go', 'do', 'plan', 'ship', 'think', 'review', 'pr', 'status', 'handoff', 'switch', 'hot', 'cool',
|
|
7691
|
+
'menu', 'init', 'install', 'uninstall', 'auth', 'go', 'do', 'plan', 'ship', 'think', 'review', 'pr', 'status', 'handoff', 'switch', 'runtime-switch', 'hot', 'cool',
|
|
7296
7692
|
'remember', 'forget', 'break-glass', 'specialists', 'search', 'shell-hook', 'watch', 'update', 'upgrade',
|
|
7297
7693
|
'--help', '-h', '--version', '-v',
|
|
7298
7694
|
...Object.keys(loadSpecialistRegistry()),
|