oh-my-codex 0.11.11 → 0.11.13

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 (342) hide show
  1. package/Cargo.lock +5 -5
  2. package/Cargo.toml +1 -1
  3. package/README.de.md +12 -6
  4. package/README.el.md +223 -0
  5. package/README.es.md +12 -6
  6. package/README.fr.md +11 -5
  7. package/README.it.md +12 -6
  8. package/README.ja.md +12 -6
  9. package/README.ko.md +12 -6
  10. package/README.md +56 -28
  11. package/README.pl.md +216 -0
  12. package/README.pt.md +12 -6
  13. package/README.ru.md +12 -6
  14. package/README.tr.md +12 -6
  15. package/README.vi.md +148 -183
  16. package/README.zh-TW.md +14 -17
  17. package/README.zh.md +12 -6
  18. package/crates/omx-runtime-core/src/engine.rs +122 -4
  19. package/crates/omx-runtime-core/src/lib.rs +17 -0
  20. package/dist/autoresearch/contracts.d.ts.map +1 -1
  21. package/dist/autoresearch/contracts.js +1 -0
  22. package/dist/autoresearch/contracts.js.map +1 -1
  23. package/dist/autoresearch/runtime.d.ts.map +1 -1
  24. package/dist/autoresearch/runtime.js +7 -1
  25. package/dist/autoresearch/runtime.js.map +1 -1
  26. package/dist/cli/__tests__/agents.test.js +24 -1
  27. package/dist/cli/__tests__/agents.test.js.map +1 -1
  28. package/dist/cli/__tests__/autoresearch.test.js +11 -0
  29. package/dist/cli/__tests__/autoresearch.test.js.map +1 -1
  30. package/dist/cli/__tests__/cleanup.test.js +117 -4
  31. package/dist/cli/__tests__/cleanup.test.js.map +1 -1
  32. package/dist/cli/__tests__/doctor-warning-copy.test.js +33 -3
  33. package/dist/cli/__tests__/doctor-warning-copy.test.js.map +1 -1
  34. package/dist/cli/__tests__/error-handling-warnings.test.js +13 -0
  35. package/dist/cli/__tests__/error-handling-warnings.test.js.map +1 -1
  36. package/dist/cli/__tests__/exec.test.js +6 -0
  37. package/dist/cli/__tests__/exec.test.js.map +1 -1
  38. package/dist/cli/__tests__/index.test.js +101 -1
  39. package/dist/cli/__tests__/index.test.js.map +1 -1
  40. package/dist/cli/__tests__/launch-fallback.test.js +3 -0
  41. package/dist/cli/__tests__/launch-fallback.test.js.map +1 -1
  42. package/dist/cli/__tests__/package-bin-contract.test.js +10 -0
  43. package/dist/cli/__tests__/package-bin-contract.test.js.map +1 -1
  44. package/dist/cli/__tests__/packaged-script-resolution.test.js +4 -3
  45. package/dist/cli/__tests__/packaged-script-resolution.test.js.map +1 -1
  46. package/dist/cli/__tests__/resume.test.js +6 -0
  47. package/dist/cli/__tests__/resume.test.js.map +1 -1
  48. package/dist/cli/__tests__/setup-refresh.test.js +29 -12
  49. package/dist/cli/__tests__/setup-refresh.test.js.map +1 -1
  50. package/dist/cli/__tests__/setup-scope.test.js +1 -1
  51. package/dist/cli/__tests__/setup-scope.test.js.map +1 -1
  52. package/dist/cli/__tests__/star-prompt.test.js +16 -0
  53. package/dist/cli/__tests__/star-prompt.test.js.map +1 -1
  54. package/dist/cli/__tests__/uninstall.test.js +112 -1
  55. package/dist/cli/__tests__/uninstall.test.js.map +1 -1
  56. package/dist/cli/__tests__/windows-popup-loop-contract.test.d.ts +2 -0
  57. package/dist/cli/__tests__/windows-popup-loop-contract.test.d.ts.map +1 -0
  58. package/dist/cli/__tests__/windows-popup-loop-contract.test.js +30 -0
  59. package/dist/cli/__tests__/windows-popup-loop-contract.test.js.map +1 -0
  60. package/dist/cli/agents.d.ts.map +1 -1
  61. package/dist/cli/agents.js +9 -3
  62. package/dist/cli/agents.js.map +1 -1
  63. package/dist/cli/autoresearch-guided.d.ts.map +1 -1
  64. package/dist/cli/autoresearch-guided.js +9 -3
  65. package/dist/cli/autoresearch-guided.js.map +1 -1
  66. package/dist/cli/autoresearch.d.ts.map +1 -1
  67. package/dist/cli/autoresearch.js +8 -2
  68. package/dist/cli/autoresearch.js.map +1 -1
  69. package/dist/cli/cleanup.d.ts +2 -0
  70. package/dist/cli/cleanup.d.ts.map +1 -1
  71. package/dist/cli/cleanup.js +27 -1
  72. package/dist/cli/cleanup.js.map +1 -1
  73. package/dist/cli/doctor.js +7 -0
  74. package/dist/cli/doctor.js.map +1 -1
  75. package/dist/cli/index.d.ts +9 -1
  76. package/dist/cli/index.d.ts.map +1 -1
  77. package/dist/cli/index.js +171 -55
  78. package/dist/cli/index.js.map +1 -1
  79. package/dist/cli/setup.d.ts.map +1 -1
  80. package/dist/cli/setup.js +18 -15
  81. package/dist/cli/setup.js.map +1 -1
  82. package/dist/cli/star-prompt.d.ts.map +1 -1
  83. package/dist/cli/star-prompt.js +2 -0
  84. package/dist/cli/star-prompt.js.map +1 -1
  85. package/dist/cli/team.d.ts.map +1 -1
  86. package/dist/cli/team.js +5 -1
  87. package/dist/cli/team.js.map +1 -1
  88. package/dist/cli/tmux-hook.d.ts.map +1 -1
  89. package/dist/cli/tmux-hook.js +4 -1
  90. package/dist/cli/tmux-hook.js.map +1 -1
  91. package/dist/cli/uninstall.d.ts.map +1 -1
  92. package/dist/cli/uninstall.js +26 -0
  93. package/dist/cli/uninstall.js.map +1 -1
  94. package/dist/cli/update.d.ts.map +1 -1
  95. package/dist/cli/update.js +1 -0
  96. package/dist/cli/update.js.map +1 -1
  97. package/dist/compat/__tests__/rust-runtime-compat.test.js +84 -1
  98. package/dist/compat/__tests__/rust-runtime-compat.test.js.map +1 -1
  99. package/dist/config/__tests__/generator-idempotent.test.js +4 -4
  100. package/dist/config/__tests__/generator-idempotent.test.js.map +1 -1
  101. package/dist/config/__tests__/mcp-registry.test.js +13 -16
  102. package/dist/config/__tests__/mcp-registry.test.js.map +1 -1
  103. package/dist/config/mcp-registry.d.ts +1 -0
  104. package/dist/config/mcp-registry.d.ts.map +1 -1
  105. package/dist/config/mcp-registry.js +4 -4
  106. package/dist/config/mcp-registry.js.map +1 -1
  107. package/dist/config/models.d.ts +1 -0
  108. package/dist/config/models.d.ts.map +1 -1
  109. package/dist/config/models.js +39 -1
  110. package/dist/config/models.js.map +1 -1
  111. package/dist/hooks/__tests__/keyword-detector.test.js +12 -1
  112. package/dist/hooks/__tests__/keyword-detector.test.js.map +1 -1
  113. package/dist/hooks/__tests__/notify-fallback-watcher.test.js +554 -18
  114. package/dist/hooks/__tests__/notify-fallback-watcher.test.js.map +1 -1
  115. package/dist/hooks/__tests__/notify-hook-auto-nudge.test.js +347 -16
  116. package/dist/hooks/__tests__/notify-hook-auto-nudge.test.js.map +1 -1
  117. package/dist/hooks/__tests__/notify-hook-modules.test.js +5 -0
  118. package/dist/hooks/__tests__/notify-hook-modules.test.js.map +1 -1
  119. package/dist/hooks/__tests__/notify-hook-ralph-resume.test.d.ts +2 -0
  120. package/dist/hooks/__tests__/notify-hook-ralph-resume.test.d.ts.map +1 -0
  121. package/dist/hooks/__tests__/notify-hook-ralph-resume.test.js +597 -0
  122. package/dist/hooks/__tests__/notify-hook-ralph-resume.test.js.map +1 -0
  123. package/dist/hooks/__tests__/notify-hook-regression-205.test.js +19 -1
  124. package/dist/hooks/__tests__/notify-hook-regression-205.test.js.map +1 -1
  125. package/dist/hooks/__tests__/notify-hook-session-scope.test.js +73 -53
  126. package/dist/hooks/__tests__/notify-hook-session-scope.test.js.map +1 -1
  127. package/dist/hooks/__tests__/notify-hook-team-dispatch.test.js +193 -2
  128. package/dist/hooks/__tests__/notify-hook-team-dispatch.test.js.map +1 -1
  129. package/dist/hooks/__tests__/notify-hook-team-leader-nudge.test.js +183 -0
  130. package/dist/hooks/__tests__/notify-hook-team-leader-nudge.test.js.map +1 -1
  131. package/dist/hooks/__tests__/notify-hook-tmux-heal.test.js +255 -97
  132. package/dist/hooks/__tests__/notify-hook-tmux-heal.test.js.map +1 -1
  133. package/dist/hooks/__tests__/notify-hook-tmux-scrollback.test.js +0 -0
  134. package/dist/hooks/__tests__/notify-hook-tmux-scrollback.test.js.map +1 -1
  135. package/dist/hooks/__tests__/notify-hook-worker-idle.test.js +46 -0
  136. package/dist/hooks/__tests__/notify-hook-worker-idle.test.js.map +1 -1
  137. package/dist/hooks/__tests__/prompt-team-routing.test.js +34 -0
  138. package/dist/hooks/__tests__/prompt-team-routing.test.js.map +1 -1
  139. package/dist/hooks/__tests__/tmux-hook-engine.test.js +32 -1
  140. package/dist/hooks/__tests__/tmux-hook-engine.test.js.map +1 -1
  141. package/dist/hooks/code-simplifier/index.d.ts.map +1 -1
  142. package/dist/hooks/code-simplifier/index.js +1 -0
  143. package/dist/hooks/code-simplifier/index.js.map +1 -1
  144. package/dist/hooks/codebase-map.d.ts.map +1 -1
  145. package/dist/hooks/codebase-map.js +1 -0
  146. package/dist/hooks/codebase-map.js.map +1 -1
  147. package/dist/hooks/extensibility/sdk/tmux.d.ts.map +1 -1
  148. package/dist/hooks/extensibility/sdk/tmux.js +3 -1
  149. package/dist/hooks/extensibility/sdk/tmux.js.map +1 -1
  150. package/dist/hooks/keyword-detector.d.ts +1 -0
  151. package/dist/hooks/keyword-detector.d.ts.map +1 -1
  152. package/dist/hooks/keyword-detector.js +48 -0
  153. package/dist/hooks/keyword-detector.js.map +1 -1
  154. package/dist/hooks/prompt-guidance-contract.d.ts.map +1 -1
  155. package/dist/hooks/prompt-guidance-contract.js +6 -0
  156. package/dist/hooks/prompt-guidance-contract.js.map +1 -1
  157. package/dist/hooks/session.d.ts.map +1 -1
  158. package/dist/hooks/session.js +1 -0
  159. package/dist/hooks/session.js.map +1 -1
  160. package/dist/hud/__tests__/state.test.js +70 -1
  161. package/dist/hud/__tests__/state.test.js.map +1 -1
  162. package/dist/hud/authority.d.ts.map +1 -1
  163. package/dist/hud/authority.js +1 -0
  164. package/dist/hud/authority.js.map +1 -1
  165. package/dist/hud/state.d.ts.map +1 -1
  166. package/dist/hud/state.js +52 -0
  167. package/dist/hud/state.js.map +1 -1
  168. package/dist/mcp/state-server.d.ts.map +1 -1
  169. package/dist/mcp/state-server.js +5 -0
  170. package/dist/mcp/state-server.js.map +1 -1
  171. package/dist/modes/__tests__/base-session-scope.test.js +46 -0
  172. package/dist/modes/__tests__/base-session-scope.test.js.map +1 -1
  173. package/dist/modes/base.d.ts.map +1 -1
  174. package/dist/modes/base.js +4 -0
  175. package/dist/modes/base.js.map +1 -1
  176. package/dist/notifications/__tests__/custom-alias-enablement.test.d.ts +2 -0
  177. package/dist/notifications/__tests__/custom-alias-enablement.test.d.ts.map +1 -0
  178. package/dist/notifications/__tests__/custom-alias-enablement.test.js +84 -0
  179. package/dist/notifications/__tests__/custom-alias-enablement.test.js.map +1 -0
  180. package/dist/notifications/__tests__/idle-cooldown.test.js +55 -0
  181. package/dist/notifications/__tests__/idle-cooldown.test.js.map +1 -1
  182. package/dist/notifications/idle-cooldown.d.ts +8 -6
  183. package/dist/notifications/idle-cooldown.d.ts.map +1 -1
  184. package/dist/notifications/idle-cooldown.js +53 -22
  185. package/dist/notifications/idle-cooldown.js.map +1 -1
  186. package/dist/notifications/notifier.js +1 -1
  187. package/dist/notifications/notifier.js.map +1 -1
  188. package/dist/notifications/reply-listener.d.ts.map +1 -1
  189. package/dist/notifications/reply-listener.js +1 -0
  190. package/dist/notifications/reply-listener.js.map +1 -1
  191. package/dist/notifications/tmux.d.ts.map +1 -1
  192. package/dist/notifications/tmux.js +4 -0
  193. package/dist/notifications/tmux.js.map +1 -1
  194. package/dist/openclaw/config.js +2 -2
  195. package/dist/openclaw/config.js.map +1 -1
  196. package/dist/runtime/bridge.d.ts +2 -0
  197. package/dist/runtime/bridge.d.ts.map +1 -1
  198. package/dist/runtime/bridge.js +8 -0
  199. package/dist/runtime/bridge.js.map +1 -1
  200. package/dist/scripts/notify-fallback-watcher.js +103 -53
  201. package/dist/scripts/notify-fallback-watcher.js.map +1 -1
  202. package/dist/scripts/notify-hook/auto-nudge.d.ts +2 -1
  203. package/dist/scripts/notify-hook/auto-nudge.d.ts.map +1 -1
  204. package/dist/scripts/notify-hook/auto-nudge.js +90 -104
  205. package/dist/scripts/notify-hook/auto-nudge.js.map +1 -1
  206. package/dist/scripts/notify-hook/managed-tmux.d.ts +19 -0
  207. package/dist/scripts/notify-hook/managed-tmux.d.ts.map +1 -0
  208. package/dist/scripts/notify-hook/managed-tmux.js +320 -0
  209. package/dist/scripts/notify-hook/managed-tmux.js.map +1 -0
  210. package/dist/scripts/notify-hook/operational-events.d.ts.map +1 -1
  211. package/dist/scripts/notify-hook/operational-events.js +2 -0
  212. package/dist/scripts/notify-hook/operational-events.js.map +1 -1
  213. package/dist/scripts/notify-hook/ralph-session-resume.d.ts +22 -0
  214. package/dist/scripts/notify-hook/ralph-session-resume.d.ts.map +1 -0
  215. package/dist/scripts/notify-hook/ralph-session-resume.js +277 -0
  216. package/dist/scripts/notify-hook/ralph-session-resume.js.map +1 -0
  217. package/dist/scripts/notify-hook/state-io.d.ts +1 -1
  218. package/dist/scripts/notify-hook/state-io.d.ts.map +1 -1
  219. package/dist/scripts/notify-hook/state-io.js +2 -10
  220. package/dist/scripts/notify-hook/state-io.js.map +1 -1
  221. package/dist/scripts/notify-hook/team-dispatch.d.ts +1 -1
  222. package/dist/scripts/notify-hook/team-dispatch.d.ts.map +1 -1
  223. package/dist/scripts/notify-hook/team-dispatch.js +123 -72
  224. package/dist/scripts/notify-hook/team-dispatch.js.map +1 -1
  225. package/dist/scripts/notify-hook/team-leader-nudge.d.ts +2 -1
  226. package/dist/scripts/notify-hook/team-leader-nudge.d.ts.map +1 -1
  227. package/dist/scripts/notify-hook/team-leader-nudge.js +13 -5
  228. package/dist/scripts/notify-hook/team-leader-nudge.js.map +1 -1
  229. package/dist/scripts/notify-hook/team-tmux-guard.d.ts.map +1 -1
  230. package/dist/scripts/notify-hook/team-tmux-guard.js +1 -19
  231. package/dist/scripts/notify-hook/team-tmux-guard.js.map +1 -1
  232. package/dist/scripts/notify-hook/team-worker.js +4 -4
  233. package/dist/scripts/notify-hook/team-worker.js.map +1 -1
  234. package/dist/scripts/notify-hook/tmux-injection.d.ts +1 -1
  235. package/dist/scripts/notify-hook/tmux-injection.d.ts.map +1 -1
  236. package/dist/scripts/notify-hook/tmux-injection.js +102 -35
  237. package/dist/scripts/notify-hook/tmux-injection.js.map +1 -1
  238. package/dist/scripts/notify-hook.js +144 -20
  239. package/dist/scripts/notify-hook.js.map +1 -1
  240. package/dist/scripts/run-provider-advisor.js +2 -0
  241. package/dist/scripts/run-provider-advisor.js.map +1 -1
  242. package/dist/scripts/run-test-files.d.ts +2 -0
  243. package/dist/scripts/run-test-files.d.ts.map +1 -0
  244. package/dist/scripts/run-test-files.js +41 -0
  245. package/dist/scripts/run-test-files.js.map +1 -0
  246. package/dist/scripts/tmux-hook-engine.d.ts +2 -0
  247. package/dist/scripts/tmux-hook-engine.d.ts.map +1 -1
  248. package/dist/scripts/tmux-hook-engine.js +15 -0
  249. package/dist/scripts/tmux-hook-engine.js.map +1 -1
  250. package/dist/team/__tests__/api-interop.test.js +136 -4
  251. package/dist/team/__tests__/api-interop.test.js.map +1 -1
  252. package/dist/team/__tests__/leader-activity.test.js +107 -2
  253. package/dist/team/__tests__/leader-activity.test.js.map +1 -1
  254. package/dist/team/__tests__/runtime-cli.test.js +32 -0
  255. package/dist/team/__tests__/runtime-cli.test.js.map +1 -1
  256. package/dist/team/__tests__/runtime.test.js +148 -0
  257. package/dist/team/__tests__/runtime.test.js.map +1 -1
  258. package/dist/team/__tests__/shutdown-fallback.test.js +13 -0
  259. package/dist/team/__tests__/shutdown-fallback.test.js.map +1 -1
  260. package/dist/team/__tests__/state-root.test.js +11 -1
  261. package/dist/team/__tests__/state-root.test.js.map +1 -1
  262. package/dist/team/__tests__/state.test.js +237 -0
  263. package/dist/team/__tests__/state.test.js.map +1 -1
  264. package/dist/team/__tests__/tmux-session.test.js +521 -2
  265. package/dist/team/__tests__/tmux-session.test.js.map +1 -1
  266. package/dist/team/api-interop.d.ts.map +1 -1
  267. package/dist/team/api-interop.js +41 -31
  268. package/dist/team/api-interop.js.map +1 -1
  269. package/dist/team/commit-hygiene.d.ts +60 -0
  270. package/dist/team/commit-hygiene.d.ts.map +1 -0
  271. package/dist/team/commit-hygiene.js +232 -0
  272. package/dist/team/commit-hygiene.js.map +1 -0
  273. package/dist/team/leader-activity.d.ts.map +1 -1
  274. package/dist/team/leader-activity.js +56 -4
  275. package/dist/team/leader-activity.js.map +1 -1
  276. package/dist/team/runtime-cli.d.ts +9 -1
  277. package/dist/team/runtime-cli.d.ts.map +1 -1
  278. package/dist/team/runtime-cli.js +15 -6
  279. package/dist/team/runtime-cli.js.map +1 -1
  280. package/dist/team/runtime.d.ts +7 -2
  281. package/dist/team/runtime.d.ts.map +1 -1
  282. package/dist/team/runtime.js +392 -171
  283. package/dist/team/runtime.js.map +1 -1
  284. package/dist/team/scaling.d.ts.map +1 -1
  285. package/dist/team/scaling.js +6 -2
  286. package/dist/team/scaling.js.map +1 -1
  287. package/dist/team/state/dispatch.d.ts +2 -0
  288. package/dist/team/state/dispatch.d.ts.map +1 -1
  289. package/dist/team/state/dispatch.js +86 -40
  290. package/dist/team/state/dispatch.js.map +1 -1
  291. package/dist/team/state/mailbox.d.ts +3 -0
  292. package/dist/team/state/mailbox.d.ts.map +1 -1
  293. package/dist/team/state/mailbox.js +93 -19
  294. package/dist/team/state/mailbox.js.map +1 -1
  295. package/dist/team/state-root.d.ts +1 -1
  296. package/dist/team/state-root.d.ts.map +1 -1
  297. package/dist/team/state-root.js +8 -3
  298. package/dist/team/state-root.js.map +1 -1
  299. package/dist/team/state.d.ts.map +1 -1
  300. package/dist/team/state.js +96 -2
  301. package/dist/team/state.js.map +1 -1
  302. package/dist/team/tmux-session.d.ts.map +1 -1
  303. package/dist/team/tmux-session.js +81 -29
  304. package/dist/team/tmux-session.js.map +1 -1
  305. package/dist/team/worker-bootstrap.d.ts.map +1 -1
  306. package/dist/team/worker-bootstrap.js +4 -0
  307. package/dist/team/worker-bootstrap.js.map +1 -1
  308. package/dist/team/worktree.d.ts.map +1 -1
  309. package/dist/team/worktree.js +9 -0
  310. package/dist/team/worktree.js.map +1 -1
  311. package/dist/utils/__tests__/paths.test.js +98 -11
  312. package/dist/utils/__tests__/paths.test.js.map +1 -1
  313. package/dist/utils/__tests__/platform-command.test.js +101 -2
  314. package/dist/utils/__tests__/platform-command.test.js.map +1 -1
  315. package/dist/utils/git-layout.d.ts +8 -0
  316. package/dist/utils/git-layout.d.ts.map +1 -0
  317. package/dist/utils/git-layout.js +58 -0
  318. package/dist/utils/git-layout.js.map +1 -0
  319. package/dist/utils/paths.d.ts +3 -0
  320. package/dist/utils/paths.d.ts.map +1 -1
  321. package/dist/utils/paths.js +14 -4
  322. package/dist/utils/paths.js.map +1 -1
  323. package/dist/utils/platform-command.d.ts.map +1 -1
  324. package/dist/utils/platform-command.js +35 -3
  325. package/dist/utils/platform-command.js.map +1 -1
  326. package/package.json +9 -5
  327. package/src/scripts/notify-fallback-watcher.ts +103 -53
  328. package/src/scripts/notify-hook/auto-nudge.ts +97 -103
  329. package/src/scripts/notify-hook/managed-tmux.ts +324 -0
  330. package/src/scripts/notify-hook/operational-events.ts +2 -0
  331. package/src/scripts/notify-hook/ralph-session-resume.ts +337 -0
  332. package/src/scripts/notify-hook/state-io.ts +2 -10
  333. package/src/scripts/notify-hook/team-dispatch.ts +131 -66
  334. package/src/scripts/notify-hook/team-leader-nudge.ts +19 -5
  335. package/src/scripts/notify-hook/team-tmux-guard.ts +0 -20
  336. package/src/scripts/notify-hook/team-worker.ts +4 -4
  337. package/src/scripts/notify-hook/tmux-injection.ts +103 -33
  338. package/src/scripts/notify-hook.ts +150 -21
  339. package/src/scripts/run-provider-advisor.ts +4 -2
  340. package/src/scripts/run-test-files.ts +48 -0
  341. package/src/scripts/tmux-hook-engine.ts +16 -0
  342. package/templates/AGENTS.md +51 -43
@@ -2,9 +2,10 @@ import { describe, it } from 'node:test';
2
2
  import assert from 'node:assert/strict';
3
3
  import { spawnSync } from 'node:child_process';
4
4
  import { chmod, mkdtemp, mkdir, readFile, rm, writeFile } from 'node:fs/promises';
5
- import { existsSync } from 'node:fs';
5
+ import { existsSync, readFileSync, writeFileSync } from 'node:fs';
6
6
  import { tmpdir } from 'node:os';
7
7
  import { join } from 'node:path';
8
+ import { buildTmuxSessionName } from '../../cli/index.js';
8
9
  const NOTIFY_HOOK_SCRIPT = new URL('../../../dist/scripts/notify-hook.js', import.meta.url);
9
10
  const DEEP_INTERVIEW_BLOCKED_APPROVAL_INPUTS = ['yes', 'y', 'proceed', 'continue', 'ok', 'sure', 'go ahead', 'next i should'];
10
11
  const NEXT_I_SHOULD_RESPONSE = 'Next I should update the focused tests.';
@@ -20,6 +21,44 @@ async function withTempWorkingDir(run) {
20
21
  async function writeJson(path, value) {
21
22
  await writeFile(path, JSON.stringify(value, null, 2));
22
23
  }
24
+ function readLinuxStartTicks(pid) {
25
+ try {
26
+ const stat = readFileSync(`/proc/${pid}/stat`, 'utf-8');
27
+ const commandEnd = stat.lastIndexOf(')');
28
+ if (commandEnd === -1)
29
+ return null;
30
+ const remainder = stat.slice(commandEnd + 1).trim();
31
+ const fields = remainder.split(/\s+/);
32
+ if (fields.length <= 19)
33
+ return null;
34
+ const startTicks = Number(fields[19]);
35
+ return Number.isFinite(startTicks) ? startTicks : null;
36
+ }
37
+ catch {
38
+ return null;
39
+ }
40
+ }
41
+ function readLinuxCmdline(pid) {
42
+ try {
43
+ const raw = readFileSync(`/proc/${pid}/cmdline`);
44
+ const text = raw.toString('utf-8').replace(/\0+/g, ' ').trim();
45
+ return text.length > 0 ? text : null;
46
+ }
47
+ catch {
48
+ return null;
49
+ }
50
+ }
51
+ async function writeManagedSessionState(stateDir, cwd) {
52
+ await writeJson(join(stateDir, 'session.json'), {
53
+ session_id: 'sess-managed',
54
+ started_at: new Date().toISOString(),
55
+ cwd,
56
+ pid: process.pid,
57
+ platform: process.platform,
58
+ pid_start_ticks: readLinuxStartTicks(process.pid),
59
+ pid_cmdline: readLinuxCmdline(process.pid),
60
+ });
61
+ }
23
62
  function escapeRegex(value) {
24
63
  return value.replace(/[-/\\^$*+?.()|[\]{}]/g, '\\$&');
25
64
  }
@@ -65,12 +104,23 @@ if [[ "\$cmd" == "display-message" ]]; then
65
104
  exit 0
66
105
  fi
67
106
  if [[ "\$format" == "#S" ]]; then
68
- echo "devsess"
107
+ echo "${'${OMX_TEST_TMUX_SESSION_NAME:-devsess}'}"
69
108
  exit 0
70
109
  fi
71
110
  exit 0
72
111
  fi
73
112
  if [[ "\$cmd" == "list-panes" ]]; then
113
+ target=""
114
+ while [[ "\$#" -gt 0 ]]; do
115
+ case "\$1" in
116
+ -t) target="\$2"; shift 2 ;;
117
+ *) shift ;;
118
+ esac
119
+ done
120
+ if [[ -n "\$target" && "\$target" == "${'${OMX_TEST_TMUX_SESSION_NAME:-devsess}'}" ]]; then
121
+ printf '%%99\tnode\tcodex --model gpt-5\n'
122
+ exit 0
123
+ fi
74
124
  echo "%1 12345"
75
125
  exit 0
76
126
  fi
@@ -78,11 +128,25 @@ exit 0
78
128
  `;
79
129
  }
80
130
  function runNotifyHook(cwd, fakeBinDir, codexHome, payloadOverrides = {}, extraEnv = {}) {
131
+ if (extraEnv.OMX_TEST_UNMANAGED_SESSION !== '1' && !extraEnv.OMX_TEAM_WORKER) {
132
+ const sessionPath = join(cwd, '.omx', 'state', 'session.json');
133
+ const sessionState = {
134
+ session_id: 'sess-managed',
135
+ started_at: new Date().toISOString(),
136
+ cwd,
137
+ pid: process.pid,
138
+ platform: process.platform,
139
+ pid_start_ticks: readLinuxStartTicks(process.pid),
140
+ pid_cmdline: readLinuxCmdline(process.pid),
141
+ };
142
+ writeFileSync(sessionPath, JSON.stringify(sessionState, null, 2));
143
+ }
81
144
  const payload = {
82
145
  cwd,
83
146
  type: 'agent-turn-complete',
84
147
  'thread-id': 'thread-test',
85
148
  'turn-id': `turn-${Date.now()}-${Math.random().toString(16).slice(2, 8)}`,
149
+ ...(extraEnv.OMX_TEST_UNMANAGED_SESSION !== '1' && !extraEnv.OMX_TEAM_WORKER ? { 'session-id': 'sess-managed' } : {}),
86
150
  'input-messages': ['test'],
87
151
  'last-assistant-message': 'done',
88
152
  ...payloadOverrides,
@@ -94,6 +158,8 @@ function runNotifyHook(cwd, fakeBinDir, codexHome, payloadOverrides = {}, extraE
94
158
  ...process.env,
95
159
  PATH: `${fakeBinDir}:${process.env.PATH || ''}`,
96
160
  CODEX_HOME: codexHome,
161
+ ...(extraEnv.OMX_TEST_UNMANAGED_SESSION !== '1' && !extraEnv.OMX_TEAM_WORKER ? { OMX_SESSION_ID: 'sess-managed' } : {}),
162
+ ...(extraEnv.OMX_TEST_UNMANAGED_SESSION !== '1' && !extraEnv.OMX_TEAM_WORKER ? { OMX_TEST_TMUX_SESSION_NAME: buildTmuxSessionName(cwd, 'sess-managed') } : {}),
97
163
  TMUX_PANE: '%99',
98
164
  TMUX: '1',
99
165
  OMX_TEAM_WORKER: '',
@@ -121,6 +187,7 @@ describe('notify-hook auto-nudge', () => {
121
187
  });
122
188
  await writeFile(join(fakeBinDir, 'tmux'), buildFakeTmux(tmuxLogPath));
123
189
  await chmod(join(fakeBinDir, 'tmux'), 0o755);
190
+ await writeManagedSessionState(stateDir, cwd);
124
191
  const result = runNotifyHook(cwd, fakeBinDir, codexHome, {
125
192
  'last-assistant-message': 'I analyzed the code. If you want me to make these changes, let me know.',
126
193
  });
@@ -149,6 +216,7 @@ describe('notify-hook auto-nudge', () => {
149
216
  await writeJson(join(codexHome, '.omx-config.json'), {
150
217
  autoNudge: { enabled: true, delaySec: 0, stallMs: 0 },
151
218
  });
219
+ await writeManagedSessionState(stateDir, cwd);
152
220
  await writeFile(join(fakeBinDir, 'tmux'), buildFakeTmux(tmuxLogPath));
153
221
  await chmod(join(fakeBinDir, 'tmux'), 0o755);
154
222
  const result = runNotifyHook(cwd, fakeBinDir, codexHome, {
@@ -163,6 +231,169 @@ describe('notify-hook auto-nudge', () => {
163
231
  assert.ok(cmMatches && cmMatches.length >= 2, `should send C-m twice, got ${cmMatches?.length ?? 0}`);
164
232
  });
165
233
  });
234
+ it('respects `.omx/tmux-hook.json` enabled:false and skips auto-nudge injection', async () => {
235
+ await withTempWorkingDir(async (cwd) => {
236
+ const omxDir = join(cwd, '.omx');
237
+ const stateDir = join(omxDir, 'state');
238
+ const logsDir = join(omxDir, 'logs');
239
+ const codexHome = join(cwd, 'codex-home');
240
+ const fakeBinDir = join(cwd, 'fake-bin');
241
+ const tmuxLogPath = join(cwd, 'tmux.log');
242
+ await mkdir(logsDir, { recursive: true });
243
+ await mkdir(stateDir, { recursive: true });
244
+ await mkdir(codexHome, { recursive: true });
245
+ await mkdir(fakeBinDir, { recursive: true });
246
+ await writeJson(join(codexHome, '.omx-config.json'), {
247
+ autoNudge: { enabled: true, delaySec: 0, stallMs: 0 },
248
+ });
249
+ await writeJson(join(omxDir, 'tmux-hook.json'), {
250
+ enabled: false,
251
+ target: { type: 'pane', value: '%99' },
252
+ });
253
+ await writeManagedSessionState(stateDir, cwd);
254
+ await writeFile(join(fakeBinDir, 'tmux'), buildFakeTmux(tmuxLogPath));
255
+ await chmod(join(fakeBinDir, 'tmux'), 0o755);
256
+ const result = runNotifyHook(cwd, fakeBinDir, codexHome, {
257
+ 'last-assistant-message': 'I analyzed the code. If you want me to make these changes, let me know.',
258
+ });
259
+ assert.equal(result.status, 0, `hook failed: ${result.stderr || result.stdout}`);
260
+ const tmuxLog = await readFile(tmuxLogPath, 'utf-8').catch(() => '');
261
+ assert.doesNotMatch(tmuxLog, /send-keys -t %99 -l yes, proceed \[OMX_TMUX_INJECT\]/);
262
+ });
263
+ });
264
+ it('does not auto-nudge plain tmux Codex sessions that only inherit OMX session env', async () => {
265
+ await withTempWorkingDir(async (cwd) => {
266
+ const omxDir = join(cwd, '.omx');
267
+ const stateDir = join(omxDir, 'state');
268
+ const logsDir = join(omxDir, 'logs');
269
+ const codexHome = join(cwd, 'codex-home');
270
+ const fakeBinDir = join(cwd, 'fake-bin');
271
+ const tmuxLogPath = join(cwd, 'tmux.log');
272
+ await mkdir(logsDir, { recursive: true });
273
+ await mkdir(stateDir, { recursive: true });
274
+ await mkdir(codexHome, { recursive: true });
275
+ await mkdir(fakeBinDir, { recursive: true });
276
+ await writeJson(join(codexHome, '.omx-config.json'), {
277
+ autoNudge: { enabled: true, delaySec: 0, stallMs: 0 },
278
+ });
279
+ const sleeper = spawnSync('bash', ['-lc', 'sleep 5 >/dev/null 2>&1 & echo $!'], { encoding: 'utf8' });
280
+ assert.equal(sleeper.status, 0, sleeper.stderr || sleeper.stdout);
281
+ const sleeperPid = Number((sleeper.stdout || '').trim());
282
+ assert.ok(Number.isFinite(sleeperPid) && sleeperPid > 1, 'expected helper pid');
283
+ await writeJson(join(stateDir, 'session.json'), {
284
+ session_id: 'sess-managed',
285
+ started_at: new Date().toISOString(),
286
+ cwd,
287
+ pid: sleeperPid,
288
+ platform: process.platform,
289
+ pid_start_ticks: readLinuxStartTicks(sleeperPid),
290
+ pid_cmdline: readLinuxCmdline(sleeperPid),
291
+ });
292
+ await writeFile(join(fakeBinDir, 'tmux'), buildFakeTmux(tmuxLogPath));
293
+ await chmod(join(fakeBinDir, 'tmux'), 0o755);
294
+ const result = runNotifyHook(cwd, fakeBinDir, codexHome, {
295
+ 'session-id': 'sess-managed',
296
+ 'last-assistant-message': 'I analyzed the code. If you want me to make these changes, let me know.',
297
+ }, {
298
+ OMX_SESSION_ID: 'sess-managed',
299
+ OMX_TEST_UNMANAGED_SESSION: '1',
300
+ });
301
+ assert.equal(result.status, 0, `hook failed: ${result.stderr || result.stdout}`);
302
+ const tmuxLog = await readFile(tmuxLogPath, 'utf-8').catch(() => '');
303
+ assert.doesNotMatch(tmuxLog, /send-keys -t %99 -l yes, proceed \[OMX_TMUX_INJECT\]/);
304
+ });
305
+ });
306
+ it('does not auto-nudge plain tmux Codex sessions that are not OMX-managed', async () => {
307
+ await withTempWorkingDir(async (cwd) => {
308
+ const omxDir = join(cwd, '.omx');
309
+ const stateDir = join(omxDir, 'state');
310
+ const logsDir = join(omxDir, 'logs');
311
+ const codexHome = join(cwd, 'codex-home');
312
+ const fakeBinDir = join(cwd, 'fake-bin');
313
+ const tmuxLogPath = join(cwd, 'tmux.log');
314
+ await mkdir(logsDir, { recursive: true });
315
+ await mkdir(stateDir, { recursive: true });
316
+ await mkdir(codexHome, { recursive: true });
317
+ await mkdir(fakeBinDir, { recursive: true });
318
+ await writeJson(join(codexHome, '.omx-config.json'), {
319
+ autoNudge: { enabled: true, delaySec: 0, stallMs: 0 },
320
+ });
321
+ await writeFile(join(fakeBinDir, 'tmux'), buildFakeTmux(tmuxLogPath));
322
+ await chmod(join(fakeBinDir, 'tmux'), 0o755);
323
+ const result = runNotifyHook(cwd, fakeBinDir, codexHome, {
324
+ 'last-assistant-message': 'I analyzed the code. If you want me to make these changes, let me know.',
325
+ }, {
326
+ OMX_TEST_UNMANAGED_SESSION: '1',
327
+ });
328
+ assert.equal(result.status, 0, `hook failed: ${result.stderr || result.stdout}`);
329
+ const tmuxLog = await readFile(tmuxLogPath, 'utf-8').catch(() => '');
330
+ assert.doesNotMatch(tmuxLog, /send-keys -t %99 -l yes, proceed \[OMX_TMUX_INJECT\]/);
331
+ });
332
+ });
333
+ it('does not auto-nudge when payload session-id disagrees with the managed tmux session identity', async () => {
334
+ await withTempWorkingDir(async (cwd) => {
335
+ const omxDir = join(cwd, '.omx');
336
+ const stateDir = join(omxDir, 'state');
337
+ const logsDir = join(omxDir, 'logs');
338
+ const codexHome = join(cwd, 'codex-home');
339
+ const fakeBinDir = join(cwd, 'fake-bin');
340
+ const tmuxLogPath = join(cwd, 'tmux.log');
341
+ const managedSessionName = buildTmuxSessionName(cwd, 'sess-managed');
342
+ await mkdir(logsDir, { recursive: true });
343
+ await mkdir(stateDir, { recursive: true });
344
+ await mkdir(codexHome, { recursive: true });
345
+ await mkdir(fakeBinDir, { recursive: true });
346
+ await writeJson(join(codexHome, '.omx-config.json'), {
347
+ autoNudge: { enabled: true, delaySec: 0, stallMs: 0 },
348
+ });
349
+ await writeManagedSessionState(stateDir, cwd);
350
+ await writeFile(join(fakeBinDir, 'tmux'), buildFakeTmux(tmuxLogPath));
351
+ await chmod(join(fakeBinDir, 'tmux'), 0o755);
352
+ const result = runNotifyHook(cwd, fakeBinDir, codexHome, {
353
+ 'session-id': 'sess-other',
354
+ 'last-assistant-message': 'I analyzed the code. If you want me to make these changes, let me know.',
355
+ }, {
356
+ OMX_SESSION_ID: 'sess-managed',
357
+ OMX_TEST_TMUX_SESSION_NAME: managedSessionName,
358
+ });
359
+ assert.equal(result.status, 0, `hook failed: ${result.stderr || result.stdout}`);
360
+ const tmuxLog = await readFile(tmuxLogPath, 'utf-8');
361
+ assert.doesNotMatch(tmuxLog, /send-keys -t %99 -l yes, proceed \[OMX_TMUX_INJECT\]/);
362
+ });
363
+ });
364
+ it('does not auto-nudge when tmux session naming drifts from the current OMX session id', async () => {
365
+ await withTempWorkingDir(async (cwd) => {
366
+ const omxDir = join(cwd, '.omx');
367
+ const stateDir = join(omxDir, 'state');
368
+ const logsDir = join(omxDir, 'logs');
369
+ const codexHome = join(cwd, 'codex-home');
370
+ const fakeBinDir = join(cwd, 'fake-bin');
371
+ const tmuxLogPath = join(cwd, 'tmux.log');
372
+ const expectedManagedSessionName = buildTmuxSessionName(cwd, 'sess-managed');
373
+ const mismatchedDetachedSessionName = buildTmuxSessionName(cwd, 'sess-legacy-detached');
374
+ await mkdir(logsDir, { recursive: true });
375
+ await mkdir(stateDir, { recursive: true });
376
+ await mkdir(codexHome, { recursive: true });
377
+ await mkdir(fakeBinDir, { recursive: true });
378
+ await writeJson(join(codexHome, '.omx-config.json'), {
379
+ autoNudge: { enabled: true, delaySec: 0, stallMs: 0 },
380
+ });
381
+ await writeManagedSessionState(stateDir, cwd);
382
+ await writeFile(join(fakeBinDir, 'tmux'), buildFakeTmux(tmuxLogPath));
383
+ await chmod(join(fakeBinDir, 'tmux'), 0o755);
384
+ const result = runNotifyHook(cwd, fakeBinDir, codexHome, {
385
+ 'session-id': 'sess-managed',
386
+ 'last-assistant-message': 'I analyzed the code. If you want me to make these changes, let me know.',
387
+ }, {
388
+ OMX_SESSION_ID: 'sess-managed',
389
+ OMX_TEST_TMUX_SESSION_NAME: mismatchedDetachedSessionName,
390
+ });
391
+ assert.equal(result.status, 0, `hook failed: ${result.stderr || result.stdout}`);
392
+ const tmuxLog = await readFile(tmuxLogPath, 'utf-8');
393
+ assert.match(tmuxLog, new RegExp(`list-panes -s -t ${escapeRegex(expectedManagedSessionName)}`), 'should resolve panes against the current OMX session identity, not the drifted tmux session name');
394
+ assert.doesNotMatch(tmuxLog, /send-keys -t %99 -l yes, proceed \[OMX_TMUX_INJECT\]/);
395
+ });
396
+ });
166
397
  it('sends nudge via capture-pane fallback when payload has no stall pattern', async () => {
167
398
  await withTempWorkingDir(async (cwd) => {
168
399
  const omxDir = join(cwd, '.omx');
@@ -179,6 +410,7 @@ describe('notify-hook auto-nudge', () => {
179
410
  await writeJson(join(codexHome, '.omx-config.json'), {
180
411
  autoNudge: { enabled: true, delaySec: 0, stallMs: 0 },
181
412
  });
413
+ await writeManagedSessionState(stateDir, cwd);
182
414
  // capture-pane will return content with a stall pattern
183
415
  await writeFile(captureFile, 'Here are the results.\nWould you like me to continue with the implementation?\n› ');
184
416
  await writeFile(join(fakeBinDir, 'tmux'), buildFakeTmux(tmuxLogPath));
@@ -202,6 +434,7 @@ describe('notify-hook auto-nudge', () => {
202
434
  const codexHome = join(cwd, 'codex-home');
203
435
  const fakeBinDir = join(cwd, 'fake-bin');
204
436
  const tmuxLogPath = join(cwd, 'tmux.log');
437
+ const managedSessionName = buildTmuxSessionName(cwd, 'sess-managed');
205
438
  await mkdir(logsDir, { recursive: true });
206
439
  await mkdir(stateDir, { recursive: true });
207
440
  await mkdir(codexHome, { recursive: true });
@@ -209,6 +442,7 @@ describe('notify-hook auto-nudge', () => {
209
442
  await writeJson(join(codexHome, '.omx-config.json'), {
210
443
  autoNudge: { enabled: true, delaySec: 0, stallMs: 0 },
211
444
  });
445
+ await writeManagedSessionState(stateDir, cwd);
212
446
  await writeJson(join(stateDir, 'ralph-state.json'), {
213
447
  active: true,
214
448
  tmux_pane_id: '%99',
@@ -245,7 +479,7 @@ if [[ "$cmd" == "display-message" ]]; then
245
479
  exit 0
246
480
  fi
247
481
  if [[ "$format" == "#S" && "$target" == "%99" ]]; then
248
- echo "devsess"
482
+ echo "${managedSessionName}"
249
483
  exit 0
250
484
  fi
251
485
  exit 0
@@ -258,7 +492,7 @@ if [[ "$cmd" == "list-panes" ]]; then
258
492
  *) shift ;;
259
493
  esac
260
494
  done
261
- if [[ "$target" == "devsess" ]]; then
495
+ if [[ "$target" == "${managedSessionName}" ]]; then
262
496
  printf "%%99\tsh\tbash\n%%100\tnode\tcodex --model gpt-5\n"
263
497
  exit 0
264
498
  fi
@@ -283,7 +517,6 @@ exit 0
283
517
  });
284
518
  assert.equal(result.status, 0, `hook failed: ${result.stderr || result.stdout}`);
285
519
  const tmuxLog = await readFile(tmuxLogPath, 'utf-8');
286
- assert.match(tmuxLog, /display-message -t %99 -p #S/, 'should anchor off the active mode pane');
287
520
  assert.match(tmuxLog, /send-keys -t %100 -l yes, proceed \[OMX_TMUX_INJECT\]/, 'should upgrade anchored shell pane to sibling codex pane');
288
521
  });
289
522
  });
@@ -294,6 +527,7 @@ exit 0
294
527
  const codexHome = join(cwd, 'codex-home');
295
528
  const fakeBinDir = join(cwd, 'fake-bin');
296
529
  const tmuxLogPath = join(cwd, 'tmux.log');
530
+ const managedSessionName = buildTmuxSessionName(cwd, 'sess-managed');
297
531
  await mkdir(logsDir, { recursive: true });
298
532
  await mkdir(workerStateRoot, { recursive: true });
299
533
  await mkdir(codexHome, { recursive: true });
@@ -351,6 +585,7 @@ exit 0
351
585
  const codexHome = join(cwd, 'codex-home');
352
586
  const fakeBinDir = join(cwd, 'fake-bin');
353
587
  const tmuxLogPath = join(cwd, 'tmux.log');
588
+ const managedSessionName = buildTmuxSessionName(cwd, 'sess-managed');
354
589
  await mkdir(logsDir, { recursive: true });
355
590
  await mkdir(stateDir, { recursive: true });
356
591
  await mkdir(codexHome, { recursive: true });
@@ -399,11 +634,11 @@ exit 0
399
634
  });
400
635
  assert.equal(result.status, 0, `hook failed: ${result.stderr || result.stdout}`);
401
636
  const tmuxLog = await readFile(tmuxLogPath, 'utf-8');
402
- assert.match(tmuxLog, /display-message -t %99 -p #\{pane_current_command\}/);
637
+ assert.ok(tmuxLog.includes('display-message -p -t %99 #S'), 'should inspect the managed anchor pane before deciding');
403
638
  assert.doesNotMatch(tmuxLog, /send-keys -t %99 -l yes, proceed \[OMX_TMUX_INJECT\]/, 'shell pane should not receive auto-nudge injection');
404
639
  });
405
640
  });
406
- it('upgrades the wrapper shell pane to the sibling live Codex pane before injecting', async () => {
641
+ it('falls back to the sibling codex pane when TMUX_PANE is a managed non-agent shell pane', async () => {
407
642
  await withTempWorkingDir(async (cwd) => {
408
643
  const omxDir = join(cwd, '.omx');
409
644
  const stateDir = join(omxDir, 'state');
@@ -411,6 +646,7 @@ exit 0
411
646
  const codexHome = join(cwd, 'codex-home');
412
647
  const fakeBinDir = join(cwd, 'fake-bin');
413
648
  const tmuxLogPath = join(cwd, 'tmux.log');
649
+ const managedSessionName = buildTmuxSessionName(cwd, 'sess-managed');
414
650
  await mkdir(logsDir, { recursive: true });
415
651
  await mkdir(stateDir, { recursive: true });
416
652
  await mkdir(codexHome, { recursive: true });
@@ -442,11 +678,11 @@ shift || true
442
678
  exit 0
443
679
  fi
444
680
  if [[ "$format" == "#S" && "$target" == "%99" ]]; then
445
- echo "devsess"
681
+ echo "${managedSessionName}"
446
682
  exit 0
447
683
  fi
448
684
  if [[ "$format" == "#S" && "$target" == "%100" ]]; then
449
- echo "devsess"
685
+ echo "${managedSessionName}"
450
686
  exit 0
451
687
  fi
452
688
  if [[ "$format" == "#{pane_current_path}" && "$target" == "%99" ]]; then
@@ -474,7 +710,7 @@ if [[ "$cmd" == "list-panes" ]]; then
474
710
  *) shift ;;
475
711
  esac
476
712
  done
477
- if [[ "$target" == "devsess" ]]; then
713
+ if [[ "$target" == "${managedSessionName}" ]]; then
478
714
  printf "%%99\t1\tsh\\n%%100\t0\tcodex --model gpt-5\\n"
479
715
  exit 0
480
716
  fi
@@ -491,7 +727,7 @@ exit 0
491
727
  assert.equal(result.status, 0, `hook failed: ${result.stderr || result.stdout}`);
492
728
  const tmuxLog = await readFile(tmuxLogPath, 'utf-8');
493
729
  assert.match(tmuxLog, /display-message -p #S/);
494
- assert.match(tmuxLog, /display-message -t %99 -p #\{pane_current_command\}/);
730
+ assert.ok(tmuxLog.includes('display-message -p -t %99 #S'), 'should inspect the anchored shell pane before upgrading');
495
731
  assert.match(tmuxLog, /send-keys -t %100 -l yes, proceed \[OMX_TMUX_INJECT\]/);
496
732
  });
497
733
  });
@@ -503,6 +739,7 @@ exit 0
503
739
  const codexHome = join(cwd, 'codex-home');
504
740
  const fakeBinDir = join(cwd, 'fake-bin');
505
741
  const tmuxLogPath = join(cwd, 'tmux.log');
742
+ const managedSessionName = buildTmuxSessionName(cwd, 'sess-managed');
506
743
  await mkdir(logsDir, { recursive: true });
507
744
  await mkdir(stateDir, { recursive: true });
508
745
  await mkdir(codexHome, { recursive: true });
@@ -518,7 +755,7 @@ exit 0
518
755
  assert.equal(result.status, 0, `hook failed: ${result.stderr || result.stdout}`);
519
756
  const tmuxLog = await readFile(tmuxLogPath, 'utf-8');
520
757
  assert.match(tmuxLog, /display-message -p #S/);
521
- assert.match(tmuxLog, /send-keys -t %99 -l yes, proceed \[OMX_TMUX_INJECT\]/, 'current implementation still injects in this copy-mode fixture');
758
+ assert.doesNotMatch(tmuxLog, /send-keys -t %99 -l yes, proceed \[OMX_TMUX_INJECT\]/, 'copy-mode pane should not receive auto-nudge injection');
522
759
  });
523
760
  });
524
761
  it('does not nudge when pane capture shows an active task despite stall-like assistant text', async () => {
@@ -552,8 +789,8 @@ exit 0
552
789
  assert.equal(result.status, 0, `hook failed: ${result.stderr || result.stdout}`);
553
790
  const tmuxLog = await readFile(tmuxLogPath, 'utf-8');
554
791
  assert.match(tmuxLog, /display-message -p #S/);
555
- assert.doesNotMatch(tmuxLog, /capture-pane -t %99/, 'current canonical pane path no longer requires capture-pane here');
556
- assert.match(tmuxLog, /send-keys -t %99 -l yes, proceed \[OMX_TMUX_INJECT\]/, 'current implementation still injects in this busy-pane fixture');
792
+ assert.match(tmuxLog, /capture-pane -t %99/, 'busy pane detection should inspect capture output');
793
+ assert.doesNotMatch(tmuxLog, /send-keys -t %99 -l yes, proceed \[OMX_TMUX_INJECT\]/, 'busy pane should not receive auto-nudge injection');
557
794
  });
558
795
  });
559
796
  it('respects enabled=false configuration', async () => {
@@ -666,6 +903,53 @@ exit 0
666
903
  assert.equal(nudgeState.lastSemanticSignature, 'stall:proceed_intent');
667
904
  });
668
905
  });
906
+ it('does not resend the exact same stalled turn after TTL expiry', async () => {
907
+ await withTempWorkingDir(async (cwd) => {
908
+ const omxDir = join(cwd, '.omx');
909
+ const stateDir = join(omxDir, 'state');
910
+ const logsDir = join(omxDir, 'logs');
911
+ const codexHome = join(cwd, 'codex-home');
912
+ const fakeBinDir = join(cwd, 'fake-bin');
913
+ const tmuxLogPath = join(cwd, 'tmux.log');
914
+ const lastTurnAt = '2026-03-01T00:00:00.000Z';
915
+ const lastMessage = 'If you want, I can keep going from here.';
916
+ await mkdir(logsDir, { recursive: true });
917
+ await mkdir(stateDir, { recursive: true });
918
+ await mkdir(codexHome, { recursive: true });
919
+ await mkdir(fakeBinDir, { recursive: true });
920
+ await writeJson(join(codexHome, '.omx-config.json'), {
921
+ autoNudge: { enabled: true, delaySec: 0, stallMs: 0, ttlMs: 5000 },
922
+ });
923
+ await writeJson(join(stateDir, 'hud-state.json'), {
924
+ last_turn_at: lastTurnAt,
925
+ turn_count: 1,
926
+ last_agent_output: lastMessage,
927
+ });
928
+ await writeFile(join(fakeBinDir, 'tmux'), buildFakeTmux(tmuxLogPath));
929
+ await chmod(join(fakeBinDir, 'tmux'), 0o755);
930
+ const first = runNotifyHook(cwd, fakeBinDir, codexHome, {
931
+ 'turn-id': 'stalled-turn-1',
932
+ 'last-assistant-message': lastMessage,
933
+ });
934
+ assert.equal(first.status, 0, `first hook failed: ${first.stderr || first.stdout}`);
935
+ const nudgeStatePath = join(stateDir, 'auto-nudge-state.json');
936
+ const firstState = JSON.parse(await readFile(nudgeStatePath, 'utf-8'));
937
+ await writeJson(nudgeStatePath, {
938
+ ...firstState,
939
+ lastNudgeAt: '2026-03-01T00:00:10.000Z',
940
+ });
941
+ const result = runNotifyHook(cwd, fakeBinDir, codexHome, {
942
+ 'turn-id': 'stalled-turn-1',
943
+ 'last-assistant-message': lastMessage,
944
+ });
945
+ assert.equal(result.status, 0, `second hook failed: ${result.stderr || result.stdout}`);
946
+ const tmuxLog = await readFile(tmuxLogPath, 'utf-8').catch(() => '');
947
+ assert.equal((tmuxLog.match(/send-keys -t %99 -l yes, proceed \[OMX_TMUX_INJECT\]/g) || []).length, 1);
948
+ const nudgeState = JSON.parse(await readFile(nudgeStatePath, 'utf-8'));
949
+ assert.equal(nudgeState.nudgeCount, 1);
950
+ assert.equal(nudgeState.lastSignature, firstState.lastSignature);
951
+ });
952
+ });
669
953
  it('uses custom response from config', async () => {
670
954
  await withTempWorkingDir(async (cwd) => {
671
955
  const omxDir = join(cwd, '.omx');
@@ -778,6 +1062,48 @@ exit 0
778
1062
  assert.doesNotMatch(tmuxLog, /send-keys -t %99 -l yes, proceed \[OMX_TMUX_INJECT\]/);
779
1063
  });
780
1064
  });
1065
+ it('disables auto-nudge when only skill-active-state carries the deep-interview input lock', async () => {
1066
+ await withTempWorkingDir(async (cwd) => {
1067
+ const omxDir = join(cwd, '.omx');
1068
+ const stateDir = join(omxDir, 'state');
1069
+ const logsDir = join(omxDir, 'logs');
1070
+ const codexHome = join(cwd, 'codex-home');
1071
+ const fakeBinDir = join(cwd, 'fake-bin');
1072
+ const tmuxLogPath = join(cwd, 'tmux.log');
1073
+ await mkdir(logsDir, { recursive: true });
1074
+ await mkdir(stateDir, { recursive: true });
1075
+ await mkdir(codexHome, { recursive: true });
1076
+ await mkdir(fakeBinDir, { recursive: true });
1077
+ await writeJson(join(codexHome, '.omx-config.json'), {
1078
+ autoNudge: { enabled: true, delaySec: 0, stallMs: 0 },
1079
+ });
1080
+ await writeJson(join(stateDir, 'skill-active-state.json'), {
1081
+ version: 1,
1082
+ active: true,
1083
+ skill: 'deep-interview',
1084
+ keyword: 'deep interview',
1085
+ phase: 'planning',
1086
+ activated_at: '2026-02-25T00:00:00.000Z',
1087
+ updated_at: '2026-02-25T00:00:00.000Z',
1088
+ source: 'keyword-detector',
1089
+ input_lock: {
1090
+ active: true,
1091
+ scope: 'deep-interview-auto-approval',
1092
+ acquired_at: '2026-02-25T00:00:00.000Z',
1093
+ blocked_inputs: DEEP_INTERVIEW_BLOCKED_APPROVAL_INPUTS,
1094
+ message: 'Deep interview is active; auto-approval shortcuts are blocked until the interview finishes.',
1095
+ },
1096
+ });
1097
+ await writeFile(join(fakeBinDir, 'tmux'), buildFakeTmux(tmuxLogPath));
1098
+ await chmod(join(fakeBinDir, 'tmux'), 0o755);
1099
+ const result = runNotifyHook(cwd, fakeBinDir, codexHome, {
1100
+ 'last-assistant-message': 'Would you like me to continue?',
1101
+ });
1102
+ assert.equal(result.status, 0, `hook failed: ${result.stderr || result.stdout}`);
1103
+ const tmuxLog = await readFile(tmuxLogPath, 'utf-8').catch(() => '');
1104
+ assert.doesNotMatch(tmuxLog, /send-keys -t %99 -l yes, proceed \[OMX_TMUX_INJECT\]/);
1105
+ });
1106
+ });
781
1107
  it('acquires the deep-interview input lock when deep-interview activates', async () => {
782
1108
  await withTempWorkingDir(async (cwd) => {
783
1109
  const omxDir = join(cwd, '.omx');
@@ -804,6 +1130,11 @@ exit 0
804
1130
  assert.equal(skillState.input_lock?.active, true);
805
1131
  assert.deepEqual(skillState.input_lock?.blocked_inputs, DEEP_INTERVIEW_BLOCKED_APPROVAL_INPUTS);
806
1132
  assert.match(skillState.input_lock?.message || '', /Deep interview is active/i);
1133
+ const modeState = JSON.parse(await readFile(join(stateDir, 'deep-interview-state.json'), 'utf-8'));
1134
+ assert.equal(modeState.active, true);
1135
+ assert.equal(modeState.mode, 'deep-interview');
1136
+ assert.equal(modeState.current_phase, 'intent-first');
1137
+ assert.equal(modeState.input_lock?.active, true);
807
1138
  });
808
1139
  });
809
1140
  for (const blockedResponse of ['yes', 'y', 'proceed', 'continue', 'ok', 'sure', 'go ahead']) {
@@ -1149,7 +1480,7 @@ exit 0
1149
1480
  assert.match(tmuxLog, /send-keys -t %99 -l yes, proceed \[OMX_TMUX_INJECT\]/, 'should nudge with default config and marker');
1150
1481
  });
1151
1482
  });
1152
- it('does not nudge when TMUX_PANE is not set', async () => {
1483
+ it('can still resolve the managed session pane when TMUX_PANE is not set', async () => {
1153
1484
  await withTempWorkingDir(async (cwd) => {
1154
1485
  const omxDir = join(cwd, '.omx');
1155
1486
  const stateDir = join(omxDir, 'state');
@@ -1175,7 +1506,7 @@ exit 0
1175
1506
  assert.equal(result.status, 0, `hook failed: ${result.stderr || result.stdout}`);
1176
1507
  if (existsSync(tmuxLogPath)) {
1177
1508
  const tmuxLog = await readFile(tmuxLogPath, 'utf-8');
1178
- assert.doesNotMatch(tmuxLog, /send-keys.*-l yes, proceed/, 'should not nudge without pane');
1509
+ assert.match(tmuxLog, /send-keys -t %99 -l yes, proceed \[OMX_TMUX_INJECT\]/, 'should fall back to the managed session pane when TMUX_PANE is absent');
1179
1510
  }
1180
1511
  });
1181
1512
  });