oh-my-codex 0.15.3 → 0.16.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 (610) hide show
  1. package/Cargo.lock +10 -7
  2. package/Cargo.toml +1 -1
  3. package/README.md +3 -0
  4. package/crates/omx-explore/Cargo.toml +3 -0
  5. package/crates/omx-explore/src/main.rs +923 -16
  6. package/dist/agents/__tests__/native-config.test.js +50 -0
  7. package/dist/agents/__tests__/native-config.test.js.map +1 -1
  8. package/dist/agents/native-config.d.ts.map +1 -1
  9. package/dist/agents/native-config.js +3 -2
  10. package/dist/agents/native-config.js.map +1 -1
  11. package/dist/autoresearch/goal.d.ts +90 -0
  12. package/dist/autoresearch/goal.d.ts.map +1 -0
  13. package/dist/autoresearch/goal.js +237 -0
  14. package/dist/autoresearch/goal.js.map +1 -0
  15. package/dist/autoresearch/skill-validation.d.ts +1 -0
  16. package/dist/autoresearch/skill-validation.d.ts.map +1 -1
  17. package/dist/autoresearch/skill-validation.js +10 -3
  18. package/dist/autoresearch/skill-validation.js.map +1 -1
  19. package/dist/catalog/__tests__/generator.test.js +9 -4
  20. package/dist/catalog/__tests__/generator.test.js.map +1 -1
  21. package/dist/catalog/__tests__/plugin-bundle-ssot.test.js +20 -1
  22. package/dist/catalog/__tests__/plugin-bundle-ssot.test.js.map +1 -1
  23. package/dist/catalog/__tests__/schema.test.js +14 -3
  24. package/dist/catalog/__tests__/schema.test.js.map +1 -1
  25. package/dist/catalog/schema.js +1 -1
  26. package/dist/catalog/schema.js.map +1 -1
  27. package/dist/cli/__tests__/autoresearch-goal.test.d.ts +2 -0
  28. package/dist/cli/__tests__/autoresearch-goal.test.d.ts.map +1 -0
  29. package/dist/cli/__tests__/autoresearch-goal.test.js +194 -0
  30. package/dist/cli/__tests__/autoresearch-goal.test.js.map +1 -0
  31. package/dist/cli/__tests__/cleanup.test.js +82 -1
  32. package/dist/cli/__tests__/cleanup.test.js.map +1 -1
  33. package/dist/cli/__tests__/codex-plugin-layout.test.js +8 -4
  34. package/dist/cli/__tests__/codex-plugin-layout.test.js.map +1 -1
  35. package/dist/cli/__tests__/doctor-warning-copy.test.js +23 -0
  36. package/dist/cli/__tests__/doctor-warning-copy.test.js.map +1 -1
  37. package/dist/cli/__tests__/explore.test.js +126 -2
  38. package/dist/cli/__tests__/explore.test.js.map +1 -1
  39. package/dist/cli/__tests__/imagegen-continuation.test.d.ts +2 -0
  40. package/dist/cli/__tests__/imagegen-continuation.test.d.ts.map +1 -0
  41. package/dist/cli/__tests__/imagegen-continuation.test.js +135 -0
  42. package/dist/cli/__tests__/imagegen-continuation.test.js.map +1 -0
  43. package/dist/cli/__tests__/index.test.js +263 -20
  44. package/dist/cli/__tests__/index.test.js.map +1 -1
  45. package/dist/cli/__tests__/launch-fallback.test.js +146 -2
  46. package/dist/cli/__tests__/launch-fallback.test.js.map +1 -1
  47. package/dist/cli/__tests__/native-assets.test.js +26 -1
  48. package/dist/cli/__tests__/native-assets.test.js.map +1 -1
  49. package/dist/cli/__tests__/package-bin-contract.test.js +2 -2
  50. package/dist/cli/__tests__/package-bin-contract.test.js.map +1 -1
  51. package/dist/cli/__tests__/performance-goal.test.d.ts +2 -0
  52. package/dist/cli/__tests__/performance-goal.test.d.ts.map +1 -0
  53. package/dist/cli/__tests__/performance-goal.test.js +144 -0
  54. package/dist/cli/__tests__/performance-goal.test.js.map +1 -0
  55. package/dist/cli/__tests__/question.test.js +8 -0
  56. package/dist/cli/__tests__/question.test.js.map +1 -1
  57. package/dist/cli/__tests__/ralph-goal-mode-contract.test.d.ts +2 -0
  58. package/dist/cli/__tests__/ralph-goal-mode-contract.test.d.ts.map +1 -0
  59. package/dist/cli/__tests__/ralph-goal-mode-contract.test.js +31 -0
  60. package/dist/cli/__tests__/ralph-goal-mode-contract.test.js.map +1 -0
  61. package/dist/cli/__tests__/ralph-prd-deep-interview.test.js +5 -4
  62. package/dist/cli/__tests__/ralph-prd-deep-interview.test.js.map +1 -1
  63. package/dist/cli/__tests__/ralph-prd-smoke.test.js +7 -0
  64. package/dist/cli/__tests__/ralph-prd-smoke.test.js.map +1 -1
  65. package/dist/cli/__tests__/ralph.test.js +62 -0
  66. package/dist/cli/__tests__/ralph.test.js.map +1 -1
  67. package/dist/cli/__tests__/setup-install-mode.test.js +102 -21
  68. package/dist/cli/__tests__/setup-install-mode.test.js.map +1 -1
  69. package/dist/cli/__tests__/setup-refresh.test.js +27 -8
  70. package/dist/cli/__tests__/setup-refresh.test.js.map +1 -1
  71. package/dist/cli/__tests__/setup-scope.test.js +18 -9
  72. package/dist/cli/__tests__/setup-scope.test.js.map +1 -1
  73. package/dist/cli/__tests__/setup-skill-validation.test.js +11 -11
  74. package/dist/cli/__tests__/setup-skill-validation.test.js.map +1 -1
  75. package/dist/cli/__tests__/setup-skills-overwrite.test.js +12 -12
  76. package/dist/cli/__tests__/setup-skills-overwrite.test.js.map +1 -1
  77. package/dist/cli/__tests__/team.test.js +646 -6
  78. package/dist/cli/__tests__/team.test.js.map +1 -1
  79. package/dist/cli/__tests__/ultragoal.test.d.ts +2 -0
  80. package/dist/cli/__tests__/ultragoal.test.d.ts.map +1 -0
  81. package/dist/cli/__tests__/ultragoal.test.js +146 -0
  82. package/dist/cli/__tests__/ultragoal.test.js.map +1 -0
  83. package/dist/cli/__tests__/uninstall.test.js +11 -0
  84. package/dist/cli/__tests__/uninstall.test.js.map +1 -1
  85. package/dist/cli/autoresearch-goal.d.ts +3 -0
  86. package/dist/cli/autoresearch-goal.d.ts.map +1 -0
  87. package/dist/cli/autoresearch-goal.js +175 -0
  88. package/dist/cli/autoresearch-goal.js.map +1 -0
  89. package/dist/cli/cleanup.d.ts +3 -1
  90. package/dist/cli/cleanup.d.ts.map +1 -1
  91. package/dist/cli/cleanup.js +42 -2
  92. package/dist/cli/cleanup.js.map +1 -1
  93. package/dist/cli/doctor.d.ts.map +1 -1
  94. package/dist/cli/doctor.js +49 -0
  95. package/dist/cli/doctor.js.map +1 -1
  96. package/dist/cli/explore.d.ts.map +1 -1
  97. package/dist/cli/explore.js +218 -10
  98. package/dist/cli/explore.js.map +1 -1
  99. package/dist/cli/index.d.ts +15 -3
  100. package/dist/cli/index.d.ts.map +1 -1
  101. package/dist/cli/index.js +266 -33
  102. package/dist/cli/index.js.map +1 -1
  103. package/dist/cli/native-assets.js +1 -1
  104. package/dist/cli/native-assets.js.map +1 -1
  105. package/dist/cli/performance-goal.d.ts +3 -0
  106. package/dist/cli/performance-goal.d.ts.map +1 -0
  107. package/dist/cli/performance-goal.js +186 -0
  108. package/dist/cli/performance-goal.js.map +1 -0
  109. package/dist/cli/ralph.d.ts.map +1 -1
  110. package/dist/cli/ralph.js +45 -3
  111. package/dist/cli/ralph.js.map +1 -1
  112. package/dist/cli/setup.d.ts.map +1 -1
  113. package/dist/cli/setup.js +106 -10
  114. package/dist/cli/setup.js.map +1 -1
  115. package/dist/cli/team.d.ts +3 -0
  116. package/dist/cli/team.d.ts.map +1 -1
  117. package/dist/cli/team.js +112 -22
  118. package/dist/cli/team.js.map +1 -1
  119. package/dist/cli/tmux-hook.d.ts.map +1 -1
  120. package/dist/cli/tmux-hook.js +2 -1
  121. package/dist/cli/tmux-hook.js.map +1 -1
  122. package/dist/cli/ultragoal.d.ts +3 -0
  123. package/dist/cli/ultragoal.d.ts.map +1 -0
  124. package/dist/cli/ultragoal.js +194 -0
  125. package/dist/cli/ultragoal.js.map +1 -0
  126. package/dist/cli/uninstall.d.ts.map +1 -1
  127. package/dist/cli/uninstall.js +4 -2
  128. package/dist/cli/uninstall.js.map +1 -1
  129. package/dist/config/__tests__/codex-hooks.test.js +5 -0
  130. package/dist/config/__tests__/codex-hooks.test.js.map +1 -1
  131. package/dist/config/__tests__/generator-idempotent.test.js +12 -1
  132. package/dist/config/__tests__/generator-idempotent.test.js.map +1 -1
  133. package/dist/config/__tests__/generator-notify.test.js +5 -0
  134. package/dist/config/__tests__/generator-notify.test.js.map +1 -1
  135. package/dist/config/__tests__/models.test.js +18 -1
  136. package/dist/config/__tests__/models.test.js.map +1 -1
  137. package/dist/config/codex-hooks.d.ts.map +1 -1
  138. package/dist/config/codex-hooks.js +4 -1
  139. package/dist/config/codex-hooks.js.map +1 -1
  140. package/dist/config/commit-lore-guard.d.ts +3 -0
  141. package/dist/config/commit-lore-guard.d.ts.map +1 -0
  142. package/dist/config/commit-lore-guard.js +9 -0
  143. package/dist/config/commit-lore-guard.js.map +1 -0
  144. package/dist/config/generator.d.ts +3 -2
  145. package/dist/config/generator.d.ts.map +1 -1
  146. package/dist/config/generator.js +52 -8
  147. package/dist/config/generator.js.map +1 -1
  148. package/dist/config/models.d.ts +6 -0
  149. package/dist/config/models.d.ts.map +1 -1
  150. package/dist/config/models.js +37 -0
  151. package/dist/config/models.js.map +1 -1
  152. package/dist/config/omx-first-party-mcp.d.ts +1 -0
  153. package/dist/config/omx-first-party-mcp.d.ts.map +1 -1
  154. package/dist/config/omx-first-party-mcp.js +4 -1
  155. package/dist/config/omx-first-party-mcp.js.map +1 -1
  156. package/dist/exec/followup.d.ts +1 -0
  157. package/dist/exec/followup.d.ts.map +1 -1
  158. package/dist/exec/followup.js +9 -3
  159. package/dist/exec/followup.js.map +1 -1
  160. package/dist/goal-workflows/__tests__/artifacts.test.d.ts +2 -0
  161. package/dist/goal-workflows/__tests__/artifacts.test.d.ts.map +1 -0
  162. package/dist/goal-workflows/__tests__/artifacts.test.js +96 -0
  163. package/dist/goal-workflows/__tests__/artifacts.test.js.map +1 -0
  164. package/dist/goal-workflows/__tests__/codex-goal-snapshot.test.d.ts +2 -0
  165. package/dist/goal-workflows/__tests__/codex-goal-snapshot.test.d.ts.map +1 -0
  166. package/dist/goal-workflows/__tests__/codex-goal-snapshot.test.js +54 -0
  167. package/dist/goal-workflows/__tests__/codex-goal-snapshot.test.js.map +1 -0
  168. package/dist/goal-workflows/artifacts.d.ts +62 -0
  169. package/dist/goal-workflows/artifacts.d.ts.map +1 -0
  170. package/dist/goal-workflows/artifacts.js +132 -0
  171. package/dist/goal-workflows/artifacts.js.map +1 -0
  172. package/dist/goal-workflows/codex-goal-snapshot.d.ts +28 -0
  173. package/dist/goal-workflows/codex-goal-snapshot.d.ts.map +1 -0
  174. package/dist/goal-workflows/codex-goal-snapshot.js +110 -0
  175. package/dist/goal-workflows/codex-goal-snapshot.js.map +1 -0
  176. package/dist/goal-workflows/handoff.d.ts +10 -0
  177. package/dist/goal-workflows/handoff.d.ts.map +1 -0
  178. package/dist/goal-workflows/handoff.js +31 -0
  179. package/dist/goal-workflows/handoff.js.map +1 -0
  180. package/dist/goal-workflows/validation.d.ts +13 -0
  181. package/dist/goal-workflows/validation.d.ts.map +1 -0
  182. package/dist/goal-workflows/validation.js +36 -0
  183. package/dist/goal-workflows/validation.js.map +1 -0
  184. package/dist/hooks/__tests__/anti-slop-workflow.test.js +22 -3
  185. package/dist/hooks/__tests__/anti-slop-workflow.test.js.map +1 -1
  186. package/dist/hooks/__tests__/consensus-execution-handoff.test.js +19 -2
  187. package/dist/hooks/__tests__/consensus-execution-handoff.test.js.map +1 -1
  188. package/dist/hooks/__tests__/deep-interview-contract.test.js +40 -0
  189. package/dist/hooks/__tests__/deep-interview-contract.test.js.map +1 -1
  190. package/dist/hooks/__tests__/foreground-isolation-contract.test.d.ts +2 -0
  191. package/dist/hooks/__tests__/foreground-isolation-contract.test.d.ts.map +1 -0
  192. package/dist/hooks/__tests__/foreground-isolation-contract.test.js +28 -0
  193. package/dist/hooks/__tests__/foreground-isolation-contract.test.js.map +1 -0
  194. package/dist/hooks/__tests__/keyword-detector.test.js +45 -32
  195. package/dist/hooks/__tests__/keyword-detector.test.js.map +1 -1
  196. package/dist/hooks/__tests__/notify-fallback-watcher.test.js +3 -3
  197. package/dist/hooks/__tests__/notify-fallback-watcher.test.js.map +1 -1
  198. package/dist/hooks/__tests__/notify-hook-team-dispatch.test.js +2 -1
  199. package/dist/hooks/__tests__/notify-hook-team-dispatch.test.js.map +1 -1
  200. package/dist/hooks/__tests__/notify-hook-team-leader-nudge.test.js +17 -24
  201. package/dist/hooks/__tests__/notify-hook-team-leader-nudge.test.js.map +1 -1
  202. package/dist/hooks/__tests__/prompt-guidance-wave-two.test.js +3 -3
  203. package/dist/hooks/__tests__/prompt-guidance-wave-two.test.js.map +1 -1
  204. package/dist/hooks/__tests__/session.test.js +32 -0
  205. package/dist/hooks/__tests__/session.test.js.map +1 -1
  206. package/dist/hooks/__tests__/task-size-detector.test.js +1 -1
  207. package/dist/hooks/__tests__/task-size-detector.test.js.map +1 -1
  208. package/dist/hooks/__tests__/visual-ralph-skill.test.js +3 -3
  209. package/dist/hooks/__tests__/visual-ralph-skill.test.js.map +1 -1
  210. package/dist/hooks/__tests__/visual-verdict-loop.test.js +7 -11
  211. package/dist/hooks/__tests__/visual-verdict-loop.test.js.map +1 -1
  212. package/dist/hooks/agents-overlay.d.ts.map +1 -1
  213. package/dist/hooks/agents-overlay.js +2 -2
  214. package/dist/hooks/agents-overlay.js.map +1 -1
  215. package/dist/hooks/codebase-map.d.ts.map +1 -1
  216. package/dist/hooks/codebase-map.js +3 -2
  217. package/dist/hooks/codebase-map.js.map +1 -1
  218. package/dist/hooks/extensibility/dispatcher.d.ts.map +1 -1
  219. package/dist/hooks/extensibility/dispatcher.js +6 -4
  220. package/dist/hooks/extensibility/dispatcher.js.map +1 -1
  221. package/dist/hooks/extensibility/logging.d.ts.map +1 -1
  222. package/dist/hooks/extensibility/logging.js +3 -2
  223. package/dist/hooks/extensibility/logging.js.map +1 -1
  224. package/dist/hooks/extensibility/sdk/paths.d.ts.map +1 -1
  225. package/dist/hooks/extensibility/sdk/paths.js +4 -3
  226. package/dist/hooks/extensibility/sdk/paths.js.map +1 -1
  227. package/dist/hooks/keyword-detector.d.ts.map +1 -1
  228. package/dist/hooks/keyword-detector.js +12 -13
  229. package/dist/hooks/keyword-detector.js.map +1 -1
  230. package/dist/hooks/keyword-registry.d.ts.map +1 -1
  231. package/dist/hooks/keyword-registry.js +2 -10
  232. package/dist/hooks/keyword-registry.js.map +1 -1
  233. package/dist/hooks/prompt-guidance-contract.d.ts.map +1 -1
  234. package/dist/hooks/prompt-guidance-contract.js +0 -4
  235. package/dist/hooks/prompt-guidance-contract.js.map +1 -1
  236. package/dist/hooks/session.d.ts.map +1 -1
  237. package/dist/hooks/session.js +24 -14
  238. package/dist/hooks/session.js.map +1 -1
  239. package/dist/hooks/task-size-detector.d.ts.map +1 -1
  240. package/dist/hooks/task-size-detector.js +1 -0
  241. package/dist/hooks/task-size-detector.js.map +1 -1
  242. package/dist/hud/__tests__/hud-tmux-injection.test.js +8 -7
  243. package/dist/hud/__tests__/hud-tmux-injection.test.js.map +1 -1
  244. package/dist/hud/__tests__/reconcile.test.js +30 -8
  245. package/dist/hud/__tests__/reconcile.test.js.map +1 -1
  246. package/dist/hud/__tests__/state.test.js +24 -0
  247. package/dist/hud/__tests__/state.test.js.map +1 -1
  248. package/dist/hud/index.js +1 -1
  249. package/dist/hud/index.js.map +1 -1
  250. package/dist/hud/reconcile.d.ts +2 -1
  251. package/dist/hud/reconcile.d.ts.map +1 -1
  252. package/dist/hud/reconcile.js +12 -0
  253. package/dist/hud/reconcile.js.map +1 -1
  254. package/dist/hud/state.d.ts.map +1 -1
  255. package/dist/hud/state.js +22 -8
  256. package/dist/hud/state.js.map +1 -1
  257. package/dist/hud/tmux.js +1 -1
  258. package/dist/hud/tmux.js.map +1 -1
  259. package/dist/imagegen/continuation.d.ts +44 -0
  260. package/dist/imagegen/continuation.d.ts.map +1 -0
  261. package/dist/imagegen/continuation.js +220 -0
  262. package/dist/imagegen/continuation.js.map +1 -0
  263. package/dist/mcp/__tests__/bootstrap.test.js +62 -4
  264. package/dist/mcp/__tests__/bootstrap.test.js.map +1 -1
  265. package/dist/mcp/__tests__/server-lifecycle.test.js +49 -1
  266. package/dist/mcp/__tests__/server-lifecycle.test.js.map +1 -1
  267. package/dist/mcp/__tests__/state-paths.test.js +54 -0
  268. package/dist/mcp/__tests__/state-paths.test.js.map +1 -1
  269. package/dist/mcp/__tests__/state-server.test.js +36 -0
  270. package/dist/mcp/__tests__/state-server.test.js.map +1 -1
  271. package/dist/mcp/bootstrap.d.ts +3 -1
  272. package/dist/mcp/bootstrap.d.ts.map +1 -1
  273. package/dist/mcp/bootstrap.js +104 -22
  274. package/dist/mcp/bootstrap.js.map +1 -1
  275. package/dist/mcp/lifecycle-telemetry.d.ts +16 -0
  276. package/dist/mcp/lifecycle-telemetry.d.ts.map +1 -0
  277. package/dist/mcp/lifecycle-telemetry.js +95 -0
  278. package/dist/mcp/lifecycle-telemetry.js.map +1 -0
  279. package/dist/mcp/state-paths.d.ts +17 -0
  280. package/dist/mcp/state-paths.d.ts.map +1 -1
  281. package/dist/mcp/state-paths.js +36 -2
  282. package/dist/mcp/state-paths.js.map +1 -1
  283. package/dist/modes/__tests__/base-session-scope.test.js +26 -0
  284. package/dist/modes/__tests__/base-session-scope.test.js.map +1 -1
  285. package/dist/modes/base.d.ts +1 -0
  286. package/dist/modes/base.d.ts.map +1 -1
  287. package/dist/modes/base.js +35 -5
  288. package/dist/modes/base.js.map +1 -1
  289. package/dist/notifications/__tests__/http-client.test.d.ts +2 -0
  290. package/dist/notifications/__tests__/http-client.test.d.ts.map +1 -0
  291. package/dist/notifications/__tests__/http-client.test.js +90 -0
  292. package/dist/notifications/__tests__/http-client.test.js.map +1 -0
  293. package/dist/notifications/__tests__/notifier.test.js +22 -60
  294. package/dist/notifications/__tests__/notifier.test.js.map +1 -1
  295. package/dist/notifications/dispatcher.d.ts.map +1 -1
  296. package/dist/notifications/dispatcher.js +35 -60
  297. package/dist/notifications/dispatcher.js.map +1 -1
  298. package/dist/notifications/http-client.d.ts +22 -0
  299. package/dist/notifications/http-client.d.ts.map +1 -0
  300. package/dist/notifications/http-client.js +298 -0
  301. package/dist/notifications/http-client.js.map +1 -0
  302. package/dist/notifications/notifier.d.ts +3 -2
  303. package/dist/notifications/notifier.d.ts.map +1 -1
  304. package/dist/notifications/notifier.js +17 -22
  305. package/dist/notifications/notifier.js.map +1 -1
  306. package/dist/openclaw/__tests__/dispatcher.test.js +62 -1
  307. package/dist/openclaw/__tests__/dispatcher.test.js.map +1 -1
  308. package/dist/openclaw/dispatcher.d.ts.map +1 -1
  309. package/dist/openclaw/dispatcher.js +3 -2
  310. package/dist/openclaw/dispatcher.js.map +1 -1
  311. package/dist/performance-goal/artifacts.d.ts +76 -0
  312. package/dist/performance-goal/artifacts.d.ts.map +1 -0
  313. package/dist/performance-goal/artifacts.js +221 -0
  314. package/dist/performance-goal/artifacts.js.map +1 -0
  315. package/dist/pipeline/__tests__/stages.test.js +304 -10
  316. package/dist/pipeline/__tests__/stages.test.js.map +1 -1
  317. package/dist/pipeline/stages/team-exec.d.ts +2 -0
  318. package/dist/pipeline/stages/team-exec.d.ts.map +1 -1
  319. package/dist/pipeline/stages/team-exec.js +51 -43
  320. package/dist/pipeline/stages/team-exec.js.map +1 -1
  321. package/dist/planning/__tests__/artifacts.test.js +153 -3
  322. package/dist/planning/__tests__/artifacts.test.js.map +1 -1
  323. package/dist/planning/__tests__/context-pack-status.test.d.ts +2 -0
  324. package/dist/planning/__tests__/context-pack-status.test.d.ts.map +1 -0
  325. package/dist/planning/__tests__/context-pack-status.test.js +271 -0
  326. package/dist/planning/__tests__/context-pack-status.test.js.map +1 -0
  327. package/dist/planning/artifacts.d.ts +13 -1
  328. package/dist/planning/artifacts.d.ts.map +1 -1
  329. package/dist/planning/artifacts.js +41 -21
  330. package/dist/planning/artifacts.js.map +1 -1
  331. package/dist/planning/context-pack-status.d.ts +42 -0
  332. package/dist/planning/context-pack-status.d.ts.map +1 -0
  333. package/dist/planning/context-pack-status.js +479 -0
  334. package/dist/planning/context-pack-status.js.map +1 -0
  335. package/dist/ralplan/__tests__/runtime.test.js +2 -0
  336. package/dist/ralplan/__tests__/runtime.test.js.map +1 -1
  337. package/dist/ralplan/runtime.d.ts.map +1 -1
  338. package/dist/ralplan/runtime.js +6 -0
  339. package/dist/ralplan/runtime.js.map +1 -1
  340. package/dist/runtime/__tests__/process-tree.test.d.ts +2 -0
  341. package/dist/runtime/__tests__/process-tree.test.d.ts.map +1 -0
  342. package/dist/runtime/__tests__/process-tree.test.js +107 -0
  343. package/dist/runtime/__tests__/process-tree.test.js.map +1 -0
  344. package/dist/runtime/process-tree.d.ts +28 -0
  345. package/dist/runtime/process-tree.d.ts.map +1 -0
  346. package/dist/runtime/process-tree.js +230 -0
  347. package/dist/runtime/process-tree.js.map +1 -0
  348. package/dist/scripts/__tests__/codex-native-hook.test.js +1603 -88
  349. package/dist/scripts/__tests__/codex-native-hook.test.js.map +1 -1
  350. package/dist/scripts/__tests__/hook-derived-watcher.test.js +33 -1
  351. package/dist/scripts/__tests__/hook-derived-watcher.test.js.map +1 -1
  352. package/dist/scripts/__tests__/notify-state-io.test.d.ts +2 -0
  353. package/dist/scripts/__tests__/notify-state-io.test.d.ts.map +1 -0
  354. package/dist/scripts/__tests__/notify-state-io.test.js +40 -0
  355. package/dist/scripts/__tests__/notify-state-io.test.js.map +1 -0
  356. package/dist/scripts/__tests__/run-test-files.test.js +36 -0
  357. package/dist/scripts/__tests__/run-test-files.test.js.map +1 -1
  358. package/dist/scripts/codex-native-hook.d.ts.map +1 -1
  359. package/dist/scripts/codex-native-hook.js +607 -57
  360. package/dist/scripts/codex-native-hook.js.map +1 -1
  361. package/dist/scripts/codex-native-pre-post.d.ts +7 -0
  362. package/dist/scripts/codex-native-pre-post.d.ts.map +1 -1
  363. package/dist/scripts/codex-native-pre-post.js +222 -19
  364. package/dist/scripts/codex-native-pre-post.js.map +1 -1
  365. package/dist/scripts/hook-derived-watcher.js +2 -1
  366. package/dist/scripts/hook-derived-watcher.js.map +1 -1
  367. package/dist/scripts/notify-fallback-watcher.js +2 -1
  368. package/dist/scripts/notify-fallback-watcher.js.map +1 -1
  369. package/dist/scripts/notify-hook/managed-tmux.d.ts.map +1 -1
  370. package/dist/scripts/notify-hook/managed-tmux.js +6 -9
  371. package/dist/scripts/notify-hook/managed-tmux.js.map +1 -1
  372. package/dist/scripts/notify-hook/orchestration-intent.d.ts +1 -2
  373. package/dist/scripts/notify-hook/orchestration-intent.d.ts.map +1 -1
  374. package/dist/scripts/notify-hook/orchestration-intent.js +2 -3
  375. package/dist/scripts/notify-hook/orchestration-intent.js.map +1 -1
  376. package/dist/scripts/notify-hook/process-runner.d.ts.map +1 -1
  377. package/dist/scripts/notify-hook/process-runner.js +4 -1
  378. package/dist/scripts/notify-hook/process-runner.js.map +1 -1
  379. package/dist/scripts/notify-hook/state-io.d.ts.map +1 -1
  380. package/dist/scripts/notify-hook/state-io.js +4 -7
  381. package/dist/scripts/notify-hook/state-io.js.map +1 -1
  382. package/dist/scripts/notify-hook/team-leader-nudge.d.ts +0 -2
  383. package/dist/scripts/notify-hook/team-leader-nudge.d.ts.map +1 -1
  384. package/dist/scripts/notify-hook/team-leader-nudge.js +8 -60
  385. package/dist/scripts/notify-hook/team-leader-nudge.js.map +1 -1
  386. package/dist/scripts/notify-hook/team-worker-stop.d.ts +15 -0
  387. package/dist/scripts/notify-hook/team-worker-stop.d.ts.map +1 -0
  388. package/dist/scripts/notify-hook/team-worker-stop.js +224 -0
  389. package/dist/scripts/notify-hook/team-worker-stop.js.map +1 -0
  390. package/dist/scripts/notify-hook/team-worker.d.ts.map +1 -1
  391. package/dist/scripts/notify-hook/team-worker.js +26 -18
  392. package/dist/scripts/notify-hook/team-worker.js.map +1 -1
  393. package/dist/scripts/notify-hook.js +25 -3
  394. package/dist/scripts/notify-hook.js.map +1 -1
  395. package/dist/scripts/run-test-files.js +17 -1
  396. package/dist/scripts/run-test-files.js.map +1 -1
  397. package/dist/scripts/sync-plugin-mirror.js +2 -2
  398. package/dist/scripts/sync-plugin-mirror.js.map +1 -1
  399. package/dist/scripts/verify-native-agents.d.ts.map +1 -1
  400. package/dist/scripts/verify-native-agents.js +3 -1
  401. package/dist/scripts/verify-native-agents.js.map +1 -1
  402. package/dist/sidecar/__tests__/tmux.test.js +1 -1
  403. package/dist/sidecar/__tests__/tmux.test.js.map +1 -1
  404. package/dist/sidecar/tmux.js +1 -1
  405. package/dist/sidecar/tmux.js.map +1 -1
  406. package/dist/state/__tests__/operations.test.js +26 -0
  407. package/dist/state/__tests__/operations.test.js.map +1 -1
  408. package/dist/state/__tests__/skill-active.test.js +35 -0
  409. package/dist/state/__tests__/skill-active.test.js.map +1 -1
  410. package/dist/state/__tests__/workflow-transition.test.js +45 -1
  411. package/dist/state/__tests__/workflow-transition.test.js.map +1 -1
  412. package/dist/state/operations.d.ts +3 -1
  413. package/dist/state/operations.d.ts.map +1 -1
  414. package/dist/state/operations.js +8 -4
  415. package/dist/state/operations.js.map +1 -1
  416. package/dist/state/skill-active.d.ts +1 -0
  417. package/dist/state/skill-active.d.ts.map +1 -1
  418. package/dist/state/skill-active.js +54 -13
  419. package/dist/state/skill-active.js.map +1 -1
  420. package/dist/state/workflow-transition-reconcile.js +2 -2
  421. package/dist/state/workflow-transition-reconcile.js.map +1 -1
  422. package/dist/state/workflow-transition.js +2 -2
  423. package/dist/state/workflow-transition.js.map +1 -1
  424. package/dist/team/__tests__/api-interop.test.js +59 -0
  425. package/dist/team/__tests__/api-interop.test.js.map +1 -1
  426. package/dist/team/__tests__/approved-execution.test.d.ts +2 -0
  427. package/dist/team/__tests__/approved-execution.test.d.ts.map +1 -0
  428. package/dist/team/__tests__/approved-execution.test.js +220 -0
  429. package/dist/team/__tests__/approved-execution.test.js.map +1 -0
  430. package/dist/team/__tests__/delivery-e2e-smoke.test.js +2 -4
  431. package/dist/team/__tests__/delivery-e2e-smoke.test.js.map +1 -1
  432. package/dist/team/__tests__/delivery-log.test.d.ts +2 -0
  433. package/dist/team/__tests__/delivery-log.test.d.ts.map +1 -0
  434. package/dist/team/__tests__/delivery-log.test.js +44 -0
  435. package/dist/team/__tests__/delivery-log.test.js.map +1 -0
  436. package/dist/team/__tests__/followup-planner.test.js +16 -0
  437. package/dist/team/__tests__/followup-planner.test.js.map +1 -1
  438. package/dist/team/__tests__/model-contract.test.js +16 -0
  439. package/dist/team/__tests__/model-contract.test.js.map +1 -1
  440. package/dist/team/__tests__/repo-aware-decomposition.test.js +20 -0
  441. package/dist/team/__tests__/repo-aware-decomposition.test.js.map +1 -1
  442. package/dist/team/__tests__/role-router.test.js +4 -4
  443. package/dist/team/__tests__/role-router.test.js.map +1 -1
  444. package/dist/team/__tests__/runtime-boxed-state.test.d.ts +2 -0
  445. package/dist/team/__tests__/runtime-boxed-state.test.d.ts.map +1 -0
  446. package/dist/team/__tests__/runtime-boxed-state.test.js +39 -0
  447. package/dist/team/__tests__/runtime-boxed-state.test.js.map +1 -0
  448. package/dist/team/__tests__/runtime-cli.test.js +16 -0
  449. package/dist/team/__tests__/runtime-cli.test.js.map +1 -1
  450. package/dist/team/__tests__/runtime.test.js +323 -13
  451. package/dist/team/__tests__/runtime.test.js.map +1 -1
  452. package/dist/team/__tests__/scaling.test.js +110 -0
  453. package/dist/team/__tests__/scaling.test.js.map +1 -1
  454. package/dist/team/__tests__/state-root.test.js +13 -0
  455. package/dist/team/__tests__/state-root.test.js.map +1 -1
  456. package/dist/team/__tests__/tmux-session.test.js +12 -0
  457. package/dist/team/__tests__/tmux-session.test.js.map +1 -1
  458. package/dist/team/__tests__/worker-bootstrap.test.js +50 -0
  459. package/dist/team/__tests__/worker-bootstrap.test.js.map +1 -1
  460. package/dist/team/__tests__/worker-runtime-identity.test.js +6 -0
  461. package/dist/team/__tests__/worker-runtime-identity.test.js.map +1 -1
  462. package/dist/team/api-interop.d.ts.map +1 -1
  463. package/dist/team/api-interop.js +4 -3
  464. package/dist/team/api-interop.js.map +1 -1
  465. package/dist/team/approved-execution.d.ts +50 -0
  466. package/dist/team/approved-execution.d.ts.map +1 -0
  467. package/dist/team/approved-execution.js +154 -0
  468. package/dist/team/approved-execution.js.map +1 -0
  469. package/dist/team/delivery-log.d.ts.map +1 -1
  470. package/dist/team/delivery-log.js +2 -1
  471. package/dist/team/delivery-log.js.map +1 -1
  472. package/dist/team/followup-planner.d.ts +1 -0
  473. package/dist/team/followup-planner.d.ts.map +1 -1
  474. package/dist/team/followup-planner.js +11 -11
  475. package/dist/team/followup-planner.js.map +1 -1
  476. package/dist/team/goal-workflow.d.ts +20 -0
  477. package/dist/team/goal-workflow.d.ts.map +1 -0
  478. package/dist/team/goal-workflow.js +57 -0
  479. package/dist/team/goal-workflow.js.map +1 -0
  480. package/dist/team/model-contract.d.ts +1 -1
  481. package/dist/team/model-contract.d.ts.map +1 -1
  482. package/dist/team/model-contract.js +4 -3
  483. package/dist/team/model-contract.js.map +1 -1
  484. package/dist/team/orchestrator.js +2 -2
  485. package/dist/team/orchestrator.js.map +1 -1
  486. package/dist/team/repo-aware-decomposition.d.ts +1 -0
  487. package/dist/team/repo-aware-decomposition.d.ts.map +1 -1
  488. package/dist/team/repo-aware-decomposition.js +5 -1
  489. package/dist/team/repo-aware-decomposition.js.map +1 -1
  490. package/dist/team/role-router.js +5 -5
  491. package/dist/team/role-router.js.map +1 -1
  492. package/dist/team/runtime-cli.d.ts +4 -0
  493. package/dist/team/runtime-cli.d.ts.map +1 -1
  494. package/dist/team/runtime-cli.js +14 -1
  495. package/dist/team/runtime-cli.js.map +1 -1
  496. package/dist/team/runtime.d.ts +7 -0
  497. package/dist/team/runtime.d.ts.map +1 -1
  498. package/dist/team/runtime.js +89 -19
  499. package/dist/team/runtime.js.map +1 -1
  500. package/dist/team/scaling.d.ts.map +1 -1
  501. package/dist/team/scaling.js +15 -6
  502. package/dist/team/scaling.js.map +1 -1
  503. package/dist/team/tmux-session.d.ts.map +1 -1
  504. package/dist/team/tmux-session.js +11 -2
  505. package/dist/team/tmux-session.js.map +1 -1
  506. package/dist/team/worker-bootstrap.d.ts +2 -0
  507. package/dist/team/worker-bootstrap.d.ts.map +1 -1
  508. package/dist/team/worker-bootstrap.js +19 -2
  509. package/dist/team/worker-bootstrap.js.map +1 -1
  510. package/dist/ultragoal/__tests__/artifacts.test.d.ts +2 -0
  511. package/dist/ultragoal/__tests__/artifacts.test.d.ts.map +1 -0
  512. package/dist/ultragoal/__tests__/artifacts.test.js +144 -0
  513. package/dist/ultragoal/__tests__/artifacts.test.js.map +1 -0
  514. package/dist/ultragoal/__tests__/docs-contract.test.d.ts +2 -0
  515. package/dist/ultragoal/__tests__/docs-contract.test.d.ts.map +1 -0
  516. package/dist/ultragoal/__tests__/docs-contract.test.js +23 -0
  517. package/dist/ultragoal/__tests__/docs-contract.test.js.map +1 -0
  518. package/dist/ultragoal/artifacts.d.ts +89 -0
  519. package/dist/ultragoal/artifacts.d.ts.map +1 -0
  520. package/dist/ultragoal/artifacts.js +269 -0
  521. package/dist/ultragoal/artifacts.js.map +1 -0
  522. package/dist/utils/__tests__/agents-model-table.test.js +3 -1
  523. package/dist/utils/__tests__/agents-model-table.test.js.map +1 -1
  524. package/dist/utils/__tests__/paths.test.js +31 -1
  525. package/dist/utils/__tests__/paths.test.js.map +1 -1
  526. package/dist/utils/agents-model-table.d.ts.map +1 -1
  527. package/dist/utils/agents-model-table.js +12 -1
  528. package/dist/utils/agents-model-table.js.map +1 -1
  529. package/dist/utils/paths.d.ts +2 -0
  530. package/dist/utils/paths.d.ts.map +1 -1
  531. package/dist/utils/paths.js +23 -7
  532. package/dist/utils/paths.js.map +1 -1
  533. package/dist/verification/__tests__/ci-rust-gates.test.js +67 -26
  534. package/dist/verification/__tests__/ci-rust-gates.test.js.map +1 -1
  535. package/package.json +5 -5
  536. package/plugins/oh-my-codex/.codex-plugin/plugin.json +1 -1
  537. package/plugins/oh-my-codex/skills/ai-slop-cleaner/SKILL.md +9 -0
  538. package/plugins/oh-my-codex/skills/ask/SKILL.md +58 -0
  539. package/plugins/oh-my-codex/skills/autoresearch-goal/SKILL.md +36 -0
  540. package/plugins/oh-my-codex/skills/deep-interview/SKILL.md +25 -2
  541. package/plugins/oh-my-codex/skills/omx-setup/SKILL.md +2 -2
  542. package/plugins/oh-my-codex/skills/performance-goal/SKILL.md +65 -0
  543. package/plugins/oh-my-codex/skills/plan/SKILL.md +8 -5
  544. package/plugins/oh-my-codex/skills/ralph/SKILL.md +22 -3
  545. package/plugins/oh-my-codex/skills/ralplan/SKILL.md +13 -3
  546. package/plugins/oh-my-codex/skills/team/SKILL.md +8 -4
  547. package/plugins/oh-my-codex/skills/ultragoal/SKILL.md +49 -0
  548. package/plugins/oh-my-codex/skills/visual-ralph/SKILL.md +17 -9
  549. package/prompts/api-reviewer.md +1 -1
  550. package/prompts/code-reviewer.md +2 -0
  551. package/prompts/performance-reviewer.md +1 -1
  552. package/prompts/planner.md +1 -1
  553. package/prompts/quality-reviewer.md +1 -1
  554. package/prompts/quality-strategist.md +2 -2
  555. package/prompts/style-reviewer.md +1 -1
  556. package/prompts/test-engineer.md +1 -1
  557. package/skills/ai-slop-cleaner/SKILL.md +9 -0
  558. package/skills/ask/SKILL.md +58 -0
  559. package/skills/ask-claude/SKILL.md +3 -54
  560. package/skills/ask-gemini/SKILL.md +3 -54
  561. package/skills/autoresearch-goal/SKILL.md +36 -0
  562. package/skills/build-fix/SKILL.md +4 -139
  563. package/skills/deep-interview/SKILL.md +25 -2
  564. package/skills/deepsearch/SKILL.md +4 -32
  565. package/skills/ecomode/SKILL.md +4 -108
  566. package/skills/help/SKILL.md +4 -196
  567. package/skills/note/SKILL.md +4 -56
  568. package/skills/omx-setup/SKILL.md +2 -2
  569. package/skills/performance-goal/SKILL.md +65 -0
  570. package/skills/plan/SKILL.md +8 -5
  571. package/skills/ralph/SKILL.md +22 -3
  572. package/skills/ralph-init/SKILL.md +4 -40
  573. package/skills/ralplan/SKILL.md +13 -3
  574. package/skills/review/SKILL.md +4 -32
  575. package/skills/security-review/SKILL.md +4 -294
  576. package/skills/swarm/SKILL.md +4 -19
  577. package/skills/tdd/SKILL.md +4 -100
  578. package/skills/team/SKILL.md +8 -4
  579. package/skills/trace/SKILL.md +4 -27
  580. package/skills/ultragoal/SKILL.md +49 -0
  581. package/skills/visual-ralph/SKILL.md +17 -9
  582. package/skills/visual-verdict/SKILL.md +4 -70
  583. package/skills/web-clone/SKILL.md +4 -18
  584. package/src/scripts/__tests__/codex-native-hook.test.ts +2810 -1083
  585. package/src/scripts/__tests__/hook-derived-watcher.test.ts +45 -1
  586. package/src/scripts/__tests__/notify-state-io.test.ts +73 -0
  587. package/src/scripts/__tests__/run-test-files.test.ts +46 -0
  588. package/src/scripts/codex-native-hook.ts +721 -66
  589. package/src/scripts/codex-native-pre-post.ts +252 -20
  590. package/src/scripts/hook-derived-watcher.ts +2 -1
  591. package/src/scripts/notify-fallback-watcher.ts +2 -1
  592. package/src/scripts/notify-hook/managed-tmux.ts +6 -7
  593. package/src/scripts/notify-hook/orchestration-intent.ts +1 -3
  594. package/src/scripts/notify-hook/process-runner.ts +4 -1
  595. package/src/scripts/notify-hook/state-io.ts +5 -7
  596. package/src/scripts/notify-hook/team-leader-nudge.ts +7 -63
  597. package/src/scripts/notify-hook/team-worker-stop.ts +246 -0
  598. package/src/scripts/notify-hook/team-worker.ts +23 -14
  599. package/src/scripts/notify-hook.ts +26 -3
  600. package/src/scripts/run-test-files.ts +20 -1
  601. package/src/scripts/sync-plugin-mirror.ts +2 -2
  602. package/src/scripts/verify-native-agents.ts +3 -1
  603. package/templates/catalog-manifest.json +45 -27
  604. package/plugins/oh-my-codex/skills/ask-claude/SKILL.md +0 -61
  605. package/plugins/oh-my-codex/skills/ask-gemini/SKILL.md +0 -61
  606. package/plugins/oh-my-codex/skills/help/SKILL.md +0 -202
  607. package/plugins/oh-my-codex/skills/note/SKILL.md +0 -62
  608. package/plugins/oh-my-codex/skills/security-review/SKILL.md +0 -300
  609. package/plugins/oh-my-codex/skills/trace/SKILL.md +0 -33
  610. package/plugins/oh-my-codex/skills/visual-verdict/SKILL.md +0 -76
@@ -3,17 +3,38 @@ use std::ffi::OsString;
3
3
  use std::fs::{
4
4
  canonicalize, create_dir_all, read_to_string, remove_dir_all, remove_file, write, File,
5
5
  };
6
- use std::io::{self, BufRead, BufReader};
6
+ use std::io::{self, BufRead, BufReader, Read};
7
7
  use std::path::{Path, PathBuf};
8
- use std::process::Command;
9
- use std::time::{SystemTime, UNIX_EPOCH};
8
+ use std::process::{Child, Command, Output, Stdio};
9
+ use std::sync::mpsc::{self, Receiver, RecvTimeoutError, TryRecvError};
10
+ use std::thread;
11
+ use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
10
12
 
11
13
  const CODEX_BIN_ENV: &str = "OMX_EXPLORE_CODEX_BIN";
12
14
  const HARNESS_ROOT_ENV: &str = "OMX_EXPLORE_ROOT";
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";
13
18
  const INTERNAL_DIRECT_WRAPPER_FLAG: &str = "--internal-allowlist-direct";
14
19
  const INTERNAL_SHELL_WRAPPER_FLAG: &str = "--internal-allowlist-shell";
15
20
  const TEMP_ALLOWLIST_DIR_PREFIX: &str = "omx-explore-allowlist-";
16
- const SHELL_STARTUP_ENV_VARS: &[&str] = &["BASH_ENV", "ENV", "PROMPT_COMMAND"];
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;
25
+ const PROCESS_TERMINATION_GRACE_MS: u64 = 500;
26
+ const PIPE_READER_READY_GRACE_MS: u64 = 25;
27
+ const PIPE_READER_JOIN_GRACE_MS: u64 = 500;
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
+ ];
17
38
  const WINDOWS_UNSUPPORTED_ALLOWLIST_MESSAGE: &str =
18
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.";
19
40
 
@@ -301,15 +322,422 @@ fn invoke_codex(args: &Args, model: &str, prompt_contract: &str) -> io::Result<A
301
322
  )
302
323
  .env("SHELL", &allowlist.shell_path);
303
324
  sanitize_explore_subprocess_env(&mut command);
304
- let output = command.output()?;
325
+ let timeout = codex_timeout();
326
+ let output = run_command_with_timeout(command, timeout)?;
305
327
 
306
328
  let markdown = read_to_string(&output_path).ok();
307
329
  let _ = remove_file(&output_path);
308
- Ok(AttemptResult {
309
- status_code: output.status.code().unwrap_or(1),
310
- stderr: String::from_utf8_lossy(&output.stderr).into_owned(),
311
- output_markdown: markdown,
312
- })
330
+ match output {
331
+ TimedCommandOutput::Completed(output) => Ok(AttemptResult {
332
+ status_code: output.status.code().unwrap_or(1),
333
+ stderr: String::from_utf8_lossy(&output.stderr).into_owned(),
334
+ output_markdown: markdown,
335
+ }),
336
+ TimedCommandOutput::TimedOut { stderr } => Ok(AttemptResult {
337
+ status_code: 124,
338
+ stderr: format!(
339
+ "[omx explore] codex exec timed out after {}ms; terminated process tree{}{}",
340
+ timeout.as_millis(),
341
+ if stderr.trim().is_empty() {
342
+ ""
343
+ } else {
344
+ ". stderr before timeout: "
345
+ },
346
+ stderr.trim()
347
+ ),
348
+ output_markdown: None,
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
+ }),
384
+ }
385
+ }
386
+
387
+ #[derive(Debug)]
388
+ enum TimedCommandOutput {
389
+ Completed(Output),
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
+ },
403
+ }
404
+
405
+ fn codex_timeout() -> Duration {
406
+ let timeout_ms = env::var(CODEX_TIMEOUT_MS_ENV)
407
+ .ok()
408
+ .and_then(|value| value.trim().parse::<u64>().ok())
409
+ .filter(|value| *value > 0)
410
+ .unwrap_or(DEFAULT_CODEX_TIMEOUT_MS);
411
+ Duration::from_millis(timeout_ms)
412
+ }
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
+
430
+ fn run_command_with_timeout(
431
+ mut command: Command,
432
+ timeout: Duration,
433
+ ) -> io::Result<TimedCommandOutput> {
434
+ command.stdout(Stdio::piped()).stderr(Stdio::piped());
435
+ configure_process_group(&mut command);
436
+ let mut child = command.spawn()?;
437
+
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);
441
+
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);
445
+ loop {
446
+ if let Some(status) = child.try_wait()? {
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(
452
+ &mut child,
453
+ &mut stdout_reader,
454
+ &mut stderr_reader,
455
+ Duration::from_millis(PIPE_READER_READY_GRACE_MS),
456
+ Duration::from_millis(PIPE_READER_JOIN_GRACE_MS),
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
+ };
469
+ return Ok(TimedCommandOutput::Completed(Output {
470
+ status,
471
+ stdout,
472
+ stderr,
473
+ }));
474
+ }
475
+
476
+ if Instant::now() >= deadline {
477
+ terminate_child_process_tree(&mut child);
478
+ let _ = child.wait();
479
+ let reader_timeout = Duration::from_millis(PIPE_READER_JOIN_GRACE_MS);
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();
483
+ return Ok(TimedCommandOutput::TimedOut {
484
+ stderr: String::from_utf8_lossy(&stderr).into_owned(),
485
+ });
486
+ }
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
+
523
+ thread::sleep(Duration::from_millis(25));
524
+ }
525
+ }
526
+
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 {
578
+ let (sender, receiver) = mpsc::channel();
579
+ thread::spawn(move || {
580
+ let _ = sender.send(read_pipe_bounded(pipe, stream, output_limit));
581
+ });
582
+ PipeReader {
583
+ receiver,
584
+ cached: None,
585
+ }
586
+ }
587
+
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>> {
593
+ let mut bytes = Vec::new();
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
+ )),
659
+ }
660
+ }
661
+
662
+ fn collect_completed_output(
663
+ child: &mut Child,
664
+ stdout_reader: &mut PipeReader,
665
+ stderr_reader: &mut PipeReader,
666
+ ready_timeout: Duration,
667
+ cleanup_timeout: Duration,
668
+ ) -> io::Result<(Vec<u8>, Vec<u8>)> {
669
+ let stdout = receive_pipe_reader_if_ready(stdout_reader, ready_timeout)?;
670
+ let stderr = receive_pipe_reader_if_ready(stderr_reader, ready_timeout)?;
671
+
672
+ if stdout.is_none() || stderr.is_none() {
673
+ terminate_child_process_tree(child);
674
+ }
675
+
676
+ let stdout = match stdout {
677
+ Some(stdout) => stdout,
678
+ None => receive_pipe_reader(stdout_reader, cleanup_timeout)?,
679
+ };
680
+ let stderr = match stderr {
681
+ Some(stderr) => stderr,
682
+ None => receive_pipe_reader(stderr_reader, cleanup_timeout)?,
683
+ };
684
+
685
+ Ok((stdout, stderr))
686
+ }
687
+
688
+ fn receive_pipe_reader_if_ready(
689
+ reader: &mut PipeReader,
690
+ timeout: Duration,
691
+ ) -> io::Result<Option<Vec<u8>>> {
692
+ match receive_pipe_reader(reader, timeout) {
693
+ Ok(bytes) => Ok(Some(bytes)),
694
+ Err(err) if err.kind() == io::ErrorKind::TimedOut => Ok(None),
695
+ Err(err) => Err(err),
696
+ }
697
+ }
698
+
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) {
704
+ Ok(result) => result,
705
+ Err(RecvTimeoutError::Timeout) => Err(io::Error::new(
706
+ io::ErrorKind::TimedOut,
707
+ "timed out waiting for subprocess output pipe to close",
708
+ )),
709
+ Err(RecvTimeoutError::Disconnected) => Err(io::Error::new(
710
+ io::ErrorKind::Other,
711
+ "subprocess output reader disconnected",
712
+ )),
713
+ }
714
+ }
715
+
716
+ #[cfg(unix)]
717
+ fn configure_process_group(command: &mut Command) {
718
+ use std::os::unix::process::CommandExt;
719
+ command.process_group(0);
720
+ }
721
+
722
+ #[cfg(not(unix))]
723
+ fn configure_process_group(_command: &mut Command) {}
724
+
725
+ #[cfg(unix)]
726
+ fn terminate_child_process_tree(child: &mut Child) {
727
+ let pgid = child.id() as libc::pid_t;
728
+ unsafe {
729
+ let _ = libc::kill(-pgid, libc::SIGTERM);
730
+ }
731
+ thread::sleep(Duration::from_millis(PROCESS_TERMINATION_GRACE_MS));
732
+ unsafe {
733
+ let _ = libc::kill(-pgid, libc::SIGKILL);
734
+ }
735
+ let _ = child.kill();
736
+ }
737
+
738
+ #[cfg(not(unix))]
739
+ fn terminate_child_process_tree(child: &mut Child) {
740
+ let _ = child.kill();
313
741
  }
314
742
 
315
743
  fn escape_toml_string(value: &str) -> String {
@@ -759,11 +1187,18 @@ fn shell_quote(value: &str) -> String {
759
1187
  }
760
1188
 
761
1189
  fn sanitize_explore_subprocess_env(command: &mut Command) {
762
- for key in SHELL_STARTUP_ENV_VARS {
1190
+ for key in EXPLORE_SUBPROCESS_ENV_VARS_TO_SCRUB {
763
1191
  command.env_remove(key);
764
1192
  }
765
1193
  }
766
1194
 
1195
+ fn shell_supports_bash_startup_suppression(shell_path: &str) -> bool {
1196
+ Path::new(shell_path)
1197
+ .file_name()
1198
+ .and_then(|name| name.to_str())
1199
+ .is_some_and(|name| name == "bash")
1200
+ }
1201
+
767
1202
  fn run_internal_direct_wrapper<I>(mut args: I) -> Result<(), String>
768
1203
  where
769
1204
  I: Iterator<Item = OsString>,
@@ -796,17 +1231,22 @@ where
796
1231
  let forwarded: Vec<String> = args.map(|arg| arg.to_string_lossy().into_owned()).collect();
797
1232
  let command = validate_shell_invocation(&forwarded)?;
798
1233
 
799
- let mut child = Command::new(&real_shell);
800
- if real_shell.ends_with("bash") {
1234
+ let status_code = execute_validated_shell(&real_shell, &command)?;
1235
+ std::process::exit(status_code);
1236
+ }
1237
+
1238
+ fn execute_validated_shell(real_shell: &str, command: &str) -> Result<i32, String> {
1239
+ let mut child = Command::new(real_shell);
1240
+ if shell_supports_bash_startup_suppression(real_shell) {
801
1241
  child.arg("--noprofile").arg("--norc");
802
1242
  }
803
1243
  sanitize_explore_subprocess_env(&mut child);
804
1244
  let status = child
805
- .arg("-lc")
806
- .arg(&command)
1245
+ .arg("-c")
1246
+ .arg(command)
807
1247
  .status()
808
1248
  .map_err(|err| format!("failed to execute validated shell command: {err}"))?;
809
- std::process::exit(status.code().unwrap_or(1));
1249
+ Ok(status.code().unwrap_or(1))
810
1250
  }
811
1251
 
812
1252
  fn validate_shell_invocation(args: &[String]) -> Result<String, String> {
@@ -1088,6 +1528,13 @@ mod tests {
1088
1528
  .unwrap_or_else(|poisoned| poisoned.into_inner())
1089
1529
  }
1090
1530
 
1531
+ fn process_tree_lock() -> std::sync::MutexGuard<'static, ()> {
1532
+ static LOCK: OnceLock<Mutex<()>> = OnceLock::new();
1533
+ LOCK.get_or_init(|| Mutex::new(()))
1534
+ .lock()
1535
+ .unwrap_or_else(|poisoned| poisoned.into_inner())
1536
+ }
1537
+
1091
1538
  #[test]
1092
1539
  fn parse_args_requires_all_fields() {
1093
1540
  let result = parse_args(vec![OsString::from("--cwd")].into_iter());
@@ -1906,6 +2353,69 @@ printf '# Answer\nok\n' > "$output_path"
1906
2353
  );
1907
2354
  }
1908
2355
 
2356
+ #[test]
2357
+ fn invoke_codex_scrubs_node_options_before_node_shebang_launch() {
2358
+ let _guard = env_lock();
2359
+ if resolve_host_command("node").is_none() {
2360
+ return;
2361
+ }
2362
+
2363
+ let root = temp_allowlist_dir().expect("temp root");
2364
+ let repo = root.path.join("repo");
2365
+ create_dir_all(&repo).expect("create repo");
2366
+ let prompt_file = root.path.join("prompt.md");
2367
+ write(&prompt_file, "contract").expect("write prompt");
2368
+ let capture_path = root.path.join("node-options.txt");
2369
+ let fake_codex = root.path.join("codex-node-stub");
2370
+ write(
2371
+ &fake_codex,
2372
+ format!(
2373
+ r#"#!/usr/bin/env node
2374
+ const fs = require('fs');
2375
+ let outputPath = '';
2376
+ for (let index = 2; index < process.argv.length; index += 1) {{
2377
+ if (process.argv[index] === '-o') {{
2378
+ outputPath = process.argv[index + 1];
2379
+ index += 1;
2380
+ }}
2381
+ }}
2382
+ fs.writeFileSync({}, `NODE_OPTIONS=${{process.env.NODE_OPTIONS || ''}}\n`);
2383
+ fs.writeFileSync(outputPath, '# Answer\nok\n');
2384
+ "#,
2385
+ shell_quote(&capture_path.display().to_string())
2386
+ ),
2387
+ )
2388
+ .expect("write fake codex");
2389
+
2390
+ unsafe {
2391
+ env::set_var(CODEX_BIN_ENV, &fake_codex);
2392
+ env::set_var("NODE_OPTIONS", "--disable-warning=");
2393
+ }
2394
+ let attempt = invoke_codex(
2395
+ &Args {
2396
+ cwd: repo.clone(),
2397
+ prompt: "find tests".to_string(),
2398
+ prompt_file,
2399
+ instructions_file: repo.join("AGENTS.md"),
2400
+ spark_model: "spark-model".to_string(),
2401
+ fallback_model: "fallback-model".to_string(),
2402
+ },
2403
+ "spark-model",
2404
+ "contract",
2405
+ )
2406
+ .expect("invoke codex");
2407
+ unsafe {
2408
+ env::remove_var(CODEX_BIN_ENV);
2409
+ env::remove_var("NODE_OPTIONS");
2410
+ }
2411
+
2412
+ assert_eq!(attempt.status_code, 0);
2413
+ assert_eq!(
2414
+ read_to_string(&capture_path).expect("read capture"),
2415
+ "NODE_OPTIONS=\n"
2416
+ );
2417
+ }
2418
+
1909
2419
  #[test]
1910
2420
  fn invoke_codex_injects_model_instructions_file_override() {
1911
2421
  let _guard = env_lock();
@@ -2009,6 +2519,403 @@ printf '# Answer\nok\n' > "$output_path"
2009
2519
  assert_eq!(read_to_string(&bash_env_log).unwrap_or_default(), "");
2010
2520
  }
2011
2521
 
2522
+ #[test]
2523
+ fn execute_validated_shell_drops_login_flag_and_startup_env() {
2524
+ let _guard = env_lock();
2525
+ let root = temp_allowlist_dir().expect("temp root");
2526
+ let fake_shell = root.path.join("fake-sh");
2527
+ let argv_log = root.path.join("argv.log");
2528
+ let startup_log = root.path.join("startup.log");
2529
+ let bash_env = root.path.join("bash-env.sh");
2530
+ write(
2531
+ &bash_env,
2532
+ format!(
2533
+ "grep -qsi '^COLOR.*none' /etc/GREP_COLORS || true\nprintf startup >> {}\n",
2534
+ shell_quote(&startup_log.display().to_string())
2535
+ ),
2536
+ )
2537
+ .expect("write fake startup hook");
2538
+ write_executable(
2539
+ &fake_shell,
2540
+ &format!(
2541
+ r#"#!/bin/sh
2542
+ printf '%s
2543
+ ' "$@" > {}
2544
+ if [ "${{BASH_ENV:-}}" ]; then
2545
+ . "$BASH_ENV"
2546
+ fi
2547
+ if [ "$1" = "-lc" ]; then
2548
+ grep -qsi '^COLOR.*none' /etc/GREP_COLORS
2549
+ fi
2550
+ if [ "$1" = "-c" ]; then
2551
+ shift
2552
+ exec /bin/sh -c "$1"
2553
+ fi
2554
+ exit 64
2555
+ "#,
2556
+ shell_quote(&argv_log.display().to_string())
2557
+ ),
2558
+ )
2559
+ .expect("write fake shell");
2560
+
2561
+ unsafe {
2562
+ env::set_var("BASH_ENV", &bash_env);
2563
+ env::set_var("ENV", &bash_env);
2564
+ env::set_var(
2565
+ "PROMPT_COMMAND",
2566
+ "grep -qsi '^COLOR.*none' /etc/GREP_COLORS",
2567
+ );
2568
+ }
2569
+ let status = execute_validated_shell(
2570
+ &fake_shell.display().to_string(),
2571
+ "printf shell-ok >/dev/null",
2572
+ )
2573
+ .expect("execute fake shell");
2574
+ unsafe {
2575
+ env::remove_var("BASH_ENV");
2576
+ env::remove_var("ENV");
2577
+ env::remove_var("PROMPT_COMMAND");
2578
+ }
2579
+
2580
+ assert_eq!(status, 0);
2581
+ assert_eq!(
2582
+ read_to_string(&argv_log).expect("read argv"),
2583
+ "-c\nprintf shell-ok >/dev/null\n"
2584
+ );
2585
+ assert_eq!(read_to_string(&startup_log).unwrap_or_default(), "");
2586
+ }
2587
+
2588
+ #[test]
2589
+ fn sanitize_explore_subprocess_env_blocks_fedora_grep_colors_startup_hook() {
2590
+ let _guard = env_lock();
2591
+ let root = temp_allowlist_dir().expect("temp root");
2592
+ let repo = root.path.join("repo");
2593
+ create_dir_all(&repo).expect("create repo");
2594
+ let startup_log = root.path.join("startup.log");
2595
+ let bash_env = root.path.join("bash-env.sh");
2596
+ write(
2597
+ &bash_env,
2598
+ format!(
2599
+ "grep -qsi '^COLOR.*none' /etc/GREP_COLORS || true\nprintf 'fedora-startup-ran\n' >> {}\n",
2600
+ shell_quote(&startup_log.display().to_string())
2601
+ ),
2602
+ )
2603
+ .expect("write bash env");
2604
+ let allowlist = prepare_allowlist_environment().expect("allowlist environment");
2605
+ let bash_path = resolve_host_command("bash").expect("host bash path");
2606
+
2607
+ unsafe {
2608
+ env::set_var("BASH_ENV", &bash_env);
2609
+ env::set_var("ENV", &bash_env);
2610
+ env::set_var(
2611
+ "PROMPT_COMMAND",
2612
+ "grep -qsi '^COLOR.*none' /etc/GREP_COLORS",
2613
+ );
2614
+ }
2615
+ let mut child = Command::new(bash_path);
2616
+ child
2617
+ .arg("--noprofile")
2618
+ .arg("--norc")
2619
+ .arg("-c")
2620
+ .arg("true")
2621
+ .env(HARNESS_ROOT_ENV, &repo)
2622
+ .env(
2623
+ "PATH",
2624
+ build_codex_path(&allowlist.bin_dir, allowlist.sandbox_bin_dir.as_deref())
2625
+ .expect("restricted path"),
2626
+ );
2627
+ sanitize_explore_subprocess_env(&mut child);
2628
+ let output = child.output().expect("run bash");
2629
+ unsafe {
2630
+ env::remove_var("BASH_ENV");
2631
+ env::remove_var("ENV");
2632
+ env::remove_var("PROMPT_COMMAND");
2633
+ }
2634
+
2635
+ assert!(output.status.success());
2636
+ assert_eq!(read_to_string(&startup_log).unwrap_or_default(), "");
2637
+ assert!(
2638
+ !String::from_utf8_lossy(&output.stderr).contains("/etc/GREP_COLORS"),
2639
+ "stderr should not contain Fedora GREP_COLORS repo escape: {}",
2640
+ String::from_utf8_lossy(&output.stderr)
2641
+ );
2642
+ }
2643
+
2644
+ #[test]
2645
+ fn invoke_codex_times_out_and_returns_bounded_failure() {
2646
+ let _guard = env_lock();
2647
+ let root = temp_allowlist_dir().expect("temp root");
2648
+ let repo = root.path.join("repo");
2649
+ create_dir_all(&repo).expect("create repo");
2650
+ let prompt_file = root.path.join("prompt.md");
2651
+ write(&prompt_file, "contract").expect("write prompt");
2652
+ let fake_codex = root.path.join("codex-sleep");
2653
+ write_executable(
2654
+ &fake_codex,
2655
+ r#"#!/bin/sh
2656
+ printf 'fake codex started
2657
+ ' >&2
2658
+ /bin/sleep 5
2659
+ "#,
2660
+ )
2661
+ .expect("write fake codex");
2662
+
2663
+ unsafe {
2664
+ env::set_var(CODEX_BIN_ENV, &fake_codex);
2665
+ env::set_var(CODEX_TIMEOUT_MS_ENV, "100");
2666
+ }
2667
+ let started = Instant::now();
2668
+ let attempt = invoke_codex(
2669
+ &Args {
2670
+ cwd: repo.clone(),
2671
+ prompt: "find tests".to_string(),
2672
+ prompt_file,
2673
+ instructions_file: repo.join("AGENTS.md"),
2674
+ spark_model: "spark-model".to_string(),
2675
+ fallback_model: "fallback-model".to_string(),
2676
+ },
2677
+ "spark-model",
2678
+ "contract",
2679
+ )
2680
+ .expect("invoke codex");
2681
+ unsafe {
2682
+ env::remove_var(CODEX_BIN_ENV);
2683
+ env::remove_var(CODEX_TIMEOUT_MS_ENV);
2684
+ }
2685
+
2686
+ assert_eq!(attempt.status_code, 124);
2687
+ assert!(attempt.stderr.contains("timed out after 100ms"));
2688
+ assert!(attempt.stderr.contains("terminated process tree"));
2689
+ assert!(started.elapsed() < Duration::from_secs(3));
2690
+ assert!(attempt.output_markdown.is_none());
2691
+ }
2692
+
2693
+ #[cfg(unix)]
2694
+ #[test]
2695
+ fn run_command_with_timeout_kills_process_group_children() {
2696
+ let _env_guard = env_lock();
2697
+ let _process_guard = process_tree_lock();
2698
+ let root = temp_allowlist_dir().expect("temp root");
2699
+ let term_file = root.path.join("grandchild.term");
2700
+ let ready_file = root.path.join("grandchild.ready");
2701
+ let script = root.path.join("spawn-grandchild.sh");
2702
+ write_executable(
2703
+ &script,
2704
+ &format!(
2705
+ r#"#!/bin/sh
2706
+ (trap 'printf term > {}; exit 0' TERM; printf ready > {}; sleep 30) &
2707
+ while [ ! -f {} ]; do
2708
+ sleep 0.01
2709
+ done
2710
+ sleep 30
2711
+ "#,
2712
+ shell_quote(&term_file.display().to_string()),
2713
+ shell_quote(&ready_file.display().to_string()),
2714
+ shell_quote(&ready_file.display().to_string()),
2715
+ ),
2716
+ )
2717
+ .expect("write script");
2718
+
2719
+ let result = run_command_with_timeout(Command::new(&script), Duration::from_millis(500))
2720
+ .expect("run with timeout");
2721
+ let TimedCommandOutput::TimedOut { .. } = result else {
2722
+ panic!("expected timeout");
2723
+ };
2724
+ assert_eq!(read_to_string(&term_file).unwrap_or_default(), "term");
2725
+ }
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
+
2847
+ #[cfg(unix)]
2848
+ #[test]
2849
+ fn run_command_with_timeout_closes_inherited_stdio_after_parent_exit() {
2850
+ let _env_guard = env_lock();
2851
+ let _process_guard = process_tree_lock();
2852
+ let root = temp_allowlist_dir().expect("temp root");
2853
+ let script = root.path.join("inherited-stdio.sh");
2854
+ write_executable(
2855
+ &script,
2856
+ r#"#!/bin/sh
2857
+ (sleep 30) &
2858
+ printf 'parent stdout\n'
2859
+ printf 'parent stderr\n' >&2
2860
+ exit 0
2861
+ "#,
2862
+ )
2863
+ .expect("write script");
2864
+
2865
+ let started = Instant::now();
2866
+ let result = run_command_with_timeout(Command::new(&script), Duration::from_secs(10))
2867
+ .expect("run with inherited stdio descendant");
2868
+
2869
+ let TimedCommandOutput::Completed(output) = result else {
2870
+ panic!("expected parent completion");
2871
+ };
2872
+ assert!(
2873
+ started.elapsed() < Duration::from_secs(3),
2874
+ "reader cleanup should not wait for the descendant sleep"
2875
+ );
2876
+ assert!(output.status.success());
2877
+ assert_eq!(String::from_utf8_lossy(&output.stdout), "parent stdout\n");
2878
+ assert_eq!(String::from_utf8_lossy(&output.stderr), "parent stderr\n");
2879
+ }
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
+
2012
2919
  fn fallback_test_event() -> FallbackEvent {
2013
2920
  FallbackEvent {
2014
2921
  from_model: "spark-model".to_string(),