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.
@@ -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
 
@@ -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({ fromProvider: 'anthropic', cwd, auto: true, interactive: true });
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({ fromProvider: 'openai', cwd, auto: true, interactive: true });
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(auto|automode|auto mode)\b/.test(text)) key = 'automode';
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
- settings[cmd.key] = cmd.value;
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 mode (on)' : 'auto mode (off)'],
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
- // Single-key commands (n, s, q, /, 1-9, Enter) only fire when buffer is empty.
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
- resolve(`__task__:${taskBuffer}`);
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 === 'automode' ? 'Auto mode' : 'Bypass permissions';
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
- 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' };
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 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' };
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 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' };
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 === 'b' || choice === 'q' || choice === 'n') return { next: 'main' };
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} (single key, no Enter needed)`),
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}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'}`,
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 mode [v] permission mode${RESET}`),
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 = nextAuto;
4894
+ settings.automode = true;
4895
+ settings.bypassPermissions = false;
4524
4896
  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' };
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 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' };
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 Permission mode: ${RED}bypass approvals and sandbox${RESET} ${DIM}(this session)${RESET}\n\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()),