dual-brain 0.3.24 → 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.
@@ -26,21 +26,37 @@ import { detectTask, primeAgentRegistry } from '../dist/src/detect.js';
26
26
 
27
27
  function _claudeResumeArgs(sessionId, cwd) {
28
28
  const args = ['--resume', sessionId];
29
- if (getEffectiveBypassPermissions(cwd || process.cwd())) args.push('--dangerously-skip-permissions');
29
+ const workspace = cwd || process.cwd();
30
+ if (getEffectiveBypassPermissions(workspace)) args.push('--dangerously-skip-permissions');
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
+ }
30
37
  return args;
31
38
  }
32
39
 
33
40
  function _claudeNewArgs(cwd) {
34
41
  const args = [];
35
- if (getEffectiveBypassPermissions(cwd || process.cwd())) args.push('--dangerously-skip-permissions');
42
+ const workspace = cwd || process.cwd();
43
+ if (getEffectiveBypassPermissions(workspace)) args.push('--dangerously-skip-permissions');
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
+ }
36
50
  return args;
37
51
  }
38
52
 
39
53
  function _codexResumeArgs(sessionId, cwd) {
40
- if (getEffectiveBypassPermissions(cwd || process.cwd())) {
54
+ const workspace = cwd || process.cwd();
55
+ if (getEffectiveBypassPermissions(workspace)) {
41
56
  return ['--dangerously-bypass-approvals-and-sandbox', 'resume', sessionId];
42
57
  }
43
- return ['--sandbox', 'workspace-write', '--ask-for-approval', 'on-request', 'resume', sessionId];
58
+ const approvalMode = getEffectiveAutomode(loadProfile(workspace), workspace) ? 'never' : 'on-request';
59
+ return ['--sandbox', 'workspace-write', '--ask-for-approval', approvalMode, 'resume', sessionId];
44
60
  }
45
61
 
46
62
  function _sessionTool(session) {
@@ -82,7 +98,7 @@ async function _primeRegistryCache() {
82
98
  }
83
99
 
84
100
  import {
85
- decideRoute, getAvailableModels,
101
+ decideRoute, getAvailableModels, recommendHeadModel,
86
102
  } from '../dist/src/decide.js';
87
103
 
88
104
  import {
@@ -1041,6 +1057,14 @@ async function cmdStatus(args = []) {
1041
1057
  console.log(`\nHead model : ${getHeadModel(profile)}`);
1042
1058
  console.log(`Mode : ${profile.mode}`);
1043
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
+ }
1044
1068
 
1045
1069
  // Runtime
1046
1070
  console.log('\nRuntime:');
@@ -1223,11 +1247,15 @@ async function cmdHandoff(args = []) {
1223
1247
  console.log(` ⚡ Switching to ${target}...`);
1224
1248
  console.log('');
1225
1249
  const { spawnHandoff } = autoHandoff;
1250
+ const effectiveAutomode = getEffectiveAutomode(profile, cwd);
1251
+ const effectiveBypass = getEffectiveBypassPermissions(cwd);
1226
1252
  const result = spawnHandoff({
1227
1253
  fromProvider,
1228
1254
  cwd,
1229
1255
  auto: true,
1230
1256
  force: true,
1257
+ automode: effectiveAutomode,
1258
+ bypassPermissions: effectiveBypass,
1231
1259
  interactive: true,
1232
1260
  taskBrief: typeof taskBrief === 'string' ? taskBrief : undefined,
1233
1261
  });
@@ -1246,14 +1274,28 @@ async function cmdHandoff(args = []) {
1246
1274
  fxH.info(ux.text);
1247
1275
  console.log('');
1248
1276
  const { spawnHandoff } = autoHandoff;
1249
- 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
+ });
1250
1285
  if (!result.success) fxH.error(result.message);
1251
1286
  } else if (openaiStatus.limited && openaiStatus.otherAvailable) {
1252
1287
  const ux = getHandoffUX(openaiStatus);
1253
1288
  fxH.info(ux.text);
1254
1289
  console.log('');
1255
1290
  const { spawnHandoff } = autoHandoff;
1256
- 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
+ });
1257
1299
  if (!result.success) fxH.error(result.message);
1258
1300
  } else {
1259
1301
  fxH.success('No provider is currently limited. No handoff needed.');
@@ -1272,6 +1314,78 @@ async function cmdSwitch(args = []) {
1272
1314
  await cmdHandoff(handoffArgs);
1273
1315
  }
1274
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
+
1275
1389
  async function cmdUpdate() {
1276
1390
  const cwd = process.cwd();
1277
1391
  console.log(' Updating Dual Brain...');
@@ -2115,6 +2229,15 @@ function getEffectiveBypassPermissions(cwd) {
2115
2229
  return !!profile.bypassPermissions;
2116
2230
  }
2117
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
+
2118
2241
  function parseModeCommand(input) {
2119
2242
  const text = input.trim().toLowerCase().replace(/\s+/g, ' ');
2120
2243
  const wantsSession = /\b(this|current)\s+(session|conversation|terminal|chat)\b|\bfor this\b|\bfor current\b/.test(text);
@@ -2122,7 +2245,8 @@ function parseModeCommand(input) {
2122
2245
  const scope = wantsGlobal && !wantsSession ? 'profile' : 'session';
2123
2246
 
2124
2247
  let key = null;
2125
- 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';
2126
2250
  if (/\b(bypass|permission|permissions|approval|approvals|sandbox|safe mode)\b/.test(text)) key = 'bypassPermissions';
2127
2251
  if (!key) return null;
2128
2252
 
@@ -2142,18 +2266,78 @@ function applyModeCommand(cmd, cwd) {
2142
2266
  const profile = loadProfile(cwd);
2143
2267
  if (cmd.key === 'automode') {
2144
2268
  profile.automode = cmd.value;
2269
+ if (cmd.value) profile.bypassPermissions = false;
2145
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 };
2146
2275
  } else {
2147
2276
  profile.bypassPermissions = cmd.value;
2277
+ if (cmd.value) {
2278
+ profile.automode = true;
2279
+ profile.settings = { ...(profile.settings || {}), automode: true };
2280
+ }
2148
2281
  }
2149
2282
  saveProfile(profile, { cwd });
2150
- return { scope: 'default', value: cmd.value };
2283
+ return { scope: 'default', value: cmd.value, key: cmd.key };
2151
2284
  }
2152
2285
 
2153
2286
  const settings = loadSessionSettings(cwd);
2154
- 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
+ }
2155
2297
  saveSessionSettings(cwd, settings);
2156
- 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);
2157
2341
  }
2158
2342
 
2159
2343
  function pidAlive(pid) {
@@ -2168,6 +2352,70 @@ function activeConversationPath(cwd) {
2168
2352
  return join(cwd, '.dualbrain', 'active-conversation.json');
2169
2353
  }
2170
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
+
2171
2419
  function readActiveConversation(cwd) {
2172
2420
  try {
2173
2421
  const lease = JSON.parse(readFileSync(activeConversationPath(cwd), 'utf8'));
@@ -2177,6 +2425,49 @@ function readActiveConversation(cwd) {
2177
2425
  } catch { return null; }
2178
2426
  }
2179
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
+
2180
2471
  function writeActiveConversation(cwd, session, tool) {
2181
2472
  const dir = join(cwd, '.dualbrain');
2182
2473
  const terminalId = getTerminalId();
@@ -2195,6 +2486,73 @@ function writeActiveConversation(cwd, session, tool) {
2195
2486
  return lease;
2196
2487
  }
2197
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
+
2198
2556
  function clearActiveConversation(cwd, sessionId) {
2199
2557
  try {
2200
2558
  const lease = JSON.parse(readFileSync(activeConversationPath(cwd), 'utf8'));
@@ -2228,6 +2586,15 @@ async function launchSessionWithLease(sess, cwd, ask = null) {
2228
2586
 
2229
2587
  const { spawnSync } = await import('node:child_process');
2230
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
+ }
2231
2598
  const launchArgs = _sessionLaunchArgs(sess, cwd);
2232
2599
  if (decision === 'takeover') {
2233
2600
  process.stdout.write(' Taking over active conversation in this terminal.\n');
@@ -3335,7 +3702,7 @@ async function mainScreen(rl, ask) {
3335
3702
  [`s`, 'settings & profiles'],
3336
3703
  [`d`, 'doctor — diagnose issues'],
3337
3704
  [`t`, 'team settings'],
3338
- [`a`, getEffectiveAutomode(profile, cwd) ? 'auto mode (on)' : 'auto mode (off)'],
3705
+ [`a`, getEffectiveAutomode(profile, cwd) && !getEffectiveBypassPermissions(cwd) ? 'smart auto (on)' : 'smart auto (off)'],
3339
3706
  [`q`, 'quit'],
3340
3707
  ];
3341
3708
  for (const [key, label] of shortcuts) {
@@ -3353,7 +3720,7 @@ async function mainScreen(rl, ask) {
3353
3720
 
3354
3721
  // ── Key handling ──────────────────────────────────────────────────────────
3355
3722
  // Use raw keypress mode so we can show a live type-to-start buffer.
3356
- // 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.
3357
3724
  let taskBuffer = '';
3358
3725
 
3359
3726
  const readline = await import('node:readline');
@@ -3404,8 +3771,15 @@ async function mainScreen(rl, ask) {
3404
3771
  if (name === 'return' || name === 'enter' || seq === '\r' || seq === '\n') {
3405
3772
  cleanup();
3406
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);
3407
3777
  process.stdout.write('\n');
3408
- resolve(`__task__:${taskBuffer}`);
3778
+ if (singleKeySet.has(shortcut) || (!isNaN(digit) && digit >= 1 && digit <= 9)) {
3779
+ resolve(shortcut);
3780
+ } else {
3781
+ resolve(`__task__:${taskBuffer}`);
3782
+ }
3409
3783
  } else {
3410
3784
  resolve('');
3411
3785
  }
@@ -3433,25 +3807,6 @@ async function mainScreen(rl, ask) {
3433
3807
  const code = str.codePointAt(0);
3434
3808
  if (code < 32 || code === 127) return;
3435
3809
 
3436
- // Single-key commands only fire when buffer is empty
3437
- if (taskBuffer.length === 0) {
3438
- const lower = str.toLowerCase();
3439
- const singleKeySet = new Set(['n', 'g', 's', 't', 'q', '/', 'i', '?', 'h', 'd', 'a']);
3440
- if (singleKeySet.has(lower)) {
3441
- cleanup();
3442
- process.stdout.write('\n');
3443
- resolve(lower);
3444
- return;
3445
- }
3446
- const digit = parseInt(str, 10);
3447
- if (!isNaN(digit) && digit >= 1 && digit <= 9) {
3448
- cleanup();
3449
- process.stdout.write('\n');
3450
- resolve(str);
3451
- return;
3452
- }
3453
- }
3454
-
3455
3810
  // Accumulate into buffer
3456
3811
  taskBuffer += str;
3457
3812
  renderBuffer(taskBuffer);
@@ -3476,11 +3831,17 @@ async function mainScreen(rl, ask) {
3476
3831
 
3477
3832
  if (cmd === 'mode') {
3478
3833
  const result = applyModeCommand(classified.modeCommand, cwd);
3479
- 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';
3480
3839
  const state = result.value ? '\x1b[32mON\x1b[0m' : '\x1b[2mOFF\x1b[0m';
3481
- process.stdout.write(`\n ${keyLabel}: ${state} \x1b[2m(${result.scope})\x1b[0m\n\n`);
3482
- await ask(' Press Enter to continue...');
3483
- 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);
3484
3845
  }
3485
3846
 
3486
3847
  if (cmd === 'resume' || cmd === 'r') {
@@ -3563,12 +3924,12 @@ async function mainScreen(rl, ask) {
3563
3924
  const nextAuto = !getEffectiveAutomode(prof, cwd2);
3564
3925
  const settings = loadSessionSettings(cwd2);
3565
3926
  settings.automode = nextAuto;
3927
+ if (nextAuto) settings.bypassPermissions = false;
3566
3928
  saveSessionSettings(cwd2, settings);
3567
3929
  const state = nextAuto ? '\x1b[32mON\x1b[0m' : '\x1b[2mOFF\x1b[0m';
3568
- process.stdout.write(`\n Automode: ${state} \x1b[2m(this session)\x1b[0m\n`);
3569
- process.stdout.write(` ${nextAuto ? 'Tasks dispatch immediately (HEAD still gates dangerous ops)' : 'Tasks require Enter to confirm'}\n\n`);
3570
- await ask(' Press Enter to continue...');
3571
- 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');
3572
3933
  }
3573
3934
  if (cmd === 'init --replit') {
3574
3935
  await cmdInit(rl);
@@ -3758,10 +4119,10 @@ async function mainScreen(rl, ask) {
3758
4119
  const nextAuto = !getEffectiveAutomode(prof, cwd);
3759
4120
  const settings = loadSessionSettings(cwd);
3760
4121
  settings.automode = nextAuto;
4122
+ if (nextAuto) settings.bypassPermissions = false;
3761
4123
  saveSessionSettings(cwd, settings);
3762
- process.stdout.write(`\n Automode: ${nextAuto ? '\x1b[32mON\x1b[0m' : '\x1b[2mOFF\x1b[0m'} \x1b[2m(this session)\x1b[0m\n\n`);
3763
- await ask(' Press Enter to continue...');
3764
- 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');
3765
4126
  }
3766
4127
  if (choice === 'q' || choice === 'exit') { return { next: 'exit' }; }
3767
4128
 
@@ -3796,11 +4157,20 @@ async function switchProviderScreen(rl, ask, ctx = {}) {
3796
4157
  process.stdout.write('\n');
3797
4158
  process.stdout.write(` Continue in ${target === 'codex' ? 'Codex/GPT' : 'Claude'}\n`);
3798
4159
  process.stdout.write(` From: ${currentTool} · ${String(label || '').replace(/\s+/g, ' ').slice(0, 80)}\n\n`);
3799
- 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`);
3800
4161
 
3801
4162
  const choice = (await ask(' Choice: ')).trim().toLowerCase();
3802
- 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' };
3803
4170
 
4171
+ setSessionResumeProvider(cwd, session, target, 'user-switch-now');
4172
+ writeHandoffConversationLease(cwd, session, currentTool, target, brief);
4173
+ markSessionSuperseded(cwd, session, target, 'manual-provider-switch');
3804
4174
  await cmdSwitch([target, brief]);
3805
4175
  return { next: 'main' };
3806
4176
  }
@@ -3821,7 +4191,7 @@ async function paletteHelpScreen(rl, ask) {
3821
4191
  const CYAN = '\x1b[36m';
3822
4192
  const lines = [
3823
4193
  top,
3824
- row(`${CYAN}Keyboard Shortcuts${RESET} (single key, no Enter needed)`),
4194
+ row(`${CYAN}Keyboard Shortcuts${RESET} (type key, then Enter)`),
3825
4195
  sep,
3826
4196
  row(`${CYAN}Enter${RESET} Resume last session`),
3827
4197
  row(`${CYAN}n${RESET} New coding session`),
@@ -4339,6 +4709,7 @@ async function settingsScreen(rl, ask) {
4339
4709
  const automode = getEffectiveAutomode(profile, cwd);
4340
4710
  const sessionSettings = loadSessionSettings(cwd);
4341
4711
  const bypassPermissions = getEffectiveBypassPermissions(cwd);
4712
+ const conversationMode = getEffectiveConversationMode(profile, cwd);
4342
4713
  const autoScope = typeof sessionSettings.automode === 'boolean' ? 'session' : 'default';
4343
4714
  const permissionScope = typeof sessionSettings.bypassPermissions === 'boolean' ? 'session' : 'default';
4344
4715
 
@@ -4387,11 +4758,18 @@ async function settingsScreen(rl, ask) {
4387
4758
  const permMode = bypassPermissions
4388
4759
  ? `${RED}bypass approvals and sandbox${RESET}`
4389
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}`;
4390
4766
  const convLines = [
4391
- ` ${DIM}Auto mode${RESET} ${autoMark} ${automode ? 'run safe tasks immediately' : 'ask before launching tasks'} ${DIM}[${autoScope}]${RESET}`,
4392
- ` ${DIM}Permissions${RESET} ${permMark} ${permMode} ${DIM}[${permissionScope}]${RESET}`,
4393
- ` ${DIM}Claude resume${RESET} ${bypassPermissions ? '--dangerously-skip-permissions' : 'normal permissions'}`,
4394
- ` ${DIM}Codex resume${RESET} ${bypassPermissions ? '--dangerously-bypass-approvals-and-sandbox' : 'workspace-write + on-request'}`,
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'}`,
4772
+ ` ${DIM}Codex resume${RESET} ${bypassPermissions ? '--dangerously-bypass-approvals-and-sandbox' : `workspace-write + ${automode ? 'never ask' : 'on-request'}`}`,
4395
4773
  ];
4396
4774
 
4397
4775
  // ── System info ──────────────────────────────────────────────────────────
@@ -4467,7 +4845,7 @@ async function settingsScreen(rl, ask) {
4467
4845
  const convContent = [
4468
4846
  ...convLines.map(l => l.replace(/^ /, '')),
4469
4847
  '',
4470
- signalLine('info', `${DIM}[o] auto mode [v] permission mode${RESET}`),
4848
+ signalLine('info', `${DIM}[o] smart auto [m] manual [v] bypass/safety${RESET}`),
4471
4849
  ];
4472
4850
 
4473
4851
  const sysContent = [
@@ -4512,23 +4890,34 @@ async function settingsScreen(rl, ask) {
4512
4890
 
4513
4891
  // Conversation behavior toggles
4514
4892
  if (choice === 'o') {
4515
- const nextAuto = !automode;
4516
4893
  const settings = loadSessionSettings(cwd);
4517
- settings.automode = nextAuto;
4894
+ settings.automode = true;
4895
+ settings.bypassPermissions = false;
4518
4896
  saveSessionSettings(cwd, settings);
4519
- process.stdout.write(`\n Auto mode: ${nextAuto ? GREEN + 'ON' + RESET : DIM + 'OFF' + RESET} ${DIM}(this session)${RESET}\n\n`);
4520
- await ask(' Press Enter to continue...');
4521
- 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;
4522
4910
  }
4523
4911
 
4524
4912
  if (choice === 'v') {
4525
4913
  if (bypassPermissions) {
4526
4914
  const settings = loadSessionSettings(cwd);
4527
4915
  settings.bypassPermissions = false;
4916
+ settings.automode = true;
4528
4917
  saveSessionSettings(cwd, settings);
4529
- process.stdout.write(`\n Permission mode: ${GREEN}safe approvals + workspace sandbox${RESET} ${DIM}(this session)${RESET}\n\n`);
4530
- await ask(' Press Enter to continue...');
4531
- 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;
4532
4921
  }
4533
4922
 
4534
4923
  process.stdout.write(`\n ${RED}Bypass mode disables provider approval prompts and sandboxing.${RESET}\n`);
@@ -4537,8 +4926,11 @@ async function settingsScreen(rl, ask) {
4537
4926
  if (confirm === 'YES') {
4538
4927
  const settings = loadSessionSettings(cwd);
4539
4928
  settings.bypassPermissions = true;
4929
+ settings.automode = true;
4540
4930
  saveSessionSettings(cwd, settings);
4541
- 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;
4542
4934
  } else {
4543
4935
  process.stdout.write('\n Permission mode unchanged.\n\n');
4544
4936
  }
@@ -5298,6 +5690,30 @@ async function runOnboardingWizard(_detection, cwd, rl) {
5298
5690
 
5299
5691
  process.stdout.write('\n');
5300
5692
 
5693
+ // ─── Step 3: Behavior defaults ───────────────────────────────────────────
5694
+ process.stdout.write(` ${BOLD}Conversation behavior:${RST}\n\n`);
5695
+ process.stdout.write(` ${GRAY}Auto mode lets safe tasks launch immediately. HEAD still gates risky work.${RST}\n`);
5696
+ process.stdout.write(` ${GRAY}[Enter]${RST} auto mode on ${GRAY}[n]${RST} ask before launching tasks\n\n`);
5697
+ const autoKey = await singleKey(['n', '\r', 'y']);
5698
+ const defaultAutomode = autoKey !== 'n';
5699
+ process.stdout.write('\n');
5700
+
5701
+ process.stdout.write(` ${BOLD}Permission mode:${RST}\n\n`);
5702
+ process.stdout.write(` ${GREEN}●${RST} Safe approvals + workspace sandbox (recommended)\n`);
5703
+ process.stdout.write(` ${GRAY}○${RST} Bypass approvals/sandbox (advanced, trusted workspaces only)\n`);
5704
+ process.stdout.write('\n');
5705
+ process.stdout.write(` ${GRAY}[Enter]${RST} Safe ${GRAY}[b]${RST} bypass\n\n`);
5706
+ const permissionKey = await singleKey(['b', '\r']);
5707
+ let defaultBypassPermissions = false;
5708
+ if (permissionKey === 'b') {
5709
+ process.stdout.write('\n');
5710
+ process.stdout.write(` ${GRAY}Bypass mode disables provider approval prompts and sandboxing.${RST}\n`);
5711
+ process.stdout.write(` ${GRAY}Type YES to confirm: ${RST}`);
5712
+ const confirm = await new Promise(res => rl.question('', res));
5713
+ defaultBypassPermissions = String(confirm).trim() === 'YES';
5714
+ process.stdout.write('\n');
5715
+ }
5716
+
5301
5717
  // Init living docs (non-fatal)
5302
5718
  try {
5303
5719
  const ld = await getLivingDocs();
@@ -5328,6 +5744,9 @@ async function runOnboardingWizard(_detection, cwd, rl) {
5328
5744
  finalProfile.mode = enabledCount >= 2 ? 'dual' : finalClaudeEnabled ? 'solo-claude' : 'solo-openai';
5329
5745
  finalProfile.bias = chosenBias;
5330
5746
  finalProfile.workStyle = chosenBias;
5747
+ finalProfile.automode = defaultAutomode;
5748
+ finalProfile.settings = { ...(finalProfile.settings || {}), automode: defaultAutomode };
5749
+ finalProfile.bypassPermissions = defaultBypassPermissions;
5331
5750
 
5332
5751
  // Ask about default shell (only on first wizard run)
5333
5752
  if (!finalProfile.defaultShellAsked) {
@@ -6421,6 +6840,15 @@ async function runScreens(startScreen = 'dashboard') {
6421
6840
  let current = startScreen;
6422
6841
  let ctx = {};
6423
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
+
6424
6852
  // Handle type-to-start dispatch from mainScreen — all work routes through pipeline.
6425
6853
  if (current === 'go' && ctx.prompt) {
6426
6854
  const prompt = ctx.prompt;
@@ -7195,6 +7623,7 @@ async function main() {
7195
7623
  if (cmd === 'status') { await cmdStatus(args.slice(1)); return; }
7196
7624
  if (cmd === 'handoff') { await cmdHandoff(args.slice(1)); return; }
7197
7625
  if (cmd === 'switch') { await cmdSwitch(args.slice(1)); return; }
7626
+ if (cmd === 'runtime-switch') { await cmdRuntimeSwitch(args.slice(1)); return; }
7198
7627
  if (cmd === 'update' || cmd === 'upgrade') { await cmdUpdate(); return; }
7199
7628
  if (cmd === 'hot') { cmdHot(args[1]); return; }
7200
7629
  if (cmd === 'cool') { cmdCool(args[1]); return; }
@@ -7259,7 +7688,7 @@ fi
7259
7688
  // If cmd is not a recognized subcommand, treat the entire arg list as a task.
7260
7689
  // e.g. `dual-brain fix failing tests` → same as `dual-brain go "fix failing tests"`
7261
7690
  const KNOWN_COMMANDS = new Set([
7262
- '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',
7263
7692
  'remember', 'forget', 'break-glass', 'specialists', 'search', 'shell-hook', 'watch', 'update', 'upgrade',
7264
7693
  '--help', '-h', '--version', '-v',
7265
7694
  ...Object.keys(loadSpecialistRegistry()),