oh-my-codex 0.16.1 → 0.16.3

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 (247) hide show
  1. package/Cargo.lock +5 -5
  2. package/Cargo.toml +1 -1
  3. package/README.md +2 -2
  4. package/dist/cli/__tests__/doctor-warning-copy.test.js +37 -1
  5. package/dist/cli/__tests__/doctor-warning-copy.test.js.map +1 -1
  6. package/dist/cli/__tests__/explore.test.js +2 -2
  7. package/dist/cli/__tests__/explore.test.js.map +1 -1
  8. package/dist/cli/__tests__/index.test.js +173 -3
  9. package/dist/cli/__tests__/index.test.js.map +1 -1
  10. package/dist/cli/__tests__/launch-fallback.test.js +58 -0
  11. package/dist/cli/__tests__/launch-fallback.test.js.map +1 -1
  12. package/dist/cli/__tests__/ralph.test.js +1 -0
  13. package/dist/cli/__tests__/ralph.test.js.map +1 -1
  14. package/dist/cli/__tests__/setup-hooks-shared-ownership.test.js +8 -0
  15. package/dist/cli/__tests__/setup-hooks-shared-ownership.test.js.map +1 -1
  16. package/dist/cli/__tests__/setup-install-mode.test.js +82 -2
  17. package/dist/cli/__tests__/setup-install-mode.test.js.map +1 -1
  18. package/dist/cli/__tests__/setup-scope.test.js +12 -0
  19. package/dist/cli/__tests__/setup-scope.test.js.map +1 -1
  20. package/dist/cli/__tests__/team.test.js +161 -0
  21. package/dist/cli/__tests__/team.test.js.map +1 -1
  22. package/dist/cli/__tests__/ultragoal.test.js +14 -9
  23. package/dist/cli/__tests__/ultragoal.test.js.map +1 -1
  24. package/dist/cli/__tests__/uninstall.test.js +44 -2
  25. package/dist/cli/__tests__/uninstall.test.js.map +1 -1
  26. package/dist/cli/__tests__/update.test.js +109 -19
  27. package/dist/cli/__tests__/update.test.js.map +1 -1
  28. package/dist/cli/doctor.d.ts.map +1 -1
  29. package/dist/cli/doctor.js +17 -0
  30. package/dist/cli/doctor.js.map +1 -1
  31. package/dist/cli/explore.d.ts.map +1 -1
  32. package/dist/cli/explore.js +3 -4
  33. package/dist/cli/explore.js.map +1 -1
  34. package/dist/cli/index.d.ts.map +1 -1
  35. package/dist/cli/index.js +34 -4
  36. package/dist/cli/index.js.map +1 -1
  37. package/dist/cli/setup.d.ts.map +1 -1
  38. package/dist/cli/setup.js +72 -6
  39. package/dist/cli/setup.js.map +1 -1
  40. package/dist/cli/team.d.ts.map +1 -1
  41. package/dist/cli/team.js +54 -15
  42. package/dist/cli/team.js.map +1 -1
  43. package/dist/cli/ultragoal.d.ts +1 -1
  44. package/dist/cli/ultragoal.d.ts.map +1 -1
  45. package/dist/cli/ultragoal.js +26 -8
  46. package/dist/cli/ultragoal.js.map +1 -1
  47. package/dist/cli/uninstall.d.ts.map +1 -1
  48. package/dist/cli/uninstall.js +68 -5
  49. package/dist/cli/uninstall.js.map +1 -1
  50. package/dist/cli/update.d.ts +10 -2
  51. package/dist/cli/update.d.ts.map +1 -1
  52. package/dist/cli/update.js +99 -5
  53. package/dist/cli/update.js.map +1 -1
  54. package/dist/config/__tests__/codex-hooks.test.js +269 -2
  55. package/dist/config/__tests__/codex-hooks.test.js.map +1 -1
  56. package/dist/config/__tests__/generator-idempotent.test.js +60 -2
  57. package/dist/config/__tests__/generator-idempotent.test.js.map +1 -1
  58. package/dist/config/__tests__/generator-notify.test.js +59 -1
  59. package/dist/config/__tests__/generator-notify.test.js.map +1 -1
  60. package/dist/config/__tests__/wiki-config-contract.test.js +2 -1
  61. package/dist/config/__tests__/wiki-config-contract.test.js.map +1 -1
  62. package/dist/config/codex-hooks.d.ts +52 -4
  63. package/dist/config/codex-hooks.d.ts.map +1 -1
  64. package/dist/config/codex-hooks.js +268 -7
  65. package/dist/config/codex-hooks.js.map +1 -1
  66. package/dist/config/generator.d.ts +13 -1
  67. package/dist/config/generator.d.ts.map +1 -1
  68. package/dist/config/generator.js +207 -12
  69. package/dist/config/generator.js.map +1 -1
  70. package/dist/hooks/__tests__/keyword-detector.test.js +37 -25
  71. package/dist/hooks/__tests__/keyword-detector.test.js.map +1 -1
  72. package/dist/hooks/__tests__/notify-fallback-watcher.test.js +29 -1
  73. package/dist/hooks/__tests__/notify-fallback-watcher.test.js.map +1 -1
  74. package/dist/hooks/__tests__/notify-hook-auto-nudge.test.js +20 -4
  75. package/dist/hooks/__tests__/notify-hook-auto-nudge.test.js.map +1 -1
  76. package/dist/hooks/__tests__/notify-hook-cross-worktree-heartbeat.test.js +1 -0
  77. package/dist/hooks/__tests__/notify-hook-cross-worktree-heartbeat.test.js.map +1 -1
  78. package/dist/hooks/__tests__/notify-hook-non-omx-guard.test.d.ts +2 -0
  79. package/dist/hooks/__tests__/notify-hook-non-omx-guard.test.d.ts.map +1 -0
  80. package/dist/hooks/__tests__/notify-hook-non-omx-guard.test.js +52 -0
  81. package/dist/hooks/__tests__/notify-hook-non-omx-guard.test.js.map +1 -0
  82. package/dist/hooks/__tests__/notify-hook-ralph-resume.test.js +148 -0
  83. package/dist/hooks/__tests__/notify-hook-ralph-resume.test.js.map +1 -1
  84. package/dist/hooks/__tests__/notify-hook-session-scope.test.js +3 -0
  85. package/dist/hooks/__tests__/notify-hook-session-scope.test.js.map +1 -1
  86. package/dist/hooks/__tests__/wiki-docs-contract.test.js +5 -4
  87. package/dist/hooks/__tests__/wiki-docs-contract.test.js.map +1 -1
  88. package/dist/hooks/keyword-detector.d.ts.map +1 -1
  89. package/dist/hooks/keyword-detector.js +2 -4
  90. package/dist/hooks/keyword-detector.js.map +1 -1
  91. package/dist/mcp/__tests__/state-server.test.js +145 -6
  92. package/dist/mcp/__tests__/state-server.test.js.map +1 -1
  93. package/dist/mcp/__tests__/wiki-server.test.js +97 -1
  94. package/dist/mcp/__tests__/wiki-server.test.js.map +1 -1
  95. package/dist/mcp/wiki-server.d.ts.map +1 -1
  96. package/dist/mcp/wiki-server.js +11 -2
  97. package/dist/mcp/wiki-server.js.map +1 -1
  98. package/dist/planning/__tests__/artifacts.test.js +64 -0
  99. package/dist/planning/__tests__/artifacts.test.js.map +1 -1
  100. package/dist/planning/__tests__/ready-context-pack-role-refs.test.d.ts +2 -0
  101. package/dist/planning/__tests__/ready-context-pack-role-refs.test.d.ts.map +1 -0
  102. package/dist/planning/__tests__/ready-context-pack-role-refs.test.js +90 -0
  103. package/dist/planning/__tests__/ready-context-pack-role-refs.test.js.map +1 -0
  104. package/dist/planning/artifacts.d.ts +7 -2
  105. package/dist/planning/artifacts.d.ts.map +1 -1
  106. package/dist/planning/artifacts.js +62 -8
  107. package/dist/planning/artifacts.js.map +1 -1
  108. package/dist/planning/context-pack-status.d.ts +6 -0
  109. package/dist/planning/context-pack-status.d.ts.map +1 -1
  110. package/dist/planning/context-pack-status.js +25 -0
  111. package/dist/planning/context-pack-status.js.map +1 -1
  112. package/dist/ralph/persistence.d.ts +1 -1
  113. package/dist/ralph/persistence.d.ts.map +1 -1
  114. package/dist/ralph/persistence.js +8 -2
  115. package/dist/ralph/persistence.js.map +1 -1
  116. package/dist/scripts/__tests__/codex-native-hook.test.js +190 -1
  117. package/dist/scripts/__tests__/codex-native-hook.test.js.map +1 -1
  118. package/dist/scripts/codex-execution-surface.d.ts +1 -1
  119. package/dist/scripts/codex-execution-surface.d.ts.map +1 -1
  120. package/dist/scripts/codex-execution-surface.js.map +1 -1
  121. package/dist/scripts/codex-native-hook.d.ts +1 -1
  122. package/dist/scripts/codex-native-hook.d.ts.map +1 -1
  123. package/dist/scripts/codex-native-hook.js +18 -3
  124. package/dist/scripts/codex-native-hook.js.map +1 -1
  125. package/dist/scripts/notify-dispatcher.d.ts +7 -0
  126. package/dist/scripts/notify-dispatcher.d.ts.map +1 -0
  127. package/dist/scripts/notify-dispatcher.js +58 -0
  128. package/dist/scripts/notify-dispatcher.js.map +1 -0
  129. package/dist/scripts/notify-fallback-watcher.js +4 -0
  130. package/dist/scripts/notify-fallback-watcher.js.map +1 -1
  131. package/dist/scripts/notify-hook/ralph-session-resume.d.ts.map +1 -1
  132. package/dist/scripts/notify-hook/ralph-session-resume.js +96 -8
  133. package/dist/scripts/notify-hook/ralph-session-resume.js.map +1 -1
  134. package/dist/scripts/notify-hook/state-io.d.ts.map +1 -1
  135. package/dist/scripts/notify-hook/state-io.js +6 -2
  136. package/dist/scripts/notify-hook/state-io.js.map +1 -1
  137. package/dist/scripts/notify-hook/visual-verdict.js +3 -3
  138. package/dist/scripts/notify-hook/visual-verdict.js.map +1 -1
  139. package/dist/scripts/notify-hook.js +124 -0
  140. package/dist/scripts/notify-hook.js.map +1 -1
  141. package/dist/state/__tests__/operations.test.js +79 -0
  142. package/dist/state/__tests__/operations.test.js.map +1 -1
  143. package/dist/state/__tests__/skill-active.test.js +10 -18
  144. package/dist/state/__tests__/skill-active.test.js.map +1 -1
  145. package/dist/state/operations.d.ts.map +1 -1
  146. package/dist/state/operations.js +1 -20
  147. package/dist/state/operations.js.map +1 -1
  148. package/dist/state/skill-active.d.ts +1 -0
  149. package/dist/state/skill-active.d.ts.map +1 -1
  150. package/dist/state/skill-active.js +28 -18
  151. package/dist/state/skill-active.js.map +1 -1
  152. package/dist/state/workflow-transition-reconcile.d.ts.map +1 -1
  153. package/dist/state/workflow-transition-reconcile.js +1 -0
  154. package/dist/state/workflow-transition-reconcile.js.map +1 -1
  155. package/dist/team/__tests__/approved-execution.test.js +45 -1
  156. package/dist/team/__tests__/approved-execution.test.js.map +1 -1
  157. package/dist/team/__tests__/runtime.test.js +173 -19
  158. package/dist/team/__tests__/runtime.test.js.map +1 -1
  159. package/dist/team/__tests__/worker-bootstrap.test.js +37 -0
  160. package/dist/team/__tests__/worker-bootstrap.test.js.map +1 -1
  161. package/dist/team/approved-execution.d.ts +1 -0
  162. package/dist/team/approved-execution.d.ts.map +1 -1
  163. package/dist/team/approved-execution.js +50 -0
  164. package/dist/team/approved-execution.js.map +1 -1
  165. package/dist/team/delivery-log.d.ts.map +1 -1
  166. package/dist/team/delivery-log.js +8 -1
  167. package/dist/team/delivery-log.js.map +1 -1
  168. package/dist/team/runtime.d.ts.map +1 -1
  169. package/dist/team/runtime.js +104 -18
  170. package/dist/team/runtime.js.map +1 -1
  171. package/dist/team/state/mailbox.d.ts +1 -0
  172. package/dist/team/state/mailbox.d.ts.map +1 -1
  173. package/dist/team/state/mailbox.js +10 -1
  174. package/dist/team/state/mailbox.js.map +1 -1
  175. package/dist/team/state-root.js +1 -1
  176. package/dist/team/state-root.js.map +1 -1
  177. package/dist/team/state.d.ts.map +1 -1
  178. package/dist/team/state.js +2 -2
  179. package/dist/team/state.js.map +1 -1
  180. package/dist/team/worker-bootstrap.d.ts +7 -2
  181. package/dist/team/worker-bootstrap.d.ts.map +1 -1
  182. package/dist/team/worker-bootstrap.js +17 -4
  183. package/dist/team/worker-bootstrap.js.map +1 -1
  184. package/dist/ultragoal/__tests__/artifacts.test.js +81 -7
  185. package/dist/ultragoal/__tests__/artifacts.test.js.map +1 -1
  186. package/dist/ultragoal/__tests__/docs-contract.test.js +8 -0
  187. package/dist/ultragoal/__tests__/docs-contract.test.js.map +1 -1
  188. package/dist/ultragoal/artifacts.d.ts +4 -0
  189. package/dist/ultragoal/artifacts.d.ts.map +1 -1
  190. package/dist/ultragoal/artifacts.js +72 -4
  191. package/dist/ultragoal/artifacts.js.map +1 -1
  192. package/dist/utils/paths.d.ts +3 -1
  193. package/dist/utils/paths.d.ts.map +1 -1
  194. package/dist/utils/paths.js +6 -2
  195. package/dist/utils/paths.js.map +1 -1
  196. package/dist/wiki/__tests__/ingest.test.js +35 -1
  197. package/dist/wiki/__tests__/ingest.test.js.map +1 -1
  198. package/dist/wiki/__tests__/lint.test.js +14 -1
  199. package/dist/wiki/__tests__/lint.test.js.map +1 -1
  200. package/dist/wiki/__tests__/query.test.js +28 -3
  201. package/dist/wiki/__tests__/query.test.js.map +1 -1
  202. package/dist/wiki/__tests__/session-hooks.test.js +30 -2
  203. package/dist/wiki/__tests__/session-hooks.test.js.map +1 -1
  204. package/dist/wiki/__tests__/storage.test.js +62 -22
  205. package/dist/wiki/__tests__/storage.test.js.map +1 -1
  206. package/dist/wiki/index.d.ts +2 -2
  207. package/dist/wiki/index.d.ts.map +1 -1
  208. package/dist/wiki/index.js +2 -2
  209. package/dist/wiki/index.js.map +1 -1
  210. package/dist/wiki/ingest.js +2 -2
  211. package/dist/wiki/ingest.js.map +1 -1
  212. package/dist/wiki/lifecycle.d.ts +5 -0
  213. package/dist/wiki/lifecycle.d.ts.map +1 -1
  214. package/dist/wiki/lifecycle.js +31 -4
  215. package/dist/wiki/lifecycle.js.map +1 -1
  216. package/dist/wiki/lint.d.ts.map +1 -1
  217. package/dist/wiki/lint.js +12 -8
  218. package/dist/wiki/lint.js.map +1 -1
  219. package/dist/wiki/query.d.ts.map +1 -1
  220. package/dist/wiki/query.js +3 -2
  221. package/dist/wiki/query.js.map +1 -1
  222. package/dist/wiki/storage.d.ts +4 -0
  223. package/dist/wiki/storage.d.ts.map +1 -1
  224. package/dist/wiki/storage.js +54 -18
  225. package/dist/wiki/storage.js.map +1 -1
  226. package/package.json +1 -1
  227. package/plugins/oh-my-codex/.codex-plugin/plugin.json +1 -1
  228. package/plugins/oh-my-codex/skills/deep-interview/SKILL.md +2 -2
  229. package/plugins/oh-my-codex/skills/plan/SKILL.md +4 -4
  230. package/plugins/oh-my-codex/skills/ralplan/SKILL.md +2 -2
  231. package/plugins/oh-my-codex/skills/ultragoal/SKILL.md +11 -7
  232. package/plugins/oh-my-codex/skills/wiki/SKILL.md +5 -5
  233. package/prompts/planner.md +1 -1
  234. package/skills/deep-interview/SKILL.md +2 -2
  235. package/skills/plan/SKILL.md +4 -4
  236. package/skills/ralplan/SKILL.md +2 -2
  237. package/skills/ultragoal/SKILL.md +11 -7
  238. package/skills/wiki/SKILL.md +5 -5
  239. package/src/scripts/__tests__/codex-native-hook.test.ts +218 -1
  240. package/src/scripts/codex-execution-surface.ts +2 -0
  241. package/src/scripts/codex-native-hook.ts +22 -3
  242. package/src/scripts/notify-dispatcher.ts +74 -0
  243. package/src/scripts/notify-fallback-watcher.ts +6 -2
  244. package/src/scripts/notify-hook/ralph-session-resume.ts +117 -8
  245. package/src/scripts/notify-hook/state-io.ts +4 -2
  246. package/src/scripts/notify-hook/visual-verdict.ts +3 -3
  247. package/src/scripts/notify-hook.ts +116 -0
@@ -1,7 +1,7 @@
1
1
  import { afterEach, describe, it, mock } from "node:test";
2
2
  import assert from "node:assert/strict";
3
3
  import { existsSync, mkdirSync, utimesSync } from "node:fs";
4
- import { chmod, mkdir, mkdtemp, readFile, rm, stat, writeFile } from "node:fs/promises";
4
+ import { chmod, lstat, mkdir, mkdtemp, readFile, rm, stat, writeFile } from "node:fs/promises";
5
5
  import { dirname, join } from "node:path";
6
6
  import { tmpdir } from "node:os";
7
7
  import { fileURLToPath } from "node:url";
@@ -450,6 +450,32 @@ describe("cleanupPostLaunchModeStateFiles", () => {
450
450
  }
451
451
  assert.deepEqual(warnings, []);
452
452
  });
453
+ it("marks active Ralph state cancelled with interrupted metadata during postLaunch cleanup", async () => {
454
+ const wd = await mkdtemp(join(tmpdir(), "omx-postlaunch-ralph-interrupted-"));
455
+ const sessionId = "sess-postlaunch-ralph-interrupted";
456
+ const sessionStateDir = join(wd, ".omx", "state", "sessions", sessionId);
457
+ await mkdir(sessionStateDir, { recursive: true });
458
+ await writeFile(join(sessionStateDir, "ralph-state.json"), JSON.stringify({
459
+ active: true,
460
+ mode: "ralph",
461
+ current_phase: "executing",
462
+ owner_omx_session_id: sessionId,
463
+ }, null, 2), "utf-8");
464
+ try {
465
+ await cleanupPostLaunchModeStateFiles(wd, sessionId, {
466
+ now: () => new Date("2026-05-09T08:00:00.000Z"),
467
+ });
468
+ const ralph = JSON.parse(await readFile(join(sessionStateDir, "ralph-state.json"), "utf-8"));
469
+ assert.equal(ralph.active, false);
470
+ assert.equal(ralph.current_phase, "cancelled");
471
+ assert.equal(ralph.completed_at, "2026-05-09T08:00:00.000Z");
472
+ assert.equal(ralph.interrupted_at, "2026-05-09T08:00:00.000Z");
473
+ assert.equal(ralph.stop_reason, "session_exit");
474
+ }
475
+ finally {
476
+ await rm(wd, { recursive: true, force: true });
477
+ }
478
+ });
453
479
  it("does not cancel root mode state during session-scoped postLaunch cleanup", async () => {
454
480
  const wd = await mkdtemp(join(tmpdir(), "omx-postlaunch-root-preserve-"));
455
481
  const sessionId = "sess-postlaunch-root-preserve";
@@ -1089,6 +1115,7 @@ describe("project launch scope helpers", () => {
1089
1115
  ].join("\n");
1090
1116
  await writeFile(configPath, originalConfig);
1091
1117
  await writeFile(join(projectCodexHome, "agents", "planner.toml"), 'name = "planner"\n');
1118
+ await writeFile(join(projectCodexHome, "hooks.json"), '{"hooks":{}}\n');
1092
1119
  await writeFile(join(projectCodexHome, "state_5.sqlite"), "state db placeholder");
1093
1120
  await writeFile(join(projectCodexHome, "state_5.sqlite-wal"), "state db wal placeholder");
1094
1121
  await writeFile(join(projectCodexHome, "logs_2.sqlite-shm"), "logs db shm placeholder");
@@ -1101,6 +1128,8 @@ describe("project launch scope helpers", () => {
1101
1128
  assert.equal(prepared.runtimeCodexHomeForCleanup, runtimeCodexHome);
1102
1129
  assert.equal(await readFile(join(runtimeCodexHome, "config.toml"), "utf-8"), originalConfig);
1103
1130
  assert.equal(await readFile(join(runtimeCodexHome, "agents", "planner.toml"), "utf-8"), 'name = "planner"\n');
1131
+ assert.equal(await readFile(join(runtimeCodexHome, "hooks.json"), "utf-8"), '{"hooks":{}}\n');
1132
+ assert.equal((await lstat(join(runtimeCodexHome, "hooks.json"))).isSymbolicLink(), false);
1104
1133
  assert.equal(existsSync(join(runtimeCodexHome, "state_5.sqlite")), false);
1105
1134
  assert.equal(existsSync(join(runtimeCodexHome, "state_5.sqlite-wal")), false);
1106
1135
  assert.equal(existsSync(join(runtimeCodexHome, "logs_2.sqlite-shm")), false);
@@ -1116,6 +1145,36 @@ describe("project launch scope helpers", () => {
1116
1145
  await rm(wd, { recursive: true, force: true });
1117
1146
  }
1118
1147
  });
1148
+ it("rewrites setup-owned hook trust state for the runtime CODEX_HOME mirror", async () => {
1149
+ const wd = await mkdtemp(join(tmpdir(), "omx-launch-runtime-hook-trust-"));
1150
+ try {
1151
+ const projectCodexHome = join(wd, ".codex");
1152
+ await mkdir(join(wd, ".omx"), { recursive: true });
1153
+ await mkdir(projectCodexHome, { recursive: true });
1154
+ await writeFile(join(wd, ".omx", "setup-scope.json"), JSON.stringify({ scope: "project" }));
1155
+ await writeFile(join(projectCodexHome, "hooks.json"), '{"hooks":{}}\n');
1156
+ await writeFile(join(projectCodexHome, "config.toml"), [
1157
+ "[features]",
1158
+ "codex_hooks = true",
1159
+ "",
1160
+ "# OMX-owned Codex hook trust state",
1161
+ "# Trusts only setup-managed codex-native-hook.js wrappers.",
1162
+ `[hooks.state."${join(projectCodexHome, "hooks.json")}:pre_tool_use:0:0"]`,
1163
+ 'trusted_hash = "stale"',
1164
+ "# End OMX-owned Codex hook trust state",
1165
+ "",
1166
+ ].join("\n"));
1167
+ await prepareCodexHomeForLaunch(wd, "session-trust", {});
1168
+ const runtimeCodexHome = runtimeCodexHomePath(wd, "session-trust");
1169
+ const runtimeConfig = await readFile(join(runtimeCodexHome, "config.toml"), "utf-8");
1170
+ assert.match(runtimeConfig, new RegExp(`\\[hooks\\.state\\."${join(runtimeCodexHome, "hooks.json").replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}:pre_tool_use:0:0"\\]`));
1171
+ assert.doesNotMatch(runtimeConfig, /trusted_hash = "stale"/);
1172
+ assert.doesNotMatch(runtimeConfig, new RegExp(join(projectCodexHome, "hooks.json").replace(/[.*+?^${}()|[\]\\]/g, "\\$&")));
1173
+ }
1174
+ finally {
1175
+ await rm(wd, { recursive: true, force: true });
1176
+ }
1177
+ });
1119
1178
  it("uses boxed runtime root for project-scope CODEX_HOME mirrors", async () => {
1120
1179
  const source = await mkdtemp(join(tmpdir(), "omx-launch-boxed-source-"));
1121
1180
  const boxedRoot = await mkdtemp(join(tmpdir(), "omx-launch-boxed-root-"));
@@ -1499,9 +1558,12 @@ describe("detached tmux new-session sequencing", () => {
1499
1558
  assert.match(leaderCmd, /wait "\$omx_codex_pid";/);
1500
1559
  assert.match(leaderCmd, /kill -TERM "\$omx_codex_pid"/);
1501
1560
  assert.match(leaderCmd, /releaseTmuxExtendedKeysLease/);
1502
- assert.match(leaderCmd, /if \[ "\$status" -lt 128 \]; then/);
1561
+ assert.match(leaderCmd, /if \[ "\$status" -eq 0 \]; then/);
1503
1562
  assert.match(leaderCmd, /tmux kill-session -t/);
1504
1563
  assert.match(leaderCmd, /"omx-demo"/);
1564
+ assert.match(leaderCmd, /codex exited immediately with code 0/);
1565
+ assert.match(leaderCmd, /codex exited with code/);
1566
+ assert.match(leaderCmd, /detached tmux session is being kept open/);
1505
1567
  assert.match(leaderCmd, /exit \$status/);
1506
1568
  });
1507
1569
  it("buildDetachedSessionBootstrapSteps finalizes postLaunch inside the detached leader when a session id is available", () => {
@@ -1514,7 +1576,7 @@ describe("detached tmux new-session sequencing", () => {
1514
1576
  assert.match(leaderCmd, /\/tmp\/project\/\.codex-project/);
1515
1577
  assert.match(leaderCmd, /\/tmp\/project\/\.omx\/runtime\/codex-home\/omx-session-123/);
1516
1578
  const helperIndex = leaderCmd.indexOf("runDetachedSessionPostLaunch");
1517
- const signalGateIndex = leaderCmd.indexOf('if [ "$status" -lt 128 ]');
1579
+ const signalGateIndex = leaderCmd.indexOf('if [ "$status" -eq 0 ]');
1518
1580
  assert.ok(helperIndex >= 0);
1519
1581
  assert.ok(signalGateIndex >= 0);
1520
1582
  assert.ok(helperIndex < signalGateIndex, "detached postLaunch helper must run before the signal-derived tmux kill-session gate");
@@ -1684,6 +1746,114 @@ exit 0
1684
1746
  await rm(cwd, { recursive: true, force: true });
1685
1747
  }
1686
1748
  });
1749
+ it("detached leader command keeps child startup errors visible instead of killing the session", async () => {
1750
+ const cwd = await mkdtemp(join(tmpdir(), "omx-detached-leader-error-"));
1751
+ const fakeBin = join(cwd, "bin");
1752
+ const logPath = join(cwd, "leader.log");
1753
+ try {
1754
+ await mkdir(fakeBin, { recursive: true });
1755
+ await writeFile(join(fakeBin, "codex"), `#!/bin/sh
1756
+ printf 'codex-stderr: unsupported startup flag\\n' >&2
1757
+ exit 42
1758
+ `);
1759
+ await chmod(join(fakeBin, "codex"), 0o755);
1760
+ await writeFile(join(fakeBin, "tmux"), `#!/bin/sh
1761
+ printf 'tmux:%s\\n' "$*" >> "${logPath}"
1762
+ case "$1" in
1763
+ display-message)
1764
+ if [ "$3" = '#{socket_path}' ] || [ "$4" = '#{socket_path}' ]; then
1765
+ printf '/tmp/tmux-test.sock\\n'
1766
+ else
1767
+ printf '0\\n'
1768
+ fi
1769
+ ;;
1770
+ show-options)
1771
+ printf 'off\\n'
1772
+ ;;
1773
+ set-option|kill-session)
1774
+ ;;
1775
+ esac
1776
+ exit 0
1777
+ `);
1778
+ await chmod(join(fakeBin, "tmux"), 0o755);
1779
+ const steps = buildDetachedSessionBootstrapSteps("omx-demo", cwd, buildTmuxPaneCommand("codex", ["--bad-startup-flag"], "/bin/sh"), "'node' '/tmp/omx.js' 'hud' '--watch'", null);
1780
+ const leaderCmd = steps[0]?.args.at(-1);
1781
+ assert.equal(typeof leaderCmd, "string");
1782
+ const result = (await import("node:child_process")).spawnSync("/bin/sh", ["-c", leaderCmd], {
1783
+ cwd,
1784
+ env: {
1785
+ ...process.env,
1786
+ PATH: `${fakeBin}:/usr/bin:/bin`,
1787
+ HOME: cwd,
1788
+ },
1789
+ input: "\n",
1790
+ encoding: "utf-8",
1791
+ });
1792
+ assert.equal(result.status, 42);
1793
+ assert.match(result.stderr, /codex-stderr: unsupported startup flag/);
1794
+ assert.match(result.stderr, /codex exited with code 42 during startup/);
1795
+ assert.match(result.stderr, /detached tmux session is being kept open/);
1796
+ const log = await readFile(logPath, "utf-8");
1797
+ assert.doesNotMatch(log, /tmux:kill-session -t omx-demo/);
1798
+ }
1799
+ finally {
1800
+ await rm(cwd, { recursive: true, force: true });
1801
+ }
1802
+ });
1803
+ it("detached leader command keeps immediate zero-code exits visible instead of silently closing", async () => {
1804
+ const cwd = await mkdtemp(join(tmpdir(), "omx-detached-leader-zero-"));
1805
+ const fakeBin = join(cwd, "bin");
1806
+ const logPath = join(cwd, "leader.log");
1807
+ try {
1808
+ await mkdir(fakeBin, { recursive: true });
1809
+ await writeFile(join(fakeBin, "codex"), `#!/bin/sh
1810
+ printf 'codex-started-then-quit\\n' >&2
1811
+ exit 0
1812
+ `);
1813
+ await chmod(join(fakeBin, "codex"), 0o755);
1814
+ await writeFile(join(fakeBin, "tmux"), `#!/bin/sh
1815
+ printf 'tmux:%s\\n' "$*" >> "${logPath}"
1816
+ case "$1" in
1817
+ display-message)
1818
+ if [ "$3" = '#{socket_path}' ] || [ "$4" = '#{socket_path}' ]; then
1819
+ printf '/tmp/tmux-test.sock\\n'
1820
+ else
1821
+ printf '0\\n'
1822
+ fi
1823
+ ;;
1824
+ show-options)
1825
+ printf 'off\\n'
1826
+ ;;
1827
+ set-option|kill-session)
1828
+ ;;
1829
+ esac
1830
+ exit 0
1831
+ `);
1832
+ await chmod(join(fakeBin, "tmux"), 0o755);
1833
+ const steps = buildDetachedSessionBootstrapSteps("omx-demo", cwd, buildTmuxPaneCommand("codex", [], "/bin/sh"), "'node' '/tmp/omx.js' 'hud' '--watch'", null);
1834
+ const leaderCmd = steps[0]?.args.at(-1);
1835
+ assert.equal(typeof leaderCmd, "string");
1836
+ const result = (await import("node:child_process")).spawnSync("/bin/sh", ["-c", leaderCmd], {
1837
+ cwd,
1838
+ env: {
1839
+ ...process.env,
1840
+ PATH: `${fakeBin}:/usr/bin:/bin`,
1841
+ HOME: cwd,
1842
+ },
1843
+ input: "\n",
1844
+ encoding: "utf-8",
1845
+ });
1846
+ assert.equal(result.status, 0, result.stderr || result.stdout);
1847
+ assert.match(result.stderr, /codex-started-then-quit/);
1848
+ assert.match(result.stderr, /codex exited immediately with code 0 during startup/);
1849
+ assert.match(result.stderr, /detached tmux session is being kept open/);
1850
+ const log = await readFile(logPath, "utf-8");
1851
+ assert.match(log, /tmux:kill-session -t omx-demo/);
1852
+ }
1853
+ finally {
1854
+ await rm(cwd, { recursive: true, force: true });
1855
+ }
1856
+ });
1687
1857
  it("detached leader command terminates codex child on external SIGHUP", async () => {
1688
1858
  const cwd = await mkdtemp(join(tmpdir(), "omx-detached-leader-hup-"));
1689
1859
  const fakeBin = join(cwd, "bin");