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
@@ -7,6 +7,7 @@ import { join } from 'node:path';
7
7
  import { spawn, spawnSync } from 'node:child_process';
8
8
  import { randomUUID } from 'node:crypto';
9
9
  import { initTeamState, enqueueDispatchRequest, readDispatchRequest } from '../../team/state.js';
10
+ import { buildWindowsMsysBackgroundHelperBootstrapScript } from '../../cli/index.js';
10
11
  import { writeSessionStart } from '../session.js';
11
12
  async function appendLine(path, line) {
12
13
  const prev = await readFile(path, 'utf-8');
@@ -111,7 +112,7 @@ if [[ "$cmd" == "display-message" ]]; then
111
112
  exit 0
112
113
  fi
113
114
  if [[ "$fmt" == "#S" ]]; then
114
- echo "session-test"
115
+ echo "\${OMX_TEST_TMUX_SESSION_NAME:-session-test}"
115
116
  exit 0
116
117
  fi
117
118
  exit 0
@@ -129,6 +130,20 @@ if [[ "$cmd" == "send-keys" ]]; then
129
130
  exit 0
130
131
  fi
131
132
  if [[ "$cmd" == "list-panes" ]]; then
133
+ target=""
134
+ while [[ "$#" -gt 0 ]]; do
135
+ case "$1" in
136
+ -t)
137
+ shift
138
+ target="$1"
139
+ ;;
140
+ esac
141
+ shift || true
142
+ done
143
+ if [[ -n "$target" ]]; then
144
+ printf "%%42\tcodex\tcodex\n"
145
+ exit 0
146
+ fi
132
147
  echo "%42 1"
133
148
  exit 0
134
149
  fi
@@ -372,6 +387,71 @@ describe('notify-fallback watcher', () => {
372
387
  await rm(wd, { recursive: true, force: true });
373
388
  }
374
389
  });
390
+ it('suppresses idle no-op lifecycle and control-plane logs during authority-only one-shot ticks', async () => {
391
+ const wd = await mkdtemp(join(tmpdir(), 'omx-fallback-authority-noop-'));
392
+ try {
393
+ await mkdir(join(wd, '.omx', 'logs'), { recursive: true });
394
+ await mkdir(join(wd, '.omx', 'state'), { recursive: true });
395
+ const watcherScript = new URL('../../../dist/scripts/notify-fallback-watcher.js', import.meta.url).pathname;
396
+ const notifyHook = new URL('../../../dist/scripts/notify-hook.js', import.meta.url).pathname;
397
+ const result = spawnSync(process.execPath, [watcherScript, '--once', '--authority-only', '--cwd', wd, '--notify-script', notifyHook, '--poll-ms', '50'], { encoding: 'utf-8', env: buildCleanNotifyEnv() });
398
+ assert.equal(result.status, 0, result.stderr || result.stdout);
399
+ const watcherStatePath = join(wd, '.omx', 'state', 'notify-fallback-state.json');
400
+ const watcherState = JSON.parse(await readFile(watcherStatePath, 'utf-8'));
401
+ assert.equal(watcherState.authority_only, true);
402
+ assert.equal(watcherState.dispatch_drain?.run_count, 1);
403
+ assert.equal(watcherState.dispatch_drain?.last_result?.processed ?? 0, 0);
404
+ assert.equal(watcherState.leader_nudge?.run_count, 1);
405
+ assert.equal(watcherState.leader_nudge?.precomputed_leader_stale, false);
406
+ assert.equal(watcherState.fallback_auto_nudge?.last_reason, 'hud_state_missing');
407
+ const logPath = join(wd, '.omx', 'logs', `notify-fallback-${new Date().toISOString().split('T')[0]}.jsonl`);
408
+ const logContent = await readFile(logPath, 'utf-8').catch(() => '');
409
+ assert.equal(logContent.trim(), '');
410
+ }
411
+ finally {
412
+ await rm(wd, { recursive: true, force: true });
413
+ }
414
+ });
415
+ it('suppresses authority-only control-plane ticks when only skill-active-state carries the deep-interview input lock', async () => {
416
+ const wd = await mkdtemp(join(tmpdir(), 'omx-fallback-authority-skill-lock-'));
417
+ try {
418
+ await mkdir(join(wd, '.omx', 'logs'), { recursive: true });
419
+ await mkdir(join(wd, '.omx', 'state'), { recursive: true });
420
+ await writeFile(join(wd, '.omx', 'state', 'skill-active-state.json'), JSON.stringify({
421
+ version: 1,
422
+ active: true,
423
+ skill: 'deep-interview',
424
+ keyword: 'deep interview',
425
+ phase: 'planning',
426
+ activated_at: '2026-02-25T00:00:00.000Z',
427
+ updated_at: '2026-02-25T00:00:00.000Z',
428
+ source: 'keyword-detector',
429
+ input_lock: {
430
+ active: true,
431
+ scope: 'deep-interview-auto-approval',
432
+ acquired_at: '2026-02-25T00:00:00.000Z',
433
+ blocked_inputs: ['yes', 'y', 'proceed', 'continue', 'ok', 'sure', 'go ahead', 'next i should'],
434
+ message: 'Deep interview is active; auto-approval shortcuts are blocked until the interview finishes.',
435
+ },
436
+ }, null, 2));
437
+ const watcherScript = new URL('../../../dist/scripts/notify-fallback-watcher.js', import.meta.url).pathname;
438
+ const notifyHook = new URL('../../../dist/scripts/notify-hook.js', import.meta.url).pathname;
439
+ const result = spawnSync(process.execPath, [watcherScript, '--once', '--authority-only', '--cwd', wd, '--notify-script', notifyHook, '--poll-ms', '50'], { encoding: 'utf-8', env: buildCleanNotifyEnv() });
440
+ assert.equal(result.status, 0, result.stderr || result.stdout);
441
+ const watcherStatePath = join(wd, '.omx', 'state', 'notify-fallback-state.json');
442
+ const watcherState = JSON.parse(await readFile(watcherStatePath, 'utf-8'));
443
+ assert.equal(watcherState.authority_only, true);
444
+ assert.equal(watcherState.dispatch_drain?.run_count, 1);
445
+ assert.equal(watcherState.leader_nudge?.run_count, 0);
446
+ assert.equal(watcherState.fallback_auto_nudge?.last_reason, 'init');
447
+ const logPath = join(wd, '.omx', 'logs', `notify-fallback-${new Date().toISOString().split('T')[0]}.jsonl`);
448
+ const logContent = await readFile(logPath, 'utf-8').catch(() => '');
449
+ assert.equal(logContent.trim(), '');
450
+ }
451
+ finally {
452
+ await rm(wd, { recursive: true, force: true });
453
+ }
454
+ });
375
455
  it('disables fallback watcher nudges when deep-interview state is active', async () => {
376
456
  const wd = await mkdtemp(join(tmpdir(), 'omx-fallback-deep-interview-suppressed-'));
377
457
  const fakeBinDir = join(wd, 'fake-bin');
@@ -423,6 +503,69 @@ describe('notify-fallback watcher', () => {
423
503
  await rm(wd, { recursive: true, force: true });
424
504
  }
425
505
  });
506
+ it('disables fallback watcher nudges when only skill-active-state carries the deep-interview input lock', async () => {
507
+ const wd = await mkdtemp(join(tmpdir(), 'omx-fallback-deep-interview-skill-lock-'));
508
+ const fakeBinDir = join(wd, 'fake-bin');
509
+ const tmuxLogPath = join(wd, 'tmux.log');
510
+ try {
511
+ await mkdir(join(wd, '.omx', 'logs'), { recursive: true });
512
+ await mkdir(join(wd, '.omx', 'state', 'team', 'dispatch-team'), { recursive: true });
513
+ await mkdir(fakeBinDir, { recursive: true });
514
+ await writeFile(join(fakeBinDir, 'tmux'), buildFakeTmux(tmuxLogPath));
515
+ await chmod(join(fakeBinDir, 'tmux'), 0o755);
516
+ await writeFile(join(wd, '.omx', 'state', 'skill-active-state.json'), JSON.stringify({
517
+ version: 1,
518
+ active: true,
519
+ skill: 'deep-interview',
520
+ keyword: 'deep interview',
521
+ phase: 'planning',
522
+ activated_at: '2026-02-25T00:00:00.000Z',
523
+ updated_at: '2026-02-25T00:00:00.000Z',
524
+ source: 'keyword-detector',
525
+ input_lock: {
526
+ active: true,
527
+ scope: 'deep-interview-auto-approval',
528
+ acquired_at: '2026-02-25T00:00:00.000Z',
529
+ blocked_inputs: ['yes', 'y', 'proceed', 'continue', 'ok', 'sure', 'go ahead', 'next i should'],
530
+ message: 'Deep interview is active; auto-approval shortcuts are blocked until the interview finishes.',
531
+ },
532
+ }, null, 2));
533
+ await writeFile(join(wd, '.omx', 'state', 'ralph-state.json'), JSON.stringify({
534
+ active: true,
535
+ current_phase: 'executing',
536
+ tmux_pane_id: '%42',
537
+ }, null, 2));
538
+ await writeFile(join(wd, '.omx', 'state', 'team-state.json'), JSON.stringify({
539
+ active: true,
540
+ team_name: 'dispatch-team',
541
+ current_phase: 'team-exec',
542
+ }, null, 2));
543
+ await writeFile(join(wd, '.omx', 'state', 'hud-state.json'), JSON.stringify({
544
+ last_turn_at: new Date(Date.now() - 300_000).toISOString(),
545
+ turn_count: 3,
546
+ last_agent_output: 'Would you like me to continue?',
547
+ }, null, 2));
548
+ await writeFile(join(wd, '.omx', 'state', 'team', 'dispatch-team', 'config.json'), JSON.stringify({
549
+ name: 'dispatch-team',
550
+ tmux_session: 'omx-team-dispatch-team',
551
+ leader_pane_id: '%42',
552
+ }, null, 2));
553
+ const watcherScript = new URL('../../../dist/scripts/notify-fallback-watcher.js', import.meta.url).pathname;
554
+ const notifyHook = new URL('../../../dist/scripts/notify-hook.js', import.meta.url).pathname;
555
+ const result = spawnSync(process.execPath, [watcherScript, '--once', '--cwd', wd, '--notify-script', notifyHook], {
556
+ encoding: 'utf-8',
557
+ env: buildCleanNotifyEnv({ PATH: `${fakeBinDir}:${process.env.PATH || ''}` }),
558
+ });
559
+ assert.equal(result.status, 0, result.stderr || result.stdout);
560
+ const tmuxLog = await readFile(tmuxLogPath, 'utf8').catch(() => '');
561
+ assert.doesNotMatch(tmuxLog, /Ralph loop active continue/);
562
+ assert.doesNotMatch(tmuxLog, /Team dispatch-team:/);
563
+ assert.doesNotMatch(tmuxLog, /yes, proceed \[OMX_TMUX_INJECT\]/);
564
+ }
565
+ finally {
566
+ await rm(wd, { recursive: true, force: true });
567
+ }
568
+ });
426
569
  it('runs leader nudge checks from the fallback watcher so stale alerts do not wait for a leader turn', async () => {
427
570
  const wd = await mkdtemp(join(tmpdir(), 'omx-fallback-leader-nudge-'));
428
571
  const fakeBinDir = join(wd, 'fake-bin');
@@ -532,9 +675,181 @@ describe('notify-fallback watcher', () => {
532
675
  const logPath = join(wd, '.omx', 'logs', `notify-fallback-${new Date().toISOString().split('T')[0]}.jsonl`);
533
676
  const logEntries = (await readFile(logPath, 'utf-8')).trim().split('\n').filter(Boolean).map((line) => JSON.parse(line));
534
677
  const nudgeEvent = logEntries.find((entry) => entry.type === 'leader_nudge_tick');
535
- assert.ok(nudgeEvent, 'expected leader_nudge_tick log event');
536
- assert.equal(nudgeEvent.precomputed_leader_stale, false);
537
- assert.equal(nudgeEvent.reason, 'leader_nudge_skipped_not_stale');
678
+ assert.equal(nudgeEvent, undefined);
679
+ }
680
+ finally {
681
+ await rm(wd, { recursive: true, force: true });
682
+ }
683
+ });
684
+ it('runs stalled-worker leader nudges from the fallback watcher even when the leader is not stale', async () => {
685
+ const wd = await mkdtemp(join(tmpdir(), 'omx-fallback-worker-stall-nudge-'));
686
+ const fakeBinDir = join(wd, 'fake-bin');
687
+ const tmuxLogPath = join(wd, 'tmux.log');
688
+ try {
689
+ await mkdir(join(wd, '.omx', 'logs'), { recursive: true });
690
+ await mkdir(join(wd, '.omx', 'state', 'team', 'dispatch-team', 'workers', 'worker-1'), { recursive: true });
691
+ await mkdir(join(wd, '.omx', 'state', 'team', 'dispatch-team', 'tasks'), { recursive: true });
692
+ await mkdir(fakeBinDir, { recursive: true });
693
+ const tmuxScript = `#!/usr/bin/env bash
694
+ set -eu
695
+ echo "$@" >> "${tmuxLogPath}"
696
+ cmd="$1"
697
+ shift || true
698
+ if [[ "$cmd" == "display-message" ]]; then
699
+ target=""
700
+ fmt=""
701
+ while [[ "$#" -gt 0 ]]; do
702
+ case "$1" in
703
+ -t)
704
+ shift
705
+ target="$1"
706
+ ;;
707
+ *)
708
+ fmt="$1"
709
+ ;;
710
+ esac
711
+ shift || true
712
+ done
713
+ if [[ "$fmt" == "#{pane_in_mode}" ]]; then
714
+ echo "0"
715
+ exit 0
716
+ fi
717
+ if [[ "$fmt" == "#{pane_id}" ]]; then
718
+ echo "\${target:-%42}"
719
+ exit 0
720
+ fi
721
+ if [[ "$fmt" == "#{pane_current_path}" ]]; then
722
+ dirname "${tmuxLogPath}"
723
+ exit 0
724
+ fi
725
+ if [[ "$fmt" == "#{pane_current_command}" ]]; then
726
+ echo "codex"
727
+ exit 0
728
+ fi
729
+ if [[ "$fmt" == "#S" ]]; then
730
+ echo "omx-team-dispatch-team"
731
+ exit 0
732
+ fi
733
+ exit 0
734
+ fi
735
+ if [[ "$cmd" == "send-keys" ]]; then
736
+ exit 0
737
+ fi
738
+ if [[ "$cmd" == "list-panes" ]]; then
739
+ target=""
740
+ while [[ "$#" -gt 0 ]]; do
741
+ case "$1" in
742
+ -t)
743
+ shift
744
+ target="$1"
745
+ ;;
746
+ esac
747
+ shift || true
748
+ done
749
+ if [[ -n "$target" ]]; then
750
+ printf "%%42 12345\n%%10 12346\n%%11 12347\n"
751
+ exit 0
752
+ fi
753
+ echo "%42 1"
754
+ exit 0
755
+ fi
756
+ exit 0
757
+ `;
758
+ await writeFile(join(fakeBinDir, 'tmux'), tmuxScript);
759
+ await chmod(join(fakeBinDir, 'tmux'), 0o755);
760
+ const now = Date.now();
761
+ await writeFile(join(wd, '.omx', 'state', 'team-state.json'), JSON.stringify({
762
+ active: true,
763
+ team_name: 'dispatch-team',
764
+ current_phase: 'team-exec',
765
+ }, null, 2));
766
+ await writeFile(join(wd, '.omx', 'state', 'hud-state.json'), JSON.stringify({
767
+ last_turn_at: new Date().toISOString(),
768
+ turn_count: 3,
769
+ }, null, 2));
770
+ await writeFile(join(wd, '.omx', 'state', 'team', 'dispatch-team', 'config.json'), JSON.stringify({
771
+ name: 'dispatch-team',
772
+ tmux_session: 'omx-team-dispatch-team',
773
+ leader_pane_id: '%42',
774
+ workers: [
775
+ { name: 'worker-1', index: 1, pane_id: '%10' },
776
+ { name: 'worker-2', index: 2, pane_id: '%11' },
777
+ ],
778
+ }, null, 2));
779
+ await writeFile(join(wd, '.omx', 'state', 'team', 'dispatch-team', 'tasks', 'task-1.json'), JSON.stringify({
780
+ id: '1',
781
+ subject: 'Pending work',
782
+ description: 'Needs attention',
783
+ status: 'pending',
784
+ created_at: new Date().toISOString(),
785
+ }, null, 2));
786
+ await writeFile(join(wd, '.omx', 'state', 'team', 'dispatch-team', 'workers', 'worker-1', 'status.json'), JSON.stringify({
787
+ state: 'working',
788
+ current_task_id: '1',
789
+ updated_at: new Date(now - 180_000).toISOString(),
790
+ }, null, 2));
791
+ await writeFile(join(wd, '.omx', 'state', 'team', 'dispatch-team', 'workers', 'worker-1', 'heartbeat.json'), JSON.stringify({
792
+ alive: true,
793
+ pid: 101,
794
+ turn_count: 2,
795
+ last_turn_at: new Date(now - 180_000).toISOString(),
796
+ }, null, 2));
797
+ await writeFile(join(wd, '.omx', 'state', 'team-leader-nudge.json'), JSON.stringify({
798
+ last_nudged_by_team: {
799
+ 'dispatch-team': {
800
+ at: new Date(now - 5_000).toISOString(),
801
+ last_message_id: '',
802
+ reason: 'new_mailbox_message',
803
+ },
804
+ },
805
+ progress_by_team: {
806
+ 'dispatch-team': {
807
+ signature: JSON.stringify({
808
+ tasks: [{ id: '1', owner: '', status: 'pending' }],
809
+ workers: [
810
+ {
811
+ worker: 'worker-1',
812
+ state: 'working',
813
+ current_task_id: '1',
814
+ status_missing: false,
815
+ turn_count: 2,
816
+ heartbeat_missing: false,
817
+ },
818
+ {
819
+ worker: 'worker-2',
820
+ state: 'unknown',
821
+ current_task_id: '',
822
+ status_missing: true,
823
+ turn_count: null,
824
+ heartbeat_missing: true,
825
+ },
826
+ ],
827
+ }),
828
+ last_progress_at: new Date(now - 180_000).toISOString(),
829
+ },
830
+ },
831
+ }, null, 2));
832
+ const watcherScript = new URL('../../../dist/scripts/notify-fallback-watcher.js', import.meta.url).pathname;
833
+ const notifyHook = new URL('../../../dist/scripts/notify-hook.js', import.meta.url).pathname;
834
+ const result = spawnSync(process.execPath, [watcherScript, '--once', '--cwd', wd, '--notify-script', notifyHook], {
835
+ encoding: 'utf-8',
836
+ env: buildCleanNotifyEnv({
837
+ PATH: `${fakeBinDir}:${process.env.PATH || ''}`,
838
+ OMX_TEAM_PROGRESS_STALL_MS: '60000',
839
+ OMX_TEAM_LEADER_NUDGE_MS: '30000',
840
+ OMX_TEAM_LEADER_STALE_MS: '60000',
841
+ }),
842
+ });
843
+ assert.equal(result.status, 0, result.stderr || result.stdout);
844
+ const tmuxLog = await readFile(tmuxLogPath, 'utf8');
845
+ assert.match(tmuxLog, /send-keys -t %42 -l Team dispatch-team: worker panes stalled, no progress 3m/);
846
+ assert.doesNotMatch(tmuxLog, /leader stale/);
847
+ const watcherStatePath = join(wd, '.omx', 'state', 'notify-fallback-state.json');
848
+ const watcherState = JSON.parse(await readFile(watcherStatePath, 'utf-8'));
849
+ assert.equal(watcherState.leader_nudge?.enabled, true);
850
+ assert.equal(watcherState.leader_nudge?.leader_only, true);
851
+ assert.equal(watcherState.leader_nudge?.run_count, 1);
852
+ assert.equal(watcherState.leader_nudge?.precomputed_leader_stale, false);
538
853
  }
539
854
  finally {
540
855
  await rm(wd, { recursive: true, force: true });
@@ -555,6 +870,7 @@ describe('notify-fallback watcher', () => {
555
870
  await writeFile(join(codexHome, '.omx-config.json'), JSON.stringify({
556
871
  autoNudge: { enabled: true, delaySec: 0, ttlMs: 30_000 },
557
872
  }, null, 2));
873
+ await writeSessionStart(wd, 'sess-managed-fallback');
558
874
  await writeFile(join(wd, '.omx', 'state', 'hud-state.json'), JSON.stringify({
559
875
  last_turn_at: new Date(Date.now() - 6_000).toISOString(),
560
876
  turn_count: 7,
@@ -567,6 +883,8 @@ describe('notify-fallback watcher', () => {
567
883
  env: buildCleanNotifyEnv({
568
884
  PATH: `${fakeBinDir}:${process.env.PATH || ''}`,
569
885
  CODEX_HOME: codexHome,
886
+ OMX_SESSION_ID: 'sess-managed-fallback',
887
+ OMX_TEST_TMUX_SESSION_NAME: 'omx-fallback-auto-nudge-stalled-managed',
570
888
  TMUX: '1',
571
889
  TMUX_PANE: '%42',
572
890
  OMX_NOTIFY_FALLBACK_AUTO_NUDGE_STALL_MS: '5000',
@@ -585,6 +903,98 @@ describe('notify-fallback watcher', () => {
585
903
  await rm(wd, { recursive: true, force: true });
586
904
  }
587
905
  });
906
+ it('respects `.omx/tmux-hook.json` enabled:false for fallback auto-nudge', async () => {
907
+ const wd = await mkdtemp(join(tmpdir(), 'omx-fallback-auto-nudge-disabled-'));
908
+ const fakeBinDir = join(wd, 'fake-bin');
909
+ const tmuxLogPath = join(wd, 'tmux.log');
910
+ const codexHome = join(wd, 'codex-home');
911
+ try {
912
+ await mkdir(join(wd, '.omx', 'logs'), { recursive: true });
913
+ await mkdir(join(wd, '.omx', 'state'), { recursive: true });
914
+ await mkdir(fakeBinDir, { recursive: true });
915
+ await mkdir(codexHome, { recursive: true });
916
+ await writeFile(join(fakeBinDir, 'tmux'), buildFakeTmux(tmuxLogPath));
917
+ await chmod(join(fakeBinDir, 'tmux'), 0o755);
918
+ await writeFile(join(codexHome, '.omx-config.json'), JSON.stringify({
919
+ autoNudge: { enabled: true, delaySec: 0, ttlMs: 30_000 },
920
+ }, null, 2));
921
+ await writeFile(join(wd, '.omx', 'tmux-hook.json'), JSON.stringify({
922
+ enabled: false,
923
+ target: { type: 'pane', value: '%42' },
924
+ }, null, 2));
925
+ await writeSessionStart(wd, 'sess-managed-fallback');
926
+ await writeFile(join(wd, '.omx', 'state', 'hud-state.json'), JSON.stringify({
927
+ last_turn_at: new Date(Date.now() - 6_000).toISOString(),
928
+ turn_count: 7,
929
+ last_agent_output: 'If you want, I can keep going from here.',
930
+ }, null, 2));
931
+ const watcherScript = new URL('../../../dist/scripts/notify-fallback-watcher.js', import.meta.url).pathname;
932
+ const notifyHook = new URL('../../../dist/scripts/notify-hook.js', import.meta.url).pathname;
933
+ const result = spawnSync(process.execPath, [watcherScript, '--once', '--cwd', wd, '--notify-script', notifyHook, '--poll-ms', '50'], {
934
+ encoding: 'utf-8',
935
+ env: buildCleanNotifyEnv({
936
+ PATH: `${fakeBinDir}:${process.env.PATH || ''}`,
937
+ CODEX_HOME: codexHome,
938
+ OMX_SESSION_ID: 'sess-managed-fallback',
939
+ TMUX: '1',
940
+ TMUX_PANE: '%42',
941
+ OMX_NOTIFY_FALLBACK_AUTO_NUDGE_STALL_MS: '5000',
942
+ }),
943
+ });
944
+ assert.equal(result.status, 0, result.stderr || result.stdout);
945
+ const tmuxLog = await readFile(tmuxLogPath, 'utf8').catch(() => '');
946
+ assert.doesNotMatch(tmuxLog, /send-keys -t %42 -l yes, proceed \[OMX_TMUX_INJECT\]/);
947
+ }
948
+ finally {
949
+ await rm(wd, { recursive: true, force: true });
950
+ }
951
+ });
952
+ it('suppresses fallback unmanaged-session auto-nudge skip logs while idle', async () => {
953
+ const wd = await mkdtemp(join(tmpdir(), 'omx-fallback-auto-nudge-unmanaged-'));
954
+ const fakeBinDir = join(wd, 'fake-bin');
955
+ const tmuxLogPath = join(wd, 'tmux.log');
956
+ const codexHome = join(wd, 'codex-home');
957
+ try {
958
+ await mkdir(join(wd, '.omx', 'logs'), { recursive: true });
959
+ await mkdir(join(wd, '.omx', 'state'), { recursive: true });
960
+ await mkdir(fakeBinDir, { recursive: true });
961
+ await mkdir(codexHome, { recursive: true });
962
+ await writeFile(join(fakeBinDir, 'tmux'), buildFakeTmux(tmuxLogPath));
963
+ await chmod(join(fakeBinDir, 'tmux'), 0o755);
964
+ await writeFile(join(codexHome, '.omx-config.json'), JSON.stringify({
965
+ autoNudge: { enabled: true, delaySec: 0, ttlMs: 30_000 },
966
+ }, null, 2));
967
+ await writeFile(join(wd, '.omx', 'state', 'hud-state.json'), JSON.stringify({
968
+ last_turn_at: new Date(Date.now() - 6_000).toISOString(),
969
+ turn_count: 9,
970
+ last_agent_output: 'If you want, I can keep going from here.',
971
+ }, null, 2));
972
+ const watcherScript = new URL('../../../dist/scripts/notify-fallback-watcher.js', import.meta.url).pathname;
973
+ const notifyHook = new URL('../../../dist/scripts/notify-hook.js', import.meta.url).pathname;
974
+ const result = spawnSync(process.execPath, [watcherScript, '--once', '--cwd', wd, '--notify-script', notifyHook, '--poll-ms', '50'], {
975
+ encoding: 'utf-8',
976
+ env: buildCleanNotifyEnv({
977
+ PATH: `${fakeBinDir}:${process.env.PATH || ''}`,
978
+ CODEX_HOME: codexHome,
979
+ TMUX: '1',
980
+ TMUX_PANE: '%42',
981
+ OMX_NOTIFY_FALLBACK_AUTO_NUDGE_STALL_MS: '5000',
982
+ }),
983
+ });
984
+ assert.equal(result.status, 0, result.stderr || result.stdout);
985
+ const tmuxLog = await readFile(tmuxLogPath, 'utf8').catch(() => '');
986
+ assert.doesNotMatch(tmuxLog, /send-keys -t %42 -l yes, proceed \[OMX_TMUX_INJECT\]/);
987
+ const watcherStatePath = join(wd, '.omx', 'state', 'notify-fallback-state.json');
988
+ const watcherState = JSON.parse(await readFile(watcherStatePath, 'utf-8'));
989
+ assert.equal(watcherState.fallback_auto_nudge?.last_reason, 'eligible_but_not_sent');
990
+ const tmuxHookLogPath = join(wd, '.omx', 'logs', `tmux-hook-${new Date().toISOString().split('T')[0]}.jsonl`);
991
+ const tmuxHookLog = await readFile(tmuxHookLogPath, 'utf-8').catch(() => '');
992
+ assert.equal(tmuxHookLog.trim(), '');
993
+ }
994
+ finally {
995
+ await rm(wd, { recursive: true, force: true });
996
+ }
997
+ });
588
998
  it('does not auto-nudge stalled-like output when the latest turn is still fresh', async () => {
589
999
  const wd = await mkdtemp(join(tmpdir(), 'omx-fallback-auto-nudge-fresh-'));
590
1000
  const fakeBinDir = join(wd, 'fake-bin');
@@ -674,7 +1084,59 @@ describe('notify-fallback watcher', () => {
674
1084
  assert.doesNotMatch(tmuxLog, /send-keys -t %42 -l yes, proceed \[OMX_TMUX_INJECT\]/);
675
1085
  const watcherStatePath = join(wd, '.omx', 'state', 'notify-fallback-state.json');
676
1086
  const watcherState = JSON.parse(await readFile(watcherStatePath, 'utf-8'));
677
- assert.equal(watcherState.fallback_auto_nudge?.last_reason, 'ttl_active');
1087
+ assert.equal(watcherState.fallback_auto_nudge?.last_reason, 'already_nudged_for_signature');
1088
+ assert.equal(watcherState.fallback_auto_nudge?.last_turn_count, 7);
1089
+ }
1090
+ finally {
1091
+ await rm(wd, { recursive: true, force: true });
1092
+ }
1093
+ });
1094
+ it('does not fallback auto-nudge the same stalled hud turn again after TTL expiry', async () => {
1095
+ const wd = await mkdtemp(join(tmpdir(), 'omx-fallback-auto-nudge-exact-dedup-'));
1096
+ const fakeBinDir = join(wd, 'fake-bin');
1097
+ const tmuxLogPath = join(wd, 'tmux.log');
1098
+ const codexHome = join(wd, 'codex-home');
1099
+ const lastTurnAt = '2026-03-01T00:00:00.000Z';
1100
+ const lastMessage = 'If you want, I can keep going from here.';
1101
+ try {
1102
+ await mkdir(join(wd, '.omx', 'logs'), { recursive: true });
1103
+ await mkdir(join(wd, '.omx', 'state'), { recursive: true });
1104
+ await mkdir(fakeBinDir, { recursive: true });
1105
+ await mkdir(codexHome, { recursive: true });
1106
+ await writeFile(join(fakeBinDir, 'tmux'), buildFakeTmux(tmuxLogPath));
1107
+ await chmod(join(fakeBinDir, 'tmux'), 0o755);
1108
+ await writeFile(join(codexHome, '.omx-config.json'), JSON.stringify({
1109
+ autoNudge: { enabled: true, delaySec: 0, ttlMs: 5000 },
1110
+ }, null, 2));
1111
+ await writeFile(join(wd, '.omx', 'state', 'hud-state.json'), JSON.stringify({
1112
+ last_turn_at: lastTurnAt,
1113
+ turn_count: 7,
1114
+ last_agent_output: lastMessage,
1115
+ }, null, 2));
1116
+ await writeFile(join(wd, '.omx', 'state', 'auto-nudge-state.json'), JSON.stringify({
1117
+ nudgeCount: 1,
1118
+ lastNudgeAt: '2026-03-01T00:00:10.000Z',
1119
+ lastSignature: `hud:7|${lastTurnAt}|stall:proceed_intent`,
1120
+ lastSemanticSignature: 'stall:proceed_intent',
1121
+ }, null, 2));
1122
+ const watcherScript = new URL('../../../dist/scripts/notify-fallback-watcher.js', import.meta.url).pathname;
1123
+ const notifyHook = new URL('../../../dist/scripts/notify-hook.js', import.meta.url).pathname;
1124
+ const result = spawnSync(process.execPath, [watcherScript, '--once', '--cwd', wd, '--notify-script', notifyHook, '--poll-ms', '50'], {
1125
+ encoding: 'utf-8',
1126
+ env: buildCleanNotifyEnv({
1127
+ PATH: `${fakeBinDir}:${process.env.PATH || ''}`,
1128
+ CODEX_HOME: codexHome,
1129
+ TMUX: '1',
1130
+ TMUX_PANE: '%42',
1131
+ OMX_NOTIFY_FALLBACK_AUTO_NUDGE_STALL_MS: '5000',
1132
+ }),
1133
+ });
1134
+ assert.equal(result.status, 0, result.stderr || result.stdout);
1135
+ const tmuxLog = await readFile(tmuxLogPath, 'utf8').catch(() => '');
1136
+ assert.doesNotMatch(tmuxLog, /send-keys -t %42 -l yes, proceed \[OMX_TMUX_INJECT\]/);
1137
+ const watcherStatePath = join(wd, '.omx', 'state', 'notify-fallback-state.json');
1138
+ const watcherState = JSON.parse(await readFile(watcherStatePath, 'utf-8'));
1139
+ assert.equal(watcherState.fallback_auto_nudge?.last_reason, 'already_nudged_for_signature');
678
1140
  assert.equal(watcherState.fallback_auto_nudge?.last_turn_count, 7);
679
1141
  }
680
1142
  finally {
@@ -683,7 +1145,9 @@ describe('notify-fallback watcher', () => {
683
1145
  });
684
1146
  it('runs bounded non-turn team dispatch drain tick in leader context', async () => {
685
1147
  const wd = await mkdtemp(join(tmpdir(), 'omx-fallback-dispatch-'));
1148
+ const previousRuntimeBridge = process.env.OMX_RUNTIME_BRIDGE;
686
1149
  try {
1150
+ process.env.OMX_RUNTIME_BRIDGE = '0';
687
1151
  await initTeamState('dispatch-team', 'task', 'executor', 1, wd);
688
1152
  const queued = await enqueueDispatchRequest('dispatch-team', {
689
1153
  kind: 'inbox',
@@ -700,12 +1164,18 @@ describe('notify-fallback watcher', () => {
700
1164
  assert.notEqual(request?.status, 'pending');
701
1165
  }
702
1166
  finally {
1167
+ if (typeof previousRuntimeBridge === 'string')
1168
+ process.env.OMX_RUNTIME_BRIDGE = previousRuntimeBridge;
1169
+ else
1170
+ delete process.env.OMX_RUNTIME_BRIDGE;
703
1171
  await rm(wd, { recursive: true, force: true });
704
1172
  }
705
1173
  });
706
1174
  it('skips dispatch drain in worker context (leader-only guard)', async () => {
707
1175
  const wd = await mkdtemp(join(tmpdir(), 'omx-fallback-dispatch-worker-'));
1176
+ const previousRuntimeBridge = process.env.OMX_RUNTIME_BRIDGE;
708
1177
  try {
1178
+ process.env.OMX_RUNTIME_BRIDGE = '0';
709
1179
  await initTeamState('dispatch-team', 'task', 'executor', 1, wd);
710
1180
  const queued = await enqueueDispatchRequest('dispatch-team', {
711
1181
  kind: 'inbox',
@@ -727,11 +1197,13 @@ describe('notify-fallback watcher', () => {
727
1197
  const logPath = join(wd, '.omx', 'logs', `notify-fallback-${new Date().toISOString().split('T')[0]}.jsonl`);
728
1198
  const logEntries = (await readFile(logPath, 'utf-8')).trim().split('\n').filter(Boolean).map((line) => JSON.parse(line));
729
1199
  const drainEvent = logEntries.find((entry) => entry.type === 'dispatch_drain_tick');
730
- assert.ok(drainEvent, 'expected dispatch_drain_tick log event');
731
- assert.equal(drainEvent.leader_only, false);
732
- assert.equal(drainEvent.reason, 'worker_context');
1200
+ assert.equal(drainEvent, undefined);
733
1201
  }
734
1202
  finally {
1203
+ if (typeof previousRuntimeBridge === 'string')
1204
+ process.env.OMX_RUNTIME_BRIDGE = previousRuntimeBridge;
1205
+ else
1206
+ delete process.env.OMX_RUNTIME_BRIDGE;
735
1207
  await rm(wd, { recursive: true, force: true });
736
1208
  }
737
1209
  });
@@ -740,18 +1212,20 @@ describe('notify-fallback watcher', () => {
740
1212
  const fakeBinDir = join(wd, 'fake-bin');
741
1213
  const tmuxLogPath = join(wd, 'tmux.log');
742
1214
  const captureFile = join(wd, 'capture.txt');
1215
+ const previousRuntimeBridge = process.env.OMX_RUNTIME_BRIDGE;
743
1216
  try {
1217
+ process.env.OMX_RUNTIME_BRIDGE = '0';
744
1218
  await mkdir(fakeBinDir, { recursive: true });
745
1219
  await writeFile(join(fakeBinDir, 'tmux'), buildFakeTmux(tmuxLogPath));
746
1220
  await chmod(join(fakeBinDir, 'tmux'), 0o755);
747
- await writeFile(captureFile, 'dispatch ping');
1221
+ await writeFile(captureFile, '... ping ...');
748
1222
  await initTeamState('dispatch-team', 'task', 'executor', 1, wd);
749
1223
  const queued = await enqueueDispatchRequest('dispatch-team', {
750
1224
  kind: 'inbox',
751
1225
  to_worker: 'worker-1',
752
1226
  worker_index: 1,
753
1227
  pane_id: '%42',
754
- trigger_message: 'dispatch ping',
1228
+ trigger_message: 'ping',
755
1229
  }, wd);
756
1230
  const watcherScript = new URL('../../../dist/scripts/notify-fallback-watcher.js', import.meta.url).pathname;
757
1231
  const notifyHook = new URL('../../../dist/scripts/notify-hook.js', import.meta.url).pathname;
@@ -765,14 +1239,21 @@ describe('notify-fallback watcher', () => {
765
1239
  const second = spawnSync(process.execPath, [watcherScript, '--once', '--cwd', wd, '--notify-script', notifyHook, '--poll-ms', '50', '--dispatch-max-per-tick', '1'], { encoding: 'utf-8', env });
766
1240
  assert.equal(second.status, 0, second.stderr || second.stdout);
767
1241
  const tmuxLog = await readFile(tmuxLogPath, 'utf8');
768
- const typeMatches = tmuxLog.match(/send-keys -t %42 -l dispatch ping/g) || [];
769
- assert.equal(typeMatches.length, 1, 'watcher retries should be submit-only when draft remains visible');
1242
+ const typeMatches = tmuxLog.match(/send-keys -t %42 -l ping/g) || [];
1243
+ assert.equal(typeMatches.length, 1, 'fresh attempt should type once; retries with draft should be submit-only');
1244
+ const cmMatches = tmuxLog.match(/send-keys -t %42 C-m/g) || [];
1245
+ assert.ok(cmMatches.length > 0, 'submit should use C-m');
770
1246
  assert.ok(!/send-keys[^\n]*-l[^\n]*C-m/.test(tmuxLog), 'must keep -l payload and C-m submits isolated');
771
1247
  const request = await readDispatchRequest('dispatch-team', queued.request.request_id, wd);
772
1248
  assert.equal(request?.status, 'pending');
1249
+ assert.equal(request?.attempt_count, 2);
773
1250
  assert.equal(request?.last_reason, 'tmux_send_keys_unconfirmed');
774
1251
  }
775
1252
  finally {
1253
+ if (typeof previousRuntimeBridge === 'string')
1254
+ process.env.OMX_RUNTIME_BRIDGE = previousRuntimeBridge;
1255
+ else
1256
+ delete process.env.OMX_RUNTIME_BRIDGE;
776
1257
  await rm(wd, { recursive: true, force: true });
777
1258
  }
778
1259
  });
@@ -935,6 +1416,7 @@ describe('notify-fallback watcher', () => {
935
1416
  }, null, 2));
936
1417
  await writeFile(statePath, JSON.stringify({
937
1418
  ralph_continue_steer: {
1419
+ pane_id: '%7',
938
1420
  last_sent_at: new Date(Date.now() - 61_000).toISOString(),
939
1421
  },
940
1422
  }, null, 2));
@@ -948,6 +1430,7 @@ describe('notify-fallback watcher', () => {
948
1430
  assert.equal(missingRun.status, 0, missingRun.stderr || missingRun.stdout);
949
1431
  let watcherState = JSON.parse(await readFile(statePath, 'utf-8'));
950
1432
  assert.equal(watcherState.ralph_continue_steer?.last_reason, 'progress_missing');
1433
+ assert.equal(watcherState.ralph_continue_steer?.pane_id, '%42');
951
1434
  await writeFile(join(stateDir, 'hud-state.json'), JSON.stringify({
952
1435
  last_progress_at: 'not-a-date',
953
1436
  }, null, 2));
@@ -955,6 +1438,7 @@ describe('notify-fallback watcher', () => {
955
1438
  assert.equal(invalidRun.status, 0, invalidRun.stderr || invalidRun.stdout);
956
1439
  watcherState = JSON.parse(await readFile(statePath, 'utf-8'));
957
1440
  assert.equal(watcherState.ralph_continue_steer?.last_reason, 'progress_invalid');
1441
+ assert.equal(watcherState.ralph_continue_steer?.pane_id, '%42');
958
1442
  const tmuxLog = await readFile(tmuxLogPath, 'utf8').catch(() => '');
959
1443
  const sends = tmuxLog.match(/send-keys -t %42 -l Ralph loop active continue \[OMX_TMUX_INJECT\]/g) || [];
960
1444
  assert.equal(sends.length, 0, 'missing or invalid progress should fail closed without sending steer');
@@ -1206,7 +1690,9 @@ describe('notify-fallback watcher', () => {
1206
1690
  const wd = await mkdtemp(join(tmpdir(), 'omx-fallback-control-plane-split-'));
1207
1691
  const fakeBinDir = join(wd, 'fake-bin');
1208
1692
  const tmuxLogPath = join(wd, 'tmux.log');
1693
+ const previousRuntimeBridge = process.env.OMX_RUNTIME_BRIDGE;
1209
1694
  try {
1695
+ process.env.OMX_RUNTIME_BRIDGE = '0';
1210
1696
  await mkdir(join(wd, '.omx', 'state'), { recursive: true });
1211
1697
  await mkdir(fakeBinDir, { recursive: true });
1212
1698
  await writeFile(join(fakeBinDir, 'tmux'), buildFakeTmux(tmuxLogPath, {
@@ -1259,6 +1745,10 @@ describe('notify-fallback watcher', () => {
1259
1745
  assert.ok(ralphFailureEvent, 'expected Ralph failure to be logged without aborting team control-plane pumping');
1260
1746
  }
1261
1747
  finally {
1748
+ if (typeof previousRuntimeBridge === 'string')
1749
+ process.env.OMX_RUNTIME_BRIDGE = previousRuntimeBridge;
1750
+ else
1751
+ delete process.env.OMX_RUNTIME_BRIDGE;
1262
1752
  await rm(wd, { recursive: true, force: true });
1263
1753
  }
1264
1754
  });
@@ -1268,7 +1758,9 @@ describe('notify-fallback watcher', () => {
1268
1758
  const tmuxLogPath = join(wd, 'tmux.log');
1269
1759
  const captureSeqFile = join(wd, 'capture-seq.txt');
1270
1760
  const captureCounterFile = join(wd, 'capture-seq.idx');
1761
+ const previousRuntimeBridge = process.env.OMX_RUNTIME_BRIDGE;
1271
1762
  try {
1763
+ process.env.OMX_RUNTIME_BRIDGE = '0';
1272
1764
  await mkdir(fakeBinDir, { recursive: true });
1273
1765
  await writeFile(join(fakeBinDir, 'tmux'), buildFakeTmux(tmuxLogPath));
1274
1766
  await chmod(join(fakeBinDir, 'tmux'), 0o755);
@@ -1277,11 +1769,11 @@ describe('notify-fallback watcher', () => {
1277
1769
  // (no trigger) so the request is retyped on every retry.
1278
1770
  await writeFile(captureSeqFile, [
1279
1771
  // Run 1 (attempt 0): 1 shared preflight + 3 verify rounds × 2 captures = 7
1280
- 'ready', 'dispatch ping', 'dispatch ping', 'dispatch ping', 'dispatch ping', 'dispatch ping', 'dispatch ping',
1772
+ 'ready', 'ping', 'ping', 'ping', 'ping', 'ping', 'ping',
1281
1773
  // Run 2 (attempt 1): 1 shared preflight + 1 pre-capture + 3 verify rounds × 2 captures = 8
1282
- 'ready', 'ready', 'dispatch ping', 'dispatch ping', 'dispatch ping', 'dispatch ping', 'dispatch ping', 'dispatch ping',
1774
+ 'ready', 'ready', 'ping', 'ping', 'ping', 'ping', 'ping', 'ping',
1283
1775
  // Run 3 (attempt 2): 1 shared preflight + 1 pre-capture + 3 verify rounds × 2 captures = 8
1284
- 'ready', 'ready', 'dispatch ping', 'dispatch ping', 'dispatch ping', 'dispatch ping', 'dispatch ping', 'dispatch ping',
1776
+ 'ready', 'ready', 'ping', 'ping', 'ping', 'ping', 'ping', 'ping',
1285
1777
  ].join('\n'));
1286
1778
  await initTeamState('dispatch-team', 'task', 'executor', 1, wd);
1287
1779
  const queued = await enqueueDispatchRequest('dispatch-team', {
@@ -1289,7 +1781,7 @@ describe('notify-fallback watcher', () => {
1289
1781
  to_worker: 'worker-1',
1290
1782
  worker_index: 1,
1291
1783
  pane_id: '%42',
1292
- trigger_message: 'dispatch ping',
1784
+ trigger_message: 'ping',
1293
1785
  }, wd);
1294
1786
  const watcherScript = new URL('../../../dist/scripts/notify-fallback-watcher.js', import.meta.url).pathname;
1295
1787
  const notifyHook = new URL('../../../dist/scripts/notify-hook.js', import.meta.url).pathname;
@@ -1304,13 +1796,17 @@ describe('notify-fallback watcher', () => {
1304
1796
  assert.equal(run.status, 0, run.stderr || run.stdout);
1305
1797
  }
1306
1798
  const tmuxLog = await readFile(tmuxLogPath, 'utf8');
1307
- const typeMatches = tmuxLog.match(/send-keys -t %42 -l dispatch ping/g) || [];
1308
- assert.equal(typeMatches.length, 3, 'initial + retype on every retry when trigger absent from narrow area');
1799
+ const typeMatches = tmuxLog.match(/send-keys -t %42 -l ping/g) || [];
1800
+ assert.equal(typeMatches.length, 3, 'should retype on every retry when trigger not in narrow capture (fresh + 2 retries)');
1309
1801
  const request = await readDispatchRequest('dispatch-team', queued.request.request_id, wd);
1310
1802
  assert.equal(request?.status, 'failed');
1311
1803
  assert.equal(request?.last_reason, 'unconfirmed_after_max_retries');
1312
1804
  }
1313
1805
  finally {
1806
+ if (typeof previousRuntimeBridge === 'string')
1807
+ process.env.OMX_RUNTIME_BRIDGE = previousRuntimeBridge;
1808
+ else
1809
+ delete process.env.OMX_RUNTIME_BRIDGE;
1314
1810
  await rm(wd, { recursive: true, force: true });
1315
1811
  }
1316
1812
  });
@@ -1806,5 +2302,45 @@ describe('notify-fallback watcher', () => {
1806
2302
  await rm(tempHome, { recursive: true, force: true });
1807
2303
  }
1808
2304
  });
2305
+ it('keeps the detached helper alive after the hidden bootstrap exits', async () => {
2306
+ const wd = await mkdtemp(join(tmpdir(), 'omx-fallback-bootstrap-survival-'));
2307
+ const readyPath = join(wd, 'helper-ready.json');
2308
+ const helperScriptPath = join(wd, 'helper-survival.js');
2309
+ try {
2310
+ await writeFile(helperScriptPath, `
2311
+ const fs = require('node:fs');
2312
+ const readyPath = process.argv[2];
2313
+ fs.writeFileSync(readyPath, JSON.stringify({ pid: process.pid, started_at: new Date().toISOString() }));
2314
+ setInterval(() => {}, 1000);
2315
+ `);
2316
+ const bootstrap = spawnSync(process.execPath, [
2317
+ '-e',
2318
+ buildWindowsMsysBackgroundHelperBootstrapScript([helperScriptPath, readyPath], wd),
2319
+ ], {
2320
+ cwd: wd,
2321
+ encoding: 'utf-8',
2322
+ stdio: ['ignore', 'pipe', 'pipe'],
2323
+ windowsHide: true,
2324
+ });
2325
+ assert.equal(bootstrap.status, 0, bootstrap.stderr || bootstrap.stdout);
2326
+ const helperPid = Number.parseInt((bootstrap.stdout || '').trim(), 10);
2327
+ assert.ok(Number.isFinite(helperPid) && helperPid > 0, 'expected detached helper pid from bootstrap');
2328
+ await waitFor(async () => {
2329
+ try {
2330
+ const ready = JSON.parse(await readFile(readyPath, 'utf-8'));
2331
+ return ready.pid === helperPid;
2332
+ }
2333
+ catch {
2334
+ return false;
2335
+ }
2336
+ }, 4000, 50);
2337
+ assert.ok(isPidAlive(helperPid), 'expected detached helper to survive after bootstrap exit');
2338
+ process.kill(helperPid, 'SIGTERM');
2339
+ await waitFor(async () => !isPidAlive(helperPid), 4000, 50);
2340
+ }
2341
+ finally {
2342
+ await rm(wd, { recursive: true, force: true });
2343
+ }
2344
+ });
1809
2345
  });
1810
2346
  //# sourceMappingURL=notify-fallback-watcher.test.js.map