oh-my-codex 0.20.1 → 0.20.3

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 (791) hide show
  1. package/Cargo.lock +7 -6
  2. package/Cargo.toml +1 -1
  3. package/README.md +36 -1
  4. package/crates/omx-runtime/Cargo.toml +1 -0
  5. package/crates/omx-runtime/src/main.rs +25 -2
  6. package/crates/omx-runtime/tests/execution.rs +219 -0
  7. package/crates/omx-runtime-core/src/dispatch.rs +96 -10
  8. package/crates/omx-runtime-core/src/engine.rs +839 -29
  9. package/crates/omx-runtime-core/src/lib.rs +8 -0
  10. package/dist/adapt/__tests__/hermes.test.js.map +1 -1
  11. package/dist/adapt/contracts.d.ts.map +1 -1
  12. package/dist/agents/__tests__/definitions.test.js +81 -0
  13. package/dist/agents/__tests__/definitions.test.js.map +1 -1
  14. package/dist/agents/__tests__/native-config.test.js +25 -0
  15. package/dist/agents/__tests__/native-config.test.js.map +1 -1
  16. package/dist/agents/native-config.d.ts +2 -1
  17. package/dist/agents/native-config.d.ts.map +1 -1
  18. package/dist/agents/native-config.js.map +1 -1
  19. package/dist/auth/__tests__/hotswap.test.d.ts +2 -0
  20. package/dist/auth/__tests__/hotswap.test.d.ts.map +1 -0
  21. package/dist/auth/__tests__/hotswap.test.js +183 -0
  22. package/dist/auth/__tests__/hotswap.test.js.map +1 -0
  23. package/dist/auth/hotswap.d.ts +6 -0
  24. package/dist/auth/hotswap.d.ts.map +1 -1
  25. package/dist/auth/hotswap.js +102 -19
  26. package/dist/auth/hotswap.js.map +1 -1
  27. package/dist/auth/paths.js.map +1 -1
  28. package/dist/auth/storage.js.map +1 -1
  29. package/dist/autopilot/__tests__/ralplan-gate.test.js +5 -5
  30. package/dist/autopilot/__tests__/ralplan-gate.test.js.map +1 -1
  31. package/dist/autopilot/completion-gate.js.map +1 -1
  32. package/dist/autopilot/fsm.d.ts +1 -1
  33. package/dist/autopilot/fsm.d.ts.map +1 -1
  34. package/dist/autoresearch/goal.js.map +1 -1
  35. package/dist/autoresearch/runtime.js.map +1 -1
  36. package/dist/capabilities/lockfile.js.map +1 -1
  37. package/dist/catalog/reader.js.map +1 -1
  38. package/dist/catalog/skill-mirror.js.map +1 -1
  39. package/dist/cli/__tests__/adapt-help.test.js.map +1 -1
  40. package/dist/cli/__tests__/agents-init.test.js.map +1 -1
  41. package/dist/cli/__tests__/agents.test.js.map +1 -1
  42. package/dist/cli/__tests__/api.test.js.map +1 -1
  43. package/dist/cli/__tests__/ask.test.js.map +1 -1
  44. package/dist/cli/__tests__/auth.test.js +60 -6
  45. package/dist/cli/__tests__/auth.test.js.map +1 -1
  46. package/dist/cli/__tests__/autoresearch-guided.test.js.map +1 -1
  47. package/dist/cli/__tests__/autoresearch.test.js.map +1 -1
  48. package/dist/cli/__tests__/capabilities.test.js.map +1 -1
  49. package/dist/cli/__tests__/codex-plugin-layout.test.js +80 -0
  50. package/dist/cli/__tests__/codex-plugin-layout.test.js.map +1 -1
  51. package/dist/cli/__tests__/doctor-context-window-warning.test.js.map +1 -1
  52. package/dist/cli/__tests__/doctor-invalid-config.test.js +47 -4
  53. package/dist/cli/__tests__/doctor-invalid-config.test.js.map +1 -1
  54. package/dist/cli/__tests__/doctor-team.test.js.map +1 -1
  55. package/dist/cli/__tests__/doctor-warning-copy.test.js +752 -15
  56. package/dist/cli/__tests__/doctor-warning-copy.test.js.map +1 -1
  57. package/dist/cli/__tests__/exec.test.js +136 -0
  58. package/dist/cli/__tests__/exec.test.js.map +1 -1
  59. package/dist/cli/__tests__/imagegen-continuation.test.js.map +1 -1
  60. package/dist/cli/__tests__/index.test.js +473 -14
  61. package/dist/cli/__tests__/index.test.js.map +1 -1
  62. package/dist/cli/__tests__/launch-fallback.test.js +336 -4
  63. package/dist/cli/__tests__/launch-fallback.test.js.map +1 -1
  64. package/dist/cli/__tests__/mcp-parity.test.js +87 -12
  65. package/dist/cli/__tests__/mcp-parity.test.js.map +1 -1
  66. package/dist/cli/__tests__/native-hook-claim-journal.test.d.ts +2 -0
  67. package/dist/cli/__tests__/native-hook-claim-journal.test.d.ts.map +1 -0
  68. package/dist/cli/__tests__/native-hook-claim-journal.test.js +206 -0
  69. package/dist/cli/__tests__/native-hook-claim-journal.test.js.map +1 -0
  70. package/dist/cli/__tests__/nested-help-routing.test.js.map +1 -1
  71. package/dist/cli/__tests__/question.test.js.map +1 -1
  72. package/dist/cli/__tests__/ralph-deslop-contract.test.js.map +1 -1
  73. package/dist/cli/__tests__/ralph-goal-mode-contract.test.js +6 -0
  74. package/dist/cli/__tests__/ralph-goal-mode-contract.test.js.map +1 -1
  75. package/dist/cli/__tests__/ralph-prd-deep-interview.test.js.map +1 -1
  76. package/dist/cli/__tests__/ralph-prd-smoke.test.js.map +1 -1
  77. package/dist/cli/__tests__/ralph.test.js +8 -3
  78. package/dist/cli/__tests__/ralph.test.js.map +1 -1
  79. package/dist/cli/__tests__/ralplan-bootstrap-3181.test.d.ts +2 -0
  80. package/dist/cli/__tests__/ralplan-bootstrap-3181.test.d.ts.map +1 -0
  81. package/dist/cli/__tests__/ralplan-bootstrap-3181.test.js +72 -0
  82. package/dist/cli/__tests__/ralplan-bootstrap-3181.test.js.map +1 -0
  83. package/dist/cli/__tests__/ralplan.test.d.ts +2 -0
  84. package/dist/cli/__tests__/ralplan.test.d.ts.map +1 -0
  85. package/dist/cli/__tests__/ralplan.test.js +55 -0
  86. package/dist/cli/__tests__/ralplan.test.js.map +1 -0
  87. package/dist/cli/__tests__/resume.test.js +17 -11
  88. package/dist/cli/__tests__/resume.test.js.map +1 -1
  89. package/dist/cli/__tests__/session-scoped-runtime.test.js +25 -0
  90. package/dist/cli/__tests__/session-scoped-runtime.test.js.map +1 -1
  91. package/dist/cli/__tests__/session-search-help.test.js +25 -1
  92. package/dist/cli/__tests__/session-search-help.test.js.map +1 -1
  93. package/dist/cli/__tests__/session-search.test.js.map +1 -1
  94. package/dist/cli/__tests__/setup-agents-overwrite.test.js +79 -0
  95. package/dist/cli/__tests__/setup-agents-overwrite.test.js.map +1 -1
  96. package/dist/cli/__tests__/setup-gh-star.test.js.map +1 -1
  97. package/dist/cli/__tests__/setup-hooks-shared-ownership.test.js +77 -3
  98. package/dist/cli/__tests__/setup-hooks-shared-ownership.test.js.map +1 -1
  99. package/dist/cli/__tests__/setup-hooks-trust-e2e.test.d.ts +2 -0
  100. package/dist/cli/__tests__/setup-hooks-trust-e2e.test.d.ts.map +1 -0
  101. package/dist/cli/__tests__/setup-hooks-trust-e2e.test.js +285 -0
  102. package/dist/cli/__tests__/setup-hooks-trust-e2e.test.js.map +1 -0
  103. package/dist/cli/__tests__/setup-install-mode.test.js +2579 -294
  104. package/dist/cli/__tests__/setup-install-mode.test.js.map +1 -1
  105. package/dist/cli/__tests__/setup-preferences.test.d.ts +2 -0
  106. package/dist/cli/__tests__/setup-preferences.test.d.ts.map +1 -0
  107. package/dist/cli/__tests__/setup-preferences.test.js +79 -0
  108. package/dist/cli/__tests__/setup-preferences.test.js.map +1 -0
  109. package/dist/cli/__tests__/setup-refresh.test.js +33 -2
  110. package/dist/cli/__tests__/setup-refresh.test.js.map +1 -1
  111. package/dist/cli/__tests__/setup-scope.test.js +46 -3
  112. package/dist/cli/__tests__/setup-scope.test.js.map +1 -1
  113. package/dist/cli/__tests__/sparkshell-cli.test.js.map +1 -1
  114. package/dist/cli/__tests__/state.test.js +37 -0
  115. package/dist/cli/__tests__/state.test.js.map +1 -1
  116. package/dist/cli/__tests__/team.test.js +105 -7
  117. package/dist/cli/__tests__/team.test.js.map +1 -1
  118. package/dist/cli/__tests__/ultragoal.test.js +61 -1
  119. package/dist/cli/__tests__/ultragoal.test.js.map +1 -1
  120. package/dist/cli/__tests__/uninstall.test.js +1817 -24
  121. package/dist/cli/__tests__/uninstall.test.js.map +1 -1
  122. package/dist/cli/__tests__/update.test.js +58 -0
  123. package/dist/cli/__tests__/update.test.js.map +1 -1
  124. package/dist/cli/__tests__/url.test.js.map +1 -1
  125. package/dist/cli/__tests__/windows-popup-loop-contract.test.js +2 -1
  126. package/dist/cli/__tests__/windows-popup-loop-contract.test.js.map +1 -1
  127. package/dist/cli/adapt.js.map +1 -1
  128. package/dist/cli/agents-init.js.map +1 -1
  129. package/dist/cli/agents.js.map +1 -1
  130. package/dist/cli/api.js.map +1 -1
  131. package/dist/cli/ask.d.ts +1 -1
  132. package/dist/cli/ask.d.ts.map +1 -1
  133. package/dist/cli/ask.js.map +1 -1
  134. package/dist/cli/auth.js.map +1 -1
  135. package/dist/cli/autoresearch-guided.js.map +1 -1
  136. package/dist/cli/autoresearch-intake.js.map +1 -1
  137. package/dist/cli/capabilities.js.map +1 -1
  138. package/dist/cli/cleanup.js.map +1 -1
  139. package/dist/cli/codex-feature-probe.js.map +1 -1
  140. package/dist/cli/codex-home.js.map +1 -1
  141. package/dist/cli/doctor.d.ts +21 -0
  142. package/dist/cli/doctor.d.ts.map +1 -1
  143. package/dist/cli/doctor.js +607 -130
  144. package/dist/cli/doctor.js.map +1 -1
  145. package/dist/cli/explore.js.map +1 -1
  146. package/dist/cli/index.d.ts +14 -3
  147. package/dist/cli/index.d.ts.map +1 -1
  148. package/dist/cli/index.js +245 -57
  149. package/dist/cli/index.js.map +1 -1
  150. package/dist/cli/mcp-parity.js +2 -2
  151. package/dist/cli/mcp-parity.js.map +1 -1
  152. package/dist/cli/mcp-serve.js.map +1 -1
  153. package/dist/cli/mission.js.map +1 -1
  154. package/dist/cli/native-assets.js.map +1 -1
  155. package/dist/cli/native-hook-claim-journal.d.ts +29 -0
  156. package/dist/cli/native-hook-claim-journal.d.ts.map +1 -0
  157. package/dist/cli/native-hook-claim-journal.js +252 -0
  158. package/dist/cli/native-hook-claim-journal.js.map +1 -0
  159. package/dist/cli/omx.js.map +1 -1
  160. package/dist/cli/plugin-marketplace.js.map +1 -1
  161. package/dist/cli/project-runtime-codex-homes.js.map +1 -1
  162. package/dist/cli/question.js.map +1 -1
  163. package/dist/cli/ralph.d.ts.map +1 -1
  164. package/dist/cli/ralph.js +3 -2
  165. package/dist/cli/ralph.js.map +1 -1
  166. package/dist/cli/ralplan.d.ts +10 -0
  167. package/dist/cli/ralplan.d.ts.map +1 -0
  168. package/dist/cli/ralplan.js +110 -0
  169. package/dist/cli/ralplan.js.map +1 -0
  170. package/dist/cli/session-search.d.ts +5 -0
  171. package/dist/cli/session-search.d.ts.map +1 -1
  172. package/dist/cli/session-search.js +87 -0
  173. package/dist/cli/session-search.js.map +1 -1
  174. package/dist/cli/setup-preferences.d.ts +11 -0
  175. package/dist/cli/setup-preferences.d.ts.map +1 -1
  176. package/dist/cli/setup-preferences.js +32 -0
  177. package/dist/cli/setup-preferences.js.map +1 -1
  178. package/dist/cli/setup.d.ts +58 -0
  179. package/dist/cli/setup.d.ts.map +1 -1
  180. package/dist/cli/setup.js +1450 -398
  181. package/dist/cli/setup.js.map +1 -1
  182. package/dist/cli/sparkshell.js.map +1 -1
  183. package/dist/cli/star-prompt.js.map +1 -1
  184. package/dist/cli/state.d.ts.map +1 -1
  185. package/dist/cli/state.js +2 -1
  186. package/dist/cli/state.js.map +1 -1
  187. package/dist/cli/team.js.map +1 -1
  188. package/dist/cli/ultragoal.d.ts +5 -1
  189. package/dist/cli/ultragoal.d.ts.map +1 -1
  190. package/dist/cli/ultragoal.js +34 -2
  191. package/dist/cli/ultragoal.js.map +1 -1
  192. package/dist/cli/uninstall.d.ts +17 -0
  193. package/dist/cli/uninstall.d.ts.map +1 -1
  194. package/dist/cli/uninstall.js +986 -95
  195. package/dist/cli/uninstall.js.map +1 -1
  196. package/dist/cli/update.d.ts.map +1 -1
  197. package/dist/cli/update.js +8 -1
  198. package/dist/cli/update.js.map +1 -1
  199. package/dist/cli/url.js.map +1 -1
  200. package/dist/compat/__tests__/doctor-contract.test.js +5 -1
  201. package/dist/compat/__tests__/doctor-contract.test.js.map +1 -1
  202. package/dist/compat/__tests__/rust-runtime-compat.test.js +1 -1
  203. package/dist/compat/__tests__/rust-runtime-compat.test.js.map +1 -1
  204. package/dist/config/__tests__/codex-hooks.test.js +1287 -11
  205. package/dist/config/__tests__/codex-hooks.test.js.map +1 -1
  206. package/dist/config/__tests__/generator-idempotent.test.js +1179 -32
  207. package/dist/config/__tests__/generator-idempotent.test.js.map +1 -1
  208. package/dist/config/__tests__/generator-notify.test.js +6 -1
  209. package/dist/config/__tests__/generator-notify.test.js.map +1 -1
  210. package/dist/config/__tests__/models.test.js +67 -3
  211. package/dist/config/__tests__/models.test.js.map +1 -1
  212. package/dist/config/codex-feature-flags.d.ts.map +1 -1
  213. package/dist/config/codex-feature-flags.js.map +1 -1
  214. package/dist/config/codex-hooks.d.ts +124 -1
  215. package/dist/config/codex-hooks.d.ts.map +1 -1
  216. package/dist/config/codex-hooks.js +2142 -265
  217. package/dist/config/codex-hooks.js.map +1 -1
  218. package/dist/config/commit-lore-guard.js.map +1 -1
  219. package/dist/config/generator.d.ts +23 -34
  220. package/dist/config/generator.d.ts.map +1 -1
  221. package/dist/config/generator.js +1480 -246
  222. package/dist/config/generator.js.map +1 -1
  223. package/dist/config/mcp-registry.js.map +1 -1
  224. package/dist/config/models.d.ts +14 -4
  225. package/dist/config/models.d.ts.map +1 -1
  226. package/dist/config/models.js +15 -1
  227. package/dist/config/models.js.map +1 -1
  228. package/dist/config/omx-first-party-mcp.js.map +1 -1
  229. package/dist/config/team-mode.d.ts.map +1 -1
  230. package/dist/document-refresh/__tests__/enforcer.test.js.map +1 -1
  231. package/dist/document-refresh/enforcer.js.map +1 -1
  232. package/dist/exec/followup.js.map +1 -1
  233. package/dist/goal-workflows/artifacts.js.map +1 -1
  234. package/dist/hooks/__tests__/agents-overlay.test.js +7 -4
  235. package/dist/hooks/__tests__/agents-overlay.test.js.map +1 -1
  236. package/dist/hooks/__tests__/analyze-routing-contract.test.js.map +1 -1
  237. package/dist/hooks/__tests__/analyze-skill-contract.test.js.map +1 -1
  238. package/dist/hooks/__tests__/anti-slop-workflow.test.js.map +1 -1
  239. package/dist/hooks/__tests__/autopilot-skill-contract.test.js +9 -3
  240. package/dist/hooks/__tests__/autopilot-skill-contract.test.js.map +1 -1
  241. package/dist/hooks/__tests__/code-review-skill-contract.test.js.map +1 -1
  242. package/dist/hooks/__tests__/consensus-execution-handoff.test.js.map +1 -1
  243. package/dist/hooks/__tests__/deep-interview-contract.test.js.map +1 -1
  244. package/dist/hooks/__tests__/design-skill.test.js.map +1 -1
  245. package/dist/hooks/__tests__/keyword-detector.test.js +1376 -6
  246. package/dist/hooks/__tests__/keyword-detector.test.js.map +1 -1
  247. package/dist/hooks/__tests__/notify-fallback-watcher.test.js +252 -543
  248. package/dist/hooks/__tests__/notify-fallback-watcher.test.js.map +1 -1
  249. package/dist/hooks/__tests__/notify-hook-all-workers-idle.test.js +38 -4
  250. package/dist/hooks/__tests__/notify-hook-all-workers-idle.test.js.map +1 -1
  251. package/dist/hooks/__tests__/notify-hook-auto-nudge.test.js +765 -10
  252. package/dist/hooks/__tests__/notify-hook-auto-nudge.test.js.map +1 -1
  253. package/dist/hooks/__tests__/notify-hook-cross-worktree-heartbeat.test.js +2 -0
  254. package/dist/hooks/__tests__/notify-hook-cross-worktree-heartbeat.test.js.map +1 -1
  255. package/dist/hooks/__tests__/notify-hook-managed-tmux.test.js +142 -1
  256. package/dist/hooks/__tests__/notify-hook-managed-tmux.test.js.map +1 -1
  257. package/dist/hooks/__tests__/notify-hook-modules.test.js.map +1 -1
  258. package/dist/hooks/__tests__/notify-hook-non-omx-guard.test.js +48 -2
  259. package/dist/hooks/__tests__/notify-hook-non-omx-guard.test.js.map +1 -1
  260. package/dist/hooks/__tests__/notify-hook-ralph-resume.test.js +61 -44
  261. package/dist/hooks/__tests__/notify-hook-ralph-resume.test.js.map +1 -1
  262. package/dist/hooks/__tests__/notify-hook-regression-205.test.js.map +1 -1
  263. package/dist/hooks/__tests__/notify-hook-session-idle-dedupe.test.js +3 -2
  264. package/dist/hooks/__tests__/notify-hook-session-idle-dedupe.test.js.map +1 -1
  265. package/dist/hooks/__tests__/notify-hook-session-scope.test.js +243 -4
  266. package/dist/hooks/__tests__/notify-hook-session-scope.test.js.map +1 -1
  267. package/dist/hooks/__tests__/notify-hook-team-dispatch.test.js +531 -12
  268. package/dist/hooks/__tests__/notify-hook-team-dispatch.test.js.map +1 -1
  269. package/dist/hooks/__tests__/notify-hook-team-leader-nudge.test.js +163 -24
  270. package/dist/hooks/__tests__/notify-hook-team-leader-nudge.test.js.map +1 -1
  271. package/dist/hooks/__tests__/notify-hook-team-tmux-guard.test.js +482 -32
  272. package/dist/hooks/__tests__/notify-hook-team-tmux-guard.test.js.map +1 -1
  273. package/dist/hooks/__tests__/notify-hook-tmux-heal.test.js +68 -1
  274. package/dist/hooks/__tests__/notify-hook-tmux-heal.test.js.map +1 -1
  275. package/dist/hooks/__tests__/notify-hook-tmux-scrollback.test.js +7 -1
  276. package/dist/hooks/__tests__/notify-hook-tmux-scrollback.test.js.map +1 -1
  277. package/dist/hooks/__tests__/notify-hook-visual-verdict.test.js.map +1 -1
  278. package/dist/hooks/__tests__/notify-hook-worker-idle.test.js +43 -4
  279. package/dist/hooks/__tests__/notify-hook-worker-idle.test.js.map +1 -1
  280. package/dist/hooks/__tests__/openclaw-setup-contract.test.js.map +1 -1
  281. package/dist/hooks/__tests__/pre-context-gate-skills.test.js.map +1 -1
  282. package/dist/hooks/__tests__/prometheus-strict-contract.test.js.map +1 -1
  283. package/dist/hooks/__tests__/prompt-guidance-fragments.test.js.map +1 -1
  284. package/dist/hooks/__tests__/prompt-guidance-test-helpers.js.map +1 -1
  285. package/dist/hooks/__tests__/prompt-session-provenance.test.d.ts +2 -0
  286. package/dist/hooks/__tests__/prompt-session-provenance.test.d.ts.map +1 -0
  287. package/dist/hooks/__tests__/prompt-session-provenance.test.js +82 -0
  288. package/dist/hooks/__tests__/prompt-session-provenance.test.js.map +1 -0
  289. package/dist/hooks/__tests__/session.test.js +878 -11
  290. package/dist/hooks/__tests__/session.test.js.map +1 -1
  291. package/dist/hooks/__tests__/skill-catalog-hygiene.test.js.map +1 -1
  292. package/dist/hooks/__tests__/team-runtime-gating-docs-contract.test.js.map +1 -1
  293. package/dist/hooks/__tests__/visual-ralph-skill.test.js.map +1 -1
  294. package/dist/hooks/__tests__/visual-verdict-loop.test.js.map +1 -1
  295. package/dist/hooks/agents-overlay.js +5 -5
  296. package/dist/hooks/agents-overlay.js.map +1 -1
  297. package/dist/hooks/code-simplifier/index.js.map +1 -1
  298. package/dist/hooks/explore-routing.js.map +1 -1
  299. package/dist/hooks/extensibility/dispatcher.js.map +1 -1
  300. package/dist/hooks/extensibility/events.js.map +1 -1
  301. package/dist/hooks/extensibility/loader.js.map +1 -1
  302. package/dist/hooks/extensibility/plugin-runner-stdin.js.map +1 -1
  303. package/dist/hooks/extensibility/sdk/logging.js.map +1 -1
  304. package/dist/hooks/keyword-detector.d.ts +33 -7
  305. package/dist/hooks/keyword-detector.d.ts.map +1 -1
  306. package/dist/hooks/keyword-detector.js +2228 -119
  307. package/dist/hooks/keyword-detector.js.map +1 -1
  308. package/dist/hooks/keyword-registry.d.ts +12 -0
  309. package/dist/hooks/keyword-registry.d.ts.map +1 -1
  310. package/dist/hooks/keyword-registry.js +41 -0
  311. package/dist/hooks/keyword-registry.js.map +1 -1
  312. package/dist/hooks/prompt-session-provenance.d.ts +87 -0
  313. package/dist/hooks/prompt-session-provenance.d.ts.map +1 -0
  314. package/dist/hooks/prompt-session-provenance.js +180 -0
  315. package/dist/hooks/prompt-session-provenance.js.map +1 -0
  316. package/dist/hooks/session.d.ts +163 -34
  317. package/dist/hooks/session.d.ts.map +1 -1
  318. package/dist/hooks/session.js +1395 -317
  319. package/dist/hooks/session.js.map +1 -1
  320. package/dist/hooks/task-size-detector.js.map +1 -1
  321. package/dist/hud/__tests__/state.test.js.map +1 -1
  322. package/dist/hud/authority.js.map +1 -1
  323. package/dist/hud/constants.js.map +1 -1
  324. package/dist/hud/index.js.map +1 -1
  325. package/dist/hud/reconcile.js.map +1 -1
  326. package/dist/hud/render.js.map +1 -1
  327. package/dist/hud/state.js.map +1 -1
  328. package/dist/hud/tmux.js.map +1 -1
  329. package/dist/leader/__tests__/contract.test.js +153 -1
  330. package/dist/leader/__tests__/contract.test.js.map +1 -1
  331. package/dist/leader/contract.d.ts +42 -5
  332. package/dist/leader/contract.d.ts.map +1 -1
  333. package/dist/leader/contract.js +184 -3
  334. package/dist/leader/contract.js.map +1 -1
  335. package/dist/mcp/__tests__/bootstrap.test.js.map +1 -1
  336. package/dist/mcp/__tests__/code-intel-server.test.js +31 -3
  337. package/dist/mcp/__tests__/code-intel-server.test.js.map +1 -1
  338. package/dist/mcp/__tests__/server-lifecycle.test.js +15 -2
  339. package/dist/mcp/__tests__/server-lifecycle.test.js.map +1 -1
  340. package/dist/mcp/__tests__/state-paths.test.js +98 -1
  341. package/dist/mcp/__tests__/state-paths.test.js.map +1 -1
  342. package/dist/mcp/__tests__/wiki-server.test.js +18 -0
  343. package/dist/mcp/__tests__/wiki-server.test.js.map +1 -1
  344. package/dist/mcp/bootstrap.js.map +1 -1
  345. package/dist/mcp/code-intel-server.d.ts +58 -58
  346. package/dist/mcp/code-intel-server.d.ts.map +1 -1
  347. package/dist/mcp/code-intel-server.js +13 -3
  348. package/dist/mcp/code-intel-server.js.map +1 -1
  349. package/dist/mcp/hermes-bridge.js.map +1 -1
  350. package/dist/mcp/hermes-server.d.ts +70 -70
  351. package/dist/mcp/hermes-server.d.ts.map +1 -1
  352. package/dist/mcp/lifecycle-telemetry.js.map +1 -1
  353. package/dist/mcp/memory-server.d.ts +34 -34
  354. package/dist/mcp/memory-server.d.ts.map +1 -1
  355. package/dist/mcp/memory-server.js.map +1 -1
  356. package/dist/mcp/state-paths.d.ts +14 -0
  357. package/dist/mcp/state-paths.d.ts.map +1 -1
  358. package/dist/mcp/state-paths.js +96 -20
  359. package/dist/mcp/state-paths.js.map +1 -1
  360. package/dist/mcp/state-server.d.ts +47 -47
  361. package/dist/mcp/state-server.d.ts.map +1 -1
  362. package/dist/mcp/trace-server.d.ts +4 -4
  363. package/dist/mcp/trace-server.d.ts.map +1 -1
  364. package/dist/mcp/trace-server.js.map +1 -1
  365. package/dist/mcp/wiki-server.d.ts +34 -23
  366. package/dist/mcp/wiki-server.d.ts.map +1 -1
  367. package/dist/mcp/wiki-server.js +9 -2
  368. package/dist/mcp/wiki-server.js.map +1 -1
  369. package/dist/modes/__tests__/base-session-scope.test.js +53 -1
  370. package/dist/modes/__tests__/base-session-scope.test.js.map +1 -1
  371. package/dist/modes/__tests__/base-tmux-pane.test.js +4 -4
  372. package/dist/modes/__tests__/base-tmux-pane.test.js.map +1 -1
  373. package/dist/modes/base.d.ts.map +1 -1
  374. package/dist/modes/base.js +5 -5
  375. package/dist/modes/base.js.map +1 -1
  376. package/dist/notifications/__tests__/lifecycle-dedupe.test.js.map +1 -1
  377. package/dist/notifications/__tests__/reply-listener.test.js.map +1 -1
  378. package/dist/notifications/__tests__/session-idle-tail-dedupe.test.js +177 -85
  379. package/dist/notifications/__tests__/session-idle-tail-dedupe.test.js.map +1 -1
  380. package/dist/notifications/__tests__/session-registry.test.js.map +1 -1
  381. package/dist/notifications/__tests__/template-engine.test.js.map +1 -1
  382. package/dist/notifications/__tests__/verbosity.test.js.map +1 -1
  383. package/dist/notifications/http-client.js.map +1 -1
  384. package/dist/notifications/index.d.ts +4 -1
  385. package/dist/notifications/index.d.ts.map +1 -1
  386. package/dist/notifications/index.js +3 -9
  387. package/dist/notifications/index.js.map +1 -1
  388. package/dist/notifications/lifecycle-dedupe.js.map +1 -1
  389. package/dist/notifications/reply-listener.d.ts.map +1 -1
  390. package/dist/notifications/reply-listener.js.map +1 -1
  391. package/dist/notifications/session-registry.js.map +1 -1
  392. package/dist/notifications/session-status.js.map +1 -1
  393. package/dist/notifications/temp-contract.d.ts.map +1 -1
  394. package/dist/notifications/temp-contract.js +4 -0
  395. package/dist/notifications/temp-contract.js.map +1 -1
  396. package/dist/notifications/tmux-detector.js.map +1 -1
  397. package/dist/notifications/tmux.js.map +1 -1
  398. package/dist/openclaw/config.js.map +1 -1
  399. package/dist/openclaw/dispatcher.js.map +1 -1
  400. package/dist/pipeline/__tests__/orchestrator.test.js +4 -4
  401. package/dist/pipeline/__tests__/orchestrator.test.js.map +1 -1
  402. package/dist/pipeline/__tests__/stages.test.js +16 -13
  403. package/dist/pipeline/__tests__/stages.test.js.map +1 -1
  404. package/dist/pipeline/stages/code-review.js.map +1 -1
  405. package/dist/pipeline/stages/ralph-verify.js.map +1 -1
  406. package/dist/pipeline/stages/ralplan.js.map +1 -1
  407. package/dist/pipeline/stages/team-exec.js.map +1 -1
  408. package/dist/pipeline/stages/ultragoal.js.map +1 -1
  409. package/dist/pipeline/stages/ultraqa.js.map +1 -1
  410. package/dist/planning/__tests__/approved-execution-lifecycle-matrix.test.js.map +1 -1
  411. package/dist/planning/artifact-names.js.map +1 -1
  412. package/dist/planning/artifacts.js.map +1 -1
  413. package/dist/question/__tests__/state.test.js.map +1 -1
  414. package/dist/question/__tests__/ui.test.js.map +1 -1
  415. package/dist/question/autopilot-wait.js.map +1 -1
  416. package/dist/question/client.d.ts.map +1 -1
  417. package/dist/question/client.js.map +1 -1
  418. package/dist/question/deep-interview.js.map +1 -1
  419. package/dist/question/events.js.map +1 -1
  420. package/dist/question/renderer.js.map +1 -1
  421. package/dist/question/state.d.ts.map +1 -1
  422. package/dist/question/state.js.map +1 -1
  423. package/dist/question/ui.js.map +1 -1
  424. package/dist/ralph/contract.d.ts +1 -1
  425. package/dist/ralph/contract.d.ts.map +1 -1
  426. package/dist/ralplan/__tests__/consensus-gate.test.js +527 -8
  427. package/dist/ralplan/__tests__/consensus-gate.test.js.map +1 -1
  428. package/dist/ralplan/__tests__/documented-leader-preflight.test.d.ts +2 -0
  429. package/dist/ralplan/__tests__/documented-leader-preflight.test.d.ts.map +1 -0
  430. package/dist/ralplan/__tests__/documented-leader-preflight.test.js +46 -0
  431. package/dist/ralplan/__tests__/documented-leader-preflight.test.js.map +1 -0
  432. package/dist/ralplan/__tests__/runtime.test.js +116 -14
  433. package/dist/ralplan/__tests__/runtime.test.js.map +1 -1
  434. package/dist/ralplan/consensus-gate.d.ts +3 -3
  435. package/dist/ralplan/consensus-gate.d.ts.map +1 -1
  436. package/dist/ralplan/consensus-gate.js +173 -70
  437. package/dist/ralplan/consensus-gate.js.map +1 -1
  438. package/dist/ralplan/documented-leader-preflight.d.ts +34 -0
  439. package/dist/ralplan/documented-leader-preflight.d.ts.map +1 -0
  440. package/dist/ralplan/documented-leader-preflight.js +58 -0
  441. package/dist/ralplan/documented-leader-preflight.js.map +1 -0
  442. package/dist/ralplan/runtime.d.ts +3 -2
  443. package/dist/ralplan/runtime.d.ts.map +1 -1
  444. package/dist/ralplan/runtime.js +12 -9
  445. package/dist/ralplan/runtime.js.map +1 -1
  446. package/dist/runtime/__tests__/bridge.test.js +42 -1
  447. package/dist/runtime/__tests__/bridge.test.js.map +1 -1
  448. package/dist/runtime/bridge.d.ts +12 -0
  449. package/dist/runtime/bridge.d.ts.map +1 -1
  450. package/dist/runtime/bridge.js +52 -1
  451. package/dist/runtime/bridge.js.map +1 -1
  452. package/dist/runtime/process-tree.js.map +1 -1
  453. package/dist/runtime/run-loop.js.map +1 -1
  454. package/dist/runtime/run-outcome.d.ts +3 -3
  455. package/dist/runtime/run-outcome.d.ts.map +1 -1
  456. package/dist/runtime/run-outcome.js.map +1 -1
  457. package/dist/runtime/run-state.js.map +1 -1
  458. package/dist/scripts/__tests__/codex-native-hook.test.js +6657 -995
  459. package/dist/scripts/__tests__/codex-native-hook.test.js.map +1 -1
  460. package/dist/scripts/__tests__/fixtures/issue-3138-installed-smoke.d.ts +2 -0
  461. package/dist/scripts/__tests__/fixtures/issue-3138-installed-smoke.d.ts.map +1 -0
  462. package/dist/scripts/__tests__/fixtures/issue-3138-installed-smoke.js +634 -0
  463. package/dist/scripts/__tests__/fixtures/issue-3138-installed-smoke.js.map +1 -0
  464. package/dist/scripts/__tests__/generate-release-body.test.js.map +1 -1
  465. package/dist/scripts/__tests__/hook-derived-watcher.test.js.map +1 -1
  466. package/dist/scripts/__tests__/notify-dispatcher.test.js.map +1 -1
  467. package/dist/scripts/__tests__/notify-fallback-delivery.test.d.ts +2 -0
  468. package/dist/scripts/__tests__/notify-fallback-delivery.test.d.ts.map +1 -0
  469. package/dist/scripts/__tests__/notify-fallback-delivery.test.js +174 -0
  470. package/dist/scripts/__tests__/notify-fallback-delivery.test.js.map +1 -0
  471. package/dist/scripts/__tests__/notify-state-io.test.js +57 -1
  472. package/dist/scripts/__tests__/notify-state-io.test.js.map +1 -1
  473. package/dist/scripts/__tests__/reasoning-artifact-contract.test.d.ts +2 -0
  474. package/dist/scripts/__tests__/reasoning-artifact-contract.test.d.ts.map +1 -0
  475. package/dist/scripts/__tests__/reasoning-artifact-contract.test.js +108 -0
  476. package/dist/scripts/__tests__/reasoning-artifact-contract.test.js.map +1 -0
  477. package/dist/scripts/__tests__/role-intent-bootstrap-e2e-3181.test.d.ts +2 -0
  478. package/dist/scripts/__tests__/role-intent-bootstrap-e2e-3181.test.d.ts.map +1 -0
  479. package/dist/scripts/__tests__/role-intent-bootstrap-e2e-3181.test.js +78 -0
  480. package/dist/scripts/__tests__/role-intent-bootstrap-e2e-3181.test.js.map +1 -0
  481. package/dist/scripts/__tests__/role-intent-durable-recovery-3181.test.d.ts +2 -0
  482. package/dist/scripts/__tests__/role-intent-durable-recovery-3181.test.d.ts.map +1 -0
  483. package/dist/scripts/__tests__/role-intent-durable-recovery-3181.test.js +55 -0
  484. package/dist/scripts/__tests__/role-intent-durable-recovery-3181.test.js.map +1 -0
  485. package/dist/scripts/__tests__/run-test-files.test.js.map +1 -1
  486. package/dist/scripts/__tests__/smoke-packed-install.test.js +1100 -4
  487. package/dist/scripts/__tests__/smoke-packed-install.test.js.map +1 -1
  488. package/dist/scripts/__tests__/test-reply-listener-live.test.js.map +1 -1
  489. package/dist/scripts/build-api.js.map +1 -1
  490. package/dist/scripts/build-explore-harness.js.map +1 -1
  491. package/dist/scripts/build-sparkshell.js.map +1 -1
  492. package/dist/scripts/cleanup-explore-harness.js.map +1 -1
  493. package/dist/scripts/codex-execution-surface.js.map +1 -1
  494. package/dist/scripts/codex-native-hook.d.ts +4 -1
  495. package/dist/scripts/codex-native-hook.d.ts.map +1 -1
  496. package/dist/scripts/codex-native-hook.js +11577 -1184
  497. package/dist/scripts/codex-native-hook.js.map +1 -1
  498. package/dist/scripts/generate-native-release-manifest.js.map +1 -1
  499. package/dist/scripts/generate-release-body.js.map +1 -1
  500. package/dist/scripts/hook-payload-guard.d.ts.map +1 -1
  501. package/dist/scripts/hook-payload-guard.js.map +1 -1
  502. package/dist/scripts/notify-fallback-delivery.d.ts +48 -0
  503. package/dist/scripts/notify-fallback-delivery.d.ts.map +1 -0
  504. package/dist/scripts/notify-fallback-delivery.js +419 -0
  505. package/dist/scripts/notify-fallback-delivery.js.map +1 -0
  506. package/dist/scripts/notify-fallback-watcher.js +218 -138
  507. package/dist/scripts/notify-fallback-watcher.js.map +1 -1
  508. package/dist/scripts/notify-hook/auto-nudge.d.ts +18 -9
  509. package/dist/scripts/notify-hook/auto-nudge.d.ts.map +1 -1
  510. package/dist/scripts/notify-hook/auto-nudge.js +112 -15
  511. package/dist/scripts/notify-hook/auto-nudge.js.map +1 -1
  512. package/dist/scripts/notify-hook/managed-tmux.d.ts +23 -0
  513. package/dist/scripts/notify-hook/managed-tmux.d.ts.map +1 -1
  514. package/dist/scripts/notify-hook/managed-tmux.js +122 -12
  515. package/dist/scripts/notify-hook/managed-tmux.js.map +1 -1
  516. package/dist/scripts/notify-hook/orchestration-intent.d.ts +7 -7
  517. package/dist/scripts/notify-hook/orchestration-intent.d.ts.map +1 -1
  518. package/dist/scripts/notify-hook/ralph-session-resume.d.ts +5 -2
  519. package/dist/scripts/notify-hook/ralph-session-resume.d.ts.map +1 -1
  520. package/dist/scripts/notify-hook/ralph-session-resume.js +33 -37
  521. package/dist/scripts/notify-hook/ralph-session-resume.js.map +1 -1
  522. package/dist/scripts/notify-hook/state-io.d.ts +17 -0
  523. package/dist/scripts/notify-hook/state-io.d.ts.map +1 -1
  524. package/dist/scripts/notify-hook/state-io.js +73 -11
  525. package/dist/scripts/notify-hook/state-io.js.map +1 -1
  526. package/dist/scripts/notify-hook/team-dispatch.d.ts +10 -1
  527. package/dist/scripts/notify-hook/team-dispatch.d.ts.map +1 -1
  528. package/dist/scripts/notify-hook/team-dispatch.js +234 -79
  529. package/dist/scripts/notify-hook/team-dispatch.js.map +1 -1
  530. package/dist/scripts/notify-hook/team-leader-nudge.d.ts +2 -2
  531. package/dist/scripts/notify-hook/team-leader-nudge.d.ts.map +1 -1
  532. package/dist/scripts/notify-hook/team-leader-nudge.js +39 -13
  533. package/dist/scripts/notify-hook/team-leader-nudge.js.map +1 -1
  534. package/dist/scripts/notify-hook/team-tmux-guard.d.ts +16 -8
  535. package/dist/scripts/notify-hook/team-tmux-guard.d.ts.map +1 -1
  536. package/dist/scripts/notify-hook/team-tmux-guard.js +365 -44
  537. package/dist/scripts/notify-hook/team-tmux-guard.js.map +1 -1
  538. package/dist/scripts/notify-hook/team-worker-posttooluse.js.map +1 -1
  539. package/dist/scripts/notify-hook/team-worker-stop.d.ts +1 -1
  540. package/dist/scripts/notify-hook/team-worker-stop.d.ts.map +1 -1
  541. package/dist/scripts/notify-hook/team-worker-stop.js +19 -18
  542. package/dist/scripts/notify-hook/team-worker-stop.js.map +1 -1
  543. package/dist/scripts/notify-hook/team-worker.d.ts +5 -2
  544. package/dist/scripts/notify-hook/team-worker.d.ts.map +1 -1
  545. package/dist/scripts/notify-hook/team-worker.js +37 -28
  546. package/dist/scripts/notify-hook/team-worker.js.map +1 -1
  547. package/dist/scripts/notify-hook/tmux-injection.d.ts +8 -1
  548. package/dist/scripts/notify-hook/tmux-injection.d.ts.map +1 -1
  549. package/dist/scripts/notify-hook/tmux-injection.js +53 -18
  550. package/dist/scripts/notify-hook/tmux-injection.js.map +1 -1
  551. package/dist/scripts/notify-hook/visual-verdict.d.ts +1 -1
  552. package/dist/scripts/notify-hook/visual-verdict.d.ts.map +1 -1
  553. package/dist/scripts/notify-hook/visual-verdict.js +15 -13
  554. package/dist/scripts/notify-hook/visual-verdict.js.map +1 -1
  555. package/dist/scripts/notify-hook.d.ts +24 -1
  556. package/dist/scripts/notify-hook.d.ts.map +1 -1
  557. package/dist/scripts/notify-hook.js +267 -90
  558. package/dist/scripts/notify-hook.js.map +1 -1
  559. package/dist/scripts/postinstall.js.map +1 -1
  560. package/dist/scripts/prompt-inventory.js.map +1 -1
  561. package/dist/scripts/smoke-packed-install.d.ts +927 -5
  562. package/dist/scripts/smoke-packed-install.d.ts.map +1 -1
  563. package/dist/scripts/smoke-packed-install.js +4317 -13
  564. package/dist/scripts/smoke-packed-install.js.map +1 -1
  565. package/dist/scripts/sync-plugin-mirror.js.map +1 -1
  566. package/dist/scripts/test-reply-listener-live.js.map +1 -1
  567. package/dist/scripts/test-sparkshell.js.map +1 -1
  568. package/dist/scripts/verify-native-agents.js.map +1 -1
  569. package/dist/session-history/friction.js.map +1 -1
  570. package/dist/session-history/search.js.map +1 -1
  571. package/dist/sidecar/__tests__/boundary.test.js.map +1 -1
  572. package/dist/sidecar/collector.js.map +1 -1
  573. package/dist/sidecar/index.js.map +1 -1
  574. package/dist/sidecar/render.js.map +1 -1
  575. package/dist/sidecar/tmux.js.map +1 -1
  576. package/dist/state/__tests__/mode-state-context.test.js +54 -0
  577. package/dist/state/__tests__/mode-state-context.test.js.map +1 -1
  578. package/dist/state/__tests__/operations.test.js +66 -15
  579. package/dist/state/__tests__/operations.test.js.map +1 -1
  580. package/dist/state/__tests__/skill-active.test.js +19 -1
  581. package/dist/state/__tests__/skill-active.test.js.map +1 -1
  582. package/dist/state/__tests__/workflow-transition.test.js +61 -0
  583. package/dist/state/__tests__/workflow-transition.test.js.map +1 -1
  584. package/dist/state/mode-state-context.d.ts +5 -0
  585. package/dist/state/mode-state-context.d.ts.map +1 -1
  586. package/dist/state/mode-state-context.js +59 -0
  587. package/dist/state/mode-state-context.js.map +1 -1
  588. package/dist/state/operations.d.ts +1 -1
  589. package/dist/state/operations.d.ts.map +1 -1
  590. package/dist/state/operations.js +49 -55
  591. package/dist/state/operations.js.map +1 -1
  592. package/dist/state/paths.d.ts +1 -1
  593. package/dist/state/paths.d.ts.map +1 -1
  594. package/dist/state/paths.js +1 -1
  595. package/dist/state/paths.js.map +1 -1
  596. package/dist/state/skill-active.d.ts +13 -1
  597. package/dist/state/skill-active.d.ts.map +1 -1
  598. package/dist/state/skill-active.js +43 -4
  599. package/dist/state/skill-active.js.map +1 -1
  600. package/dist/state/terminal-normalization.js.map +1 -1
  601. package/dist/state/workflow-transition-reconcile.d.ts.map +1 -1
  602. package/dist/state/workflow-transition-reconcile.js +20 -4
  603. package/dist/state/workflow-transition-reconcile.js.map +1 -1
  604. package/dist/state/workflow-transition.d.ts +1 -1
  605. package/dist/state/workflow-transition.d.ts.map +1 -1
  606. package/dist/state/workflow-transition.js.map +1 -1
  607. package/dist/subagents/__tests__/leader-bootstrap-3181.test.d.ts +2 -0
  608. package/dist/subagents/__tests__/leader-bootstrap-3181.test.d.ts.map +1 -0
  609. package/dist/subagents/__tests__/leader-bootstrap-3181.test.js +52 -0
  610. package/dist/subagents/__tests__/leader-bootstrap-3181.test.js.map +1 -0
  611. package/dist/subagents/__tests__/tracker.test.js +1550 -10
  612. package/dist/subagents/__tests__/tracker.test.js.map +1 -1
  613. package/dist/subagents/role-routing-marker.d.ts +10 -0
  614. package/dist/subagents/role-routing-marker.d.ts.map +1 -0
  615. package/dist/subagents/role-routing-marker.js +136 -0
  616. package/dist/subagents/role-routing-marker.js.map +1 -0
  617. package/dist/subagents/tracker.d.ts +91 -0
  618. package/dist/subagents/tracker.d.ts.map +1 -1
  619. package/dist/subagents/tracker.js +806 -62
  620. package/dist/subagents/tracker.js.map +1 -1
  621. package/dist/team/__tests__/api-interop.test.js +91 -1
  622. package/dist/team/__tests__/api-interop.test.js.map +1 -1
  623. package/dist/team/__tests__/approved-execution.test.js.map +1 -1
  624. package/dist/team/__tests__/delivery-e2e-smoke.test.js +52 -10
  625. package/dist/team/__tests__/delivery-e2e-smoke.test.js.map +1 -1
  626. package/dist/team/__tests__/idle-nudge.test.js +45 -15
  627. package/dist/team/__tests__/idle-nudge.test.js.map +1 -1
  628. package/dist/team/__tests__/mcp-comm.test.js +93 -27
  629. package/dist/team/__tests__/mcp-comm.test.js.map +1 -1
  630. package/dist/team/__tests__/model-contract.test.js +341 -4
  631. package/dist/team/__tests__/model-contract.test.js.map +1 -1
  632. package/dist/team/__tests__/role-router.test.js.map +1 -1
  633. package/dist/team/__tests__/runtime-cli.test.js +9 -0
  634. package/dist/team/__tests__/runtime-cli.test.js.map +1 -1
  635. package/dist/team/__tests__/runtime.test.js +3624 -355
  636. package/dist/team/__tests__/runtime.test.js.map +1 -1
  637. package/dist/team/__tests__/scaling.test.js +2516 -118
  638. package/dist/team/__tests__/scaling.test.js.map +1 -1
  639. package/dist/team/__tests__/state-root.test.js.map +1 -1
  640. package/dist/team/__tests__/state.test.js +804 -36
  641. package/dist/team/__tests__/state.test.js.map +1 -1
  642. package/dist/team/__tests__/team-ops-contract.test.js +6 -0
  643. package/dist/team/__tests__/team-ops-contract.test.js.map +1 -1
  644. package/dist/team/__tests__/tmux-session.test.js +3664 -969
  645. package/dist/team/__tests__/tmux-session.test.js.map +1 -1
  646. package/dist/team/__tests__/tmux-test-fixture.js.map +1 -1
  647. package/dist/team/__tests__/worker-runtime-identity.test.js +38 -2
  648. package/dist/team/__tests__/worker-runtime-identity.test.js.map +1 -1
  649. package/dist/team/__tests__/worktree.test.js +6 -0
  650. package/dist/team/__tests__/worktree.test.js.map +1 -1
  651. package/dist/team/api-interop.d.ts +2 -2
  652. package/dist/team/api-interop.d.ts.map +1 -1
  653. package/dist/team/api-interop.js +15 -9
  654. package/dist/team/api-interop.js.map +1 -1
  655. package/dist/team/commit-hygiene.js.map +1 -1
  656. package/dist/team/contracts.d.ts +5 -5
  657. package/dist/team/contracts.d.ts.map +1 -1
  658. package/dist/team/delivery-log.js.map +1 -1
  659. package/dist/team/exact-pane.d.ts +23 -0
  660. package/dist/team/exact-pane.d.ts.map +1 -0
  661. package/dist/team/exact-pane.js +133 -0
  662. package/dist/team/exact-pane.js.map +1 -0
  663. package/dist/team/followup-planner.js.map +1 -1
  664. package/dist/team/goal-workflow.js.map +1 -1
  665. package/dist/team/idle-nudge.d.ts +12 -1
  666. package/dist/team/idle-nudge.d.ts.map +1 -1
  667. package/dist/team/idle-nudge.js +41 -14
  668. package/dist/team/idle-nudge.js.map +1 -1
  669. package/dist/team/model-contract.d.ts +18 -2
  670. package/dist/team/model-contract.d.ts.map +1 -1
  671. package/dist/team/model-contract.js +314 -35
  672. package/dist/team/model-contract.js.map +1 -1
  673. package/dist/team/orchestrator.js.map +1 -1
  674. package/dist/team/pane-status.js.map +1 -1
  675. package/dist/team/phase-controller.js.map +1 -1
  676. package/dist/team/reminder-intents.d.ts +1 -1
  677. package/dist/team/reminder-intents.d.ts.map +1 -1
  678. package/dist/team/reminder-intents.js.map +1 -1
  679. package/dist/team/runtime-cli.js.map +1 -1
  680. package/dist/team/runtime.d.ts +1 -0
  681. package/dist/team/runtime.d.ts.map +1 -1
  682. package/dist/team/runtime.js +1326 -231
  683. package/dist/team/runtime.js.map +1 -1
  684. package/dist/team/scaling.d.ts +22 -8
  685. package/dist/team/scaling.d.ts.map +1 -1
  686. package/dist/team/scaling.js +1463 -376
  687. package/dist/team/scaling.js.map +1 -1
  688. package/dist/team/state/dispatch-lock.js.map +1 -1
  689. package/dist/team/state/dispatch.d.ts.map +1 -1
  690. package/dist/team/state/dispatch.js +26 -8
  691. package/dist/team/state/dispatch.js.map +1 -1
  692. package/dist/team/state/events.js.map +1 -1
  693. package/dist/team/state/locks.d.ts.map +1 -1
  694. package/dist/team/state/locks.js +61 -13
  695. package/dist/team/state/locks.js.map +1 -1
  696. package/dist/team/state/tasks.d.ts.map +1 -1
  697. package/dist/team/state/tasks.js +4 -3
  698. package/dist/team/state/tasks.js.map +1 -1
  699. package/dist/team/state-root.js.map +1 -1
  700. package/dist/team/state.d.ts +67 -1
  701. package/dist/team/state.d.ts.map +1 -1
  702. package/dist/team/state.js +636 -129
  703. package/dist/team/state.js.map +1 -1
  704. package/dist/team/team-identity.d.ts.map +1 -1
  705. package/dist/team/team-identity.js.map +1 -1
  706. package/dist/team/team-ops.d.ts +5 -0
  707. package/dist/team/team-ops.d.ts.map +1 -1
  708. package/dist/team/team-ops.js +4 -0
  709. package/dist/team/team-ops.js.map +1 -1
  710. package/dist/team/tmux-session.d.ts +125 -23
  711. package/dist/team/tmux-session.d.ts.map +1 -1
  712. package/dist/team/tmux-session.js +1340 -333
  713. package/dist/team/tmux-session.js.map +1 -1
  714. package/dist/team/worker-bootstrap.js.map +1 -1
  715. package/dist/team/worktree.d.ts.map +1 -1
  716. package/dist/team/worktree.js +4 -0
  717. package/dist/team/worktree.js.map +1 -1
  718. package/dist/ultragoal/__tests__/artifacts.test.js.map +1 -1
  719. package/dist/ultragoal/__tests__/docs-contract.test.js.map +1 -1
  720. package/dist/ultragoal/artifacts.d.ts.map +1 -1
  721. package/dist/ultragoal/artifacts.js +4 -1
  722. package/dist/ultragoal/artifacts.js.map +1 -1
  723. package/dist/url-reader/index.js.map +1 -1
  724. package/dist/utils/__tests__/file-durability.test.d.ts +2 -0
  725. package/dist/utils/__tests__/file-durability.test.d.ts.map +1 -0
  726. package/dist/utils/__tests__/file-durability.test.js +57 -0
  727. package/dist/utils/__tests__/file-durability.test.js.map +1 -0
  728. package/dist/utils/agents-model-table.js.map +1 -1
  729. package/dist/utils/file-durability.d.ts +15 -0
  730. package/dist/utils/file-durability.d.ts.map +1 -0
  731. package/dist/utils/file-durability.js +37 -0
  732. package/dist/utils/file-durability.js.map +1 -0
  733. package/dist/utils/package.js.map +1 -1
  734. package/dist/utils/paths.js.map +1 -1
  735. package/dist/utils/platform-command.js.map +1 -1
  736. package/dist/utils/repo-deps.js.map +1 -1
  737. package/dist/utils/version.js.map +1 -1
  738. package/dist/verification/__tests__/ci-rust-gates.test.js +1 -1
  739. package/dist/verification/__tests__/dev-merge-issue-close-workflow.test.js +3 -0
  740. package/dist/verification/__tests__/dev-merge-issue-close-workflow.test.js.map +1 -1
  741. package/dist/verification/verifier.js.map +1 -1
  742. package/dist/visual/constants.d.ts +1 -1
  743. package/dist/visual/constants.d.ts.map +1 -1
  744. package/dist/vscode/__tests__/index.test.js.map +1 -1
  745. package/dist/vscode/index.js.map +1 -1
  746. package/dist/wiki/__tests__/cjk-tokenize.test.js.map +1 -1
  747. package/dist/wiki/__tests__/lint.test.js.map +1 -1
  748. package/dist/wiki/__tests__/query.test.js.map +1 -1
  749. package/dist/wiki/__tests__/storage.test.js.map +1 -1
  750. package/dist/wiki/__tests__/test-helpers.d.ts.map +1 -1
  751. package/dist/wiki/lint.d.ts +4 -1
  752. package/dist/wiki/lint.d.ts.map +1 -1
  753. package/dist/wiki/lint.js +3 -2
  754. package/dist/wiki/lint.js.map +1 -1
  755. package/dist/wiki/query.js.map +1 -1
  756. package/dist/wiki/storage.js.map +1 -1
  757. package/package.json +2 -2
  758. package/plugins/oh-my-codex/.codex-plugin/plugin.json +1 -1
  759. package/plugins/oh-my-codex/hooks/codex-native-hook.mjs +24 -0
  760. package/plugins/oh-my-codex/skills/omx-setup/SKILL.md +10 -5
  761. package/plugins/oh-my-codex/skills/ralph/SKILL.md +6 -2
  762. package/plugins/oh-my-codex/skills/ralplan/SKILL.md +6 -2
  763. package/plugins/oh-my-codex/skills/team/SKILL.md +13 -9
  764. package/skills/omx-setup/SKILL.md +10 -5
  765. package/skills/ralph/SKILL.md +6 -2
  766. package/skills/ralplan/SKILL.md +6 -2
  767. package/skills/team/SKILL.md +13 -9
  768. package/src/scripts/__tests__/codex-native-hook.test.ts +20048 -10827
  769. package/src/scripts/__tests__/fixtures/issue-3138-installed-smoke.ts +734 -0
  770. package/src/scripts/__tests__/notify-fallback-delivery.test.ts +177 -0
  771. package/src/scripts/__tests__/notify-state-io.test.ts +73 -0
  772. package/src/scripts/__tests__/reasoning-artifact-contract.test.ts +129 -0
  773. package/src/scripts/__tests__/role-intent-bootstrap-e2e-3181.test.ts +78 -0
  774. package/src/scripts/__tests__/role-intent-durable-recovery-3181.test.ts +60 -0
  775. package/src/scripts/__tests__/smoke-packed-install.test.ts +1309 -4
  776. package/src/scripts/codex-native-hook.ts +11429 -1163
  777. package/src/scripts/notify-fallback-delivery.ts +416 -0
  778. package/src/scripts/notify-fallback-watcher.ts +216 -155
  779. package/src/scripts/notify-hook/auto-nudge.ts +113 -14
  780. package/src/scripts/notify-hook/managed-tmux.ts +149 -12
  781. package/src/scripts/notify-hook/ralph-session-resume.ts +37 -36
  782. package/src/scripts/notify-hook/state-io.ts +100 -13
  783. package/src/scripts/notify-hook/team-dispatch.ts +242 -75
  784. package/src/scripts/notify-hook/team-leader-nudge.ts +40 -18
  785. package/src/scripts/notify-hook/team-tmux-guard.ts +366 -39
  786. package/src/scripts/notify-hook/team-worker-stop.ts +20 -16
  787. package/src/scripts/notify-hook/team-worker.ts +39 -26
  788. package/src/scripts/notify-hook/tmux-injection.ts +66 -23
  789. package/src/scripts/notify-hook/visual-verdict.ts +22 -12
  790. package/src/scripts/notify-hook.ts +336 -90
  791. package/src/scripts/smoke-packed-install.ts +5154 -149
@@ -1,14 +1,16 @@
1
1
  import { after, before, describe, it } from "node:test";
2
2
  import assert from "node:assert/strict";
3
- import { existsSync } from "node:fs";
4
- import { chmod, cp, mkdir, mkdtemp, readFile, readdir, rm, writeFile, } from "node:fs/promises";
3
+ import { existsSync, lstatSync, readFileSync, renameSync, rmSync, symlinkSync, writeFileSync } from "node:fs";
4
+ import { chmod, cp, lstat, mkdir, mkdtemp, readFile, readdir, rm, symlink, writeFile, stat, } from "node:fs/promises";
5
5
  import { tmpdir } from "node:os";
6
- import { dirname, join } from "node:path";
6
+ import { basename, dirname, join } from "node:path";
7
7
  import { parse as parseToml } from "@iarna/toml";
8
- import { setup } from "../setup.js";
8
+ import { decideWindowsNativeHookShimReference, setNativeHookTransactionFailureInjectorForTest, setSetupLatePhaseFailureInjectorForTest, setNativeHookTransactionPlatformForTest, setNativeHookTransactionRegularFileSyncForTest, setNativeHookTransactionTemporaryPathForTest, setup, } from "../setup.js";
9
+ import { resolveSetupRefreshArgs } from "../update.js";
9
10
  import { uninstall } from "../uninstall.js";
10
11
  import { OMX_DEVELOPER_INSTRUCTIONS, OMX_PLUGIN_DEVELOPER_INSTRUCTIONS, } from "../../config/generator.js";
11
12
  import { materializePackagedOmxPluginCache, resolvePackagedOmxMarketplace, } from "../plugin-marketplace.js";
13
+ import { buildManagedCodexHookTrustState, buildManagedCodexHooksConfig, buildManagedCodexNativeHookWindowsShimContent, buildManagedCodexNativeHookWindowsShimPath, } from "../../config/codex-hooks.js";
12
14
  const packageRoot = process.cwd();
13
15
  let previousPathForFakeCodex;
14
16
  let fakeCodexBinDir = null;
@@ -96,6 +98,32 @@ async function withIsolatedUserHome(wd, fn) {
96
98
  }
97
99
  }
98
100
  }
101
+ async function withDriveQualifiedWindowsCodexHome(wd, fn) {
102
+ const previousCwd = process.cwd();
103
+ const previousHome = process.env.HOME;
104
+ const previousCodexHome = process.env.CODEX_HOME;
105
+ const codexHomeDir = "C:\\Users\\omx\\.codex";
106
+ process.chdir(wd);
107
+ process.env.HOME = wd;
108
+ process.env.CODEX_HOME = codexHomeDir;
109
+ try {
110
+ await mkdir(codexHomeDir, { recursive: true });
111
+ return await fn(codexHomeDir);
112
+ }
113
+ finally {
114
+ process.chdir(previousCwd);
115
+ if (typeof previousHome === "string")
116
+ process.env.HOME = previousHome;
117
+ else
118
+ delete process.env.HOME;
119
+ if (typeof previousCodexHome === "string") {
120
+ process.env.CODEX_HOME = previousCodexHome;
121
+ }
122
+ else {
123
+ delete process.env.CODEX_HOME;
124
+ }
125
+ }
126
+ }
99
127
  describe("notify setup scope", () => {
100
128
  it("does not write unsupported project-scope notify", async () => {
101
129
  const wd = await mkdtemp(join(tmpdir(), "omx-project-no-notify-"));
@@ -1357,6 +1385,12 @@ describe("omx setup install mode behavior", () => {
1357
1385
  assert.match(config, /Registered Codex plugin marketplace surfaces supply OMX workflows and plugin-scoped companion resources/);
1358
1386
  assert.match(config, /User-installed skills may still live under ~\/.codex\/skills/);
1359
1387
  assert.match(config, /native agent roles are installed as setup-owned Codex agent TOML files in plugin mode so agent_type routing works/i);
1388
+ assert.match(config, /When the native surface exposes `agent_type` role routing, set `agent_type` to an installed role and never omit it for OMX work/i);
1389
+ assert.match(config, /role_routing_unavailable/i);
1390
+ assert.match(config, /do not fabricate `agent_type`/i);
1391
+ assert.match(config, /omx ralplan preflight --json/i);
1392
+ assert.match(config, /unsupported_documented_leader_proof/i);
1393
+ assert.match(config, /never fake the role via a prompt label/i);
1360
1394
  assert.doesNotMatch(config, /Native subagents live in \.codex\/agents/);
1361
1395
  assert.doesNotMatch(config, /Treat installed prompts as narrower execution surfaces/);
1362
1396
  assert.match(config, /^plugin_hooks = true$/m);
@@ -1463,7 +1497,7 @@ describe("omx setup install mode behavior", () => {
1463
1497
  await rm(wd, { recursive: true, force: true });
1464
1498
  }
1465
1499
  });
1466
- it("preserves latest unwrapped developer_instructions without prompting", async () => {
1500
+ it("migrates historical unwrapped developer_instructions after prompting", async () => {
1467
1501
  const wd = await mkdtemp(join(tmpdir(), "omx-setup-install-mode-"));
1468
1502
  try {
1469
1503
  await withIsolatedUserHome(wd, async (codexHomeDir) => {
@@ -1480,10 +1514,12 @@ describe("omx setup install mode behavior", () => {
1480
1514
  return "refresh";
1481
1515
  },
1482
1516
  });
1483
- assert.equal(promptCount, 0);
1517
+ assert.equal(promptCount, 1);
1484
1518
  const config = await readFile(configPath, "utf-8");
1485
- assert.match(config, /You have oh-my-codex installed through Codex plugin mode/);
1486
- assert.doesNotMatch(config, /<omx version=/);
1519
+ assert.match(config, /<omx version=\\"1\\">You have oh-my-codex installed through Codex plugin mode/);
1520
+ assert.match(config, /When the native surface exposes `agent_type` role routing, set `agent_type` to an installed role and never omit it for OMX work/i);
1521
+ assert.match(config, /role_routing_unavailable/i);
1522
+ assert.match(config, /omx ralplan preflight --json/i);
1487
1523
  assert.equal((config.match(/^developer_instructions\s*=/gm) ?? []).length, 1);
1488
1524
  });
1489
1525
  });
@@ -1734,463 +1770,2712 @@ describe("omx setup install mode behavior", () => {
1734
1770
  await rm(wd, { recursive: true, force: true });
1735
1771
  }
1736
1772
  });
1737
- it("preserves existing user hooks while using plugin-scoped hooks", async () => {
1738
- const wd = await mkdtemp(join(tmpdir(), "omx-setup-install-mode-"));
1773
+ it("fails before plugin transition writes when marker-wrapped trust state has foreign siblings", async () => {
1774
+ const resetPlatform = setNativeHookTransactionPlatformForTest("win32");
1775
+ const representations = [
1776
+ {
1777
+ name: "inline",
1778
+ render: (key, hash) => [
1779
+ `hooks = { state = { ${JSON.stringify(key)} = { trusted_hash = ${JSON.stringify(hash)}, foreign = true } } }`,
1780
+ ],
1781
+ },
1782
+ {
1783
+ name: "dotted",
1784
+ render: (key, hash) => [
1785
+ `hooks.state.${JSON.stringify(key)}.trusted_hash = ${JSON.stringify(hash)}`,
1786
+ `hooks.state.${JSON.stringify(key)}.foreign = true`,
1787
+ ],
1788
+ },
1789
+ {
1790
+ name: "table",
1791
+ render: (key, hash) => [
1792
+ `[hooks.state.${JSON.stringify(key)}]`,
1793
+ `trusted_hash = ${JSON.stringify(hash)}`,
1794
+ "foreign = true",
1795
+ ],
1796
+ },
1797
+ {
1798
+ name: "separate-foreign-table",
1799
+ render: (key, hash) => [
1800
+ `[hooks.state.${JSON.stringify(key)}]`,
1801
+ `trusted_hash = ${JSON.stringify(hash)}`,
1802
+ "",
1803
+ '[hooks.state."foreign-key"]',
1804
+ 'trusted_hash = "sha256:foreign"',
1805
+ ],
1806
+ },
1807
+ ];
1739
1808
  try {
1740
- await withIsolatedUserHome(wd, async (codexHomeDir) => {
1741
- await withTempCwd(wd, async () => {
1742
- const hooksPath = join(codexHomeDir, "hooks.json");
1743
- const existingHooks = JSON.stringify({ hooks: { UserPromptSubmit: [] } }, null, 2) + "\n";
1744
- await writeFile(hooksPath, existingHooks);
1745
- await setup({ scope: "user", installMode: "plugin" });
1746
- const hooks = await readFile(hooksPath, "utf-8");
1747
- assert.match(hooks, /"UserPromptSubmit"/);
1748
- assert.doesNotMatch(hooks, /codex-native-hook\.js/);
1749
- const config = await readFile(join(codexHomeDir, "config.toml"), "utf-8");
1750
- assert.match(config, /^plugin_hooks = true$/m);
1751
- });
1752
- });
1809
+ for (const representation of representations) {
1810
+ const wd = await mkdtemp(join(tmpdir(), `omx-plugin-trust-${representation.name}-`));
1811
+ try {
1812
+ await withIsolatedUserHome(wd, async (codexHomeDir) => {
1813
+ await withTempCwd(wd, async () => {
1814
+ const configPath = join(codexHomeDir, "config.toml");
1815
+ const hooksPath = join(codexHomeDir, "hooks.json");
1816
+ const shimPath = buildManagedCodexNativeHookWindowsShimPath(codexHomeDir);
1817
+ const metadataPath = join(codexHomeDir, ".omx", "notify-dispatch.json");
1818
+ const hooks = `${JSON.stringify(buildManagedCodexHooksConfig(packageRoot, {
1819
+ platform: "win32",
1820
+ codexHomeDir,
1821
+ }), null, 2)}\n`;
1822
+ const [key, trust] = Object.entries(buildManagedCodexHookTrustState(hooksPath, packageRoot, {
1823
+ platform: "win32",
1824
+ codexHomeDir,
1825
+ hooksContent: hooks,
1826
+ }))[0] ?? [];
1827
+ assert.ok(key);
1828
+ assert.ok(trust);
1829
+ const config = [
1830
+ 'model = "foreign"',
1831
+ "",
1832
+ "# ============================================================",
1833
+ "# oh-my-codex (OMX) Configuration",
1834
+ "# Managed by omx setup - manual edits preserved on next setup",
1835
+ "# ============================================================",
1836
+ "",
1837
+ ...representation.render(key, trust.trusted_hash),
1838
+ "",
1839
+ "# ============================================================",
1840
+ "# End oh-my-codex",
1841
+ "",
1842
+ ].join("\n");
1843
+ const metadata = Buffer.from('{"managedBy":"foreign"}\n', "utf-8");
1844
+ const shim = Buffer.from(buildManagedCodexNativeHookWindowsShimContent(packageRoot), "utf-8");
1845
+ await mkdir(dirname(metadataPath), { recursive: true });
1846
+ await writeFile(configPath, config);
1847
+ await writeFile(hooksPath, hooks);
1848
+ await writeFile(shimPath, shim);
1849
+ await writeFile(metadataPath, metadata);
1850
+ await assert.rejects(setup({
1851
+ scope: "user",
1852
+ installMode: "plugin",
1853
+ pluginAgentsMdPrompt: async () => false,
1854
+ codexFeaturesProbe: () => "hooks stable true\nplugin_hooks experimental true\n",
1855
+ }), (error) => typeof error === "object" &&
1856
+ error !== null &&
1857
+ "code" in error &&
1858
+ error.code === "managed_trust_key_conflict");
1859
+ assert.deepEqual(await readFile(configPath), Buffer.from(config, "utf-8"));
1860
+ assert.deepEqual(await readFile(hooksPath), Buffer.from(hooks, "utf-8"));
1861
+ assert.deepEqual(await readFile(shimPath), shim);
1862
+ assert.deepEqual(await readFile(metadataPath), metadata);
1863
+ assert.deepEqual((await readdir(codexHomeDir)).filter((entry) => entry.includes(".omx-")), []);
1864
+ assert.deepEqual((await readdir(wd)).filter((entry) => entry.includes(".omx-")), []);
1865
+ });
1866
+ });
1867
+ }
1868
+ finally {
1869
+ await rm(wd, { recursive: true, force: true });
1870
+ }
1871
+ }
1753
1872
  }
1754
1873
  finally {
1755
- await rm(wd, { recursive: true, force: true });
1874
+ resetPlatform();
1756
1875
  }
1757
1876
  });
1758
- it("preserves same-key user hook trust state in plugin-scoped setup", async () => {
1759
- const wd = await mkdtemp(join(tmpdir(), "omx-setup-install-mode-"));
1877
+ it("fails before plugin transition writes when marker trust has absent or non-managed hooks expectations", async () => {
1878
+ const resetPlatform = setNativeHookTransactionPlatformForTest("win32");
1879
+ const fixtures = [
1880
+ { name: "absent", hooks: null },
1881
+ {
1882
+ name: "non-managed",
1883
+ hooks: `${JSON.stringify({
1884
+ hooks: {
1885
+ UserPromptSubmit: [{ hooks: [{ type: "command", command: "echo foreign" }] }],
1886
+ },
1887
+ }, null, 2)}\n`,
1888
+ },
1889
+ ];
1890
+ try {
1891
+ for (const fixture of fixtures) {
1892
+ const wd = await mkdtemp(join(tmpdir(), `omx-plugin-marker-${fixture.name}-`));
1893
+ try {
1894
+ await withIsolatedUserHome(wd, async (codexHomeDir) => {
1895
+ await withTempCwd(wd, async () => {
1896
+ const configPath = join(codexHomeDir, "config.toml");
1897
+ const hooksPath = join(codexHomeDir, "hooks.json");
1898
+ const shimPath = buildManagedCodexNativeHookWindowsShimPath(codexHomeDir);
1899
+ const metadataPath = join(codexHomeDir, ".omx", "notify-dispatch.json");
1900
+ const config = [
1901
+ 'model = "foreign"',
1902
+ "",
1903
+ "# ============================================================",
1904
+ "# oh-my-codex (OMX) Configuration",
1905
+ "# Managed by omx setup - manual edits preserved on next setup",
1906
+ "# ============================================================",
1907
+ "",
1908
+ '[hooks.state."foreign-key"]',
1909
+ 'trusted_hash = "sha256:foreign"',
1910
+ "",
1911
+ "# ============================================================",
1912
+ "# End oh-my-codex",
1913
+ "",
1914
+ ].join("\n");
1915
+ const shim = Buffer.from(buildManagedCodexNativeHookWindowsShimContent(packageRoot), "utf-8");
1916
+ const metadata = Buffer.from('{"managedBy":"foreign"}\n', "utf-8");
1917
+ await mkdir(dirname(metadataPath), { recursive: true });
1918
+ await writeFile(configPath, config);
1919
+ if (fixture.hooks !== null)
1920
+ await writeFile(hooksPath, fixture.hooks);
1921
+ await writeFile(shimPath, shim);
1922
+ await writeFile(metadataPath, metadata);
1923
+ await assert.rejects(setup({
1924
+ scope: "user",
1925
+ installMode: "plugin",
1926
+ pluginAgentsMdPrompt: async () => false,
1927
+ skipNativeAgentRefresh: true,
1928
+ codexFeaturesProbe: () => "hooks stable true\nplugin_hooks experimental true\n",
1929
+ }), (error) => typeof error === "object" &&
1930
+ error !== null &&
1931
+ "code" in error &&
1932
+ error.code === "managed_trust_key_conflict");
1933
+ assert.deepEqual(await readFile(configPath), Buffer.from(config, "utf-8"));
1934
+ assert.deepEqual(await readFile(shimPath), shim);
1935
+ assert.deepEqual(await readFile(metadataPath), metadata);
1936
+ if (fixture.hooks === null) {
1937
+ assert.equal(existsSync(hooksPath), false);
1938
+ }
1939
+ else {
1940
+ assert.deepEqual(await readFile(hooksPath), Buffer.from(fixture.hooks, "utf-8"));
1941
+ }
1942
+ assert.deepEqual((await readdir(codexHomeDir)).filter((entry) => entry.includes(".omx-")), []);
1943
+ assert.deepEqual((await readdir(wd)).filter((entry) => entry.includes(".omx-")), []);
1944
+ });
1945
+ });
1946
+ }
1947
+ finally {
1948
+ await rm(wd, { recursive: true, force: true });
1949
+ }
1950
+ }
1951
+ }
1952
+ finally {
1953
+ resetPlatform();
1954
+ }
1955
+ });
1956
+ it("treats normalized managed Windows shim identity as exact and every distinct target as ambiguous", () => {
1957
+ const shimBasename = "omx-native-hook-windows-shim.ps1";
1958
+ const targetShimPath = `C:\\Users\\alice\\.codex\\hooks\\${shimBasename}`;
1959
+ const futureCommand = (shimPath) => `& 'C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe' -NoProfile -ExecutionPolicy Bypass -File '${shimPath}'`;
1960
+ const futureHooks = (shimPath) => JSON.stringify({
1961
+ hooks: {
1962
+ FutureEvent: [{ hooks: [{ type: "command", command: futureCommand(shimPath) }] }],
1963
+ },
1964
+ });
1965
+ assert.equal(decideWindowsNativeHookShimReference(futureHooks(`c:/users/alice/.codex/hooks/../hooks/${shimBasename.toUpperCase()}`), targetShimPath), "referenced");
1966
+ assert.equal(decideWindowsNativeHookShimReference(futureHooks(`D:\\Users\\alice\\.codex\\hooks\\${shimBasename}`), targetShimPath), "ambiguous");
1967
+ assert.equal(decideWindowsNativeHookShimReference(futureHooks("D:\\Users\\alice\\.codex\\hooks\\unrelated.ps1"), targetShimPath), "ambiguous");
1968
+ assert.equal(decideWindowsNativeHookShimReference(JSON.stringify({
1969
+ hooks: {
1970
+ FutureEvent: [{ hooks: [{ type: "command", command: "& $env:OMX_SHIM" }] }],
1971
+ },
1972
+ }), targetShimPath), "ambiguous");
1973
+ for (const command of [
1974
+ "& %OMX_SHIM%",
1975
+ "& !OMX_SHIM!",
1976
+ "& $(Get-Item env:OMX_SHIM)",
1977
+ ]) {
1978
+ assert.equal(decideWindowsNativeHookShimReference(JSON.stringify({
1979
+ hooks: {
1980
+ FutureEvent: [{ hooks: [{ type: "command", command }] }],
1981
+ },
1982
+ }), targetShimPath), "ambiguous", command);
1983
+ }
1984
+ for (const command of [
1985
+ `Write-Output "don't"; & $env:HOOK_SCRIPT; Write-Output "user's"`,
1986
+ "Invoke-Expression '& $env:HOOK_SCRIPT'",
1987
+ "cmd /c 'call %HOOK_SCRIPT%'",
1988
+ `& ('C:\\Users\\alice\\.codex\\hooks\\omx-native-hook-windows-' + 'shim.ps1')`,
1989
+ "& (Get-Content C:\\shim-path.txt)",
1990
+ ]) {
1991
+ assert.equal(decideWindowsNativeHookShimReference(JSON.stringify({
1992
+ hooks: {
1993
+ FutureEvent: [{ hooks: [{ type: "command", command }] }],
1994
+ },
1995
+ }), targetShimPath), "ambiguous", command);
1996
+ }
1997
+ assert.equal(decideWindowsNativeHookShimReference(JSON.stringify({
1998
+ hooks: {
1999
+ FutureEvent: [{
2000
+ hooks: [{
2001
+ type: "command",
2002
+ command: "& 'C:\\%OMX_INERT%\\powershell.exe' -NoProfile -ExecutionPolicy Bypass -File 'D:\\tools\\unrelated-$env:INERT.ps1'",
2003
+ }],
2004
+ }],
2005
+ },
2006
+ }), targetShimPath), "ambiguous");
2007
+ for (const ambiguousPath of [
2008
+ `\\Users\\alice\\.codex\\hooks\\${shimBasename}`,
2009
+ `C:Users\\alice\\.codex\\hooks\\${shimBasename}`,
2010
+ `C:\\\\Users\\alice\\.codex\\hooks\\${shimBasename}`,
2011
+ String.raw `\Users\ALICE~1\.codex\hooks\OMX-NA~1.PS1`,
2012
+ String.raw `C:Users\ALICE~1\.codex\hooks\OMX-NA~1.PS1`,
2013
+ `\\\\?\\C:\\Users\\alice\\.codex\\hooks\\${shimBasename}`,
2014
+ `C:\\$env:USERPROFILE\\hooks\\${shimBasename}`,
2015
+ `.\\hooks\\${shimBasename}`,
2016
+ `D:\\aliases\\${shimBasename}. `,
2017
+ `C:\\Users\\ALICE~1\\.codex\\hooks\\OMX-NA~1.PS1`,
2018
+ ]) {
2019
+ assert.equal(decideWindowsNativeHookShimReference(futureHooks(ambiguousPath), targetShimPath), "ambiguous", ambiguousPath);
2020
+ }
2021
+ const uncTargetShimPath = `\\\\server\\share\\users\\alice\\.codex\\hooks\\${shimBasename}`;
2022
+ assert.equal(decideWindowsNativeHookShimReference(futureHooks(`//SERVER/share/users/alice/.codex/hooks/../hooks/${shimBasename.toUpperCase()}`), uncTargetShimPath), "referenced");
2023
+ });
2024
+ it("rolls back when an environment-indirected preserved Windows shim drifts during hooks or config writes", async () => {
2025
+ const resetPlatform = setNativeHookTransactionPlatformForTest("win32");
2026
+ try {
2027
+ for (const mutationKind of ["hooks", "config"]) {
2028
+ const wd = await mkdtemp(join(tmpdir(), `omx-plugin-preserved-shim-${mutationKind}-`));
2029
+ let resetFailureInjector;
2030
+ try {
2031
+ await withIsolatedUserHome(wd, async (codexHomeDir) => {
2032
+ await withTempCwd(wd, async () => {
2033
+ const hooksPath = join(codexHomeDir, "hooks.json");
2034
+ const configPath = join(codexHomeDir, "config.toml");
2035
+ const shimPath = buildManagedCodexNativeHookWindowsShimPath(codexHomeDir);
2036
+ const managed = buildManagedCodexHooksConfig(packageRoot, {
2037
+ platform: "win32",
2038
+ codexHomeDir,
2039
+ });
2040
+ const hooksBefore = Buffer.from(`${JSON.stringify({
2041
+ ...managed,
2042
+ hooks: {
2043
+ ...managed.hooks,
2044
+ FutureEvent: [{
2045
+ hooks: [{ type: "command", command: "& $env:OMX_SHIM" }],
2046
+ }],
2047
+ },
2048
+ }, null, 2)}\n`, "utf-8");
2049
+ const foreignShim = Buffer.from(`# concurrent ${mutationKind} shim replacement\n`, "utf-8");
2050
+ await writeFile(hooksPath, hooksBefore);
2051
+ await writeFile(shimPath, buildManagedCodexNativeHookWindowsShimContent(packageRoot));
2052
+ let injected = false;
2053
+ resetFailureInjector = setNativeHookTransactionFailureInjectorForTest((stage, target) => {
2054
+ if (stage !== "before_temp_write" ||
2055
+ target.kind !== mutationKind ||
2056
+ injected) {
2057
+ return;
2058
+ }
2059
+ injected = true;
2060
+ writeFileSync(shimPath, foreignShim);
2061
+ });
2062
+ await assert.rejects(setup({
2063
+ scope: "user",
2064
+ installMode: "plugin",
2065
+ pluginAgentsMdPrompt: async () => false,
2066
+ skipNativeAgentRefresh: true,
2067
+ codexFeaturesProbe: () => "hooks stable true\nplugin_hooks experimental true\n",
2068
+ }), /precondition changed/);
2069
+ assert.equal(injected, true, mutationKind);
2070
+ assert.deepEqual(await readFile(shimPath), foreignShim);
2071
+ assert.deepEqual(await readFile(hooksPath), hooksBefore);
2072
+ assert.equal(existsSync(configPath), false);
2073
+ });
2074
+ });
2075
+ }
2076
+ finally {
2077
+ resetFailureInjector?.();
2078
+ await rm(wd, { recursive: true, force: true });
2079
+ }
2080
+ }
2081
+ }
2082
+ finally {
2083
+ resetPlatform();
2084
+ }
2085
+ });
2086
+ it("rolls back when a preserved Windows shim drifts after staged cleanup finalization", async () => {
2087
+ const wd = await mkdtemp(join(tmpdir(), "omx-plugin-preserved-shim-finalization-"));
2088
+ const resetPlatform = setNativeHookTransactionPlatformForTest("win32");
2089
+ let resetFailureInjector;
1760
2090
  try {
1761
2091
  await withIsolatedUserHome(wd, async (codexHomeDir) => {
1762
2092
  await withTempCwd(wd, async () => {
1763
2093
  const hooksPath = join(codexHomeDir, "hooks.json");
1764
2094
  const configPath = join(codexHomeDir, "config.toml");
1765
- await writeFile(hooksPath, JSON.stringify({
2095
+ const shimPath = buildManagedCodexNativeHookWindowsShimPath(codexHomeDir);
2096
+ const managed = buildManagedCodexHooksConfig(packageRoot, {
2097
+ platform: "win32",
2098
+ codexHomeDir,
2099
+ });
2100
+ const hooksBefore = Buffer.from(`${JSON.stringify({
2101
+ ...managed,
1766
2102
  hooks: {
1767
- PostCompact: [
1768
- {
1769
- hooks: [
1770
- {
1771
- type: "command",
1772
- command: "/usr/bin/python3 /tmp/user-hook.py",
1773
- timeout: 5,
1774
- },
1775
- ],
1776
- },
1777
- ],
2103
+ ...managed.hooks,
2104
+ FutureEvent: [{
2105
+ hooks: [{ type: "command", command: "& $env:OMX_SHIM" }],
2106
+ }],
1778
2107
  },
1779
- }, null, 2) + "\n");
1780
- await writeFile(configPath, [
1781
- 'model = "gpt-5.6-sol"',
1782
- "",
1783
- `[hooks.state."${hooksPath}:post_compact:0:0"]`,
1784
- 'trusted_hash = "sha256:user"',
1785
- "enabled = false",
1786
- "",
1787
- ].join("\n"));
1788
- await setup({ scope: "user", installMode: "plugin" });
1789
- const config = await readFile(configPath, "utf-8");
1790
- assert.match(config, /^plugin_hooks = true$/m);
1791
- assert.match(config, /^trusted_hash = "sha256:user"$/m);
1792
- assert.match(config, /^enabled = false$/m);
1793
- assert.equal(config
1794
- .split(/\r?\n/)
1795
- .filter((line) => line.trim() ===
1796
- `[hooks.state."${hooksPath}:post_compact:0:0"]`).length, 1, "plugin setup must not duplicate preserved user hook trust state");
1797
- assert.doesNotThrow(() => parseToml(config));
2108
+ }, null, 2)}\n`, "utf-8");
2109
+ const foreignShim = Buffer.from("# concurrent finalization shim replacement\n", "utf-8");
2110
+ await writeFile(hooksPath, hooksBefore);
2111
+ await writeFile(shimPath, buildManagedCodexNativeHookWindowsShimContent(packageRoot));
2112
+ let injected = false;
2113
+ resetFailureInjector = setNativeHookTransactionFailureInjectorForTest((stage) => {
2114
+ if (stage !== "after_staged_cleanup" || injected)
2115
+ return;
2116
+ injected = true;
2117
+ writeFileSync(shimPath, foreignShim);
2118
+ });
2119
+ await assert.rejects(setup({
2120
+ scope: "user",
2121
+ installMode: "plugin",
2122
+ pluginAgentsMdPrompt: async () => false,
2123
+ skipNativeAgentRefresh: true,
2124
+ codexFeaturesProbe: () => "hooks stable true\nplugin_hooks experimental true\n",
2125
+ }), /precondition changed/);
2126
+ assert.equal(injected, true);
2127
+ assert.deepEqual(await readFile(shimPath), foreignShim);
2128
+ assert.deepEqual(await readFile(hooksPath), hooksBefore);
2129
+ assert.equal(existsSync(configPath), false);
1798
2130
  });
1799
2131
  });
1800
2132
  }
1801
2133
  finally {
2134
+ resetFailureInjector?.();
2135
+ resetPlatform();
1802
2136
  await rm(wd, { recursive: true, force: true });
1803
2137
  }
1804
2138
  });
1805
- it("honors persisted project-scoped plugin mode on repeat setup", async () => {
1806
- const wd = await mkdtemp(join(tmpdir(), "omx-setup-install-mode-"));
2139
+ it("preserves a drive-qualified shim for a drive-less future reference and aborts stale plugin transitions", async () => {
2140
+ const wd = await mkdtemp(join(tmpdir(), "omx-plugin-drive-qualified-future-shim-"));
2141
+ const resetPlatform = setNativeHookTransactionPlatformForTest("win32");
2142
+ let resetFailureInjector;
1807
2143
  try {
1808
- await withTempCwd(wd, async () => {
1809
- await setup({ scope: "project", installMode: "plugin" });
1810
- const persisted = JSON.parse(await readFile(join(wd, ".omx", "setup-scope.json"), "utf-8"));
1811
- assert.deepEqual(persisted, {
1812
- scope: "project",
2144
+ await withDriveQualifiedWindowsCodexHome(wd, async (codexHomeDir) => {
2145
+ const hooksPath = join(codexHomeDir, "hooks.json");
2146
+ const configPath = join(codexHomeDir, "config.toml");
2147
+ const shimPath = buildManagedCodexNativeHookWindowsShimPath(codexHomeDir);
2148
+ const managed = buildManagedCodexHooksConfig(packageRoot, {
2149
+ platform: "win32",
2150
+ codexHomeDir,
2151
+ });
2152
+ const driveLessFutureShimPath = "\\Users\\omx\\.codex\\hooks\\omx-native-hook-windows-shim.ps1";
2153
+ const futureHooks = `${JSON.stringify({
2154
+ ...managed,
2155
+ hooks: {
2156
+ ...managed.hooks,
2157
+ FutureEvent: [{
2158
+ hooks: [{
2159
+ type: "command",
2160
+ command: `& 'C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe' -NoProfile -ExecutionPolicy Bypass -File '${driveLessFutureShimPath}'`,
2161
+ }],
2162
+ }],
2163
+ },
2164
+ }, null, 2)}\n`;
2165
+ const pluginSetup = () => setup({
2166
+ scope: "user",
1813
2167
  installMode: "plugin",
1814
- mcpMode: "none",
2168
+ pluginAgentsMdPrompt: async () => false,
2169
+ skipNativeAgentRefresh: true,
2170
+ codexFeaturesProbe: () => "hooks stable true\nplugin_hooks experimental true\n",
1815
2171
  });
1816
- await setup({ scope: "project" });
1817
- assert.equal(existsSync(join(wd, ".codex", "skills", "ask", "SKILL.md")), false);
1818
- assert.equal(existsSync(join(wd, ".codex", "agents", "planner.toml")), true);
1819
- assert.equal(existsSync(join(wd, ".codex", "prompts", "executor.md")), false);
1820
- assert.equal(existsSync(join(wd, ".codex", "hooks.json")), false);
2172
+ await writeFile(hooksPath, futureHooks);
2173
+ await writeFile(shimPath, buildManagedCodexNativeHookWindowsShimContent(packageRoot));
2174
+ await pluginSetup();
2175
+ assert.equal(existsSync(shimPath), true);
2176
+ assert.match(await readFile(hooksPath, "utf-8"), /FutureEvent/);
2177
+ await writeFile(hooksPath, futureHooks);
2178
+ const hooksBefore = await readFile(hooksPath);
2179
+ const configBefore = await readFile(configPath);
2180
+ const foreignShim = Buffer.from("# concurrent foreign shim\n", "utf-8");
2181
+ let injected = false;
2182
+ resetFailureInjector = setNativeHookTransactionFailureInjectorForTest((stage, target) => {
2183
+ if (stage !== "before_precondition" || target.kind !== "shim" || injected) {
2184
+ return;
2185
+ }
2186
+ injected = true;
2187
+ writeFileSync(shimPath, foreignShim);
2188
+ });
2189
+ await assert.rejects(pluginSetup(), /precondition changed/);
2190
+ assert.equal(injected, true);
2191
+ assert.deepEqual(await readFile(shimPath), foreignShim);
2192
+ assert.deepEqual(await readFile(hooksPath), hooksBefore);
2193
+ assert.deepEqual(await readFile(configPath), configBefore);
1821
2194
  });
1822
2195
  }
1823
2196
  finally {
2197
+ resetFailureInjector?.();
2198
+ resetPlatform();
1824
2199
  await rm(wd, { recursive: true, force: true });
1825
2200
  }
1826
2201
  });
1827
- it("lets explicit project legacy setup clear persisted project plugin mode", async () => {
1828
- const wd = await mkdtemp(join(tmpdir(), "omx-setup-install-mode-"));
2202
+ it("preserves a shim referenced by a Unicode-escaped future hook event", async () => {
2203
+ const wd = await mkdtemp(join(tmpdir(), "omx-plugin-future-shim-"));
2204
+ const resetPlatform = setNativeHookTransactionPlatformForTest("win32");
1829
2205
  try {
1830
- await withTempCwd(wd, async () => {
1831
- await mkdir(join(wd, ".omx"), { recursive: true });
1832
- await writeFile(join(wd, ".omx", "setup-scope.json"), JSON.stringify({ scope: "project", installMode: "plugin" }));
1833
- await setup({ scope: "project", installMode: "legacy" });
1834
- const persisted = JSON.parse(await readFile(join(wd, ".omx", "setup-scope.json"), "utf-8"));
1835
- assert.deepEqual(persisted, { scope: "project", mcpMode: "none" });
1836
- assert.equal(existsSync(join(wd, ".codex", "skills", "ask", "SKILL.md")), true);
1837
- assert.equal(existsSync(join(wd, ".codex", "agents", "planner.toml")), true);
1838
- assert.equal(existsSync(join(wd, ".codex", "prompts", "executor.md")), true);
2206
+ await withIsolatedUserHome(wd, async (codexHomeDir) => {
2207
+ await withTempCwd(wd, async () => {
2208
+ const hooksPath = join(codexHomeDir, "hooks.json");
2209
+ const shimPath = buildManagedCodexNativeHookWindowsShimPath(codexHomeDir);
2210
+ const managed = buildManagedCodexHooksConfig(packageRoot, {
2211
+ platform: "win32",
2212
+ codexHomeDir,
2213
+ });
2214
+ const sentinel = "__OMX_ESCAPED_SHIM_COMMAND__";
2215
+ const futureHooks = JSON.stringify({
2216
+ ...managed,
2217
+ hooks: {
2218
+ ...managed.hooks,
2219
+ FutureEvent: [{ hooks: [{ type: "command", command: sentinel }] }],
2220
+ },
2221
+ }, null, 2).replace(JSON.stringify(sentinel), JSON.stringify(`& 'C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe' -NoProfile -ExecutionPolicy Bypass -File '${shimPath}'`).replace(/\\\\/g, "\\u005c")) + "\n";
2222
+ await writeFile(hooksPath, futureHooks);
2223
+ await writeFile(shimPath, buildManagedCodexNativeHookWindowsShimContent(packageRoot));
2224
+ await setup({
2225
+ scope: "user",
2226
+ installMode: "plugin",
2227
+ pluginAgentsMdPrompt: async () => false,
2228
+ skipNativeAgentRefresh: true,
2229
+ codexFeaturesProbe: () => "hooks stable true\nplugin_hooks experimental true\n",
2230
+ });
2231
+ assert.equal(existsSync(shimPath), true);
2232
+ const finalHooks = await readFile(hooksPath, "utf-8");
2233
+ assert.match(finalHooks, /FutureEvent/);
2234
+ assert.match(finalHooks, /\\u005c/);
2235
+ assert.doesNotMatch(finalHooks, /codex-native-hook\.js/);
2236
+ });
1839
2237
  });
1840
2238
  }
1841
2239
  finally {
2240
+ resetPlatform();
1842
2241
  await rm(wd, { recursive: true, force: true });
1843
2242
  }
1844
2243
  });
1845
- it("lets explicit user legacy setup override persisted user plugin mode", async () => {
1846
- const wd = await mkdtemp(join(tmpdir(), "omx-setup-install-mode-"));
2244
+ it("preserves a shim referenced through a normalized Windows path alias", async () => {
2245
+ const wd = await mkdtemp(join(tmpdir(), "omx-plugin-shim-path-alias-"));
2246
+ const resetPlatform = setNativeHookTransactionPlatformForTest("win32");
1847
2247
  try {
1848
2248
  await withIsolatedUserHome(wd, async (codexHomeDir) => {
1849
2249
  await withTempCwd(wd, async () => {
1850
- await mkdir(join(wd, ".omx"), { recursive: true });
1851
- await writeFile(join(wd, ".omx", "setup-scope.json"), JSON.stringify({ scope: "user", installMode: "plugin" }));
1852
- await setup({ installMode: "legacy" });
1853
- const persisted = JSON.parse(await readFile(join(wd, ".omx", "setup-scope.json"), "utf-8"));
1854
- assert.deepEqual(persisted, {
2250
+ const hooksPath = join(codexHomeDir, "hooks.json");
2251
+ const shimPath = buildManagedCodexNativeHookWindowsShimPath(codexHomeDir);
2252
+ const shimAlias = shimPath
2253
+ .replace(/[\\/]+hooks[\\/]+/i, "\\hooks\\\\.\\")
2254
+ .replace(/omx-native-hook-windows-shim\.ps1$/i, "OMX-NATIVE-HOOK-WINDOWS-SHIM.PS1")
2255
+ .replace(/\\/g, "/");
2256
+ const managed = buildManagedCodexHooksConfig(packageRoot, {
2257
+ platform: "win32",
2258
+ codexHomeDir,
2259
+ });
2260
+ await writeFile(hooksPath, JSON.stringify({
2261
+ ...managed,
2262
+ hooks: {
2263
+ ...managed.hooks,
2264
+ FutureEvent: [{
2265
+ hooks: [{
2266
+ type: "command",
2267
+ command: `& 'C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe' -NoProfile -ExecutionPolicy Bypass -File '${shimAlias}'`,
2268
+ }],
2269
+ }],
2270
+ },
2271
+ }, null, 2) + "\n");
2272
+ await writeFile(shimPath, buildManagedCodexNativeHookWindowsShimContent(packageRoot));
2273
+ await setup({
1855
2274
  scope: "user",
1856
- installMode: "legacy",
1857
- mcpMode: "none",
2275
+ installMode: "plugin",
2276
+ pluginAgentsMdPrompt: async () => false,
2277
+ skipNativeAgentRefresh: true,
2278
+ codexFeaturesProbe: () => "hooks stable true\nplugin_hooks experimental true\n",
1858
2279
  });
1859
- assert.equal(existsSync(join(codexHomeDir, "skills", "ask", "SKILL.md")), true);
1860
- assert.equal(existsSync(join(codexHomeDir, "agents", "planner.toml")), true);
1861
- assert.equal(existsSync(join(codexHomeDir, "prompts", "executor.md")), true);
2280
+ assert.equal(existsSync(shimPath), true);
2281
+ assert.match(await readFile(hooksPath, "utf-8"), /FutureEvent/);
1862
2282
  });
1863
2283
  });
1864
2284
  }
1865
2285
  finally {
2286
+ resetPlatform();
1866
2287
  await rm(wd, { recursive: true, force: true });
1867
2288
  }
1868
2289
  });
1869
- it("dedupes plugin-mode hook trust state when switching user setup back to legacy", async () => {
1870
- const wd = await mkdtemp(join(tmpdir(), "omx-setup-install-mode-"));
2290
+ it("preserves a shim for a distinct absolute -File target", async () => {
2291
+ const wd = await mkdtemp(join(tmpdir(), "omx-plugin-quoted-inert-shim-"));
2292
+ const resetPlatform = setNativeHookTransactionPlatformForTest("win32");
1871
2293
  try {
1872
2294
  await withIsolatedUserHome(wd, async (codexHomeDir) => {
1873
2295
  await withTempCwd(wd, async () => {
1874
- const configPath = join(codexHomeDir, "config.toml");
1875
- await setup({ scope: "user", installMode: "plugin", force: true });
1876
- const pluginConfig = await readFile(configPath, "utf-8");
1877
- const staleUnfencedPluginConfig = pluginConfig
1878
- .split(/\r?\n/)
1879
- .filter((line) => line.trim() !== "# OMX-owned Codex hook trust state" &&
1880
- line.trim() !==
1881
- "# Trusts only setup-managed codex-native-hook.js wrappers." &&
1882
- line.trim() !== "# End OMX-owned Codex hook trust state")
1883
- .join("\n");
1884
- await writeFile(configPath, staleUnfencedPluginConfig);
1885
- assert.doesNotThrow(() => parseToml(staleUnfencedPluginConfig));
1886
- await setup({ scope: "user", installMode: "legacy", force: true });
1887
- const legacyConfig = await readFile(configPath, "utf-8");
1888
- assert.doesNotThrow(() => parseToml(legacyConfig));
1889
- assert.equal(legacyConfig
1890
- .split(/\r?\n/)
1891
- .filter((line) => line.trim() ===
1892
- `[hooks.state."${join(codexHomeDir, "hooks.json")}:post_compact:0:0"]`).length, 1, "legacy setup should replace stale plugin-mode hook trust state instead of duplicating it");
1893
- assert.match(legacyConfig, /# OMX-owned Codex hook trust state[\s\S]*# End OMX-owned Codex hook trust state/);
1894
- const persisted = JSON.parse(await readFile(join(wd, ".omx", "setup-scope.json"), "utf-8"));
1895
- assert.deepEqual(persisted, {
2296
+ const hooksPath = join(codexHomeDir, "hooks.json");
2297
+ const shimPath = buildManagedCodexNativeHookWindowsShimPath(codexHomeDir);
2298
+ const managed = buildManagedCodexHooksConfig(packageRoot, {
2299
+ platform: "win32",
2300
+ codexHomeDir,
2301
+ });
2302
+ await writeFile(hooksPath, JSON.stringify({
2303
+ ...managed,
2304
+ hooks: {
2305
+ ...managed.hooks,
2306
+ FutureEvent: [{
2307
+ hooks: [{
2308
+ type: "command",
2309
+ command: "& 'C:\\%OMX_INERT%\\powershell.exe' -NoProfile -ExecutionPolicy Bypass -File 'D:\\tools\\unrelated-$env:INERT.ps1'",
2310
+ }],
2311
+ }],
2312
+ },
2313
+ }, null, 2) + "\n");
2314
+ await writeFile(shimPath, buildManagedCodexNativeHookWindowsShimContent(packageRoot));
2315
+ await setup({
1896
2316
  scope: "user",
1897
- installMode: "legacy",
1898
- mcpMode: "none",
2317
+ installMode: "plugin",
2318
+ pluginAgentsMdPrompt: async () => false,
2319
+ skipNativeAgentRefresh: true,
2320
+ codexFeaturesProbe: () => "hooks stable true\nplugin_hooks experimental true\n",
1899
2321
  });
2322
+ assert.equal(existsSync(shimPath), true);
2323
+ assert.match(await readFile(hooksPath, "utf-8"), /FutureEvent/);
1900
2324
  });
1901
2325
  });
1902
2326
  }
1903
2327
  finally {
2328
+ resetPlatform();
1904
2329
  await rm(wd, { recursive: true, force: true });
1905
2330
  }
1906
2331
  });
1907
- it("uses project-scoped plugin hooks when plugin mode is explicitly requested", async () => {
1908
- const wd = await mkdtemp(join(tmpdir(), "omx-setup-install-mode-"));
2332
+ it("does not retain a shim for inert future-event metadata", async () => {
2333
+ const wd = await mkdtemp(join(tmpdir(), "omx-plugin-future-shim-metadata-"));
2334
+ const resetPlatform = setNativeHookTransactionPlatformForTest("win32");
1909
2335
  try {
1910
- await withTempCwd(wd, async () => {
1911
- await setup({ scope: "project", installMode: "plugin" });
1912
- await assertProjectPluginModeArtifacts(wd);
2336
+ await withIsolatedUserHome(wd, async (codexHomeDir) => {
2337
+ await withTempCwd(wd, async () => {
2338
+ const hooksPath = join(codexHomeDir, "hooks.json");
2339
+ const shimPath = buildManagedCodexNativeHookWindowsShimPath(codexHomeDir);
2340
+ const managed = buildManagedCodexHooksConfig(packageRoot, {
2341
+ platform: "win32",
2342
+ codexHomeDir,
2343
+ });
2344
+ await writeFile(hooksPath, JSON.stringify({
2345
+ ...managed,
2346
+ hooks: {
2347
+ ...managed.hooks,
2348
+ FutureEvent: [
2349
+ {
2350
+ metadata: shimPath,
2351
+ hooks: [{ type: "agent", prompt: shimPath }],
2352
+ },
2353
+ ],
2354
+ },
2355
+ }, null, 2) + "\n");
2356
+ await writeFile(shimPath, buildManagedCodexNativeHookWindowsShimContent(packageRoot));
2357
+ await setup({
2358
+ scope: "user",
2359
+ installMode: "plugin",
2360
+ pluginAgentsMdPrompt: async () => false,
2361
+ skipNativeAgentRefresh: true,
2362
+ codexFeaturesProbe: () => "hooks stable true\nplugin_hooks experimental true\n",
2363
+ });
2364
+ assert.equal(existsSync(shimPath), false);
2365
+ assert.match(await readFile(hooksPath, "utf-8"), /FutureEvent/);
2366
+ });
1913
2367
  });
1914
2368
  }
1915
2369
  finally {
2370
+ resetPlatform();
1916
2371
  await rm(wd, { recursive: true, force: true });
1917
2372
  }
1918
2373
  });
1919
- it("honors persisted project plugin mode on repeat setup", async () => {
1920
- const wd = await mkdtemp(join(tmpdir(), "omx-setup-install-mode-"));
2374
+ it("completes Windows native-hook transactions when regular-file fsync reports EPERM", async () => {
2375
+ const wd = await mkdtemp(join(tmpdir(), "omx-setup-windows-fsync-eperm-"));
2376
+ const resetPlatform = setNativeHookTransactionPlatformForTest("win32");
2377
+ let syncCalls = 0;
2378
+ const stderr = [];
2379
+ const originalStderrWrite = process.stderr.write;
2380
+ process.stderr.write = ((chunk) => {
2381
+ stderr.push(String(chunk));
2382
+ return true;
2383
+ });
2384
+ const resetSync = setNativeHookTransactionRegularFileSyncForTest(async (platform) => {
2385
+ assert.equal(platform, "win32");
2386
+ syncCalls += 1;
2387
+ throw Object.assign(new Error("EPERM: fsync"), { code: "EPERM" });
2388
+ });
1921
2389
  try {
1922
- await withTempCwd(wd, async () => {
1923
- await setup({ scope: "project", installMode: "plugin" });
1924
- await setup();
1925
- await assertProjectPluginModeArtifacts(wd);
2390
+ await withIsolatedUserHome(wd, async (codexHomeDir) => {
2391
+ await withTempCwd(wd, async () => {
2392
+ await setup({
2393
+ scope: "user",
2394
+ installMode: "legacy",
2395
+ skipNativeAgentRefresh: true,
2396
+ });
2397
+ assert.ok(syncCalls > 0);
2398
+ assert.equal(existsSync(join(codexHomeDir, "hooks.json")), true);
2399
+ assert.equal(existsSync(buildManagedCodexNativeHookWindowsShimPath(codexHomeDir)), true);
2400
+ assert.equal(existsSync(join(codexHomeDir, "config.toml")), true);
2401
+ });
2402
+ assert.equal(stderr.filter((line) => line.includes("native-hook setup")).length, 1);
1926
2403
  });
1927
2404
  }
1928
2405
  finally {
2406
+ process.stderr.write = originalStderrWrite;
2407
+ resetSync();
2408
+ resetPlatform();
1929
2409
  await rm(wd, { recursive: true, force: true });
1930
2410
  }
1931
2411
  });
1932
- it("prints plugin-mode next steps without legacy-only claims", async () => {
1933
- const wd = await mkdtemp(join(tmpdir(), "omx-setup-install-mode-"));
2412
+ it("orders Windows plugin-transition mutations as hooks, shim, then config", async () => {
2413
+ const wd = await mkdtemp(join(tmpdir(), "omx-setup-windows-hook-removal-"));
2414
+ const forwardOrder = [];
2415
+ const resetPlatform = setNativeHookTransactionPlatformForTest("win32");
2416
+ let resetFailureInjector;
1934
2417
  try {
1935
- await withIsolatedUserHome(wd, async () => {
2418
+ await withIsolatedUserHome(wd, async (codexHomeDir) => {
1936
2419
  await withTempCwd(wd, async () => {
1937
- const pluginOutput = await captureConsoleOutput(async () => {
1938
- await setup({ scope: "project", installMode: "plugin" });
2420
+ await setup({
2421
+ scope: "user",
2422
+ installMode: "legacy",
2423
+ skipNativeAgentRefresh: true,
1939
2424
  });
1940
- assert.match(pluginOutput, /Using setup install mode: plugin/);
1941
- assert.match(pluginOutput, /Plugin-scoped Codex hooks and runtime feature flags refresh complete .*plugin_hooks, goals/);
1942
- assert.doesNotMatch(pluginOutput, /user-scope skill delivery mode/);
1943
- assert.doesNotMatch(pluginOutput, /use explicit agent_type when spawning OMX roles/);
1944
- assert.doesNotMatch(pluginOutput, /Use role\/workflow keywords like \$architect, \$executor, and \$plan/);
1945
- assert.doesNotMatch(pluginOutput, /AGENTS keyword routing can also activate them implicitly/);
1946
- assert.doesNotMatch(pluginOutput, /The AGENTS\.md orchestration brain is loaded automatically/);
1947
- assert.match(pluginOutput, /Registered Codex marketplace oh-my-codex-local supplies OMX skills and workflow surfaces/);
1948
- assert.match(pluginOutput, /Browse plugin-provided skills with \/skills/);
1949
- assert.match(pluginOutput, /Plugin-mode AGENTS\.md defaults provide persistent orchestration guidance; developer_instructions is an optional bootstrap/);
1950
- const legacyWd = join(wd, "legacy");
2425
+ const hooksPath = join(codexHomeDir, "hooks.json");
2426
+ const shimPath = buildManagedCodexNativeHookWindowsShimPath(codexHomeDir);
2427
+ assert.equal(existsSync(hooksPath), true);
2428
+ assert.equal(existsSync(shimPath), true);
2429
+ resetFailureInjector = setNativeHookTransactionFailureInjectorForTest((stage, artifact) => {
2430
+ if (stage === "before_temp_write")
2431
+ forwardOrder.push(artifact.kind);
2432
+ });
2433
+ await setup({
2434
+ scope: "user",
2435
+ installMode: "plugin",
2436
+ skipNativeAgentRefresh: true,
2437
+ pluginAgentsMdPrompt: async () => false,
2438
+ codexFeaturesProbe: () => [
2439
+ "hooks stable true",
2440
+ "plugin_hooks experimental true",
2441
+ "",
2442
+ ].join("\n"),
2443
+ });
2444
+ assert.deepEqual(forwardOrder, ["hooks", "shim", "config"]);
2445
+ assert.equal(existsSync(hooksPath), false);
2446
+ assert.equal(existsSync(shimPath), false);
2447
+ });
2448
+ });
2449
+ }
2450
+ finally {
2451
+ resetFailureInjector?.();
2452
+ resetPlatform();
2453
+ await rm(wd, { recursive: true, force: true });
2454
+ }
2455
+ });
2456
+ it("rolls back Windows plugin-transition artifacts in exact reverse order", async () => {
2457
+ const wd = await mkdtemp(join(tmpdir(), "omx-setup-windows-hook-rollback-"));
2458
+ const forwardOrder = [];
2459
+ const restorationOrder = [];
2460
+ const resetPlatform = setNativeHookTransactionPlatformForTest("win32");
2461
+ let resetFailureInjector;
2462
+ try {
2463
+ await withIsolatedUserHome(wd, async (codexHomeDir) => {
2464
+ await withTempCwd(wd, async () => {
2465
+ await setup({
2466
+ scope: "user",
2467
+ installMode: "legacy",
2468
+ skipNativeAgentRefresh: true,
2469
+ });
2470
+ const hooksPath = join(codexHomeDir, "hooks.json");
2471
+ const shimPath = buildManagedCodexNativeHookWindowsShimPath(codexHomeDir);
2472
+ const hooksBefore = await readFile(hooksPath);
2473
+ const shimBefore = await readFile(shimPath);
2474
+ const configBefore = await readFile(join(codexHomeDir, "config.toml"));
2475
+ resetFailureInjector = setNativeHookTransactionFailureInjectorForTest((stage, artifact) => {
2476
+ if (stage === "before_temp_write")
2477
+ forwardOrder.push(artifact.kind);
2478
+ if (stage === "before_readback" && artifact.kind === "config") {
2479
+ throw new Error("injected Windows config replacement failure");
2480
+ }
2481
+ if (stage === "before_rollback_rename") {
2482
+ restorationOrder.push(artifact.kind);
2483
+ }
2484
+ });
2485
+ await assert.rejects(setup({
2486
+ scope: "user",
2487
+ installMode: "plugin",
2488
+ skipNativeAgentRefresh: true,
2489
+ pluginAgentsMdPrompt: async () => false,
2490
+ codexFeaturesProbe: () => [
2491
+ "hooks stable true",
2492
+ "plugin_hooks experimental true",
2493
+ "",
2494
+ ].join("\n"),
2495
+ }), /rolled back: injected Windows config replacement failure/);
2496
+ assert.deepEqual(forwardOrder, ["hooks", "shim", "config"]);
2497
+ assert.deepEqual(restorationOrder, ["config", "shim", "hooks"]);
2498
+ assert.deepEqual(await readFile(join(codexHomeDir, "config.toml")), configBefore);
2499
+ assert.deepEqual(await readFile(shimPath), shimBefore);
2500
+ assert.deepEqual(await readFile(hooksPath), hooksBefore);
2501
+ });
2502
+ });
2503
+ }
2504
+ finally {
2505
+ resetFailureInjector?.();
2506
+ resetPlatform();
2507
+ await rm(wd, { recursive: true, force: true });
2508
+ }
2509
+ });
2510
+ it("preserves foreign native hook enablement when transitioning from plugin fallback", async () => {
2511
+ const wd = await mkdtemp(join(tmpdir(), "omx-setup-install-mode-"));
2512
+ try {
2513
+ await withIsolatedUserHome(wd, async (codexHomeDir) => {
2514
+ await withTempCwd(wd, async () => {
2515
+ await setup({
2516
+ scope: "user",
2517
+ installMode: "plugin",
2518
+ codexFeaturesProbe: () => "hooks stable true\n",
2519
+ });
2520
+ const hooksPath = join(codexHomeDir, "hooks.json");
2521
+ const existingHooks = JSON.parse(await readFile(hooksPath, "utf-8"));
2522
+ const foreignHookGroup = {
2523
+ hooks: [
2524
+ {
2525
+ type: "command",
2526
+ command: "/usr/bin/python3 /tmp/foreign-hook.py",
2527
+ timeout: 5,
2528
+ },
2529
+ ],
2530
+ };
2531
+ const existingRegistrations = existingHooks.hooks ?? {};
2532
+ existingHooks.hooks = {
2533
+ ...existingRegistrations,
2534
+ UserPromptSubmit: [
2535
+ foreignHookGroup,
2536
+ ...(existingRegistrations.UserPromptSubmit ?? []),
2537
+ ],
2538
+ };
2539
+ await writeFile(hooksPath, JSON.stringify(existingHooks, null, 2) + "\n");
2540
+ await setup({ scope: "user", installMode: "plugin" });
2541
+ const finalHooksContent = await readFile(hooksPath, "utf-8");
2542
+ const config = await readFile(join(codexHomeDir, "config.toml"), "utf-8");
2543
+ assert.match(finalHooksContent, /foreign-hook\.py/);
2544
+ assert.doesNotMatch(finalHooksContent, /codex-native-hook\.js/);
2545
+ assert.match(config, /^plugin_hooks = true$/m);
2546
+ assert.match(config, /^hooks = true$/m);
2547
+ });
2548
+ });
2549
+ }
2550
+ finally {
2551
+ await rm(wd, { recursive: true, force: true });
2552
+ }
2553
+ });
2554
+ it("keeps native hooks enabled for pre-existing foreign hooks in plugin-scoped setup", async () => {
2555
+ const wd = await mkdtemp(join(tmpdir(), "omx-setup-install-mode-"));
2556
+ try {
2557
+ await withIsolatedUserHome(wd, async (codexHomeDir) => {
2558
+ await withTempCwd(wd, async () => {
2559
+ const hooksPath = join(codexHomeDir, "hooks.json");
2560
+ const foreignHookGroup = {
2561
+ hooks: [
2562
+ {
2563
+ type: "command",
2564
+ command: "/usr/bin/python3 /tmp/foreign-hook.py",
2565
+ timeout: 5,
2566
+ },
2567
+ ],
2568
+ };
2569
+ await writeFile(hooksPath, JSON.stringify({ hooks: { UserPromptSubmit: [foreignHookGroup] } }, null, 2) + "\n");
2570
+ await setup({ scope: "user", installMode: "plugin" });
2571
+ const finalHooksContent = await readFile(hooksPath, "utf-8");
2572
+ assert.match(finalHooksContent, /"UserPromptSubmit"/);
2573
+ assert.match(finalHooksContent, /foreign-hook\.py/);
2574
+ assert.doesNotMatch(finalHooksContent, /codex-native-hook\.js/);
2575
+ const config = await readFile(join(codexHomeDir, "config.toml"), "utf-8");
2576
+ assert.match(config, /^plugin_hooks = true$/m);
2577
+ assert.match(config, /^hooks = true$/m, "plugin-scoped setup must keep native hooks enabled for foreign hooks");
2578
+ });
2579
+ });
2580
+ }
2581
+ finally {
2582
+ await rm(wd, { recursive: true, force: true });
2583
+ }
2584
+ });
2585
+ it("preserves same-key user hook trust state in plugin-scoped setup", async () => {
2586
+ const wd = await mkdtemp(join(tmpdir(), "omx-setup-install-mode-"));
2587
+ try {
2588
+ await withIsolatedUserHome(wd, async (codexHomeDir) => {
2589
+ await withTempCwd(wd, async () => {
2590
+ const hooksPath = join(codexHomeDir, "hooks.json");
2591
+ const configPath = join(codexHomeDir, "config.toml");
2592
+ await writeFile(hooksPath, JSON.stringify({
2593
+ hooks: {
2594
+ PostCompact: [
2595
+ {
2596
+ hooks: [
2597
+ {
2598
+ type: "command",
2599
+ command: "/usr/bin/python3 /tmp/user-hook.py",
2600
+ timeout: 5,
2601
+ },
2602
+ ],
2603
+ },
2604
+ ],
2605
+ },
2606
+ }, null, 2) + "\n");
2607
+ await writeFile(configPath, [
2608
+ 'model = "gpt-5.6-sol"',
2609
+ "",
2610
+ `[hooks.state."${hooksPath}:post_compact:0:0"]`,
2611
+ 'trusted_hash = "sha256:user"',
2612
+ "enabled = false",
2613
+ "",
2614
+ ].join("\n"));
2615
+ await setup({ scope: "user", installMode: "plugin" });
2616
+ const config = await readFile(configPath, "utf-8");
2617
+ assert.match(config, /^plugin_hooks = true$/m);
2618
+ assert.match(config, /^trusted_hash = "sha256:user"$/m);
2619
+ assert.match(config, /^enabled = false$/m);
2620
+ assert.equal(config
2621
+ .split(/\r?\n/)
2622
+ .filter((line) => line.trim() ===
2623
+ `[hooks.state."${hooksPath}:post_compact:0:0"]`).length, 1, "plugin setup must not duplicate preserved user hook trust state");
2624
+ assert.doesNotThrow(() => parseToml(config));
2625
+ });
2626
+ });
2627
+ }
2628
+ finally {
2629
+ await rm(wd, { recursive: true, force: true });
2630
+ }
2631
+ });
2632
+ it("honors persisted project-scoped plugin mode on repeat setup", async () => {
2633
+ const wd = await mkdtemp(join(tmpdir(), "omx-setup-install-mode-"));
2634
+ try {
2635
+ await withTempCwd(wd, async () => {
2636
+ await setup({ scope: "project", installMode: "plugin" });
2637
+ const persisted = JSON.parse(await readFile(join(wd, ".omx", "setup-scope.json"), "utf-8"));
2638
+ assert.deepEqual(persisted, {
2639
+ scope: "project",
2640
+ installMode: "plugin",
2641
+ mcpMode: "none",
2642
+ });
2643
+ await setup({ scope: "project" });
2644
+ assert.equal(existsSync(join(wd, ".codex", "skills", "ask", "SKILL.md")), false);
2645
+ assert.equal(existsSync(join(wd, ".codex", "agents", "planner.toml")), true);
2646
+ assert.equal(existsSync(join(wd, ".codex", "prompts", "executor.md")), false);
2647
+ assert.equal(existsSync(join(wd, ".codex", "hooks.json")), false);
2648
+ });
2649
+ }
2650
+ finally {
2651
+ await rm(wd, { recursive: true, force: true });
2652
+ }
2653
+ });
2654
+ it("lets explicit project legacy setup clear persisted project plugin mode", async () => {
2655
+ const wd = await mkdtemp(join(tmpdir(), "omx-setup-install-mode-"));
2656
+ try {
2657
+ await withTempCwd(wd, async () => {
2658
+ await mkdir(join(wd, ".omx"), { recursive: true });
2659
+ await writeFile(join(wd, ".omx", "setup-scope.json"), JSON.stringify({ scope: "project", installMode: "plugin" }));
2660
+ await setup({ scope: "project", installMode: "legacy" });
2661
+ const persisted = JSON.parse(await readFile(join(wd, ".omx", "setup-scope.json"), "utf-8"));
2662
+ assert.deepEqual(persisted, { scope: "project", mcpMode: "none" });
2663
+ assert.equal(existsSync(join(wd, ".codex", "skills", "ask", "SKILL.md")), true);
2664
+ assert.equal(existsSync(join(wd, ".codex", "agents", "planner.toml")), true);
2665
+ assert.equal(existsSync(join(wd, ".codex", "prompts", "executor.md")), true);
2666
+ });
2667
+ }
2668
+ finally {
2669
+ await rm(wd, { recursive: true, force: true });
2670
+ }
2671
+ });
2672
+ it("lets explicit user legacy setup override persisted user plugin mode", async () => {
2673
+ const wd = await mkdtemp(join(tmpdir(), "omx-setup-install-mode-"));
2674
+ try {
2675
+ await withIsolatedUserHome(wd, async (codexHomeDir) => {
2676
+ await withTempCwd(wd, async () => {
2677
+ await mkdir(join(wd, ".omx"), { recursive: true });
2678
+ await writeFile(join(wd, ".omx", "setup-scope.json"), JSON.stringify({ scope: "user", installMode: "plugin" }));
2679
+ await setup({ installMode: "legacy" });
2680
+ const persisted = JSON.parse(await readFile(join(wd, ".omx", "setup-scope.json"), "utf-8"));
2681
+ assert.deepEqual(persisted, {
2682
+ scope: "user",
2683
+ installMode: "legacy",
2684
+ mcpMode: "none",
2685
+ });
2686
+ assert.equal(existsSync(join(codexHomeDir, "skills", "ask", "SKILL.md")), true);
2687
+ assert.equal(existsSync(join(codexHomeDir, "agents", "planner.toml")), true);
2688
+ assert.equal(existsSync(join(codexHomeDir, "prompts", "executor.md")), true);
2689
+ });
2690
+ });
2691
+ }
2692
+ finally {
2693
+ await rm(wd, { recursive: true, force: true });
2694
+ }
2695
+ });
2696
+ it("dedupes plugin-mode hook trust state when switching user setup back to legacy", async () => {
2697
+ const wd = await mkdtemp(join(tmpdir(), "omx-setup-install-mode-"));
2698
+ try {
2699
+ await withIsolatedUserHome(wd, async (codexHomeDir) => {
2700
+ await withTempCwd(wd, async () => {
2701
+ const configPath = join(codexHomeDir, "config.toml");
2702
+ await setup({ scope: "user", installMode: "plugin", force: true });
2703
+ const pluginConfig = await readFile(configPath, "utf-8");
2704
+ const staleUnfencedPluginConfig = pluginConfig
2705
+ .split(/\r?\n/)
2706
+ .filter((line) => line.trim() !== "# OMX-owned Codex hook trust state" &&
2707
+ line.trim() !==
2708
+ "# Trusts only setup-managed codex-native-hook.js wrappers." &&
2709
+ line.trim() !== "# End OMX-owned Codex hook trust state")
2710
+ .join("\n");
2711
+ await writeFile(configPath, staleUnfencedPluginConfig);
2712
+ assert.doesNotThrow(() => parseToml(staleUnfencedPluginConfig));
2713
+ await setup({ scope: "user", installMode: "legacy", force: true });
2714
+ const legacyConfig = await readFile(configPath, "utf-8");
2715
+ assert.doesNotThrow(() => parseToml(legacyConfig));
2716
+ assert.equal(legacyConfig
2717
+ .split(/\r?\n/)
2718
+ .filter((line) => line.trim() ===
2719
+ `[hooks.state."${join(codexHomeDir, "hooks.json")}:post_compact:0:0"]`).length, 1, "legacy setup should replace stale plugin-mode hook trust state instead of duplicating it");
2720
+ assert.match(legacyConfig, /# OMX-owned Codex hook trust state[\s\S]*# End OMX-owned Codex hook trust state/);
2721
+ const persisted = JSON.parse(await readFile(join(wd, ".omx", "setup-scope.json"), "utf-8"));
2722
+ assert.deepEqual(persisted, {
2723
+ scope: "user",
2724
+ installMode: "legacy",
2725
+ mcpMode: "none",
2726
+ });
2727
+ });
2728
+ });
2729
+ }
2730
+ finally {
2731
+ await rm(wd, { recursive: true, force: true });
2732
+ }
2733
+ });
2734
+ it("uses project-scoped plugin hooks when plugin mode is explicitly requested", async () => {
2735
+ const wd = await mkdtemp(join(tmpdir(), "omx-setup-install-mode-"));
2736
+ try {
2737
+ await withTempCwd(wd, async () => {
2738
+ await setup({ scope: "project", installMode: "plugin" });
2739
+ await assertProjectPluginModeArtifacts(wd);
2740
+ });
2741
+ }
2742
+ finally {
2743
+ await rm(wd, { recursive: true, force: true });
2744
+ }
2745
+ });
2746
+ it("honors persisted project plugin mode on repeat setup", async () => {
2747
+ const wd = await mkdtemp(join(tmpdir(), "omx-setup-install-mode-"));
2748
+ try {
2749
+ await withTempCwd(wd, async () => {
2750
+ await setup({ scope: "project", installMode: "plugin" });
2751
+ await setup();
2752
+ await assertProjectPluginModeArtifacts(wd);
2753
+ });
2754
+ }
2755
+ finally {
2756
+ await rm(wd, { recursive: true, force: true });
2757
+ }
2758
+ });
2759
+ it("prints plugin-mode next steps without legacy-only claims", async () => {
2760
+ const wd = await mkdtemp(join(tmpdir(), "omx-setup-install-mode-"));
2761
+ try {
2762
+ await withIsolatedUserHome(wd, async () => {
2763
+ await withTempCwd(wd, async () => {
2764
+ const pluginOutput = await captureConsoleOutput(async () => {
2765
+ await setup({ scope: "project", installMode: "plugin" });
2766
+ });
2767
+ assert.match(pluginOutput, /Using setup install mode: plugin/);
2768
+ assert.match(pluginOutput, /Plugin-scoped Codex hooks and runtime feature flags refresh complete .*plugin_hooks, goals/);
2769
+ assert.doesNotMatch(pluginOutput, /user-scope skill delivery mode/);
2770
+ assert.doesNotMatch(pluginOutput, /use explicit agent_type when spawning OMX roles/);
2771
+ assert.doesNotMatch(pluginOutput, /Use role\/workflow keywords like \$architect, \$executor, and \$plan/);
2772
+ assert.doesNotMatch(pluginOutput, /AGENTS keyword routing can also activate them implicitly/);
2773
+ assert.doesNotMatch(pluginOutput, /The AGENTS\.md orchestration brain is loaded automatically/);
2774
+ assert.match(pluginOutput, /Registered Codex marketplace oh-my-codex-local supplies OMX skills and workflow surfaces/);
2775
+ assert.match(pluginOutput, /Browse plugin-provided skills with \/skills/);
2776
+ assert.match(pluginOutput, /Plugin-mode AGENTS\.md defaults provide persistent orchestration guidance; developer_instructions is an optional bootstrap/);
2777
+ const legacyWd = join(wd, "legacy");
1951
2778
  await mkdir(legacyWd, { recursive: true });
1952
2779
  await withTempCwd(legacyWd, async () => {
1953
2780
  const legacyOutput = await captureConsoleOutput(async () => {
1954
2781
  await setup({ scope: "user", installMode: "legacy" });
1955
2782
  });
1956
- assert.match(legacyOutput, /Native agent role TOML files written to \.codex\/agents\//);
1957
- assert.match(legacyOutput, /Use role\/workflow keywords like \$architect, \$executor, and \$plan/);
1958
- assert.match(legacyOutput, /AGENTS keyword routing can also activate them implicitly/);
1959
- assert.match(legacyOutput, /The AGENTS\.md orchestration brain is loaded automatically/);
2783
+ assert.match(legacyOutput, /Native agent role TOML files written to \.codex\/agents\//);
2784
+ assert.match(legacyOutput, /Use role\/workflow keywords like \$architect, \$executor, and \$plan/);
2785
+ assert.match(legacyOutput, /AGENTS keyword routing can also activate them implicitly/);
2786
+ assert.match(legacyOutput, /The AGENTS\.md orchestration brain is loaded automatically/);
2787
+ });
2788
+ });
2789
+ });
2790
+ }
2791
+ finally {
2792
+ await rm(wd, { recursive: true, force: true });
2793
+ }
2794
+ });
2795
+ it("removes legacy user components when plugin mode is selected", async () => {
2796
+ const wd = await mkdtemp(join(tmpdir(), "omx-setup-install-mode-"));
2797
+ try {
2798
+ await withIsolatedUserHome(wd, async (codexHomeDir) => {
2799
+ await withTempCwd(wd, async () => {
2800
+ await setup({ scope: "user", installMode: "legacy" });
2801
+ const askSkillPath = join(codexHomeDir, "skills", "ask", "SKILL.md");
2802
+ const promptPath = join(codexHomeDir, "prompts", "executor.md");
2803
+ const agentPath = join(codexHomeDir, "agents", "planner.toml");
2804
+ const hooksPath = join(codexHomeDir, "hooks.json");
2805
+ const configPath = join(codexHomeDir, "config.toml");
2806
+ const agentsMdPath = join(codexHomeDir, "AGENTS.md");
2807
+ assert.equal(existsSync(askSkillPath), true);
2808
+ assert.equal(existsSync(promptPath), true);
2809
+ assert.equal(existsSync(agentPath), true);
2810
+ assert.equal(existsSync(hooksPath), true);
2811
+ assert.equal(existsSync(configPath), true);
2812
+ assert.equal(existsSync(agentsMdPath), true);
2813
+ await setup({ scope: "user", installMode: "plugin" });
2814
+ assert.equal(existsSync(askSkillPath), false);
2815
+ assert.equal(existsSync(promptPath), false);
2816
+ assert.equal(existsSync(agentPath), true);
2817
+ assert.equal(existsSync(hooksPath), false);
2818
+ assert.equal(existsSync(agentsMdPath), true);
2819
+ const config = await readFile(configPath, "utf-8");
2820
+ assert.match(config, /^plugin_hooks = true$/m);
2821
+ assert.doesNotMatch(config, /^\s*(?:notify)\s*=|^\s*\[mcp_servers[.\]]/m);
2822
+ assert.match(config, /^developer_instructions\s*=/m);
2823
+ assert.match(config, /You have oh-my-codex installed\. AGENTS\.md/);
2824
+ });
2825
+ });
2826
+ }
2827
+ finally {
2828
+ await rm(wd, { recursive: true, force: true });
2829
+ }
2830
+ });
2831
+ it("preserves existing AGENTS.md when plugin AGENTS defaults are declined", async () => {
2832
+ const wd = await mkdtemp(join(tmpdir(), "omx-setup-install-mode-"));
2833
+ try {
2834
+ await withIsolatedUserHome(wd, async (codexHomeDir) => {
2835
+ await withTempCwd(wd, async () => {
2836
+ await setup({ scope: "user", installMode: "legacy" });
2837
+ const agentsMdPath = join(codexHomeDir, "AGENTS.md");
2838
+ const before = await readFile(agentsMdPath, "utf-8");
2839
+ assert.match(before, /<!-- omx:generated:agents-md -->/);
2840
+ await setup({
2841
+ scope: "user",
2842
+ installMode: "plugin",
2843
+ pluginAgentsMdPrompt: async () => false,
2844
+ });
2845
+ assert.equal(await readFile(agentsMdPath, "utf-8"), before);
2846
+ });
2847
+ });
2848
+ }
2849
+ finally {
2850
+ await rm(wd, { recursive: true, force: true });
2851
+ }
2852
+ });
2853
+ it("repairs existing AGENTS.md during non-interactive plugin force setup", async () => {
2854
+ const wd = await mkdtemp(join(tmpdir(), "omx-setup-install-mode-"));
2855
+ try {
2856
+ await withIsolatedUserHome(wd, async (codexHomeDir) => {
2857
+ await withTempCwd(wd, async () => {
2858
+ await mkdir(codexHomeDir, { recursive: true });
2859
+ const agentsMdPath = join(codexHomeDir, "AGENTS.md");
2860
+ await writeFile(agentsMdPath, "# local instructions\n");
2861
+ await setup({ scope: "user", installMode: "plugin", force: true });
2862
+ const after = await readFile(agentsMdPath, "utf-8");
2863
+ assert.match(after, /<!-- omx:generated:agents-md -->/);
2864
+ assert.match(after, /oh-my-codex - Intelligent Multi-Agent Orchestration/);
2865
+ const backupRoot = join(wd, "home", ".omx", "backups", "setup");
2866
+ const backupRuns = await readdir(backupRoot);
2867
+ assert.equal(backupRuns.some((entry) => existsSync(join(backupRoot, entry, ".codex", "AGENTS.md"))), true);
2868
+ });
2869
+ });
2870
+ }
2871
+ finally {
2872
+ await rm(wd, { recursive: true, force: true });
2873
+ }
2874
+ });
2875
+ it("archives stale legacy prompts and preserves modified native agents when plugin mode refreshes", async () => {
2876
+ const wd = await mkdtemp(join(tmpdir(), "omx-setup-install-mode-"));
2877
+ try {
2878
+ await withIsolatedUserHome(wd, async (codexHomeDir) => {
2879
+ await withTempCwd(wd, async () => {
2880
+ await setup({ scope: "user", installMode: "legacy" });
2881
+ const promptPath = join(codexHomeDir, "prompts", "executor.md");
2882
+ const agentPath = join(codexHomeDir, "agents", "planner.toml");
2883
+ await writeFile(promptPath, "---\ndescription: stale legacy executor prompt\n---\n\nold executor body\n");
2884
+ const staleAgentToml = [
2885
+ "# oh-my-codex agent: planner",
2886
+ 'name = "planner"',
2887
+ 'description = "stale legacy generated planner"',
2888
+ 'developer_instructions = """old planner body"""',
2889
+ "",
2890
+ ].join("\n");
2891
+ await writeFile(agentPath, staleAgentToml);
2892
+ const output = await captureConsoleOutput(async () => {
2893
+ await setup({ scope: "user", installMode: "plugin" });
2894
+ });
2895
+ assert.equal(existsSync(promptPath), false);
2896
+ assert.equal(existsSync(agentPath), true);
2897
+ assert.equal(await readFile(agentPath, "utf-8"), staleAgentToml);
2898
+ assert.match(output, /Archived and removed .* legacy OMX-managed prompt file/);
2899
+ assert.match(output, /Native agent role refresh complete/);
2900
+ const backupRoot = join(wd, "home", ".omx", "backups", "setup");
2901
+ const backupRuns = await readdir(backupRoot);
2902
+ assert.ok(backupRuns.length > 0);
2903
+ assert.equal(backupRuns.some((entry) => existsSync(join(backupRoot, entry, ".codex", "prompts", "executor.md"))), true);
2904
+ assert.equal(backupRuns.some((entry) => existsSync(join(backupRoot, entry, ".codex", "agents", "planner.toml"))), false);
2905
+ });
2906
+ });
2907
+ }
2908
+ finally {
2909
+ await rm(wd, { recursive: true, force: true });
2910
+ }
2911
+ });
2912
+ it("preserves unmanaged native agent TOMLs with obsolete skill_ref during plugin refresh", async () => {
2913
+ const wd = await mkdtemp(join(tmpdir(), "omx-setup-install-mode-"));
2914
+ try {
2915
+ await withIsolatedUserHome(wd, async (codexHomeDir) => {
2916
+ await withTempCwd(wd, async () => {
2917
+ await setup({ scope: "user", installMode: "legacy" });
2918
+ const customAgentPath = join(codexHomeDir, "agents", "custom-reviewer.toml");
2919
+ const generatedAgentPath = join(codexHomeDir, "agents", "ghost.toml");
2920
+ const customAgentToml = [
2921
+ 'name = "custom-reviewer"',
2922
+ 'description = "user-managed reviewer"',
2923
+ 'skill_ref = "custom-reviewer"',
2924
+ "",
2925
+ ].join("\n");
2926
+ await writeFile(customAgentPath, customAgentToml);
2927
+ await writeFile(generatedAgentPath, [
2928
+ "# oh-my-codex agent: ghost",
2929
+ 'name = "ghost"',
2930
+ 'description = "obsolete generated reviewer"',
2931
+ 'skill_ref = "ghost"',
2932
+ "",
2933
+ ].join("\n"));
2934
+ await setup({ scope: "user", installMode: "plugin" });
2935
+ assert.equal(await readFile(customAgentPath, "utf-8"), customAgentToml);
2936
+ assert.equal(existsSync(generatedAgentPath), false);
2937
+ });
2938
+ });
2939
+ }
2940
+ finally {
2941
+ await rm(wd, { recursive: true, force: true });
2942
+ }
2943
+ });
2944
+ it("counts plugin cleanup skill directory backups in the setup summary", async () => {
2945
+ const wd = await mkdtemp(join(tmpdir(), "omx-setup-install-mode-"));
2946
+ try {
2947
+ await withIsolatedUserHome(wd, async (codexHomeDir) => {
2948
+ await withTempCwd(wd, async () => {
2949
+ await setup({ scope: "user", installMode: "legacy" });
2950
+ await seedPluginCacheFromInstalledSkills(codexHomeDir);
2951
+ const output = await captureConsoleOutput(async () => {
2952
+ await setup({ scope: "user", installMode: "plugin" });
2953
+ });
2954
+ const skillsSummary = output.match(/skills: updated=0, unchanged=0, backed_up=(\d+), skipped=0, removed=(\d+)/);
2955
+ assert.notEqual(skillsSummary, null);
2956
+ const backedUp = Number(skillsSummary?.[1]);
2957
+ const removed = Number(skillsSummary?.[2]);
2958
+ assert.ok(backedUp > 0);
2959
+ assert.equal(backedUp, removed);
2960
+ });
2961
+ });
2962
+ }
2963
+ finally {
2964
+ await rm(wd, { recursive: true, force: true });
2965
+ }
2966
+ });
2967
+ it("removes matching legacy user skills even when plugin readiness is proven", async () => {
2968
+ const wd = await mkdtemp(join(tmpdir(), "omx-setup-install-mode-"));
2969
+ try {
2970
+ await withIsolatedUserHome(wd, async (codexHomeDir) => {
2971
+ await withTempCwd(wd, async () => {
2972
+ await setup({ scope: "user", installMode: "legacy" });
2973
+ await seedPluginCacheFromInstalledSkills(codexHomeDir);
2974
+ const askSkillDir = join(codexHomeDir, "skills", "ask");
2975
+ const wikiSkillDir = join(codexHomeDir, "skills", "wiki");
2976
+ assert.equal(existsSync(askSkillDir), true);
2977
+ assert.equal(existsSync(wikiSkillDir), true);
2978
+ const outputLines = [];
2979
+ const previousLog = console.log;
2980
+ console.log = (...args) => {
2981
+ outputLines.push(args.join(" "));
2982
+ };
2983
+ try {
2984
+ await setup({ scope: "user", installMode: "plugin" });
2985
+ }
2986
+ finally {
2987
+ console.log = previousLog;
2988
+ }
2989
+ const setupOutput = outputLines.join("\n");
2990
+ assert.equal(existsSync(askSkillDir), false);
2991
+ assert.equal(existsSync(wikiSkillDir), false);
2992
+ assert.match(setupOutput, /skills: updated=0, unchanged=0, backed_up=\d+, skipped=0, removed=\d+/);
2993
+ const backupSetupRoot = join(wd, "home", ".omx", "backups", "setup");
2994
+ const backupTimestamps = await readdir(backupSetupRoot);
2995
+ assert.equal(backupTimestamps.length, 1);
2996
+ const backupSkillsDir = join(backupSetupRoot, backupTimestamps[0], ".codex", "skills");
2997
+ const backedUpSkillNames = await readdir(backupSkillsDir);
2998
+ assert.ok(backedUpSkillNames.includes("ask"));
2999
+ assert.ok(backedUpSkillNames.includes("wiki"));
3000
+ assert.match(setupOutput, new RegExp(`skills: updated=0, unchanged=0, backed_up=${backedUpSkillNames.length}, skipped=0, removed=${backedUpSkillNames.length}`));
3001
+ });
3002
+ });
3003
+ }
3004
+ finally {
3005
+ await rm(wd, { recursive: true, force: true });
3006
+ }
3007
+ });
3008
+ it("preserves customized legacy user skills during plugin cleanup", async () => {
3009
+ const wd = await mkdtemp(join(tmpdir(), "omx-setup-install-mode-"));
3010
+ try {
3011
+ await withIsolatedUserHome(wd, async (codexHomeDir) => {
3012
+ await withTempCwd(wd, async () => {
3013
+ await setup({ scope: "user", installMode: "legacy" });
3014
+ await seedPluginCacheFromInstalledSkills(codexHomeDir);
3015
+ const askSkillPath = join(codexHomeDir, "skills", "ask", "SKILL.md");
3016
+ const wikiSkillDir = join(codexHomeDir, "skills", "wiki");
3017
+ await writeFile(askSkillPath, "# customized ask\n");
3018
+ await setup({ scope: "user", installMode: "plugin" });
3019
+ assert.equal(await readFile(askSkillPath, "utf-8"), "# customized ask\n");
3020
+ assert.equal(existsSync(wikiSkillDir), false);
3021
+ });
3022
+ });
3023
+ }
3024
+ finally {
3025
+ await rm(wd, { recursive: true, force: true });
3026
+ }
3027
+ });
3028
+ it("fails plugin hook-removal preflight before creating unrelated setup artifacts", async () => {
3029
+ const wd = await mkdtemp(join(tmpdir(), "omx-setup-preflight-"));
3030
+ try {
3031
+ await withIsolatedUserHome(wd, async (codexHomeDir) => {
3032
+ await withTempCwd(wd, async () => {
3033
+ const hooksPath = join(codexHomeDir, "hooks.json");
3034
+ const configPath = join(codexHomeDir, "config.toml");
3035
+ const hooksContent = JSON.stringify({
3036
+ hooks: {
3037
+ SessionStart: [
3038
+ {
3039
+ matcher: "startup|resume|clear",
3040
+ hooks: [
3041
+ {
3042
+ type: "command",
3043
+ command: 'node "/repo/dist/scripts/codex-native-hook.js"',
3044
+ },
3045
+ ],
3046
+ },
3047
+ { hooks: [{ type: "command", command: "echo foreign" }] },
3048
+ ],
3049
+ },
3050
+ }, null, 2) + "\n";
3051
+ const configContent = 'model = "foreign-config"\n';
3052
+ await writeFile(hooksPath, hooksContent);
3053
+ await writeFile(configPath, configContent);
3054
+ await assert.rejects(() => setup({
3055
+ scope: "user",
3056
+ installMode: "plugin",
3057
+ mergeAgents: true,
3058
+ codexFeaturesProbe: () => "hooks stable true\nplugin_hooks experimental true\n",
3059
+ }), (error) => {
3060
+ assert.equal(error.code, "unsafe_managed_removal");
3061
+ return true;
3062
+ });
3063
+ assert.equal(await readFile(hooksPath, "utf-8"), hooksContent);
3064
+ assert.equal(await readFile(configPath, "utf-8"), configContent);
3065
+ assert.equal(existsSync(join(wd, ".omx")), false);
3066
+ assert.equal(existsSync(join(codexHomeDir, "prompts")), false);
3067
+ assert.equal(existsSync(join(codexHomeDir, "agents")), false);
3068
+ });
3069
+ });
3070
+ }
3071
+ finally {
3072
+ await rm(wd, { recursive: true, force: true });
3073
+ }
3074
+ });
3075
+ it("rejects symlinked Codex transaction ancestors without writing foreign storage", async () => {
3076
+ const wd = await mkdtemp(join(tmpdir(), "omx-setup-symlinked-codex-home-"));
3077
+ try {
3078
+ await withIsolatedUserHome(wd, async (codexHomeDir) => {
3079
+ await withTempCwd(wd, async () => {
3080
+ const foreignCodexHome = join(wd, "foreign-codex-home");
3081
+ await mkdir(foreignCodexHome);
3082
+ await writeFile(join(foreignCodexHome, "sentinel.txt"), "foreign\n");
3083
+ await rm(codexHomeDir, { recursive: true, force: true });
3084
+ await symlink(foreignCodexHome, codexHomeDir);
3085
+ await assert.rejects(setup({
3086
+ scope: "user",
3087
+ installMode: "legacy",
3088
+ skipNativeAgentRefresh: true,
3089
+ }), /ancestor .*symbolic link/);
3090
+ assert.deepEqual(await readdir(foreignCodexHome), ["sentinel.txt"]);
3091
+ assert.equal(existsSync(join(wd, ".omx")), false);
3092
+ });
3093
+ });
3094
+ }
3095
+ finally {
3096
+ await rm(wd, { recursive: true, force: true });
3097
+ }
3098
+ });
3099
+ it("rejects a symlinked native artifact parent before touching its target", async () => {
3100
+ const wd = await mkdtemp(join(tmpdir(), "omx-setup-symlinked-native-parent-"));
3101
+ try {
3102
+ await withIsolatedUserHome(wd, async (codexHomeDir) => {
3103
+ await withTempCwd(wd, async () => {
3104
+ const foreignMetadataDir = join(wd, "foreign-native-metadata");
3105
+ await mkdir(foreignMetadataDir);
3106
+ await writeFile(join(foreignMetadataDir, "sentinel.txt"), "foreign\n");
3107
+ await symlink(foreignMetadataDir, join(codexHomeDir, ".omx"));
3108
+ await assert.rejects(setup({
3109
+ scope: "user",
3110
+ installMode: "legacy",
3111
+ skipNativeAgentRefresh: true,
3112
+ }), /ancestor .*symbolic link/);
3113
+ assert.deepEqual(await readdir(foreignMetadataDir), ["sentinel.txt"]);
3114
+ assert.equal(existsSync(join(codexHomeDir, "config.toml")), false);
3115
+ assert.equal(existsSync(join(codexHomeDir, "hooks.json")), false);
3116
+ assert.equal(existsSync(join(wd, ".omx")), false);
3117
+ });
3118
+ });
3119
+ }
3120
+ finally {
3121
+ await rm(wd, { recursive: true, force: true });
3122
+ }
3123
+ });
3124
+ it("revalidates native artifact parent topology immediately before mutation", async () => {
3125
+ const wd = await mkdtemp(join(tmpdir(), "omx-setup-stale-native-parent-"));
3126
+ let resetFailureInjector;
3127
+ try {
3128
+ await withIsolatedUserHome(wd, async (codexHomeDir) => {
3129
+ await withTempCwd(wd, async () => {
3130
+ const metadataParent = join(codexHomeDir, ".omx");
3131
+ let injected = false;
3132
+ resetFailureInjector = setNativeHookTransactionFailureInjectorForTest((stage) => {
3133
+ if (stage !== "before_precondition" || injected)
3134
+ return;
3135
+ injected = true;
3136
+ writeFileSync(metadataParent, "foreign\n");
3137
+ });
3138
+ await assert.rejects(setup({
3139
+ scope: "user",
3140
+ installMode: "legacy",
3141
+ skipNativeAgentRefresh: true,
3142
+ }), /ancestor .*not a directory/);
3143
+ assert.equal(injected, true);
3144
+ assert.equal(await readFile(metadataParent, "utf-8"), "foreign\n");
3145
+ assert.equal(existsSync(join(codexHomeDir, "config.toml")), false);
3146
+ assert.equal(existsSync(join(codexHomeDir, "hooks.json")), false);
3147
+ });
3148
+ });
3149
+ }
3150
+ finally {
3151
+ resetFailureInjector?.();
3152
+ await rm(wd, { recursive: true, force: true });
3153
+ }
3154
+ });
3155
+ it("rejects a modified Windows hook shim during setup preflight", async () => {
3156
+ const wd = await mkdtemp(join(tmpdir(), "omx-setup-windows-shim-"));
3157
+ const resetPlatform = setNativeHookTransactionPlatformForTest("win32");
3158
+ try {
3159
+ await withIsolatedUserHome(wd, async (codexHomeDir) => {
3160
+ await withTempCwd(wd, async () => {
3161
+ const shimPath = buildManagedCodexNativeHookWindowsShimPath(codexHomeDir);
3162
+ const modifiedShim = "# modified by user\n";
3163
+ await writeFile(shimPath, modifiedShim);
3164
+ await assert.rejects(() => setup({
3165
+ scope: "user",
3166
+ installMode: "legacy",
3167
+ mergeAgents: true,
3168
+ }), /modified Windows native hook shim/);
3169
+ assert.equal(await readFile(shimPath, "utf-8"), modifiedShim);
3170
+ assert.equal(existsSync(join(codexHomeDir, "hooks.json")), false);
3171
+ assert.equal(existsSync(join(codexHomeDir, "config.toml")), false);
3172
+ assert.equal(existsSync(join(wd, ".omx")), false);
3173
+ });
3174
+ });
3175
+ }
3176
+ finally {
3177
+ resetPlatform();
3178
+ await rm(wd, { recursive: true, force: true });
3179
+ }
3180
+ });
3181
+ it("writes hooks, notification metadata, then config/trust and rolls all writes back when config replacement fails", async () => {
3182
+ const wd = await mkdtemp(join(tmpdir(), "omx-setup-hook-transaction-"));
3183
+ const writeOrder = [];
3184
+ const resetFailureInjector = setNativeHookTransactionFailureInjectorForTest((stage, artifact) => {
3185
+ if (stage !== "before_temp_write")
3186
+ return;
3187
+ writeOrder.push(artifact.kind);
3188
+ if (artifact.kind === "config") {
3189
+ throw new Error("injected config replacement failure");
3190
+ }
3191
+ });
3192
+ try {
3193
+ await withIsolatedUserHome(wd, async (codexHomeDir) => {
3194
+ await withTempCwd(wd, async () => {
3195
+ await writeFile(join(codexHomeDir, "config.toml"), 'notify = ["node", "/tmp/user-notify.js"]\n');
3196
+ await assert.rejects(() => setup({
3197
+ scope: "user",
3198
+ installMode: "legacy",
3199
+ mergeAgents: true,
3200
+ skipNativeAgentRefresh: true,
3201
+ }), /rolled back: injected config replacement failure/);
3202
+ assert.deepEqual(writeOrder, ["hooks", "metadata", "config"]);
3203
+ assert.equal(existsSync(join(codexHomeDir, "hooks.json")), false);
3204
+ assert.equal(await readFile(join(codexHomeDir, "config.toml"), "utf-8"), 'notify = ["node", "/tmp/user-notify.js"]\n');
3205
+ assert.equal(existsSync(join(codexHomeDir, ".omx", "notify-dispatch.json")), false);
3206
+ });
3207
+ });
3208
+ }
3209
+ finally {
3210
+ resetFailureInjector();
3211
+ await rm(wd, { recursive: true, force: true });
3212
+ }
3213
+ });
3214
+ it("writes the Windows shim, hooks, notification metadata, then config/trust and rolls all writes back", async () => {
3215
+ const wd = await mkdtemp(join(tmpdir(), "omx-setup-windows-hook-transaction-"));
3216
+ const writeOrder = [];
3217
+ const resetFailureInjector = setNativeHookTransactionFailureInjectorForTest((stage, artifact) => {
3218
+ if (stage !== "before_temp_write")
3219
+ return;
3220
+ writeOrder.push(artifact.kind);
3221
+ if (artifact.kind === "config") {
3222
+ throw new Error("injected Windows config replacement failure");
3223
+ }
3224
+ });
3225
+ const resetPlatform = setNativeHookTransactionPlatformForTest("win32");
3226
+ try {
3227
+ await withIsolatedUserHome(wd, async (codexHomeDir) => {
3228
+ await withTempCwd(wd, async () => {
3229
+ await writeFile(join(codexHomeDir, "config.toml"), 'notify = ["node", "/tmp/user-notify.js"]\n');
3230
+ const shimPath = buildManagedCodexNativeHookWindowsShimPath(codexHomeDir);
3231
+ await assert.rejects(() => setup({
3232
+ scope: "user",
3233
+ installMode: "legacy",
3234
+ mergeAgents: true,
3235
+ skipNativeAgentRefresh: true,
3236
+ }), /rolled back: injected Windows config replacement failure/);
3237
+ assert.deepEqual(writeOrder, ["shim", "hooks", "metadata", "config"]);
3238
+ assert.equal(existsSync(shimPath), false);
3239
+ assert.equal(existsSync(join(codexHomeDir, "hooks.json")), false);
3240
+ assert.equal(await readFile(join(codexHomeDir, "config.toml"), "utf-8"), 'notify = ["node", "/tmp/user-notify.js"]\n');
3241
+ assert.equal(existsSync(join(codexHomeDir, ".omx", "notify-dispatch.json")), false);
3242
+ });
3243
+ });
3244
+ }
3245
+ finally {
3246
+ resetFailureInjector();
3247
+ resetPlatform();
3248
+ await rm(wd, { recursive: true, force: true });
3249
+ }
3250
+ });
3251
+ it("aborts stale native snapshots before setup can write unrelated artifacts", async () => {
3252
+ const originalConfig = Buffer.from('model = "before"\n', "utf-8");
3253
+ const originalHooks = Buffer.from('{"hooks": {}}\n', "utf-8");
3254
+ const foreignHooks = Buffer.from('{"hooks": {"Stop": []}}\n', "utf-8");
3255
+ const foreignConfig = Buffer.from('model = "foreign"\n', "utf-8");
3256
+ const fixtures = [
3257
+ {
3258
+ name: "hooks creation",
3259
+ seedHooks: null,
3260
+ mutate: (hooksPath, _configPath) => writeFileSync(hooksPath, foreignHooks),
3261
+ expectedHooks: foreignHooks,
3262
+ expectedConfig: originalConfig,
3263
+ },
3264
+ {
3265
+ name: "config modification",
3266
+ seedHooks: originalHooks,
3267
+ mutate: (_hooksPath, configPath) => writeFileSync(configPath, foreignConfig),
3268
+ expectedHooks: originalHooks,
3269
+ expectedConfig: foreignConfig,
3270
+ },
3271
+ {
3272
+ name: "hooks deletion",
3273
+ seedHooks: originalHooks,
3274
+ mutate: (hooksPath, _configPath) => rmSync(hooksPath),
3275
+ expectedHooks: null,
3276
+ expectedConfig: originalConfig,
3277
+ },
3278
+ {
3279
+ name: "config deletion",
3280
+ seedHooks: originalHooks,
3281
+ mutate: (_hooksPath, configPath) => rmSync(configPath),
3282
+ expectedHooks: originalHooks,
3283
+ expectedConfig: null,
3284
+ },
3285
+ ];
3286
+ for (const fixture of fixtures) {
3287
+ const wd = await mkdtemp(join(tmpdir(), "omx-setup-stale-native-"));
3288
+ let resetFailureInjector;
3289
+ try {
3290
+ await withIsolatedUserHome(wd, async (codexHomeDir) => {
3291
+ await withTempCwd(wd, async () => {
3292
+ const hooksPath = join(codexHomeDir, "hooks.json");
3293
+ const configPath = join(codexHomeDir, "config.toml");
3294
+ await writeFile(configPath, originalConfig);
3295
+ if (fixture.seedHooks)
3296
+ await writeFile(hooksPath, fixture.seedHooks);
3297
+ let injected = false;
3298
+ resetFailureInjector = setNativeHookTransactionFailureInjectorForTest((stage) => {
3299
+ if (stage !== "before_precondition" || injected)
3300
+ return;
3301
+ injected = true;
3302
+ fixture.mutate(hooksPath, configPath);
3303
+ });
3304
+ await assert.rejects(setup({
3305
+ scope: "user",
3306
+ installMode: "legacy",
3307
+ skipNativeAgentRefresh: true,
3308
+ codexFeaturesProbe: () => null,
3309
+ codexVersionProbe: () => null,
3310
+ }), /precondition changed/);
3311
+ assert.equal(injected, true, fixture.name);
3312
+ if (fixture.expectedConfig === null) {
3313
+ assert.equal(existsSync(configPath), false);
3314
+ }
3315
+ else {
3316
+ assert.deepEqual(await readFile(configPath), fixture.expectedConfig);
3317
+ }
3318
+ if (fixture.expectedHooks === null) {
3319
+ assert.equal(existsSync(hooksPath), false);
3320
+ }
3321
+ else {
3322
+ assert.deepEqual(await readFile(hooksPath), fixture.expectedHooks);
3323
+ }
3324
+ assert.equal(existsSync(join(wd, ".omx")), false);
3325
+ assert.equal(existsSync(join(codexHomeDir, "agents")), false);
3326
+ });
3327
+ });
3328
+ }
3329
+ finally {
3330
+ resetFailureInjector?.();
3331
+ await rm(wd, { recursive: true, force: true });
3332
+ }
3333
+ }
3334
+ });
3335
+ it("aborts when notification metadata changes after planning", async () => {
3336
+ const wd = await mkdtemp(join(tmpdir(), "omx-setup-stale-notify-metadata-"));
3337
+ let resetFailureInjector;
3338
+ try {
3339
+ await withIsolatedUserHome(wd, async (codexHomeDir) => {
3340
+ await withTempCwd(wd, async () => {
3341
+ const configPath = join(codexHomeDir, "config.toml");
3342
+ const hooksPath = join(codexHomeDir, "hooks.json");
3343
+ const metadataPath = join(codexHomeDir, ".omx", "notify-dispatch.json");
3344
+ const configBefore = 'notify = ["node", "/tmp/user-notify.js"]\n';
3345
+ const foreignMetadata = Buffer.from('{"managedBy":"foreign"}\n', "utf-8");
3346
+ await writeFile(configPath, configBefore);
3347
+ await mkdir(dirname(metadataPath), { recursive: true });
3348
+ let injected = false;
3349
+ resetFailureInjector = setNativeHookTransactionFailureInjectorForTest((stage, target) => {
3350
+ if (stage !== "before_precondition" ||
3351
+ target.kind !== "metadata" ||
3352
+ injected) {
3353
+ return;
3354
+ }
3355
+ injected = true;
3356
+ writeFileSync(metadataPath, foreignMetadata);
3357
+ });
3358
+ await assert.rejects(setup({
3359
+ scope: "user",
3360
+ installMode: "legacy",
3361
+ skipNativeAgentRefresh: true,
3362
+ codexFeaturesProbe: () => null,
3363
+ codexVersionProbe: () => null,
3364
+ }), /precondition changed/);
3365
+ assert.equal(injected, true);
3366
+ assert.deepEqual(await readFile(metadataPath), foreignMetadata);
3367
+ assert.equal(await readFile(configPath, "utf-8"), configBefore);
3368
+ assert.equal(existsSync(hooksPath), false);
3369
+ });
3370
+ });
3371
+ }
3372
+ finally {
3373
+ resetFailureInjector?.();
3374
+ await rm(wd, { recursive: true, force: true });
3375
+ }
3376
+ });
3377
+ it("aborts when the Windows shim changes after planning", async () => {
3378
+ const wd = await mkdtemp(join(tmpdir(), "omx-setup-stale-windows-shim-"));
3379
+ const resetPlatform = setNativeHookTransactionPlatformForTest("win32");
3380
+ let resetFailureInjector;
3381
+ try {
3382
+ await withIsolatedUserHome(wd, async (codexHomeDir) => {
3383
+ await withTempCwd(wd, async () => {
3384
+ await setup({
3385
+ scope: "user",
3386
+ installMode: "legacy",
3387
+ skipNativeAgentRefresh: true,
3388
+ codexFeaturesProbe: () => null,
3389
+ codexVersionProbe: () => null,
3390
+ });
3391
+ const shimPath = buildManagedCodexNativeHookWindowsShimPath(codexHomeDir);
3392
+ const hooksPath = join(codexHomeDir, "hooks.json");
3393
+ const configPath = join(codexHomeDir, "config.toml");
3394
+ const configBefore = await readFile(configPath);
3395
+ await writeFile(hooksPath, '{"hooks": {}}\n');
3396
+ const hooksBefore = await readFile(hooksPath);
3397
+ const foreignShim = Buffer.from("# foreign shim\n", "utf-8");
3398
+ let injected = false;
3399
+ resetFailureInjector = setNativeHookTransactionFailureInjectorForTest((stage, target) => {
3400
+ if (stage === "before_precondition" &&
3401
+ target.kind === "shim" &&
3402
+ !injected) {
3403
+ injected = true;
3404
+ writeFileSync(shimPath, foreignShim);
3405
+ }
3406
+ });
3407
+ await assert.rejects(setup({
3408
+ scope: "user",
3409
+ installMode: "legacy",
3410
+ skipNativeAgentRefresh: true,
3411
+ codexFeaturesProbe: () => null,
3412
+ codexVersionProbe: () => null,
3413
+ }), /precondition changed/);
3414
+ assert.equal(injected, true);
3415
+ assert.deepEqual(await readFile(shimPath), foreignShim);
3416
+ assert.deepEqual(await readFile(hooksPath), hooksBefore);
3417
+ assert.deepEqual(await readFile(configPath), configBefore);
3418
+ });
3419
+ });
3420
+ }
3421
+ finally {
3422
+ resetFailureInjector?.();
3423
+ resetPlatform();
3424
+ await rm(wd, { recursive: true, force: true });
3425
+ }
3426
+ });
3427
+ it("never deletes plugin hooks from a stale snapshot", async () => {
3428
+ const wd = await mkdtemp(join(tmpdir(), "omx-setup-plugin-stale-hooks-"));
3429
+ let resetFailureInjector;
3430
+ try {
3431
+ await withIsolatedUserHome(wd, async (codexHomeDir) => {
3432
+ await withTempCwd(wd, async () => {
3433
+ await setup({ scope: "user", installMode: "legacy", skipNativeAgentRefresh: true });
3434
+ const hooksPath = join(codexHomeDir, "hooks.json");
3435
+ const configPath = join(codexHomeDir, "config.toml");
3436
+ const configBefore = await readFile(configPath);
3437
+ const foreignHooks = Buffer.from('{"hooks": {"Stop": []}}\n', "utf-8");
3438
+ let injected = false;
3439
+ resetFailureInjector = setNativeHookTransactionFailureInjectorForTest((stage) => {
3440
+ if (stage !== "before_precondition" || injected)
3441
+ return;
3442
+ injected = true;
3443
+ writeFileSync(hooksPath, foreignHooks);
3444
+ });
3445
+ await assert.rejects(setup({
3446
+ scope: "user",
3447
+ installMode: "plugin",
3448
+ pluginAgentsMdPrompt: async () => false,
3449
+ skipNativeAgentRefresh: true,
3450
+ }), /precondition changed/);
3451
+ assert.equal(injected, true);
3452
+ assert.deepEqual(await readFile(hooksPath), foreignHooks);
3453
+ assert.deepEqual(await readFile(configPath), configBefore);
3454
+ });
3455
+ });
3456
+ }
3457
+ finally {
3458
+ resetFailureInjector?.();
3459
+ await rm(wd, { recursive: true, force: true });
3460
+ }
3461
+ });
3462
+ it("never rolls back a concurrent foreign hook replacement", async () => {
3463
+ const wd = await mkdtemp(join(tmpdir(), "omx-setup-stale-rollback-"));
3464
+ let resetFailureInjector;
3465
+ try {
3466
+ await withIsolatedUserHome(wd, async (codexHomeDir) => {
3467
+ await withTempCwd(wd, async () => {
3468
+ const hooksPath = join(codexHomeDir, "hooks.json");
3469
+ const configPath = join(codexHomeDir, "config.toml");
3470
+ const hooksBefore = Buffer.from('{"hooks": {}}\n', "utf-8");
3471
+ const configBefore = Buffer.from('model = "before"\n', "utf-8");
3472
+ const foreignHooks = Buffer.from('{"hooks": {"Stop": []}}\n', "utf-8");
3473
+ await writeFile(hooksPath, hooksBefore);
3474
+ await writeFile(configPath, configBefore);
3475
+ resetFailureInjector = setNativeHookTransactionFailureInjectorForTest((stage, target) => {
3476
+ if (stage !== "before_temp_write" || target.kind !== "config")
3477
+ return;
3478
+ writeFileSync(hooksPath, foreignHooks);
3479
+ throw new Error("injected config replacement failure after foreign hook write");
3480
+ });
3481
+ await assert.rejects(setup({
3482
+ scope: "user",
3483
+ installMode: "legacy",
3484
+ skipNativeAgentRefresh: true,
3485
+ codexFeaturesProbe: () => null,
3486
+ codexVersionProbe: () => null,
3487
+ }), /rollback failed/);
3488
+ assert.deepEqual(await readFile(hooksPath), foreignHooks);
3489
+ assert.deepEqual(await readFile(configPath), configBefore);
3490
+ });
3491
+ });
3492
+ }
3493
+ finally {
3494
+ resetFailureInjector?.();
3495
+ await rm(wd, { recursive: true, force: true });
3496
+ }
3497
+ });
3498
+ it("preserves pre-existing native bytes and modes across injected transaction failures", async () => {
3499
+ const failures = [
3500
+ { stage: "before_backup", kind: "shim" },
3501
+ { stage: "before_temp_write", kind: "shim" },
3502
+ { stage: "before_rename", kind: "hooks" },
3503
+ { stage: "before_temp_write", kind: "metadata" },
3504
+ { stage: "before_readback", kind: "config" },
3505
+ ];
3506
+ for (const failure of failures) {
3507
+ const wd = await mkdtemp(join(tmpdir(), "omx-setup-transaction-rollback-"));
3508
+ const resetPlatform = setNativeHookTransactionPlatformForTest("win32");
3509
+ let resetFailureInjector;
3510
+ try {
3511
+ await withIsolatedUserHome(wd, async (codexHomeDir) => {
3512
+ await withTempCwd(wd, async () => {
3513
+ const shimPath = buildManagedCodexNativeHookWindowsShimPath(codexHomeDir);
3514
+ const hooksPath = join(codexHomeDir, "hooks.json");
3515
+ const configPath = join(codexHomeDir, "config.toml");
3516
+ const shimBefore = Buffer.from(buildManagedCodexNativeHookWindowsShimContent("", {
3517
+ nodePath: "C:\\Historical Node\\node.exe",
3518
+ hookScriptPath: "C:\\Historical Install\\oh-my-codex\\dist\\scripts\\codex-native-hook.js",
3519
+ }), "utf-8");
3520
+ const hooksBefore = Buffer.from('{"hooks": {}}\n', "utf-8");
3521
+ const configBefore = Buffer.from('model = "before"\nnotify = ["node", "/tmp/user-notify.js"]\n', "utf-8");
3522
+ await mkdir(dirname(shimPath), { recursive: true });
3523
+ await writeFile(shimPath, shimBefore);
3524
+ await writeFile(hooksPath, hooksBefore);
3525
+ await writeFile(configPath, configBefore);
3526
+ await Promise.all([
3527
+ chmod(shimPath, 0o640),
3528
+ chmod(hooksPath, 0o640),
3529
+ chmod(configPath, 0o640),
3530
+ ]);
3531
+ resetFailureInjector = setNativeHookTransactionFailureInjectorForTest((stage, target) => {
3532
+ if (stage === failure.stage && target.kind === failure.kind) {
3533
+ throw new Error(`injected ${failure.stage} ${failure.kind} failure`);
3534
+ }
3535
+ });
3536
+ await assert.rejects(setup({
3537
+ scope: "user",
3538
+ installMode: "legacy",
3539
+ skipNativeAgentRefresh: true,
3540
+ codexFeaturesProbe: () => null,
3541
+ codexVersionProbe: () => null,
3542
+ }), /injected .* failure/);
3543
+ assert.deepEqual(await readFile(shimPath), shimBefore, failure.stage);
3544
+ assert.deepEqual(await readFile(hooksPath), hooksBefore, failure.stage);
3545
+ assert.deepEqual(await readFile(configPath), configBefore, failure.stage);
3546
+ assert.equal(existsSync(join(codexHomeDir, ".omx", "notify-dispatch.json")), false, failure.stage);
3547
+ for (const path of [shimPath, hooksPath, configPath]) {
3548
+ assert.equal((await stat(path)).mode & 0o777, 0o640, path);
3549
+ }
3550
+ });
3551
+ });
3552
+ }
3553
+ finally {
3554
+ resetFailureInjector?.();
3555
+ resetPlatform();
3556
+ await rm(wd, { recursive: true, force: true });
3557
+ }
3558
+ }
3559
+ });
3560
+ it("fails closed when a managed dispatcher metadata snapshot is not valid UTF-8", async () => {
3561
+ const wd = await mkdtemp(join(tmpdir(), "omx-setup-invalid-notify-metadata-"));
3562
+ try {
3563
+ await withIsolatedUserHome(wd, async (codexHomeDir) => {
3564
+ await withTempCwd(wd, async () => {
3565
+ const configPath = join(codexHomeDir, "config.toml");
3566
+ const metadataPath = join(codexHomeDir, ".omx", "notify-dispatch.json");
3567
+ const dispatcherPath = join(packageRoot, "dist", "scripts", "notify-dispatcher.js");
3568
+ const configBefore = `notify = ${JSON.stringify([
3569
+ "node",
3570
+ dispatcherPath,
3571
+ "--metadata",
3572
+ metadataPath,
3573
+ ])}\n`;
3574
+ await mkdir(dirname(metadataPath), { recursive: true });
3575
+ await writeFile(configPath, configBefore);
3576
+ await writeFile(metadataPath, Buffer.from([0xff]));
3577
+ await assert.rejects(setup({ scope: "user", installMode: "legacy", skipNativeAgentRefresh: true }), /notification metadata .*invalid UTF-8/);
3578
+ assert.equal(await readFile(configPath, "utf-8"), configBefore);
3579
+ assert.deepEqual(await readFile(metadataPath), Buffer.from([0xff]));
3580
+ assert.equal(existsSync(join(codexHomeDir, "hooks.json")), false);
3581
+ });
3582
+ });
3583
+ }
3584
+ finally {
3585
+ await rm(wd, { recursive: true, force: true });
3586
+ }
3587
+ });
3588
+ it("keeps consulted unchanged notification metadata in the native transaction preconditions", async () => {
3589
+ const wd = await mkdtemp(join(tmpdir(), "omx-setup-notify-precondition-"));
3590
+ let resetFailureInjector;
3591
+ try {
3592
+ await withIsolatedUserHome(wd, async (codexHomeDir) => {
3593
+ await withTempCwd(wd, async () => {
3594
+ const configPath = join(codexHomeDir, "config.toml");
3595
+ const hooksPath = join(codexHomeDir, "hooks.json");
3596
+ const metadataPath = join(codexHomeDir, ".omx", "notify-dispatch.json");
3597
+ await writeFile(configPath, 'notify = ["node", "/tmp/user-notify.js"]\n');
3598
+ await setup({
3599
+ scope: "user",
3600
+ installMode: "legacy",
3601
+ skipNativeAgentRefresh: true,
3602
+ });
3603
+ const configBefore = await readFile(configPath);
3604
+ const hooksBefore = await readFile(hooksPath);
3605
+ const foreignMetadata = Buffer.from('{"managedBy":"foreign"}\n', "utf-8");
3606
+ let injected = false;
3607
+ resetFailureInjector = setNativeHookTransactionFailureInjectorForTest((stage, target) => {
3608
+ if (stage === "before_precondition" &&
3609
+ target.kind === "metadata" &&
3610
+ !injected) {
3611
+ injected = true;
3612
+ writeFileSync(metadataPath, foreignMetadata);
3613
+ }
3614
+ });
3615
+ await assert.rejects(setup({
3616
+ scope: "user",
3617
+ installMode: "legacy",
3618
+ skipNativeAgentRefresh: true,
3619
+ }), /precondition changed/);
3620
+ assert.equal(injected, true);
3621
+ assert.deepEqual(await readFile(metadataPath), foreignMetadata);
3622
+ assert.deepEqual(await readFile(configPath), configBefore);
3623
+ assert.deepEqual(await readFile(hooksPath), hooksBefore);
3624
+ });
3625
+ });
3626
+ }
3627
+ finally {
3628
+ resetFailureInjector?.();
3629
+ await rm(wd, { recursive: true, force: true });
3630
+ }
3631
+ });
3632
+ it("rejects config decisions whose MCP-removal callback changed the planned config snapshot", async () => {
3633
+ const wd = await mkdtemp(join(tmpdir(), "omx-setup-stale-config-decision-"));
3634
+ try {
3635
+ await withIsolatedUserHome(wd, async (codexHomeDir) => {
3636
+ await withTempCwd(wd, async () => {
3637
+ const configPath = join(codexHomeDir, "config.toml");
3638
+ const configBefore = '[mcp_servers.omx_state]\ncommand = "node"\n';
3639
+ const foreignConfig = 'model = "foreign"\n';
3640
+ await writeFile(configPath, configBefore);
3641
+ await assert.rejects(setup({
3642
+ scope: "user",
3643
+ installMode: "legacy",
3644
+ skipNativeAgentRefresh: true,
3645
+ firstPartyMcpRemovalPrompt: async () => {
3646
+ await writeFile(configPath, foreignConfig);
3647
+ return false;
3648
+ },
3649
+ }), /precondition changed/);
3650
+ assert.equal(await readFile(configPath, "utf-8"), foreignConfig);
3651
+ assert.equal(existsSync(join(codexHomeDir, "hooks.json")), false);
3652
+ assert.equal(existsSync(join(codexHomeDir, ".omx")), false);
3653
+ });
3654
+ });
3655
+ }
3656
+ finally {
3657
+ await rm(wd, { recursive: true, force: true });
3658
+ }
3659
+ });
3660
+ it("fails closed for pre-existing native transaction write temporaries", async () => {
3661
+ const fixtures = ["regular file", "symlink"];
3662
+ for (const fixture of fixtures) {
3663
+ const wd = await mkdtemp(join(tmpdir(), "omx-setup-temp-collision-"));
3664
+ let resetTemporaryPath;
3665
+ try {
3666
+ await withIsolatedUserHome(wd, async (codexHomeDir) => {
3667
+ await withTempCwd(wd, async () => {
3668
+ const configPath = join(codexHomeDir, "config.toml");
3669
+ const hooksPath = join(codexHomeDir, "hooks.json");
3670
+ const collisionPath = join(codexHomeDir, ".hooks-write-collision");
3671
+ const configBefore = 'model = "before"\n';
3672
+ await writeFile(configPath, configBefore);
3673
+ if (fixture === "regular file") {
3674
+ await writeFile(collisionPath, "collision\n");
3675
+ }
3676
+ else {
3677
+ const targetPath = join(wd, "collision-target");
3678
+ await writeFile(targetPath, "collision\n");
3679
+ await symlink(targetPath, collisionPath);
3680
+ }
3681
+ resetTemporaryPath = setNativeHookTransactionTemporaryPathForTest((path, purpose) => path === hooksPath && purpose === "write"
3682
+ ? collisionPath
3683
+ : join(dirname(path), `.${purpose}-unused`));
3684
+ await assert.rejects(setup({
3685
+ scope: "user",
3686
+ installMode: "legacy",
3687
+ skipNativeAgentRefresh: true,
3688
+ }), /EEXIST/);
3689
+ assert.equal(await readFile(collisionPath, "utf-8"), "collision\n");
3690
+ assert.equal(await readFile(configPath, "utf-8"), configBefore);
3691
+ assert.equal(existsSync(hooksPath), false);
3692
+ });
3693
+ });
3694
+ }
3695
+ finally {
3696
+ resetTemporaryPath?.();
3697
+ await rm(wd, { recursive: true, force: true });
3698
+ }
3699
+ }
3700
+ });
3701
+ it("fails closed when a non-throwing injector replaces an owned write temporary", async () => {
3702
+ const fixtures = ["regular file", "symlink"];
3703
+ for (const fixture of fixtures) {
3704
+ const wd = await mkdtemp(join(tmpdir(), "omx-setup-write-temp-replacement-"));
3705
+ let resetFailureInjector;
3706
+ let resetTemporaryPath;
3707
+ try {
3708
+ await withIsolatedUserHome(wd, async (codexHomeDir) => {
3709
+ await withTempCwd(wd, async () => {
3710
+ const configPath = join(codexHomeDir, "config.toml");
3711
+ const hooksPath = join(codexHomeDir, "hooks.json");
3712
+ const temporaryPath = join(codexHomeDir, ".hooks-write-owned");
3713
+ const foreignTargetPath = join(wd, "foreign-write-temporary-target");
3714
+ const configBefore = Buffer.from('model = "before"\n', "utf-8");
3715
+ const foreignContents = `foreign ${fixture} write temporary\n`;
3716
+ await writeFile(configPath, configBefore);
3717
+ if (fixture === "symlink") {
3718
+ await writeFile(foreignTargetPath, foreignContents);
3719
+ }
3720
+ resetTemporaryPath = setNativeHookTransactionTemporaryPathForTest((path, purpose) => path === hooksPath && purpose === "write"
3721
+ ? temporaryPath
3722
+ : join(dirname(path), `.${basename(path)}.${purpose}-fallback`));
3723
+ let injected = false;
3724
+ resetFailureInjector = setNativeHookTransactionFailureInjectorForTest((stage, target) => {
3725
+ if (stage !== "before_rename" || target.kind !== "hooks")
3726
+ return;
3727
+ injected = true;
3728
+ rmSync(temporaryPath);
3729
+ if (fixture === "regular file") {
3730
+ writeFileSync(temporaryPath, foreignContents);
3731
+ }
3732
+ else {
3733
+ symlinkSync(foreignTargetPath, temporaryPath);
3734
+ }
3735
+ });
3736
+ await assert.rejects(setup({
3737
+ scope: "user",
3738
+ installMode: "legacy",
3739
+ skipNativeAgentRefresh: true,
3740
+ }), /temporary.*manual recovery/);
3741
+ assert.equal(injected, true);
3742
+ const replacementStatus = await lstat(temporaryPath);
3743
+ if (fixture === "regular file") {
3744
+ assert.equal(replacementStatus.isFile(), true);
3745
+ assert.equal(await readFile(temporaryPath, "utf-8"), foreignContents);
3746
+ }
3747
+ else {
3748
+ assert.equal(replacementStatus.isSymbolicLink(), true);
3749
+ assert.equal(await readFile(foreignTargetPath, "utf-8"), foreignContents);
3750
+ }
3751
+ assert.equal(existsSync(hooksPath), false);
3752
+ assert.deepEqual(await readFile(configPath), configBefore);
3753
+ });
3754
+ });
3755
+ }
3756
+ finally {
3757
+ resetFailureInjector?.();
3758
+ resetTemporaryPath?.();
3759
+ await rm(wd, { recursive: true, force: true });
3760
+ }
3761
+ }
3762
+ });
3763
+ it("fails closed when a non-throwing injector replaces an owned staged deletion", async () => {
3764
+ const fixtures = ["regular file", "symlink"];
3765
+ for (const fixture of fixtures) {
3766
+ const wd = await mkdtemp(join(tmpdir(), "omx-setup-staged-delete-replacement-"));
3767
+ const resetPlatform = setNativeHookTransactionPlatformForTest("win32");
3768
+ let resetFailureInjector;
3769
+ let resetTemporaryPath;
3770
+ try {
3771
+ await withIsolatedUserHome(wd, async (codexHomeDir) => {
3772
+ await withTempCwd(wd, async () => {
3773
+ await setup({
3774
+ scope: "user",
3775
+ installMode: "legacy",
3776
+ skipNativeAgentRefresh: true,
3777
+ });
3778
+ const shimPath = buildManagedCodexNativeHookWindowsShimPath(codexHomeDir);
3779
+ const hooksPath = join(codexHomeDir, "hooks.json");
3780
+ const configPath = join(codexHomeDir, "config.toml");
3781
+ const stagedDeletionPath = join(codexHomeDir, ".hooks-delete-owned");
3782
+ const foreignTargetPath = join(wd, "foreign-staged-deletion-target");
3783
+ const shimBefore = await readFile(shimPath);
3784
+ const hooksBefore = await readFile(hooksPath);
3785
+ const configBefore = await readFile(configPath);
3786
+ const foreignContents = `foreign ${fixture} staged deletion\n`;
3787
+ if (fixture === "symlink") {
3788
+ await writeFile(foreignTargetPath, foreignContents);
3789
+ }
3790
+ resetTemporaryPath = setNativeHookTransactionTemporaryPathForTest((path, purpose) => path === hooksPath && purpose === "delete"
3791
+ ? stagedDeletionPath
3792
+ : join(dirname(path), `.${basename(path)}.${purpose}-fallback`));
3793
+ let injected = false;
3794
+ resetFailureInjector = setNativeHookTransactionFailureInjectorForTest((stage, target) => {
3795
+ if (stage !== "before_remove" || target.kind !== "hooks")
3796
+ return;
3797
+ injected = true;
3798
+ rmSync(stagedDeletionPath);
3799
+ if (fixture === "regular file") {
3800
+ writeFileSync(stagedDeletionPath, foreignContents);
3801
+ }
3802
+ else {
3803
+ symlinkSync(foreignTargetPath, stagedDeletionPath);
3804
+ }
3805
+ });
3806
+ await assert.rejects(setup({
3807
+ scope: "user",
3808
+ installMode: "plugin",
3809
+ pluginAgentsMdPrompt: async () => false,
3810
+ skipNativeAgentRefresh: true,
3811
+ }), /staged deletion.*manual recovery/);
3812
+ assert.equal(injected, true);
3813
+ const replacementStatus = await lstat(stagedDeletionPath);
3814
+ if (fixture === "regular file") {
3815
+ assert.equal(replacementStatus.isFile(), true);
3816
+ assert.equal(await readFile(stagedDeletionPath, "utf-8"), foreignContents);
3817
+ }
3818
+ else {
3819
+ assert.equal(replacementStatus.isSymbolicLink(), true);
3820
+ assert.equal(await readFile(foreignTargetPath, "utf-8"), foreignContents);
3821
+ }
3822
+ assert.deepEqual(await readFile(shimPath), shimBefore);
3823
+ assert.deepEqual(await readFile(hooksPath), hooksBefore);
3824
+ assert.deepEqual(await readFile(configPath), configBefore);
3825
+ });
3826
+ });
3827
+ }
3828
+ finally {
3829
+ resetFailureInjector?.();
3830
+ resetTemporaryPath?.();
3831
+ resetPlatform();
3832
+ await rm(wd, { recursive: true, force: true });
3833
+ }
3834
+ }
3835
+ });
3836
+ it("rolls back a renamed native hook when post-rename verification fails", async () => {
3837
+ const wd = await mkdtemp(join(tmpdir(), "omx-setup-post-rename-rollback-"));
3838
+ const resetPlatform = setNativeHookTransactionPlatformForTest("linux");
3839
+ let resetFailureInjector;
3840
+ try {
3841
+ await withIsolatedUserHome(wd, async (codexHomeDir) => {
3842
+ await withTempCwd(wd, async () => {
3843
+ const hooksPath = join(codexHomeDir, "hooks.json");
3844
+ const configPath = join(codexHomeDir, "config.toml");
3845
+ const hooksBefore = Buffer.from('{"hooks": {}}\n', "utf-8");
3846
+ const configBefore = Buffer.from('model = "before"\n', "utf-8");
3847
+ await writeFile(hooksPath, hooksBefore);
3848
+ await writeFile(configPath, configBefore);
3849
+ await Promise.all([chmod(hooksPath, 0o640), chmod(configPath, 0o640)]);
3850
+ resetFailureInjector = setNativeHookTransactionFailureInjectorForTest((stage, target) => {
3851
+ if (stage === "after_rename" && target.kind === "hooks") {
3852
+ throw new Error("injected post-rename verification failure");
3853
+ }
3854
+ });
3855
+ await assert.rejects(setup({
3856
+ scope: "user",
3857
+ installMode: "legacy",
3858
+ skipNativeAgentRefresh: true,
3859
+ codexFeaturesProbe: () => null,
3860
+ codexVersionProbe: () => null,
3861
+ }), /rolled back: injected post-rename verification failure/);
3862
+ assert.deepEqual(await readFile(hooksPath), hooksBefore);
3863
+ assert.deepEqual(await readFile(configPath), configBefore);
3864
+ assert.equal((await stat(hooksPath)).mode & 0o777, 0o640);
3865
+ assert.equal((await stat(configPath)).mode & 0o777, 0o640);
3866
+ });
3867
+ });
3868
+ }
3869
+ finally {
3870
+ resetFailureInjector?.();
3871
+ resetPlatform();
3872
+ await rm(wd, { recursive: true, force: true });
3873
+ }
3874
+ });
3875
+ it("preserves a concurrent replacement after staged source removal for manual recovery", async () => {
3876
+ const wd = await mkdtemp(join(tmpdir(), "omx-setup-post-remove-manual-recovery-"));
3877
+ const resetPlatform = setNativeHookTransactionPlatformForTest("linux");
3878
+ let resetFailureInjector;
3879
+ let resetTemporaryPath;
3880
+ try {
3881
+ await withIsolatedUserHome(wd, async (codexHomeDir) => {
3882
+ await withTempCwd(wd, async () => {
3883
+ await setup({
3884
+ scope: "user",
3885
+ installMode: "legacy",
3886
+ skipNativeAgentRefresh: true,
3887
+ });
3888
+ const hooksPath = join(codexHomeDir, "hooks.json");
3889
+ const configPath = join(codexHomeDir, "config.toml");
3890
+ const stagedDeletionPath = join(codexHomeDir, ".hooks-delete-post-remove");
3891
+ const hooksBefore = await readFile(hooksPath);
3892
+ const configBefore = await readFile(configPath);
3893
+ const foreignHooks = Buffer.from('{"hooks":{"Stop":[]}}\n', "utf-8");
3894
+ resetTemporaryPath = setNativeHookTransactionTemporaryPathForTest((path, purpose) => path === hooksPath && purpose === "delete"
3895
+ ? stagedDeletionPath
3896
+ : join(dirname(path), `.${basename(path)}.${purpose}-fallback`));
3897
+ resetFailureInjector = setNativeHookTransactionFailureInjectorForTest((stage, target) => {
3898
+ if (stage === "after_remove" && target.kind === "hooks") {
3899
+ writeFileSync(hooksPath, foreignHooks);
3900
+ throw new Error("injected post-remove verification failure");
3901
+ }
3902
+ });
3903
+ await assert.rejects(setup({
3904
+ scope: "user",
3905
+ installMode: "plugin",
3906
+ skipNativeAgentRefresh: true,
3907
+ pluginAgentsMdPrompt: async () => false,
3908
+ codexFeaturesProbe: () => [
3909
+ "hooks stable true",
3910
+ "plugin_hooks experimental true",
3911
+ "",
3912
+ ].join("\n"),
3913
+ }), (error) => {
3914
+ const message = error instanceof Error ? error.message : String(error);
3915
+ assert.match(message, /manual recovery/);
3916
+ assert.ok(message.includes(hooksPath));
3917
+ return true;
1960
3918
  });
3919
+ assert.deepEqual(await readFile(hooksPath), foreignHooks);
3920
+ assert.deepEqual(await readFile(configPath), configBefore);
3921
+ assert.deepEqual(await readFile(stagedDeletionPath), hooksBefore);
1961
3922
  });
1962
3923
  });
1963
3924
  }
1964
3925
  finally {
3926
+ resetFailureInjector?.();
3927
+ resetTemporaryPath?.();
3928
+ resetPlatform();
1965
3929
  await rm(wd, { recursive: true, force: true });
1966
3930
  }
1967
3931
  });
1968
- it("removes legacy user components when plugin mode is selected", async () => {
1969
- const wd = await mkdtemp(join(tmpdir(), "omx-setup-install-mode-"));
3932
+ it("never removes a concurrent replacement while rolling back a newly created artifact", async () => {
3933
+ const wd = await mkdtemp(join(tmpdir(), "omx-setup-rollback-remove-"));
3934
+ let resetFailureInjector;
1970
3935
  try {
1971
3936
  await withIsolatedUserHome(wd, async (codexHomeDir) => {
1972
3937
  await withTempCwd(wd, async () => {
1973
- await setup({ scope: "user", installMode: "legacy" });
1974
- const askSkillPath = join(codexHomeDir, "skills", "ask", "SKILL.md");
1975
- const promptPath = join(codexHomeDir, "prompts", "executor.md");
1976
- const agentPath = join(codexHomeDir, "agents", "planner.toml");
1977
3938
  const hooksPath = join(codexHomeDir, "hooks.json");
1978
3939
  const configPath = join(codexHomeDir, "config.toml");
1979
- const agentsMdPath = join(codexHomeDir, "AGENTS.md");
1980
- assert.equal(existsSync(askSkillPath), true);
1981
- assert.equal(existsSync(promptPath), true);
1982
- assert.equal(existsSync(agentPath), true);
1983
- assert.equal(existsSync(hooksPath), true);
1984
- assert.equal(existsSync(configPath), true);
1985
- assert.equal(existsSync(agentsMdPath), true);
1986
- await setup({ scope: "user", installMode: "plugin" });
1987
- assert.equal(existsSync(askSkillPath), false);
1988
- assert.equal(existsSync(promptPath), false);
1989
- assert.equal(existsSync(agentPath), true);
1990
- assert.equal(existsSync(hooksPath), false);
1991
- assert.equal(existsSync(agentsMdPath), true);
1992
- const config = await readFile(configPath, "utf-8");
1993
- assert.match(config, /^plugin_hooks = true$/m);
1994
- assert.doesNotMatch(config, /^\s*(?:notify)\s*=|^\s*\[mcp_servers[.\]]/m);
1995
- assert.match(config, /^developer_instructions\s*=/m);
1996
- assert.match(config, /You have oh-my-codex installed\. AGENTS\.md/);
3940
+ const configBefore = Buffer.from('notify = ["node", "/tmp/user-notify.js"]\n', "utf-8");
3941
+ const foreignHooks = Buffer.from('{"hooks":{"Stop":[]}}\n', "utf-8");
3942
+ await writeFile(configPath, configBefore);
3943
+ resetFailureInjector = setNativeHookTransactionFailureInjectorForTest((stage, target) => {
3944
+ if (stage === "before_temp_write" && target.kind === "config") {
3945
+ throw new Error("injected config failure");
3946
+ }
3947
+ if (stage === "before_rollback_remove" && target.kind === "hooks") {
3948
+ writeFileSync(hooksPath, foreignHooks);
3949
+ }
3950
+ });
3951
+ await assert.rejects(setup({
3952
+ scope: "user",
3953
+ installMode: "legacy",
3954
+ skipNativeAgentRefresh: true,
3955
+ }), /rollback failed/);
3956
+ assert.deepEqual(await readFile(hooksPath), foreignHooks);
3957
+ assert.deepEqual(await readFile(configPath), configBefore);
1997
3958
  });
1998
3959
  });
1999
3960
  }
2000
3961
  finally {
3962
+ resetFailureInjector?.();
2001
3963
  await rm(wd, { recursive: true, force: true });
2002
3964
  }
2003
3965
  });
2004
- it("preserves existing AGENTS.md when plugin AGENTS defaults are declined", async () => {
2005
- const wd = await mkdtemp(join(tmpdir(), "omx-setup-install-mode-"));
3966
+ it("does not roll back when the second staged-deletion copy drifts after the first cleanup", async () => {
3967
+ const wd = await mkdtemp(join(tmpdir(), "omx-setup-staged-deletion-cleanup-"));
3968
+ const resetPlatform = setNativeHookTransactionPlatformForTest("win32");
3969
+ let resetFailureInjector;
3970
+ let resetTemporaryPath;
2006
3971
  try {
2007
3972
  await withIsolatedUserHome(wd, async (codexHomeDir) => {
2008
3973
  await withTempCwd(wd, async () => {
2009
- await setup({ scope: "user", installMode: "legacy" });
2010
- const agentsMdPath = join(codexHomeDir, "AGENTS.md");
2011
- const before = await readFile(agentsMdPath, "utf-8");
2012
- assert.match(before, /<!-- omx:generated:agents-md -->/);
2013
3974
  await setup({
3975
+ scope: "user",
3976
+ installMode: "legacy",
3977
+ skipNativeAgentRefresh: true,
3978
+ });
3979
+ const shimPath = buildManagedCodexNativeHookWindowsShimPath(codexHomeDir);
3980
+ const hooksPath = join(codexHomeDir, "hooks.json");
3981
+ const configPath = join(codexHomeDir, "config.toml");
3982
+ const stagedDeletionPath = (path, purpose) => join(dirname(path), `.${basename(path)}.cleanup-${purpose}`);
3983
+ const foreignStagedCopy = Buffer.from("foreign second staged deletion\n", "utf-8");
3984
+ let cleanupCount = 0;
3985
+ let driftedStagedPath;
3986
+ resetTemporaryPath = setNativeHookTransactionTemporaryPathForTest(stagedDeletionPath);
3987
+ resetFailureInjector = setNativeHookTransactionFailureInjectorForTest((stage, target) => {
3988
+ if (stage !== "before_staged_cleanup" ||
3989
+ (target.kind !== "shim" && target.kind !== "hooks")) {
3990
+ return;
3991
+ }
3992
+ cleanupCount += 1;
3993
+ if (cleanupCount === 2) {
3994
+ driftedStagedPath = stagedDeletionPath(target.path, "delete");
3995
+ writeFileSync(driftedStagedPath, foreignStagedCopy);
3996
+ }
3997
+ });
3998
+ await assert.rejects(setup({
2014
3999
  scope: "user",
2015
4000
  installMode: "plugin",
4001
+ skipNativeAgentRefresh: true,
2016
4002
  pluginAgentsMdPrompt: async () => false,
4003
+ }), /committed but staged deletion cleanup failed/);
4004
+ assert.equal(cleanupCount, 2);
4005
+ assert.ok(driftedStagedPath);
4006
+ assert.deepEqual(await readFile(driftedStagedPath), foreignStagedCopy);
4007
+ assert.equal(existsSync(shimPath), false);
4008
+ assert.equal(existsSync(hooksPath), false);
4009
+ assert.match(await readFile(configPath, "utf-8"), /^plugin_hooks = true$/m);
4010
+ });
4011
+ });
4012
+ }
4013
+ finally {
4014
+ resetFailureInjector?.();
4015
+ resetTemporaryPath?.();
4016
+ resetPlatform();
4017
+ await rm(wd, { recursive: true, force: true });
4018
+ }
4019
+ });
4020
+ it("treats applied hook, shim, and config snapshots as CAS through commit finalization", async () => {
4021
+ const fixtures = [
4022
+ {
4023
+ name: "later commit",
4024
+ stage: "before_temp_write",
4025
+ target: "shim",
4026
+ drift: "hooks",
4027
+ committed: false,
4028
+ },
4029
+ {
4030
+ name: "staged cleanup",
4031
+ stage: "before_staged_cleanup",
4032
+ target: "hooks",
4033
+ drift: "shim",
4034
+ committed: false,
4035
+ },
4036
+ {
4037
+ name: "finalization",
4038
+ stage: "after_staged_cleanup",
4039
+ target: "config",
4040
+ drift: "config",
4041
+ committed: true,
4042
+ },
4043
+ ];
4044
+ for (const fixture of fixtures) {
4045
+ const wd = await mkdtemp(join(tmpdir(), `omx-setup-applied-${fixture.name}-`));
4046
+ const resetPlatform = setNativeHookTransactionPlatformForTest("win32");
4047
+ let resetFailureInjector;
4048
+ try {
4049
+ await withIsolatedUserHome(wd, async (codexHomeDir) => {
4050
+ await withTempCwd(wd, async () => {
4051
+ await setup({
4052
+ scope: "user",
4053
+ installMode: "legacy",
4054
+ skipNativeAgentRefresh: true,
4055
+ });
4056
+ const shimPath = buildManagedCodexNativeHookWindowsShimPath(codexHomeDir);
4057
+ const hooksPath = join(codexHomeDir, "hooks.json");
4058
+ const configPath = join(codexHomeDir, "config.toml");
4059
+ const before = {
4060
+ shim: await readFile(shimPath),
4061
+ hooks: await readFile(hooksPath),
4062
+ config: await readFile(configPath),
4063
+ };
4064
+ const paths = { shim: shimPath, hooks: hooksPath, config: configPath };
4065
+ const foreign = Buffer.from(`foreign applied ${fixture.drift} ${fixture.name}\n`, "utf-8");
4066
+ let injected = false;
4067
+ resetFailureInjector = setNativeHookTransactionFailureInjectorForTest((stage, target) => {
4068
+ if (stage !== fixture.stage ||
4069
+ target.kind !== fixture.target ||
4070
+ injected) {
4071
+ return;
4072
+ }
4073
+ injected = true;
4074
+ writeFileSync(paths[fixture.drift], foreign);
4075
+ });
4076
+ await assert.rejects(setup({
4077
+ scope: "user",
4078
+ installMode: "plugin",
4079
+ pluginAgentsMdPrompt: async () => false,
4080
+ skipNativeAgentRefresh: true,
4081
+ }), fixture.committed
4082
+ ? /committed but staged deletion cleanup failed during finalization/
4083
+ : /rollback failed/);
4084
+ assert.equal(injected, true, fixture.name);
4085
+ if (fixture.name === "staged cleanup") {
4086
+ assert.equal(existsSync(hooksPath), false);
4087
+ assert.deepEqual(await readFile(shimPath), foreign);
4088
+ assert.notDeepEqual(await readFile(configPath), before.config);
4089
+ return;
4090
+ }
4091
+ if (fixture.committed) {
4092
+ assert.equal(existsSync(hooksPath), false);
4093
+ assert.equal(existsSync(shimPath), false);
4094
+ assert.deepEqual(await readFile(configPath), foreign);
4095
+ return;
4096
+ }
4097
+ assert.deepEqual(await readFile(hooksPath), fixture.drift === "hooks" ? foreign : before.hooks);
4098
+ assert.deepEqual(await readFile(shimPath), before.shim);
4099
+ assert.deepEqual(await readFile(configPath), before.config);
2017
4100
  });
2018
- assert.equal(await readFile(agentsMdPath, "utf-8"), before);
4101
+ });
4102
+ }
4103
+ finally {
4104
+ resetFailureInjector?.();
4105
+ resetPlatform();
4106
+ await rm(wd, { recursive: true, force: true });
4107
+ }
4108
+ }
4109
+ });
4110
+ it("preserves a same-byte foreign replacement made immediately after a native-hook rename", async () => {
4111
+ const wd = await mkdtemp(join(tmpdir(), "omx-setup-immediate-post-rename-"));
4112
+ let resetFailureInjector;
4113
+ try {
4114
+ await withIsolatedUserHome(wd, async (codexHomeDir) => {
4115
+ await withTempCwd(wd, async () => {
4116
+ const hooksPath = join(codexHomeDir, "hooks.json");
4117
+ const configPath = join(codexHomeDir, "config.toml");
4118
+ const configBefore = Buffer.from('model = "before"\n', "utf-8");
4119
+ let replacement;
4120
+ let replacedInode;
4121
+ await writeFile(configPath, configBefore);
4122
+ resetFailureInjector = setNativeHookTransactionFailureInjectorForTest((stage, target) => {
4123
+ if (stage !== "after_rename" || target.kind !== "hooks")
4124
+ return;
4125
+ replacement = readFileSync(hooksPath);
4126
+ const ownedInode = lstatSync(hooksPath).ino;
4127
+ const foreignPath = join(codexHomeDir, ".foreign-hooks-same-byte");
4128
+ writeFileSync(foreignPath, replacement);
4129
+ assert.notEqual(lstatSync(foreignPath).ino, ownedInode);
4130
+ rmSync(hooksPath);
4131
+ renameSync(foreignPath, hooksPath);
4132
+ replacedInode = lstatSync(hooksPath).ino;
4133
+ assert.notEqual(replacedInode, ownedInode);
4134
+ });
4135
+ await assert.rejects(setup({
4136
+ scope: "user",
4137
+ installMode: "legacy",
4138
+ skipNativeAgentRefresh: true,
4139
+ codexFeaturesProbe: () => null,
4140
+ codexVersionProbe: () => null,
4141
+ }), /rollback failed/);
4142
+ assert.ok(replacement);
4143
+ assert.ok(replacedInode);
4144
+ assert.deepEqual(await readFile(hooksPath), replacement);
4145
+ assert.deepEqual(await readFile(configPath), configBefore);
2019
4146
  });
2020
4147
  });
2021
4148
  }
2022
4149
  finally {
4150
+ resetFailureInjector?.();
2023
4151
  await rm(wd, { recursive: true, force: true });
2024
4152
  }
2025
4153
  });
2026
- it("repairs existing AGENTS.md during non-interactive plugin force setup", async () => {
2027
- const wd = await mkdtemp(join(tmpdir(), "omx-setup-install-mode-"));
4154
+ it("preflights every staged native-hook recovery copy before rollback", async () => {
4155
+ const wd = await mkdtemp(join(tmpdir(), "omx-setup-rollback-recovery-preflight-"));
4156
+ const resetPlatform = setNativeHookTransactionPlatformForTest("win32");
4157
+ let resetFailureInjector;
4158
+ let resetTemporaryPath;
2028
4159
  try {
2029
4160
  await withIsolatedUserHome(wd, async (codexHomeDir) => {
2030
4161
  await withTempCwd(wd, async () => {
2031
- await mkdir(codexHomeDir, { recursive: true });
2032
- const agentsMdPath = join(codexHomeDir, "AGENTS.md");
2033
- await writeFile(agentsMdPath, "# local instructions\n");
2034
- await setup({ scope: "user", installMode: "plugin", force: true });
2035
- const after = await readFile(agentsMdPath, "utf-8");
2036
- assert.match(after, /<!-- omx:generated:agents-md -->/);
2037
- assert.match(after, /oh-my-codex - Intelligent Multi-Agent Orchestration/);
2038
- const backupRoot = join(wd, "home", ".omx", "backups", "setup");
2039
- const backupRuns = await readdir(backupRoot);
2040
- assert.equal(backupRuns.some((entry) => existsSync(join(backupRoot, entry, ".codex", "AGENTS.md"))), true);
4162
+ await setup({ scope: "user", installMode: "legacy", skipNativeAgentRefresh: true });
4163
+ const hooksPath = join(codexHomeDir, "hooks.json");
4164
+ const shimPath = buildManagedCodexNativeHookWindowsShimPath(codexHomeDir);
4165
+ const configPath = join(codexHomeDir, "config.toml");
4166
+ const configBefore = await readFile(configPath);
4167
+ const stagedHooksPath = join(codexHomeDir, ".hooks-preflight-recovery");
4168
+ resetTemporaryPath = setNativeHookTransactionTemporaryPathForTest((path, purpose) => path === hooksPath && purpose === "delete"
4169
+ ? stagedHooksPath
4170
+ : join(dirname(path), `.${basename(path)}.${purpose}-preflight`));
4171
+ resetFailureInjector = setNativeHookTransactionFailureInjectorForTest((stage, target) => {
4172
+ if (stage !== "before_readback" || target.kind !== "config")
4173
+ return;
4174
+ rmSync(stagedHooksPath);
4175
+ throw new Error("injected missing staged recovery copy");
4176
+ });
4177
+ await assert.rejects(setup({
4178
+ scope: "user",
4179
+ installMode: "plugin",
4180
+ pluginAgentsMdPrompt: async () => false,
4181
+ skipNativeAgentRefresh: true,
4182
+ codexFeaturesProbe: () => "hooks stable true\nplugin_hooks experimental true\n",
4183
+ }), /rollback failed.*recovery preflight/);
4184
+ assert.equal(existsSync(hooksPath), false);
4185
+ assert.equal(existsSync(shimPath), false);
4186
+ assert.notDeepEqual(await readFile(configPath), configBefore);
4187
+ assert.equal(existsSync(stagedHooksPath), false);
2041
4188
  });
2042
4189
  });
2043
4190
  }
2044
4191
  finally {
4192
+ resetFailureInjector?.();
4193
+ resetTemporaryPath?.();
4194
+ resetPlatform();
2045
4195
  await rm(wd, { recursive: true, force: true });
2046
4196
  }
2047
4197
  });
2048
- it("archives stale legacy prompts and preserves modified native agents when plugin mode refreshes", async () => {
2049
- const wd = await mkdtemp(join(tmpdir(), "omx-setup-install-mode-"));
4198
+ it("stops later native-hook rollback restores when the first restored artifact drifts", async () => {
4199
+ const wd = await mkdtemp(join(tmpdir(), "omx-setup-rollback-restored-drift-"));
4200
+ const resetPlatform = setNativeHookTransactionPlatformForTest("win32");
4201
+ let resetFailureInjector;
2050
4202
  try {
2051
4203
  await withIsolatedUserHome(wd, async (codexHomeDir) => {
2052
4204
  await withTempCwd(wd, async () => {
2053
- await setup({ scope: "user", installMode: "legacy" });
2054
- const promptPath = join(codexHomeDir, "prompts", "executor.md");
2055
- const agentPath = join(codexHomeDir, "agents", "planner.toml");
2056
- await writeFile(promptPath, "---\ndescription: stale legacy executor prompt\n---\n\nold executor body\n");
2057
- const staleAgentToml = [
2058
- "# oh-my-codex agent: planner",
2059
- 'name = "planner"',
2060
- 'description = "stale legacy generated planner"',
2061
- 'developer_instructions = """old planner body"""',
2062
- "",
2063
- ].join("\n");
2064
- await writeFile(agentPath, staleAgentToml);
2065
- const output = await captureConsoleOutput(async () => {
2066
- await setup({ scope: "user", installMode: "plugin" });
4205
+ await setup({ scope: "user", installMode: "legacy", skipNativeAgentRefresh: true });
4206
+ const hooksPath = join(codexHomeDir, "hooks.json");
4207
+ const shimPath = buildManagedCodexNativeHookWindowsShimPath(codexHomeDir);
4208
+ const configPath = join(codexHomeDir, "config.toml");
4209
+ const configBefore = await readFile(configPath);
4210
+ let injectedFailure = false;
4211
+ let drifted = false;
4212
+ resetFailureInjector = setNativeHookTransactionFailureInjectorForTest((stage, target) => {
4213
+ if (stage === "before_readback" && target.kind === "config" && !injectedFailure) {
4214
+ injectedFailure = true;
4215
+ throw new Error("injected rollback start failure");
4216
+ }
4217
+ if (stage === "before_rollback_rename" && target.kind === "shim" && !drifted) {
4218
+ const restoredInode = lstatSync(configPath).ino;
4219
+ const foreignPath = join(codexHomeDir, ".foreign-restored-config");
4220
+ writeFileSync(foreignPath, configBefore);
4221
+ assert.notEqual(lstatSync(foreignPath).ino, restoredInode);
4222
+ rmSync(configPath);
4223
+ renameSync(foreignPath, configPath);
4224
+ drifted = lstatSync(configPath).ino !== restoredInode;
4225
+ }
2067
4226
  });
2068
- assert.equal(existsSync(promptPath), false);
2069
- assert.equal(existsSync(agentPath), true);
2070
- assert.equal(await readFile(agentPath, "utf-8"), staleAgentToml);
2071
- assert.match(output, /Archived and removed .* legacy OMX-managed prompt file/);
2072
- assert.match(output, /Native agent role refresh complete/);
2073
- const backupRoot = join(wd, "home", ".omx", "backups", "setup");
2074
- const backupRuns = await readdir(backupRoot);
2075
- assert.ok(backupRuns.length > 0);
2076
- assert.equal(backupRuns.some((entry) => existsSync(join(backupRoot, entry, ".codex", "prompts", "executor.md"))), true);
2077
- assert.equal(backupRuns.some((entry) => existsSync(join(backupRoot, entry, ".codex", "agents", "planner.toml"))), false);
4227
+ await assert.rejects(setup({
4228
+ scope: "user",
4229
+ installMode: "plugin",
4230
+ pluginAgentsMdPrompt: async () => false,
4231
+ skipNativeAgentRefresh: true,
4232
+ codexFeaturesProbe: () => "hooks stable true\nplugin_hooks experimental true\n",
4233
+ }), /rollback failed/);
4234
+ assert.equal(injectedFailure, true);
4235
+ assert.equal(drifted, true);
4236
+ assert.deepEqual(await readFile(configPath), configBefore);
4237
+ assert.equal(existsSync(shimPath), false);
4238
+ assert.equal(existsSync(hooksPath), false);
2078
4239
  });
2079
4240
  });
2080
4241
  }
2081
4242
  finally {
4243
+ resetFailureInjector?.();
4244
+ resetPlatform();
2082
4245
  await rm(wd, { recursive: true, force: true });
2083
4246
  }
2084
4247
  });
2085
- it("preserves unmanaged native agent TOMLs with obsolete skill_ref during plugin refresh", async () => {
2086
- const wd = await mkdtemp(join(tmpdir(), "omx-setup-install-mode-"));
4248
+ it("preserves a foreign inode injected after final setup rename validation", async () => {
4249
+ const wd = await mkdtemp(join(tmpdir(), "omx-setup-final-rename-claim-"));
4250
+ let resetFailureInjector;
2087
4251
  try {
2088
4252
  await withIsolatedUserHome(wd, async (codexHomeDir) => {
2089
4253
  await withTempCwd(wd, async () => {
2090
- await setup({ scope: "user", installMode: "legacy" });
2091
- const customAgentPath = join(codexHomeDir, "agents", "custom-reviewer.toml");
2092
- const generatedAgentPath = join(codexHomeDir, "agents", "ghost.toml");
2093
- const customAgentToml = [
2094
- 'name = "custom-reviewer"',
2095
- 'description = "user-managed reviewer"',
2096
- 'skill_ref = "custom-reviewer"',
2097
- "",
2098
- ].join("\n");
2099
- await writeFile(customAgentPath, customAgentToml);
2100
- await writeFile(generatedAgentPath, [
2101
- "# oh-my-codex agent: ghost",
2102
- 'name = "ghost"',
2103
- 'description = "obsolete generated reviewer"',
2104
- 'skill_ref = "ghost"',
2105
- "",
2106
- ].join("\n"));
2107
- await setup({ scope: "user", installMode: "plugin" });
2108
- assert.equal(await readFile(customAgentPath, "utf-8"), customAgentToml);
2109
- assert.equal(existsSync(generatedAgentPath), false);
4254
+ const hooksPath = join(codexHomeDir, "hooks.json");
4255
+ const configPath = join(codexHomeDir, "config.toml");
4256
+ const configBefore = Buffer.from('model = "before"\n', "utf-8");
4257
+ const foreignHooks = Buffer.from('{"hooks":{"Stop":[]}}\n', "utf-8");
4258
+ await writeFile(configPath, configBefore);
4259
+ await writeFile(hooksPath, '{"hooks": {}}\n');
4260
+ let foreignInode;
4261
+ resetFailureInjector = setNativeHookTransactionFailureInjectorForTest((stage, target) => {
4262
+ if (stage !== "after_final_rename_validation" || target.kind !== "hooks")
4263
+ return;
4264
+ const foreignPath = join(codexHomeDir, ".foreign-final-rename-hooks");
4265
+ writeFileSync(foreignPath, foreignHooks);
4266
+ foreignInode = lstatSync(foreignPath).ino;
4267
+ rmSync(hooksPath);
4268
+ renameSync(foreignPath, hooksPath);
4269
+ });
4270
+ await assert.rejects(setup({ scope: "user", installMode: "legacy", skipNativeAgentRefresh: true }), /precondition changed/);
4271
+ assert.ok(foreignInode);
4272
+ assert.equal(lstatSync(hooksPath).ino, foreignInode);
4273
+ assert.deepEqual(await readFile(hooksPath), foreignHooks);
4274
+ assert.deepEqual(await readFile(configPath), configBefore);
2110
4275
  });
2111
4276
  });
2112
4277
  }
2113
4278
  finally {
4279
+ resetFailureInjector?.();
2114
4280
  await rm(wd, { recursive: true, force: true });
2115
4281
  }
2116
4282
  });
2117
- it("counts plugin cleanup skill directory backups in the setup summary", async () => {
2118
- const wd = await mkdtemp(join(tmpdir(), "omx-setup-install-mode-"));
4283
+ it("preserves a foreign inode injected after final setup removal validation", async () => {
4284
+ const wd = await mkdtemp(join(tmpdir(), "omx-setup-final-remove-claim-"));
4285
+ let resetFailureInjector;
2119
4286
  try {
2120
4287
  await withIsolatedUserHome(wd, async (codexHomeDir) => {
2121
4288
  await withTempCwd(wd, async () => {
2122
- await setup({ scope: "user", installMode: "legacy" });
2123
- await seedPluginCacheFromInstalledSkills(codexHomeDir);
2124
- const output = await captureConsoleOutput(async () => {
2125
- await setup({ scope: "user", installMode: "plugin" });
4289
+ await setup({ scope: "user", installMode: "legacy", skipNativeAgentRefresh: true });
4290
+ const hooksPath = join(codexHomeDir, "hooks.json");
4291
+ const configPath = join(codexHomeDir, "config.toml");
4292
+ const configBefore = await readFile(configPath);
4293
+ const foreignHooks = Buffer.from('{"hooks":{"Stop":[]}}\n', "utf-8");
4294
+ let foreignInode;
4295
+ resetFailureInjector = setNativeHookTransactionFailureInjectorForTest((stage, target) => {
4296
+ if (stage !== "after_final_remove_validation" || target.kind !== "hooks")
4297
+ return;
4298
+ const foreignPath = join(codexHomeDir, ".foreign-final-remove-hooks");
4299
+ writeFileSync(foreignPath, foreignHooks);
4300
+ foreignInode = lstatSync(foreignPath).ino;
4301
+ rmSync(hooksPath);
4302
+ renameSync(foreignPath, hooksPath);
2126
4303
  });
2127
- const skillsSummary = output.match(/skills: updated=0, unchanged=0, backed_up=(\d+), skipped=0, removed=(\d+)/);
2128
- assert.notEqual(skillsSummary, null);
2129
- const backedUp = Number(skillsSummary?.[1]);
2130
- const removed = Number(skillsSummary?.[2]);
2131
- assert.ok(backedUp > 0);
2132
- assert.equal(backedUp, removed);
4304
+ await assert.rejects(setup({
4305
+ scope: "user",
4306
+ installMode: "plugin",
4307
+ pluginAgentsMdPrompt: async () => false,
4308
+ skipNativeAgentRefresh: true,
4309
+ }), /precondition changed/);
4310
+ assert.ok(foreignInode);
4311
+ assert.equal(lstatSync(hooksPath).ino, foreignInode);
4312
+ assert.deepEqual(await readFile(hooksPath), foreignHooks);
4313
+ assert.deepEqual(await readFile(configPath), configBefore);
2133
4314
  });
2134
4315
  });
2135
4316
  }
2136
4317
  finally {
4318
+ resetFailureInjector?.();
2137
4319
  await rm(wd, { recursive: true, force: true });
2138
4320
  }
2139
4321
  });
2140
- it("removes matching legacy user skills even when plugin readiness is proven", async () => {
2141
- const wd = await mkdtemp(join(tmpdir(), "omx-setup-install-mode-"));
4322
+ it("preserves a foreign inode injected after final setup rollback validation", async () => {
4323
+ const wd = await mkdtemp(join(tmpdir(), "omx-setup-final-restore-claim-"));
4324
+ let resetFailureInjector;
2142
4325
  try {
2143
4326
  await withIsolatedUserHome(wd, async (codexHomeDir) => {
2144
4327
  await withTempCwd(wd, async () => {
2145
- await setup({ scope: "user", installMode: "legacy" });
2146
- await seedPluginCacheFromInstalledSkills(codexHomeDir);
2147
- const askSkillDir = join(codexHomeDir, "skills", "ask");
2148
- const wikiSkillDir = join(codexHomeDir, "skills", "wiki");
2149
- assert.equal(existsSync(askSkillDir), true);
2150
- assert.equal(existsSync(wikiSkillDir), true);
2151
- const outputLines = [];
2152
- const previousLog = console.log;
2153
- console.log = (...args) => {
2154
- outputLines.push(args.join(" "));
2155
- };
2156
- try {
2157
- await setup({ scope: "user", installMode: "plugin" });
2158
- }
2159
- finally {
2160
- console.log = previousLog;
2161
- }
2162
- const setupOutput = outputLines.join("\n");
2163
- assert.equal(existsSync(askSkillDir), false);
2164
- assert.equal(existsSync(wikiSkillDir), false);
2165
- assert.match(setupOutput, /skills: updated=0, unchanged=0, backed_up=\d+, skipped=0, removed=\d+/);
2166
- const backupSetupRoot = join(wd, "home", ".omx", "backups", "setup");
2167
- const backupTimestamps = await readdir(backupSetupRoot);
2168
- assert.equal(backupTimestamps.length, 1);
2169
- const backupSkillsDir = join(backupSetupRoot, backupTimestamps[0], ".codex", "skills");
2170
- const backedUpSkillNames = await readdir(backupSkillsDir);
2171
- assert.ok(backedUpSkillNames.includes("ask"));
2172
- assert.ok(backedUpSkillNames.includes("wiki"));
2173
- assert.match(setupOutput, new RegExp(`skills: updated=0, unchanged=0, backed_up=${backedUpSkillNames.length}, skipped=0, removed=${backedUpSkillNames.length}`));
4328
+ const hooksPath = join(codexHomeDir, "hooks.json");
4329
+ const configPath = join(codexHomeDir, "config.toml");
4330
+ const configBefore = Buffer.from('model = "before"\n', "utf-8");
4331
+ const foreignHooks = Buffer.from('{"hooks":{"Stop":[]}}\n', "utf-8");
4332
+ await writeFile(configPath, configBefore);
4333
+ let foreignInode;
4334
+ resetFailureInjector = setNativeHookTransactionFailureInjectorForTest((stage, target) => {
4335
+ if (stage === "before_temp_write" && target.kind === "config") {
4336
+ throw new Error("injected rollback start");
4337
+ }
4338
+ if (stage !== "after_final_restore_validation" || target.kind !== "hooks")
4339
+ return;
4340
+ const foreignPath = join(codexHomeDir, ".foreign-final-restore-hooks");
4341
+ writeFileSync(foreignPath, foreignHooks);
4342
+ foreignInode = lstatSync(foreignPath).ino;
4343
+ rmSync(hooksPath);
4344
+ renameSync(foreignPath, hooksPath);
4345
+ });
4346
+ await assert.rejects(setup({ scope: "user", installMode: "legacy", skipNativeAgentRefresh: true }), /rollback failed/);
4347
+ assert.ok(foreignInode);
4348
+ assert.equal(lstatSync(hooksPath).ino, foreignInode);
4349
+ assert.deepEqual(await readFile(hooksPath), foreignHooks);
4350
+ assert.deepEqual(await readFile(configPath), configBefore);
2174
4351
  });
2175
4352
  });
2176
4353
  }
2177
4354
  finally {
4355
+ resetFailureInjector?.();
2178
4356
  await rm(wd, { recursive: true, force: true });
2179
4357
  }
2180
4358
  });
2181
- it("preserves customized legacy user skills during plugin cleanup", async () => {
2182
- const wd = await mkdtemp(join(tmpdir(), "omx-setup-install-mode-"));
4359
+ });
4360
+ describe("late setup failure transaction boundary", () => {
4361
+ it("does not commit config or hooks when a later setup phase fails", async () => {
4362
+ const wd = await mkdtemp(join(tmpdir(), "omx-setup-late-failure-"));
4363
+ let resetLateFailure;
2183
4364
  try {
2184
4365
  await withIsolatedUserHome(wd, async (codexHomeDir) => {
2185
4366
  await withTempCwd(wd, async () => {
2186
- await setup({ scope: "user", installMode: "legacy" });
2187
- await seedPluginCacheFromInstalledSkills(codexHomeDir);
2188
- const askSkillPath = join(codexHomeDir, "skills", "ask", "SKILL.md");
2189
- const wikiSkillDir = join(codexHomeDir, "skills", "wiki");
2190
- await writeFile(askSkillPath, "# customized ask\n");
2191
- await setup({ scope: "user", installMode: "plugin" });
2192
- assert.equal(await readFile(askSkillPath, "utf-8"), "# customized ask\n");
2193
- assert.equal(existsSync(wikiSkillDir), false);
4367
+ const hooksPath = join(codexHomeDir, "hooks.json");
4368
+ const configPath = join(codexHomeDir, "config.toml");
4369
+ const hooksBefore = Buffer.from('{"hooks":{"FutureEvent":[{"hooks":[{"type":"prompt","prompt":"keep"}]}]}}\n');
4370
+ const configBefore = Buffer.from('[features]\nhooks = true\n');
4371
+ await mkdir(codexHomeDir, { recursive: true });
4372
+ await writeFile(hooksPath, hooksBefore);
4373
+ await writeFile(configPath, configBefore);
4374
+ resetLateFailure = setSetupLatePhaseFailureInjectorForTest(() => {
4375
+ throw new Error("injected late setup phase failure");
4376
+ });
4377
+ await assert.rejects(setup({
4378
+ scope: "user",
4379
+ installMode: "legacy",
4380
+ skipNativeAgentRefresh: true,
4381
+ codexFeaturesProbe: () => null,
4382
+ codexVersionProbe: () => null,
4383
+ }), /injected late setup phase failure/);
4384
+ assert.deepEqual(await readFile(hooksPath), hooksBefore);
4385
+ assert.deepEqual(await readFile(configPath), configBefore);
4386
+ });
4387
+ });
4388
+ }
4389
+ finally {
4390
+ resetLateFailure?.();
4391
+ await rm(wd, { recursive: true, force: true });
4392
+ }
4393
+ });
4394
+ });
4395
+ describe("persisted merge policy lifecycle", () => {
4396
+ it("preserves matching policy through review and lets explicit sets override reset or a scope change", async () => {
4397
+ const wd = await mkdtemp(join(tmpdir(), "omx-setup-merge-policy-lifecycle-"));
4398
+ try {
4399
+ await withIsolatedUserHome(wd, async () => {
4400
+ await withTempCwd(wd, async () => {
4401
+ await mkdir(join(wd, ".omx"), { recursive: true });
4402
+ const statePath = join(wd, ".omx", "setup-scope.json");
4403
+ await writeFile(statePath, JSON.stringify({ scope: "user", installMode: "legacy", mergeAgents: true }));
4404
+ await setup({ persistedSetupReviewPrompt: async () => "review", setupScopePrompt: async () => "user", installModePrompt: async () => "legacy" });
4405
+ assert.equal(JSON.parse(await readFile(statePath, "utf-8")).mergeAgents, true);
4406
+ await setup({ persistedSetupReviewPrompt: async () => "reset", scope: "user", mergeAgents: false });
4407
+ assert.equal(JSON.parse(await readFile(statePath, "utf-8")).mergeAgents, false);
4408
+ await setup({ persistedSetupReviewPrompt: async () => "review", scope: "project", installMode: "legacy", mergeAgents: true });
4409
+ const changedScope = JSON.parse(await readFile(statePath, "utf-8"));
4410
+ assert.equal(changedScope.scope, "project");
4411
+ assert.equal(changedScope.mergeAgents, true);
4412
+ });
4413
+ });
4414
+ }
4415
+ finally {
4416
+ await rm(wd, { recursive: true, force: true });
4417
+ }
4418
+ });
4419
+ it("removes an explicit policy when clear succeeds", async () => {
4420
+ const wd = await mkdtemp(join(tmpdir(), "omx-setup-merge-policy-clear-"));
4421
+ try {
4422
+ await withIsolatedUserHome(wd, async () => {
4423
+ await withTempCwd(wd, async () => {
4424
+ await mkdir(join(wd, ".omx"), { recursive: true });
4425
+ const statePath = join(wd, ".omx", "setup-scope.json");
4426
+ await writeFile(statePath, JSON.stringify({ scope: "project", mergeAgents: true }));
4427
+ await setup({
4428
+ scope: "project",
4429
+ installMode: "legacy",
4430
+ mergeAgentsPolicy: { kind: "clear" },
4431
+ });
4432
+ assert.equal(Object.hasOwn(JSON.parse(await readFile(statePath, "utf-8")), "mergeAgents"), false);
4433
+ });
4434
+ });
4435
+ }
4436
+ finally {
4437
+ await rm(wd, { recursive: true, force: true });
4438
+ }
4439
+ });
4440
+ it("covers persisted and explicit merge policy precedence across force", async () => {
4441
+ const wd = await mkdtemp(join(tmpdir(), "omx-setup-merge-policy-force-"));
4442
+ try {
4443
+ await withIsolatedUserHome(wd, async () => {
4444
+ await withTempCwd(wd, async () => {
4445
+ const statePath = join(wd, ".omx", "setup-scope.json");
4446
+ const agentsPath = join(wd, "AGENTS.md");
4447
+ const rows = [
4448
+ { name: "persisted true + force", stored: true, policy: undefined, keepsCustom: true, persisted: true, refresh: "--merge-agents" },
4449
+ { name: "persisted false + force", stored: false, policy: undefined, keepsCustom: false, persisted: false, refresh: "--no-merge-agents" },
4450
+ { name: "absent + force", stored: undefined, policy: undefined, keepsCustom: false, persisted: undefined, refresh: undefined },
4451
+ { name: "force + explicit true", stored: false, policy: { kind: "set", value: true }, keepsCustom: true, persisted: true, refresh: "--merge-agents" },
4452
+ { name: "force + explicit false", stored: true, policy: { kind: "set", value: false }, keepsCustom: false, persisted: false, refresh: "--no-merge-agents" },
4453
+ { name: "force + explicit clear", stored: true, policy: { kind: "clear" }, keepsCustom: false, persisted: undefined, refresh: undefined },
4454
+ ];
4455
+ for (const row of rows) {
4456
+ await rm(join(wd, ".omx"), { recursive: true, force: true });
4457
+ await rm(join(wd, ".codex"), { recursive: true, force: true });
4458
+ await mkdir(join(wd, ".omx"), { recursive: true });
4459
+ await writeFile(statePath, JSON.stringify({ scope: "project", ...(row.stored === undefined ? {} : { mergeAgents: row.stored }) }));
4460
+ await writeFile(agentsPath, `# custom ${row.name}\n`);
4461
+ await setup({
4462
+ scope: "project",
4463
+ installMode: "legacy",
4464
+ force: true,
4465
+ mergeAgentsPolicy: row.policy,
4466
+ });
4467
+ const agents = await readFile(agentsPath, "utf-8");
4468
+ assert.equal(agents.includes(`# custom ${row.name}`), row.keepsCustom, row.name);
4469
+ const persisted = JSON.parse(await readFile(statePath, "utf-8"));
4470
+ assert.equal(persisted.mergeAgents, row.persisted, row.name);
4471
+ assert.equal(Object.hasOwn(persisted, "force"), false, row.name);
4472
+ const refreshArgs = resolveSetupRefreshArgs(wd);
4473
+ assert.equal(refreshArgs.includes("--force"), false, row.name);
4474
+ if (row.refresh)
4475
+ assert.equal(refreshArgs.includes(row.refresh), true, row.name);
4476
+ else
4477
+ assert.equal(refreshArgs.some((arg) => arg === "--merge-agents" || arg === "--no-merge-agents"), false, row.name);
4478
+ }
2194
4479
  });
2195
4480
  });
2196
4481
  }