oh-my-codex 0.17.3 → 0.18.0

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 (158) hide show
  1. package/Cargo.lock +13 -5
  2. package/Cargo.toml +2 -1
  3. package/README.md +1 -0
  4. package/crates/omx-api/Cargo.toml +19 -0
  5. package/crates/omx-api/src/lib.rs +2940 -0
  6. package/crates/omx-api/src/main.rs +10 -0
  7. package/crates/omx-api/tests/cli.rs +558 -0
  8. package/crates/omx-explore/src/main.rs +4 -0
  9. package/crates/omx-sparkshell/src/codex_bridge.rs +437 -123
  10. package/crates/omx-sparkshell/src/exec.rs +4 -0
  11. package/crates/omx-sparkshell/src/main.rs +738 -29
  12. package/crates/omx-sparkshell/src/prompt.rs +25 -3
  13. package/crates/omx-sparkshell/src/redaction.rs +241 -0
  14. package/crates/omx-sparkshell/tests/execution.rs +479 -238
  15. package/dist/cli/__tests__/api.test.d.ts +2 -0
  16. package/dist/cli/__tests__/api.test.d.ts.map +1 -0
  17. package/dist/cli/__tests__/api.test.js +175 -0
  18. package/dist/cli/__tests__/api.test.js.map +1 -0
  19. package/dist/cli/__tests__/ask.test.js +72 -5
  20. package/dist/cli/__tests__/ask.test.js.map +1 -1
  21. package/dist/cli/__tests__/autoresearch-goal.test.js +14 -1
  22. package/dist/cli/__tests__/autoresearch-goal.test.js.map +1 -1
  23. package/dist/cli/__tests__/explore.test.js +23 -0
  24. package/dist/cli/__tests__/explore.test.js.map +1 -1
  25. package/dist/cli/__tests__/index.test.js +123 -5
  26. package/dist/cli/__tests__/index.test.js.map +1 -1
  27. package/dist/cli/__tests__/launch-fallback.test.js +76 -0
  28. package/dist/cli/__tests__/launch-fallback.test.js.map +1 -1
  29. package/dist/cli/__tests__/package-bin-contract.test.js +4 -3
  30. package/dist/cli/__tests__/package-bin-contract.test.js.map +1 -1
  31. package/dist/cli/__tests__/setup-install-mode.test.js +138 -0
  32. package/dist/cli/__tests__/setup-install-mode.test.js.map +1 -1
  33. package/dist/cli/__tests__/sparkshell-cli.test.js +5 -0
  34. package/dist/cli/__tests__/sparkshell-cli.test.js.map +1 -1
  35. package/dist/cli/__tests__/version-sync-contract.test.js +4 -0
  36. package/dist/cli/__tests__/version-sync-contract.test.js.map +1 -1
  37. package/dist/cli/__tests__/windows-popup-loop-contract.test.js +1 -1
  38. package/dist/cli/__tests__/windows-popup-loop-contract.test.js.map +1 -1
  39. package/dist/cli/api.d.ts +26 -0
  40. package/dist/cli/api.d.ts.map +1 -0
  41. package/dist/cli/api.js +153 -0
  42. package/dist/cli/api.js.map +1 -0
  43. package/dist/cli/explore.d.ts +2 -0
  44. package/dist/cli/explore.d.ts.map +1 -1
  45. package/dist/cli/explore.js +43 -1
  46. package/dist/cli/explore.js.map +1 -1
  47. package/dist/cli/index.d.ts +10 -4
  48. package/dist/cli/index.d.ts.map +1 -1
  49. package/dist/cli/index.js +128 -10
  50. package/dist/cli/index.js.map +1 -1
  51. package/dist/cli/native-assets.d.ts +2 -1
  52. package/dist/cli/native-assets.d.ts.map +1 -1
  53. package/dist/cli/native-assets.js +1 -0
  54. package/dist/cli/native-assets.js.map +1 -1
  55. package/dist/cli/sparkshell.d.ts.map +1 -1
  56. package/dist/cli/sparkshell.js +20 -3
  57. package/dist/cli/sparkshell.js.map +1 -1
  58. package/dist/config/generator.d.ts.map +1 -1
  59. package/dist/config/generator.js +90 -0
  60. package/dist/config/generator.js.map +1 -1
  61. package/dist/hooks/__tests__/best-practice-research-skill.test.d.ts +2 -0
  62. package/dist/hooks/__tests__/best-practice-research-skill.test.d.ts.map +1 -0
  63. package/dist/hooks/__tests__/best-practice-research-skill.test.js +27 -0
  64. package/dist/hooks/__tests__/best-practice-research-skill.test.js.map +1 -0
  65. package/dist/hooks/__tests__/keyword-detector.test.js +11 -0
  66. package/dist/hooks/__tests__/keyword-detector.test.js.map +1 -1
  67. package/dist/hooks/__tests__/notify-hook-team-leader-nudge.test.js +6 -0
  68. package/dist/hooks/__tests__/notify-hook-team-leader-nudge.test.js.map +1 -1
  69. package/dist/hooks/__tests__/prompt-guidance-wave-two.test.js +4 -0
  70. package/dist/hooks/__tests__/prompt-guidance-wave-two.test.js.map +1 -1
  71. package/dist/hooks/keyword-registry.d.ts.map +1 -1
  72. package/dist/hooks/keyword-registry.js +1 -0
  73. package/dist/hooks/keyword-registry.js.map +1 -1
  74. package/dist/hud/__tests__/reconcile.test.js +2 -2
  75. package/dist/hud/__tests__/reconcile.test.js.map +1 -1
  76. package/dist/hud/__tests__/tmux.test.js +23 -18
  77. package/dist/hud/__tests__/tmux.test.js.map +1 -1
  78. package/dist/hud/tmux.d.ts.map +1 -1
  79. package/dist/hud/tmux.js +7 -6
  80. package/dist/hud/tmux.js.map +1 -1
  81. package/dist/mcp/__tests__/bootstrap.test.js +75 -1
  82. package/dist/mcp/__tests__/bootstrap.test.js.map +1 -1
  83. package/dist/mcp/bootstrap.d.ts +3 -1
  84. package/dist/mcp/bootstrap.d.ts.map +1 -1
  85. package/dist/mcp/bootstrap.js +71 -2
  86. package/dist/mcp/bootstrap.js.map +1 -1
  87. package/dist/scripts/__tests__/codex-native-hook.test.js +737 -26
  88. package/dist/scripts/__tests__/codex-native-hook.test.js.map +1 -1
  89. package/dist/scripts/__tests__/notify-dispatcher.test.js +183 -1
  90. package/dist/scripts/__tests__/notify-dispatcher.test.js.map +1 -1
  91. package/dist/scripts/__tests__/smoke-packed-install.test.js +4 -1
  92. package/dist/scripts/__tests__/smoke-packed-install.test.js.map +1 -1
  93. package/dist/scripts/build-api.d.ts +2 -0
  94. package/dist/scripts/build-api.d.ts.map +1 -0
  95. package/dist/scripts/build-api.js +44 -0
  96. package/dist/scripts/build-api.js.map +1 -0
  97. package/dist/scripts/codex-native-hook.d.ts.map +1 -1
  98. package/dist/scripts/codex-native-hook.js +208 -8
  99. package/dist/scripts/codex-native-hook.js.map +1 -1
  100. package/dist/scripts/codex-native-pre-post.d.ts.map +1 -1
  101. package/dist/scripts/codex-native-pre-post.js +89 -24
  102. package/dist/scripts/codex-native-pre-post.js.map +1 -1
  103. package/dist/scripts/notify-dispatcher.js +88 -0
  104. package/dist/scripts/notify-dispatcher.js.map +1 -1
  105. package/dist/scripts/notify-hook/team-dispatch.d.ts.map +1 -1
  106. package/dist/scripts/notify-hook/team-dispatch.js +27 -9
  107. package/dist/scripts/notify-hook/team-dispatch.js.map +1 -1
  108. package/dist/scripts/notify-hook/team-leader-nudge.d.ts.map +1 -1
  109. package/dist/scripts/notify-hook/team-leader-nudge.js +26 -11
  110. package/dist/scripts/notify-hook/team-leader-nudge.js.map +1 -1
  111. package/dist/scripts/notify-hook/team-tmux-guard.d.ts +1 -0
  112. package/dist/scripts/notify-hook/team-tmux-guard.d.ts.map +1 -1
  113. package/dist/scripts/notify-hook/team-tmux-guard.js +38 -0
  114. package/dist/scripts/notify-hook/team-tmux-guard.js.map +1 -1
  115. package/dist/scripts/notify-hook/team-worker-stop.d.ts.map +1 -1
  116. package/dist/scripts/notify-hook/team-worker-stop.js +27 -14
  117. package/dist/scripts/notify-hook/team-worker-stop.js.map +1 -1
  118. package/dist/scripts/run-provider-advisor.js +9 -3
  119. package/dist/scripts/run-provider-advisor.js.map +1 -1
  120. package/dist/scripts/smoke-packed-install.d.ts +1 -1
  121. package/dist/scripts/smoke-packed-install.d.ts.map +1 -1
  122. package/dist/scripts/smoke-packed-install.js +2 -0
  123. package/dist/scripts/smoke-packed-install.js.map +1 -1
  124. package/dist/team/__tests__/runtime.test.js +2 -2
  125. package/dist/team/__tests__/runtime.test.js.map +1 -1
  126. package/dist/team/__tests__/tmux-session.test.js +96 -19
  127. package/dist/team/__tests__/tmux-session.test.js.map +1 -1
  128. package/dist/team/tmux-session.d.ts +1 -0
  129. package/dist/team/tmux-session.d.ts.map +1 -1
  130. package/dist/team/tmux-session.js +34 -10
  131. package/dist/team/tmux-session.js.map +1 -1
  132. package/dist/verification/__tests__/ci-rust-gates.test.js +85 -10
  133. package/dist/verification/__tests__/ci-rust-gates.test.js.map +1 -1
  134. package/dist/verification/__tests__/explore-harness-release-workflow.test.js +1 -0
  135. package/dist/verification/__tests__/explore-harness-release-workflow.test.js.map +1 -1
  136. package/package.json +4 -3
  137. package/plugins/oh-my-codex/.codex-plugin/plugin.json +1 -1
  138. package/plugins/oh-my-codex/skills/best-practice-research/SKILL.md +83 -0
  139. package/plugins/oh-my-codex/skills/deep-interview/SKILL.md +1 -0
  140. package/plugins/oh-my-codex/skills/ralplan/SKILL.md +1 -1
  141. package/prompts/researcher.md +15 -10
  142. package/skills/best-practice-research/SKILL.md +83 -0
  143. package/skills/deep-interview/SKILL.md +1 -0
  144. package/skills/ralplan/SKILL.md +1 -1
  145. package/src/scripts/__tests__/codex-native-hook.test.ts +810 -4
  146. package/src/scripts/__tests__/notify-dispatcher.test.ts +223 -1
  147. package/src/scripts/__tests__/smoke-packed-install.test.ts +8 -2
  148. package/src/scripts/build-api.ts +48 -0
  149. package/src/scripts/codex-native-hook.ts +262 -10
  150. package/src/scripts/codex-native-pre-post.ts +103 -24
  151. package/src/scripts/notify-dispatcher.ts +97 -0
  152. package/src/scripts/notify-hook/team-dispatch.ts +27 -8
  153. package/src/scripts/notify-hook/team-leader-nudge.ts +25 -11
  154. package/src/scripts/notify-hook/team-tmux-guard.ts +42 -0
  155. package/src/scripts/notify-hook/team-worker-stop.ts +24 -13
  156. package/src/scripts/run-provider-advisor.ts +11 -3
  157. package/src/scripts/smoke-packed-install.ts +2 -0
  158. package/templates/catalog-manifest.json +7 -0
package/dist/cli/index.js CHANGED
@@ -24,6 +24,7 @@ import { stateCommand } from "./state.js";
24
24
  import { cleanupCommand, cleanupOmxMcpProcesses, findLaunchSafeCleanupCandidates, } from "./cleanup.js";
25
25
  import { exploreCommand } from "./explore.js";
26
26
  import { sparkshellCommand } from "./sparkshell.js";
27
+ import { apiCommand } from "./api.js";
27
28
  import { agentsInitCommand } from "./agents-init.js";
28
29
  import { agentsCommand } from "./agents.js";
29
30
  import { sessionCommand } from "./session-search.js";
@@ -51,7 +52,7 @@ import { cleanCodexModelAvailabilityNuxIfNeeded, extractSharedMcpRegistryServers
51
52
  import { OMX_FIRST_PARTY_MCP_SERVER_NAMES } from "../config/omx-first-party-mcp.js";
52
53
  import { HUD_TMUX_HEIGHT_LINES } from "../hud/constants.js";
53
54
  import { OMX_TMUX_HUD_OWNER_ENV } from "../hud/reconcile.js";
54
- import { createHudWatchPane as createSharedHudWatchPane, killTmuxPane as killSharedTmuxPane, listCurrentWindowHudPaneIds, parsePaneIdFromTmuxOutput, } from "../hud/tmux.js";
55
+ import { createHudWatchPane as createSharedHudWatchPane, killTmuxPane as killSharedTmuxPane, listCurrentWindowHudPaneIds, parsePaneIdFromTmuxOutput, registerHudResizeHook, } from "../hud/tmux.js";
55
56
  export { parseTmuxPaneSnapshot, isHudWatchPane, findHudWatchPaneIds } from "../hud/tmux.js";
56
57
  rememberOmxLaunchContext();
57
58
  import { classifySpawnError, resolveTmuxBinaryForPlatform, spawnPlatformCommandSync, } from "../utils/platform-command.js";
@@ -98,6 +99,7 @@ Usage:
98
99
  omx adapt Scaffold OMX-owned adapter foundations for persistent external targets
99
100
  omx resume Resume a previous interactive Codex session
100
101
  omx explore Default read-only exploration entrypoint (may adaptively use sparkshell backend)
102
+ omx api Run native omx-api localhost gateway commands (serve|status|stop|generate)
101
103
  omx session Search prior local session transcripts and history artifacts
102
104
  omx agents-init [path]
103
105
  Bootstrap lightweight AGENTS.md files for a repo/subtree
@@ -250,6 +252,7 @@ const NESTED_HELP_COMMANDS = new Set([
250
252
  "performance-goal",
251
253
  "resume",
252
254
  "session",
255
+ "api",
253
256
  "sparkshell",
254
257
  "team",
255
258
  "tmux-hook",
@@ -843,6 +846,7 @@ export async function main(args) {
843
846
  "autoresearch",
844
847
  "autoresearch-goal",
845
848
  "explore",
849
+ "api",
846
850
  "sparkshell",
847
851
  "team",
848
852
  "ralph",
@@ -947,6 +951,9 @@ export async function main(args) {
947
951
  case "explore":
948
952
  await exploreCommand(args.slice(1));
949
953
  break;
954
+ case "api":
955
+ await apiCommand(args.slice(1));
956
+ break;
950
957
  case "exec":
951
958
  if (launchArgs[0] === "inject") {
952
959
  await execInjectCommand(launchArgs);
@@ -1833,10 +1840,16 @@ function withTmuxExtendedKeysLeaseLock(cwd, socketPath, run) {
1833
1840
  }
1834
1841
  throw new Error(`timed out waiting for tmux extended-keys lease lock: ${lockPath}`);
1835
1842
  }
1836
- function buildDetachedSessionLeaderCommand(cwd, sessionName, codexCmd, sessionId, codexHomeOverride, projectLocalCodexHomeForCleanup, runtimeCodexHomeForCleanup) {
1843
+ function buildDetachedSessionLeaderCommand(cwd, sessionName, codexCmd, sessionId, codexHomeOverride, projectLocalCodexHomeForCleanup, runtimeCodexHomeForCleanup, parentEnvFilePath) {
1837
1844
  const detachedPostLaunchHelper = sessionId
1838
1845
  ? `${buildDetachedSessionPostLaunchHelperCommand(cwd, sessionId, codexHomeOverride, projectLocalCodexHomeForCleanup, runtimeCodexHomeForCleanup)} >/dev/null 2>&1 || true;`
1839
1846
  : "";
1847
+ const parentEnvSource = parentEnvFilePath && parentEnvFilePath.trim()
1848
+ ? `if [ -r ${quoteShellArg(parentEnvFilePath)} ]; then . ${quoteShellArg(parentEnvFilePath)}; rm -f ${quoteShellArg(parentEnvFilePath)}; fi;`
1849
+ : "";
1850
+ const parentEnvCleanup = parentEnvFilePath && parentEnvFilePath.trim()
1851
+ ? `rm -f ${quoteShellArg(parentEnvFilePath)} 2>/dev/null || true;`
1852
+ : "";
1840
1853
  const wrapped = [
1841
1854
  buildTmuxExtendedKeysAcquireShellSnippet(cwd),
1842
1855
  'exec 3<&0;',
@@ -1850,6 +1863,7 @@ function buildDetachedSessionLeaderCommand(cwd, sessionName, codexCmd, sessionId
1850
1863
  "fi;",
1851
1864
  'exec 3<&- 2>/dev/null || true;',
1852
1865
  buildTmuxExtendedKeysReleaseShellSnippet(cwd),
1866
+ parentEnvCleanup,
1853
1867
  detachedPostLaunchHelper,
1854
1868
  'if [ "$status" -eq 0 ]; then',
1855
1869
  `tmux kill-session -t "${escapeShellDoubleQuotedValue(sessionName)}" >/dev/null 2>&1 || true;`,
@@ -1857,6 +1871,7 @@ function buildDetachedSessionLeaderCommand(cwd, sessionName, codexCmd, sessionId
1857
1871
  "exit $status;",
1858
1872
  "};",
1859
1873
  "trap omx_detached_session_cleanup 0 INT TERM HUP;",
1874
+ parentEnvSource,
1860
1875
  "omx_codex_started_at=$(date +%s 2>/dev/null || printf 0);",
1861
1876
  `${codexCmd} <&3 &`,
1862
1877
  "omx_codex_pid=$!;",
@@ -1997,6 +2012,34 @@ function buildTmuxExtendedKeysAcquireShellSnippet(cwd) {
1997
2012
  function buildTmuxExtendedKeysReleaseShellSnippet(cwd) {
1998
2013
  return `if [ -n "\${OMX_TMUX_EXTENDED_KEYS_LEASE:-}" ]; then ${buildTmuxExtendedKeysHelperCommand(cwd, "release")} "\${OMX_TMUX_EXTENDED_KEYS_LEASE}" >/dev/null 2>&1 || true; fi;`;
1999
2014
  }
2015
+ const SHELL_ENV_NAME_PATTERN = /^[A-Za-z_][A-Za-z0-9_]*$/;
2016
+ export function serializeDetachedSessionParentEnv(env) {
2017
+ const lines = [];
2018
+ for (const key of Object.keys(env).sort()) {
2019
+ if (!SHELL_ENV_NAME_PATTERN.test(key))
2020
+ continue;
2021
+ const value = env[key];
2022
+ if (typeof value !== "string")
2023
+ continue;
2024
+ if (value.includes("\0"))
2025
+ continue;
2026
+ lines.push(`export ${key}=${quoteShellArg(value)}`);
2027
+ }
2028
+ return `${lines.join("\n")}\n`;
2029
+ }
2030
+ export function detachedSessionParentEnvFilePath(cwd, sessionId) {
2031
+ const safeSessionId = sessionId.replace(/[^A-Za-z0-9_.-]/g, "_");
2032
+ return join(omxRoot(cwd), "runtime", "tmux-env", `${safeSessionId}.env`);
2033
+ }
2034
+ export function writeDetachedSessionParentEnvFile(cwd, sessionId, env) {
2035
+ const filePath = detachedSessionParentEnvFilePath(cwd, sessionId);
2036
+ mkdirSync(dirname(filePath), { recursive: true, mode: 0o700 });
2037
+ writeFileSync(filePath, serializeDetachedSessionParentEnv(env), {
2038
+ encoding: "utf-8",
2039
+ mode: 0o600,
2040
+ });
2041
+ return filePath;
2042
+ }
2000
2043
  export function withTmuxExtendedKeys(cwd, run, execFileSyncImpl = (file, tmuxArgs) => execFileSync(file, tmuxArgs, {
2001
2044
  encoding: "utf-8",
2002
2045
  ...(process.platform === "win32" ? { windowsHide: true } : {}),
@@ -2010,10 +2053,10 @@ export function withTmuxExtendedKeys(cwd, run, execFileSyncImpl = (file, tmuxArg
2010
2053
  releaseTmuxExtendedKeysLease(cwd, leaseHandle, execFileSyncImpl);
2011
2054
  }
2012
2055
  }
2013
- export function buildDetachedSessionBootstrapSteps(sessionName, cwd, codexCmd, hudCmd, workerLaunchArgs, codexHomeOverride, notifyTempContractRaw, nativeWindows = false, sessionId, projectLocalCodexHomeForCleanup, runtimeCodexHomeForCleanup, omxRootOverride, env = process.env, sqliteHomeOverride) {
2056
+ export function buildDetachedSessionBootstrapSteps(sessionName, cwd, codexCmd, hudCmd, workerLaunchArgs, codexHomeOverride, notifyTempContractRaw, nativeWindows = false, sessionId, projectLocalCodexHomeForCleanup, runtimeCodexHomeForCleanup, omxRootOverride, env = process.env, sqliteHomeOverride, parentEnvFilePath) {
2014
2057
  const detachedLeaderCmd = nativeWindows
2015
2058
  ? "powershell.exe"
2016
- : buildDetachedSessionLeaderCommand(cwd, sessionName, codexCmd, sessionId, codexHomeOverride, projectLocalCodexHomeForCleanup, runtimeCodexHomeForCleanup);
2059
+ : buildDetachedSessionLeaderCommand(cwd, sessionName, codexCmd, sessionId, codexHomeOverride, projectLocalCodexHomeForCleanup, runtimeCodexHomeForCleanup, parentEnvFilePath);
2017
2060
  const newSessionArgs = [
2018
2061
  "new-session",
2019
2062
  "-d",
@@ -2249,6 +2292,26 @@ async function readPostLaunchModeStateFile(path, dependencies = {}) {
2249
2292
  function cleanPostLaunchString(value) {
2250
2293
  return typeof value === "string" ? value.trim() : "";
2251
2294
  }
2295
+ function isAutopilotReviewPendingPostLaunchState(state) {
2296
+ if (!state || state.active !== true)
2297
+ return false;
2298
+ const mode = cleanPostLaunchString(state.mode).toLowerCase();
2299
+ if (mode && mode !== "autopilot")
2300
+ return false;
2301
+ const phase = cleanPostLaunchString(state.current_phase ?? state.currentPhase)
2302
+ .toLowerCase()
2303
+ .replace(/_/g, "-");
2304
+ if (phase === "code-review" || phase === "review" || phase === "reviewing" || phase === "review-pending") {
2305
+ return true;
2306
+ }
2307
+ const nestedState = state.state && typeof state.state === "object"
2308
+ ? state.state
2309
+ : {};
2310
+ return state.review_pending === true
2311
+ || state.reviewPending === true
2312
+ || nestedState.review_pending === true
2313
+ || nestedState.reviewPending === true;
2314
+ }
2252
2315
  function postLaunchUniqueStrings(values) {
2253
2316
  return [...new Set(values.map((value) => value.trim()).filter(Boolean))];
2254
2317
  }
@@ -2336,8 +2399,16 @@ export async function cleanupPostLaunchModeStateFiles(cwd, sessionId, dependenci
2336
2399
  const rootSkillActiveStateBeforeCleanup = sessionId
2337
2400
  ? await readSkillActiveState(getSkillActiveStatePathsForStateDir(rootStateDir).rootPath)
2338
2401
  : null;
2402
+ let preserveSkillActiveForReviewPendingAutopilot = false;
2339
2403
  for (const stateDir of scopedDirs) {
2340
2404
  const files = await readdir(stateDir).catch(() => []);
2405
+ const autopilotPath = join(stateDir, "autopilot-state.json");
2406
+ const autopilotPrecheck = files.includes("autopilot-state.json")
2407
+ ? await readPostLaunchModeStateFile(autopilotPath, dependencies)
2408
+ : null;
2409
+ const preserveReviewPendingAutopilot = autopilotPrecheck?.kind === "ok"
2410
+ && isAutopilotReviewPendingPostLaunchState(autopilotPrecheck.state);
2411
+ preserveSkillActiveForReviewPendingAutopilot ||= preserveReviewPendingAutopilot;
2341
2412
  for (const file of files) {
2342
2413
  if (!file.endsWith("-state.json") || file === "session.json")
2343
2414
  continue;
@@ -2395,6 +2466,10 @@ export async function cleanupPostLaunchModeStateFiles(cwd, sessionId, dependenci
2395
2466
  }
2396
2467
  continue;
2397
2468
  }
2469
+ if (preserveReviewPendingAutopilot
2470
+ && (mode === "autopilot" || mode === SKILL_ACTIVE_STATE_MODE)) {
2471
+ continue;
2472
+ }
2398
2473
  try {
2399
2474
  const completedAt = now().toISOString();
2400
2475
  if (mode === SKILL_ACTIVE_STATE_MODE) {
@@ -2433,7 +2508,9 @@ export async function cleanupPostLaunchModeStateFiles(cwd, sessionId, dependenci
2433
2508
  }
2434
2509
  if (sessionId) {
2435
2510
  try {
2436
- await scrubPostLaunchRootSkillActiveForSession(rootStateDir, sessionId, now().toISOString(), writeFile, rootSkillActiveStateBeforeCleanup);
2511
+ if (!preserveSkillActiveForReviewPendingAutopilot) {
2512
+ await scrubPostLaunchRootSkillActiveForSession(rootStateDir, sessionId, now().toISOString(), writeFile, rootSkillActiveStateBeforeCleanup);
2513
+ }
2437
2514
  }
2438
2515
  catch (err) {
2439
2516
  writeWarn(`[omx] postLaunch: failed to reconcile root skill-active state: ${err instanceof Error ? err.message : err}`);
@@ -2610,6 +2687,9 @@ function runCodex(cwd, args, sessionId, workerDefaultModel, codexHomeOverride, s
2610
2687
  let hudPaneId = null;
2611
2688
  try {
2612
2689
  hudPaneId = createHudWatchPane(cwd, hudCmd);
2690
+ if (hudPaneId && currentPaneId) {
2691
+ registerHudResizeHook(hudPaneId, currentPaneId, HUD_TMUX_HEIGHT_LINES);
2692
+ }
2613
2693
  }
2614
2694
  catch (err) {
2615
2695
  logCliOperationFailure(err);
@@ -2678,8 +2758,18 @@ function runCodex(cwd, args, sessionId, workerDefaultModel, codexHomeOverride, s
2678
2758
  let registeredHookTarget = null;
2679
2759
  let registeredHookName = null;
2680
2760
  let registeredClientAttachedHookName = null;
2761
+ let detachedParentEnvFilePath;
2681
2762
  try {
2682
- const bootstrapSteps = buildDetachedSessionBootstrapSteps(sessionName, cwd, codexCmd, hudCmd, workerLaunchArgs, codexHomeOverride, notifyTempContractRaw, nativeWindows, sessionId, projectLocalCodexHomeForCleanup, runtimeCodexHomeForCleanup, omxRootOverride, process.env, sqliteHomeOverride);
2763
+ // This path is the user-shell interactive launch: OMX creates a tmux
2764
+ // session and immediately attaches the user's terminal to it. If a tmux
2765
+ // server already exists, `new-session -e` only forwards explicit values,
2766
+ // so provider-specific parent-shell keys would disappear. Source a
2767
+ // private env file inside the leader shell instead of putting every
2768
+ // parent env value on the tmux command line or in logs.
2769
+ if (!nativeWindows) {
2770
+ detachedParentEnvFilePath = writeDetachedSessionParentEnvFile(cwd, sessionId, codexEnvWithNotify);
2771
+ }
2772
+ const bootstrapSteps = buildDetachedSessionBootstrapSteps(sessionName, cwd, codexCmd, hudCmd, workerLaunchArgs, codexHomeOverride, notifyTempContractRaw, nativeWindows, sessionId, projectLocalCodexHomeForCleanup, runtimeCodexHomeForCleanup, omxRootOverride, process.env, sqliteHomeOverride, detachedParentEnvFilePath);
2683
2773
  for (const step of bootstrapSteps) {
2684
2774
  const output = execTmuxFileSync(step.args, {
2685
2775
  stdio: "pipe",
@@ -2748,6 +2838,9 @@ function runCodex(cwd, args, sessionId, workerDefaultModel, codexHomeOverride, s
2748
2838
  }
2749
2839
  catch (err) {
2750
2840
  logCliOperationFailure(err);
2841
+ if (detachedParentEnvFilePath) {
2842
+ rmSync(detachedParentEnvFilePath, { force: true });
2843
+ }
2751
2844
  if (createdDetachedSession) {
2752
2845
  const rollbackSteps = buildDetachedSessionRollbackSteps(sessionName, registeredHookTarget, registeredHookName, registeredClientAttachedHookName);
2753
2846
  for (const rollbackStep of rollbackSteps) {
@@ -3117,6 +3210,22 @@ function parseWatcherPidFile(content) {
3117
3210
  return Number.isFinite(pid) && pid > 0 ? pid : null;
3118
3211
  }
3119
3212
  }
3213
+ const DEFAULT_NOTIFY_FALLBACK_REAP_GRACE_MS = 5000;
3214
+ function resolveNotifyFallbackReapGraceMs(env = process.env) {
3215
+ const parsed = Number.parseInt(env.OMX_NOTIFY_FALLBACK_REAP_GRACE_MS || "", 10);
3216
+ if (Number.isFinite(parsed) && parsed >= 0)
3217
+ return parsed;
3218
+ return DEFAULT_NOTIFY_FALLBACK_REAP_GRACE_MS;
3219
+ }
3220
+ function isWatcherRecordWithinReapGrace(record, nowMs = Date.now(), graceMs = resolveNotifyFallbackReapGraceMs()) {
3221
+ if (graceMs <= 0 || !record.startedAt)
3222
+ return false;
3223
+ const startedMs = Date.parse(record.startedAt);
3224
+ if (!Number.isFinite(startedMs))
3225
+ return false;
3226
+ const ageMs = nowMs - startedMs;
3227
+ return ageMs >= 0 && ageMs < graceMs;
3228
+ }
3120
3229
  function parseWatcherPidRecord(content) {
3121
3230
  const trimmed = content.trim();
3122
3231
  if (!trimmed)
@@ -3159,7 +3268,7 @@ function isLikelyOmxWatcherProcess(pid, execFileSyncFn = execFileSync, platform
3159
3268
  export async function reapStaleNotifyFallbackWatcher(pidPath, deps = {}) {
3160
3269
  const exists = deps.exists ?? existsSync;
3161
3270
  if (!exists(pidPath))
3162
- return;
3271
+ return "missing";
3163
3272
  const { readFile } = await import("fs/promises");
3164
3273
  const readFileImpl = deps.readFile ?? readFile;
3165
3274
  const tryKillPidImpl = deps.tryKillPid ?? tryKillPid;
@@ -3168,9 +3277,15 @@ export async function reapStaleNotifyFallbackWatcher(pidPath, deps = {}) {
3168
3277
  const isWatcherProcessImpl = deps.isWatcherProcess ?? isLikelyOmxWatcherProcess;
3169
3278
  try {
3170
3279
  const record = parseWatcherPidRecord(await readFileImpl(pidPath, "utf-8"));
3171
- if (record && isWatcherProcessImpl(record.pid)) {
3172
- tryKillPidImpl(record.pid, "SIGTERM");
3280
+ if (!record)
3281
+ return "invalid";
3282
+ if (!isWatcherProcessImpl(record.pid))
3283
+ return "identity_mismatch";
3284
+ if (isWatcherRecordWithinReapGrace(record, deps.nowMs?.() ?? Date.now(), deps.reapGraceMs ?? resolveNotifyFallbackReapGraceMs())) {
3285
+ return "recent_active";
3173
3286
  }
3287
+ tryKillPidImpl(record.pid, "SIGTERM");
3288
+ return "reaped";
3174
3289
  }
3175
3290
  catch (error) {
3176
3291
  if (!hasErrnoCodeImpl(error, "ESRCH")) {
@@ -3179,6 +3294,7 @@ export async function reapStaleNotifyFallbackWatcher(pidPath, deps = {}) {
3179
3294
  error: error instanceof Error ? error.message : String(error),
3180
3295
  });
3181
3296
  }
3297
+ return "failed";
3182
3298
  }
3183
3299
  }
3184
3300
  function tryKillPid(pid, signal = "SIGTERM") {
@@ -3196,7 +3312,9 @@ function tryKillPid(pid, signal = "SIGTERM") {
3196
3312
  async function startNotifyFallbackWatcher(cwd, options = {}) {
3197
3313
  const { mkdir, writeFile } = await import("fs/promises");
3198
3314
  const pidPath = notifyFallbackPidPath(cwd);
3199
- await reapStaleNotifyFallbackWatcher(pidPath);
3315
+ const reapResult = await reapStaleNotifyFallbackWatcher(pidPath);
3316
+ if (reapResult === "recent_active")
3317
+ return;
3200
3318
  if (!shouldEnableNotifyFallbackWatcher(process.env, process.platform))
3201
3319
  return;
3202
3320
  const pkgRoot = getPackageRoot();