oh-my-codex 0.20.1 → 0.20.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 (654) hide show
  1. package/Cargo.lock +6 -6
  2. package/Cargo.toml +1 -1
  3. package/README.md +10 -1
  4. package/dist/adapt/__tests__/hermes.test.js.map +1 -1
  5. package/dist/adapt/contracts.d.ts.map +1 -1
  6. package/dist/agents/native-config.js.map +1 -1
  7. package/dist/auth/__tests__/hotswap.test.d.ts +2 -0
  8. package/dist/auth/__tests__/hotswap.test.d.ts.map +1 -0
  9. package/dist/auth/__tests__/hotswap.test.js +183 -0
  10. package/dist/auth/__tests__/hotswap.test.js.map +1 -0
  11. package/dist/auth/hotswap.d.ts.map +1 -1
  12. package/dist/auth/hotswap.js +17 -4
  13. package/dist/auth/hotswap.js.map +1 -1
  14. package/dist/auth/paths.js.map +1 -1
  15. package/dist/auth/storage.js.map +1 -1
  16. package/dist/autopilot/__tests__/ralplan-gate.test.js +4 -4
  17. package/dist/autopilot/__tests__/ralplan-gate.test.js.map +1 -1
  18. package/dist/autopilot/completion-gate.js.map +1 -1
  19. package/dist/autopilot/fsm.d.ts +1 -1
  20. package/dist/autopilot/fsm.d.ts.map +1 -1
  21. package/dist/autoresearch/goal.js.map +1 -1
  22. package/dist/autoresearch/runtime.js.map +1 -1
  23. package/dist/capabilities/lockfile.js.map +1 -1
  24. package/dist/catalog/reader.js.map +1 -1
  25. package/dist/catalog/skill-mirror.js.map +1 -1
  26. package/dist/cli/__tests__/adapt-help.test.js.map +1 -1
  27. package/dist/cli/__tests__/agents-init.test.js.map +1 -1
  28. package/dist/cli/__tests__/agents.test.js.map +1 -1
  29. package/dist/cli/__tests__/api.test.js.map +1 -1
  30. package/dist/cli/__tests__/ask.test.js.map +1 -1
  31. package/dist/cli/__tests__/auth.test.js.map +1 -1
  32. package/dist/cli/__tests__/autoresearch-guided.test.js.map +1 -1
  33. package/dist/cli/__tests__/autoresearch.test.js.map +1 -1
  34. package/dist/cli/__tests__/capabilities.test.js.map +1 -1
  35. package/dist/cli/__tests__/codex-plugin-layout.test.js.map +1 -1
  36. package/dist/cli/__tests__/doctor-context-window-warning.test.js.map +1 -1
  37. package/dist/cli/__tests__/doctor-invalid-config.test.js +47 -4
  38. package/dist/cli/__tests__/doctor-invalid-config.test.js.map +1 -1
  39. package/dist/cli/__tests__/doctor-team.test.js.map +1 -1
  40. package/dist/cli/__tests__/doctor-warning-copy.test.js +650 -15
  41. package/dist/cli/__tests__/doctor-warning-copy.test.js.map +1 -1
  42. package/dist/cli/__tests__/exec.test.js.map +1 -1
  43. package/dist/cli/__tests__/imagegen-continuation.test.js.map +1 -1
  44. package/dist/cli/__tests__/index.test.js +216 -11
  45. package/dist/cli/__tests__/index.test.js.map +1 -1
  46. package/dist/cli/__tests__/launch-fallback.test.js +49 -3
  47. package/dist/cli/__tests__/launch-fallback.test.js.map +1 -1
  48. package/dist/cli/__tests__/mcp-parity.test.js +84 -12
  49. package/dist/cli/__tests__/mcp-parity.test.js.map +1 -1
  50. package/dist/cli/__tests__/native-hook-claim-journal.test.d.ts +2 -0
  51. package/dist/cli/__tests__/native-hook-claim-journal.test.d.ts.map +1 -0
  52. package/dist/cli/__tests__/native-hook-claim-journal.test.js +128 -0
  53. package/dist/cli/__tests__/native-hook-claim-journal.test.js.map +1 -0
  54. package/dist/cli/__tests__/nested-help-routing.test.js.map +1 -1
  55. package/dist/cli/__tests__/question.test.js.map +1 -1
  56. package/dist/cli/__tests__/ralph-deslop-contract.test.js.map +1 -1
  57. package/dist/cli/__tests__/ralph-goal-mode-contract.test.js +6 -0
  58. package/dist/cli/__tests__/ralph-goal-mode-contract.test.js.map +1 -1
  59. package/dist/cli/__tests__/ralph-prd-deep-interview.test.js.map +1 -1
  60. package/dist/cli/__tests__/ralph-prd-smoke.test.js.map +1 -1
  61. package/dist/cli/__tests__/ralph.test.js +8 -3
  62. package/dist/cli/__tests__/ralph.test.js.map +1 -1
  63. package/dist/cli/__tests__/ralplan-bootstrap-3181.test.d.ts +2 -0
  64. package/dist/cli/__tests__/ralplan-bootstrap-3181.test.d.ts.map +1 -0
  65. package/dist/cli/__tests__/ralplan-bootstrap-3181.test.js +187 -0
  66. package/dist/cli/__tests__/ralplan-bootstrap-3181.test.js.map +1 -0
  67. package/dist/cli/__tests__/ralplan.test.d.ts +2 -0
  68. package/dist/cli/__tests__/ralplan.test.d.ts.map +1 -0
  69. package/dist/cli/__tests__/ralplan.test.js +129 -0
  70. package/dist/cli/__tests__/ralplan.test.js.map +1 -0
  71. package/dist/cli/__tests__/resume.test.js +6 -1
  72. package/dist/cli/__tests__/resume.test.js.map +1 -1
  73. package/dist/cli/__tests__/session-scoped-runtime.test.js +25 -0
  74. package/dist/cli/__tests__/session-scoped-runtime.test.js.map +1 -1
  75. package/dist/cli/__tests__/session-search-help.test.js.map +1 -1
  76. package/dist/cli/__tests__/session-search.test.js.map +1 -1
  77. package/dist/cli/__tests__/setup-agents-overwrite.test.js +79 -0
  78. package/dist/cli/__tests__/setup-agents-overwrite.test.js.map +1 -1
  79. package/dist/cli/__tests__/setup-gh-star.test.js.map +1 -1
  80. package/dist/cli/__tests__/setup-hooks-shared-ownership.test.js +77 -3
  81. package/dist/cli/__tests__/setup-hooks-shared-ownership.test.js.map +1 -1
  82. package/dist/cli/__tests__/setup-hooks-trust-e2e.test.d.ts +2 -0
  83. package/dist/cli/__tests__/setup-hooks-trust-e2e.test.d.ts.map +1 -0
  84. package/dist/cli/__tests__/setup-hooks-trust-e2e.test.js +285 -0
  85. package/dist/cli/__tests__/setup-hooks-trust-e2e.test.js.map +1 -0
  86. package/dist/cli/__tests__/setup-install-mode.test.js +2261 -14
  87. package/dist/cli/__tests__/setup-install-mode.test.js.map +1 -1
  88. package/dist/cli/__tests__/setup-preferences.test.d.ts +2 -0
  89. package/dist/cli/__tests__/setup-preferences.test.d.ts.map +1 -0
  90. package/dist/cli/__tests__/setup-preferences.test.js +79 -0
  91. package/dist/cli/__tests__/setup-preferences.test.js.map +1 -0
  92. package/dist/cli/__tests__/setup-refresh.test.js +33 -2
  93. package/dist/cli/__tests__/setup-refresh.test.js.map +1 -1
  94. package/dist/cli/__tests__/setup-scope.test.js +46 -3
  95. package/dist/cli/__tests__/setup-scope.test.js.map +1 -1
  96. package/dist/cli/__tests__/sparkshell-cli.test.js.map +1 -1
  97. package/dist/cli/__tests__/state.test.js +37 -0
  98. package/dist/cli/__tests__/state.test.js.map +1 -1
  99. package/dist/cli/__tests__/team.test.js.map +1 -1
  100. package/dist/cli/__tests__/ultragoal.test.js +61 -1
  101. package/dist/cli/__tests__/ultragoal.test.js.map +1 -1
  102. package/dist/cli/__tests__/uninstall.test.js +1771 -24
  103. package/dist/cli/__tests__/uninstall.test.js.map +1 -1
  104. package/dist/cli/__tests__/update.test.js +58 -0
  105. package/dist/cli/__tests__/update.test.js.map +1 -1
  106. package/dist/cli/__tests__/url.test.js.map +1 -1
  107. package/dist/cli/__tests__/windows-popup-loop-contract.test.js.map +1 -1
  108. package/dist/cli/adapt.js.map +1 -1
  109. package/dist/cli/agents-init.js.map +1 -1
  110. package/dist/cli/agents.js.map +1 -1
  111. package/dist/cli/api.js.map +1 -1
  112. package/dist/cli/ask.d.ts +1 -1
  113. package/dist/cli/ask.d.ts.map +1 -1
  114. package/dist/cli/ask.js.map +1 -1
  115. package/dist/cli/auth.js.map +1 -1
  116. package/dist/cli/autoresearch-guided.js.map +1 -1
  117. package/dist/cli/autoresearch-intake.js.map +1 -1
  118. package/dist/cli/capabilities.js.map +1 -1
  119. package/dist/cli/cleanup.js.map +1 -1
  120. package/dist/cli/codex-feature-probe.js.map +1 -1
  121. package/dist/cli/codex-home.js.map +1 -1
  122. package/dist/cli/doctor.d.ts +18 -0
  123. package/dist/cli/doctor.d.ts.map +1 -1
  124. package/dist/cli/doctor.js +594 -130
  125. package/dist/cli/doctor.js.map +1 -1
  126. package/dist/cli/explore.js.map +1 -1
  127. package/dist/cli/index.d.ts +11 -3
  128. package/dist/cli/index.d.ts.map +1 -1
  129. package/dist/cli/index.js +108 -20
  130. package/dist/cli/index.js.map +1 -1
  131. package/dist/cli/mcp-serve.js.map +1 -1
  132. package/dist/cli/mission.js.map +1 -1
  133. package/dist/cli/native-assets.js.map +1 -1
  134. package/dist/cli/native-hook-claim-journal.d.ts +18 -0
  135. package/dist/cli/native-hook-claim-journal.d.ts.map +1 -0
  136. package/dist/cli/native-hook-claim-journal.js +239 -0
  137. package/dist/cli/native-hook-claim-journal.js.map +1 -0
  138. package/dist/cli/omx.js.map +1 -1
  139. package/dist/cli/plugin-marketplace.js.map +1 -1
  140. package/dist/cli/project-runtime-codex-homes.js.map +1 -1
  141. package/dist/cli/question.js.map +1 -1
  142. package/dist/cli/ralph.d.ts.map +1 -1
  143. package/dist/cli/ralph.js +3 -2
  144. package/dist/cli/ralph.js.map +1 -1
  145. package/dist/cli/ralplan.d.ts +14 -0
  146. package/dist/cli/ralplan.d.ts.map +1 -0
  147. package/dist/cli/ralplan.js +203 -0
  148. package/dist/cli/ralplan.js.map +1 -0
  149. package/dist/cli/setup-preferences.d.ts +11 -0
  150. package/dist/cli/setup-preferences.d.ts.map +1 -1
  151. package/dist/cli/setup-preferences.js +32 -0
  152. package/dist/cli/setup-preferences.js.map +1 -1
  153. package/dist/cli/setup.d.ts +53 -0
  154. package/dist/cli/setup.d.ts.map +1 -1
  155. package/dist/cli/setup.js +1401 -398
  156. package/dist/cli/setup.js.map +1 -1
  157. package/dist/cli/sparkshell.js.map +1 -1
  158. package/dist/cli/star-prompt.js.map +1 -1
  159. package/dist/cli/state.d.ts.map +1 -1
  160. package/dist/cli/state.js +2 -1
  161. package/dist/cli/state.js.map +1 -1
  162. package/dist/cli/team.js.map +1 -1
  163. package/dist/cli/ultragoal.d.ts +5 -1
  164. package/dist/cli/ultragoal.d.ts.map +1 -1
  165. package/dist/cli/ultragoal.js +34 -2
  166. package/dist/cli/ultragoal.js.map +1 -1
  167. package/dist/cli/uninstall.d.ts +8 -0
  168. package/dist/cli/uninstall.d.ts.map +1 -1
  169. package/dist/cli/uninstall.js +944 -95
  170. package/dist/cli/uninstall.js.map +1 -1
  171. package/dist/cli/update.d.ts.map +1 -1
  172. package/dist/cli/update.js +8 -1
  173. package/dist/cli/update.js.map +1 -1
  174. package/dist/cli/url.js.map +1 -1
  175. package/dist/compat/__tests__/doctor-contract.test.js +5 -1
  176. package/dist/compat/__tests__/doctor-contract.test.js.map +1 -1
  177. package/dist/compat/__tests__/rust-runtime-compat.test.js.map +1 -1
  178. package/dist/config/__tests__/codex-hooks.test.js +1287 -11
  179. package/dist/config/__tests__/codex-hooks.test.js.map +1 -1
  180. package/dist/config/__tests__/generator-idempotent.test.js +850 -32
  181. package/dist/config/__tests__/generator-idempotent.test.js.map +1 -1
  182. package/dist/config/__tests__/generator-notify.test.js +6 -1
  183. package/dist/config/__tests__/generator-notify.test.js.map +1 -1
  184. package/dist/config/codex-feature-flags.d.ts.map +1 -1
  185. package/dist/config/codex-feature-flags.js.map +1 -1
  186. package/dist/config/codex-hooks.d.ts +124 -1
  187. package/dist/config/codex-hooks.d.ts.map +1 -1
  188. package/dist/config/codex-hooks.js +2142 -265
  189. package/dist/config/codex-hooks.js.map +1 -1
  190. package/dist/config/commit-lore-guard.js.map +1 -1
  191. package/dist/config/generator.d.ts +21 -16
  192. package/dist/config/generator.d.ts.map +1 -1
  193. package/dist/config/generator.js +991 -137
  194. package/dist/config/generator.js.map +1 -1
  195. package/dist/config/mcp-registry.js.map +1 -1
  196. package/dist/config/models.d.ts +2 -2
  197. package/dist/config/models.d.ts.map +1 -1
  198. package/dist/config/models.js.map +1 -1
  199. package/dist/config/omx-first-party-mcp.js.map +1 -1
  200. package/dist/config/team-mode.d.ts.map +1 -1
  201. package/dist/document-refresh/__tests__/enforcer.test.js.map +1 -1
  202. package/dist/document-refresh/enforcer.js.map +1 -1
  203. package/dist/exec/followup.js.map +1 -1
  204. package/dist/goal-workflows/artifacts.js.map +1 -1
  205. package/dist/hooks/__tests__/agents-overlay.test.js +7 -4
  206. package/dist/hooks/__tests__/agents-overlay.test.js.map +1 -1
  207. package/dist/hooks/__tests__/analyze-routing-contract.test.js.map +1 -1
  208. package/dist/hooks/__tests__/analyze-skill-contract.test.js.map +1 -1
  209. package/dist/hooks/__tests__/anti-slop-workflow.test.js.map +1 -1
  210. package/dist/hooks/__tests__/autopilot-skill-contract.test.js +9 -3
  211. package/dist/hooks/__tests__/autopilot-skill-contract.test.js.map +1 -1
  212. package/dist/hooks/__tests__/code-review-skill-contract.test.js.map +1 -1
  213. package/dist/hooks/__tests__/consensus-execution-handoff.test.js.map +1 -1
  214. package/dist/hooks/__tests__/deep-interview-contract.test.js.map +1 -1
  215. package/dist/hooks/__tests__/design-skill.test.js.map +1 -1
  216. package/dist/hooks/__tests__/keyword-detector.test.js +1372 -2
  217. package/dist/hooks/__tests__/keyword-detector.test.js.map +1 -1
  218. package/dist/hooks/__tests__/notify-fallback-watcher.test.js +14 -0
  219. package/dist/hooks/__tests__/notify-fallback-watcher.test.js.map +1 -1
  220. package/dist/hooks/__tests__/notify-hook-all-workers-idle.test.js.map +1 -1
  221. package/dist/hooks/__tests__/notify-hook-auto-nudge.test.js +669 -1
  222. package/dist/hooks/__tests__/notify-hook-auto-nudge.test.js.map +1 -1
  223. package/dist/hooks/__tests__/notify-hook-cross-worktree-heartbeat.test.js +2 -0
  224. package/dist/hooks/__tests__/notify-hook-cross-worktree-heartbeat.test.js.map +1 -1
  225. package/dist/hooks/__tests__/notify-hook-managed-tmux.test.js +142 -1
  226. package/dist/hooks/__tests__/notify-hook-managed-tmux.test.js.map +1 -1
  227. package/dist/hooks/__tests__/notify-hook-modules.test.js.map +1 -1
  228. package/dist/hooks/__tests__/notify-hook-non-omx-guard.test.js +48 -2
  229. package/dist/hooks/__tests__/notify-hook-non-omx-guard.test.js.map +1 -1
  230. package/dist/hooks/__tests__/notify-hook-ralph-resume.test.js +61 -44
  231. package/dist/hooks/__tests__/notify-hook-ralph-resume.test.js.map +1 -1
  232. package/dist/hooks/__tests__/notify-hook-regression-205.test.js.map +1 -1
  233. package/dist/hooks/__tests__/notify-hook-session-idle-dedupe.test.js +3 -2
  234. package/dist/hooks/__tests__/notify-hook-session-idle-dedupe.test.js.map +1 -1
  235. package/dist/hooks/__tests__/notify-hook-session-scope.test.js +243 -4
  236. package/dist/hooks/__tests__/notify-hook-session-scope.test.js.map +1 -1
  237. package/dist/hooks/__tests__/notify-hook-team-dispatch.test.js.map +1 -1
  238. package/dist/hooks/__tests__/notify-hook-team-leader-nudge.test.js +8 -1
  239. package/dist/hooks/__tests__/notify-hook-team-leader-nudge.test.js.map +1 -1
  240. package/dist/hooks/__tests__/notify-hook-team-tmux-guard.test.js.map +1 -1
  241. package/dist/hooks/__tests__/notify-hook-tmux-heal.test.js.map +1 -1
  242. package/dist/hooks/__tests__/notify-hook-tmux-scrollback.test.js.map +1 -1
  243. package/dist/hooks/__tests__/notify-hook-visual-verdict.test.js.map +1 -1
  244. package/dist/hooks/__tests__/notify-hook-worker-idle.test.js.map +1 -1
  245. package/dist/hooks/__tests__/openclaw-setup-contract.test.js.map +1 -1
  246. package/dist/hooks/__tests__/pre-context-gate-skills.test.js.map +1 -1
  247. package/dist/hooks/__tests__/prometheus-strict-contract.test.js.map +1 -1
  248. package/dist/hooks/__tests__/prompt-guidance-fragments.test.js.map +1 -1
  249. package/dist/hooks/__tests__/prompt-guidance-test-helpers.js.map +1 -1
  250. package/dist/hooks/__tests__/prompt-session-provenance.test.d.ts +2 -0
  251. package/dist/hooks/__tests__/prompt-session-provenance.test.d.ts.map +1 -0
  252. package/dist/hooks/__tests__/prompt-session-provenance.test.js +82 -0
  253. package/dist/hooks/__tests__/prompt-session-provenance.test.js.map +1 -0
  254. package/dist/hooks/__tests__/session.test.js +532 -11
  255. package/dist/hooks/__tests__/session.test.js.map +1 -1
  256. package/dist/hooks/__tests__/skill-catalog-hygiene.test.js.map +1 -1
  257. package/dist/hooks/__tests__/team-runtime-gating-docs-contract.test.js.map +1 -1
  258. package/dist/hooks/__tests__/visual-ralph-skill.test.js.map +1 -1
  259. package/dist/hooks/__tests__/visual-verdict-loop.test.js.map +1 -1
  260. package/dist/hooks/agents-overlay.js +5 -5
  261. package/dist/hooks/agents-overlay.js.map +1 -1
  262. package/dist/hooks/code-simplifier/index.js.map +1 -1
  263. package/dist/hooks/explore-routing.js.map +1 -1
  264. package/dist/hooks/extensibility/dispatcher.js.map +1 -1
  265. package/dist/hooks/extensibility/events.js.map +1 -1
  266. package/dist/hooks/extensibility/loader.js.map +1 -1
  267. package/dist/hooks/extensibility/plugin-runner-stdin.js.map +1 -1
  268. package/dist/hooks/extensibility/sdk/logging.js.map +1 -1
  269. package/dist/hooks/keyword-detector.d.ts +33 -7
  270. package/dist/hooks/keyword-detector.d.ts.map +1 -1
  271. package/dist/hooks/keyword-detector.js +2228 -119
  272. package/dist/hooks/keyword-detector.js.map +1 -1
  273. package/dist/hooks/keyword-registry.d.ts +12 -0
  274. package/dist/hooks/keyword-registry.d.ts.map +1 -1
  275. package/dist/hooks/keyword-registry.js +41 -0
  276. package/dist/hooks/keyword-registry.js.map +1 -1
  277. package/dist/hooks/prompt-session-provenance.d.ts +87 -0
  278. package/dist/hooks/prompt-session-provenance.d.ts.map +1 -0
  279. package/dist/hooks/prompt-session-provenance.js +180 -0
  280. package/dist/hooks/prompt-session-provenance.js.map +1 -0
  281. package/dist/hooks/session.d.ts +139 -34
  282. package/dist/hooks/session.d.ts.map +1 -1
  283. package/dist/hooks/session.js +1239 -317
  284. package/dist/hooks/session.js.map +1 -1
  285. package/dist/hooks/task-size-detector.js.map +1 -1
  286. package/dist/hud/__tests__/state.test.js.map +1 -1
  287. package/dist/hud/authority.js.map +1 -1
  288. package/dist/hud/constants.js.map +1 -1
  289. package/dist/hud/index.js.map +1 -1
  290. package/dist/hud/reconcile.js.map +1 -1
  291. package/dist/hud/render.js.map +1 -1
  292. package/dist/hud/state.js.map +1 -1
  293. package/dist/hud/tmux.js.map +1 -1
  294. package/dist/leader/__tests__/contract.test.js +103 -1
  295. package/dist/leader/__tests__/contract.test.js.map +1 -1
  296. package/dist/leader/contract.d.ts +26 -5
  297. package/dist/leader/contract.d.ts.map +1 -1
  298. package/dist/leader/contract.js +109 -3
  299. package/dist/leader/contract.js.map +1 -1
  300. package/dist/mcp/__tests__/bootstrap.test.js.map +1 -1
  301. package/dist/mcp/__tests__/server-lifecycle.test.js +15 -2
  302. package/dist/mcp/__tests__/server-lifecycle.test.js.map +1 -1
  303. package/dist/mcp/__tests__/state-paths.test.js +98 -1
  304. package/dist/mcp/__tests__/state-paths.test.js.map +1 -1
  305. package/dist/mcp/bootstrap.js.map +1 -1
  306. package/dist/mcp/code-intel-server.d.ts +58 -58
  307. package/dist/mcp/code-intel-server.d.ts.map +1 -1
  308. package/dist/mcp/code-intel-server.js.map +1 -1
  309. package/dist/mcp/hermes-bridge.js.map +1 -1
  310. package/dist/mcp/hermes-server.d.ts +70 -70
  311. package/dist/mcp/hermes-server.d.ts.map +1 -1
  312. package/dist/mcp/lifecycle-telemetry.js.map +1 -1
  313. package/dist/mcp/memory-server.d.ts +34 -34
  314. package/dist/mcp/memory-server.d.ts.map +1 -1
  315. package/dist/mcp/memory-server.js.map +1 -1
  316. package/dist/mcp/state-paths.d.ts +14 -0
  317. package/dist/mcp/state-paths.d.ts.map +1 -1
  318. package/dist/mcp/state-paths.js +96 -20
  319. package/dist/mcp/state-paths.js.map +1 -1
  320. package/dist/mcp/state-server.d.ts +47 -47
  321. package/dist/mcp/state-server.d.ts.map +1 -1
  322. package/dist/mcp/trace-server.d.ts +4 -4
  323. package/dist/mcp/trace-server.d.ts.map +1 -1
  324. package/dist/mcp/trace-server.js.map +1 -1
  325. package/dist/mcp/wiki-server.d.ts +23 -23
  326. package/dist/mcp/wiki-server.d.ts.map +1 -1
  327. package/dist/mcp/wiki-server.js.map +1 -1
  328. package/dist/modes/__tests__/base-session-scope.test.js +53 -1
  329. package/dist/modes/__tests__/base-session-scope.test.js.map +1 -1
  330. package/dist/modes/base.d.ts.map +1 -1
  331. package/dist/modes/base.js +5 -5
  332. package/dist/modes/base.js.map +1 -1
  333. package/dist/notifications/__tests__/lifecycle-dedupe.test.js.map +1 -1
  334. package/dist/notifications/__tests__/reply-listener.test.js.map +1 -1
  335. package/dist/notifications/__tests__/session-idle-tail-dedupe.test.js +177 -85
  336. package/dist/notifications/__tests__/session-idle-tail-dedupe.test.js.map +1 -1
  337. package/dist/notifications/__tests__/session-registry.test.js.map +1 -1
  338. package/dist/notifications/__tests__/template-engine.test.js.map +1 -1
  339. package/dist/notifications/__tests__/verbosity.test.js.map +1 -1
  340. package/dist/notifications/http-client.js.map +1 -1
  341. package/dist/notifications/index.d.ts +4 -1
  342. package/dist/notifications/index.d.ts.map +1 -1
  343. package/dist/notifications/index.js +3 -9
  344. package/dist/notifications/index.js.map +1 -1
  345. package/dist/notifications/lifecycle-dedupe.js.map +1 -1
  346. package/dist/notifications/reply-listener.d.ts.map +1 -1
  347. package/dist/notifications/reply-listener.js.map +1 -1
  348. package/dist/notifications/session-registry.js.map +1 -1
  349. package/dist/notifications/session-status.js.map +1 -1
  350. package/dist/notifications/temp-contract.js.map +1 -1
  351. package/dist/notifications/tmux-detector.js.map +1 -1
  352. package/dist/notifications/tmux.js.map +1 -1
  353. package/dist/openclaw/config.js.map +1 -1
  354. package/dist/openclaw/dispatcher.js.map +1 -1
  355. package/dist/pipeline/__tests__/orchestrator.test.js.map +1 -1
  356. package/dist/pipeline/__tests__/stages.test.js +8 -5
  357. package/dist/pipeline/__tests__/stages.test.js.map +1 -1
  358. package/dist/pipeline/stages/code-review.js.map +1 -1
  359. package/dist/pipeline/stages/ralph-verify.js.map +1 -1
  360. package/dist/pipeline/stages/ralplan.js.map +1 -1
  361. package/dist/pipeline/stages/team-exec.js.map +1 -1
  362. package/dist/pipeline/stages/ultragoal.js.map +1 -1
  363. package/dist/pipeline/stages/ultraqa.js.map +1 -1
  364. package/dist/planning/__tests__/approved-execution-lifecycle-matrix.test.js.map +1 -1
  365. package/dist/planning/artifact-names.js.map +1 -1
  366. package/dist/planning/artifacts.js.map +1 -1
  367. package/dist/question/__tests__/state.test.js.map +1 -1
  368. package/dist/question/__tests__/ui.test.js.map +1 -1
  369. package/dist/question/autopilot-wait.js.map +1 -1
  370. package/dist/question/client.d.ts.map +1 -1
  371. package/dist/question/client.js.map +1 -1
  372. package/dist/question/deep-interview.js.map +1 -1
  373. package/dist/question/events.js.map +1 -1
  374. package/dist/question/renderer.js.map +1 -1
  375. package/dist/question/state.d.ts.map +1 -1
  376. package/dist/question/state.js.map +1 -1
  377. package/dist/question/ui.js.map +1 -1
  378. package/dist/ralph/contract.d.ts +1 -1
  379. package/dist/ralph/contract.d.ts.map +1 -1
  380. package/dist/ralplan/__tests__/consensus-gate.test.js +403 -8
  381. package/dist/ralplan/__tests__/consensus-gate.test.js.map +1 -1
  382. package/dist/ralplan/__tests__/runtime.test.js +112 -14
  383. package/dist/ralplan/__tests__/runtime.test.js.map +1 -1
  384. package/dist/ralplan/consensus-gate.d.ts +3 -3
  385. package/dist/ralplan/consensus-gate.d.ts.map +1 -1
  386. package/dist/ralplan/consensus-gate.js +155 -60
  387. package/dist/ralplan/consensus-gate.js.map +1 -1
  388. package/dist/ralplan/runtime.d.ts +2 -2
  389. package/dist/ralplan/runtime.d.ts.map +1 -1
  390. package/dist/ralplan/runtime.js +1 -2
  391. package/dist/ralplan/runtime.js.map +1 -1
  392. package/dist/runtime/bridge.d.ts.map +1 -1
  393. package/dist/runtime/bridge.js.map +1 -1
  394. package/dist/runtime/process-tree.js.map +1 -1
  395. package/dist/runtime/run-loop.js.map +1 -1
  396. package/dist/runtime/run-outcome.d.ts +3 -3
  397. package/dist/runtime/run-outcome.d.ts.map +1 -1
  398. package/dist/runtime/run-outcome.js.map +1 -1
  399. package/dist/runtime/run-state.js.map +1 -1
  400. package/dist/scripts/__tests__/codex-native-hook.test.js +4121 -592
  401. package/dist/scripts/__tests__/codex-native-hook.test.js.map +1 -1
  402. package/dist/scripts/__tests__/fixtures/issue-3138-installed-smoke.d.ts +2 -0
  403. package/dist/scripts/__tests__/fixtures/issue-3138-installed-smoke.d.ts.map +1 -0
  404. package/dist/scripts/__tests__/fixtures/issue-3138-installed-smoke.js +634 -0
  405. package/dist/scripts/__tests__/fixtures/issue-3138-installed-smoke.js.map +1 -0
  406. package/dist/scripts/__tests__/generate-release-body.test.js.map +1 -1
  407. package/dist/scripts/__tests__/hook-derived-watcher.test.js.map +1 -1
  408. package/dist/scripts/__tests__/notify-dispatcher.test.js.map +1 -1
  409. package/dist/scripts/__tests__/notify-fallback-delivery.test.d.ts +2 -0
  410. package/dist/scripts/__tests__/notify-fallback-delivery.test.d.ts.map +1 -0
  411. package/dist/scripts/__tests__/notify-fallback-delivery.test.js +174 -0
  412. package/dist/scripts/__tests__/notify-fallback-delivery.test.js.map +1 -0
  413. package/dist/scripts/__tests__/notify-state-io.test.js +57 -1
  414. package/dist/scripts/__tests__/notify-state-io.test.js.map +1 -1
  415. package/dist/scripts/__tests__/role-intent-bootstrap-e2e-3181.test.d.ts +2 -0
  416. package/dist/scripts/__tests__/role-intent-bootstrap-e2e-3181.test.d.ts.map +1 -0
  417. package/dist/scripts/__tests__/role-intent-bootstrap-e2e-3181.test.js +205 -0
  418. package/dist/scripts/__tests__/role-intent-bootstrap-e2e-3181.test.js.map +1 -0
  419. package/dist/scripts/__tests__/role-intent-durable-recovery-3181.test.d.ts +2 -0
  420. package/dist/scripts/__tests__/role-intent-durable-recovery-3181.test.d.ts.map +1 -0
  421. package/dist/scripts/__tests__/role-intent-durable-recovery-3181.test.js +175 -0
  422. package/dist/scripts/__tests__/role-intent-durable-recovery-3181.test.js.map +1 -0
  423. package/dist/scripts/__tests__/run-test-files.test.js.map +1 -1
  424. package/dist/scripts/__tests__/smoke-packed-install.test.js +848 -4
  425. package/dist/scripts/__tests__/smoke-packed-install.test.js.map +1 -1
  426. package/dist/scripts/__tests__/test-reply-listener-live.test.js.map +1 -1
  427. package/dist/scripts/build-api.js.map +1 -1
  428. package/dist/scripts/build-explore-harness.js.map +1 -1
  429. package/dist/scripts/build-sparkshell.js.map +1 -1
  430. package/dist/scripts/cleanup-explore-harness.js.map +1 -1
  431. package/dist/scripts/codex-execution-surface.js.map +1 -1
  432. package/dist/scripts/codex-native-hook.d.ts.map +1 -1
  433. package/dist/scripts/codex-native-hook.js +689 -156
  434. package/dist/scripts/codex-native-hook.js.map +1 -1
  435. package/dist/scripts/generate-native-release-manifest.js.map +1 -1
  436. package/dist/scripts/generate-release-body.js.map +1 -1
  437. package/dist/scripts/hook-payload-guard.d.ts.map +1 -1
  438. package/dist/scripts/hook-payload-guard.js.map +1 -1
  439. package/dist/scripts/notify-fallback-delivery.d.ts +48 -0
  440. package/dist/scripts/notify-fallback-delivery.d.ts.map +1 -0
  441. package/dist/scripts/notify-fallback-delivery.js +419 -0
  442. package/dist/scripts/notify-fallback-delivery.js.map +1 -0
  443. package/dist/scripts/notify-fallback-watcher.js +136 -30
  444. package/dist/scripts/notify-fallback-watcher.js.map +1 -1
  445. package/dist/scripts/notify-hook/auto-nudge.d.ts +18 -9
  446. package/dist/scripts/notify-hook/auto-nudge.d.ts.map +1 -1
  447. package/dist/scripts/notify-hook/auto-nudge.js +39 -13
  448. package/dist/scripts/notify-hook/auto-nudge.js.map +1 -1
  449. package/dist/scripts/notify-hook/managed-tmux.d.ts +23 -0
  450. package/dist/scripts/notify-hook/managed-tmux.d.ts.map +1 -1
  451. package/dist/scripts/notify-hook/managed-tmux.js +122 -12
  452. package/dist/scripts/notify-hook/managed-tmux.js.map +1 -1
  453. package/dist/scripts/notify-hook/orchestration-intent.d.ts +7 -7
  454. package/dist/scripts/notify-hook/orchestration-intent.d.ts.map +1 -1
  455. package/dist/scripts/notify-hook/ralph-session-resume.d.ts +5 -2
  456. package/dist/scripts/notify-hook/ralph-session-resume.d.ts.map +1 -1
  457. package/dist/scripts/notify-hook/ralph-session-resume.js +33 -37
  458. package/dist/scripts/notify-hook/ralph-session-resume.js.map +1 -1
  459. package/dist/scripts/notify-hook/state-io.d.ts +17 -0
  460. package/dist/scripts/notify-hook/state-io.d.ts.map +1 -1
  461. package/dist/scripts/notify-hook/state-io.js +73 -11
  462. package/dist/scripts/notify-hook/state-io.js.map +1 -1
  463. package/dist/scripts/notify-hook/team-dispatch.d.ts +1 -1
  464. package/dist/scripts/notify-hook/team-dispatch.js.map +1 -1
  465. package/dist/scripts/notify-hook/team-leader-nudge.d.ts +2 -2
  466. package/dist/scripts/notify-hook/team-tmux-guard.d.ts +4 -4
  467. package/dist/scripts/notify-hook/team-worker-posttooluse.js.map +1 -1
  468. package/dist/scripts/notify-hook/team-worker-stop.d.ts +1 -1
  469. package/dist/scripts/notify-hook/team-worker.d.ts +2 -2
  470. package/dist/scripts/notify-hook/tmux-injection.d.ts +8 -1
  471. package/dist/scripts/notify-hook/tmux-injection.d.ts.map +1 -1
  472. package/dist/scripts/notify-hook/tmux-injection.js +25 -16
  473. package/dist/scripts/notify-hook/tmux-injection.js.map +1 -1
  474. package/dist/scripts/notify-hook/visual-verdict.d.ts +1 -1
  475. package/dist/scripts/notify-hook/visual-verdict.d.ts.map +1 -1
  476. package/dist/scripts/notify-hook/visual-verdict.js +15 -13
  477. package/dist/scripts/notify-hook/visual-verdict.js.map +1 -1
  478. package/dist/scripts/notify-hook.d.ts +24 -1
  479. package/dist/scripts/notify-hook.d.ts.map +1 -1
  480. package/dist/scripts/notify-hook.js +266 -90
  481. package/dist/scripts/notify-hook.js.map +1 -1
  482. package/dist/scripts/postinstall.js.map +1 -1
  483. package/dist/scripts/prompt-inventory.js.map +1 -1
  484. package/dist/scripts/smoke-packed-install.d.ts +879 -2
  485. package/dist/scripts/smoke-packed-install.d.ts.map +1 -1
  486. package/dist/scripts/smoke-packed-install.js +1528 -7
  487. package/dist/scripts/smoke-packed-install.js.map +1 -1
  488. package/dist/scripts/sync-plugin-mirror.js.map +1 -1
  489. package/dist/scripts/test-reply-listener-live.js.map +1 -1
  490. package/dist/scripts/test-sparkshell.js.map +1 -1
  491. package/dist/scripts/verify-native-agents.js.map +1 -1
  492. package/dist/session-history/friction.js.map +1 -1
  493. package/dist/session-history/search.js.map +1 -1
  494. package/dist/sidecar/__tests__/boundary.test.js.map +1 -1
  495. package/dist/sidecar/collector.js.map +1 -1
  496. package/dist/sidecar/index.js.map +1 -1
  497. package/dist/sidecar/render.js.map +1 -1
  498. package/dist/sidecar/tmux.js.map +1 -1
  499. package/dist/state/__tests__/operations.test.js +64 -15
  500. package/dist/state/__tests__/operations.test.js.map +1 -1
  501. package/dist/state/__tests__/skill-active.test.js +19 -1
  502. package/dist/state/__tests__/skill-active.test.js.map +1 -1
  503. package/dist/state/__tests__/workflow-transition.test.js +61 -0
  504. package/dist/state/__tests__/workflow-transition.test.js.map +1 -1
  505. package/dist/state/mode-state-context.js.map +1 -1
  506. package/dist/state/operations.d.ts +1 -1
  507. package/dist/state/operations.d.ts.map +1 -1
  508. package/dist/state/operations.js +49 -55
  509. package/dist/state/operations.js.map +1 -1
  510. package/dist/state/paths.d.ts +1 -1
  511. package/dist/state/paths.d.ts.map +1 -1
  512. package/dist/state/paths.js +1 -1
  513. package/dist/state/paths.js.map +1 -1
  514. package/dist/state/skill-active.d.ts +13 -1
  515. package/dist/state/skill-active.d.ts.map +1 -1
  516. package/dist/state/skill-active.js +43 -4
  517. package/dist/state/skill-active.js.map +1 -1
  518. package/dist/state/terminal-normalization.js.map +1 -1
  519. package/dist/state/workflow-transition-reconcile.d.ts.map +1 -1
  520. package/dist/state/workflow-transition-reconcile.js +20 -4
  521. package/dist/state/workflow-transition-reconcile.js.map +1 -1
  522. package/dist/state/workflow-transition.d.ts +1 -1
  523. package/dist/state/workflow-transition.d.ts.map +1 -1
  524. package/dist/state/workflow-transition.js.map +1 -1
  525. package/dist/subagents/__tests__/adapted-role-binding.test.d.ts +2 -0
  526. package/dist/subagents/__tests__/adapted-role-binding.test.d.ts.map +1 -0
  527. package/dist/subagents/__tests__/adapted-role-binding.test.js +988 -0
  528. package/dist/subagents/__tests__/adapted-role-binding.test.js.map +1 -0
  529. package/dist/subagents/__tests__/leader-bootstrap-3181.test.d.ts +2 -0
  530. package/dist/subagents/__tests__/leader-bootstrap-3181.test.d.ts.map +1 -0
  531. package/dist/subagents/__tests__/leader-bootstrap-3181.test.js +273 -0
  532. package/dist/subagents/__tests__/leader-bootstrap-3181.test.js.map +1 -0
  533. package/dist/subagents/__tests__/tracker.test.js +1550 -10
  534. package/dist/subagents/__tests__/tracker.test.js.map +1 -1
  535. package/dist/subagents/adapted-role-binding.d.ts +17 -0
  536. package/dist/subagents/adapted-role-binding.d.ts.map +1 -0
  537. package/dist/subagents/adapted-role-binding.js +75 -0
  538. package/dist/subagents/adapted-role-binding.js.map +1 -0
  539. package/dist/subagents/role-routing-marker.d.ts +10 -0
  540. package/dist/subagents/role-routing-marker.d.ts.map +1 -0
  541. package/dist/subagents/role-routing-marker.js +136 -0
  542. package/dist/subagents/role-routing-marker.js.map +1 -0
  543. package/dist/subagents/tracker.d.ts +172 -0
  544. package/dist/subagents/tracker.d.ts.map +1 -1
  545. package/dist/subagents/tracker.js +1077 -61
  546. package/dist/subagents/tracker.js.map +1 -1
  547. package/dist/team/__tests__/approved-execution.test.js.map +1 -1
  548. package/dist/team/__tests__/delivery-e2e-smoke.test.js.map +1 -1
  549. package/dist/team/__tests__/model-contract.test.js +231 -1
  550. package/dist/team/__tests__/model-contract.test.js.map +1 -1
  551. package/dist/team/__tests__/role-router.test.js.map +1 -1
  552. package/dist/team/__tests__/runtime.test.js +401 -1
  553. package/dist/team/__tests__/runtime.test.js.map +1 -1
  554. package/dist/team/__tests__/scaling.test.js +181 -0
  555. package/dist/team/__tests__/scaling.test.js.map +1 -1
  556. package/dist/team/__tests__/state-root.test.js.map +1 -1
  557. package/dist/team/__tests__/tmux-session.test.js +434 -4
  558. package/dist/team/__tests__/tmux-session.test.js.map +1 -1
  559. package/dist/team/__tests__/tmux-test-fixture.js.map +1 -1
  560. package/dist/team/api-interop.d.ts +2 -2
  561. package/dist/team/api-interop.d.ts.map +1 -1
  562. package/dist/team/api-interop.js.map +1 -1
  563. package/dist/team/commit-hygiene.js.map +1 -1
  564. package/dist/team/contracts.d.ts +5 -5
  565. package/dist/team/contracts.d.ts.map +1 -1
  566. package/dist/team/delivery-log.js.map +1 -1
  567. package/dist/team/followup-planner.js.map +1 -1
  568. package/dist/team/goal-workflow.js.map +1 -1
  569. package/dist/team/idle-nudge.d.ts.map +1 -1
  570. package/dist/team/model-contract.d.ts +20 -1
  571. package/dist/team/model-contract.d.ts.map +1 -1
  572. package/dist/team/model-contract.js +267 -30
  573. package/dist/team/model-contract.js.map +1 -1
  574. package/dist/team/orchestrator.js.map +1 -1
  575. package/dist/team/pane-status.js.map +1 -1
  576. package/dist/team/phase-controller.js.map +1 -1
  577. package/dist/team/reminder-intents.d.ts +1 -1
  578. package/dist/team/reminder-intents.d.ts.map +1 -1
  579. package/dist/team/reminder-intents.js.map +1 -1
  580. package/dist/team/runtime-cli.js.map +1 -1
  581. package/dist/team/runtime.d.ts.map +1 -1
  582. package/dist/team/runtime.js +83 -52
  583. package/dist/team/runtime.js.map +1 -1
  584. package/dist/team/scaling.d.ts +9 -7
  585. package/dist/team/scaling.d.ts.map +1 -1
  586. package/dist/team/scaling.js +69 -28
  587. package/dist/team/scaling.js.map +1 -1
  588. package/dist/team/state/dispatch-lock.js.map +1 -1
  589. package/dist/team/state/dispatch.js.map +1 -1
  590. package/dist/team/state/events.js.map +1 -1
  591. package/dist/team/state/tasks.js.map +1 -1
  592. package/dist/team/state-root.js.map +1 -1
  593. package/dist/team/state.js.map +1 -1
  594. package/dist/team/team-identity.d.ts.map +1 -1
  595. package/dist/team/team-identity.js.map +1 -1
  596. package/dist/team/tmux-session.d.ts +2 -0
  597. package/dist/team/tmux-session.d.ts.map +1 -1
  598. package/dist/team/tmux-session.js +90 -42
  599. package/dist/team/tmux-session.js.map +1 -1
  600. package/dist/team/worker-bootstrap.js.map +1 -1
  601. package/dist/team/worktree.js.map +1 -1
  602. package/dist/ultragoal/__tests__/artifacts.test.js.map +1 -1
  603. package/dist/ultragoal/__tests__/docs-contract.test.js.map +1 -1
  604. package/dist/ultragoal/artifacts.d.ts.map +1 -1
  605. package/dist/ultragoal/artifacts.js +4 -1
  606. package/dist/ultragoal/artifacts.js.map +1 -1
  607. package/dist/url-reader/index.js.map +1 -1
  608. package/dist/utils/agents-model-table.js.map +1 -1
  609. package/dist/utils/package.js.map +1 -1
  610. package/dist/utils/paths.js.map +1 -1
  611. package/dist/utils/platform-command.js.map +1 -1
  612. package/dist/utils/repo-deps.js.map +1 -1
  613. package/dist/utils/version.js.map +1 -1
  614. package/dist/verification/__tests__/ci-rust-gates.test.js +1 -1
  615. package/dist/verification/__tests__/dev-merge-issue-close-workflow.test.js.map +1 -1
  616. package/dist/verification/verifier.js.map +1 -1
  617. package/dist/visual/constants.d.ts +1 -1
  618. package/dist/visual/constants.d.ts.map +1 -1
  619. package/dist/vscode/__tests__/index.test.js.map +1 -1
  620. package/dist/vscode/index.js.map +1 -1
  621. package/dist/wiki/__tests__/cjk-tokenize.test.js.map +1 -1
  622. package/dist/wiki/__tests__/lint.test.js.map +1 -1
  623. package/dist/wiki/__tests__/query.test.js.map +1 -1
  624. package/dist/wiki/__tests__/storage.test.js.map +1 -1
  625. package/dist/wiki/__tests__/test-helpers.d.ts.map +1 -1
  626. package/dist/wiki/lint.js.map +1 -1
  627. package/dist/wiki/query.js.map +1 -1
  628. package/dist/wiki/storage.js.map +1 -1
  629. package/package.json +2 -2
  630. package/plugins/oh-my-codex/.codex-plugin/plugin.json +1 -1
  631. package/plugins/oh-my-codex/skills/omx-setup/SKILL.md +10 -5
  632. package/plugins/oh-my-codex/skills/ralph/SKILL.md +4 -2
  633. package/plugins/oh-my-codex/skills/ralplan/SKILL.md +5 -2
  634. package/skills/omx-setup/SKILL.md +10 -5
  635. package/skills/ralph/SKILL.md +4 -2
  636. package/skills/ralplan/SKILL.md +5 -2
  637. package/src/scripts/__tests__/codex-native-hook.test.ts +17522 -10630
  638. package/src/scripts/__tests__/fixtures/issue-3138-installed-smoke.ts +734 -0
  639. package/src/scripts/__tests__/notify-fallback-delivery.test.ts +177 -0
  640. package/src/scripts/__tests__/notify-state-io.test.ts +73 -0
  641. package/src/scripts/__tests__/role-intent-bootstrap-e2e-3181.test.ts +215 -0
  642. package/src/scripts/__tests__/role-intent-durable-recovery-3181.test.ts +193 -0
  643. package/src/scripts/__tests__/smoke-packed-install.test.ts +991 -4
  644. package/src/scripts/codex-native-hook.ts +779 -159
  645. package/src/scripts/notify-fallback-delivery.ts +416 -0
  646. package/src/scripts/notify-fallback-watcher.ts +132 -40
  647. package/src/scripts/notify-hook/auto-nudge.ts +43 -12
  648. package/src/scripts/notify-hook/managed-tmux.ts +149 -12
  649. package/src/scripts/notify-hook/ralph-session-resume.ts +37 -36
  650. package/src/scripts/notify-hook/state-io.ts +100 -13
  651. package/src/scripts/notify-hook/tmux-injection.ts +38 -21
  652. package/src/scripts/notify-hook/visual-verdict.ts +22 -12
  653. package/src/scripts/notify-hook.ts +335 -90
  654. package/src/scripts/smoke-packed-install.ts +1829 -6
@@ -1,17 +1,33 @@
1
1
  import {
2
+ existsSync,
3
+ lstatSync,
4
+ mkdirSync,
2
5
  mkdtempSync,
6
+ readFileSync,
7
+ readdirSync,
3
8
  realpathSync,
4
9
  rmSync,
10
+ statSync,
11
+ writeFileSync,
5
12
  } from 'node:fs';
6
- import { mkdirSync } from 'node:fs';
7
13
  import { tmpdir } from 'node:os';
8
- import { join } from 'node:path';
9
- import { spawnSync } from 'node:child_process';
14
+ import { delimiter, join, resolve } from 'node:path';
15
+ import { spawn, spawnSync, type ChildProcessWithoutNullStreams } from 'node:child_process';
10
16
  import { pathToFileURL } from 'node:url';
17
+ import { TextDecoder } from 'node:util';
18
+
19
+ import TOML from '@iarna/toml';
20
+ import { isManagedCodexHookCommand, planManagedCodexHooksRemoval } from '../config/codex-hooks.js';
21
+ import {
22
+ spawnPlatformCommand,
23
+ spawnPlatformCommandSync,
24
+ } from '../utils/platform-command.js';
25
+
11
26
  import {
12
27
  ensureReusableNodeModules,
13
28
  } from '../utils/repo-deps.js';
14
29
 
30
+
15
31
  export {
16
32
  hasUsableNodeModules,
17
33
  resolveGitCommonDir,
@@ -25,7 +41,7 @@ export const PACKED_INSTALL_SMOKE_CORE_COMMANDS = [
25
41
  ['sparkshell', '--help'],
26
42
  ] as const;
27
43
 
28
- export const PACKED_INSTALL_NATIVE_HOOK_SMOKE_EVENTS = [
44
+ export const MANAGED_CODEX_HOOK_EVENTS = [
29
45
  'SessionStart',
30
46
  'PreToolUse',
31
47
  'PostToolUse',
@@ -35,12 +51,1162 @@ export const PACKED_INSTALL_NATIVE_HOOK_SMOKE_EVENTS = [
35
51
  'Stop',
36
52
  ] as const;
37
53
 
54
+ export const PACKED_INSTALL_NATIVE_HOOK_REGRESSION_PROMPTS = [
55
+ { name: 'directive-use-ralplan', prompt: 'use $ralplan plan this', expectedSkill: 'ralplan', expectedStopBlock: true },
56
+ { name: 'directive-please-use-ralplan', prompt: 'please use $ralplan plan it', expectedSkill: 'ralplan', expectedStopBlock: true },
57
+ { name: 'directive-run-ralplan', prompt: 'run $ralplan plan this', expectedSkill: 'ralplan', expectedStopBlock: true },
58
+ { name: 'directive-list-use-ralplan', prompt: '- use $ralplan plan this', expectedSkill: 'ralplan', expectedStopBlock: true },
59
+ { name: 'directive-documentation-then-command', prompt: 'use $ralplan is the consensus-planning command\n$autopilot build it', expectedSkill: 'autopilot', expectedStopBlock: true },
60
+ { name: 'directive-documentation-then-implicit-command', prompt: 'use $ralplan is the consensus-planning command\nUse autopilot mode.', expectedSkill: 'autopilot', expectedStopBlock: true },
61
+ { name: 'directive-documentation-trailing-prose-then-command', prompt: 'use $ralplan is the workflow command for planning\n$autopilot build it', expectedSkill: 'autopilot', expectedStopBlock: true },
62
+ { name: 'directive-documentation-implicit-prose', prompt: 'use $ralplan is the workflow command for autopilot mode', expectedSkill: null, expectedStopBlock: false },
63
+ { name: 'directive-documentation-alias-prose', prompt: 'use $ralplan is the consensus-planning command\nAutopilot mode is its alias.', expectedSkill: null, expectedStopBlock: false },
64
+ { name: 'directive-coordinated-documentation-then-command', prompt: '- use $ralplan and $autopilot are workflow commands\n$ralph execute it', expectedSkill: 'ralph', expectedStopBlock: true },
65
+ { name: 'directive-documentation-semicolon-directive', prompt: 'use $ralplan is the consensus-planning command; use $autopilot build it', expectedSkill: 'autopilot', expectedStopBlock: true },
66
+ { name: 'directive-two-documentation-blocks', prompt: 'use $ralplan is the consensus-planning command\nuse $autopilot is the autonomous workflow command\n$ralph execute it', expectedSkill: 'ralph', expectedStopBlock: true },
67
+ { name: 'directive-documentation-embedded-token-then-command', prompt: 'use $ralplan is the workflow command for $team\n$autopilot build it', expectedSkill: 'autopilot', expectedStopBlock: true },
68
+ { name: 'doc-task-noun', prompt: 'use $ralplan is the workflow command; use $autopilot update the documentation', expectedSkill: 'autopilot', expectedStopBlock: true },
69
+ { name: 'doc-implicit-followup', prompt: 'use $ralplan is the consensus-planning command; use autopilot mode.', expectedSkill: 'autopilot', expectedStopBlock: true },
70
+ { name: 'doc-transition-followup', prompt: 'use $ralplan is the consensus-planning command; then use $autopilot build it', expectedSkill: 'autopilot', expectedStopBlock: true },
71
+ { name: 'doc-explicit-alias', prompt: 'use $ralplan is the consensus-planning command; $team is its alias', expectedSkill: null, expectedStopBlock: false },
72
+ { name: 'doc-implicit-chain', prompt: 'use $ralplan is the consensus-planning command\nAutopilot mode is its alias.\n$ralph execute it', expectedSkill: 'ralph', expectedStopBlock: true },
73
+ { name: 'doc-fullwidth-separator', prompt: 'use $ralplan,$autopilot are workflow commands', expectedSkill: null, expectedStopBlock: false },
74
+ { name: 'doc-compact-slash', prompt: 'use $ralplan/$autopilot are workflow commands\n$ralph execute it', expectedSkill: 'ralph', expectedStopBlock: true },
75
+ { name: 'reference-prompts-followup', prompt: '[docs]: /target "title\nUse /prompts:architect"\n$ralph execute it', expectedSkill: 'ralph', expectedStopBlock: true },
76
+ { name: 'doc-period-implicit', prompt: 'use $ralplan is the consensus-planning command. Use autopilot mode.', expectedSkill: 'autopilot', expectedStopBlock: true },
77
+ { name: 'doc-bare-implicit', prompt: 'use $ralplan is the consensus-planning command; autopilot mode.', expectedSkill: 'autopilot', expectedStopBlock: true },
78
+ { name: 'doc-fullwidth-semicolon', prompt: 'use $ralplan is the consensus-planning command; use $autopilot build it', expectedSkill: 'autopilot', expectedStopBlock: true },
79
+ { name: 'doc-but-followup', prompt: 'use $ralplan is the workflow command; but use $autopilot build it', expectedSkill: 'autopilot', expectedStopBlock: true },
80
+ { name: 'doc-fullwidth-oxford', prompt: 'use $ralplan, $autopilot, and $team are workflow commands', expectedSkill: null, expectedStopBlock: false },
81
+ { name: 'reference-zero-title', prompt: '[docs]: ./target\n(autopilot mode)', expectedSkill: null, expectedStopBlock: false },
82
+ { name: 'doc-also-alias-explicit', prompt: 'use $ralplan is the consensus-planning command; $team is also its alias', expectedSkill: null, expectedStopBlock: false },
83
+ { name: 'doc-also-alias-implicit', prompt: 'use $ralplan is the consensus-planning command\nAutopilot mode is also its alias.', expectedSkill: null, expectedStopBlock: false },
84
+ { name: 'doc-embedded-mention', prompt: 'use $ralplan is the workflow command; $team appears in the documentation.', expectedSkill: null, expectedStopBlock: false },
85
+ { name: 'chained-negation', prompt: '$ralplan; $autopilot is prohibited', expectedSkill: 'ralplan', expectedStopBlock: true, expectedDeferredSkills: [] },
86
+ { name: 'long-negation', prompt: `$ralplan; $autopilot${' '.repeat(193)}is prohibited`, expectedSkill: 'ralplan', expectedStopBlock: true, expectedDeferredSkills: [] },
87
+ { name: 'doc-arabic-comma', prompt: 'use $ralplan، $autopilot are workflow commands', expectedSkill: null, expectedStopBlock: false },
88
+ { name: 'arabic-negation', prompt: '$ralplan، $autopilot are prohibited', expectedSkill: null, expectedStopBlock: false },
89
+ { name: 'implicit-arabic-negation', prompt: 'Autopilot mode، deep interview are prohibited.', expectedSkill: null, expectedStopBlock: false },
90
+ { name: 'fullwidth-frame-reset', prompt: 'For instance: manual mode is slower。 Use autopilot mode.', expectedSkill: 'autopilot', expectedStopBlock: true },
91
+ { name: 'doc-abbreviation', prompt: 'use $ralplan is the workflow command, e.g. use $autopilot in examples.', expectedSkill: null, expectedStopBlock: false },
92
+ { name: 'implicit-doc-mention', prompt: 'use $ralplan is the workflow command; autopilot mode appears in the documentation.', expectedSkill: null, expectedStopBlock: false },
93
+ { name: 'implicit-doc-chain', prompt: 'use $ralplan is the workflow command; autopilot mode is its alias; $ralph execute it', expectedSkill: 'ralph', expectedStopBlock: true },
94
+ { name: 'long-command-gap', prompt: `use $ralplan is the workflow command; use${' '.repeat(161)}$autopilot build it`, expectedSkill: 'autopilot', expectedStopBlock: true },
95
+ { name: 'ideographic-negation', prompt: '$ralplan、 $autopilot are prohibited', expectedSkill: null, expectedStopBlock: false },
96
+ { name: 'implicit-ideo-negation', prompt: 'Autopilot mode、 deep interview are prohibited.', expectedSkill: null, expectedStopBlock: false },
97
+ { name: 'doc-exclamation', prompt: 'use $ralplan is the consensus-planning command! run $autopilot', expectedSkill: 'autopilot', expectedStopBlock: true },
98
+ { name: 'doc-fullwidth-question', prompt: 'use $ralplan is the consensus-planning command? run $autopilot', expectedSkill: 'autopilot', expectedStopBlock: true },
99
+ { name: 'implicit-doc-predecessor', prompt: 'Autopilot mode is workflow documentation.\n$ralph execute it', expectedSkill: 'ralph', expectedStopBlock: true },
100
+ { name: 'confusable-use-verb', prompt: 'uſe $ralplan plan it', expectedSkill: null, expectedStopBlock: false },
101
+ { name: 'confusable-please', prompt: 'pleaſe use $ralplan plan it', expectedSkill: null, expectedStopBlock: false },
102
+ { name: 'confusable-prompts-token-then-command', prompt: '/promptſ:architect; use autopilot mode.', expectedSkill: 'autopilot', expectedStopBlock: true },
103
+ { name: 'reserved-em-dash-boundary', prompt: '/prompts:architect— use autopilot mode', expectedSkill: null, expectedStopBlock: false },
104
+ { name: 'reserved-fullwidth-comma-boundary', prompt: '/prompts:architect, use autopilot mode', expectedSkill: null, expectedStopBlock: false },
105
+ { name: 'confusable-implicit-verb', prompt: 'Do not use deep interview but uſe autopilot mode.', expectedSkill: null, expectedStopBlock: false },
106
+ { name: 'frame-fullwidth-colon', prompt: 'For instance: use autopilot mode.', expectedSkill: null, expectedStopBlock: false },
107
+ { name: 'frame-fullwidth-comma', prompt: 'For instance, use autopilot mode.', expectedSkill: null, expectedStopBlock: false },
108
+ { name: 'frame-arabic-comma', prompt: 'For instance، use autopilot mode.', expectedSkill: null, expectedStopBlock: false },
109
+ { name: 'frame-ideo-comma', prompt: 'For instance、 use autopilot mode.', expectedSkill: null, expectedStopBlock: false },
110
+ { name: 'doc-explicit-documented', prompt: 'use $ralplan is the workflow command; $autopilot is documented in the guide.', expectedSkill: null, expectedStopBlock: false },
111
+ { name: 'doc-explicit-described', prompt: '$autopilot is described in the manual.', expectedSkill: null, expectedStopBlock: false },
112
+ { name: 'doc-comma-followup', prompt: 'use $ralplan is the workflow command, but use $autopilot build it', expectedSkill: 'autopilot', expectedStopBlock: true },
113
+ { name: 'doc-fw-comma-followup', prompt: 'use $ralplan is the workflow command, but use $autopilot build it', expectedSkill: 'autopilot', expectedStopBlock: true },
114
+ { name: 'doc-arabic-followup', prompt: 'use $ralplan is the workflow command، but use $autopilot build it', expectedSkill: 'autopilot', expectedStopBlock: true },
115
+ { name: 'doc-ideo-followup', prompt: 'use $ralplan is the workflow command、 but use $autopilot build it', expectedSkill: 'autopilot', expectedStopBlock: true },
116
+ { name: 'neg-arabic-followup', prompt: 'Do not run $ralplan، use $autopilot build it', expectedSkill: 'autopilot', expectedStopBlock: true },
117
+ { name: 'neg-ideo-followup', prompt: 'Do not run $ralplan、 use $autopilot build it', expectedSkill: 'autopilot', expectedStopBlock: true },
118
+ { name: 'implicit-doc-prefix-next', prompt: 'The docs mention autopilot mode.\n$ralplan plan it', expectedSkill: 'ralplan', expectedStopBlock: true },
119
+ { name: 'implicit-doc-prefix-line', prompt: 'The docs mention autopilot mode; use $ralplan plan it', expectedSkill: 'ralplan', expectedStopBlock: true },
120
+ { name: 'implicit-doc-subject-line', prompt: 'Autopilot mode is workflow documentation; use $ralplan plan it', expectedSkill: 'ralplan', expectedStopBlock: true },
121
+ { name: 'compact-explicit-negation', prompt: '$ralplan,$autopilot are prohibited', expectedSkill: null, expectedStopBlock: false },
122
+ { name: 'compact-implicit-negation', prompt: 'Autopilot mode،deep interview are prohibited.', expectedSkill: null, expectedStopBlock: false },
123
+ { name: 'doc-clause-local-prefix', prompt: '$ralplan; $autopilot is documented in the guide.', expectedSkill: 'ralplan', expectedStopBlock: true },
124
+ { name: 'doc-chain-described', prompt: 'use $ralplan is the workflow command; autopilot mode is documented in the guide; $team execute it', expectedSkill: 'team', expectedStopBlock: false, insideTmux: true },
125
+ { name: 'doc-chain-workflow', prompt: 'use $ralplan is the workflow command; autopilot mode is workflow documentation; use $ralph execute it', expectedSkill: 'ralph', expectedStopBlock: true },
126
+ { name: 'ref-inline-explicit', prompt: '[docs]: $ralplan\n$autopilot build it', expectedSkill: 'autopilot', expectedStopBlock: true },
127
+ { name: 'ref-inline-prompts', prompt: '[docs]: /prompts:architect\n$autopilot build it', expectedSkill: 'autopilot', expectedStopBlock: true },
128
+ { name: 'list-fullwidth-explicit-doc', prompt: '- $ralplan: consensus-planning workflow', expectedSkill: null, expectedStopBlock: false },
129
+ { name: 'list-fullwidth-implicit-doc', prompt: '- autopilot mode: autonomous workflow command', expectedSkill: null, expectedStopBlock: false },
130
+ { name: 'possessive-straight', prompt: "$ralplan's workflow is documented", expectedSkill: null, expectedStopBlock: false },
131
+ { name: 'possessive-curly', prompt: '$ralplan’s workflow is documented', expectedSkill: null, expectedStopBlock: false },
132
+ { name: 'possessive-fullwidth', prompt: '$ralplan's workflow is documented', expectedSkill: null, expectedStopBlock: false },
133
+ { name: 'possessive-prompts', prompt: "/prompts:architect's syntax is documented", expectedSkill: null, expectedStopBlock: false },
134
+ { name: 'malformed-prefix-kata', prompt: '$・autopilot mode', expectedSkill: null, expectedStopBlock: false },
135
+ { name: 'malformed-prefix-half', prompt: '$・autopilot mode', expectedSkill: null, expectedStopBlock: false },
136
+ { name: 'malformed-prefix-arabic', prompt: '$٪autopilot mode', expectedSkill: null, expectedStopBlock: false },
137
+ { name: 'malformed-prefix-division', prompt: '$∕autopilot mode', expectedSkill: null, expectedStopBlock: false },
138
+ { name: 'doc-but-directive', prompt: 'use $autopilot is documented but use $ralplan plan it', expectedSkill: 'ralplan', expectedStopBlock: true },
139
+ { name: 'list-directive-fw-colon', prompt: '- use $ralplan: consensus-planning workflow', expectedSkill: null, expectedStopBlock: false },
140
+ { name: 'doc-arabic-question', prompt: 'use $ralplan is the workflow command؟ run $autopilot', expectedSkill: 'autopilot', expectedStopBlock: true },
141
+ { name: 'arabic-semicolon-negation', prompt: '$ralplan؛ $autopilot is prohibited', expectedSkill: 'ralplan', expectedStopBlock: true, expectedDeferredSkills: [] },
142
+ { name: 'confusable-postposed-transition', prompt: '$ralplan is prohibited but uſe autopilot mode.', expectedSkill: null, expectedStopBlock: false },
143
+ { name: 'mixed-negation', prompt: 'Autopilot mode and $ralplan are prohibited.', expectedSkill: null, expectedStopBlock: false },
144
+ { name: 'both-mixed-negation', prompt: 'Both autopilot mode and $ralplan are prohibited.', expectedSkill: null, expectedStopBlock: false },
145
+ { name: 'mixed-documentation', prompt: 'use $ralplan and autopilot mode are workflow commands', expectedSkill: null, expectedStopBlock: false },
146
+ { name: 'prose-doc-no-reopen', prompt: '$ralplan is prohibited because docs use $autopilot.', expectedSkill: null, expectedStopBlock: false },
147
+ { name: 'neg-fw-dot-reopen', prompt: 'Do not run $ralplan. use $autopilot build it', expectedSkill: 'autopilot', expectedStopBlock: true },
148
+ { name: 'neg-greek-q-reopen', prompt: 'Do not run $ralplan; use $autopilot build it', expectedSkill: 'autopilot', expectedStopBlock: true },
149
+ { name: 'unicode-attached-contrast', prompt: 'Do not use deep interview яbut use autopilot mode.', expectedSkill: null, expectedStopBlock: false },
150
+ { name: 'prefix-list-followup', prompt: 'Do not run $ralplan, $autopilot; use $team execute it', expectedSkill: 'team', expectedStopBlock: false, insideTmux: true },
151
+ { name: 'mixed-postposed-chain', prompt: '$ralplan, autopilot mode, $team are prohibited.', expectedSkill: null, expectedStopBlock: false },
152
+ { name: 'implicit-first-doc-chain', prompt: 'Autopilot mode and $ralplan are workflow commands; use $team execute it', expectedSkill: 'team', expectedStopBlock: false, insideTmux: true },
153
+ { name: 'both-mixed-doc-followup', prompt: 'Both autopilot mode and $ralplan are workflow commands; use $team execute it', expectedSkill: 'team', expectedStopBlock: false, insideTmux: true },
154
+ { name: 'doc-semicolon-preserves-earlier', prompt: 'Use autopilot mode; use $ralplan is the workflow command.', expectedSkill: 'autopilot', expectedStopBlock: true },
155
+ { name: 'doc-independent-comma', prompt: 'Use autopilot mode, and $ralplan is documented in the guide.', expectedSkill: 'autopilot', expectedStopBlock: true },
156
+ { name: 'reference-unclosed-quote-destination', prompt: '[docs]: "target\n$autopilot build it', expectedSkill: null, expectedStopBlock: false },
157
+ { name: 'reference-unclosed-inline-destination', prompt: '[docs]: `target\n$autopilot build it', expectedSkill: null, expectedStopBlock: false },
158
+ { name: 'mixed-prefix-negation-implicit', prompt: 'Do not run $ralplan and use autopilot mode.', expectedSkill: null, expectedStopBlock: false },
159
+ { name: 'repeated-postposed-followup', prompt: '$team is prohibited and is forbidden; use $ralplan plan it', expectedSkill: 'ralplan', expectedStopBlock: true },
160
+ { name: 'doc-preserves-earlier', prompt: 'Use autopilot mode; "note"; use $ralplan is the workflow command.', expectedSkill: 'autopilot', expectedStopBlock: true },
161
+ { name: 'doc-colon-followup', prompt: 'use $ralplan is the workflow command: use $autopilot build it', expectedSkill: 'autopilot', expectedStopBlock: true },
162
+ { name: 'table-followup', prompt: 'Mode | Meaning\n--- | ---\nmanual | documentation\n$ralplan plan it', expectedSkill: 'ralplan', expectedStopBlock: true },
163
+ { name: 'neg-advance-reopen', prompt: 'Do not run $ralplan but advance to $ultragoal', expectedSkill: 'ultragoal', expectedStopBlock: false },
164
+ { name: 'neg-jump-reopen', prompt: 'Do not run $ralplan but jump straight to $ultragoal', expectedSkill: 'ultragoal', expectedStopBlock: false },
165
+ { name: 'reference-plain-title', prompt: '[docs]: /target "title\nplain text"\n$ralplan plan it', expectedSkill: 'ralplan', expectedStopBlock: true },
166
+ { name: 'reference-plain-destination', prompt: '[docs]: ./target\n$ralplan plan it', expectedSkill: 'ralplan', expectedStopBlock: true },
167
+ { name: 'directive-use-the', prompt: 'Do not run $ralplan; use the $autopilot build it', expectedSkill: 'autopilot', expectedStopBlock: true },
168
+ { name: 'directive-continue-after-quote', prompt: '"quoted"\ncontinue with $ralplan', expectedSkill: 'ralplan', expectedStopBlock: true },
169
+ { name: 'doc-advance-followup', prompt: 'use $ralplan is the workflow command; advance to $ultragoal', expectedSkill: 'ultragoal', expectedStopBlock: false },
170
+ { name: 'directive-run-code-review', prompt: 'run $code-review', expectedSkill: 'code-review', expectedStopBlock: false },
171
+ { name: 'directive-documentation', prompt: 'use $ralplan is the consensus-planning command', expectedSkill: null, expectedStopBlock: false },
172
+ { name: 'nested-bounded-child-unbounded-parent', prompt: '"`x`\n$ralplan plan it', expectedSkill: null, expectedStopBlock: false },
173
+ { name: 'first-contiguous-block-terminal', prompt: '$ralplan plan it\n"x"\n$autopilot build it', expectedSkill: 'ralplan', expectedStopBlock: true, expectedDeferredSkills: [] },
174
+ { name: 'leading-reserved-dominance', prompt: '/prompts:architect\n"x"\n$ralplan plan it', expectedSkill: null, expectedStopBlock: false },
175
+ { name: 'list-fence-root-opener', prompt: '- ```\n sample\n```\n$ralplan plan it', expectedSkill: null, expectedStopBlock: false },
176
+ { name: 'list-fence-relative-closer', prompt: '- ```\n sample\n ```\n$ralplan plan it', expectedSkill: 'ralplan', expectedStopBlock: true },
177
+ { name: 'reference-multiline-title-explicit', prompt: '[docs]: /target "title\nuse /prompts:architect\n$ralplan plan it"', expectedSkill: null, expectedStopBlock: false },
178
+ { name: 'reference-multiline-title-implicit', prompt: '[docs]: /target "title\nuse autopilot mode"', expectedSkill: null, expectedStopBlock: false },
179
+ { name: 'reference-next-line-title', prompt: '[docs]: ./target\n (autopilot mode)', expectedSkill: null, expectedStopBlock: false },
180
+ { name: 'reference-next-line-destination-title', prompt: '[docs]:\n ./target\n (autopilot mode)', expectedSkill: null, expectedStopBlock: false },
181
+ { name: 'kelvin-case-fold-suffix', prompt: '$ultraworK execute', expectedSkill: null, expectedStopBlock: false },
182
+ { name: 'katakana-middle-dot-suffix', prompt: '$ralplan・suffix plan it', expectedSkill: null, expectedStopBlock: false },
183
+ { name: 'halfwidth-middle-dot-suffix', prompt: '$ralplan・suffix plan it', expectedSkill: null, expectedStopBlock: false },
184
+ { name: 'arabic-percent-suffix', prompt: '$ralplan٪docs', expectedSkill: null, expectedStopBlock: false },
185
+ { name: 'division-slash-suffix', prompt: '$ralplan∕config', expectedSkill: null, expectedStopBlock: false },
186
+ { name: 'unclosed-prompts-quote', prompt: '"Use /prompts:architect\n$ralplan plan it', expectedSkill: null, expectedStopBlock: false },
187
+ { name: 'malformed-prompts-suffix', prompt: '/prompts:architect한글\n$ralplan plan it', expectedSkill: null, expectedStopBlock: false },
188
+ { name: 'stale-predecessor-prose-directive', prompt: '> quoted context\nProse\n$ralplan implement this', expectedSkill: null, expectedStopBlock: false },
189
+ { name: 'stale-predecessor-reserved-directive', prompt: '> quoted context\n/prompts:architect\n$ralplan plan this', expectedSkill: null, expectedStopBlock: false },
190
+ { name: 'stale-predecessor-first-block-terminal', prompt: '> quoted context\n$ralplan plan it\nLater discussion.\n$autopilot build it', expectedSkill: 'ralplan', expectedStopBlock: true, expectedDeferredSkills: [] },
191
+ { name: 'stale-predecessor-unclosed-after-negation', prompt: 'Do not run $ralplan.\n"unclosed context\n$autopilot build it', expectedSkill: null, expectedStopBlock: false },
192
+ { name: 'stale-predecessor-reference-unclosed', prompt: '[$ralplan]: ./docs\n"unclosed context\n$autopilot build it', expectedSkill: null, expectedStopBlock: false },
193
+ { name: 'stale-directive-clause-prefix', prompt: '> quoted context\nProse\nUse $ralplan plan this', expectedSkill: null, expectedStopBlock: false },
194
+ { name: 'nested-reserved-predecessor-successor', prompt: '- Use /prompts:architect.\n$ralplan plan it', expectedSkill: 'ralplan', expectedStopBlock: true },
195
+ { name: 'nested-fence-predecessor-successor', prompt: '- - ```\n quoted context\n ```\n$ralplan plan it', expectedSkill: 'ralplan', expectedStopBlock: true },
196
+ { name: 'reference-multiline-destination-implicit', prompt: '[docs]:\nautopilot', expectedSkill: null, expectedStopBlock: false },
197
+ { name: 'middle-dot-suffix', prompt: '$ralplan·suffix plan it', expectedSkill: null, expectedStopBlock: false },
198
+ { name: 'percent-suffix', prompt: '$ralplan%docs', expectedSkill: null, expectedStopBlock: false },
199
+ { name: 'fullwidth-percent-suffix', prompt: '$ralplan%docs', expectedSkill: null, expectedStopBlock: false },
200
+ { name: 'g1a-ordered-multi-skill', prompt: '$ralplan, $autopilot; $team', expectedSkill: 'ralplan', expectedStopBlock: true, expectedDeferredSkills: ['autopilot', 'team'], expectedActiveSkills: ['ralplan'], insideTmux: true },
201
+ { name: 'g1c-duplicate-alias', prompt: '$autopilot $oh-my-codex:autopilot build it', expectedSkill: 'autopilot', expectedStopBlock: true, expectedDeferredSkills: [], expectedActiveSkills: ['autopilot'] },
202
+ { name: 'b3-longer-valid-fence', prompt: '```text\n$autopilot build it\n````\n$ralplan plan it', expectedSkill: 'ralplan', expectedStopBlock: true },
203
+ { name: 'b4-shorter-invalid-fence', prompt: '````text\n$autopilot build it\n```\n$ralplan plan it', expectedSkill: null, expectedStopBlock: false },
204
+ { name: 'b5-different-marker-invalid-fence', prompt: '```text\n$autopilot build it\n~~~\n$ralplan plan it', expectedSkill: null, expectedStopBlock: false },
205
+ ] as const;
206
+
207
+ export function buildPackedRegressionEnvironment(
208
+ testCase: { readonly name: string; readonly insideTmux?: boolean },
209
+ baseEnv: NodeJS.ProcessEnv = process.env,
210
+ ): NodeJS.ProcessEnv {
211
+ const insideTmux = testCase.insideTmux === true;
212
+ return {
213
+ ...baseEnv,
214
+ OMX_ROOT: '',
215
+ OMX_STATE_ROOT: '',
216
+ OMX_TEAM_STATE_ROOT: '',
217
+ OMX_SESSION_ID: '',
218
+ CODEX_SESSION_ID: '',
219
+ SESSION_ID: '',
220
+ OMX_TEAM_WORKER: '',
221
+ OMX_TEAM_INTERNAL_WORKER: '',
222
+ OMX_TEAM_LEADER_CWD: '',
223
+ OMX_TEAM_MODE: insideTmux ? 'enabled' : '',
224
+ OMX_QUESTION_RETURN_PANE: '',
225
+ OMX_LEADER_PANE_ID: '',
226
+ OMX_TMUX_HUD_OWNER: '',
227
+ TMUX: insideTmux ? '/tmp/tmux-pr3140-regression' : '',
228
+ TMUX_PANE: insideTmux ? '%3140' : '',
229
+ };
230
+ }
231
+
232
+ export const PACKED_INSTALL_NATIVE_HOOK_SMOKE_EVENTS = MANAGED_CODEX_HOOK_EVENTS;
233
+
234
+ export type ManagedCodexHookEvent = typeof MANAGED_CODEX_HOOK_EVENTS[number];
235
+
236
+ export const CODEX_APP_SERVER_TIMEOUTS = {
237
+ versionProbeMs: 2_000,
238
+ initializeMs: 15_000,
239
+ requestMs: 10_000,
240
+ shutdownMs: 5_000,
241
+ } as const;
242
+
243
+ const CODEX_TRUST_STATUSES = new Set([
244
+ 'managed',
245
+ 'untrusted',
246
+ 'trusted',
247
+ 'modified',
248
+ ]);
249
+
250
+ const CODEX_EVENT_LABELS: Readonly<Record<string, string>> = {
251
+ preToolUse: 'PreToolUse',
252
+ permissionRequest: 'PermissionRequest',
253
+ postToolUse: 'PostToolUse',
254
+ preCompact: 'PreCompact',
255
+ postCompact: 'PostCompact',
256
+ sessionStart: 'SessionStart',
257
+ userPromptSubmit: 'UserPromptSubmit',
258
+ subagentStart: 'SubagentStart',
259
+ subagentStop: 'SubagentStop',
260
+ stop: 'Stop',
261
+ };
262
+
263
+ const PINNED_CODEX_VERSION = '0.142.5';
264
+ const PINNED_CODEX_VERSION_OUTPUT = `codex-cli ${PINNED_CODEX_VERSION}`;
265
+ const CODEX_APP_SERVER_MAX_STDOUT_FRAME_LENGTH = 1_048_576;
266
+
267
+ type JsonRecord = Record<string, unknown>;
268
+
269
+ type JsonRpcEnvelope = {
270
+ id?: number;
271
+ method?: string;
272
+ params?: unknown;
273
+ result?: unknown;
274
+ error?: unknown;
275
+ };
276
+
277
+ type JsonRpcRequestEnvelope = JsonRpcEnvelope & {
278
+ id: number;
279
+ method: string;
280
+ };
281
+
282
+
283
+ type PendingRequest = {
284
+ method: string;
285
+ resolve: (value: unknown) => void;
286
+ reject: (reason: Error) => void;
287
+ timeout: NodeJS.Timeout;
288
+ };
289
+
290
+ export interface CodexHookMetadata {
291
+ event: ManagedCodexHookEvent | string;
292
+ command: string;
293
+ sourcePath: string;
294
+ key: string;
295
+ currentHash: string;
296
+ displayOrder: number;
297
+ trustStatus: 'managed' | 'untrusted' | 'trusted' | 'modified';
298
+ }
299
+
300
+ export interface CodexHooksListEntry {
301
+ cwd: string;
302
+ hooks: CodexHookMetadata[];
303
+ warnings: unknown[];
304
+ errors: unknown[];
305
+ }
306
+
307
+ export class CodexExecutableNotFoundError extends Error {
308
+ constructor() {
309
+ super('codex executable was not found');
310
+ this.name = 'CodexExecutableNotFoundError';
311
+ }
312
+ }
313
+
314
+ function isRecord(value: unknown): value is JsonRecord {
315
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
316
+ }
317
+
318
+ function isNodeErrorWithCode(value: unknown, code: string): boolean {
319
+ return value instanceof Error
320
+ && isRecord(value)
321
+ && value.code === code;
322
+ }
323
+
324
+ function compactDiagnostic(value: string, limit = 4_000): string {
325
+ return value.length <= limit ? value : value.slice(-limit);
326
+ }
327
+
328
+ function protocolError(message: string, stderr = ''): Error {
329
+ return new Error(`${message}${stderr.trim() ? `\nCodex stderr:\n${compactDiagnostic(stderr)}` : ''}`);
330
+ }
331
+
332
+ function requireString(value: unknown, label: string): string {
333
+ if (typeof value !== 'string' || value.length === 0) {
334
+ throw new Error(`Codex hooks/list response is missing ${label}`);
335
+ }
336
+ return value;
337
+ }
338
+
339
+ function requireRecord(value: unknown, label: string): JsonRecord {
340
+ if (!isRecord(value)) throw new Error(`Codex response has malformed ${label}`);
341
+ return value;
342
+ }
343
+
344
+ function hasExactPinnedCodexVersionStdout(stdout: string): boolean {
345
+ return stdout === PINNED_CODEX_VERSION_OUTPUT
346
+ || stdout === `${PINNED_CODEX_VERSION_OUTPUT}\n`
347
+ || stdout === `${PINNED_CODEX_VERSION_OUTPUT}\r\n`;
348
+ }
349
+
350
+ function hasOnlyBenignCodexVersionStderr(stderr: string): boolean {
351
+ const diagnostics = stderr.split(/\r?\n/).map((line) => line.trim()).filter((line) => line.length > 0);
352
+ return diagnostics.every((line) => /^(?:warning|note): /i.test(line));
353
+ }
354
+
355
+ function formatVersionProbeOutput(stdout: string, stderr: string): string {
356
+ return [
357
+ `stdout=${JSON.stringify(compactDiagnostic(stdout))}`,
358
+ ...(stderr.length === 0 ? [] : [`stderr=${JSON.stringify(compactDiagnostic(stderr))}`]),
359
+ ].join(' ');
360
+ }
361
+
362
+ const CODEX_VERSION_PROBE_CANDIDATE_BUDGET = 32;
363
+ const CODEX_VERSION_PROBE_DEADLINE_MS = 5_000;
364
+
365
+ type CodexCandidateInspection =
366
+ | { kind: 'absent' }
367
+ | { kind: 'present' }
368
+ | { kind: 'dangling' }
369
+ | { kind: 'uninspectable'; error: Error };
370
+
371
+ function inspectCodexCandidate(executable: string): CodexCandidateInspection {
372
+ try {
373
+ lstatSync(executable);
374
+ } catch (error) {
375
+ if (isNodeErrorWithCode(error, 'ENOENT')) return { kind: 'absent' };
376
+ return {
377
+ kind: 'uninspectable',
378
+ error: error instanceof Error ? error : new Error(String(error)),
379
+ };
380
+ }
381
+
382
+ try {
383
+ statSync(executable);
384
+ return { kind: 'present' };
385
+ } catch (error) {
386
+ if (isNodeErrorWithCode(error, 'ENOENT')) return { kind: 'dangling' };
387
+ return {
388
+ kind: 'uninspectable',
389
+ error: error instanceof Error ? error : new Error(String(error)),
390
+ };
391
+ }
392
+ }
393
+
394
+ function versionProbeDeadlineError(): Error {
395
+ return new Error(
396
+ `Codex version resolution exceeded the ${CODEX_VERSION_PROBE_DEADLINE_MS}ms global deadline`,
397
+ );
398
+ }
399
+
400
+ export interface CodexCommandSpawnSeam {
401
+ platform?: NodeJS.Platform;
402
+ spawnSyncImpl?: typeof spawnSync;
403
+ spawnImpl?: typeof spawn;
404
+ }
405
+
406
+ function* streamPathEntries(pathValue: string, deadline: number, platform: NodeJS.Platform): Iterable<string> {
407
+ const pathDelimiter = platform === 'win32' ? ';' : delimiter;
408
+ let start = 0;
409
+ for (let index = 0; index < pathValue.length; index += 1) {
410
+ if ((index & 0x3ff) === 0 && Date.now() >= deadline) throw versionProbeDeadlineError();
411
+ if (pathValue[index] !== pathDelimiter) continue;
412
+ yield pathValue.slice(start, index);
413
+ start = index + 1;
414
+ }
415
+ if (Date.now() >= deadline) throw versionProbeDeadlineError();
416
+ yield pathValue.slice(start);
417
+ }
418
+
419
+ function codexCandidatePaths(pathEntry: string, env: NodeJS.ProcessEnv, platform: NodeJS.Platform): string[] {
420
+ if (platform !== 'win32') return [join(pathEntry, 'codex')];
421
+ const pathext = String(env.PATHEXT ?? '.COM;.EXE;.BAT;.CMD;.PS1')
422
+ .split(';')
423
+ .map((extension) => extension.trim().toLowerCase())
424
+ .filter(Boolean);
425
+ return [...new Set([...pathext.map((extension) => join(pathEntry, `codex${extension}`)), join(pathEntry, 'codex')])];
426
+ }
427
+
428
+ function resolvePinnedCodexExecutable(
429
+ cwd: string,
430
+ env: NodeJS.ProcessEnv,
431
+ seam: CodexCommandSpawnSeam = {},
432
+ ): { executable: string; version: string } {
433
+ const platform = seam.platform ?? process.platform;
434
+ const deadline = Date.now() + CODEX_VERSION_PROBE_DEADLINE_MS;
435
+ const pathValue = env.PATH ?? env.Path ?? '';
436
+ const observed: string[] = [];
437
+ const seenPathEntries = new Set<string>();
438
+
439
+ for (const entry of streamPathEntries(pathValue, deadline, platform)) {
440
+ if (Date.now() >= deadline) throw versionProbeDeadlineError();
441
+ const pathEntry = resolve(cwd, entry || '.');
442
+ if (seenPathEntries.has(pathEntry)) continue;
443
+ if (seenPathEntries.size === CODEX_VERSION_PROBE_CANDIDATE_BUDGET) {
444
+ throw new Error(
445
+ `Codex version resolution exceeded the ${CODEX_VERSION_PROBE_CANDIDATE_BUDGET}-candidate PATH budget`,
446
+ );
447
+ }
448
+ seenPathEntries.add(pathEntry);
449
+ const remainingMs = deadline - Date.now();
450
+ if (remainingMs <= 0) throw versionProbeDeadlineError();
451
+
452
+ let pathEntryState = inspectCodexCandidate(pathEntry);
453
+ if (pathEntryState.kind === 'absent') {
454
+ pathEntryState = inspectCodexCandidate(pathEntry);
455
+ if (pathEntryState.kind === 'absent') continue;
456
+ }
457
+ if (pathEntryState.kind === 'dangling') {
458
+ observed.push(`${pathEntry}: PATH entry exists but its target is unavailable (dangling or changed during probe)`);
459
+ continue;
460
+ }
461
+ if (pathEntryState.kind === 'uninspectable') {
462
+ observed.push(`${pathEntry}: PATH entry cannot be inspected (${pathEntryState.error.message})`);
463
+ continue;
464
+ }
465
+
466
+ let selected: { executable: string; before: CodexCandidateInspection } | null = null;
467
+ for (const executable of codexCandidatePaths(pathEntry, env, platform)) {
468
+ if (Date.now() >= deadline) throw versionProbeDeadlineError();
469
+ let before = inspectCodexCandidate(executable);
470
+ if (before.kind === 'absent') {
471
+ before = inspectCodexCandidate(executable);
472
+ if (before.kind === 'absent') continue;
473
+ }
474
+ selected = { executable, before };
475
+ break;
476
+ }
477
+ if (!selected) continue;
478
+
479
+ const { executable, before } = selected;
480
+ if (before.kind === 'dangling') {
481
+ observed.push(`${executable}: candidate exists but its target is unavailable (dangling or changed during probe)`);
482
+ continue;
483
+ }
484
+ if (before.kind === 'uninspectable') {
485
+ observed.push(`${executable}: candidate cannot be inspected (${before.error.message})`);
486
+ continue;
487
+ }
488
+
489
+ const { result } = spawnPlatformCommandSync(executable, ['--version'], {
490
+ cwd,
491
+ env,
492
+ encoding: 'utf-8',
493
+ timeout: Math.max(1, Math.min(CODEX_APP_SERVER_TIMEOUTS.versionProbeMs, remainingMs)),
494
+ killSignal: 'SIGKILL',
495
+ }, platform, env, undefined, seam.spawnSyncImpl ?? spawnSync);
496
+ if (Date.now() >= deadline) throw versionProbeDeadlineError();
497
+ if (isNodeErrorWithCode(result.error, 'ENOENT')) {
498
+ const after = inspectCodexCandidate(executable);
499
+ observed.push(
500
+ `${executable}: launch returned ENOENT after an existing candidate was observed (${after.kind})`,
501
+ );
502
+ continue;
503
+ }
504
+ if (isNodeErrorWithCode(result.error, 'ETIMEDOUT')) {
505
+ observed.push(`${executable}: version probe timed out after ${CODEX_APP_SERVER_TIMEOUTS.versionProbeMs}ms and was force-terminated`);
506
+ continue;
507
+ }
508
+ if (result.error) {
509
+ observed.push(`${executable}: version probe failed (${result.error.message})`);
510
+ continue;
511
+ }
512
+ if (result.status !== 0) {
513
+ observed.push(`${executable}: exit ${String(result.status)}`);
514
+ continue;
515
+ }
516
+
517
+ const stdout = String(result.stdout ?? '');
518
+ const stderr = String(result.stderr ?? '');
519
+ if (hasExactPinnedCodexVersionStdout(stdout) && hasOnlyBenignCodexVersionStderr(stderr)) {
520
+ return { executable, version: PINNED_CODEX_VERSION_OUTPUT };
521
+ }
522
+ observed.push(`${executable}: ${formatVersionProbeOutput(stdout, stderr)}`);
523
+ }
524
+
525
+ if (Date.now() >= deadline) throw versionProbeDeadlineError();
526
+ if (observed.length === 0) throw new CodexExecutableNotFoundError();
527
+ throw new Error(
528
+ `Unsupported installed Codex version for the ${PINNED_CODEX_VERSION} boundary:\n${observed.join('\n')}`,
529
+ );
530
+ }
531
+
532
+
533
+ /**
534
+ * Newline-delimited JSON-RPC client for the installed Codex app-server boundary.
535
+ * It accepts no protocol fallback: malformed messages, request errors, unexpected
536
+ * exits, and cleanup failures are test failures.
537
+ */
538
+ export class CodexAppServer {
539
+ private readonly pending = new Map<number, PendingRequest>();
540
+ private readonly closePromise: Promise<{ code: number | null; signal: NodeJS.Signals | null }>;
541
+ private readonly stdoutDecoder = new TextDecoder('utf-8', { fatal: true });
542
+ private stderr = '';
543
+ private stdoutBuffer = '';
544
+ private failure: Error | null = null;
545
+ private closing = false;
546
+ private spawned = false;
547
+
548
+ private constructor(private readonly child: ChildProcessWithoutNullStreams) {
549
+ this.closePromise = new Promise((resolveClose) => {
550
+ child.once('close', (code, signal) => {
551
+ const exit = { code, signal };
552
+ this.flushStdoutDecoder();
553
+ if (this.stdoutBuffer.length > 0) {
554
+ this.fail(protocolError(
555
+ `codex app-server closed with unterminated JSON-RPC stdout: ${JSON.stringify(compactDiagnostic(this.stdoutBuffer))}`,
556
+ this.stderr,
557
+ ));
558
+ }
559
+ if (!this.closing) {
560
+ this.fail(protocolError(
561
+ `codex app-server exited unexpectedly (code ${String(code)}, signal ${String(signal)})`,
562
+ this.stderr,
563
+ ));
564
+ }
565
+ resolveClose(exit);
566
+ });
567
+ });
568
+
569
+ child.once('spawn', () => {
570
+ this.spawned = true;
571
+ });
572
+ child.once('error', (error) => {
573
+ this.fail(error instanceof Error ? error : new Error(String(error)));
574
+ });
575
+ child.stdout.on('data', (chunk: Buffer) => this.onStdout(chunk));
576
+ child.stderr.setEncoding('utf-8');
577
+ child.stderr.on('data', (chunk: string) => {
578
+ this.stderr = compactDiagnostic(this.stderr + chunk);
579
+ });
580
+ }
581
+
582
+
583
+ static async start(options: {
584
+ cwd: string;
585
+ env: NodeJS.ProcessEnv;
586
+ commandSeam?: CodexCommandSpawnSeam;
587
+ }): Promise<CodexAppServer> {
588
+ const seam = options.commandSeam;
589
+ const platform = seam?.platform ?? process.platform;
590
+ const { executable } = resolvePinnedCodexExecutable(options.cwd, options.env, seam);
591
+ const { child } = spawnPlatformCommand(executable, ['app-server', '--stdio'], {
592
+ cwd: options.cwd,
593
+ env: options.env,
594
+ stdio: 'pipe',
595
+ }, platform, options.env, undefined, seam?.spawnImpl ?? spawn);
596
+
597
+ const server = new CodexAppServer(child as ChildProcessWithoutNullStreams);
598
+ try {
599
+ await server.waitForStartup();
600
+ } catch (error) {
601
+ try {
602
+ await server.close();
603
+ } catch {
604
+ // Preserve the startup failure as the primary diagnostic.
605
+ }
606
+ throw error;
607
+ }
608
+ return server;
609
+ }
610
+
611
+ private async waitForStartup(): Promise<void> {
612
+ const started = new Promise<void>((resolveStarted, rejectStarted) => {
613
+ if (this.spawned) {
614
+ resolveStarted();
615
+ return;
616
+ }
617
+ this.child.once('spawn', resolveStarted);
618
+ this.child.once('error', rejectStarted);
619
+ });
620
+ await this.withTimeout(started, CODEX_APP_SERVER_TIMEOUTS.initializeMs, 'codex app-server did not start');
621
+ }
622
+
623
+ private onStdout(chunk: Buffer): void {
624
+ let decoded: string;
625
+ try {
626
+ decoded = this.stdoutDecoder.decode(chunk, { stream: true });
627
+ } catch {
628
+ this.fail(protocolError('codex app-server emitted invalid UTF-8 stdout', this.stderr));
629
+ return;
630
+ }
631
+ this.onStdoutText(decoded);
632
+ }
633
+
634
+ private flushStdoutDecoder(): void {
635
+ try {
636
+ const decoded = this.stdoutDecoder.decode();
637
+ if (decoded) this.onStdoutText(decoded);
638
+ } catch {
639
+ this.fail(protocolError('codex app-server emitted invalid UTF-8 stdout', this.stderr));
640
+ }
641
+ }
642
+
643
+ private onStdoutText(chunk: string): void {
644
+ this.stdoutBuffer += chunk;
645
+ while (true) {
646
+ const newline = this.stdoutBuffer.indexOf('\n');
647
+ const frameLength = Buffer.byteLength(
648
+ newline < 0 ? this.stdoutBuffer : this.stdoutBuffer.slice(0, newline),
649
+ 'utf-8',
650
+ );
651
+ if (frameLength > CODEX_APP_SERVER_MAX_STDOUT_FRAME_LENGTH) {
652
+ this.fail(protocolError('codex app-server emitted an oversized JSON-RPC stdout frame', this.stderr));
653
+ return;
654
+ }
655
+ if (newline < 0) return;
656
+ const line = this.stdoutBuffer.slice(0, newline).trim();
657
+ this.stdoutBuffer = this.stdoutBuffer.slice(newline + 1);
658
+ if (!line) continue;
659
+ let envelope: unknown;
660
+ try {
661
+ envelope = JSON.parse(line);
662
+ } catch {
663
+ this.fail(protocolError(`codex app-server emitted malformed JSON-RPC stdout: ${line}`, this.stderr));
664
+ return;
665
+ }
666
+ if (!isRecord(envelope)) {
667
+ this.fail(protocolError('codex app-server emitted a non-object JSON-RPC envelope', this.stderr));
668
+ return;
669
+ }
670
+ this.onEnvelope(envelope);
671
+ }
672
+ }
673
+
674
+ private isMethodBearingNotification(envelope: JsonRecord): boolean {
675
+ return !Object.hasOwn(envelope, 'id')
676
+ && typeof envelope.method === 'string'
677
+ && envelope.method.trim().length > 0
678
+ && !Object.hasOwn(envelope, 'result')
679
+ && !Object.hasOwn(envelope, 'error');
680
+ }
681
+
682
+ private validateResponseEnvelope(envelope: JsonRecord): string | null {
683
+ if (Object.hasOwn(envelope, 'method') || Object.hasOwn(envelope, 'params')) {
684
+ return 'codex app-server response mixed request or notification fields with an id';
685
+ }
686
+ const hasResult = Object.hasOwn(envelope, 'result');
687
+ const hasError = Object.hasOwn(envelope, 'error');
688
+ if (hasResult === hasError) {
689
+ return 'codex app-server response must contain exactly one of result or error';
690
+ }
691
+ if (hasError) {
692
+ const error = envelope.error;
693
+ if (!isRecord(error)
694
+ || typeof error.code !== 'number'
695
+ || !Number.isFinite(error.code)
696
+ || typeof error.message !== 'string') {
697
+ return 'codex app-server response has malformed JSON-RPC error';
698
+ }
699
+ }
700
+ return null;
701
+ }
702
+
703
+ private onEnvelope(envelope: JsonRecord): void {
704
+ if (!Object.hasOwn(envelope, 'id')) {
705
+ if (!this.isMethodBearingNotification(envelope)) {
706
+ this.fail(protocolError('codex app-server emitted an invalid idless JSON-RPC envelope', this.stderr));
707
+ }
708
+ return;
709
+ }
710
+ if (typeof envelope.id !== 'number') {
711
+ this.fail(protocolError('codex app-server response had a non-numeric id', this.stderr));
712
+ return;
713
+ }
714
+ const validationError = this.validateResponseEnvelope(envelope);
715
+ if (validationError) {
716
+ this.fail(protocolError(validationError, this.stderr));
717
+ return;
718
+ }
719
+ const pending = this.pending.get(envelope.id);
720
+ if (!pending) {
721
+ this.fail(protocolError(`codex app-server returned an unexpected response id ${envelope.id}`, this.stderr));
722
+ return;
723
+ }
724
+ this.pending.delete(envelope.id);
725
+ clearTimeout(pending.timeout);
726
+ if (Object.hasOwn(envelope, 'error')) {
727
+ pending.reject(protocolError(
728
+ `codex app-server ${pending.method} returned JSON-RPC error ${JSON.stringify(envelope.error)}`,
729
+ this.stderr,
730
+ ));
731
+ return;
732
+ }
733
+ pending.resolve(envelope.result);
734
+ }
735
+
736
+
737
+ private fail(error: Error): void {
738
+ if (this.failure) return;
739
+ this.failure = error;
740
+ for (const [id, pending] of this.pending) {
741
+ this.pending.delete(id);
742
+ clearTimeout(pending.timeout);
743
+ pending.reject(error);
744
+ }
745
+ }
746
+
747
+ private withTimeout<T>(promise: Promise<T>, timeoutMs: number, message: string): Promise<T> {
748
+ return new Promise<T>((resolvePromise, rejectPromise) => {
749
+ const timeout = setTimeout(() => rejectPromise(protocolError(message, this.stderr)), timeoutMs);
750
+ promise.then(
751
+ (value) => {
752
+ clearTimeout(timeout);
753
+ resolvePromise(value);
754
+ },
755
+ (error: unknown) => {
756
+ clearTimeout(timeout);
757
+ rejectPromise(error);
758
+ },
759
+ );
760
+ });
761
+ }
762
+
763
+ async request<T = unknown>(envelope: Required<Pick<JsonRpcEnvelope, 'id' | 'method'>> & JsonRpcEnvelope, timeoutMs: number): Promise<T> {
764
+ if (this.failure) throw this.failure;
765
+ if (!Number.isInteger(envelope.id) || envelope.id <= 0 || !envelope.method) {
766
+ throw new Error('Codex JSON-RPC request requires a positive numeric id and method');
767
+ }
768
+ if (this.pending.has(envelope.id)) {
769
+ throw new Error(`duplicate Codex JSON-RPC request id ${envelope.id}`);
770
+ }
771
+ const response = new Promise<unknown>((resolveResponse, rejectResponse) => {
772
+ const timeout = setTimeout(() => {
773
+ this.pending.delete(envelope.id);
774
+ rejectResponse(protocolError(
775
+ `codex app-server ${envelope.method} request ${envelope.id} timed out after ${timeoutMs}ms`,
776
+ this.stderr,
777
+ ));
778
+ }, timeoutMs);
779
+ this.pending.set(envelope.id, {
780
+ method: envelope.method,
781
+ resolve: resolveResponse,
782
+ reject: rejectResponse,
783
+ timeout,
784
+ });
785
+ });
786
+ this.child.stdin.write(`${JSON.stringify(envelope)}\n`, 'utf-8', (error) => {
787
+ if (error) this.fail(error instanceof Error ? error : new Error(String(error)));
788
+ });
789
+ return await response as T;
790
+ }
791
+
792
+ notify(envelope: Omit<JsonRpcEnvelope, 'id'>): void {
793
+ if (this.failure) throw this.failure;
794
+ if (!envelope.method) throw new Error('Codex JSON-RPC notification requires a method');
795
+ this.child.stdin.write(`${JSON.stringify(envelope)}\n`, 'utf-8', (error) => {
796
+ if (error) this.fail(error instanceof Error ? error : new Error(String(error)));
797
+ });
798
+ }
799
+
800
+ async close(): Promise<void> {
801
+ if (this.closing) return;
802
+ this.closing = true;
803
+ this.child.stdin.end();
804
+ let exit: { code: number | null; signal: NodeJS.Signals | null };
805
+ try {
806
+ exit = await this.withTimeout(
807
+ this.closePromise,
808
+ CODEX_APP_SERVER_TIMEOUTS.shutdownMs,
809
+ 'codex app-server did not exit after stdin closed',
810
+ );
811
+ } catch (error) {
812
+ this.child.kill('SIGKILL');
813
+ try {
814
+ await this.withTimeout(
815
+ this.closePromise,
816
+ CODEX_APP_SERVER_TIMEOUTS.shutdownMs,
817
+ 'codex app-server did not exit after SIGKILL',
818
+ );
819
+ } catch {
820
+ // Preserve the original graceful-cleanup failure, which has better diagnostics.
821
+ }
822
+ throw error;
823
+ }
824
+ if (exit.code !== 0 || exit.signal !== null) {
825
+ throw protocolError(
826
+ `codex app-server exited abnormally during cleanup (code ${String(exit.code)}, signal ${String(exit.signal)})`,
827
+ this.stderr,
828
+ );
829
+ }
830
+ if (this.failure) throw this.failure;
831
+ }
832
+ }
833
+
834
+ export function createCodexInitializeEnvelope(clientName: string): JsonRpcRequestEnvelope {
835
+ return {
836
+ id: 1,
837
+ method: 'initialize',
838
+ params: {
839
+ clientInfo: { name: clientName, version: '1.0.0' },
840
+ capabilities: null,
841
+ },
842
+ };
843
+ }
844
+
845
+ export const CODEX_INITIALIZED_ENVELOPE: JsonRpcEnvelope = { method: 'initialized' };
846
+
847
+ export function createCodexHooksListEnvelope(projectCwd: string): JsonRpcRequestEnvelope {
848
+ return {
849
+ id: 2,
850
+ method: 'hooks/list',
851
+ params: { cwds: [projectCwd] },
852
+ };
853
+ }
854
+
855
+ export function createCodexBatchWriteEnvelope(trustByKey: Record<string, string>): JsonRpcRequestEnvelope {
856
+ return {
857
+ id: 3,
858
+ method: 'config/batchWrite',
859
+ params: {
860
+ edits: [{
861
+ keyPath: 'hooks.state',
862
+ value: Object.fromEntries(
863
+ Object.entries(trustByKey).map(([key, currentHash]) => [key, { trusted_hash: currentHash }]),
864
+ ),
865
+ mergeStrategy: 'upsert',
866
+ }],
867
+ filePath: null,
868
+ expectedVersion: null,
869
+ reloadUserConfig: true,
870
+ },
871
+ };
872
+ }
873
+
874
+ export async function initializeCodexAppServer(server: CodexAppServer, clientName: string): Promise<void> {
875
+ await server.request(createCodexInitializeEnvelope(clientName), CODEX_APP_SERVER_TIMEOUTS.initializeMs);
876
+ server.notify(CODEX_INITIALIZED_ENVELOPE);
877
+ }
878
+
879
+ export async function listCodexHooks(
880
+ server: CodexAppServer,
881
+ projectCwd: string,
882
+ hooksPath: string,
883
+ ): Promise<CodexHooksListEntry> {
884
+ const result = await server.request<unknown>(
885
+ createCodexHooksListEnvelope(projectCwd),
886
+ CODEX_APP_SERVER_TIMEOUTS.requestMs,
887
+ );
888
+ return parseCodexHooksListResult(result, projectCwd, hooksPath);
889
+ }
890
+
891
+ export function parseCodexHooksListResult(
892
+ result: unknown,
893
+ projectCwd: string,
894
+ hooksPath: string,
895
+ ): CodexHooksListEntry {
896
+ const resultRecord = requireRecord(result, 'hooks/list result');
897
+ if (!Array.isArray(resultRecord.data) || resultRecord.data.length !== 1) {
898
+ throw new Error('Codex hooks/list response must contain exactly one data entry');
899
+ }
900
+ const entry = requireRecord(resultRecord.data[0], 'hooks/list data entry');
901
+ if (entry.cwd !== projectCwd) {
902
+ throw new Error(`Codex hooks/list response cwd mismatch: expected ${projectCwd}, got ${String(entry.cwd)}`);
903
+ }
904
+ if (!Array.isArray(entry.hooks) || !Array.isArray(entry.warnings) || !Array.isArray(entry.errors)) {
905
+ throw new Error('Codex hooks/list response must contain hooks, warnings, and errors arrays');
906
+ }
907
+ if (entry.warnings.length !== 0 || entry.errors.length !== 0) {
908
+ throw new Error(`Codex hooks/list reported warnings/errors: ${JSON.stringify({ warnings: entry.warnings, errors: entry.errors })}`);
909
+ }
910
+
911
+ const hooks = entry.hooks.map((value, index) => {
912
+ const hook = requireRecord(value, `hooks/list hook ${index}`);
913
+ const rawEvent = requireString(hook.eventName, `hooks[${index}].eventName`);
914
+ const event = CODEX_EVENT_LABELS[rawEvent] ?? rawEvent;
915
+ const command = requireString(hook.command, `hooks[${index}].command`);
916
+ const enabled = hook.enabled === undefined ? true : hook.enabled;
917
+ if (enabled !== true) {
918
+ throw new Error(`Codex hooks/list hook ${index} is not an enabled command handler`);
919
+ }
920
+ const sourcePath = requireString(hook.sourcePath, `hooks[${index}].sourcePath`);
921
+ const key = requireString(hook.key, `hooks[${index}].key`);
922
+ const currentHash = requireString(hook.currentHash, `hooks[${index}].currentHash`);
923
+ const displayOrder = hook.displayOrder;
924
+ if (typeof displayOrder !== 'number' || !Number.isSafeInteger(displayOrder) || displayOrder < 0) {
925
+ throw new Error(`Codex hooks/list response has invalid hooks[${index}].displayOrder`);
926
+ }
927
+ const trustStatus = requireString(hook.trustStatus, `hooks[${index}].trustStatus`);
928
+ if (!CODEX_TRUST_STATUSES.has(trustStatus)) {
929
+ throw new Error(`Codex hooks/list response has unsupported trustStatus ${trustStatus}`);
930
+ }
931
+
932
+
933
+ if (sourcePath !== hooksPath) {
934
+ throw new Error(`Codex hooks/list sourcePath mismatch: expected ${hooksPath}, got ${sourcePath}`);
935
+ }
936
+ return {
937
+ event,
938
+ command,
939
+ sourcePath,
940
+ key,
941
+ currentHash,
942
+ displayOrder,
943
+ trustStatus: trustStatus as CodexHookMetadata['trustStatus'],
944
+ };
945
+ });
946
+
947
+ return {
948
+ cwd: projectCwd,
949
+ hooks,
950
+ warnings: entry.warnings,
951
+ errors: entry.errors,
952
+ };
953
+ }
954
+
955
+
956
+
957
+ export function managedCodexHooksByEvent(hooks: readonly CodexHookMetadata[]): Record<ManagedCodexHookEvent, CodexHookMetadata> {
958
+ const result = {} as Record<ManagedCodexHookEvent, CodexHookMetadata>;
959
+ for (const event of MANAGED_CODEX_HOOK_EVENTS) {
960
+ const matching = hooks.filter((hook) => hook.event === event && isManagedCodexHookCommand(hook.command));
961
+ if (matching.length !== 1) {
962
+ throw new Error(`Expected exactly one OMX ${event} hook from Codex, received ${matching.length}`);
963
+ }
964
+ result[event] = matching[0]!;
965
+ }
966
+ return result;
967
+ }
968
+
969
+ export function generatedHookTrustState(configToml: string): Record<string, string> {
970
+ const parsed = TOML.parse(configToml) as { hooks?: { state?: Record<string, unknown> } };
971
+ const state = parsed.hooks?.state;
972
+ if (!isRecord(state)) throw new Error('setup config.toml is missing hooks.state');
973
+ const trust = Object.create(null) as Record<string, string>;
974
+ for (const [key, value] of Object.entries(state)) {
975
+ if (!isRecord(value)) {
976
+ throw new Error(`setup config.toml has invalid hooks.state entry ${key}: expected an object`);
977
+ }
978
+ const entryKeys = Object.keys(value);
979
+ if (entryKeys.length !== 1 || entryKeys[0] !== 'trusted_hash') {
980
+ throw new Error(`setup config.toml has invalid hooks.state entry ${key}: expected only trusted_hash`);
981
+ }
982
+ if (typeof value.trusted_hash !== 'string' || value.trusted_hash.trim().length === 0) {
983
+ throw new Error(`setup config.toml has invalid hooks.state entry ${key}: trusted_hash must be non-empty`);
984
+ }
985
+ trust[key] = value.trusted_hash;
986
+ }
987
+ return trust;
988
+ }
989
+
990
+ export function managedTrustByKey(hooks: readonly CodexHookMetadata[]): Record<string, string> {
991
+ return Object.fromEntries(
992
+ Object.values(managedCodexHooksByEvent(hooks)).map((hook) => [hook.key, hook.currentHash]),
993
+ );
994
+ }
995
+
996
+ export function assertGeneratedTrustMatchesCodex(
997
+ generatedTrust: Record<string, string>,
998
+ hooks: readonly CodexHookMetadata[],
999
+ ): void {
1000
+ const expected = managedTrustByKey(hooks);
1001
+ const expectedKeys = Object.keys(expected).sort();
1002
+ const actualKeys = Object.keys(generatedTrust).sort();
1003
+ if (actualKeys.length !== MANAGED_CODEX_HOOK_EVENTS.length
1004
+ || actualKeys.length !== expectedKeys.length
1005
+ || actualKeys.some((key, index) => key !== expectedKeys[index])) {
1006
+ throw new Error(
1007
+ `Generated trust must contain exactly the ${MANAGED_CODEX_HOOK_EVENTS.length} current OMX hooks with no stale keys`,
1008
+ );
1009
+ }
1010
+ for (const [key, currentHash] of Object.entries(expected)) {
1011
+ if (generatedTrust[key] !== currentHash) {
1012
+ throw new Error(`Generated hook trust does not equal Codex hooks/list metadata for ${key}`);
1013
+ }
1014
+ }
1015
+ }
1016
+
1017
+ export async function approveManagedHooksInCodex(
1018
+ server: CodexAppServer,
1019
+ hooks: readonly CodexHookMetadata[],
1020
+ expectedConfigPath: string,
1021
+ ): Promise<void> {
1022
+ const result = await server.request<unknown>(
1023
+ createCodexBatchWriteEnvelope(managedTrustByKey(hooks)),
1024
+ CODEX_APP_SERVER_TIMEOUTS.requestMs,
1025
+ );
1026
+ assertCodexBatchWriteResult(result, expectedConfigPath);
1027
+ }
1028
+
1029
+ export function assertCodexBatchWriteResult(result: unknown, expectedConfigPath: string): void {
1030
+ const response = requireRecord(result, 'config/batchWrite result');
1031
+ if (response.filePath !== expectedConfigPath) {
1032
+ throw new Error(
1033
+ `Codex config/batchWrite wrote ${String(response.filePath)}, expected isolated user config ${expectedConfigPath}`,
1034
+ );
1035
+ }
1036
+ if (response.status !== 'ok' && response.status !== 'okOverridden') {
1037
+ throw new Error(`Codex config/batchWrite returned unsupported status ${String(response.status)}`);
1038
+ }
1039
+ if (!Object.hasOwn(response, 'version')) {
1040
+ throw new Error('Codex config/batchWrite result is missing version');
1041
+ }
1042
+ }
1043
+
1044
+ export function hookMetadataSnapshot(hooks: readonly CodexHookMetadata[]): Array<Pick<CodexHookMetadata, 'event' | 'command' | 'key' | 'currentHash' | 'displayOrder' | 'trustStatus'>> {
1045
+ return hooks
1046
+ .map(({ event, command, key, currentHash, displayOrder, trustStatus }) => ({
1047
+ event,
1048
+ command,
1049
+ key,
1050
+ currentHash,
1051
+ displayOrder,
1052
+ trustStatus,
1053
+ }))
1054
+ .sort((left, right) => left.displayOrder - right.displayOrder);
1055
+ }
1056
+
1057
+ export function appendForeignHookGroups(
1058
+ hooksContent: string,
1059
+ marker: string,
1060
+ options: { appendGroups: boolean },
1061
+ ): string {
1062
+ const parsed = JSON.parse(hooksContent) as { hooks?: Record<string, unknown> };
1063
+ if (!isRecord(parsed.hooks)) throw new Error('hooks.json is missing hooks object');
1064
+ const hooks = parsed.hooks as Record<string, unknown>;
1065
+ const foreignCommand = (event: string, suffix = '') =>
1066
+ `${JSON.stringify(process.execPath)} ${JSON.stringify(`${marker}-${event}${suffix}.js`)}`;
1067
+ const foreignGroup = (event: string) => ({
1068
+ ...(event === 'PreToolUse' ? { matcher: 'Bash' } : {}),
1069
+ foreign_group_metadata: {
1070
+ marker,
1071
+ nested: { keep: ['foreign', event], depth: { value: 7 } },
1072
+ },
1073
+ hooks: [{
1074
+ type: 'command',
1075
+ command: foreignCommand(event),
1076
+ statusMessage: `${marker} ${event}`,
1077
+ foreign_handler_metadata: { marker, event, values: [1, { two: true }] },
1078
+ }],
1079
+ });
1080
+ for (const event of ['PreToolUse', 'PostToolUse'] as const) {
1081
+ const existing = hooks[event];
1082
+ if (existing !== undefined && !Array.isArray(existing)) {
1083
+ throw new Error(`hooks.${event} is not an array`);
1084
+ }
1085
+ const entries = (existing ?? []) as unknown[];
1086
+ if (options.appendGroups) {
1087
+ entries.push(foreignGroup(event));
1088
+ } else {
1089
+ const group = entries.find((entry): entry is JsonRecord => isRecord(entry)
1090
+ && Array.isArray(entry.hooks)
1091
+ && entry.hooks.some((hook) => isRecord(hook) && typeof hook.command === 'string' && hook.command.includes(marker)));
1092
+ if (!group) {
1093
+ throw new Error(`missing pre-seeded foreign ${event} group`);
1094
+ }
1095
+ const groupHooks = group.hooks;
1096
+ if (!Array.isArray(groupHooks)) {
1097
+ throw new Error(`missing pre-seeded foreign ${event} handler array`);
1098
+ }
1099
+ groupHooks.push({
1100
+ type: 'command',
1101
+ command: foreignCommand(event, '-inserted'),
1102
+ statusMessage: `${marker} inserted ${event}`,
1103
+ foreign_handler_metadata: { marker, inserted: true, nested: { event } },
1104
+ });
1105
+ }
1106
+ hooks[event] = entries;
1107
+ }
1108
+ return JSON.stringify(parsed, null, 2) + '\n';
1109
+ }
1110
+
1111
+ export function appendDisplayOrderStableForeignHookGroups(
1112
+ hooksContent: string,
1113
+ marker: string,
1114
+ options: { appendGroups: boolean },
1115
+ ): string {
1116
+ const parsed = JSON.parse(hooksContent) as { hooks?: Record<string, unknown> };
1117
+ if (!isRecord(parsed.hooks)) throw new Error('hooks.json is missing hooks object');
1118
+ const hooks = parsed.hooks as Record<string, unknown>;
1119
+ const event = 'PreToolUse';
1120
+ const existing = hooks[event];
1121
+ if (existing !== undefined && !Array.isArray(existing)) {
1122
+ throw new Error(`hooks.${event} is not an array`);
1123
+ }
1124
+ const entries = (existing ?? []) as unknown[];
1125
+ const foreignCommand = (groupLabel: string, suffix = '') =>
1126
+ `${JSON.stringify(process.execPath)} ${JSON.stringify(`${marker}-${event}-${groupLabel}${suffix}.js`)}`;
1127
+ if (options.appendGroups) {
1128
+ for (const groupLabel of ['first', 'second']) {
1129
+ entries.push({
1130
+ ...(groupLabel === 'first' ? { matcher: 'Bash' } : {}),
1131
+ foreign_group_metadata: {
1132
+ marker,
1133
+ groupLabel,
1134
+ nested: { keep: ['foreign', event, groupLabel], depth: { value: 7 } },
1135
+ },
1136
+ hooks: [{
1137
+ type: 'command',
1138
+ command: foreignCommand(groupLabel),
1139
+ statusMessage: `${marker} ${event} ${groupLabel}`,
1140
+ foreign_handler_metadata: { marker, event, groupLabel, values: [1, { two: true }] },
1141
+ }],
1142
+ });
1143
+ }
1144
+ } else {
1145
+ const groupLabel = 'second';
1146
+ const group = entries.find((entry): entry is JsonRecord => isRecord(entry)
1147
+ && Array.isArray(entry.hooks)
1148
+ && entry.hooks.some((hook) => isRecord(hook)
1149
+ && hook.command === foreignCommand(groupLabel)));
1150
+ if (!group || !Array.isArray(group.hooks)) {
1151
+ throw new Error(`missing pre-seeded foreign ${event} ${groupLabel} group`);
1152
+ }
1153
+ for (const insertionLabel of ['one', 'two']) {
1154
+ group.hooks.push({
1155
+ type: 'command',
1156
+ command: foreignCommand(groupLabel, `-${insertionLabel}-inserted`),
1157
+ statusMessage: `${marker} inserted ${event} ${insertionLabel}`,
1158
+ foreign_handler_metadata: { marker, inserted: true, insertionLabel, nested: { event } },
1159
+ });
1160
+ }
1161
+ }
1162
+ hooks[event] = entries;
1163
+ return JSON.stringify(parsed, null, 2) + '\n';
1164
+ }
1165
+
1166
+
1167
+ export function foreignHookGroupSnapshot(hooksContent: string, marker: string): unknown[] {
1168
+ const parsed = JSON.parse(hooksContent) as { hooks?: Record<string, unknown> };
1169
+ if (!isRecord(parsed.hooks)) throw new Error('hooks.json is missing hooks object');
1170
+ const result: unknown[] = [];
1171
+ for (const [event, entries] of Object.entries(parsed.hooks)) {
1172
+ if (!Array.isArray(entries)) continue;
1173
+ entries.forEach((entry, groupIndex) => {
1174
+ if (!isRecord(entry) || !Array.isArray(entry.hooks)) return;
1175
+ entry.hooks.forEach((hook, handlerIndex) => {
1176
+ if (isRecord(hook) && typeof hook.command === 'string' && hook.command.includes(marker)) {
1177
+ result.push({ event, groupIndex, handlerIndex, group: structuredClone(entry), handler: structuredClone(hook) });
1178
+ }
1179
+ });
1180
+ });
1181
+ }
1182
+ return result;
1183
+ }
1184
+
1185
+ export function assertForeignHookGroupsPreserved(
1186
+ expected: unknown[],
1187
+ actualContent: string,
1188
+ marker: string,
1189
+ ): void {
1190
+ const actual = foreignHookGroupSnapshot(actualContent, marker);
1191
+ if (JSON.stringify(actual) !== JSON.stringify(expected)) {
1192
+ throw new Error(`Foreign hook groups or metadata changed: ${JSON.stringify({ expected, actual })}`);
1193
+ }
1194
+ }
1195
+
1196
+
1197
+ export function probeCodexVersion(
1198
+ cwd: string,
1199
+ env: NodeJS.ProcessEnv,
1200
+ seam?: CodexCommandSpawnSeam,
1201
+ ): string {
1202
+ return resolvePinnedCodexExecutable(cwd, env, seam).version;
1203
+ }
38
1204
  function usage(): string {
39
1205
  return [
40
1206
  'Usage: node scripts/smoke-packed-install.mjs',
41
1207
  '',
42
1208
  'Creates an npm tarball, installs it into an isolated prefix, and smoke tests the installed omx CLI.',
43
- 'Release smoke stays intentionally minimal: install + boot + 1-2 core commands only.',
1209
+ 'Release smoke validates installed CLI boot, native-hook dispatch, and the isolated setup/rerun/uninstall lifecycle; Codex trust checks run when the pinned CLI is present.',
44
1210
  ].join('\n');
45
1211
  }
46
1212
 
@@ -131,15 +1297,23 @@ function resolveGlobalNodeModules(prefixDir: string): string {
131
1297
  }
132
1298
 
133
1299
  export function validateHookStdout(eventName: string, stdout: string): void {
1300
+ if (eventName === 'PostCompact') {
1301
+ if (stdout.length === 0) return;
1302
+ throw new Error('native hook PostCompact must emit empty stdout');
1303
+ }
134
1304
  const trimmed = stdout.trim();
135
1305
  if (!trimmed) return;
1306
+ let parsed: unknown;
136
1307
  try {
137
- JSON.parse(trimmed);
1308
+ parsed = JSON.parse(trimmed);
138
1309
  } catch (error) {
139
1310
  throw new Error(
140
1311
  `native hook ${eventName} emitted invalid JSON stdout: ${error instanceof Error ? error.message : String(error)}`,
141
1312
  );
142
1313
  }
1314
+ if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {
1315
+ throw new Error(`native hook ${eventName} emitted a non-object JSON stdout payload`);
1316
+ }
143
1317
  }
144
1318
 
145
1319
  export function buildNativeHookSmokePayload(
@@ -190,6 +1364,81 @@ export function buildNativeHookSmokePayload(
190
1364
  }
191
1365
 
192
1366
  function smokeInstalledNativeHookDist(prefixDir: string): void {
1367
+ function runPackedTransportRegressions(hookScript: string, smokeCwd: string): void {
1368
+ const invoke = (cwd: string, environment: NodeJS.ProcessEnv, payload: Record<string, unknown>) => run(process.execPath, [realpathSync(hookScript)], {
1369
+ cwd, env: environment, input: JSON.stringify(payload),
1370
+ });
1371
+ const stopDecision = (cwd: string, environment: NodeJS.ProcessEnv, payload: Record<string, unknown>) => (
1372
+ JSON.parse(String(invoke(cwd, environment, { ...payload, hook_event_name: 'Stop' }).stdout || '{}')) as { decision?: string }
1373
+ ).decision;
1374
+
1375
+ const g1bCwd = join(smokeCwd, 'g1bu');
1376
+ const g1bSession = 'g1bu';
1377
+ const g1bThread = 'g1bu-thread';
1378
+ const g1bPriorTurn = 'g1bu-turn-old';
1379
+ const g1bTurn = 'g1bu-turn-new';
1380
+ const g1bStateDir = join(g1bCwd, '.omx', 'state');
1381
+ const g1bSessionDir = join(g1bStateDir, 'sessions', g1bSession);
1382
+ mkdirSync(g1bSessionDir, { recursive: true });
1383
+ for (const [path, marker] of [
1384
+ [join(g1bStateDir, 'skill-active-state.json'), 'root-skill'],
1385
+ [join(g1bSessionDir, 'skill-active-state.json'), 'session-skill'],
1386
+ ] as const) {
1387
+ writeFileSync(path, JSON.stringify({ active: true, skill: 'autopilot', phase: 'completing', session_id: g1bSession, thread_id: g1bThread, turn_id: g1bPriorTurn, marker, active_skills: [{ skill: 'autopilot', phase: 'completing', active: true, session_id: g1bSession, thread_id: g1bThread, turn_id: g1bPriorTurn }] }));
1388
+ }
1389
+ for (const [path, marker] of [
1390
+ [join(g1bStateDir, 'autopilot-state.json'), 'root-autopilot'],
1391
+ [join(g1bSessionDir, 'autopilot-state.json'), 'session-autopilot'],
1392
+ ] as const) {
1393
+ writeFileSync(path, JSON.stringify({ active: false, mode: 'autopilot', current_phase: 'complete', session_id: g1bSession, thread_id: g1bThread, turn_id: g1bPriorTurn, marker }));
1394
+ }
1395
+ const g1bEnv = { ...buildPackedRegressionEnvironment({ name: 'g1bu' }), OMX_TEAM_MODE: 'disabled' };
1396
+ const g1bPrompt = '$team $autopilot restart — café';
1397
+ const g1bPayload = { hook_event_name: 'UserPromptSubmit', cwd: g1bCwd, session_id: g1bSession, thread_id: g1bThread, turn_id: g1bTurn, prompt: g1bPrompt };
1398
+ validateHookStdout('UserPromptSubmit', String(invoke(g1bCwd, g1bEnv, g1bPayload).stdout || ''));
1399
+ const g1bState = JSON.parse(readFileSync(join(g1bSessionDir, 'skill-active-state.json'), 'utf-8')) as { active?: boolean; skill?: string; session_id?: string; thread_id?: string; turn_id?: string; active_skills?: Array<{ active?: boolean; skill?: string; session_id?: string; thread_id?: string; turn_id?: string }> };
1400
+ const [g1bActiveSkill] = g1bState.active_skills ?? [];
1401
+ if (!g1bState.active || g1bState.skill !== 'autopilot' || g1bState.session_id !== g1bSession || g1bState.thread_id !== g1bThread || g1bState.turn_id !== g1bTurn || g1bState.active_skills?.length !== 1 || !g1bActiveSkill?.active || g1bActiveSkill.skill !== 'autopilot' || g1bActiveSkill.session_id !== g1bSession || g1bActiveSkill.thread_id !== g1bThread || g1bActiveSkill.turn_id !== g1bTurn) throw new Error('packed G1b-U did not persist singleton Autopilot ownership for the new turn');
1402
+ const g1bAutopilotState = JSON.parse(readFileSync(join(g1bSessionDir, 'autopilot-state.json'), 'utf-8')) as { active?: boolean; current_phase?: string; session_id?: string; thread_id?: string; turn_id?: string; state?: { handoff_artifacts?: { context_snapshot?: { path?: string } } } };
1403
+ const g1bContextSnapshot = g1bAutopilotState.state?.handoff_artifacts?.context_snapshot?.path;
1404
+ if (!g1bAutopilotState.active || g1bAutopilotState.current_phase !== 'deep-interview' || g1bAutopilotState.session_id !== g1bSession || g1bAutopilotState.thread_id !== g1bThread || g1bAutopilotState.turn_id !== g1bTurn || !g1bContextSnapshot || !readFileSync(resolve(g1bCwd, g1bContextSnapshot)).includes(Buffer.from(g1bPrompt, 'utf-8'))) throw new Error('packed G1b-U did not restart Autopilot with its UTF-8 context snapshot');
1405
+ if (existsSync(join(g1bStateDir, 'team-state.json')) || existsSync(join(g1bSessionDir, 'team-state.json'))) throw new Error('packed G1b-U created Team state');
1406
+ if (stopDecision(g1bCwd, g1bEnv, g1bPayload) !== 'block') throw new Error('packed G1b-U did not block Stop');
1407
+
1408
+ const g2aCwd = join(smokeCwd, 'g2a');
1409
+ const g2aSession = 'g2a-stale-predecessor';
1410
+ const g2aStateDir = join(g2aCwd, '.omx', 'state');
1411
+ const g2aSessionDir = join(g2aStateDir, 'sessions', g2aSession);
1412
+ const g2aFiles = [join(g2aStateDir, 'skill-active-state.json'), join(g2aStateDir, 'ralplan-state.json'), join(g2aSessionDir, 'skill-active-state.json'), join(g2aSessionDir, 'ralplan-state.json'), join(g2aStateDir, 'session.json')];
1413
+ mkdirSync(g2aCwd, { recursive: true });
1414
+ if (g2aFiles.some((file) => existsSync(file))) throw new Error('packed G2a fixture unexpectedly contains state');
1415
+ const g2aEnv = buildPackedRegressionEnvironment({ name: 'g2a' });
1416
+ const g2aPayload = { hook_event_name: 'UserPromptSubmit', cwd: g2aCwd, session_id: g2aSession, thread_id: 'g2a-thread', turn_id: 'g2a-turn', prompt: 'use $ralplan is the consensus-planning command' };
1417
+ validateHookStdout('UserPromptSubmit', String(invoke(g2aCwd, g2aEnv, g2aPayload).stdout || ''));
1418
+ if (g2aFiles.some((file) => existsSync(file))) throw new Error('packed G2a stale predecessor created skill/detail state');
1419
+ if (stopDecision(g2aCwd, g2aEnv, g2aPayload) === 'block') throw new Error('packed G2a stale predecessor blocked Stop');
1420
+ if (g2aFiles.some((file) => existsSync(file))) throw new Error('packed G2a stale predecessor created skill/detail state after Stop');
1421
+
1422
+ const g2bCwd = join(smokeCwd, 'g2b');
1423
+ const g2bSession = 'g2b-terminal-session';
1424
+ const g2bStateDir = join(g2bCwd, '.omx', 'state');
1425
+ const g2bSessionDir = join(g2bStateDir, 'sessions', g2bSession);
1426
+ const g2bFiles = [join(g2bStateDir, 'skill-active-state.json'), join(g2bStateDir, 'autopilot-state.json'), join(g2bSessionDir, 'skill-active-state.json'), join(g2bSessionDir, 'autopilot-state.json'), join(g2bStateDir, 'session.json')];
1427
+ mkdirSync(g2bSessionDir, { recursive: true });
1428
+ writeFileSync(g2bFiles[0]!, JSON.stringify({ version: 1, active: false, skill: 'autopilot', phase: 'complete', completed_at: '2026-06-01T00:00:01.001Z', session_id: 'g2b-root-session', thread_id: 'g2b-root-thread', turn_id: 'g2b-root-skill-turn', marker: 'g2b-root-skill' }));
1429
+ writeFileSync(g2bFiles[1]!, JSON.stringify({ mode: 'autopilot', active: false, current_phase: 'complete', completed_at: '2026-06-01T00:00:02.002Z', session_id: 'g2b-root-session', thread_id: 'g2b-root-thread', turn_id: 'g2b-root-detail-turn', marker: 'g2b-root-detail' }));
1430
+ writeFileSync(g2bFiles[2]!, JSON.stringify({ version: 1, active: false, skill: 'autopilot', phase: 'complete', completed_at: '2026-06-01T00:00:03.003Z', session_id: g2bSession, thread_id: 'g2b-session-thread', turn_id: 'g2b-session-skill-turn', marker: 'g2b-session-skill' }));
1431
+ writeFileSync(g2bFiles[3]!, JSON.stringify({ mode: 'autopilot', active: false, current_phase: 'complete', completed_at: '2026-06-01T00:00:04.004Z', session_id: g2bSession, thread_id: 'g2b-session-thread', turn_id: 'g2b-session-detail-turn', marker: 'g2b-session-detail' }));
1432
+ writeFileSync(g2bFiles[4]!, JSON.stringify({ session_id: g2bSession, cwd: g2bCwd, created_at: '2026-06-01T00:00:05.005Z', updated_at: '2026-06-01T00:00:06.006Z', last_turn_id: 'g2b-session-json-turn', marker: 'g2b-session-json' }));
1433
+ const g2bBefore: Buffer[] = g2bFiles.map((file) => readFileSync(file));
1434
+ const g2bEnv = buildPackedRegressionEnvironment({ name: 'g2b' });
1435
+ const g2bPayload = { hook_event_name: 'UserPromptSubmit', cwd: g2bCwd, session_id: g2bSession, thread_id: 'g2b-prompt-thread', turn_id: 'g2b-prompt-turn', prompt: 'do not start $autopilot — café' };
1436
+ validateHookStdout('UserPromptSubmit', String(invoke(g2bCwd, g2bEnv, g2bPayload).stdout || ''));
1437
+ for (const [index, file] of g2bFiles.entries()) if (Buffer.compare(readFileSync(file), g2bBefore[index]!) !== 0) throw new Error(`packed G2b negated prompt mutated terminal state ${file}`);
1438
+ if (stopDecision(g2bCwd, g2bEnv, g2bPayload) === 'block') throw new Error('packed G2b negated prompt blocked Stop');
1439
+ for (const [index, file] of g2bFiles.entries()) if (Buffer.compare(readFileSync(file), g2bBefore[index]!) !== 0) throw new Error(`packed G2b negated prompt mutated terminal state after Stop ${file}`);
1440
+ }
1441
+
193
1442
  const globalNodeModules = resolveGlobalNodeModules(prefixDir);
194
1443
  const packageRoot = join(globalNodeModules, 'oh-my-codex');
195
1444
  const hookScript = join(packageRoot, 'dist', 'scripts', 'codex-native-hook.js');
@@ -211,11 +1460,579 @@ function smokeInstalledNativeHookDist(prefixDir: string): void {
211
1460
  });
212
1461
  validateHookStdout(eventName, result.stdout as string);
213
1462
  }
1463
+
1464
+ for (const [caseIndex, testCase] of PACKED_INSTALL_NATIVE_HOOK_REGRESSION_PROMPTS.entries()) {
1465
+ const caseCwd = join(smokeCwd, testCase.name);
1466
+ const sessionId = `packed-regression-${caseIndex}`;
1467
+ mkdirSync(caseCwd, { recursive: true });
1468
+ const environment = buildPackedRegressionEnvironment(testCase);
1469
+ const promptPayload = {
1470
+ hook_event_name: 'UserPromptSubmit',
1471
+ cwd: caseCwd,
1472
+ source: 'codex-app',
1473
+ session_id: sessionId,
1474
+ thread_id: `thread-${caseIndex}`,
1475
+ turn_id: `turn-${caseIndex}`,
1476
+ prompt: testCase.prompt,
1477
+ };
1478
+ const promptResult = run(process.execPath, [realpathSync(hookScript)], {
1479
+ cwd: caseCwd,
1480
+ env: environment,
1481
+ input: JSON.stringify(promptPayload),
1482
+ });
1483
+ validateHookStdout('UserPromptSubmit', promptResult.stdout as string);
1484
+ const skillStatePath = join(caseCwd, '.omx', 'state', 'sessions', sessionId, 'skill-active-state.json');
1485
+ if (testCase.expectedSkill === null) {
1486
+ if (existsSync(skillStatePath)) throw new Error(`packed regression ${testCase.name} created workflow state`);
1487
+ } else {
1488
+ if (!existsSync(skillStatePath)) throw new Error(`packed regression ${testCase.name} did not create workflow state`);
1489
+ const skillState = JSON.parse(readFileSync(skillStatePath, 'utf-8')) as { active?: boolean; skill?: string; deferred_skills?: string[]; active_skills?: Array<{ skill?: string }> };
1490
+ if (!skillState.active || skillState.skill !== testCase.expectedSkill) {
1491
+ throw new Error(`packed regression ${testCase.name} persisted unexpected workflow state`);
1492
+ }
1493
+ if ('expectedDeferredSkills' in testCase && JSON.stringify(skillState.deferred_skills ?? []) !== JSON.stringify(testCase.expectedDeferredSkills)) {
1494
+ throw new Error(`packed regression ${testCase.name} persisted unexpected deferred workflows`);
1495
+ }
1496
+ if ('expectedActiveSkills' in testCase && JSON.stringify(skillState.active_skills?.map((entry) => entry.skill) ?? []) !== JSON.stringify(testCase.expectedActiveSkills)) {
1497
+ throw new Error(`packed regression ${testCase.name} persisted unexpected active workflows`);
1498
+ }
1499
+ }
1500
+
1501
+ const stopResult = run(process.execPath, [realpathSync(hookScript)], {
1502
+ cwd: caseCwd,
1503
+ env: environment,
1504
+ input: JSON.stringify({ ...promptPayload, hook_event_name: 'Stop', turn_id: `stop-${caseIndex}` }),
1505
+ });
1506
+ const stopOutput = JSON.parse(String(stopResult.stdout || '{}')) as { decision?: string };
1507
+ if (testCase.expectedStopBlock && stopOutput.decision !== 'block') {
1508
+ throw new Error(`packed regression ${testCase.name} did not block Stop`);
1509
+ }
1510
+ if (!testCase.expectedStopBlock && stopOutput.decision === 'block') {
1511
+ throw new Error(`packed regression ${testCase.name} blocked Stop: ${JSON.stringify(stopOutput)}`);
1512
+ }
1513
+ }
1514
+ runPackedTransportRegressions(hookScript, smokeCwd);
214
1515
  } finally {
215
1516
  rmSync(smokeCwd, { recursive: true, force: true });
216
1517
  }
217
1518
  }
218
1519
 
1520
+ function sameJson(left: unknown, right: unknown): boolean {
1521
+ return JSON.stringify(left) === JSON.stringify(right);
1522
+ }
1523
+
1524
+ function isolatedSmokeEnv(home: string, codexHome: string): NodeJS.ProcessEnv {
1525
+ const env: NodeJS.ProcessEnv = { ...process.env, HOME: home, CODEX_HOME: codexHome };
1526
+ for (const key of [
1527
+ 'OMX_SESSION_ID',
1528
+ 'OMX_RUN_ID',
1529
+ 'OMX_ROOT',
1530
+ 'OMX_STATE_ROOT',
1531
+ 'OMX_ACTIVE_SESSION_PID',
1532
+ 'CODEX_SESSION_ID',
1533
+ 'TMUX',
1534
+ 'TMUX_PANE',
1535
+ ]) {
1536
+ delete env[key];
1537
+ }
1538
+ return env;
1539
+ }
1540
+
1541
+ function trustedProjectConfig(projectDir: string): string {
1542
+ const escapedProjectDir = projectDir.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
1543
+ return `[projects."${escapedProjectDir}"]\ntrust_level = "trusted"\n`;
1544
+ }
1545
+
1546
+
1547
+ function rawManagedHookCount(hooksContent: string): number {
1548
+ const parsed = JSON.parse(hooksContent) as { hooks?: Record<string, unknown> };
1549
+ if (!isRecord(parsed.hooks)) throw new Error('hooks.json is missing hooks object');
1550
+ return Object.values(parsed.hooks).flatMap((entries) => Array.isArray(entries) ? entries : [])
1551
+ .flatMap((entry) => isRecord(entry) && Array.isArray(entry.hooks) ? entry.hooks : [])
1552
+ .filter((hook) => isRecord(hook)
1553
+ && typeof hook.command === 'string'
1554
+ && isManagedCodexHookCommand(hook.command)).length;
1555
+ }
1556
+
1557
+ function assertNoEmptyManagedEventGroups(hooksContent: string): void {
1558
+ const parsed = JSON.parse(hooksContent) as { hooks?: Record<string, unknown> };
1559
+ if (!isRecord(parsed.hooks)) throw new Error('hooks.json is missing hooks object');
1560
+ for (const event of MANAGED_CODEX_HOOK_EVENTS) {
1561
+ const entries = parsed.hooks[event];
1562
+ if (!Array.isArray(entries)) continue;
1563
+ entries.forEach((entry, groupIndex) => {
1564
+ if (isRecord(entry) && (!Array.isArray(entry.hooks) || entry.hooks.length === 0)) {
1565
+ throw new Error(`packed uninstall left an empty ${event} group at coordinate ${groupIndex}`);
1566
+ }
1567
+ });
1568
+ }
1569
+ }
1570
+
1571
+ function assertNoSetupOwnedTrustKeys(configToml: string, setupTrust: Record<string, string>): void {
1572
+ const parsed = TOML.parse(configToml) as { hooks?: { state?: Record<string, unknown> } };
1573
+ const state = parsed.hooks?.state;
1574
+ if (!isRecord(state)) return;
1575
+ const retained = Object.keys(setupTrust).filter((key) => Object.hasOwn(state, key));
1576
+ if (retained.length > 0) {
1577
+ throw new Error(`packed uninstall retained setup-owned OMX hook trust keys: ${retained.join(', ')}`);
1578
+ }
1579
+ }
1580
+
1581
+ function assertNativeHooksEnabledForForeignGroups(configToml: string): void {
1582
+ const parsed = TOML.parse(configToml) as { features?: Record<string, unknown> };
1583
+ const features = parsed.features;
1584
+ if (!isRecord(features) || (features.hooks !== true && features.codex_hooks !== true)) {
1585
+ throw new Error('packed uninstall disabled native hooks despite preserved foreign hook groups');
1586
+ }
1587
+ }
1588
+
1589
+ function assertNoUninstallTransactionArtifacts(codexDir: string): void {
1590
+ const artifacts = readdirSync(codexDir).filter((entry) => entry.includes('.omx-uninstall-'));
1591
+ if (artifacts.length > 0) {
1592
+ throw new Error(`unsafe packed uninstall left replacement, staged, or tombstone paths: ${artifacts.join(', ')}`);
1593
+ }
1594
+ }
1595
+
1596
+ function preToolUseGroupIndices(hooksContent: string, marker: string): {
1597
+ foreignIndices: number[];
1598
+ managedIndices: number[];
1599
+ } {
1600
+ const parsed = JSON.parse(hooksContent) as { hooks?: Record<string, unknown> };
1601
+ if (!isRecord(parsed.hooks)) throw new Error('hooks.json is missing hooks object');
1602
+ const entries = parsed.hooks.PreToolUse;
1603
+ if (!Array.isArray(entries)) throw new Error('hooks.PreToolUse is missing');
1604
+ const foreignIndices: number[] = [];
1605
+ const managedIndices: number[] = [];
1606
+ entries.forEach((entry, index) => {
1607
+ if (!isRecord(entry) || !Array.isArray(entry.hooks)) return;
1608
+ if (entry.hooks.some((hook) => isRecord(hook)
1609
+ && typeof hook.command === 'string'
1610
+ && hook.command.includes(marker))) {
1611
+ foreignIndices.push(index);
1612
+ }
1613
+ if (entry.hooks.some((hook) => isRecord(hook)
1614
+ && typeof hook.command === 'string'
1615
+ && isManagedCodexHookCommand(hook.command))) {
1616
+ managedIndices.push(index);
1617
+ }
1618
+ });
1619
+ return { foreignIndices, managedIndices };
1620
+ }
1621
+
1622
+ function assertManagedHooksAppendAfterForeign(hooksContent: string, marker: string): void {
1623
+ const { foreignIndices, managedIndices } = preToolUseGroupIndices(hooksContent, marker);
1624
+ if (
1625
+ foreignIndices.length !== 2 ||
1626
+ managedIndices.length !== 1 ||
1627
+ managedIndices[0]! <= Math.max(...foreignIndices)
1628
+ ) {
1629
+ throw new Error('packed first install did not append the OMX PreToolUse group after both foreign groups');
1630
+ }
1631
+ }
1632
+
1633
+ function assertManagedHooksRemainBeforeForeign(hooksContent: string, marker: string): void {
1634
+ const { foreignIndices, managedIndices } = preToolUseGroupIndices(hooksContent, marker);
1635
+ if (
1636
+ foreignIndices.length !== 2 ||
1637
+ managedIndices.length !== 1 ||
1638
+ managedIndices[0]! >= Math.min(...foreignIndices)
1639
+ ) {
1640
+ throw new Error('packed setup rerun reordered the managed-first PreToolUse topology');
1641
+ }
1642
+ }
1643
+
1644
+
1645
+
1646
+ function assertTrustedManagedHooks(hooks: readonly CodexHookMetadata[]): void {
1647
+ for (const [event, hook] of Object.entries(managedCodexHooksByEvent(hooks))) {
1648
+ if (hook.trustStatus !== 'trusted') {
1649
+ throw new Error(`Codex did not trust OMX ${event}; received ${hook.trustStatus}`);
1650
+ }
1651
+ }
1652
+ }
1653
+
1654
+ function assertManagedHooksNotAlreadyTrusted(hooks: readonly CodexHookMetadata[]): void {
1655
+ for (const [event, hook] of Object.entries(managedCodexHooksByEvent(hooks))) {
1656
+ if (hook.trustStatus === 'trusted') {
1657
+ throw new Error(`setup-generated project trust unexpectedly pre-approved OMX ${event}`);
1658
+ }
1659
+ }
1660
+ }
1661
+ function preseededForeignHookMetadata(
1662
+ hooks: readonly CodexHookMetadata[],
1663
+ marker: string,
1664
+ ): Array<Pick<CodexHookMetadata, 'event' | 'command' | 'key' | 'currentHash' | 'displayOrder' | 'trustStatus'>> {
1665
+ return hookMetadataSnapshot(
1666
+ hooks.filter((hook) => hook.command.includes(marker) && !hook.command.includes('-inserted.js')),
1667
+ );
1668
+ }
1669
+
1670
+ function assertPreseededForeignHookEvidence(
1671
+ expected: readonly Pick<CodexHookMetadata, 'event' | 'command' | 'key' | 'currentHash' | 'displayOrder' | 'trustStatus'>[],
1672
+ actualHooks: readonly CodexHookMetadata[],
1673
+ codexUserConfig: string,
1674
+ marker: string,
1675
+ phase: string,
1676
+ ): void {
1677
+ const actual = preseededForeignHookMetadata(actualHooks, marker);
1678
+ if (actual.length !== 2) {
1679
+ throw new Error(`Codex ${phase} did not report exactly two pre-seeded foreign hooks; received ${actual.length}`);
1680
+ }
1681
+ if (!sameJson(actual, expected)) {
1682
+ const changes = expected.flatMap((expectedHook, index) =>
1683
+ Object.entries(expectedHook).flatMap(([field, expectedValue]) => {
1684
+ const actualValue = actual[index]?.[field as keyof typeof expectedHook];
1685
+ return actualValue === expectedValue
1686
+ ? []
1687
+ : [`${expectedHook.event}.${field}: ${JSON.stringify(expectedValue)} -> ${JSON.stringify(actualValue)}`];
1688
+ }));
1689
+ throw new Error(
1690
+ `Codex ${phase} changed pre-seeded foreign hook key, hash, coordinate, or display order: ${changes.join('; ')}`,
1691
+ );
1692
+ }
1693
+ if (actual.some((hook) => hook.trustStatus !== 'trusted')) {
1694
+ throw new Error(`Codex ${phase} did not retain trusted status for every pre-seeded foreign hook`);
1695
+ }
1696
+ const trust = generatedHookTrustState(codexUserConfig);
1697
+ for (const hook of actual) {
1698
+ if (trust[hook.key] !== hook.currentHash) {
1699
+ throw new Error(`Codex ${phase} did not retain the foreign trust entry for ${hook.key}`);
1700
+ }
1701
+ }
1702
+ }
1703
+
1704
+ async function observeInstalledCodexHooks(
1705
+ projectDir: string,
1706
+ hooksPath: string,
1707
+ env: NodeJS.ProcessEnv,
1708
+ ): Promise<CodexHooksListEntry> {
1709
+ const server = await CodexAppServer.start({ cwd: projectDir, env });
1710
+ try {
1711
+ await initializeCodexAppServer(server, 'omx-packed-install-smoke');
1712
+ return await listCodexHooks(server, projectDir, hooksPath);
1713
+ } finally {
1714
+ await server.close();
1715
+ }
1716
+ }
1717
+
1718
+ export interface PackedHookTrustLifecycleResult {
1719
+ codexVersion: string | null;
1720
+ }
1721
+
1722
+ /**
1723
+ * Exercise setup and removal against the installed package. The deterministic
1724
+ * filesystem lifecycle always runs; the installed-Codex trust leg is skipped
1725
+ * only when the `codex` executable is absent.
1726
+ */
1727
+ export async function smokePackedHookTrustLifecycle(
1728
+ omxPath: string,
1729
+ ): Promise<PackedHookTrustLifecycleResult> {
1730
+ const lifecycleRoot = mkdtempSync(join(tmpdir(), 'omx-packed-hook-trust-'));
1731
+ const projectDir = resolve(lifecycleRoot, 'project');
1732
+ const home = join(lifecycleRoot, 'home');
1733
+ const codexHome = join(lifecycleRoot, 'codex-home');
1734
+ const hooksPath = join(projectDir, '.codex', 'hooks.json');
1735
+ const configPath = join(projectDir, '.codex', 'config.toml');
1736
+ const marker = 'omx-packed-foreign';
1737
+ const agentsPath = join(projectDir, 'AGENTS.md');
1738
+ const foreignAgentsContent = '# User project instructions\n\nPreserve this packed lifecycle guidance.\n';
1739
+ const env = isolatedSmokeEnv(home, codexHome);
1740
+
1741
+ try {
1742
+ mkdirSync(projectDir, { recursive: true });
1743
+ mkdirSync(home, { recursive: true });
1744
+ mkdirSync(codexHome, { recursive: true });
1745
+ mkdirSync(join(projectDir, '.codex'), { recursive: true });
1746
+ writeFileSync(join(codexHome, 'config.toml'), trustedProjectConfig(projectDir), 'utf-8');
1747
+ writeFileSync(agentsPath, foreignAgentsContent, 'utf-8');
1748
+
1749
+ // Pre-seed foreign groups so uninstall may safely remove OMX's appended suffix.
1750
+ writeFileSync(
1751
+ hooksPath,
1752
+ appendDisplayOrderStableForeignHookGroups('{"hooks":{}}', marker, { appendGroups: true }),
1753
+ 'utf-8',
1754
+ );
1755
+ const preSetupForeignSnapshot = foreignHookGroupSnapshot(readFileSync(hooksPath, 'utf-8'), marker);
1756
+ if (preSetupForeignSnapshot.length !== 2) {
1757
+ throw new Error(`expected two pre-seeded foreign hook coordinates, received ${preSetupForeignSnapshot.length}`);
1758
+ }
1759
+
1760
+ let codexVersion: string | null;
1761
+ try {
1762
+ codexVersion = probeCodexVersion(projectDir, env);
1763
+ } catch (error) {
1764
+ if (!(error instanceof CodexExecutableNotFoundError)) throw error;
1765
+ codexVersion = null;
1766
+ }
1767
+
1768
+ const codexUserConfigPath = join(codexHome, 'config.toml');
1769
+ let approvedPreSetupForeignMetadata: Array<Pick<CodexHookMetadata, 'event' | 'command' | 'key' | 'currentHash' | 'displayOrder' | 'trustStatus'>> | null = null;
1770
+ if (codexVersion !== null) {
1771
+ const server = await CodexAppServer.start({ cwd: projectDir, env });
1772
+ try {
1773
+ await initializeCodexAppServer(server, 'omx-packed-install-smoke');
1774
+ const preSetupCodexHooks = await listCodexHooks(server, projectDir, hooksPath);
1775
+ const preSetupForeignHooks = preSetupCodexHooks.hooks.filter((hook) =>
1776
+ hook.command.includes(marker) && !hook.command.includes('-inserted.js'));
1777
+ if (preSetupForeignHooks.length !== 2) {
1778
+ throw new Error(`Codex hooks/list did not report exactly two pre-seeded foreign hooks; received ${preSetupForeignHooks.length}`);
1779
+ }
1780
+ if (preSetupForeignHooks.some((hook) => hook.trustStatus === 'trusted')) {
1781
+ throw new Error('isolated Codex approval state unexpectedly pre-approved a foreign hook before setup');
1782
+ }
1783
+ const approval = await server.request<unknown>(
1784
+ createCodexBatchWriteEnvelope(Object.fromEntries(
1785
+ preSetupForeignHooks.map((hook) => [hook.key, hook.currentHash]),
1786
+ )),
1787
+ CODEX_APP_SERVER_TIMEOUTS.requestMs,
1788
+ );
1789
+ assertCodexBatchWriteResult(approval, codexUserConfigPath);
1790
+ } finally {
1791
+ await server.close();
1792
+ }
1793
+ const approved = await observeInstalledCodexHooks(projectDir, hooksPath, env);
1794
+ approvedPreSetupForeignMetadata = preseededForeignHookMetadata(approved.hooks, marker);
1795
+ assertPreseededForeignHookEvidence(
1796
+ approvedPreSetupForeignMetadata,
1797
+ approved.hooks,
1798
+ readFileSync(codexUserConfigPath, 'utf-8'),
1799
+ marker,
1800
+ 'pre-setup approval',
1801
+ );
1802
+ }
1803
+
1804
+ const setupResult = run(omxPath, ['setup', '--scope', 'project', '--merge-agents', '--legacy'], {
1805
+ cwd: projectDir,
1806
+ env,
1807
+ });
1808
+ if (!existsSync(agentsPath) || !readFileSync(agentsPath, 'utf-8').includes(foreignAgentsContent.trim())) {
1809
+ throw new Error(
1810
+ `packed setup --merge-agents did not preserve pre-existing user AGENTS.md guidance; stdout=${JSON.stringify(String(setupResult.stdout || ''))} stderr=${JSON.stringify(String(setupResult.stderr || ''))}`,
1811
+ );
1812
+ }
1813
+
1814
+ const initialHooksContent = readFileSync(hooksPath, 'utf-8');
1815
+ assertForeignHookGroupsPreserved(preSetupForeignSnapshot, initialHooksContent, marker);
1816
+ if (rawManagedHookCount(initialHooksContent) !== MANAGED_CODEX_HOOK_EVENTS.length) {
1817
+ throw new Error('packed setup did not install exactly seven managed native hooks');
1818
+ }
1819
+ assertManagedHooksAppendAfterForeign(initialHooksContent, marker);
1820
+ const generatedTrust = generatedHookTrustState(readFileSync(configPath, 'utf-8'));
1821
+
1822
+ let postForeignCodexHooks: CodexHookMetadata[] | null = null;
1823
+ if (codexVersion !== null) {
1824
+ const server = await CodexAppServer.start({ cwd: projectDir, env });
1825
+ try {
1826
+ await initializeCodexAppServer(server, 'omx-packed-install-smoke');
1827
+ const initialCodexHooks = await listCodexHooks(server, projectDir, hooksPath);
1828
+ assertGeneratedTrustMatchesCodex(generatedTrust, initialCodexHooks.hooks);
1829
+ assertManagedHooksNotAlreadyTrusted(initialCodexHooks.hooks);
1830
+ await approveManagedHooksInCodex(server, initialCodexHooks.hooks, codexUserConfigPath);
1831
+ } finally {
1832
+ await server.close();
1833
+ }
1834
+ const afterSetup = await observeInstalledCodexHooks(projectDir, hooksPath, env);
1835
+ assertTrustedManagedHooks(afterSetup.hooks);
1836
+ assertPreseededForeignHookEvidence(
1837
+ approvedPreSetupForeignMetadata!,
1838
+ afterSetup.hooks,
1839
+ readFileSync(codexUserConfigPath, 'utf-8'),
1840
+ marker,
1841
+ 'setup',
1842
+ );
1843
+ }
1844
+
1845
+ writeFileSync(
1846
+ hooksPath,
1847
+ appendDisplayOrderStableForeignHookGroups(readFileSync(hooksPath, 'utf-8'), marker, { appendGroups: false }),
1848
+ 'utf-8',
1849
+ );
1850
+ const postForeignHooksContent = readFileSync(hooksPath, 'utf-8');
1851
+ const postForeignConfigContent = readFileSync(configPath, 'utf-8');
1852
+
1853
+ const foreignSnapshot = foreignHookGroupSnapshot(postForeignHooksContent, marker);
1854
+ if (foreignSnapshot.length !== 4) {
1855
+ throw new Error(`expected four foreign hook coordinates after insertion, received ${foreignSnapshot.length}`);
1856
+ }
1857
+ if (codexVersion !== null) {
1858
+ const inspected = await observeInstalledCodexHooks(projectDir, hooksPath, env);
1859
+ assertTrustedManagedHooks(inspected.hooks);
1860
+ postForeignCodexHooks = inspected.hooks;
1861
+ assertPreseededForeignHookEvidence(
1862
+ approvedPreSetupForeignMetadata!,
1863
+ inspected.hooks,
1864
+ readFileSync(codexUserConfigPath, 'utf-8'),
1865
+ marker,
1866
+ 'foreign insertion before setup rerun',
1867
+ );
1868
+ }
1869
+
1870
+ run(omxPath, ['setup', '--scope', 'project', '--merge-agents', '--legacy'], {
1871
+ cwd: projectDir,
1872
+ env,
1873
+ });
1874
+ const afterRerunHooksContent = readFileSync(hooksPath, 'utf-8');
1875
+ if (afterRerunHooksContent !== postForeignHooksContent) {
1876
+ throw new Error('packed setup rerun changed hooks.json after foreign insertion');
1877
+ }
1878
+ if (readFileSync(configPath, 'utf-8') !== postForeignConfigContent) {
1879
+ throw new Error('packed setup rerun changed config.toml after foreign insertion');
1880
+ }
1881
+
1882
+ assertForeignHookGroupsPreserved(foreignSnapshot, afterRerunHooksContent, marker);
1883
+ if (postForeignCodexHooks !== null) {
1884
+ const afterRerun = await observeInstalledCodexHooks(projectDir, hooksPath, env);
1885
+ assertTrustedManagedHooks(afterRerun.hooks);
1886
+ assertPreseededForeignHookEvidence(
1887
+ approvedPreSetupForeignMetadata!,
1888
+ afterRerun.hooks,
1889
+ readFileSync(codexUserConfigPath, 'utf-8'),
1890
+ marker,
1891
+ 'setup rerun',
1892
+ );
1893
+ if (!sameJson(hookMetadataSnapshot(afterRerun.hooks), hookMetadataSnapshot(postForeignCodexHooks))) {
1894
+ throw new Error('packed setup rerun changed Codex hook metadata or display order');
1895
+ }
1896
+ }
1897
+
1898
+ run(omxPath, ['setup', '--scope', 'project', '--merge-agents', '--legacy'], {
1899
+ cwd: projectDir,
1900
+ env,
1901
+ });
1902
+ const afterNoopHooksContent = readFileSync(hooksPath, 'utf-8');
1903
+ if (afterNoopHooksContent !== postForeignHooksContent) {
1904
+ throw new Error('third packed setup was not a hooks.json byte no-op');
1905
+ }
1906
+ if (readFileSync(configPath, 'utf-8') !== postForeignConfigContent) {
1907
+ throw new Error('third packed setup was not a config.toml byte no-op');
1908
+ }
1909
+ assertForeignHookGroupsPreserved(foreignSnapshot, afterNoopHooksContent, marker);
1910
+ if (postForeignCodexHooks !== null) {
1911
+ const afterNoop = await observeInstalledCodexHooks(projectDir, hooksPath, env);
1912
+ assertTrustedManagedHooks(afterNoop.hooks);
1913
+ assertPreseededForeignHookEvidence(
1914
+ approvedPreSetupForeignMetadata!,
1915
+ afterNoop.hooks,
1916
+ readFileSync(codexUserConfigPath, 'utf-8'),
1917
+ marker,
1918
+ 'third setup',
1919
+ );
1920
+ if (!sameJson(hookMetadataSnapshot(afterNoop.hooks), hookMetadataSnapshot(postForeignCodexHooks))) {
1921
+ throw new Error('third packed setup changed Codex hook metadata or display order');
1922
+ }
1923
+ }
1924
+ run(omxPath, ['doctor', '--verbose'], { cwd: projectDir, env });
1925
+ if (!existsSync(agentsPath) || !readFileSync(agentsPath, 'utf-8').includes(foreignAgentsContent.trim())) {
1926
+ throw new Error('packed doctor lifecycle did not preserve pre-existing user AGENTS.md guidance');
1927
+ }
1928
+
1929
+ run(omxPath, ['uninstall'], { cwd: projectDir, env });
1930
+ const afterUninstallHooksContent = readFileSync(hooksPath, 'utf-8');
1931
+ if (rawManagedHookCount(afterUninstallHooksContent) !== 0) {
1932
+ throw new Error('packed uninstall retained an OMX-managed native hook');
1933
+ }
1934
+ assertNoEmptyManagedEventGroups(afterUninstallHooksContent);
1935
+ assertForeignHookGroupsPreserved(foreignSnapshot, afterUninstallHooksContent, marker);
1936
+ if (!existsSync(agentsPath) || !readFileSync(agentsPath, 'utf-8').includes(foreignAgentsContent.trim())) {
1937
+ throw new Error('packed uninstall removed pre-existing user AGENTS.md guidance');
1938
+ }
1939
+ const afterUninstallConfig = readFileSync(configPath, 'utf-8');
1940
+ assertNoSetupOwnedTrustKeys(afterUninstallConfig, generatedTrust);
1941
+ assertNativeHooksEnabledForForeignGroups(afterUninstallConfig);
1942
+ if (postForeignCodexHooks !== null) {
1943
+ const afterUninstall = await observeInstalledCodexHooks(projectDir, hooksPath, env);
1944
+ const foreignIdentity = (hooks: readonly CodexHookMetadata[]) =>
1945
+ hookMetadataSnapshot(hooks.filter((hook) => hook.command.includes(marker)));
1946
+ const foreignBefore = foreignIdentity(postForeignCodexHooks);
1947
+ const foreignAfter = foreignIdentity(afterUninstall.hooks);
1948
+ if (!sameJson(foreignAfter, foreignBefore)) {
1949
+ throw new Error('packed uninstall changed foreign Codex hook metadata or display order');
1950
+ }
1951
+ }
1952
+
1953
+ const managedFirstProjectDir = resolve(lifecycleRoot, 'managed-first-project');
1954
+ const managedFirstHooksPath = join(managedFirstProjectDir, '.codex', 'hooks.json');
1955
+ const managedFirstConfigPath = join(managedFirstProjectDir, '.codex', 'config.toml');
1956
+ const managedFirstMarker = 'omx-packed-managed-first-foreign';
1957
+ mkdirSync(join(managedFirstProjectDir, '.codex'), { recursive: true });
1958
+ run(omxPath, ['setup', '--scope', 'project', '--merge-agents', '--legacy'], {
1959
+ cwd: managedFirstProjectDir,
1960
+ env,
1961
+ });
1962
+ writeFileSync(
1963
+ managedFirstHooksPath,
1964
+ appendDisplayOrderStableForeignHookGroups(
1965
+ readFileSync(managedFirstHooksPath, 'utf-8'),
1966
+ managedFirstMarker,
1967
+ { appendGroups: true },
1968
+ ),
1969
+ 'utf-8',
1970
+ );
1971
+ const managedFirstBeforeRerunHooks = readFileSync(managedFirstHooksPath, 'utf-8');
1972
+ const managedFirstBeforeRerunConfig = readFileSync(managedFirstConfigPath, 'utf-8');
1973
+ const managedFirstForeignSnapshot = foreignHookGroupSnapshot(managedFirstBeforeRerunHooks, managedFirstMarker);
1974
+ if (managedFirstForeignSnapshot.length !== 2) {
1975
+ throw new Error('packed managed-first fixture did not append both foreign hook groups');
1976
+ }
1977
+ assertManagedHooksRemainBeforeForeign(managedFirstBeforeRerunHooks, managedFirstMarker);
1978
+
1979
+ run(omxPath, ['setup', '--scope', 'project', '--merge-agents', '--legacy'], {
1980
+ cwd: managedFirstProjectDir,
1981
+ env,
1982
+ });
1983
+ if (readFileSync(managedFirstHooksPath, 'utf-8') !== managedFirstBeforeRerunHooks) {
1984
+ throw new Error('packed setup rerun reordered managed-first foreign hook groups');
1985
+ }
1986
+ if (readFileSync(managedFirstConfigPath, 'utf-8') !== managedFirstBeforeRerunConfig) {
1987
+ throw new Error('packed setup rerun changed managed-first config.toml');
1988
+ }
1989
+ assertForeignHookGroupsPreserved(
1990
+ managedFirstForeignSnapshot,
1991
+ readFileSync(managedFirstHooksPath, 'utf-8'),
1992
+ managedFirstMarker,
1993
+ );
1994
+ const managedFirstBeforeUninstallHooksBytes = readFileSync(managedFirstHooksPath);
1995
+ const managedFirstBeforeUninstallConfigBytes = readFileSync(managedFirstConfigPath);
1996
+
1997
+ const expectedUnsafeManagedRemovalDiagnostic =
1998
+ 'Removing OMX hooks would shift a foreign coordinate or discard opaque metadata.';
1999
+ const unsafeRemoval = planManagedCodexHooksRemoval(managedFirstBeforeRerunHooks, managedFirstHooksPath);
2000
+ if (
2001
+ unsafeRemoval.ok ||
2002
+ unsafeRemoval.error.code !== 'unsafe_managed_removal' ||
2003
+ unsafeRemoval.error.message !== expectedUnsafeManagedRemovalDiagnostic
2004
+ ) {
2005
+ throw new Error('packed managed-first fixture did not return the exact unsafe_managed_removal diagnostic');
2006
+ }
2007
+ const failedUninstall = spawnSync(omxPath, ['uninstall'], {
2008
+ cwd: managedFirstProjectDir,
2009
+ env,
2010
+ encoding: 'utf-8',
2011
+ stdio: 'pipe',
2012
+ });
2013
+ if (failedUninstall.error) throw failedUninstall.error;
2014
+ if (failedUninstall.status !== 1) {
2015
+ throw new Error(
2016
+ `packed unsafe uninstall exited ${String(failedUninstall.status)}, expected 1\nstdout:\n${failedUninstall.stdout || ''}\nstderr:\n${failedUninstall.stderr || ''}`,
2017
+ );
2018
+ }
2019
+ if (failedUninstall.stderr !== `Error: ${expectedUnsafeManagedRemovalDiagnostic}\n`) {
2020
+ throw new Error(`packed unsafe uninstall returned an unexpected diagnostic: ${failedUninstall.stderr || ''}`);
2021
+ }
2022
+ if (!readFileSync(managedFirstHooksPath).equals(managedFirstBeforeUninstallHooksBytes)) {
2023
+ throw new Error('unsafe packed uninstall changed raw hooks.json bytes');
2024
+ }
2025
+ if (!readFileSync(managedFirstConfigPath).equals(managedFirstBeforeUninstallConfigBytes)) {
2026
+ throw new Error('unsafe packed uninstall changed raw config.toml bytes');
2027
+ }
2028
+ assertNoUninstallTransactionArtifacts(join(managedFirstProjectDir, '.codex'));
2029
+
2030
+ return { codexVersion };
2031
+ } finally {
2032
+ rmSync(lifecycleRoot, { recursive: true, force: true });
2033
+ }
2034
+ }
2035
+
219
2036
  export function parseNpmPackJsonOutput(stdout: string): Array<{ filename: string }> {
220
2037
  const start = stdout.lastIndexOf('\n[');
221
2038
  const jsonText = (start >= 0 ? stdout.slice(start + 1) : stdout).trim();
@@ -252,6 +2069,12 @@ async function main(): Promise<void> {
252
2069
  run(omxPath, argv, { cwd: repoRoot });
253
2070
  }
254
2071
  smokeInstalledNativeHookDist(prefixDir);
2072
+ const lifecycle = await smokePackedHookTrustLifecycle(omxPath);
2073
+ console.log(
2074
+ lifecycle.codexVersion !== null
2075
+ ? `packed install smoke: installed Codex 0.142.5 lifecycle passed (${lifecycle.codexVersion})`
2076
+ : 'packed install smoke: Codex executable absent; installed-Codex trust leg skipped after deterministic lifecycle',
2077
+ );
255
2078
 
256
2079
  console.log('packed install smoke: PASS');
257
2080
  } finally {