oh-my-codex 0.17.3 → 0.18.1

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 (381) hide show
  1. package/Cargo.lock +13 -5
  2. package/Cargo.toml +2 -1
  3. package/README.md +44 -19
  4. package/crates/omx-api/Cargo.toml +19 -0
  5. package/crates/omx-api/src/lib.rs +2997 -0
  6. package/crates/omx-api/src/main.rs +10 -0
  7. package/crates/omx-api/tests/cli.rs +558 -0
  8. package/crates/omx-explore/src/main.rs +4 -0
  9. package/crates/omx-sparkshell/src/codex_bridge.rs +437 -123
  10. package/crates/omx-sparkshell/src/exec.rs +127 -1
  11. package/crates/omx-sparkshell/src/main.rs +829 -30
  12. package/crates/omx-sparkshell/src/prompt.rs +25 -3
  13. package/crates/omx-sparkshell/src/redaction.rs +241 -0
  14. package/crates/omx-sparkshell/tests/execution.rs +702 -237
  15. package/dist/cli/__tests__/api.test.d.ts +2 -0
  16. package/dist/cli/__tests__/api.test.d.ts.map +1 -0
  17. package/dist/cli/__tests__/api.test.js +175 -0
  18. package/dist/cli/__tests__/api.test.js.map +1 -0
  19. package/dist/cli/__tests__/ask.test.js +72 -5
  20. package/dist/cli/__tests__/ask.test.js.map +1 -1
  21. package/dist/cli/__tests__/autoresearch-goal.test.js +14 -1
  22. package/dist/cli/__tests__/autoresearch-goal.test.js.map +1 -1
  23. package/dist/cli/__tests__/codex-plugin-layout.test.js +15 -7
  24. package/dist/cli/__tests__/codex-plugin-layout.test.js.map +1 -1
  25. package/dist/cli/__tests__/doctor-warning-copy.test.js +76 -3
  26. package/dist/cli/__tests__/doctor-warning-copy.test.js.map +1 -1
  27. package/dist/cli/__tests__/explore.test.js +23 -0
  28. package/dist/cli/__tests__/explore.test.js.map +1 -1
  29. package/dist/cli/__tests__/index.test.js +171 -5
  30. package/dist/cli/__tests__/index.test.js.map +1 -1
  31. package/dist/cli/__tests__/install-docs-contract.test.d.ts +2 -0
  32. package/dist/cli/__tests__/install-docs-contract.test.d.ts.map +1 -0
  33. package/dist/cli/__tests__/install-docs-contract.test.js +55 -0
  34. package/dist/cli/__tests__/install-docs-contract.test.js.map +1 -0
  35. package/dist/cli/__tests__/launch-fallback.test.js +191 -0
  36. package/dist/cli/__tests__/launch-fallback.test.js.map +1 -1
  37. package/dist/cli/__tests__/package-bin-contract.test.js +4 -3
  38. package/dist/cli/__tests__/package-bin-contract.test.js.map +1 -1
  39. package/dist/cli/__tests__/question.test.js +27 -41
  40. package/dist/cli/__tests__/question.test.js.map +1 -1
  41. package/dist/cli/__tests__/setup-install-mode.test.js +232 -35
  42. package/dist/cli/__tests__/setup-install-mode.test.js.map +1 -1
  43. package/dist/cli/__tests__/sparkshell-cli.test.js +25 -1
  44. package/dist/cli/__tests__/sparkshell-cli.test.js.map +1 -1
  45. package/dist/cli/__tests__/sparkshell-packaging.test.js +1 -0
  46. package/dist/cli/__tests__/sparkshell-packaging.test.js.map +1 -1
  47. package/dist/cli/__tests__/ultragoal.test.js +227 -4
  48. package/dist/cli/__tests__/ultragoal.test.js.map +1 -1
  49. package/dist/cli/__tests__/update.test.js +72 -1
  50. package/dist/cli/__tests__/update.test.js.map +1 -1
  51. package/dist/cli/__tests__/version-sync-contract.test.js +4 -0
  52. package/dist/cli/__tests__/version-sync-contract.test.js.map +1 -1
  53. package/dist/cli/__tests__/windows-popup-loop-contract.test.js +1 -1
  54. package/dist/cli/__tests__/windows-popup-loop-contract.test.js.map +1 -1
  55. package/dist/cli/api.d.ts +26 -0
  56. package/dist/cli/api.d.ts.map +1 -0
  57. package/dist/cli/api.js +153 -0
  58. package/dist/cli/api.js.map +1 -0
  59. package/dist/cli/codex-feature-probe.d.ts +5 -0
  60. package/dist/cli/codex-feature-probe.d.ts.map +1 -1
  61. package/dist/cli/codex-feature-probe.js +13 -7
  62. package/dist/cli/codex-feature-probe.js.map +1 -1
  63. package/dist/cli/doctor.d.ts +7 -0
  64. package/dist/cli/doctor.d.ts.map +1 -1
  65. package/dist/cli/doctor.js +119 -10
  66. package/dist/cli/doctor.js.map +1 -1
  67. package/dist/cli/explore.d.ts +2 -0
  68. package/dist/cli/explore.d.ts.map +1 -1
  69. package/dist/cli/explore.js +43 -1
  70. package/dist/cli/explore.js.map +1 -1
  71. package/dist/cli/index.d.ts +12 -4
  72. package/dist/cli/index.d.ts.map +1 -1
  73. package/dist/cli/index.js +460 -87
  74. package/dist/cli/index.js.map +1 -1
  75. package/dist/cli/native-assets.d.ts +2 -1
  76. package/dist/cli/native-assets.d.ts.map +1 -1
  77. package/dist/cli/native-assets.js +1 -0
  78. package/dist/cli/native-assets.js.map +1 -1
  79. package/dist/cli/plugin-marketplace.d.ts +2 -0
  80. package/dist/cli/plugin-marketplace.d.ts.map +1 -1
  81. package/dist/cli/plugin-marketplace.js +15 -1
  82. package/dist/cli/plugin-marketplace.js.map +1 -1
  83. package/dist/cli/setup.d.ts.map +1 -1
  84. package/dist/cli/setup.js +71 -11
  85. package/dist/cli/setup.js.map +1 -1
  86. package/dist/cli/sparkshell.d.ts +7 -1
  87. package/dist/cli/sparkshell.d.ts.map +1 -1
  88. package/dist/cli/sparkshell.js +31 -4
  89. package/dist/cli/sparkshell.js.map +1 -1
  90. package/dist/cli/ultragoal.d.ts +1 -1
  91. package/dist/cli/ultragoal.d.ts.map +1 -1
  92. package/dist/cli/ultragoal.js +184 -10
  93. package/dist/cli/ultragoal.js.map +1 -1
  94. package/dist/cli/update.d.ts +2 -0
  95. package/dist/cli/update.d.ts.map +1 -1
  96. package/dist/cli/update.js +14 -3
  97. package/dist/cli/update.js.map +1 -1
  98. package/dist/compat/__tests__/doctor-contract.test.js +3 -0
  99. package/dist/compat/__tests__/doctor-contract.test.js.map +1 -1
  100. package/dist/config/__tests__/codex-feature-flags.test.js +11 -1
  101. package/dist/config/__tests__/codex-feature-flags.test.js.map +1 -1
  102. package/dist/config/__tests__/codex-hooks.test.js +19 -8
  103. package/dist/config/__tests__/codex-hooks.test.js.map +1 -1
  104. package/dist/config/__tests__/commit-lore-guard.test.d.ts +2 -0
  105. package/dist/config/__tests__/commit-lore-guard.test.d.ts.map +1 -0
  106. package/dist/config/__tests__/commit-lore-guard.test.js +20 -0
  107. package/dist/config/__tests__/commit-lore-guard.test.js.map +1 -0
  108. package/dist/config/codex-feature-flags.d.ts +4 -0
  109. package/dist/config/codex-feature-flags.d.ts.map +1 -1
  110. package/dist/config/codex-feature-flags.js +4 -0
  111. package/dist/config/codex-feature-flags.js.map +1 -1
  112. package/dist/config/codex-hooks.js +6 -6
  113. package/dist/config/codex-hooks.js.map +1 -1
  114. package/dist/config/commit-lore-guard.d.ts +1 -0
  115. package/dist/config/commit-lore-guard.d.ts.map +1 -1
  116. package/dist/config/commit-lore-guard.js +29 -3
  117. package/dist/config/commit-lore-guard.js.map +1 -1
  118. package/dist/config/generator.d.ts +3 -1
  119. package/dist/config/generator.d.ts.map +1 -1
  120. package/dist/config/generator.js +114 -10
  121. package/dist/config/generator.js.map +1 -1
  122. package/dist/goal-workflows/codex-goal-snapshot.d.ts +1 -0
  123. package/dist/goal-workflows/codex-goal-snapshot.d.ts.map +1 -1
  124. package/dist/goal-workflows/codex-goal-snapshot.js +5 -1
  125. package/dist/goal-workflows/codex-goal-snapshot.js.map +1 -1
  126. package/dist/hooks/__tests__/autopilot-skill-contract.test.js +10 -6
  127. package/dist/hooks/__tests__/autopilot-skill-contract.test.js.map +1 -1
  128. package/dist/hooks/__tests__/best-practice-research-skill.test.d.ts +2 -0
  129. package/dist/hooks/__tests__/best-practice-research-skill.test.d.ts.map +1 -0
  130. package/dist/hooks/__tests__/best-practice-research-skill.test.js +27 -0
  131. package/dist/hooks/__tests__/best-practice-research-skill.test.js.map +1 -0
  132. package/dist/hooks/__tests__/consensus-execution-handoff.test.d.ts +1 -1
  133. package/dist/hooks/__tests__/consensus-execution-handoff.test.js +13 -11
  134. package/dist/hooks/__tests__/consensus-execution-handoff.test.js.map +1 -1
  135. package/dist/hooks/__tests__/deep-interview-contract.test.js +4 -3
  136. package/dist/hooks/__tests__/deep-interview-contract.test.js.map +1 -1
  137. package/dist/hooks/__tests__/keyword-detector.test.js +15 -3
  138. package/dist/hooks/__tests__/keyword-detector.test.js.map +1 -1
  139. package/dist/hooks/__tests__/notify-hook-team-leader-nudge.test.js +6 -0
  140. package/dist/hooks/__tests__/notify-hook-team-leader-nudge.test.js.map +1 -1
  141. package/dist/hooks/__tests__/notify-hook-team-tmux-guard.test.js +33 -0
  142. package/dist/hooks/__tests__/notify-hook-team-tmux-guard.test.js.map +1 -1
  143. package/dist/hooks/__tests__/prompt-guidance-wave-two.test.js +4 -0
  144. package/dist/hooks/__tests__/prompt-guidance-wave-two.test.js.map +1 -1
  145. package/dist/hooks/extensibility/__tests__/dispatcher.test.js +26 -3
  146. package/dist/hooks/extensibility/__tests__/dispatcher.test.js.map +1 -1
  147. package/dist/hooks/extensibility/dispatcher.d.ts.map +1 -1
  148. package/dist/hooks/extensibility/dispatcher.js +29 -14
  149. package/dist/hooks/extensibility/dispatcher.js.map +1 -1
  150. package/dist/hooks/keyword-detector.d.ts.map +1 -1
  151. package/dist/hooks/keyword-detector.js +8 -3
  152. package/dist/hooks/keyword-detector.js.map +1 -1
  153. package/dist/hooks/keyword-registry.d.ts.map +1 -1
  154. package/dist/hooks/keyword-registry.js +1 -0
  155. package/dist/hooks/keyword-registry.js.map +1 -1
  156. package/dist/hooks/prompt-guidance-contract.d.ts.map +1 -1
  157. package/dist/hooks/prompt-guidance-contract.js +3 -2
  158. package/dist/hooks/prompt-guidance-contract.js.map +1 -1
  159. package/dist/hud/__tests__/hud-tmux-injection.test.js +14 -8
  160. package/dist/hud/__tests__/hud-tmux-injection.test.js.map +1 -1
  161. package/dist/hud/__tests__/reconcile.test.js +4 -4
  162. package/dist/hud/__tests__/reconcile.test.js.map +1 -1
  163. package/dist/hud/__tests__/resource-leak-watch.test.d.ts +2 -0
  164. package/dist/hud/__tests__/resource-leak-watch.test.d.ts.map +1 -0
  165. package/dist/hud/__tests__/resource-leak-watch.test.js +28 -0
  166. package/dist/hud/__tests__/resource-leak-watch.test.js.map +1 -0
  167. package/dist/hud/__tests__/tmux.test.js +23 -18
  168. package/dist/hud/__tests__/tmux.test.js.map +1 -1
  169. package/dist/hud/index.d.ts +1 -1
  170. package/dist/hud/index.d.ts.map +1 -1
  171. package/dist/hud/index.js +10 -4
  172. package/dist/hud/index.js.map +1 -1
  173. package/dist/hud/tmux.d.ts.map +1 -1
  174. package/dist/hud/tmux.js +9 -8
  175. package/dist/hud/tmux.js.map +1 -1
  176. package/dist/mcp/__tests__/bootstrap.test.js +75 -1
  177. package/dist/mcp/__tests__/bootstrap.test.js.map +1 -1
  178. package/dist/mcp/bootstrap.d.ts +3 -1
  179. package/dist/mcp/bootstrap.d.ts.map +1 -1
  180. package/dist/mcp/bootstrap.js +71 -2
  181. package/dist/mcp/bootstrap.js.map +1 -1
  182. package/dist/notifications/__tests__/http-client-resource.test.d.ts +2 -0
  183. package/dist/notifications/__tests__/http-client-resource.test.d.ts.map +1 -0
  184. package/dist/notifications/__tests__/http-client-resource.test.js +41 -0
  185. package/dist/notifications/__tests__/http-client-resource.test.js.map +1 -0
  186. package/dist/notifications/__tests__/verbosity.test.js +20 -0
  187. package/dist/notifications/__tests__/verbosity.test.js.map +1 -1
  188. package/dist/notifications/config.d.ts.map +1 -1
  189. package/dist/notifications/config.js +6 -3
  190. package/dist/notifications/config.js.map +1 -1
  191. package/dist/notifications/http-client.d.ts.map +1 -1
  192. package/dist/notifications/http-client.js +78 -27
  193. package/dist/notifications/http-client.js.map +1 -1
  194. package/dist/notifications/types.d.ts +2 -0
  195. package/dist/notifications/types.d.ts.map +1 -1
  196. package/dist/openclaw/__tests__/dispatcher.test.js +49 -1
  197. package/dist/openclaw/__tests__/dispatcher.test.js.map +1 -1
  198. package/dist/openclaw/dispatcher.d.ts +7 -4
  199. package/dist/openclaw/dispatcher.d.ts.map +1 -1
  200. package/dist/openclaw/dispatcher.js +32 -69
  201. package/dist/openclaw/dispatcher.js.map +1 -1
  202. package/dist/pipeline/__tests__/orchestrator.test.js +65 -3
  203. package/dist/pipeline/__tests__/orchestrator.test.js.map +1 -1
  204. package/dist/pipeline/__tests__/stages.test.js +50 -5
  205. package/dist/pipeline/__tests__/stages.test.js.map +1 -1
  206. package/dist/pipeline/index.d.ts +8 -2
  207. package/dist/pipeline/index.d.ts.map +1 -1
  208. package/dist/pipeline/index.js +5 -2
  209. package/dist/pipeline/index.js.map +1 -1
  210. package/dist/pipeline/orchestrator.d.ts +5 -4
  211. package/dist/pipeline/orchestrator.d.ts.map +1 -1
  212. package/dist/pipeline/orchestrator.js +56 -15
  213. package/dist/pipeline/orchestrator.js.map +1 -1
  214. package/dist/pipeline/stages/code-review.d.ts +2 -2
  215. package/dist/pipeline/stages/code-review.d.ts.map +1 -1
  216. package/dist/pipeline/stages/code-review.js +5 -3
  217. package/dist/pipeline/stages/code-review.js.map +1 -1
  218. package/dist/pipeline/stages/deep-interview.d.ts +15 -0
  219. package/dist/pipeline/stages/deep-interview.d.ts.map +1 -0
  220. package/dist/pipeline/stages/deep-interview.js +32 -0
  221. package/dist/pipeline/stages/deep-interview.js.map +1 -0
  222. package/dist/pipeline/stages/ralph-verify.d.ts +5 -5
  223. package/dist/pipeline/stages/ralph-verify.d.ts.map +1 -1
  224. package/dist/pipeline/stages/ralph-verify.js +2 -2
  225. package/dist/pipeline/stages/ralph-verify.js.map +1 -1
  226. package/dist/pipeline/stages/ultragoal.d.ts +19 -0
  227. package/dist/pipeline/stages/ultragoal.d.ts.map +1 -0
  228. package/dist/pipeline/stages/ultragoal.js +38 -0
  229. package/dist/pipeline/stages/ultragoal.js.map +1 -0
  230. package/dist/pipeline/stages/ultraqa.d.ts +30 -0
  231. package/dist/pipeline/stages/ultraqa.d.ts.map +1 -0
  232. package/dist/pipeline/stages/ultraqa.js +46 -0
  233. package/dist/pipeline/stages/ultraqa.js.map +1 -0
  234. package/dist/pipeline/types.d.ts +8 -6
  235. package/dist/pipeline/types.d.ts.map +1 -1
  236. package/dist/pipeline/types.js +2 -2
  237. package/dist/scripts/__tests__/codex-native-hook.test.js +1488 -117
  238. package/dist/scripts/__tests__/codex-native-hook.test.js.map +1 -1
  239. package/dist/scripts/__tests__/notify-dispatcher.test.js +183 -1
  240. package/dist/scripts/__tests__/notify-dispatcher.test.js.map +1 -1
  241. package/dist/scripts/__tests__/smoke-packed-install.test.js +27 -2
  242. package/dist/scripts/__tests__/smoke-packed-install.test.js.map +1 -1
  243. package/dist/scripts/__tests__/verify-native-agents.test.js +16 -1
  244. package/dist/scripts/__tests__/verify-native-agents.test.js.map +1 -1
  245. package/dist/scripts/build-api.d.ts +2 -0
  246. package/dist/scripts/build-api.d.ts.map +1 -0
  247. package/dist/scripts/build-api.js +44 -0
  248. package/dist/scripts/build-api.js.map +1 -0
  249. package/dist/scripts/cleanup-explore-harness.js +1 -0
  250. package/dist/scripts/cleanup-explore-harness.js.map +1 -1
  251. package/dist/scripts/codex-native-hook.d.ts.map +1 -1
  252. package/dist/scripts/codex-native-hook.js +364 -16
  253. package/dist/scripts/codex-native-hook.js.map +1 -1
  254. package/dist/scripts/codex-native-pre-post.d.ts.map +1 -1
  255. package/dist/scripts/codex-native-pre-post.js +98 -25
  256. package/dist/scripts/codex-native-pre-post.js.map +1 -1
  257. package/dist/scripts/notify-dispatcher.js +88 -0
  258. package/dist/scripts/notify-dispatcher.js.map +1 -1
  259. package/dist/scripts/notify-hook/process-runner.d.ts.map +1 -1
  260. package/dist/scripts/notify-hook/process-runner.js +39 -17
  261. package/dist/scripts/notify-hook/process-runner.js.map +1 -1
  262. package/dist/scripts/notify-hook/team-dispatch.d.ts.map +1 -1
  263. package/dist/scripts/notify-hook/team-dispatch.js +36 -14
  264. package/dist/scripts/notify-hook/team-dispatch.js.map +1 -1
  265. package/dist/scripts/notify-hook/team-leader-nudge.d.ts.map +1 -1
  266. package/dist/scripts/notify-hook/team-leader-nudge.js +26 -11
  267. package/dist/scripts/notify-hook/team-leader-nudge.js.map +1 -1
  268. package/dist/scripts/notify-hook/team-tmux-guard.d.ts +2 -1
  269. package/dist/scripts/notify-hook/team-tmux-guard.d.ts.map +1 -1
  270. package/dist/scripts/notify-hook/team-tmux-guard.js +45 -1
  271. package/dist/scripts/notify-hook/team-tmux-guard.js.map +1 -1
  272. package/dist/scripts/notify-hook/team-worker-stop.d.ts.map +1 -1
  273. package/dist/scripts/notify-hook/team-worker-stop.js +27 -14
  274. package/dist/scripts/notify-hook/team-worker-stop.js.map +1 -1
  275. package/dist/scripts/run-provider-advisor.js +9 -3
  276. package/dist/scripts/run-provider-advisor.js.map +1 -1
  277. package/dist/scripts/smoke-packed-install.d.ts +4 -1
  278. package/dist/scripts/smoke-packed-install.d.ts.map +1 -1
  279. package/dist/scripts/smoke-packed-install.js +101 -1
  280. package/dist/scripts/smoke-packed-install.js.map +1 -1
  281. package/dist/scripts/sync-plugin-mirror.js +2 -2
  282. package/dist/scripts/sync-plugin-mirror.js.map +1 -1
  283. package/dist/scripts/verify-native-agents.js +2 -2
  284. package/dist/scripts/verify-native-agents.js.map +1 -1
  285. package/dist/sidecar/__tests__/resource-leak-watch.test.d.ts +2 -0
  286. package/dist/sidecar/__tests__/resource-leak-watch.test.d.ts.map +1 -0
  287. package/dist/sidecar/__tests__/resource-leak-watch.test.js +38 -0
  288. package/dist/sidecar/__tests__/resource-leak-watch.test.js.map +1 -0
  289. package/dist/sidecar/index.d.ts +1 -1
  290. package/dist/sidecar/index.d.ts.map +1 -1
  291. package/dist/sidecar/index.js +29 -12
  292. package/dist/sidecar/index.js.map +1 -1
  293. package/dist/state/__tests__/operations-ralph-phase.test.js +88 -1
  294. package/dist/state/__tests__/operations-ralph-phase.test.js.map +1 -1
  295. package/dist/state/operations.d.ts.map +1 -1
  296. package/dist/state/operations.js +11 -0
  297. package/dist/state/operations.js.map +1 -1
  298. package/dist/team/__tests__/runtime.test.js +2 -2
  299. package/dist/team/__tests__/runtime.test.js.map +1 -1
  300. package/dist/team/__tests__/tmux-session.test.js +207 -22
  301. package/dist/team/__tests__/tmux-session.test.js.map +1 -1
  302. package/dist/team/tmux-session.d.ts +1 -0
  303. package/dist/team/tmux-session.d.ts.map +1 -1
  304. package/dist/team/tmux-session.js +73 -28
  305. package/dist/team/tmux-session.js.map +1 -1
  306. package/dist/ultragoal/__tests__/artifacts.test.js +714 -10
  307. package/dist/ultragoal/__tests__/artifacts.test.js.map +1 -1
  308. package/dist/ultragoal/__tests__/docs-contract.test.js +57 -1
  309. package/dist/ultragoal/__tests__/docs-contract.test.js.map +1 -1
  310. package/dist/ultragoal/__tests__/steering-fixtures.d.ts +68 -0
  311. package/dist/ultragoal/__tests__/steering-fixtures.d.ts.map +1 -0
  312. package/dist/ultragoal/__tests__/steering-fixtures.js +259 -0
  313. package/dist/ultragoal/__tests__/steering-fixtures.js.map +1 -0
  314. package/dist/ultragoal/__tests__/steering-fixtures.test.d.ts +2 -0
  315. package/dist/ultragoal/__tests__/steering-fixtures.test.d.ts.map +1 -0
  316. package/dist/ultragoal/__tests__/steering-fixtures.test.js +65 -0
  317. package/dist/ultragoal/__tests__/steering-fixtures.test.js.map +1 -0
  318. package/dist/ultragoal/artifacts.d.ts +97 -2
  319. package/dist/ultragoal/artifacts.d.ts.map +1 -1
  320. package/dist/ultragoal/artifacts.js +811 -256
  321. package/dist/ultragoal/artifacts.js.map +1 -1
  322. package/dist/utils/__tests__/sleep-resource.test.d.ts +2 -0
  323. package/dist/utils/__tests__/sleep-resource.test.d.ts.map +1 -0
  324. package/dist/utils/__tests__/sleep-resource.test.js +39 -0
  325. package/dist/utils/__tests__/sleep-resource.test.js.map +1 -0
  326. package/dist/utils/sleep.d.ts.map +1 -1
  327. package/dist/utils/sleep.js +17 -6
  328. package/dist/utils/sleep.js.map +1 -1
  329. package/dist/verification/__tests__/ci-rust-gates.test.js +85 -10
  330. package/dist/verification/__tests__/ci-rust-gates.test.js.map +1 -1
  331. package/dist/verification/__tests__/explore-harness-release-workflow.test.js +1 -0
  332. package/dist/verification/__tests__/explore-harness-release-workflow.test.js.map +1 -1
  333. package/package.json +5 -3
  334. package/plugins/oh-my-codex/.codex-plugin/plugin.json +4 -3
  335. package/plugins/oh-my-codex/hooks/codex-native-hook.mjs +56 -0
  336. package/plugins/oh-my-codex/hooks/hooks.json +77 -0
  337. package/plugins/oh-my-codex/skills/autopilot/SKILL.md +77 -47
  338. package/plugins/oh-my-codex/skills/best-practice-research/SKILL.md +83 -0
  339. package/plugins/oh-my-codex/skills/cancel/SKILL.md +2 -2
  340. package/plugins/oh-my-codex/skills/deep-interview/SKILL.md +9 -8
  341. package/plugins/oh-my-codex/skills/omx-setup/SKILL.md +1 -1
  342. package/plugins/oh-my-codex/skills/pipeline/SKILL.md +22 -11
  343. package/plugins/oh-my-codex/skills/plan/SKILL.md +8 -8
  344. package/plugins/oh-my-codex/skills/ralph/SKILL.md +7 -0
  345. package/plugins/oh-my-codex/skills/ralplan/SKILL.md +5 -5
  346. package/plugins/oh-my-codex/skills/team/SKILL.md +1 -1
  347. package/plugins/oh-my-codex/skills/ultragoal/SKILL.md +38 -4
  348. package/plugins/oh-my-codex/skills/ultrawork/SKILL.md +1 -1
  349. package/prompts/planner.md +1 -1
  350. package/prompts/researcher.md +15 -10
  351. package/skills/autopilot/SKILL.md +77 -47
  352. package/skills/best-practice-research/SKILL.md +83 -0
  353. package/skills/cancel/SKILL.md +2 -2
  354. package/skills/deep-interview/SKILL.md +9 -8
  355. package/skills/omx-setup/SKILL.md +1 -1
  356. package/skills/pipeline/SKILL.md +22 -11
  357. package/skills/plan/SKILL.md +8 -8
  358. package/skills/ralph/SKILL.md +7 -0
  359. package/skills/ralplan/SKILL.md +5 -5
  360. package/skills/team/SKILL.md +1 -1
  361. package/skills/ultragoal/SKILL.md +38 -4
  362. package/skills/ultrawork/SKILL.md +1 -1
  363. package/src/scripts/__tests__/codex-native-hook.test.ts +1758 -166
  364. package/src/scripts/__tests__/notify-dispatcher.test.ts +223 -1
  365. package/src/scripts/__tests__/smoke-packed-install.test.ts +39 -2
  366. package/src/scripts/__tests__/verify-native-agents.test.ts +21 -1
  367. package/src/scripts/build-api.ts +48 -0
  368. package/src/scripts/cleanup-explore-harness.ts +1 -0
  369. package/src/scripts/codex-native-hook.ts +416 -18
  370. package/src/scripts/codex-native-pre-post.ts +119 -25
  371. package/src/scripts/notify-dispatcher.ts +97 -0
  372. package/src/scripts/notify-hook/process-runner.ts +40 -16
  373. package/src/scripts/notify-hook/team-dispatch.ts +36 -13
  374. package/src/scripts/notify-hook/team-leader-nudge.ts +25 -11
  375. package/src/scripts/notify-hook/team-tmux-guard.ts +49 -0
  376. package/src/scripts/notify-hook/team-worker-stop.ts +24 -13
  377. package/src/scripts/run-provider-advisor.ts +11 -3
  378. package/src/scripts/smoke-packed-install.ts +107 -0
  379. package/src/scripts/sync-plugin-mirror.ts +3 -3
  380. package/src/scripts/verify-native-agents.ts +2 -2
  381. package/templates/catalog-manifest.json +7 -0
package/dist/cli/index.js CHANGED
@@ -7,6 +7,7 @@ import { basename, dirname, join, posix, win32 } from "path";
7
7
  import { existsSync, mkdirSync, readFileSync, rmSync, statSync, writeFileSync } from "fs";
8
8
  import { copyFile, cp, lstat, mkdir, readFile, readdir, rm, symlink, writeFile } from "fs/promises";
9
9
  import { constants as osConstants, homedir } from "os";
10
+ import { createHash } from "crypto";
10
11
  import { setup, SETUP_MCP_MODES, SETUP_SCOPES, } from "./setup.js";
11
12
  import { uninstall } from "./uninstall.js";
12
13
  import { version } from "./version.js";
@@ -24,6 +25,7 @@ import { stateCommand } from "./state.js";
24
25
  import { cleanupCommand, cleanupOmxMcpProcesses, findLaunchSafeCleanupCandidates, } from "./cleanup.js";
25
26
  import { exploreCommand } from "./explore.js";
26
27
  import { sparkshellCommand } from "./sparkshell.js";
28
+ import { apiCommand } from "./api.js";
27
29
  import { agentsInitCommand } from "./agents-init.js";
28
30
  import { agentsCommand } from "./agents.js";
29
31
  import { sessionCommand } from "./session-search.js";
@@ -51,7 +53,7 @@ import { cleanCodexModelAvailabilityNuxIfNeeded, extractSharedMcpRegistryServers
51
53
  import { OMX_FIRST_PARTY_MCP_SERVER_NAMES } from "../config/omx-first-party-mcp.js";
52
54
  import { HUD_TMUX_HEIGHT_LINES } from "../hud/constants.js";
53
55
  import { OMX_TMUX_HUD_OWNER_ENV } from "../hud/reconcile.js";
54
- import { createHudWatchPane as createSharedHudWatchPane, killTmuxPane as killSharedTmuxPane, listCurrentWindowHudPaneIds, parsePaneIdFromTmuxOutput, } from "../hud/tmux.js";
56
+ import { createHudWatchPane as createSharedHudWatchPane, killTmuxPane as killSharedTmuxPane, listCurrentWindowHudPaneIds, parsePaneIdFromTmuxOutput, registerHudResizeHook, } from "../hud/tmux.js";
55
57
  export { parseTmuxPaneSnapshot, isHudWatchPane, findHudWatchPaneIds } from "../hud/tmux.js";
56
58
  rememberOmxLaunchContext();
57
59
  import { classifySpawnError, resolveTmuxBinaryForPlatform, spawnPlatformCommandSync, } from "../utils/platform-command.js";
@@ -98,6 +100,7 @@ Usage:
98
100
  omx adapt Scaffold OMX-owned adapter foundations for persistent external targets
99
101
  omx resume Resume a previous interactive Codex session
100
102
  omx explore Default read-only exploration entrypoint (may adaptively use sparkshell backend)
103
+ omx api Run native omx-api localhost gateway commands (serve|status|stop|generate)
101
104
  omx session Search prior local session transcripts and history artifacts
102
105
  omx agents-init [path]
103
106
  Bootstrap lightweight AGENTS.md files for a repo/subtree
@@ -250,6 +253,7 @@ const NESTED_HELP_COMMANDS = new Set([
250
253
  "performance-goal",
251
254
  "resume",
252
255
  "session",
256
+ "api",
253
257
  "sparkshell",
254
258
  "team",
255
259
  "tmux-hook",
@@ -475,6 +479,27 @@ async function linkOrCopyCodexHomeEntry(source, destination) {
475
479
  function isCodexSqliteArtifact(entryName) {
476
480
  return /^(?:state|logs)_\d+\.sqlite(?:-(?:shm|wal))?$/.test(entryName);
477
481
  }
482
+ const PROJECT_LAUNCH_PERSISTED_RUNTIME_ENTRY_NAMES = new Set([
483
+ // Codex CLI writes browser/OTP login state here when CODEX_HOME points at
484
+ // the per-session mirror. Persist only the opaque file itself; never parse or
485
+ // log the contents.
486
+ "auth.json",
487
+ ]);
488
+ function shouldPersistProjectLaunchRuntimeEntry(entryName) {
489
+ return PROJECT_LAUNCH_PERSISTED_RUNTIME_ENTRY_NAMES.has(entryName);
490
+ }
491
+ export async function persistProjectLaunchRuntimeAuthState(runtimeCodexHome, projectCodexHome) {
492
+ if (!runtimeCodexHome || !projectCodexHome)
493
+ return;
494
+ if (!existsSync(runtimeCodexHome))
495
+ return;
496
+ await mkdir(projectCodexHome, { recursive: true });
497
+ for (const entry of await readdir(runtimeCodexHome, { withFileTypes: true })) {
498
+ if (!shouldPersistProjectLaunchRuntimeEntry(entry.name) || !entry.isFile())
499
+ continue;
500
+ await copyFile(join(runtimeCodexHome, entry.name), join(projectCodexHome, entry.name));
501
+ }
502
+ }
478
503
  /**
479
504
  * Project-scope setup keeps durable Codex config under <repo>/.codex, but the
480
505
  * Codex TUI also stores model-availability NUX counters in CODEX_HOME/config.toml.
@@ -530,9 +555,10 @@ export async function prepareCodexHomeForLaunch(cwd, sessionId, env = process.en
530
555
  projectLocalCodexHomeForCleanup,
531
556
  };
532
557
  }
533
- async function cleanupRuntimeCodexHome(runtimeCodexHomeForCleanup) {
558
+ async function cleanupRuntimeCodexHome(runtimeCodexHomeForCleanup, projectCodexHomeForPersistence) {
534
559
  if (!runtimeCodexHomeForCleanup)
535
560
  return;
561
+ await persistProjectLaunchRuntimeAuthState(runtimeCodexHomeForCleanup, projectCodexHomeForPersistence);
536
562
  await rm(runtimeCodexHomeForCleanup, { recursive: true, force: true });
537
563
  }
538
564
  function execTmuxFileSync(args, options) {
@@ -797,22 +823,192 @@ export function shouldAutoIsolateMadmaxLaunch(command, launchArgs, env = process
797
823
  function sanitizeRunIdSegment(value) {
798
824
  return value.replace(/[^a-zA-Z0-9._-]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "");
799
825
  }
826
+ const MADMAX_DETACHED_ACTIVE_DIR = "active-detached";
827
+ const MADMAX_DETACHED_LOCK_STALE_MS = 30_000;
828
+ const MADMAX_DETACHED_LOCK_RETRY_MS = 50;
829
+ const MADMAX_DETACHED_LOCK_MAX_ATTEMPTS = 100;
830
+ const OMX_MADMAX_DETACHED_CONTEXT_ENV = "OMX_MADMAX_DETACHED_CONTEXT";
831
+ function resolveMadmaxRunsRoot(env = process.env) {
832
+ return env.OMX_RUNS_DIR || join(homedir(), ".omx-runs");
833
+ }
834
+ function canonicalizeLaunchCwd(cwd) {
835
+ try {
836
+ return execFileSync("git", ["rev-parse", "--show-toplevel"], {
837
+ cwd,
838
+ encoding: "utf-8",
839
+ stdio: ["ignore", "pipe", "ignore"],
840
+ }).trim() || cwd;
841
+ }
842
+ catch {
843
+ return cwd;
844
+ }
845
+ }
846
+ function normalizeMadmaxDetachedLaunchArgv(argv) {
847
+ const passthrough = [];
848
+ const semanticFlags = new Set();
849
+ let reasoningFlag = null;
850
+ let afterEndOfOptions = false;
851
+ for (const arg of argv) {
852
+ if (afterEndOfOptions) {
853
+ passthrough.push(arg);
854
+ continue;
855
+ }
856
+ if (arg === "--") {
857
+ afterEndOfOptions = true;
858
+ passthrough.push(arg);
859
+ continue;
860
+ }
861
+ if (arg === "--tmux" || arg === "--direct") {
862
+ continue;
863
+ }
864
+ if (arg === MADMAX_FLAG ||
865
+ arg === MADMAX_SPARK_FLAG) {
866
+ semanticFlags.add(arg);
867
+ continue;
868
+ }
869
+ if (arg === HIGH_REASONING_FLAG || arg === XHIGH_REASONING_FLAG) {
870
+ reasoningFlag = arg;
871
+ continue;
872
+ }
873
+ passthrough.push(arg);
874
+ }
875
+ return [
876
+ ...Array.from(semanticFlags).sort(),
877
+ ...(reasoningFlag ? [reasoningFlag] : []),
878
+ ...passthrough,
879
+ ];
880
+ }
881
+ export function buildMadmaxDetachedLaunchContextKey(sourceCwd, argv) {
882
+ const payload = JSON.stringify({
883
+ source_cwd: canonicalizeLaunchCwd(sourceCwd),
884
+ argv: normalizeMadmaxDetachedLaunchArgv(argv),
885
+ });
886
+ return createHash("sha256").update(payload).digest("hex").slice(0, 32);
887
+ }
888
+ function madmaxDetachedActiveRecordPath(runsRoot, contextKey) {
889
+ return join(runsRoot, MADMAX_DETACHED_ACTIVE_DIR, `${contextKey}.json`);
890
+ }
891
+ function readMadmaxDetachedActiveRecord(recordPath) {
892
+ if (!existsSync(recordPath))
893
+ return null;
894
+ try {
895
+ const parsed = JSON.parse(readFileSync(recordPath, "utf-8"));
896
+ if (parsed.version !== 1 ||
897
+ typeof parsed.context_key !== "string" ||
898
+ typeof parsed.source_cwd !== "string" ||
899
+ typeof parsed.run_dir !== "string" ||
900
+ typeof parsed.tmux_session_name !== "string" ||
901
+ !Array.isArray(parsed.argv) ||
902
+ !parsed.argv.every((arg) => typeof arg === "string")) {
903
+ return null;
904
+ }
905
+ return {
906
+ version: 1,
907
+ context_key: parsed.context_key,
908
+ created_at: typeof parsed.created_at === "string" ? parsed.created_at : "",
909
+ source_cwd: parsed.source_cwd,
910
+ argv: [...parsed.argv],
911
+ run_dir: parsed.run_dir,
912
+ tmux_session_name: parsed.tmux_session_name,
913
+ };
914
+ }
915
+ catch {
916
+ return null;
917
+ }
918
+ }
919
+ function detachedTmuxSessionExists(sessionName) {
920
+ try {
921
+ execTmuxFileSync(["has-session", "-t", sessionName], { stdio: "ignore" });
922
+ return true;
923
+ }
924
+ catch {
925
+ return false;
926
+ }
927
+ }
928
+ function withMadmaxDetachedContextLock(runsRoot, contextKey, run) {
929
+ const lockPath = join(runsRoot, MADMAX_DETACHED_ACTIVE_DIR, `${contextKey}.lock`);
930
+ mkdirSync(dirname(lockPath), { recursive: true });
931
+ for (let attempt = 0; attempt < MADMAX_DETACHED_LOCK_MAX_ATTEMPTS; attempt++) {
932
+ try {
933
+ mkdirSync(lockPath);
934
+ try {
935
+ writeFileSync(join(lockPath, "pid"), String(process.pid));
936
+ return run();
937
+ }
938
+ finally {
939
+ rmSync(lockPath, { recursive: true, force: true });
940
+ }
941
+ }
942
+ catch (err) {
943
+ const code = err && typeof err === "object" && "code" in err
944
+ ? String(err.code)
945
+ : "";
946
+ if (code !== "EEXIST")
947
+ throw err;
948
+ const lockStat = statSync(lockPath, { throwIfNoEntry: false });
949
+ if (lockStat && Date.now() - lockStat.mtimeMs > MADMAX_DETACHED_LOCK_STALE_MS) {
950
+ let holderAlive = false;
951
+ try {
952
+ const holderPid = Number.parseInt(readFileSync(join(lockPath, "pid"), "utf-8").trim(), 10);
953
+ if (Number.isSafeInteger(holderPid) && holderPid > 0) {
954
+ process.kill(holderPid, 0);
955
+ holderAlive = true;
956
+ }
957
+ }
958
+ catch {
959
+ holderAlive = false;
960
+ }
961
+ if (!holderAlive) {
962
+ rmSync(lockPath, { recursive: true, force: true });
963
+ continue;
964
+ }
965
+ }
966
+ blockMs(MADMAX_DETACHED_LOCK_RETRY_MS);
967
+ }
968
+ }
969
+ throw new MadmaxDetachedGuardError(`timed out waiting for madmax detached launch context lock: ${lockPath}`);
970
+ }
971
+ function isMadmaxDetachedGuardEnabled(env) {
972
+ return env.OMXBOX_ACTIVE === "1" && typeof env[OMX_MADMAX_DETACHED_CONTEXT_ENV] === "string";
973
+ }
974
+ function cleanupCurrentMadmaxReuseRunRoot(env, runsRoot) {
975
+ const runRoot = env.OMX_ROOT;
976
+ if (!runRoot || !env.OMXBOX_ACTIVE)
977
+ return;
978
+ const normalizedRunsRoot = runsRoot.endsWith("/") ? runsRoot : `${runsRoot}/`;
979
+ if (runRoot !== runsRoot && !runRoot.startsWith(normalizedRunsRoot))
980
+ return;
981
+ rmSync(runRoot, { recursive: true, force: true });
982
+ }
983
+ function writeMadmaxDetachedActiveRecord(recordPath, record) {
984
+ mkdirSync(dirname(recordPath), { recursive: true });
985
+ writeFileSync(recordPath, `${JSON.stringify(record, null, 2)}\n`, { mode: 0o600 });
986
+ }
987
+ class MadmaxDetachedReuseError extends Error {
988
+ failClosed = true;
989
+ }
990
+ class MadmaxDetachedGuardError extends Error {
991
+ failClosed = true;
992
+ }
800
993
  export function createMadmaxIsolatedRoot(sourceCwd, argv, env = process.env) {
801
- const runsRoot = env.OMX_RUNS_DIR || join(homedir(), ".omx-runs");
994
+ const runsRoot = resolveMadmaxRunsRoot(env);
802
995
  mkdirSync(runsRoot, { recursive: true });
803
996
  const stamp = new Date().toISOString().replace(/[-:TZ.]/g, "").slice(0, 14);
804
997
  const suffix = Math.random().toString(16).slice(2, 6);
805
998
  const runDir = join(runsRoot, sanitizeRunIdSegment(`run-${stamp}-${suffix}`));
806
999
  mkdirSync(runDir, { recursive: false });
1000
+ const detachedLaunchContext = buildMadmaxDetachedLaunchContextKey(sourceCwd, argv);
807
1001
  const metadata = {
808
1002
  launcher: "omx --madmax",
809
1003
  created_at: new Date().toISOString(),
810
1004
  cwd: runDir,
811
1005
  source_cwd: sourceCwd,
812
1006
  argv,
1007
+ detached_launch_context: detachedLaunchContext,
813
1008
  };
814
1009
  writeFileSync(join(runDir, ".omxbox-run.json"), `${JSON.stringify(metadata, null, 2)}\n`);
815
1010
  writeFileSync(join(runsRoot, "registry.jsonl"), `${JSON.stringify(metadata)}\n`, { flag: "a" });
1011
+ env[OMX_MADMAX_DETACHED_CONTEXT_ENV] = detachedLaunchContext;
816
1012
  return runDir;
817
1013
  }
818
1014
  function activateMadmaxIsolationIfNeeded(command, launchArgs, cwd, env = process.env) {
@@ -843,6 +1039,7 @@ export async function main(args) {
843
1039
  "autoresearch",
844
1040
  "autoresearch-goal",
845
1041
  "explore",
1042
+ "api",
846
1043
  "sparkshell",
847
1044
  "team",
848
1045
  "ralph",
@@ -947,6 +1144,9 @@ export async function main(args) {
947
1144
  case "explore":
948
1145
  await exploreCommand(args.slice(1));
949
1146
  break;
1147
+ case "api":
1148
+ await apiCommand(args.slice(1));
1149
+ break;
950
1150
  case "exec":
951
1151
  if (launchArgs[0] === "inject") {
952
1152
  await execInjectCommand(launchArgs);
@@ -1021,6 +1221,11 @@ export async function main(args) {
1021
1221
  case "reasoning":
1022
1222
  await reasoningCommand(args.slice(1));
1023
1223
  break;
1224
+ case "codex-native-hook": {
1225
+ const { runCodexNativeHookCli } = await import("../scripts/codex-native-hook.js");
1226
+ await runCodexNativeHookCli();
1227
+ break;
1228
+ }
1024
1229
  case "help":
1025
1230
  case "--help":
1026
1231
  case "-h":
@@ -1220,7 +1425,7 @@ export async function launchWithHud(args) {
1220
1425
  // ── Phase 3: postLaunch ─────────────────────────────────────────────
1221
1426
  if (!postLaunchHandledExternally) {
1222
1427
  await postLaunch(cwd, sessionId, codexHomeOverride, enableNotifyFallbackAuthority, projectLocalCodexHomeForCleanup);
1223
- await cleanupRuntimeCodexHome(preparedCodexHome.runtimeCodexHomeForCleanup).catch(logCliOperationFailure);
1428
+ await cleanupRuntimeCodexHome(preparedCodexHome.runtimeCodexHomeForCleanup, projectLocalCodexHomeForCleanup).catch(logCliOperationFailure);
1224
1429
  }
1225
1430
  }
1226
1431
  }
@@ -1312,7 +1517,7 @@ export async function execWithOverlay(args) {
1312
1517
  }
1313
1518
  finally {
1314
1519
  await postLaunch(cwd, sessionId, codexHomeOverride, true, projectLocalCodexHomeForCleanup);
1315
- await cleanupRuntimeCodexHome(preparedCodexHome.runtimeCodexHomeForCleanup).catch(logCliOperationFailure);
1520
+ await cleanupRuntimeCodexHome(preparedCodexHome.runtimeCodexHomeForCleanup, projectLocalCodexHomeForCleanup).catch(logCliOperationFailure);
1316
1521
  }
1317
1522
  }
1318
1523
  export function normalizeCodexLaunchArgs(args) {
@@ -1833,10 +2038,16 @@ function withTmuxExtendedKeysLeaseLock(cwd, socketPath, run) {
1833
2038
  }
1834
2039
  throw new Error(`timed out waiting for tmux extended-keys lease lock: ${lockPath}`);
1835
2040
  }
1836
- function buildDetachedSessionLeaderCommand(cwd, sessionName, codexCmd, sessionId, codexHomeOverride, projectLocalCodexHomeForCleanup, runtimeCodexHomeForCleanup) {
2041
+ function buildDetachedSessionLeaderCommand(cwd, sessionName, codexCmd, sessionId, codexHomeOverride, projectLocalCodexHomeForCleanup, runtimeCodexHomeForCleanup, parentEnvFilePath) {
1837
2042
  const detachedPostLaunchHelper = sessionId
1838
2043
  ? `${buildDetachedSessionPostLaunchHelperCommand(cwd, sessionId, codexHomeOverride, projectLocalCodexHomeForCleanup, runtimeCodexHomeForCleanup)} >/dev/null 2>&1 || true;`
1839
2044
  : "";
2045
+ const parentEnvSource = parentEnvFilePath && parentEnvFilePath.trim()
2046
+ ? `if [ -r ${quoteShellArg(parentEnvFilePath)} ]; then . ${quoteShellArg(parentEnvFilePath)}; rm -f ${quoteShellArg(parentEnvFilePath)}; fi;`
2047
+ : "";
2048
+ const parentEnvCleanup = parentEnvFilePath && parentEnvFilePath.trim()
2049
+ ? `rm -f ${quoteShellArg(parentEnvFilePath)} 2>/dev/null || true;`
2050
+ : "";
1840
2051
  const wrapped = [
1841
2052
  buildTmuxExtendedKeysAcquireShellSnippet(cwd),
1842
2053
  'exec 3<&0;',
@@ -1850,6 +2061,7 @@ function buildDetachedSessionLeaderCommand(cwd, sessionName, codexCmd, sessionId
1850
2061
  "fi;",
1851
2062
  'exec 3<&- 2>/dev/null || true;',
1852
2063
  buildTmuxExtendedKeysReleaseShellSnippet(cwd),
2064
+ parentEnvCleanup,
1853
2065
  detachedPostLaunchHelper,
1854
2066
  'if [ "$status" -eq 0 ]; then',
1855
2067
  `tmux kill-session -t "${escapeShellDoubleQuotedValue(sessionName)}" >/dev/null 2>&1 || true;`,
@@ -1857,6 +2069,7 @@ function buildDetachedSessionLeaderCommand(cwd, sessionName, codexCmd, sessionId
1857
2069
  "exit $status;",
1858
2070
  "};",
1859
2071
  "trap omx_detached_session_cleanup 0 INT TERM HUP;",
2072
+ parentEnvSource,
1860
2073
  "omx_codex_started_at=$(date +%s 2>/dev/null || printf 0);",
1861
2074
  `${codexCmd} <&3 &`,
1862
2075
  "omx_codex_pid=$!;",
@@ -1997,6 +2210,34 @@ function buildTmuxExtendedKeysAcquireShellSnippet(cwd) {
1997
2210
  function buildTmuxExtendedKeysReleaseShellSnippet(cwd) {
1998
2211
  return `if [ -n "\${OMX_TMUX_EXTENDED_KEYS_LEASE:-}" ]; then ${buildTmuxExtendedKeysHelperCommand(cwd, "release")} "\${OMX_TMUX_EXTENDED_KEYS_LEASE}" >/dev/null 2>&1 || true; fi;`;
1999
2212
  }
2213
+ const SHELL_ENV_NAME_PATTERN = /^[A-Za-z_][A-Za-z0-9_]*$/;
2214
+ export function serializeDetachedSessionParentEnv(env) {
2215
+ const lines = [];
2216
+ for (const key of Object.keys(env).sort()) {
2217
+ if (!SHELL_ENV_NAME_PATTERN.test(key))
2218
+ continue;
2219
+ const value = env[key];
2220
+ if (typeof value !== "string")
2221
+ continue;
2222
+ if (value.includes("\0"))
2223
+ continue;
2224
+ lines.push(`export ${key}=${quoteShellArg(value)}`);
2225
+ }
2226
+ return `${lines.join("\n")}\n`;
2227
+ }
2228
+ export function detachedSessionParentEnvFilePath(cwd, sessionId) {
2229
+ const safeSessionId = sessionId.replace(/[^A-Za-z0-9_.-]/g, "_");
2230
+ return join(omxRoot(cwd), "runtime", "tmux-env", `${safeSessionId}.env`);
2231
+ }
2232
+ export function writeDetachedSessionParentEnvFile(cwd, sessionId, env) {
2233
+ const filePath = detachedSessionParentEnvFilePath(cwd, sessionId);
2234
+ mkdirSync(dirname(filePath), { recursive: true, mode: 0o700 });
2235
+ writeFileSync(filePath, serializeDetachedSessionParentEnv(env), {
2236
+ encoding: "utf-8",
2237
+ mode: 0o600,
2238
+ });
2239
+ return filePath;
2240
+ }
2000
2241
  export function withTmuxExtendedKeys(cwd, run, execFileSyncImpl = (file, tmuxArgs) => execFileSync(file, tmuxArgs, {
2001
2242
  encoding: "utf-8",
2002
2243
  ...(process.platform === "win32" ? { windowsHide: true } : {}),
@@ -2010,10 +2251,10 @@ export function withTmuxExtendedKeys(cwd, run, execFileSyncImpl = (file, tmuxArg
2010
2251
  releaseTmuxExtendedKeysLease(cwd, leaseHandle, execFileSyncImpl);
2011
2252
  }
2012
2253
  }
2013
- export function buildDetachedSessionBootstrapSteps(sessionName, cwd, codexCmd, hudCmd, workerLaunchArgs, codexHomeOverride, notifyTempContractRaw, nativeWindows = false, sessionId, projectLocalCodexHomeForCleanup, runtimeCodexHomeForCleanup, omxRootOverride, env = process.env, sqliteHomeOverride) {
2254
+ export function buildDetachedSessionBootstrapSteps(sessionName, cwd, codexCmd, hudCmd, workerLaunchArgs, codexHomeOverride, notifyTempContractRaw, nativeWindows = false, sessionId, projectLocalCodexHomeForCleanup, runtimeCodexHomeForCleanup, omxRootOverride, env = process.env, sqliteHomeOverride, parentEnvFilePath) {
2014
2255
  const detachedLeaderCmd = nativeWindows
2015
2256
  ? "powershell.exe"
2016
- : buildDetachedSessionLeaderCommand(cwd, sessionName, codexCmd, sessionId, codexHomeOverride, projectLocalCodexHomeForCleanup, runtimeCodexHomeForCleanup);
2257
+ : buildDetachedSessionLeaderCommand(cwd, sessionName, codexCmd, sessionId, codexHomeOverride, projectLocalCodexHomeForCleanup, runtimeCodexHomeForCleanup, parentEnvFilePath);
2017
2258
  const newSessionArgs = [
2018
2259
  "new-session",
2019
2260
  "-d",
@@ -2249,6 +2490,26 @@ async function readPostLaunchModeStateFile(path, dependencies = {}) {
2249
2490
  function cleanPostLaunchString(value) {
2250
2491
  return typeof value === "string" ? value.trim() : "";
2251
2492
  }
2493
+ function isAutopilotReviewPendingPostLaunchState(state) {
2494
+ if (!state || state.active !== true)
2495
+ return false;
2496
+ const mode = cleanPostLaunchString(state.mode).toLowerCase();
2497
+ if (mode && mode !== "autopilot")
2498
+ return false;
2499
+ const phase = cleanPostLaunchString(state.current_phase ?? state.currentPhase)
2500
+ .toLowerCase()
2501
+ .replace(/_/g, "-");
2502
+ if (phase === "code-review" || phase === "review" || phase === "reviewing" || phase === "review-pending") {
2503
+ return true;
2504
+ }
2505
+ const nestedState = state.state && typeof state.state === "object"
2506
+ ? state.state
2507
+ : {};
2508
+ return state.review_pending === true
2509
+ || state.reviewPending === true
2510
+ || nestedState.review_pending === true
2511
+ || nestedState.reviewPending === true;
2512
+ }
2252
2513
  function postLaunchUniqueStrings(values) {
2253
2514
  return [...new Set(values.map((value) => value.trim()).filter(Boolean))];
2254
2515
  }
@@ -2336,8 +2597,16 @@ export async function cleanupPostLaunchModeStateFiles(cwd, sessionId, dependenci
2336
2597
  const rootSkillActiveStateBeforeCleanup = sessionId
2337
2598
  ? await readSkillActiveState(getSkillActiveStatePathsForStateDir(rootStateDir).rootPath)
2338
2599
  : null;
2600
+ let preserveSkillActiveForReviewPendingAutopilot = false;
2339
2601
  for (const stateDir of scopedDirs) {
2340
2602
  const files = await readdir(stateDir).catch(() => []);
2603
+ const autopilotPath = join(stateDir, "autopilot-state.json");
2604
+ const autopilotPrecheck = files.includes("autopilot-state.json")
2605
+ ? await readPostLaunchModeStateFile(autopilotPath, dependencies)
2606
+ : null;
2607
+ const preserveReviewPendingAutopilot = autopilotPrecheck?.kind === "ok"
2608
+ && isAutopilotReviewPendingPostLaunchState(autopilotPrecheck.state);
2609
+ preserveSkillActiveForReviewPendingAutopilot ||= preserveReviewPendingAutopilot;
2341
2610
  for (const file of files) {
2342
2611
  if (!file.endsWith("-state.json") || file === "session.json")
2343
2612
  continue;
@@ -2395,6 +2664,10 @@ export async function cleanupPostLaunchModeStateFiles(cwd, sessionId, dependenci
2395
2664
  }
2396
2665
  continue;
2397
2666
  }
2667
+ if (preserveReviewPendingAutopilot
2668
+ && (mode === "autopilot" || mode === SKILL_ACTIVE_STATE_MODE)) {
2669
+ continue;
2670
+ }
2398
2671
  try {
2399
2672
  const completedAt = now().toISOString();
2400
2673
  if (mode === SKILL_ACTIVE_STATE_MODE) {
@@ -2433,7 +2706,9 @@ export async function cleanupPostLaunchModeStateFiles(cwd, sessionId, dependenci
2433
2706
  }
2434
2707
  if (sessionId) {
2435
2708
  try {
2436
- await scrubPostLaunchRootSkillActiveForSession(rootStateDir, sessionId, now().toISOString(), writeFile, rootSkillActiveStateBeforeCleanup);
2709
+ if (!preserveSkillActiveForReviewPendingAutopilot) {
2710
+ await scrubPostLaunchRootSkillActiveForSession(rootStateDir, sessionId, now().toISOString(), writeFile, rootSkillActiveStateBeforeCleanup);
2711
+ }
2437
2712
  }
2438
2713
  catch (err) {
2439
2714
  writeWarn(`[omx] postLaunch: failed to reconcile root skill-active state: ${err instanceof Error ? err.message : err}`);
@@ -2610,6 +2885,9 @@ function runCodex(cwd, args, sessionId, workerDefaultModel, codexHomeOverride, s
2610
2885
  let hudPaneId = null;
2611
2886
  try {
2612
2887
  hudPaneId = createHudWatchPane(cwd, hudCmd);
2888
+ if (hudPaneId && currentPaneId) {
2889
+ registerHudResizeHook(hudPaneId, currentPaneId, HUD_TMUX_HEIGHT_LINES);
2890
+ }
2613
2891
  }
2614
2892
  catch (err) {
2615
2893
  logCliOperationFailure(err);
@@ -2670,96 +2948,166 @@ function runCodex(cwd, args, sessionId, workerDefaultModel, codexHomeOverride, s
2670
2948
  ? buildWindowsPromptCommand("codex", launchArgs)
2671
2949
  : null;
2672
2950
  const sessionName = buildDetachedTmuxSessionName(cwd, sessionId);
2673
- void writeSessionStart(cwd, sessionId, { tmuxSessionName: sessionName }).catch((err) => {
2674
- logCliOperationFailure(err);
2675
- // Non-fatal: managed tmux recovery can still use compatibility fallback.
2676
- });
2677
- let createdDetachedSession = false;
2678
- let registeredHookTarget = null;
2679
- let registeredHookName = null;
2680
- let registeredClientAttachedHookName = null;
2681
- try {
2682
- const bootstrapSteps = buildDetachedSessionBootstrapSteps(sessionName, cwd, codexCmd, hudCmd, workerLaunchArgs, codexHomeOverride, notifyTempContractRaw, nativeWindows, sessionId, projectLocalCodexHomeForCleanup, runtimeCodexHomeForCleanup, omxRootOverride, process.env, sqliteHomeOverride);
2683
- for (const step of bootstrapSteps) {
2684
- const output = execTmuxFileSync(step.args, {
2685
- stdio: "pipe",
2686
- encoding: "utf-8",
2687
- });
2688
- if (step.name === "new-session") {
2689
- createdDetachedSession = true;
2690
- parsePaneIdFromTmuxOutput(output || "");
2951
+ const launchDetachedSession = () => {
2952
+ const contextKey = process.env[OMX_MADMAX_DETACHED_CONTEXT_ENV]?.trim();
2953
+ const runsRoot = resolveMadmaxRunsRoot(process.env);
2954
+ const activeRecordPath = contextKey
2955
+ ? madmaxDetachedActiveRecordPath(runsRoot, contextKey)
2956
+ : null;
2957
+ const activeRecord = activeRecordPath
2958
+ ? readMadmaxDetachedActiveRecord(activeRecordPath)
2959
+ : null;
2960
+ if (activeRecord &&
2961
+ activeRecord.context_key === contextKey &&
2962
+ detachedTmuxSessionExists(activeRecord.tmux_session_name)) {
2963
+ cleanupCurrentMadmaxReuseRunRoot(process.env, runsRoot);
2964
+ process.stderr.write(`[omx] madmax detached launch already active for this context; attaching ${activeRecord.tmux_session_name} instead of starting a duplicate.\n`);
2965
+ try {
2966
+ execTmuxFileSync(["attach-session", "-t", activeRecord.tmux_session_name], {
2967
+ stdio: "inherit",
2968
+ });
2691
2969
  }
2692
- if (step.name === "split-and-capture-hud-pane") {
2693
- const hudPaneId = parsePaneIdFromTmuxOutput(output || "");
2694
- const hookWindowIndex = hudPaneId
2695
- ? detectDetachedSessionWindowIndex(sessionName)
2696
- : null;
2697
- const hookTarget = hudPaneId && hookWindowIndex
2698
- ? buildResizeHookTarget(sessionName, hookWindowIndex)
2699
- : null;
2700
- const hookName = hudPaneId && hookWindowIndex
2701
- ? buildResizeHookName("launch", sessionName, hookWindowIndex, hudPaneId)
2702
- : null;
2703
- const clientAttachedHookName = hudPaneId && hookWindowIndex
2704
- ? buildClientAttachedReconcileHookName("launch", sessionName, hookWindowIndex, hudPaneId)
2705
- : null;
2706
- const finalizeSteps = buildDetachedSessionFinalizeSteps(sessionName, hudPaneId, hookWindowIndex, process.env.OMX_MOUSE !== "0", nativeWindows);
2707
- if (nativeWindows && detachedWindowsCodexCmd) {
2708
- scheduleDetachedWindowsCodexLaunch(sessionName, detachedWindowsCodexCmd);
2970
+ catch (err) {
2971
+ logCliOperationFailure(err);
2972
+ throw new MadmaxDetachedReuseError(`refusing duplicate madmax detached launch: existing session ${activeRecord.tmux_session_name} is active but attach failed`);
2973
+ }
2974
+ return { postLaunchHandledExternally: true };
2975
+ }
2976
+ if (activeRecordPath && activeRecord) {
2977
+ rmSync(activeRecordPath, { force: true });
2978
+ }
2979
+ void writeSessionStart(cwd, sessionId, { tmuxSessionName: sessionName }).catch((err) => {
2980
+ logCliOperationFailure(err);
2981
+ // Non-fatal: managed tmux recovery can still use compatibility fallback.
2982
+ });
2983
+ let createdDetachedSession = false;
2984
+ let registeredHookTarget = null;
2985
+ let registeredHookName = null;
2986
+ let registeredClientAttachedHookName = null;
2987
+ let detachedParentEnvFilePath;
2988
+ try {
2989
+ // This path is the user-shell interactive launch: OMX creates a tmux
2990
+ // session and immediately attaches the user's terminal to it. If a tmux
2991
+ // server already exists, `new-session -e` only forwards explicit values,
2992
+ // so provider-specific parent-shell keys would disappear. Source a
2993
+ // private env file inside the leader shell instead of putting every
2994
+ // parent env value on the tmux command line or in logs.
2995
+ if (!nativeWindows) {
2996
+ detachedParentEnvFilePath = writeDetachedSessionParentEnvFile(cwd, sessionId, codexEnvWithNotify);
2997
+ }
2998
+ const bootstrapSteps = buildDetachedSessionBootstrapSteps(sessionName, cwd, codexCmd, hudCmd, workerLaunchArgs, codexHomeOverride, notifyTempContractRaw, nativeWindows, sessionId, projectLocalCodexHomeForCleanup, runtimeCodexHomeForCleanup, omxRootOverride, process.env, sqliteHomeOverride, detachedParentEnvFilePath);
2999
+ for (const step of bootstrapSteps) {
3000
+ const output = execTmuxFileSync(step.args, {
3001
+ stdio: "pipe",
3002
+ encoding: "utf-8",
3003
+ });
3004
+ if (step.name === "new-session") {
3005
+ createdDetachedSession = true;
3006
+ if (activeRecordPath && contextKey) {
3007
+ writeMadmaxDetachedActiveRecord(activeRecordPath, {
3008
+ version: 1,
3009
+ context_key: contextKey,
3010
+ created_at: new Date().toISOString(),
3011
+ source_cwd: process.env.OMX_SOURCE_CWD || cwd,
3012
+ argv: args,
3013
+ run_dir: process.env.OMX_ROOT || cwd,
3014
+ tmux_session_name: sessionName,
3015
+ });
3016
+ }
3017
+ parsePaneIdFromTmuxOutput(output || "");
2709
3018
  }
2710
- for (const finalizeStep of finalizeSteps) {
2711
- if (finalizeStep.name === "sanitize-copy-mode-style") {
3019
+ if (step.name === "split-and-capture-hud-pane") {
3020
+ const hudPaneId = parsePaneIdFromTmuxOutput(output || "");
3021
+ const hookWindowIndex = hudPaneId
3022
+ ? detectDetachedSessionWindowIndex(sessionName)
3023
+ : null;
3024
+ const hookTarget = hudPaneId && hookWindowIndex
3025
+ ? buildResizeHookTarget(sessionName, hookWindowIndex)
3026
+ : null;
3027
+ const hookName = hudPaneId && hookWindowIndex
3028
+ ? buildResizeHookName("launch", sessionName, hookWindowIndex, hudPaneId)
3029
+ : null;
3030
+ const clientAttachedHookName = hudPaneId && hookWindowIndex
3031
+ ? buildClientAttachedReconcileHookName("launch", sessionName, hookWindowIndex, hudPaneId)
3032
+ : null;
3033
+ const finalizeSteps = buildDetachedSessionFinalizeSteps(sessionName, hudPaneId, hookWindowIndex, process.env.OMX_MOUSE !== "0", nativeWindows);
3034
+ if (nativeWindows && detachedWindowsCodexCmd) {
3035
+ scheduleDetachedWindowsCodexLaunch(sessionName, detachedWindowsCodexCmd);
3036
+ }
3037
+ for (const finalizeStep of finalizeSteps) {
3038
+ if (finalizeStep.name === "sanitize-copy-mode-style") {
3039
+ try {
3040
+ mitigateCopyModeUnderlineArtifacts(sessionName);
3041
+ }
3042
+ catch (err) {
3043
+ logCliOperationFailure(err);
3044
+ }
3045
+ continue;
3046
+ }
3047
+ const stdio = finalizeStep.name === "attach-session" ? "inherit" : "ignore";
2712
3048
  try {
2713
- mitigateCopyModeUnderlineArtifacts(sessionName);
3049
+ const startedAtMs = Date.now();
3050
+ execTmuxFileSync(finalizeStep.args, { stdio });
3051
+ if (finalizeStep.name === "attach-session") {
3052
+ assertDetachedAttachDidNotNoop(sessionName, Date.now() - startedAtMs, process.env);
3053
+ }
2714
3054
  }
2715
3055
  catch (err) {
2716
3056
  logCliOperationFailure(err);
3057
+ if (finalizeStep.name === "attach-session")
3058
+ throw new Error("failed to attach detached tmux session");
3059
+ continue;
2717
3060
  }
2718
- continue;
2719
- }
2720
- const stdio = finalizeStep.name === "attach-session" ? "inherit" : "ignore";
2721
- try {
2722
- const startedAtMs = Date.now();
2723
- execTmuxFileSync(finalizeStep.args, { stdio });
2724
- if (finalizeStep.name === "attach-session") {
2725
- assertDetachedAttachDidNotNoop(sessionName, Date.now() - startedAtMs, process.env);
3061
+ if (finalizeStep.name === "register-resize-hook" &&
3062
+ hookTarget &&
3063
+ hookName) {
3064
+ registeredHookTarget = hookTarget;
3065
+ registeredHookName = hookName;
3066
+ }
3067
+ if (finalizeStep.name === "register-client-attached-reconcile" &&
3068
+ clientAttachedHookName) {
3069
+ registeredClientAttachedHookName = clientAttachedHookName;
2726
3070
  }
2727
3071
  }
2728
- catch (err) {
2729
- logCliOperationFailure(err);
2730
- if (finalizeStep.name === "attach-session")
2731
- throw new Error("failed to attach detached tmux session");
2732
- continue;
2733
- }
2734
- if (finalizeStep.name === "register-resize-hook" &&
2735
- hookTarget &&
2736
- hookName) {
2737
- registeredHookTarget = hookTarget;
2738
- registeredHookName = hookName;
3072
+ }
3073
+ }
3074
+ return { postLaunchHandledExternally: !nativeWindows };
3075
+ }
3076
+ catch (err) {
3077
+ if (detachedParentEnvFilePath) {
3078
+ rmSync(detachedParentEnvFilePath, { force: true });
3079
+ }
3080
+ if (activeRecordPath) {
3081
+ rmSync(activeRecordPath, { force: true });
3082
+ }
3083
+ if (createdDetachedSession) {
3084
+ const rollbackSteps = buildDetachedSessionRollbackSteps(sessionName, registeredHookTarget, registeredHookName, registeredClientAttachedHookName);
3085
+ for (const rollbackStep of rollbackSteps) {
3086
+ try {
3087
+ execTmuxFileSync(rollbackStep.args, { stdio: "ignore" });
2739
3088
  }
2740
- if (finalizeStep.name === "register-client-attached-reconcile" &&
2741
- clientAttachedHookName) {
2742
- registeredClientAttachedHookName = clientAttachedHookName;
3089
+ catch (rollbackErr) {
3090
+ logCliOperationFailure(rollbackErr);
3091
+ // best-effort rollback only
2743
3092
  }
2744
3093
  }
2745
3094
  }
3095
+ throw err;
3096
+ }
3097
+ };
3098
+ const contextKey = process.env[OMX_MADMAX_DETACHED_CONTEXT_ENV]?.trim();
3099
+ const runsRoot = resolveMadmaxRunsRoot(process.env);
3100
+ try {
3101
+ if (isMadmaxDetachedGuardEnabled(process.env) && contextKey) {
3102
+ return withMadmaxDetachedContextLock(runsRoot, contextKey, launchDetachedSession);
2746
3103
  }
2747
- return { postLaunchHandledExternally: !nativeWindows };
3104
+ return launchDetachedSession();
2748
3105
  }
2749
3106
  catch (err) {
2750
- logCliOperationFailure(err);
2751
- if (createdDetachedSession) {
2752
- const rollbackSteps = buildDetachedSessionRollbackSteps(sessionName, registeredHookTarget, registeredHookName, registeredClientAttachedHookName);
2753
- for (const rollbackStep of rollbackSteps) {
2754
- try {
2755
- execTmuxFileSync(rollbackStep.args, { stdio: "ignore" });
2756
- }
2757
- catch (err) {
2758
- logCliOperationFailure(err);
2759
- // best-effort rollback only
2760
- }
2761
- }
3107
+ if (err instanceof MadmaxDetachedReuseError || err instanceof MadmaxDetachedGuardError) {
3108
+ throw err;
2762
3109
  }
3110
+ logCliOperationFailure(err);
2763
3111
  // tmux not available or failed, just run codex directly
2764
3112
  runCodexBlocking(cwd, launchArgs, codexEnvWithNotify);
2765
3113
  return { postLaunchHandledExternally: false };
@@ -3016,7 +3364,7 @@ async function postLaunch(cwd, sessionId, codexHomeOverride, enableNotifyFallbac
3016
3364
  }
3017
3365
  export async function runDetachedSessionPostLaunch(cwd, sessionId, codexHomeOverride, projectLocalCodexHomeForCleanup, runtimeCodexHomeForCleanup) {
3018
3366
  await postLaunch(cwd, sessionId, codexHomeOverride, false, projectLocalCodexHomeForCleanup);
3019
- await cleanupRuntimeCodexHome(runtimeCodexHomeForCleanup).catch(logCliOperationFailure);
3367
+ await cleanupRuntimeCodexHome(runtimeCodexHomeForCleanup, projectLocalCodexHomeForCleanup).catch(logCliOperationFailure);
3020
3368
  }
3021
3369
  async function emitNativeHookEvent(cwd, event, opts = {}) {
3022
3370
  const payload = buildHookEvent(event, {
@@ -3117,6 +3465,22 @@ function parseWatcherPidFile(content) {
3117
3465
  return Number.isFinite(pid) && pid > 0 ? pid : null;
3118
3466
  }
3119
3467
  }
3468
+ const DEFAULT_NOTIFY_FALLBACK_REAP_GRACE_MS = 5000;
3469
+ function resolveNotifyFallbackReapGraceMs(env = process.env) {
3470
+ const parsed = Number.parseInt(env.OMX_NOTIFY_FALLBACK_REAP_GRACE_MS || "", 10);
3471
+ if (Number.isFinite(parsed) && parsed >= 0)
3472
+ return parsed;
3473
+ return DEFAULT_NOTIFY_FALLBACK_REAP_GRACE_MS;
3474
+ }
3475
+ function isWatcherRecordWithinReapGrace(record, nowMs = Date.now(), graceMs = resolveNotifyFallbackReapGraceMs()) {
3476
+ if (graceMs <= 0 || !record.startedAt)
3477
+ return false;
3478
+ const startedMs = Date.parse(record.startedAt);
3479
+ if (!Number.isFinite(startedMs))
3480
+ return false;
3481
+ const ageMs = nowMs - startedMs;
3482
+ return ageMs >= 0 && ageMs < graceMs;
3483
+ }
3120
3484
  function parseWatcherPidRecord(content) {
3121
3485
  const trimmed = content.trim();
3122
3486
  if (!trimmed)
@@ -3159,7 +3523,7 @@ function isLikelyOmxWatcherProcess(pid, execFileSyncFn = execFileSync, platform
3159
3523
  export async function reapStaleNotifyFallbackWatcher(pidPath, deps = {}) {
3160
3524
  const exists = deps.exists ?? existsSync;
3161
3525
  if (!exists(pidPath))
3162
- return;
3526
+ return "missing";
3163
3527
  const { readFile } = await import("fs/promises");
3164
3528
  const readFileImpl = deps.readFile ?? readFile;
3165
3529
  const tryKillPidImpl = deps.tryKillPid ?? tryKillPid;
@@ -3168,9 +3532,15 @@ export async function reapStaleNotifyFallbackWatcher(pidPath, deps = {}) {
3168
3532
  const isWatcherProcessImpl = deps.isWatcherProcess ?? isLikelyOmxWatcherProcess;
3169
3533
  try {
3170
3534
  const record = parseWatcherPidRecord(await readFileImpl(pidPath, "utf-8"));
3171
- if (record && isWatcherProcessImpl(record.pid)) {
3172
- tryKillPidImpl(record.pid, "SIGTERM");
3535
+ if (!record)
3536
+ return "invalid";
3537
+ if (!isWatcherProcessImpl(record.pid))
3538
+ return "identity_mismatch";
3539
+ if (isWatcherRecordWithinReapGrace(record, deps.nowMs?.() ?? Date.now(), deps.reapGraceMs ?? resolveNotifyFallbackReapGraceMs())) {
3540
+ return "recent_active";
3173
3541
  }
3542
+ tryKillPidImpl(record.pid, "SIGTERM");
3543
+ return "reaped";
3174
3544
  }
3175
3545
  catch (error) {
3176
3546
  if (!hasErrnoCodeImpl(error, "ESRCH")) {
@@ -3179,6 +3549,7 @@ export async function reapStaleNotifyFallbackWatcher(pidPath, deps = {}) {
3179
3549
  error: error instanceof Error ? error.message : String(error),
3180
3550
  });
3181
3551
  }
3552
+ return "failed";
3182
3553
  }
3183
3554
  }
3184
3555
  function tryKillPid(pid, signal = "SIGTERM") {
@@ -3196,7 +3567,9 @@ function tryKillPid(pid, signal = "SIGTERM") {
3196
3567
  async function startNotifyFallbackWatcher(cwd, options = {}) {
3197
3568
  const { mkdir, writeFile } = await import("fs/promises");
3198
3569
  const pidPath = notifyFallbackPidPath(cwd);
3199
- await reapStaleNotifyFallbackWatcher(pidPath);
3570
+ const reapResult = await reapStaleNotifyFallbackWatcher(pidPath);
3571
+ if (reapResult === "recent_active")
3572
+ return;
3200
3573
  if (!shouldEnableNotifyFallbackWatcher(process.env, process.platform))
3201
3574
  return;
3202
3575
  const pkgRoot = getPackageRoot();