oh-my-codex 0.16.0 → 0.16.2

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 (357) hide show
  1. package/Cargo.lock +5 -5
  2. package/Cargo.toml +1 -1
  3. package/README.md +2 -2
  4. package/crates/omx-explore/src/main.rs +434 -28
  5. package/dist/agents/__tests__/native-config.test.js +50 -0
  6. package/dist/agents/__tests__/native-config.test.js.map +1 -1
  7. package/dist/agents/native-config.d.ts.map +1 -1
  8. package/dist/agents/native-config.js +3 -2
  9. package/dist/agents/native-config.js.map +1 -1
  10. package/dist/cli/__tests__/codex-plugin-layout.test.js +1 -0
  11. package/dist/cli/__tests__/codex-plugin-layout.test.js.map +1 -1
  12. package/dist/cli/__tests__/doctor-warning-copy.test.js +1 -1
  13. package/dist/cli/__tests__/doctor-warning-copy.test.js.map +1 -1
  14. package/dist/cli/__tests__/explore.test.js +120 -3
  15. package/dist/cli/__tests__/explore.test.js.map +1 -1
  16. package/dist/cli/__tests__/imagegen-continuation.test.d.ts +2 -0
  17. package/dist/cli/__tests__/imagegen-continuation.test.d.ts.map +1 -0
  18. package/dist/cli/__tests__/imagegen-continuation.test.js +135 -0
  19. package/dist/cli/__tests__/imagegen-continuation.test.js.map +1 -0
  20. package/dist/cli/__tests__/index.test.js +182 -18
  21. package/dist/cli/__tests__/index.test.js.map +1 -1
  22. package/dist/cli/__tests__/launch-fallback.test.js +88 -2
  23. package/dist/cli/__tests__/launch-fallback.test.js.map +1 -1
  24. package/dist/cli/__tests__/ralph.test.js +62 -0
  25. package/dist/cli/__tests__/ralph.test.js.map +1 -1
  26. package/dist/cli/__tests__/setup-install-mode.test.js +48 -0
  27. package/dist/cli/__tests__/setup-install-mode.test.js.map +1 -1
  28. package/dist/cli/__tests__/setup-scope.test.js +12 -0
  29. package/dist/cli/__tests__/setup-scope.test.js.map +1 -1
  30. package/dist/cli/__tests__/team.test.js +465 -12
  31. package/dist/cli/__tests__/team.test.js.map +1 -1
  32. package/dist/cli/__tests__/ultragoal.test.js +50 -5
  33. package/dist/cli/__tests__/ultragoal.test.js.map +1 -1
  34. package/dist/cli/__tests__/uninstall.test.js +6 -2
  35. package/dist/cli/__tests__/uninstall.test.js.map +1 -1
  36. package/dist/cli/explore.d.ts.map +1 -1
  37. package/dist/cli/explore.js +211 -12
  38. package/dist/cli/explore.js.map +1 -1
  39. package/dist/cli/index.d.ts +11 -3
  40. package/dist/cli/index.d.ts.map +1 -1
  41. package/dist/cli/index.js +124 -18
  42. package/dist/cli/index.js.map +1 -1
  43. package/dist/cli/ralph.d.ts.map +1 -1
  44. package/dist/cli/ralph.js +37 -3
  45. package/dist/cli/ralph.js.map +1 -1
  46. package/dist/cli/setup.d.ts.map +1 -1
  47. package/dist/cli/setup.js +100 -9
  48. package/dist/cli/setup.js.map +1 -1
  49. package/dist/cli/team.d.ts +1 -0
  50. package/dist/cli/team.d.ts.map +1 -1
  51. package/dist/cli/team.js +42 -7
  52. package/dist/cli/team.js.map +1 -1
  53. package/dist/cli/ultragoal.d.ts +1 -1
  54. package/dist/cli/ultragoal.d.ts.map +1 -1
  55. package/dist/cli/ultragoal.js +29 -8
  56. package/dist/cli/ultragoal.js.map +1 -1
  57. package/dist/cli/uninstall.d.ts.map +1 -1
  58. package/dist/cli/uninstall.js +2 -1
  59. package/dist/cli/uninstall.js.map +1 -1
  60. package/dist/config/__tests__/codex-hooks.test.js +97 -2
  61. package/dist/config/__tests__/codex-hooks.test.js.map +1 -1
  62. package/dist/config/__tests__/generator-idempotent.test.js +1 -1
  63. package/dist/config/__tests__/generator-idempotent.test.js.map +1 -1
  64. package/dist/config/__tests__/generator-notify.test.js +22 -0
  65. package/dist/config/__tests__/generator-notify.test.js.map +1 -1
  66. package/dist/config/__tests__/models.test.js +18 -1
  67. package/dist/config/__tests__/models.test.js.map +1 -1
  68. package/dist/config/__tests__/wiki-config-contract.test.js +2 -1
  69. package/dist/config/__tests__/wiki-config-contract.test.js.map +1 -1
  70. package/dist/config/codex-hooks.d.ts +17 -3
  71. package/dist/config/codex-hooks.d.ts.map +1 -1
  72. package/dist/config/codex-hooks.js +102 -2
  73. package/dist/config/codex-hooks.js.map +1 -1
  74. package/dist/config/generator.d.ts +4 -1
  75. package/dist/config/generator.d.ts.map +1 -1
  76. package/dist/config/generator.js +69 -12
  77. package/dist/config/generator.js.map +1 -1
  78. package/dist/config/models.d.ts +6 -0
  79. package/dist/config/models.d.ts.map +1 -1
  80. package/dist/config/models.js +37 -0
  81. package/dist/config/models.js.map +1 -1
  82. package/dist/exec/followup.d.ts +1 -0
  83. package/dist/exec/followup.d.ts.map +1 -1
  84. package/dist/exec/followup.js +9 -3
  85. package/dist/exec/followup.js.map +1 -1
  86. package/dist/hooks/__tests__/anti-slop-workflow.test.js +19 -0
  87. package/dist/hooks/__tests__/anti-slop-workflow.test.js.map +1 -1
  88. package/dist/hooks/__tests__/consensus-execution-handoff.test.js +19 -2
  89. package/dist/hooks/__tests__/consensus-execution-handoff.test.js.map +1 -1
  90. package/dist/hooks/__tests__/deep-interview-contract.test.js +40 -0
  91. package/dist/hooks/__tests__/deep-interview-contract.test.js.map +1 -1
  92. package/dist/hooks/__tests__/foreground-isolation-contract.test.d.ts +2 -0
  93. package/dist/hooks/__tests__/foreground-isolation-contract.test.d.ts.map +1 -0
  94. package/dist/hooks/__tests__/foreground-isolation-contract.test.js +28 -0
  95. package/dist/hooks/__tests__/foreground-isolation-contract.test.js.map +1 -0
  96. package/dist/hooks/__tests__/keyword-detector.test.js +37 -25
  97. package/dist/hooks/__tests__/keyword-detector.test.js.map +1 -1
  98. package/dist/hooks/__tests__/notify-hook-auto-nudge.test.js +10 -4
  99. package/dist/hooks/__tests__/notify-hook-auto-nudge.test.js.map +1 -1
  100. package/dist/hooks/__tests__/session.test.js +32 -0
  101. package/dist/hooks/__tests__/session.test.js.map +1 -1
  102. package/dist/hooks/__tests__/wiki-docs-contract.test.js +6 -4
  103. package/dist/hooks/__tests__/wiki-docs-contract.test.js.map +1 -1
  104. package/dist/hooks/codebase-map.d.ts.map +1 -1
  105. package/dist/hooks/codebase-map.js +3 -2
  106. package/dist/hooks/codebase-map.js.map +1 -1
  107. package/dist/hooks/extensibility/dispatcher.d.ts.map +1 -1
  108. package/dist/hooks/extensibility/dispatcher.js +6 -4
  109. package/dist/hooks/extensibility/dispatcher.js.map +1 -1
  110. package/dist/hooks/extensibility/logging.d.ts.map +1 -1
  111. package/dist/hooks/extensibility/logging.js +3 -2
  112. package/dist/hooks/extensibility/logging.js.map +1 -1
  113. package/dist/hooks/extensibility/sdk/paths.d.ts.map +1 -1
  114. package/dist/hooks/extensibility/sdk/paths.js +4 -3
  115. package/dist/hooks/extensibility/sdk/paths.js.map +1 -1
  116. package/dist/hooks/keyword-detector.d.ts.map +1 -1
  117. package/dist/hooks/keyword-detector.js +2 -4
  118. package/dist/hooks/keyword-detector.js.map +1 -1
  119. package/dist/hooks/session.d.ts.map +1 -1
  120. package/dist/hooks/session.js +22 -12
  121. package/dist/hooks/session.js.map +1 -1
  122. package/dist/hud/__tests__/hud-tmux-injection.test.js +8 -7
  123. package/dist/hud/__tests__/hud-tmux-injection.test.js.map +1 -1
  124. package/dist/hud/__tests__/reconcile.test.js +1 -1
  125. package/dist/hud/__tests__/state.test.js +24 -0
  126. package/dist/hud/__tests__/state.test.js.map +1 -1
  127. package/dist/hud/index.js +1 -1
  128. package/dist/hud/index.js.map +1 -1
  129. package/dist/hud/state.d.ts.map +1 -1
  130. package/dist/hud/state.js +22 -8
  131. package/dist/hud/state.js.map +1 -1
  132. package/dist/hud/tmux.js +1 -1
  133. package/dist/hud/tmux.js.map +1 -1
  134. package/dist/imagegen/continuation.d.ts +44 -0
  135. package/dist/imagegen/continuation.d.ts.map +1 -0
  136. package/dist/imagegen/continuation.js +220 -0
  137. package/dist/imagegen/continuation.js.map +1 -0
  138. package/dist/mcp/__tests__/bootstrap.test.js +47 -2
  139. package/dist/mcp/__tests__/bootstrap.test.js.map +1 -1
  140. package/dist/mcp/__tests__/server-lifecycle.test.js +49 -1
  141. package/dist/mcp/__tests__/server-lifecycle.test.js.map +1 -1
  142. package/dist/mcp/__tests__/state-server.test.js +145 -6
  143. package/dist/mcp/__tests__/state-server.test.js.map +1 -1
  144. package/dist/mcp/__tests__/wiki-server.test.js +97 -1
  145. package/dist/mcp/__tests__/wiki-server.test.js.map +1 -1
  146. package/dist/mcp/bootstrap.d.ts +2 -0
  147. package/dist/mcp/bootstrap.d.ts.map +1 -1
  148. package/dist/mcp/bootstrap.js +95 -15
  149. package/dist/mcp/bootstrap.js.map +1 -1
  150. package/dist/mcp/lifecycle-telemetry.d.ts +16 -0
  151. package/dist/mcp/lifecycle-telemetry.d.ts.map +1 -0
  152. package/dist/mcp/lifecycle-telemetry.js +95 -0
  153. package/dist/mcp/lifecycle-telemetry.js.map +1 -0
  154. package/dist/mcp/wiki-server.d.ts.map +1 -1
  155. package/dist/mcp/wiki-server.js +11 -2
  156. package/dist/mcp/wiki-server.js.map +1 -1
  157. package/dist/pipeline/__tests__/stages.test.js +274 -5
  158. package/dist/pipeline/__tests__/stages.test.js.map +1 -1
  159. package/dist/pipeline/stages/team-exec.d.ts +2 -0
  160. package/dist/pipeline/stages/team-exec.d.ts.map +1 -1
  161. package/dist/pipeline/stages/team-exec.js +51 -26
  162. package/dist/pipeline/stages/team-exec.js.map +1 -1
  163. package/dist/planning/__tests__/artifacts.test.js +138 -3
  164. package/dist/planning/__tests__/artifacts.test.js.map +1 -1
  165. package/dist/planning/__tests__/context-pack-status.test.d.ts +2 -0
  166. package/dist/planning/__tests__/context-pack-status.test.d.ts.map +1 -0
  167. package/dist/planning/__tests__/context-pack-status.test.js +271 -0
  168. package/dist/planning/__tests__/context-pack-status.test.js.map +1 -0
  169. package/dist/planning/artifacts.d.ts +12 -1
  170. package/dist/planning/artifacts.d.ts.map +1 -1
  171. package/dist/planning/artifacts.js +32 -9
  172. package/dist/planning/artifacts.js.map +1 -1
  173. package/dist/planning/context-pack-status.d.ts +42 -0
  174. package/dist/planning/context-pack-status.d.ts.map +1 -0
  175. package/dist/planning/context-pack-status.js +479 -0
  176. package/dist/planning/context-pack-status.js.map +1 -0
  177. package/dist/runtime/__tests__/process-tree.test.d.ts +2 -0
  178. package/dist/runtime/__tests__/process-tree.test.d.ts.map +1 -0
  179. package/dist/runtime/__tests__/process-tree.test.js +107 -0
  180. package/dist/runtime/__tests__/process-tree.test.js.map +1 -0
  181. package/dist/runtime/process-tree.d.ts +28 -0
  182. package/dist/runtime/process-tree.d.ts.map +1 -0
  183. package/dist/runtime/process-tree.js +230 -0
  184. package/dist/runtime/process-tree.js.map +1 -0
  185. package/dist/scripts/__tests__/codex-native-hook.test.js +267 -2
  186. package/dist/scripts/__tests__/codex-native-hook.test.js.map +1 -1
  187. package/dist/scripts/__tests__/notify-state-io.test.d.ts +2 -0
  188. package/dist/scripts/__tests__/notify-state-io.test.d.ts.map +1 -0
  189. package/dist/scripts/__tests__/notify-state-io.test.js +40 -0
  190. package/dist/scripts/__tests__/notify-state-io.test.js.map +1 -0
  191. package/dist/scripts/codex-execution-surface.d.ts +1 -1
  192. package/dist/scripts/codex-execution-surface.d.ts.map +1 -1
  193. package/dist/scripts/codex-execution-surface.js.map +1 -1
  194. package/dist/scripts/codex-native-hook.d.ts +1 -1
  195. package/dist/scripts/codex-native-hook.d.ts.map +1 -1
  196. package/dist/scripts/codex-native-hook.js +141 -9
  197. package/dist/scripts/codex-native-hook.js.map +1 -1
  198. package/dist/scripts/notify-hook/managed-tmux.d.ts.map +1 -1
  199. package/dist/scripts/notify-hook/managed-tmux.js +6 -9
  200. package/dist/scripts/notify-hook/managed-tmux.js.map +1 -1
  201. package/dist/scripts/notify-hook/process-runner.d.ts.map +1 -1
  202. package/dist/scripts/notify-hook/process-runner.js +4 -1
  203. package/dist/scripts/notify-hook/process-runner.js.map +1 -1
  204. package/dist/scripts/notify-hook/state-io.d.ts.map +1 -1
  205. package/dist/scripts/notify-hook/state-io.js +4 -7
  206. package/dist/scripts/notify-hook/state-io.js.map +1 -1
  207. package/dist/scripts/notify-hook.js +25 -3
  208. package/dist/scripts/notify-hook.js.map +1 -1
  209. package/dist/scripts/verify-native-agents.d.ts.map +1 -1
  210. package/dist/scripts/verify-native-agents.js +3 -1
  211. package/dist/scripts/verify-native-agents.js.map +1 -1
  212. package/dist/sidecar/__tests__/tmux.test.js +1 -1
  213. package/dist/sidecar/__tests__/tmux.test.js.map +1 -1
  214. package/dist/sidecar/tmux.js +1 -1
  215. package/dist/sidecar/tmux.js.map +1 -1
  216. package/dist/state/__tests__/operations.test.js +79 -0
  217. package/dist/state/__tests__/operations.test.js.map +1 -1
  218. package/dist/state/__tests__/skill-active.test.js +10 -18
  219. package/dist/state/__tests__/skill-active.test.js.map +1 -1
  220. package/dist/state/__tests__/workflow-transition.test.js +45 -1
  221. package/dist/state/__tests__/workflow-transition.test.js.map +1 -1
  222. package/dist/state/operations.d.ts.map +1 -1
  223. package/dist/state/operations.js +1 -20
  224. package/dist/state/operations.js.map +1 -1
  225. package/dist/state/skill-active.d.ts +1 -0
  226. package/dist/state/skill-active.d.ts.map +1 -1
  227. package/dist/state/skill-active.js +28 -18
  228. package/dist/state/skill-active.js.map +1 -1
  229. package/dist/state/workflow-transition-reconcile.d.ts.map +1 -1
  230. package/dist/state/workflow-transition-reconcile.js +3 -2
  231. package/dist/state/workflow-transition-reconcile.js.map +1 -1
  232. package/dist/state/workflow-transition.js +2 -2
  233. package/dist/state/workflow-transition.js.map +1 -1
  234. package/dist/team/__tests__/approved-execution.test.js +96 -0
  235. package/dist/team/__tests__/approved-execution.test.js.map +1 -1
  236. package/dist/team/__tests__/followup-planner.test.js +16 -0
  237. package/dist/team/__tests__/followup-planner.test.js.map +1 -1
  238. package/dist/team/__tests__/model-contract.test.js +16 -0
  239. package/dist/team/__tests__/model-contract.test.js.map +1 -1
  240. package/dist/team/__tests__/repo-aware-decomposition.test.js +20 -0
  241. package/dist/team/__tests__/repo-aware-decomposition.test.js.map +1 -1
  242. package/dist/team/__tests__/runtime-cli.test.js +16 -0
  243. package/dist/team/__tests__/runtime-cli.test.js.map +1 -1
  244. package/dist/team/__tests__/runtime.test.js +209 -11
  245. package/dist/team/__tests__/runtime.test.js.map +1 -1
  246. package/dist/team/__tests__/scaling.test.js +110 -0
  247. package/dist/team/__tests__/scaling.test.js.map +1 -1
  248. package/dist/team/__tests__/tmux-session.test.js +9 -0
  249. package/dist/team/__tests__/tmux-session.test.js.map +1 -1
  250. package/dist/team/__tests__/worker-runtime-identity.test.js +6 -0
  251. package/dist/team/__tests__/worker-runtime-identity.test.js.map +1 -1
  252. package/dist/team/approved-execution.d.ts +13 -0
  253. package/dist/team/approved-execution.d.ts.map +1 -1
  254. package/dist/team/approved-execution.js +40 -22
  255. package/dist/team/approved-execution.js.map +1 -1
  256. package/dist/team/followup-planner.d.ts +1 -0
  257. package/dist/team/followup-planner.d.ts.map +1 -1
  258. package/dist/team/followup-planner.js +9 -9
  259. package/dist/team/followup-planner.js.map +1 -1
  260. package/dist/team/model-contract.d.ts +1 -1
  261. package/dist/team/model-contract.d.ts.map +1 -1
  262. package/dist/team/model-contract.js +4 -3
  263. package/dist/team/model-contract.js.map +1 -1
  264. package/dist/team/repo-aware-decomposition.d.ts +1 -0
  265. package/dist/team/repo-aware-decomposition.d.ts.map +1 -1
  266. package/dist/team/repo-aware-decomposition.js +5 -1
  267. package/dist/team/repo-aware-decomposition.js.map +1 -1
  268. package/dist/team/runtime-cli.d.ts +4 -0
  269. package/dist/team/runtime-cli.d.ts.map +1 -1
  270. package/dist/team/runtime-cli.js +14 -1
  271. package/dist/team/runtime-cli.js.map +1 -1
  272. package/dist/team/runtime.d.ts +1 -0
  273. package/dist/team/runtime.d.ts.map +1 -1
  274. package/dist/team/runtime.js +46 -16
  275. package/dist/team/runtime.js.map +1 -1
  276. package/dist/team/scaling.d.ts.map +1 -1
  277. package/dist/team/scaling.js +13 -6
  278. package/dist/team/scaling.js.map +1 -1
  279. package/dist/team/tmux-session.d.ts.map +1 -1
  280. package/dist/team/tmux-session.js +7 -0
  281. package/dist/team/tmux-session.js.map +1 -1
  282. package/dist/ultragoal/__tests__/artifacts.test.js +129 -4
  283. package/dist/ultragoal/__tests__/artifacts.test.js.map +1 -1
  284. package/dist/ultragoal/__tests__/docs-contract.test.d.ts +2 -0
  285. package/dist/ultragoal/__tests__/docs-contract.test.d.ts.map +1 -0
  286. package/dist/ultragoal/__tests__/docs-contract.test.js +31 -0
  287. package/dist/ultragoal/__tests__/docs-contract.test.js.map +1 -0
  288. package/dist/ultragoal/artifacts.d.ts +6 -2
  289. package/dist/ultragoal/artifacts.d.ts.map +1 -1
  290. package/dist/ultragoal/artifacts.js +108 -4
  291. package/dist/ultragoal/artifacts.js.map +1 -1
  292. package/dist/utils/paths.d.ts +3 -1
  293. package/dist/utils/paths.d.ts.map +1 -1
  294. package/dist/utils/paths.js +6 -2
  295. package/dist/utils/paths.js.map +1 -1
  296. package/dist/verification/__tests__/ci-rust-gates.test.js +44 -14
  297. package/dist/verification/__tests__/ci-rust-gates.test.js.map +1 -1
  298. package/dist/wiki/__tests__/ingest.test.js +35 -1
  299. package/dist/wiki/__tests__/ingest.test.js.map +1 -1
  300. package/dist/wiki/__tests__/lint.test.js +14 -1
  301. package/dist/wiki/__tests__/lint.test.js.map +1 -1
  302. package/dist/wiki/__tests__/query.test.js +28 -3
  303. package/dist/wiki/__tests__/query.test.js.map +1 -1
  304. package/dist/wiki/__tests__/session-hooks.test.js +30 -2
  305. package/dist/wiki/__tests__/session-hooks.test.js.map +1 -1
  306. package/dist/wiki/__tests__/storage.test.js +62 -22
  307. package/dist/wiki/__tests__/storage.test.js.map +1 -1
  308. package/dist/wiki/index.d.ts +2 -2
  309. package/dist/wiki/index.d.ts.map +1 -1
  310. package/dist/wiki/index.js +2 -2
  311. package/dist/wiki/index.js.map +1 -1
  312. package/dist/wiki/ingest.js +2 -2
  313. package/dist/wiki/ingest.js.map +1 -1
  314. package/dist/wiki/lifecycle.d.ts +5 -0
  315. package/dist/wiki/lifecycle.d.ts.map +1 -1
  316. package/dist/wiki/lifecycle.js +31 -4
  317. package/dist/wiki/lifecycle.js.map +1 -1
  318. package/dist/wiki/lint.d.ts.map +1 -1
  319. package/dist/wiki/lint.js +12 -8
  320. package/dist/wiki/lint.js.map +1 -1
  321. package/dist/wiki/query.d.ts.map +1 -1
  322. package/dist/wiki/query.js +3 -2
  323. package/dist/wiki/query.js.map +1 -1
  324. package/dist/wiki/storage.d.ts +4 -0
  325. package/dist/wiki/storage.d.ts.map +1 -1
  326. package/dist/wiki/storage.js +54 -18
  327. package/dist/wiki/storage.js.map +1 -1
  328. package/package.json +1 -1
  329. package/plugins/oh-my-codex/.codex-plugin/plugin.json +1 -1
  330. package/plugins/oh-my-codex/skills/ai-slop-cleaner/SKILL.md +9 -0
  331. package/plugins/oh-my-codex/skills/deep-interview/SKILL.md +25 -2
  332. package/plugins/oh-my-codex/skills/omx-setup/SKILL.md +1 -1
  333. package/plugins/oh-my-codex/skills/plan/SKILL.md +7 -4
  334. package/plugins/oh-my-codex/skills/ralplan/SKILL.md +13 -3
  335. package/plugins/oh-my-codex/skills/team/SKILL.md +2 -2
  336. package/plugins/oh-my-codex/skills/ultragoal/SKILL.md +11 -7
  337. package/plugins/oh-my-codex/skills/visual-ralph/SKILL.md +8 -0
  338. package/plugins/oh-my-codex/skills/wiki/SKILL.md +5 -5
  339. package/prompts/planner.md +1 -1
  340. package/skills/ai-slop-cleaner/SKILL.md +9 -0
  341. package/skills/deep-interview/SKILL.md +25 -2
  342. package/skills/omx-setup/SKILL.md +1 -1
  343. package/skills/plan/SKILL.md +7 -4
  344. package/skills/ralplan/SKILL.md +13 -3
  345. package/skills/team/SKILL.md +2 -2
  346. package/skills/ultragoal/SKILL.md +11 -7
  347. package/skills/visual-ralph/SKILL.md +8 -0
  348. package/skills/wiki/SKILL.md +5 -5
  349. package/src/scripts/__tests__/codex-native-hook.test.ts +302 -2
  350. package/src/scripts/__tests__/notify-state-io.test.ts +73 -0
  351. package/src/scripts/codex-execution-surface.ts +2 -0
  352. package/src/scripts/codex-native-hook.ts +163 -16
  353. package/src/scripts/notify-hook/managed-tmux.ts +6 -7
  354. package/src/scripts/notify-hook/process-runner.ts +4 -1
  355. package/src/scripts/notify-hook/state-io.ts +5 -7
  356. package/src/scripts/notify-hook.ts +26 -3
  357. package/src/scripts/verify-native-agents.ts +3 -1
package/Cargo.lock CHANGED
@@ -32,14 +32,14 @@ checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79"
32
32
 
33
33
  [[package]]
34
34
  name = "omx-explore-harness"
35
- version = "0.16.0"
35
+ version = "0.16.2"
36
36
  dependencies = [
37
37
  "libc",
38
38
  ]
39
39
 
40
40
  [[package]]
41
41
  name = "omx-mux"
42
- version = "0.16.0"
42
+ version = "0.16.2"
43
43
  dependencies = [
44
44
  "serde",
45
45
  "serde_json",
@@ -47,7 +47,7 @@ dependencies = [
47
47
 
48
48
  [[package]]
49
49
  name = "omx-runtime"
50
- version = "0.16.0"
50
+ version = "0.16.2"
51
51
  dependencies = [
52
52
  "omx-mux",
53
53
  "omx-runtime-core",
@@ -56,7 +56,7 @@ dependencies = [
56
56
 
57
57
  [[package]]
58
58
  name = "omx-runtime-core"
59
- version = "0.16.0"
59
+ version = "0.16.2"
60
60
  dependencies = [
61
61
  "fs2",
62
62
  "serde",
@@ -65,7 +65,7 @@ dependencies = [
65
65
 
66
66
  [[package]]
67
67
  name = "omx-sparkshell"
68
- version = "0.16.0"
68
+ version = "0.16.2"
69
69
  dependencies = [
70
70
  "omx-mux",
71
71
  ]
package/Cargo.toml CHANGED
@@ -10,7 +10,7 @@ resolver = "2"
10
10
 
11
11
  [workspace.package]
12
12
 
13
- version = "0.16.0"
13
+ version = "0.16.2"
14
14
 
15
15
  edition = "2021"
16
16
  rust-version = "1.73"
package/README.md CHANGED
@@ -265,7 +265,7 @@ If `Shift+Enter` still submits instead of inserting a newline inside an OMX-mana
265
265
 
266
266
  - `omx explore --prompt "..."` is for read-only repository lookup
267
267
  - `omx sparkshell <command>` is for shell-native inspection and bounded verification
268
- - when `.omx/wiki/` exists, `omx explore` can inject wiki-first context before falling back to broader repository search
268
+ - when `omx_wiki/` exists, `omx explore` can inject wiki-first context before falling back to broader repository search
269
269
  - fallback boundaries are explicit: sparkshell-backend fallback is reported on stderr, and spark-model fallback emits stderr metadata plus an `## OMX Explore fallback` notice in stdout so users can see when cost/behavior may differ from the low-cost path
270
270
 
271
271
  Examples:
@@ -279,7 +279,7 @@ omx sparkshell --tmux-pane %12 --tail-lines 400
279
279
  ### Wiki
280
280
 
281
281
  - `omx wiki` is the CLI parity surface for the OMX wiki MCP server
282
- - wiki data lives locally under `.omx/wiki/`
282
+ - wiki data lives as repository project knowledge under `omx_wiki/`
283
283
  - the wiki is markdown-first and search-first, not vector-first
284
284
 
285
285
  Examples:
@@ -6,22 +6,35 @@ use std::fs::{
6
6
  use std::io::{self, BufRead, BufReader, Read};
7
7
  use std::path::{Path, PathBuf};
8
8
  use std::process::{Child, Command, Output, Stdio};
9
- use std::sync::mpsc::{self, Receiver, RecvTimeoutError};
9
+ use std::sync::mpsc::{self, Receiver, RecvTimeoutError, TryRecvError};
10
10
  use std::thread;
11
11
  use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
12
12
 
13
13
  const CODEX_BIN_ENV: &str = "OMX_EXPLORE_CODEX_BIN";
14
14
  const HARNESS_ROOT_ENV: &str = "OMX_EXPLORE_ROOT";
15
15
  const CODEX_TIMEOUT_MS_ENV: &str = "OMX_EXPLORE_CODEX_TIMEOUT_MS";
16
+ const PROCESS_LIMIT_ENV: &str = "OMX_EXPLORE_PROCESS_LIMIT";
17
+ const CODEX_OUTPUT_LIMIT_BYTES_ENV: &str = "OMX_EXPLORE_CODEX_OUTPUT_LIMIT_BYTES";
16
18
  const INTERNAL_DIRECT_WRAPPER_FLAG: &str = "--internal-allowlist-direct";
17
19
  const INTERNAL_SHELL_WRAPPER_FLAG: &str = "--internal-allowlist-shell";
18
20
  const TEMP_ALLOWLIST_DIR_PREFIX: &str = "omx-explore-allowlist-";
19
21
  const DEFAULT_CODEX_TIMEOUT_MS: u64 = 180_000;
22
+ const DEFAULT_PROCESS_LIMIT: usize = 96;
23
+ const DEFAULT_CODEX_OUTPUT_LIMIT_BYTES: usize = 8 * 1024 * 1024;
24
+ const PROCESS_LIMIT_POLL_MS: u64 = 100;
20
25
  const PROCESS_TERMINATION_GRACE_MS: u64 = 500;
21
26
  const PIPE_READER_READY_GRACE_MS: u64 = 25;
22
27
  const PIPE_READER_JOIN_GRACE_MS: u64 = 500;
23
- const EXPLORE_SUBPROCESS_ENV_VARS_TO_SCRUB: &[&str] =
24
- &["BASH_ENV", "ENV", "PROMPT_COMMAND", "NODE_OPTIONS"];
28
+ const EXPLORE_SUBPROCESS_ENV_VARS_TO_SCRUB: &[&str] = &[
29
+ "BASH_ENV",
30
+ "ENV",
31
+ "PROMPT_COMMAND",
32
+ "NODE_OPTIONS",
33
+ "SHELLOPTS",
34
+ "BASHOPTS",
35
+ "GREP_OPTIONS",
36
+ "GREP_COLORS",
37
+ ];
25
38
  const WINDOWS_UNSUPPORTED_ALLOWLIST_MESSAGE: &str =
26
39
  "omx explore built-in harness is not ready on Windows because its allowlist runtime relies on POSIX sh/bash wrappers. Set OMX_EXPLORE_BIN to a compatible custom harness, prefer `omx sparkshell` for shell-native read-only lookups, or run `omx doctor` for readiness details.";
27
40
 
@@ -334,13 +347,59 @@ fn invoke_codex(args: &Args, model: &str, prompt_contract: &str) -> io::Result<A
334
347
  ),
335
348
  output_markdown: None,
336
349
  }),
350
+ TimedCommandOutput::ProcessLimitExceeded {
351
+ stderr,
352
+ process_count,
353
+ process_limit,
354
+ } => Ok(AttemptResult {
355
+ status_code: 125,
356
+ stderr: format!(
357
+ "[omx explore] codex exec exceeded per-run process limit ({process_count}>{process_limit}); terminated process tree to avoid runaway shell storms{}{}",
358
+ if stderr.trim().is_empty() {
359
+ ""
360
+ } else {
361
+ ". stderr before termination: "
362
+ },
363
+ stderr.trim()
364
+ ),
365
+ output_markdown: None,
366
+ }),
367
+ TimedCommandOutput::OutputLimitExceeded {
368
+ stderr,
369
+ output_limit,
370
+ stream,
371
+ } => Ok(AttemptResult {
372
+ status_code: 126,
373
+ stderr: format!(
374
+ "[omx explore] codex exec exceeded subprocess {stream} output limit ({output_limit} bytes); terminated process tree to avoid unbounded memory growth{}{}",
375
+ if stderr.trim().is_empty() {
376
+ ""
377
+ } else {
378
+ ". stderr before termination: "
379
+ },
380
+ stderr.trim()
381
+ ),
382
+ output_markdown: None,
383
+ }),
337
384
  }
338
385
  }
339
386
 
340
387
  #[derive(Debug)]
341
388
  enum TimedCommandOutput {
342
389
  Completed(Output),
343
- TimedOut { stderr: String },
390
+ TimedOut {
391
+ stderr: String,
392
+ },
393
+ ProcessLimitExceeded {
394
+ stderr: String,
395
+ process_count: usize,
396
+ process_limit: usize,
397
+ },
398
+ OutputLimitExceeded {
399
+ stderr: String,
400
+ output_limit: usize,
401
+ stream: &'static str,
402
+ },
344
403
  }
345
404
 
346
405
  fn codex_timeout() -> Duration {
@@ -352,6 +411,22 @@ fn codex_timeout() -> Duration {
352
411
  Duration::from_millis(timeout_ms)
353
412
  }
354
413
 
414
+ fn codex_output_limit_bytes() -> usize {
415
+ env::var(CODEX_OUTPUT_LIMIT_BYTES_ENV)
416
+ .ok()
417
+ .and_then(|value| value.trim().parse::<usize>().ok())
418
+ .filter(|value| *value > 0)
419
+ .unwrap_or(DEFAULT_CODEX_OUTPUT_LIMIT_BYTES)
420
+ }
421
+
422
+ fn process_limit() -> usize {
423
+ env::var(PROCESS_LIMIT_ENV)
424
+ .ok()
425
+ .and_then(|value| value.trim().parse::<usize>().ok())
426
+ .filter(|value| *value > 0)
427
+ .unwrap_or(DEFAULT_PROCESS_LIMIT)
428
+ }
429
+
355
430
  fn run_command_with_timeout(
356
431
  mut command: Command,
357
432
  timeout: Duration,
@@ -360,19 +435,37 @@ fn run_command_with_timeout(
360
435
  configure_process_group(&mut command);
361
436
  let mut child = command.spawn()?;
362
437
 
363
- let stdout_reader = spawn_pipe_reader(child.stdout.take());
364
- let stderr_reader = spawn_pipe_reader(child.stderr.take());
438
+ let output_limit = codex_output_limit_bytes();
439
+ let mut stdout_reader = spawn_pipe_reader("stdout", child.stdout.take(), output_limit);
440
+ let mut stderr_reader = spawn_pipe_reader("stderr", child.stderr.take(), output_limit);
365
441
 
366
442
  let deadline = Instant::now() + timeout;
443
+ let process_limit = process_limit();
444
+ let mut next_process_limit_poll = Instant::now() + Duration::from_millis(PROCESS_LIMIT_POLL_MS);
367
445
  loop {
368
446
  if let Some(status) = child.try_wait()? {
369
- let (stdout, stderr) = collect_completed_output(
447
+ // The wrapper may exit while grandchildren keep the process group
448
+ // alive. Sweep it before collecting pipes so completed harness
449
+ // runs cannot leave detached shells behind.
450
+ terminate_child_process_tree(&mut child);
451
+ let output = collect_completed_output(
370
452
  &mut child,
371
- &stdout_reader,
372
- &stderr_reader,
453
+ &mut stdout_reader,
454
+ &mut stderr_reader,
373
455
  Duration::from_millis(PIPE_READER_READY_GRACE_MS),
374
456
  Duration::from_millis(PIPE_READER_JOIN_GRACE_MS),
375
- )?;
457
+ );
458
+ let (stdout, stderr) = match output {
459
+ Ok(output) => output,
460
+ Err(err) if is_output_limit_error(&err) => {
461
+ return Ok(TimedCommandOutput::OutputLimitExceeded {
462
+ stderr: String::new(),
463
+ output_limit,
464
+ stream: output_limit_stream(&err),
465
+ });
466
+ }
467
+ Err(err) => return Err(err),
468
+ };
376
469
  return Ok(TimedCommandOutput::Completed(Output {
377
470
  status,
378
471
  stdout,
@@ -384,37 +477,192 @@ fn run_command_with_timeout(
384
477
  terminate_child_process_tree(&mut child);
385
478
  let _ = child.wait();
386
479
  let reader_timeout = Duration::from_millis(PIPE_READER_JOIN_GRACE_MS);
387
- let _ = receive_pipe_reader(&stdout_reader, reader_timeout);
388
- let stderr = receive_pipe_reader(&stderr_reader, reader_timeout).unwrap_or_default();
480
+ let _ = receive_pipe_reader(&mut stdout_reader, reader_timeout);
481
+ let stderr =
482
+ receive_pipe_reader(&mut stderr_reader, reader_timeout).unwrap_or_default();
389
483
  return Ok(TimedCommandOutput::TimedOut {
390
484
  stderr: String::from_utf8_lossy(&stderr).into_owned(),
391
485
  });
392
486
  }
393
487
 
488
+ if Instant::now() >= next_process_limit_poll {
489
+ next_process_limit_poll = Instant::now() + Duration::from_millis(PROCESS_LIMIT_POLL_MS);
490
+ if let Some(process_count) = count_process_tree(child.id()) {
491
+ if process_count > process_limit {
492
+ terminate_child_process_tree(&mut child);
493
+ let _ = child.wait();
494
+ let reader_timeout = Duration::from_millis(PIPE_READER_JOIN_GRACE_MS);
495
+ let _ = receive_pipe_reader(&mut stdout_reader, reader_timeout);
496
+ let stderr =
497
+ receive_pipe_reader(&mut stderr_reader, reader_timeout).unwrap_or_default();
498
+ return Ok(TimedCommandOutput::ProcessLimitExceeded {
499
+ stderr: String::from_utf8_lossy(&stderr).into_owned(),
500
+ process_count,
501
+ process_limit,
502
+ });
503
+ }
504
+ }
505
+ }
506
+
507
+ if let Some(stream) = poll_output_limit(&mut stdout_reader, &mut stderr_reader)? {
508
+ terminate_child_process_tree(&mut child);
509
+ let _ = child.wait();
510
+ let reader_timeout = Duration::from_millis(PIPE_READER_JOIN_GRACE_MS);
511
+ let stderr = if stream == "stderr" {
512
+ Vec::new()
513
+ } else {
514
+ receive_pipe_reader(&mut stderr_reader, reader_timeout).unwrap_or_default()
515
+ };
516
+ return Ok(TimedCommandOutput::OutputLimitExceeded {
517
+ stderr: String::from_utf8_lossy(&stderr).into_owned(),
518
+ output_limit,
519
+ stream,
520
+ });
521
+ }
522
+
394
523
  thread::sleep(Duration::from_millis(25));
395
524
  }
396
525
  }
397
526
 
398
- fn spawn_pipe_reader<R: Read + Send + 'static>(pipe: Option<R>) -> Receiver<io::Result<Vec<u8>>> {
527
+ #[cfg(target_os = "linux")]
528
+ fn count_process_tree(root_pid: u32) -> Option<usize> {
529
+ use std::collections::HashMap;
530
+ let entries = std::fs::read_dir("/proc").ok()?;
531
+ let mut children: HashMap<u32, Vec<u32>> = HashMap::new();
532
+ for entry in entries.flatten() {
533
+ let name = entry.file_name();
534
+ let Some(name) = name.to_str() else {
535
+ continue;
536
+ };
537
+ let Ok(pid) = name.parse::<u32>() else {
538
+ continue;
539
+ };
540
+ let Ok(stat) = std::fs::read_to_string(format!("/proc/{pid}/stat")) else {
541
+ continue;
542
+ };
543
+ let Some(close_paren) = stat.rfind(')') else {
544
+ continue;
545
+ };
546
+ let fields: Vec<&str> = stat[close_paren + 2..].split(' ').collect();
547
+ let Some(ppid) = fields.get(1).and_then(|field| field.parse::<u32>().ok()) else {
548
+ continue;
549
+ };
550
+ children.entry(ppid).or_default().push(pid);
551
+ }
552
+ let mut count = 1;
553
+ let mut stack = children.remove(&root_pid).unwrap_or_default();
554
+ while let Some(pid) = stack.pop() {
555
+ count += 1;
556
+ if let Some(mut nested) = children.remove(&pid) {
557
+ stack.append(&mut nested);
558
+ }
559
+ }
560
+ Some(count)
561
+ }
562
+
563
+ #[cfg(not(target_os = "linux"))]
564
+ fn count_process_tree(_root_pid: u32) -> Option<usize> {
565
+ None
566
+ }
567
+
568
+ struct PipeReader {
569
+ receiver: Receiver<io::Result<Vec<u8>>>,
570
+ cached: Option<io::Result<Vec<u8>>>,
571
+ }
572
+
573
+ fn spawn_pipe_reader<R: Read + Send + 'static>(
574
+ stream: &'static str,
575
+ pipe: Option<R>,
576
+ output_limit: usize,
577
+ ) -> PipeReader {
399
578
  let (sender, receiver) = mpsc::channel();
400
579
  thread::spawn(move || {
401
- let _ = sender.send(read_pipe_to_end(pipe));
580
+ let _ = sender.send(read_pipe_bounded(pipe, stream, output_limit));
402
581
  });
403
- receiver
582
+ PipeReader {
583
+ receiver,
584
+ cached: None,
585
+ }
404
586
  }
405
587
 
406
- fn read_pipe_to_end<R: Read + Send + 'static>(pipe: Option<R>) -> io::Result<Vec<u8>> {
588
+ fn read_pipe_bounded<R: Read + Send + 'static>(
589
+ pipe: Option<R>,
590
+ stream: &'static str,
591
+ output_limit: usize,
592
+ ) -> io::Result<Vec<u8>> {
407
593
  let mut bytes = Vec::new();
408
- if let Some(mut pipe) = pipe {
409
- pipe.read_to_end(&mut bytes)?;
594
+ let Some(pipe) = pipe else {
595
+ return Ok(bytes);
596
+ };
597
+ let mut reader = BufReader::new(pipe);
598
+ let mut chunk = [0_u8; 8192];
599
+ loop {
600
+ let read = reader.read(&mut chunk)?;
601
+ if read == 0 {
602
+ return Ok(bytes);
603
+ }
604
+ if bytes.len().saturating_add(read) > output_limit {
605
+ return Err(output_limit_error(stream, output_limit));
606
+ }
607
+ bytes.extend_from_slice(&chunk[..read]);
608
+ }
609
+ }
610
+
611
+ fn output_limit_error(stream: &'static str, output_limit: usize) -> io::Error {
612
+ io::Error::new(
613
+ io::ErrorKind::Other,
614
+ format!("subprocess {stream} exceeded output limit of {output_limit} bytes"),
615
+ )
616
+ }
617
+
618
+ fn is_output_limit_error(err: &io::Error) -> bool {
619
+ err.to_string().contains("exceeded output limit")
620
+ }
621
+
622
+ fn output_limit_stream(err: &io::Error) -> &'static str {
623
+ if err.to_string().contains("stderr") {
624
+ "stderr"
625
+ } else {
626
+ "stdout"
627
+ }
628
+ }
629
+
630
+ fn poll_output_limit(
631
+ stdout_reader: &mut PipeReader,
632
+ stderr_reader: &mut PipeReader,
633
+ ) -> io::Result<Option<&'static str>> {
634
+ if let Some(stream) = poll_one_output_limit("stdout", stdout_reader)? {
635
+ return Ok(Some(stream));
636
+ }
637
+ poll_one_output_limit("stderr", stderr_reader)
638
+ }
639
+
640
+ fn poll_one_output_limit(
641
+ stream: &'static str,
642
+ reader: &mut PipeReader,
643
+ ) -> io::Result<Option<&'static str>> {
644
+ if reader.cached.is_some() {
645
+ return Ok(None);
646
+ }
647
+ match reader.receiver.try_recv() {
648
+ Ok(Ok(bytes)) => {
649
+ reader.cached = Some(Ok(bytes));
650
+ Ok(None)
651
+ }
652
+ Ok(Err(err)) if is_output_limit_error(&err) => Ok(Some(stream)),
653
+ Ok(Err(err)) => Err(err),
654
+ Err(TryRecvError::Empty) => Ok(None),
655
+ Err(TryRecvError::Disconnected) => Err(io::Error::new(
656
+ io::ErrorKind::Other,
657
+ "subprocess output reader disconnected",
658
+ )),
410
659
  }
411
- Ok(bytes)
412
660
  }
413
661
 
414
662
  fn collect_completed_output(
415
663
  child: &mut Child,
416
- stdout_reader: &Receiver<io::Result<Vec<u8>>>,
417
- stderr_reader: &Receiver<io::Result<Vec<u8>>>,
664
+ stdout_reader: &mut PipeReader,
665
+ stderr_reader: &mut PipeReader,
418
666
  ready_timeout: Duration,
419
667
  cleanup_timeout: Duration,
420
668
  ) -> io::Result<(Vec<u8>, Vec<u8>)> {
@@ -438,21 +686,21 @@ fn collect_completed_output(
438
686
  }
439
687
 
440
688
  fn receive_pipe_reader_if_ready(
441
- receiver: &Receiver<io::Result<Vec<u8>>>,
689
+ reader: &mut PipeReader,
442
690
  timeout: Duration,
443
691
  ) -> io::Result<Option<Vec<u8>>> {
444
- match receive_pipe_reader(receiver, timeout) {
692
+ match receive_pipe_reader(reader, timeout) {
445
693
  Ok(bytes) => Ok(Some(bytes)),
446
694
  Err(err) if err.kind() == io::ErrorKind::TimedOut => Ok(None),
447
695
  Err(err) => Err(err),
448
696
  }
449
697
  }
450
698
 
451
- fn receive_pipe_reader(
452
- receiver: &Receiver<io::Result<Vec<u8>>>,
453
- timeout: Duration,
454
- ) -> io::Result<Vec<u8>> {
455
- match receiver.recv_timeout(timeout) {
699
+ fn receive_pipe_reader(reader: &mut PipeReader, timeout: Duration) -> io::Result<Vec<u8>> {
700
+ if let Some(result) = reader.cached.take() {
701
+ return result;
702
+ }
703
+ match reader.receiver.recv_timeout(timeout) {
456
704
  Ok(result) => result,
457
705
  Err(RecvTimeoutError::Timeout) => Err(io::Error::new(
458
706
  io::ErrorKind::TimedOut,
@@ -2476,6 +2724,126 @@ sleep 30
2476
2724
  assert_eq!(read_to_string(&term_file).unwrap_or_default(), "term");
2477
2725
  }
2478
2726
 
2727
+ #[cfg(target_os = "linux")]
2728
+ #[test]
2729
+ fn run_command_with_timeout_aborts_suspicious_process_storm() {
2730
+ let _env_guard = env_lock();
2731
+ let _process_guard = process_tree_lock();
2732
+ let root = temp_allowlist_dir().expect("temp root");
2733
+ let script = root.path.join("storm.sh");
2734
+ write_executable(
2735
+ &script,
2736
+ r#"#!/bin/sh
2737
+ while :; do
2738
+ sleep 30 &
2739
+ sleep 0.01
2740
+ done
2741
+ "#,
2742
+ )
2743
+ .expect("write script");
2744
+
2745
+ unsafe {
2746
+ env::set_var(PROCESS_LIMIT_ENV, "12");
2747
+ }
2748
+ let started = Instant::now();
2749
+ let result = run_command_with_timeout(Command::new(&script), Duration::from_secs(10))
2750
+ .expect("run with process storm");
2751
+ unsafe {
2752
+ env::remove_var(PROCESS_LIMIT_ENV);
2753
+ }
2754
+
2755
+ let TimedCommandOutput::ProcessLimitExceeded {
2756
+ process_count,
2757
+ process_limit,
2758
+ ..
2759
+ } = result
2760
+ else {
2761
+ panic!("expected process limit failure");
2762
+ };
2763
+ assert!(process_count > process_limit);
2764
+ assert!(started.elapsed() < Duration::from_secs(5));
2765
+ }
2766
+
2767
+ #[cfg(unix)]
2768
+ #[test]
2769
+ fn run_command_with_timeout_fails_closed_on_large_stdout() {
2770
+ let _env_guard = env_lock();
2771
+ let _process_guard = process_tree_lock();
2772
+ let root = temp_allowlist_dir().expect("temp root");
2773
+ let script = root.path.join("large-stdout.sh");
2774
+ write_executable(
2775
+ &script,
2776
+ r#"#!/bin/sh
2777
+ while :; do
2778
+ printf 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'
2779
+ done
2780
+ "#,
2781
+ )
2782
+ .expect("write script");
2783
+
2784
+ unsafe {
2785
+ env::set_var(CODEX_OUTPUT_LIMIT_BYTES_ENV, "4096");
2786
+ }
2787
+ let started = Instant::now();
2788
+ let result = run_command_with_timeout(Command::new(&script), Duration::from_secs(10))
2789
+ .expect("run with large stdout");
2790
+ unsafe {
2791
+ env::remove_var(CODEX_OUTPUT_LIMIT_BYTES_ENV);
2792
+ }
2793
+
2794
+ let TimedCommandOutput::OutputLimitExceeded {
2795
+ stream,
2796
+ output_limit,
2797
+ ..
2798
+ } = result
2799
+ else {
2800
+ panic!("expected stdout output limit failure");
2801
+ };
2802
+ assert_eq!(stream, "stdout");
2803
+ assert_eq!(output_limit, 4096);
2804
+ assert!(started.elapsed() < Duration::from_secs(3));
2805
+ }
2806
+
2807
+ #[cfg(unix)]
2808
+ #[test]
2809
+ fn run_command_with_timeout_fails_closed_on_large_stderr() {
2810
+ let _env_guard = env_lock();
2811
+ let _process_guard = process_tree_lock();
2812
+ let root = temp_allowlist_dir().expect("temp root");
2813
+ let script = root.path.join("large-stderr.sh");
2814
+ write_executable(
2815
+ &script,
2816
+ r#"#!/bin/sh
2817
+ while :; do
2818
+ printf 'eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee' >&2
2819
+ done
2820
+ "#,
2821
+ )
2822
+ .expect("write script");
2823
+
2824
+ unsafe {
2825
+ env::set_var(CODEX_OUTPUT_LIMIT_BYTES_ENV, "4096");
2826
+ }
2827
+ let started = Instant::now();
2828
+ let result = run_command_with_timeout(Command::new(&script), Duration::from_secs(10))
2829
+ .expect("run with large stderr");
2830
+ unsafe {
2831
+ env::remove_var(CODEX_OUTPUT_LIMIT_BYTES_ENV);
2832
+ }
2833
+
2834
+ let TimedCommandOutput::OutputLimitExceeded {
2835
+ stream,
2836
+ output_limit,
2837
+ ..
2838
+ } = result
2839
+ else {
2840
+ panic!("expected stderr output limit failure");
2841
+ };
2842
+ assert_eq!(stream, "stderr");
2843
+ assert_eq!(output_limit, 4096);
2844
+ assert!(started.elapsed() < Duration::from_secs(3));
2845
+ }
2846
+
2479
2847
  #[cfg(unix)]
2480
2848
  #[test]
2481
2849
  fn run_command_with_timeout_closes_inherited_stdio_after_parent_exit() {
@@ -2510,6 +2878,44 @@ exit 0
2510
2878
  assert_eq!(String::from_utf8_lossy(&output.stderr), "parent stderr\n");
2511
2879
  }
2512
2880
 
2881
+ #[cfg(unix)]
2882
+ #[test]
2883
+ fn run_command_with_timeout_sweeps_detached_grandchildren_after_parent_exit() {
2884
+ let _env_guard = env_lock();
2885
+ let _process_guard = process_tree_lock();
2886
+ let root = temp_allowlist_dir().expect("temp root");
2887
+ let term_file = root.path.join("orphan.term");
2888
+ let ready_file = root.path.join("orphan.ready");
2889
+ let script = root.path.join("spawn-detached-grandchild.sh");
2890
+ write_executable(
2891
+ &script,
2892
+ &format!(
2893
+ r#"#!/bin/sh
2894
+ (trap 'printf term > {}; exit 0' TERM; printf ready > {}; sleep 30) >/dev/null 2>&1 &
2895
+ while [ ! -f {} ]; do
2896
+ sleep 0.01
2897
+ done
2898
+ printf 'parent done\n'
2899
+ exit 0
2900
+ "#,
2901
+ shell_quote(&term_file.display().to_string()),
2902
+ shell_quote(&ready_file.display().to_string()),
2903
+ shell_quote(&ready_file.display().to_string()),
2904
+ ),
2905
+ )
2906
+ .expect("write script");
2907
+
2908
+ let result = run_command_with_timeout(Command::new(&script), Duration::from_secs(10))
2909
+ .expect("run with detached grandchild");
2910
+
2911
+ let TimedCommandOutput::Completed(output) = result else {
2912
+ panic!("expected parent completion");
2913
+ };
2914
+ assert!(output.status.success());
2915
+ assert_eq!(String::from_utf8_lossy(&output.stdout), "parent done\n");
2916
+ assert_eq!(read_to_string(&term_file).unwrap_or_default(), "term");
2917
+ }
2918
+
2513
2919
  fn fallback_test_event() -> FallbackEvent {
2514
2920
  FallbackEvent {
2515
2921
  from_model: "spark-model".to_string(),