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.
@@ -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)) args.push('--permission-mode', 'auto');
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)) args.push('--permission-mode', 'auto');
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 ['--sandbox', 'workspace-write', '--ask-for-approval', approvalMode, 'resume', sessionId];
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({ fromProvider: 'anthropic', cwd, auto: true, interactive: true });
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({ fromProvider: 'openai', cwd, auto: true, interactive: true });
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(auto|automode|auto mode)\b/.test(text)) key = 'automode';
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
- settings[cmd.key] = cmd.value;
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 writeActiveConversation(cwd, session, tool) {
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
- spawnSync(tool, launchArgs, { stdio: 'inherit' });
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 mode (on)' : 'auto mode (off)'],
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
- // Single-key commands (n, s, q, /, 1-9, Enter) only fire when buffer is empty.
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
- resolve(`__task__:${taskBuffer}`);
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 === 'automode' ? 'Auto mode' : 'Bypass permissions';
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
- process.stdout.write(`\n ${keyLabel}: ${state} \x1b[2m(${result.scope})\x1b[0m\n\n`);
3488
- await ask(' Press Enter to continue...');
3489
- return { next: 'main' };
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 Automode: ${state} \x1b[2m(this session)\x1b[0m\n`);
3575
- process.stdout.write(` ${nextAuto ? 'Tasks dispatch immediately (HEAD still gates dangerous ops)' : 'Tasks require Enter to confirm'}\n\n`);
3576
- await ask(' Press Enter to continue...');
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 Automode: ${nextAuto ? '\x1b[32mON\x1b[0m' : '\x1b[2mOFF\x1b[0m'} \x1b[2m(this session)\x1b[0m\n\n`);
3769
- await ask(' Press Enter to continue...');
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 === 'b' || choice === 'q' || choice === 'n') return { next: 'main' };
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} (single key, no Enter needed)`),
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}Auto mode${RESET} ${autoMark} ${automode ? 'run safe tasks immediately' : 'ask before launching tasks'} ${DIM}[${autoScope}]${RESET}`,
4398
- ` ${DIM}Permissions${RESET} ${permMark} ${permMode} ${DIM}[${permissionScope}]${RESET}`,
4399
- ` ${DIM}Claude resume${RESET} ${bypassPermissions ? '--dangerously-skip-permissions' : 'normal permissions'}`,
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 mode [v] permission mode${RESET}`),
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 = nextAuto;
5038
+ settings.automode = true;
5039
+ settings.bypassPermissions = false;
4524
5040
  saveSessionSettings(cwd, settings);
4525
- process.stdout.write(`\n Auto mode: ${nextAuto ? GREEN + 'ON' + RESET : DIM + 'OFF' + RESET} ${DIM}(this session)${RESET}\n\n`);
4526
- await ask(' Press Enter to continue...');
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 Permission mode: ${GREEN}safe approvals + workspace sandbox${RESET} ${DIM}(this session)${RESET}\n\n`);
4536
- await ask(' Press Enter to continue...');
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 Permission mode: ${RED}bypass approvals and sandbox${RESET} ${DIM}(this session)${RESET}\n\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
- const tool = _sessionTool(sess);
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
- const { spawnSync } = await import('node:child_process');
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()),