ctx-switch 2.0.4 → 2.0.5

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.
Files changed (2) hide show
  1. package/dist/index.mjs +299 -45
  2. package/package.json +1 -1
package/dist/index.mjs CHANGED
@@ -14,7 +14,7 @@ import path from "node:path";
14
14
  // src/types.ts
15
15
  var SUPPORTED_COMMANDS = ["continue", "doctor", "sessions"];
16
16
  var SUPPORTED_PROVIDERS = ["openrouter"];
17
- var SUPPORTED_TARGETS = ["generic", "codex", "cursor", "chatgpt"];
17
+ var SUPPORTED_TARGETS = ["generic", "claude", "codex", "cursor", "chatgpt"];
18
18
  var SUPPORTED_SOURCES = ["claude", "codex", "opencode"];
19
19
 
20
20
  // src/args.ts
@@ -142,7 +142,7 @@ function getHelpText({ name, version }) {
142
142
  " -o, --output <file> Write the final prompt to a file",
143
143
  " --source <name> Session source: claude, codex, opencode (interactive if omitted)",
144
144
  " --session <id|path> Use a specific session file or session id",
145
- " --target <name> Prompt target: generic, codex, cursor, chatgpt",
145
+ " --target <name> Prompt target: generic, claude, codex, cursor, chatgpt",
146
146
  " -n, --limit <count> Limit rows for the sessions command (default: 10)",
147
147
  "",
148
148
  "Refinement (optional)",
@@ -160,6 +160,7 @@ function getHelpText({ name, version }) {
160
160
  "Examples",
161
161
  ` ${name} # interactive source picker`,
162
162
  ` ${name} --source claude # use Claude Code sessions`,
163
+ ` ${name} --source codex --target claude`,
163
164
  ` ${name} --source codex --target codex`,
164
165
  ` ${name} --source opencode`,
165
166
  ` ${name} --refine --model openrouter/free`,
@@ -600,6 +601,60 @@ import path5 from "node:path";
600
601
  var CODEX_DIR = path5.join(os3.homedir(), ".codex");
601
602
  var CODEX_SESSIONS_DIR = path5.join(CODEX_DIR, "sessions");
602
603
  var CODEX_INDEX_PATH = path5.join(CODEX_DIR, "session_index.jsonl");
604
+ function parseJsonRecord(line) {
605
+ try {
606
+ return JSON.parse(line);
607
+ } catch {
608
+ return null;
609
+ }
610
+ }
611
+ function parseFunctionArguments(raw) {
612
+ if (!raw) return {};
613
+ try {
614
+ const parsed = JSON.parse(raw);
615
+ return parsed && typeof parsed === "object" ? parsed : {};
616
+ } catch {
617
+ return {};
618
+ }
619
+ }
620
+ function parseCustomToolInput(toolName, raw) {
621
+ if (!raw) return {};
622
+ if (typeof raw !== "string") return raw;
623
+ const trimmed = raw.trim();
624
+ if (trimmed.startsWith("{") || trimmed.startsWith("[")) {
625
+ try {
626
+ const parsed = JSON.parse(trimmed);
627
+ if (parsed && typeof parsed === "object") {
628
+ return parsed;
629
+ }
630
+ } catch {
631
+ }
632
+ }
633
+ if (toolName === "apply_patch") {
634
+ return { patch: raw };
635
+ }
636
+ return { input: raw };
637
+ }
638
+ function parseToolOutput(raw) {
639
+ if (!raw) return { text: "" };
640
+ let text = raw;
641
+ let exitCode;
642
+ try {
643
+ const parsed = JSON.parse(raw);
644
+ if (typeof parsed.output === "string") {
645
+ text = parsed.output;
646
+ }
647
+ if (typeof parsed.metadata?.exit_code === "number") {
648
+ exitCode = parsed.metadata.exit_code;
649
+ }
650
+ } catch {
651
+ }
652
+ const isError = exitCode !== void 0 ? exitCode !== 0 : /Process exited with code [^0]/.test(text) || /error|Error|ERROR/.test(text.slice(0, 200));
653
+ return {
654
+ text: text.slice(0, 1500),
655
+ isError: isError || void 0
656
+ };
657
+ }
603
658
  function listSessionsForProject2(cwd) {
604
659
  if (!fs4.existsSync(CODEX_SESSIONS_DIR)) {
605
660
  return [];
@@ -680,12 +735,8 @@ function parseSession2(sessionPath) {
680
735
  }
681
736
  }
682
737
  for (const line of lines) {
683
- let record;
684
- try {
685
- record = JSON.parse(line);
686
- } catch {
687
- continue;
688
- }
738
+ const record = parseJsonRecord(line);
739
+ if (!record) continue;
689
740
  if (record.type === "session_meta") {
690
741
  const payload2 = record.payload;
691
742
  if (payload2.cwd) meta.cwd = payload2.cwd;
@@ -693,6 +744,21 @@ function parseSession2(sessionPath) {
693
744
  if (payload2.id) meta.sessionId = payload2.id;
694
745
  continue;
695
746
  }
747
+ if (record.type === "event_msg" && record.payload.type === "exec_command_end" && record.payload.call_id) {
748
+ const tc = pendingCalls.get(record.payload.call_id);
749
+ if (tc) {
750
+ if (Array.isArray(record.payload.parsed_cmd)) {
751
+ tc.input.parsed_cmd = record.payload.parsed_cmd;
752
+ }
753
+ if (typeof record.payload.aggregated_output === "string" && !tc.result) {
754
+ tc.result = record.payload.aggregated_output.slice(0, 1500);
755
+ }
756
+ if (typeof record.payload.exit_code === "number") {
757
+ tc.isError = record.payload.exit_code !== 0;
758
+ }
759
+ }
760
+ continue;
761
+ }
696
762
  if (record.type !== "response_item") continue;
697
763
  const payload = record.payload;
698
764
  const payloadType = payload.type;
@@ -718,11 +784,7 @@ function parseSession2(sessionPath) {
718
784
  continue;
719
785
  }
720
786
  if (payloadType === "function_call" && payload.name) {
721
- let input = {};
722
- try {
723
- input = JSON.parse(payload.arguments || "{}");
724
- } catch {
725
- }
787
+ const input = parseFunctionArguments(payload.arguments);
726
788
  const tc = {
727
789
  id: payload.call_id || null,
728
790
  tool: payload.name,
@@ -734,12 +796,26 @@ function parseSession2(sessionPath) {
734
796
  }
735
797
  continue;
736
798
  }
737
- if (payloadType === "function_call_output" && payload.call_id) {
799
+ if (payloadType === "custom_tool_call" && payload.name) {
800
+ const tc = {
801
+ id: payload.call_id || null,
802
+ tool: payload.name,
803
+ input: parseCustomToolInput(payload.name, payload.input)
804
+ };
805
+ currentToolCalls.push(tc);
806
+ if (payload.call_id) {
807
+ pendingCalls.set(payload.call_id, tc);
808
+ }
809
+ continue;
810
+ }
811
+ if ((payloadType === "function_call_output" || payloadType === "custom_tool_call_output") && payload.call_id) {
738
812
  const tc = pendingCalls.get(payload.call_id);
739
813
  if (tc) {
740
- const output = payload.output || "";
741
- tc.result = output.slice(0, 1500);
742
- tc.isError = /Process exited with code [^0]/.test(output) || /error|Error|ERROR/.test(output.slice(0, 200));
814
+ const parsed = parseToolOutput(payload.output);
815
+ tc.result = parsed.text;
816
+ if (parsed.isError !== void 0) {
817
+ tc.isError = parsed.isError;
818
+ }
743
819
  }
744
820
  continue;
745
821
  }
@@ -950,6 +1026,20 @@ function extractCommand(input) {
950
1026
  const value = input.command || input.cmd;
951
1027
  return typeof value === "string" ? value : null;
952
1028
  }
1029
+ function resolveSessionFilePath(filePath, cwd) {
1030
+ return path7.isAbsolute(filePath) ? filePath : path7.resolve(cwd, filePath);
1031
+ }
1032
+ function extractPatchFilePaths(input, cwd) {
1033
+ const patch = input.patch;
1034
+ if (typeof patch !== "string") return [];
1035
+ const matches = [...patch.matchAll(/^\*\*\* (?:Update|Add|Delete) File: (.+)$/gm)];
1036
+ return matches.map((match) => resolveSessionFilePath(match[1].trim(), cwd));
1037
+ }
1038
+ function extractParsedCommands(input) {
1039
+ const parsed = input.parsed_cmd;
1040
+ if (!Array.isArray(parsed)) return [];
1041
+ return parsed.filter((entry) => Boolean(entry) && typeof entry === "object");
1042
+ }
953
1043
  function buildSessionContext({
954
1044
  messages,
955
1045
  meta,
@@ -967,10 +1057,19 @@ function buildSessionContext({
967
1057
  const toolName = String(toolCall.tool || "").toLowerCase();
968
1058
  const filePath = extractFilePath(toolCall.input);
969
1059
  const command = extractCommand(toolCall.input);
1060
+ const patchPaths = extractPatchFilePaths(toolCall.input, cwd);
1061
+ const parsedCommands = extractParsedCommands(toolCall.input);
970
1062
  if (filePath && /(edit|write|create|multi_edit)/.test(toolName)) {
971
- filesModified.add(filePath);
1063
+ filesModified.add(resolveSessionFilePath(filePath, cwd));
972
1064
  } else if (filePath && /(read|grep|glob|search)/.test(toolName)) {
973
- filesRead.add(filePath);
1065
+ filesRead.add(resolveSessionFilePath(filePath, cwd));
1066
+ }
1067
+ for (const patchPath of patchPaths) {
1068
+ filesModified.add(patchPath);
1069
+ }
1070
+ for (const parsedCommand of parsedCommands) {
1071
+ if (parsedCommand.type !== "read" || typeof parsedCommand.path !== "string") continue;
1072
+ filesRead.add(resolveSessionFilePath(parsedCommand.path, cwd));
974
1073
  }
975
1074
  if (command && /(bash|command|run|exec_command)/.test(toolName)) {
976
1075
  commands.push(command);
@@ -985,8 +1084,10 @@ function buildSessionContext({
985
1084
  const toolSummary = message.toolCalls.map((toolCall) => {
986
1085
  const filePath = extractFilePath(toolCall.input);
987
1086
  const command = extractCommand(toolCall.input);
1087
+ const patchPaths = extractPatchFilePaths(toolCall.input, cwd);
988
1088
  let summary = "";
989
- if (filePath) summary = `${toolCall.tool} ${filePath}`;
1089
+ if (filePath) summary = `${toolCall.tool} ${resolveSessionFilePath(filePath, cwd)}`;
1090
+ else if (patchPaths.length > 0) summary = `${toolCall.tool} ${patchPaths.join(", ")}`;
990
1091
  else if (command) summary = `${toolCall.tool}: ${command}`;
991
1092
  else summary = toolCall.tool;
992
1093
  if (toolCall.isError && toolCall.result) {
@@ -1156,8 +1257,18 @@ function compactText(text, maxChars = 800) {
1156
1257
  function unique(list) {
1157
1258
  return [...new Set(list.filter(Boolean))];
1158
1259
  }
1260
+ function extractFilePath2(input) {
1261
+ const value = input.file_path || input.path || input.target_file || input.filePath;
1262
+ return typeof value === "string" ? value : null;
1263
+ }
1264
+ function extractCommand2(input) {
1265
+ const value = input.command || input.cmd;
1266
+ return typeof value === "string" ? value : null;
1267
+ }
1159
1268
  function buildTargetGuidance(target) {
1160
1269
  switch (target) {
1270
+ case "claude":
1271
+ return "The next agent is Claude Code. It should read the active files first, trust the current workspace and git diff over stale transcript details, and continue the implementation or debugging directly.";
1161
1272
  case "codex":
1162
1273
  return "The next agent is Codex. It should inspect the current files first, avoid redoing completed work, and finish any remaining implementation or verification.";
1163
1274
  case "cursor":
@@ -1165,19 +1276,20 @@ function buildTargetGuidance(target) {
1165
1276
  case "chatgpt":
1166
1277
  return "The next agent is ChatGPT. It should reason from the current workspace state, explain what remains, and provide explicit next actions.";
1167
1278
  default:
1168
- return "The next agent should continue the interrupted work from the current workspace state without redoing completed steps.";
1279
+ return "The next agent should read the active files first, trust the current workspace and git diff over stale transcript details, continue the interrupted work directly, and avoid redoing completed steps.";
1169
1280
  }
1170
1281
  }
1171
1282
  function isNoiseMessage(text) {
1172
1283
  const trimmed = text.trim().toLowerCase();
1284
+ const normalized = trimmed.replace(/[^\p{L}\p{N}\s]/gu, " ").replace(/\s+/g, " ").trim();
1173
1285
  if (trimmed.length < 5) return true;
1174
1286
  const noise = [
1175
1287
  "yes",
1288
+ "yes please",
1176
1289
  "no",
1177
1290
  "ok",
1178
1291
  "okay",
1179
1292
  "try",
1180
- "try?",
1181
1293
  "sure",
1182
1294
  "do it",
1183
1295
  "go ahead",
@@ -1194,11 +1306,16 @@ function isNoiseMessage(text) {
1194
1306
  "try turn off thinking",
1195
1307
  "try without timeout"
1196
1308
  ];
1197
- if (noise.includes(trimmed)) return true;
1198
- if (/^(try|yes|ok|sure|test|run)\s/i.test(trimmed) && trimmed.length < 40) return true;
1309
+ if (noise.includes(normalized)) return true;
1310
+ if (/^(try|yes|ok|sure|test|run)\s/i.test(normalized) && normalized.length < 40) return true;
1199
1311
  if (trimmed.startsWith("[request interrupted")) return true;
1200
1312
  return false;
1201
1313
  }
1314
+ function isReferentialMessage(text) {
1315
+ const normalized = text.trim().toLowerCase().replace(/[^\p{L}\p{N}\s]/gu, " ").replace(/\s+/g, " ").trim();
1316
+ if (!normalized || normalized.length > 220) return false;
1317
+ return /^(ok|okay|alright|now|so)\b/.test(normalized) || /\b(it|that|again|better|same|continue|still|also|another|more)\b/.test(normalized);
1318
+ }
1202
1319
  function filterUserMessages(messages) {
1203
1320
  const all = messages.filter((m) => m.role === "user" && m.content).map((m) => m.content.trim());
1204
1321
  if (all.length <= 2) return all;
@@ -1228,30 +1345,140 @@ function extractKeyDecisions(messages) {
1228
1345
  for (const msg of messages) {
1229
1346
  if (msg.role !== "assistant" || !msg.content) continue;
1230
1347
  const lower = msg.content.toLowerCase();
1231
- if (lower.includes("instead") || lower.includes("let me try") || lower.includes("switching to") || lower.includes("the issue is") || lower.includes("the problem is") || lower.includes("root cause")) {
1348
+ if (/\b(handoff|prompt)\b/.test(lower) && /\b(good|bad|better|worse|quality)\b/.test(lower)) {
1349
+ continue;
1350
+ }
1351
+ if (/\b(root cause|the issue is|the problem is|caused by|failed because|failing because|need to)\b/.test(lower) || /\b(exposed|revealed|showed)\b.*\b(gap|issue|problem|bug)\b/.test(lower) || /\bmissing\b/.test(lower)) {
1232
1352
  decisions.push(compactText(msg.content, 300));
1233
1353
  }
1234
1354
  }
1235
1355
  return decisions.slice(-5);
1236
1356
  }
1357
+ function findFocusedWindow(messages) {
1358
+ if (messages.length === 0) {
1359
+ return { messages, sessionAppearsComplete: false };
1360
+ }
1361
+ const substantiveUserIndexes = messages.map((message, index) => ({ message, index })).filter(({ message }) => message.role === "user" && message.content && !isNoiseMessage(message.content)).map(({ index }) => index);
1362
+ if (substantiveUserIndexes.length === 0) {
1363
+ return { messages, sessionAppearsComplete: false };
1364
+ }
1365
+ const lastToolIndex = messages.reduce(
1366
+ (last, message, index) => message.role === "assistant" && message.toolCalls.length > 0 ? index : last,
1367
+ -1
1368
+ );
1369
+ const postToolUsers = substantiveUserIndexes.filter((index) => index > lastToolIndex);
1370
+ let startIndex = 0;
1371
+ if (postToolUsers.length > 0) {
1372
+ startIndex = postToolUsers[0];
1373
+ } else if (lastToolIndex >= 0) {
1374
+ startIndex = substantiveUserIndexes.filter((index) => index <= lastToolIndex).at(-1) ?? 0;
1375
+ } else {
1376
+ startIndex = substantiveUserIndexes.at(-1) ?? 0;
1377
+ }
1378
+ const startMessage = messages[startIndex];
1379
+ if (startMessage?.role === "user" && isReferentialMessage(startMessage.content)) {
1380
+ const previousSubstantive = substantiveUserIndexes.filter((index) => index < startIndex).at(-1);
1381
+ if (typeof previousSubstantive === "number") {
1382
+ startIndex = previousSubstantive;
1383
+ }
1384
+ }
1385
+ const focused = messages.slice(startIndex);
1386
+ const hasToolActivity = focused.some((message) => message.role === "assistant" && message.toolCalls.length > 0);
1387
+ const lastMessage = focused.at(-1);
1388
+ const sessionAppearsComplete = Boolean(lastMessage) && lastMessage.role === "assistant" && lastMessage.toolCalls.length === 0 && !hasToolActivity;
1389
+ return { messages: focused, sessionAppearsComplete };
1390
+ }
1391
+ function extractWorkSummary(messages) {
1392
+ const filesModified = /* @__PURE__ */ new Set();
1393
+ const commands = [];
1394
+ for (const message of messages) {
1395
+ if (message.role !== "assistant" || message.toolCalls.length === 0) continue;
1396
+ for (const toolCall of message.toolCalls) {
1397
+ const toolName = String(toolCall.tool || "").toLowerCase();
1398
+ const filePath = extractFilePath2(toolCall.input);
1399
+ const command = extractCommand2(toolCall.input);
1400
+ if (filePath && /(edit|write|create|multi_edit)/.test(toolName)) {
1401
+ filesModified.add(filePath);
1402
+ }
1403
+ if (command && /(bash|command|run|exec_command)/.test(toolName)) {
1404
+ commands.push(command);
1405
+ }
1406
+ }
1407
+ }
1408
+ return {
1409
+ filesModified: [...filesModified],
1410
+ commands
1411
+ };
1412
+ }
1413
+ function extractLastAssistantAnswer(messages) {
1414
+ for (let i = messages.length - 1; i >= 0; i--) {
1415
+ const message = messages[i];
1416
+ if (message.role === "assistant" && message.content.trim()) {
1417
+ return compactText(message.content, 500);
1418
+ }
1419
+ }
1420
+ return null;
1421
+ }
1422
+ function summarizeCommand(command) {
1423
+ return compactText(command.replace(/\s+/g, " ").trim(), 140);
1424
+ }
1425
+ function extractRecentCommands(commands) {
1426
+ return unique(commands.map(summarizeCommand)).slice(-6);
1427
+ }
1428
+ function extractStatusPaths(status) {
1429
+ if (!status) return [];
1430
+ return status.split("\n").map((line) => line.trim()).filter(Boolean).map((line) => {
1431
+ const renamed = line.includes(" -> ") ? line.split(" -> ").at(-1)?.trim() || "" : "";
1432
+ if (renamed) {
1433
+ return renamed.replace(/^(?:\?\?|[A-Z?!]{1,2})\s+/, "");
1434
+ }
1435
+ const match = line.match(/^(?:\?\?|[A-Z?!]{1,2})\s+(.*)$/);
1436
+ return match?.[1]?.trim() || line;
1437
+ }).filter(Boolean);
1438
+ }
1439
+ function extractFocusFiles(ctx, work) {
1440
+ return unique([
1441
+ ...work.filesModified,
1442
+ ...extractStatusPaths(ctx.gitContext.status),
1443
+ ...ctx.gitContext.untracked.map((file) => file.path)
1444
+ ]).slice(0, 6);
1445
+ }
1237
1446
  function buildRawPrompt(ctx, options = {}) {
1238
- const userMessages = filterUserMessages(ctx.messages);
1239
- const errors = extractUnresolvedErrors(ctx.messages);
1240
- const decisions = extractKeyDecisions(ctx.messages);
1447
+ const focused = findFocusedWindow(ctx.messages);
1448
+ const userMessages = filterUserMessages(focused.messages);
1449
+ const errors = extractUnresolvedErrors(focused.messages);
1450
+ const decisions = extractKeyDecisions(focused.messages);
1451
+ const work = extractWorkSummary(focused.messages);
1452
+ const focusFiles = extractFocusFiles(ctx, work);
1453
+ const recentCommands = extractRecentCommands(work.commands);
1454
+ const lastAssistantAnswer = extractLastAssistantAnswer(focused.messages);
1241
1455
  let prompt = "";
1242
1456
  prompt += "# Task\n\n";
1243
1457
  prompt += `Project: \`${ctx.sessionCwd}\`
1244
1458
  `;
1245
1459
  if (ctx.branch) prompt += `Branch: \`${ctx.branch}\`
1246
1460
  `;
1247
- prompt += "\nThis is a continuation of an interrupted AI coding session. ";
1248
- prompt += "The previous agent was working on the task below. Pick up where it left off.\n\n";
1249
- prompt += "## What The User Asked (chronological)\n\n";
1461
+ if (focused.sessionAppearsComplete) {
1462
+ prompt += "\nThe latest exchange in this session appears complete. ";
1463
+ prompt += "Use the focused context below only if the user wants to continue from that point.\n\n";
1464
+ } else {
1465
+ prompt += "\nThis is a continuation of an interrupted AI coding session. ";
1466
+ prompt += "The previous agent was working on the task below. Pick up where it left off.\n\n";
1467
+ }
1468
+ prompt += `## What The User Asked (${focused.sessionAppearsComplete ? "recent focus" : "chronological"})
1469
+
1470
+ `;
1250
1471
  for (const msg of userMessages) {
1251
1472
  prompt += `- ${compactText(msg, 500)}
1252
1473
  `;
1253
1474
  }
1254
1475
  prompt += "\n";
1476
+ if (focused.sessionAppearsComplete && lastAssistantAnswer) {
1477
+ prompt += "## Last Answer Already Given\n\n";
1478
+ prompt += `- ${lastAssistantAnswer}
1479
+
1480
+ `;
1481
+ }
1255
1482
  if (errors.length > 0) {
1256
1483
  prompt += "## DO NOT REPEAT \u2014 Unresolved Errors\n\n";
1257
1484
  prompt += "These errors occurred and were NOT fixed. Avoid the same approaches.\n\n";
@@ -1269,27 +1496,45 @@ function buildRawPrompt(ctx, options = {}) {
1269
1496
  }
1270
1497
  prompt += "\n";
1271
1498
  }
1272
- prompt += "## Work Already Completed\n\n";
1273
- if (unique(ctx.filesModified).length > 0) {
1499
+ if (work.filesModified.length > 0) {
1500
+ prompt += "## Work Already Completed\n\n";
1501
+ }
1502
+ if (work.filesModified.length > 0) {
1274
1503
  prompt += "**Files modified:**\n";
1275
- for (const filePath of unique(ctx.filesModified)) {
1504
+ for (const filePath of unique(work.filesModified)) {
1276
1505
  prompt += `- \`${filePath}\`
1277
1506
  `;
1278
1507
  }
1279
1508
  prompt += "\n";
1280
1509
  }
1281
- if (ctx.gitContext.recentCommits) {
1510
+ if (!focused.sessionAppearsComplete && work.filesModified.length > 0 && ctx.gitContext.recentCommits) {
1282
1511
  prompt += "**Recent commits:**\n```\n";
1283
1512
  prompt += `${ctx.gitContext.recentCommits}
1284
1513
  `;
1285
1514
  prompt += "```\n\n";
1286
1515
  }
1287
- if (ctx.gitContext.committedDiff) {
1516
+ if (!focused.sessionAppearsComplete && work.filesModified.length > 0 && ctx.gitContext.committedDiff) {
1288
1517
  prompt += "**Files changed in recent commits:**\n```\n";
1289
1518
  prompt += `${ctx.gitContext.committedDiff}
1290
1519
  `;
1291
1520
  prompt += "```\n\n";
1292
1521
  }
1522
+ if (recentCommands.length > 0) {
1523
+ prompt += "## Recent Commands / Checks\n\n";
1524
+ for (const command of recentCommands) {
1525
+ prompt += `- \`${command}\`
1526
+ `;
1527
+ }
1528
+ prompt += "\n";
1529
+ }
1530
+ if (focusFiles.length > 0) {
1531
+ prompt += "## Read These Files First\n\n";
1532
+ for (const filePath of focusFiles) {
1533
+ prompt += `- \`${filePath}\`
1534
+ `;
1535
+ }
1536
+ prompt += "\n";
1537
+ }
1293
1538
  const git = ctx.gitContext;
1294
1539
  if (git.isGitRepo && git.hasChanges) {
1295
1540
  prompt += "## Uncommitted Changes\n\n";
@@ -1317,19 +1562,28 @@ function buildRawPrompt(ctx, options = {}) {
1317
1562
  }
1318
1563
  }
1319
1564
  prompt += "## Your Instructions\n\n";
1320
- prompt += `${buildTargetGuidance(options.target)}
1565
+ if (focused.sessionAppearsComplete) {
1566
+ prompt += "The latest thread appears finished. Do not resume older tasks unless the user explicitly asks for them.\n\n";
1567
+ prompt += "1. **Start from the recent focus above** \u2014 ignore stale history unless the user points back to it.\n";
1568
+ prompt += "2. **Use the last answer as prior context** \u2014 avoid restating or redoing already completed work.\n";
1569
+ prompt += `3. **Inspect the workspace only as needed** \u2014 respond to follow-up questions or new work from the current repo state${focusFiles.length > 0 ? `, starting with ${focusFiles.map((filePath) => `\`${filePath}\``).join(", ")}` : ""}.
1570
+ `;
1571
+ } else {
1572
+ prompt += `${buildTargetGuidance(options.target)}
1321
1573
 
1322
1574
  `;
1323
- prompt += "1. **Read modified files first** \u2014 verify their current state before changing anything.\n";
1324
- if (errors.length > 0) {
1325
- prompt += "2. **Check the errors above** \u2014 do NOT repeat failed approaches. Try a different strategy.\n";
1326
- }
1327
- prompt += `${errors.length > 0 ? "3" : "2"}. **Identify what's done vs what remains** \u2014 the commits and modified files above show completed work.
1575
+ prompt += `1. **Read the active files first** \u2014 verify their current state before changing anything${focusFiles.length > 0 ? `: ${focusFiles.map((filePath) => `\`${filePath}\``).join(", ")}` : ""}.
1328
1576
  `;
1329
- prompt += `${errors.length > 0 ? "4" : "3"}. **Do the remaining work** \u2014 pick up exactly where the previous agent stopped.
1577
+ if (errors.length > 0) {
1578
+ prompt += "2. **Check the errors above** \u2014 do NOT repeat failed approaches. Try a different strategy.\n";
1579
+ }
1580
+ prompt += `${errors.length > 0 ? "3" : "2"}. **Identify what's done vs what remains** \u2014 use the recent commands, active files, and git state above as the source of truth for the current thread.
1581
+ `;
1582
+ prompt += `${errors.length > 0 ? "4" : "3"}. **Do the remaining work** \u2014 pick up exactly where the previous agent stopped.
1330
1583
  `;
1331
- prompt += `${errors.length > 0 ? "5" : "4"}. **Verify** \u2014 run tests/builds to confirm everything works.
1584
+ prompt += `${errors.length > 0 ? "5" : "4"}. **Verify** \u2014 rerun or extend the relevant commands/checks above to confirm everything works.
1332
1585
  `;
1586
+ }
1333
1587
  return prompt;
1334
1588
  }
1335
1589
  function buildRefinementDump(ctx, options = {}) {
@@ -1828,7 +2082,7 @@ async function promptForSource() {
1828
2082
  }
1829
2083
  async function main(argv = process.argv.slice(2)) {
1830
2084
  const options = parseArgs(argv);
1831
- const pkgInfo = { name: "ctx-switch", version: "2.0.4" };
2085
+ const pkgInfo = { name: "ctx-switch", version: "2.0.5" };
1832
2086
  const ui = createTheme(process.stderr);
1833
2087
  if (options.help) {
1834
2088
  process.stdout.write(`${getHelpText(pkgInfo)}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ctx-switch",
3
- "version": "2.0.4",
3
+ "version": "2.0.5",
4
4
  "description": "Switch coding agents without losing context. Generate handoff prompts across Claude Code, Codex, and OpenCode.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.mjs",