oh-my-codex 0.20.3 → 0.21.0

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 (1126) hide show
  1. package/Cargo.lock +35 -18
  2. package/Cargo.toml +1 -1
  3. package/README.md +22 -19
  4. package/crates/omx-runtime/Cargo.toml +3 -0
  5. package/crates/omx-runtime/src/main.rs +390 -0
  6. package/crates/omx-runtime/tests/execution.rs +264 -0
  7. package/dist/adapt/__tests__/herdr-target.test.d.ts +2 -0
  8. package/dist/adapt/__tests__/herdr-target.test.d.ts.map +1 -0
  9. package/dist/adapt/__tests__/herdr-target.test.js +72 -0
  10. package/dist/adapt/__tests__/herdr-target.test.js.map +1 -0
  11. package/dist/adapt/contracts.d.ts +1 -1
  12. package/dist/adapt/contracts.d.ts.map +1 -1
  13. package/dist/adapt/contracts.js +1 -1
  14. package/dist/adapt/contracts.js.map +1 -1
  15. package/dist/adapt/herdr/__tests__/authority.test.d.ts +2 -0
  16. package/dist/adapt/herdr/__tests__/authority.test.d.ts.map +1 -0
  17. package/dist/adapt/herdr/__tests__/authority.test.js +49 -0
  18. package/dist/adapt/herdr/__tests__/authority.test.js.map +1 -0
  19. package/dist/adapt/herdr/__tests__/bridge.test.d.ts +2 -0
  20. package/dist/adapt/herdr/__tests__/bridge.test.d.ts.map +1 -0
  21. package/dist/adapt/herdr/__tests__/bridge.test.js +176 -0
  22. package/dist/adapt/herdr/__tests__/bridge.test.js.map +1 -0
  23. package/dist/adapt/herdr/__tests__/sanitize.test.d.ts +2 -0
  24. package/dist/adapt/herdr/__tests__/sanitize.test.d.ts.map +1 -0
  25. package/dist/adapt/herdr/__tests__/sanitize.test.js +45 -0
  26. package/dist/adapt/herdr/__tests__/sanitize.test.js.map +1 -0
  27. package/dist/adapt/herdr/__tests__/semantic.test.d.ts +2 -0
  28. package/dist/adapt/herdr/__tests__/semantic.test.d.ts.map +1 -0
  29. package/dist/adapt/herdr/__tests__/semantic.test.js +130 -0
  30. package/dist/adapt/herdr/__tests__/semantic.test.js.map +1 -0
  31. package/dist/adapt/herdr/__tests__/seq-store.test.d.ts +2 -0
  32. package/dist/adapt/herdr/__tests__/seq-store.test.d.ts.map +1 -0
  33. package/dist/adapt/herdr/__tests__/seq-store.test.js +54 -0
  34. package/dist/adapt/herdr/__tests__/seq-store.test.js.map +1 -0
  35. package/dist/adapt/herdr/__tests__/socket-live.test.d.ts +2 -0
  36. package/dist/adapt/herdr/__tests__/socket-live.test.d.ts.map +1 -0
  37. package/dist/adapt/herdr/__tests__/socket-live.test.js +104 -0
  38. package/dist/adapt/herdr/__tests__/socket-live.test.js.map +1 -0
  39. package/dist/adapt/herdr/__tests__/team-map.test.d.ts +2 -0
  40. package/dist/adapt/herdr/__tests__/team-map.test.d.ts.map +1 -0
  41. package/dist/adapt/herdr/__tests__/team-map.test.js +46 -0
  42. package/dist/adapt/herdr/__tests__/team-map.test.js.map +1 -0
  43. package/dist/adapt/herdr/__tests__/transport.test.d.ts +2 -0
  44. package/dist/adapt/herdr/__tests__/transport.test.d.ts.map +1 -0
  45. package/dist/adapt/herdr/__tests__/transport.test.js +160 -0
  46. package/dist/adapt/herdr/__tests__/transport.test.js.map +1 -0
  47. package/dist/adapt/herdr/__tests__/wiring.test.d.ts +2 -0
  48. package/dist/adapt/herdr/__tests__/wiring.test.d.ts.map +1 -0
  49. package/dist/adapt/herdr/__tests__/wiring.test.js +118 -0
  50. package/dist/adapt/herdr/__tests__/wiring.test.js.map +1 -0
  51. package/dist/adapt/herdr/authority.d.ts +34 -0
  52. package/dist/adapt/herdr/authority.d.ts.map +1 -0
  53. package/dist/adapt/herdr/authority.js +71 -0
  54. package/dist/adapt/herdr/authority.js.map +1 -0
  55. package/dist/adapt/herdr/bridge.d.ts +84 -0
  56. package/dist/adapt/herdr/bridge.d.ts.map +1 -0
  57. package/dist/adapt/herdr/bridge.js +222 -0
  58. package/dist/adapt/herdr/bridge.js.map +1 -0
  59. package/dist/adapt/herdr/sanitize.d.ts +20 -0
  60. package/dist/adapt/herdr/sanitize.d.ts.map +1 -0
  61. package/dist/adapt/herdr/sanitize.js +70 -0
  62. package/dist/adapt/herdr/sanitize.js.map +1 -0
  63. package/dist/adapt/herdr/semantic.d.ts +67 -0
  64. package/dist/adapt/herdr/semantic.d.ts.map +1 -0
  65. package/dist/adapt/herdr/semantic.js +170 -0
  66. package/dist/adapt/herdr/semantic.js.map +1 -0
  67. package/dist/adapt/herdr/seq-store.d.ts +32 -0
  68. package/dist/adapt/herdr/seq-store.d.ts.map +1 -0
  69. package/dist/adapt/herdr/seq-store.js +126 -0
  70. package/dist/adapt/herdr/seq-store.js.map +1 -0
  71. package/dist/adapt/herdr/team-map.d.ts +30 -0
  72. package/dist/adapt/herdr/team-map.d.ts.map +1 -0
  73. package/dist/adapt/herdr/team-map.js +37 -0
  74. package/dist/adapt/herdr/team-map.js.map +1 -0
  75. package/dist/adapt/herdr/transport.d.ts +115 -0
  76. package/dist/adapt/herdr/transport.d.ts.map +1 -0
  77. package/dist/adapt/herdr/transport.js +260 -0
  78. package/dist/adapt/herdr/transport.js.map +1 -0
  79. package/dist/adapt/herdr/wiring.d.ts +24 -0
  80. package/dist/adapt/herdr/wiring.d.ts.map +1 -0
  81. package/dist/adapt/herdr/wiring.js +62 -0
  82. package/dist/adapt/herdr/wiring.js.map +1 -0
  83. package/dist/adapt/herdr.d.ts +19 -0
  84. package/dist/adapt/herdr.d.ts.map +1 -0
  85. package/dist/adapt/herdr.js +115 -0
  86. package/dist/adapt/herdr.js.map +1 -0
  87. package/dist/adapt/index.d.ts +4 -0
  88. package/dist/adapt/index.d.ts.map +1 -1
  89. package/dist/adapt/index.js +48 -0
  90. package/dist/adapt/index.js.map +1 -1
  91. package/dist/adapt/registry.d.ts.map +1 -1
  92. package/dist/adapt/registry.js +11 -0
  93. package/dist/adapt/registry.js.map +1 -1
  94. package/dist/agents/__tests__/definitions.test.js +0 -27
  95. package/dist/agents/__tests__/definitions.test.js.map +1 -1
  96. package/dist/agents/__tests__/native-config.test.js +3 -8
  97. package/dist/agents/__tests__/native-config.test.js.map +1 -1
  98. package/dist/agents/definitions.d.ts.map +1 -1
  99. package/dist/agents/definitions.js +0 -41
  100. package/dist/agents/definitions.js.map +1 -1
  101. package/dist/agents/policy.d.ts.map +1 -1
  102. package/dist/agents/policy.js +0 -1
  103. package/dist/agents/policy.js.map +1 -1
  104. package/dist/auth/__tests__/hotswap.test.js +60 -1
  105. package/dist/auth/__tests__/hotswap.test.js.map +1 -1
  106. package/dist/auth/__tests__/storage.test.js +33 -1
  107. package/dist/auth/__tests__/storage.test.js.map +1 -1
  108. package/dist/auth/hotswap.d.ts +16 -2
  109. package/dist/auth/hotswap.d.ts.map +1 -1
  110. package/dist/auth/hotswap.js +22 -7
  111. package/dist/auth/hotswap.js.map +1 -1
  112. package/dist/auth/storage.js +1 -1
  113. package/dist/auth/storage.js.map +1 -1
  114. package/dist/autopilot/__tests__/completion-gate-advisory.test.d.ts +2 -0
  115. package/dist/autopilot/__tests__/completion-gate-advisory.test.d.ts.map +1 -0
  116. package/dist/autopilot/__tests__/completion-gate-advisory.test.js +383 -0
  117. package/dist/autopilot/__tests__/completion-gate-advisory.test.js.map +1 -0
  118. package/dist/autopilot/completion-gate.d.ts +6 -1
  119. package/dist/autopilot/completion-gate.d.ts.map +1 -1
  120. package/dist/autopilot/completion-gate.js +247 -7
  121. package/dist/autopilot/completion-gate.js.map +1 -1
  122. package/dist/catalog/__tests__/generator.test.js +15 -10
  123. package/dist/catalog/__tests__/generator.test.js.map +1 -1
  124. package/dist/catalog/__tests__/plugin-bundle-ssot.test.js +13 -7
  125. package/dist/catalog/__tests__/plugin-bundle-ssot.test.js.map +1 -1
  126. package/dist/catalog/__tests__/schema.test.js +3 -5
  127. package/dist/catalog/__tests__/schema.test.js.map +1 -1
  128. package/dist/catalog/schema.js +1 -1
  129. package/dist/catalog/schema.js.map +1 -1
  130. package/dist/cli/__tests__/agents-init.test.js +4 -23
  131. package/dist/cli/__tests__/agents-init.test.js.map +1 -1
  132. package/dist/cli/__tests__/api.test.js +61 -24
  133. package/dist/cli/__tests__/api.test.js.map +1 -1
  134. package/dist/cli/__tests__/auth.test.js +3 -1
  135. package/dist/cli/__tests__/auth.test.js.map +1 -1
  136. package/dist/cli/__tests__/autopilot.test.d.ts +2 -0
  137. package/dist/cli/__tests__/autopilot.test.d.ts.map +1 -0
  138. package/dist/cli/__tests__/autopilot.test.js +143 -0
  139. package/dist/cli/__tests__/autopilot.test.js.map +1 -0
  140. package/dist/cli/__tests__/codex-feature-probe.test.js +66 -1
  141. package/dist/cli/__tests__/codex-feature-probe.test.js.map +1 -1
  142. package/dist/cli/__tests__/codex-plugin-layout.test.js +76 -4
  143. package/dist/cli/__tests__/codex-plugin-layout.test.js.map +1 -1
  144. package/dist/cli/__tests__/detached-leader-teardown.test.d.ts +2 -0
  145. package/dist/cli/__tests__/detached-leader-teardown.test.d.ts.map +1 -0
  146. package/dist/cli/__tests__/detached-leader-teardown.test.js +473 -0
  147. package/dist/cli/__tests__/detached-leader-teardown.test.js.map +1 -0
  148. package/dist/cli/__tests__/doctor-process-identity-readiness.test.d.ts +2 -0
  149. package/dist/cli/__tests__/doctor-process-identity-readiness.test.d.ts.map +1 -0
  150. package/dist/cli/__tests__/doctor-process-identity-readiness.test.js +103 -0
  151. package/dist/cli/__tests__/doctor-process-identity-readiness.test.js.map +1 -0
  152. package/dist/cli/__tests__/doctor-state-root-binding.test.d.ts +2 -0
  153. package/dist/cli/__tests__/doctor-state-root-binding.test.d.ts.map +1 -0
  154. package/dist/cli/__tests__/doctor-state-root-binding.test.js +206 -0
  155. package/dist/cli/__tests__/doctor-state-root-binding.test.js.map +1 -0
  156. package/dist/cli/__tests__/doctor-team.test.js +85 -0
  157. package/dist/cli/__tests__/doctor-team.test.js.map +1 -1
  158. package/dist/cli/__tests__/doctor-warning-copy.test.js +38 -6
  159. package/dist/cli/__tests__/doctor-warning-copy.test.js.map +1 -1
  160. package/dist/cli/__tests__/exec.test.js +75 -0
  161. package/dist/cli/__tests__/exec.test.js.map +1 -1
  162. package/dist/cli/__tests__/index.test.js +621 -460
  163. package/dist/cli/__tests__/index.test.js.map +1 -1
  164. package/dist/cli/__tests__/launch-fallback.test.js +1125 -143
  165. package/dist/cli/__tests__/launch-fallback.test.js.map +1 -1
  166. package/dist/cli/__tests__/mcp-parity.test.js +4 -3
  167. package/dist/cli/__tests__/mcp-parity.test.js.map +1 -1
  168. package/dist/cli/__tests__/native-assets.test.js +398 -16
  169. package/dist/cli/__tests__/native-assets.test.js.map +1 -1
  170. package/dist/cli/__tests__/native-hook-claim-journal.test.js +53 -5
  171. package/dist/cli/__tests__/native-hook-claim-journal.test.js.map +1 -1
  172. package/dist/cli/__tests__/package-bin-contract.test.js +5 -8
  173. package/dist/cli/__tests__/package-bin-contract.test.js.map +1 -1
  174. package/dist/cli/__tests__/ralph-deslop-contract.test.js +4 -18
  175. package/dist/cli/__tests__/ralph-deslop-contract.test.js.map +1 -1
  176. package/dist/cli/__tests__/ralph-goal-mode-contract.test.js +4 -30
  177. package/dist/cli/__tests__/ralph-goal-mode-contract.test.js.map +1 -1
  178. package/dist/cli/__tests__/ralph-prd-deep-interview.test.js +4 -15
  179. package/dist/cli/__tests__/ralph-prd-deep-interview.test.js.map +1 -1
  180. package/dist/cli/__tests__/ralph.test.js +2 -2
  181. package/dist/cli/__tests__/ralph.test.js.map +1 -1
  182. package/dist/cli/__tests__/ralplan-bootstrap-3181.test.js +57 -57
  183. package/dist/cli/__tests__/ralplan-bootstrap-3181.test.js.map +1 -1
  184. package/dist/cli/__tests__/ralplan-child-binding-3212.test.d.ts +2 -0
  185. package/dist/cli/__tests__/ralplan-child-binding-3212.test.d.ts.map +1 -0
  186. package/dist/cli/__tests__/ralplan-child-binding-3212.test.js +56 -0
  187. package/dist/cli/__tests__/ralplan-child-binding-3212.test.js.map +1 -0
  188. package/dist/cli/__tests__/ralplan.test.js +131 -14
  189. package/dist/cli/__tests__/ralplan.test.js.map +1 -1
  190. package/dist/cli/__tests__/session-scoped-runtime.test.js +1153 -43
  191. package/dist/cli/__tests__/session-scoped-runtime.test.js.map +1 -1
  192. package/dist/cli/__tests__/session-search-help.test.js +65 -3
  193. package/dist/cli/__tests__/session-search-help.test.js.map +1 -1
  194. package/dist/cli/__tests__/setup-agents-overwrite.test.js +57 -55
  195. package/dist/cli/__tests__/setup-agents-overwrite.test.js.map +1 -1
  196. package/dist/cli/__tests__/setup-hooks-shared-ownership.test.js +34 -0
  197. package/dist/cli/__tests__/setup-hooks-shared-ownership.test.js.map +1 -1
  198. package/dist/cli/__tests__/setup-hooks-trust-e2e.test.js +1 -1
  199. package/dist/cli/__tests__/setup-hooks-trust-e2e.test.js.map +1 -1
  200. package/dist/cli/__tests__/setup-install-mode.test.js +258 -26
  201. package/dist/cli/__tests__/setup-install-mode.test.js.map +1 -1
  202. package/dist/cli/__tests__/setup-prompts-overwrite.test.js +1 -1
  203. package/dist/cli/__tests__/setup-prompts-overwrite.test.js.map +1 -1
  204. package/dist/cli/__tests__/setup-refresh.test.js +3 -6
  205. package/dist/cli/__tests__/setup-refresh.test.js.map +1 -1
  206. package/dist/cli/__tests__/setup-skills-overwrite.test.js +180 -21
  207. package/dist/cli/__tests__/setup-skills-overwrite.test.js.map +1 -1
  208. package/dist/cli/__tests__/sparkshell-cli.test.js +93 -41
  209. package/dist/cli/__tests__/sparkshell-cli.test.js.map +1 -1
  210. package/dist/cli/__tests__/team.test.js +45 -1
  211. package/dist/cli/__tests__/team.test.js.map +1 -1
  212. package/dist/cli/__tests__/ultragoal.test.js +161 -2
  213. package/dist/cli/__tests__/ultragoal.test.js.map +1 -1
  214. package/dist/cli/__tests__/uninstall.test.js +73 -28
  215. package/dist/cli/__tests__/uninstall.test.js.map +1 -1
  216. package/dist/cli/__tests__/update.test.js +749 -177
  217. package/dist/cli/__tests__/update.test.js.map +1 -1
  218. package/dist/cli/__tests__/windows-popup-loop-contract.test.js +32 -2
  219. package/dist/cli/__tests__/windows-popup-loop-contract.test.js.map +1 -1
  220. package/dist/cli/api.d.ts.map +1 -1
  221. package/dist/cli/api.js +9 -10
  222. package/dist/cli/api.js.map +1 -1
  223. package/dist/cli/autopilot.d.ts +8 -0
  224. package/dist/cli/autopilot.d.ts.map +1 -0
  225. package/dist/cli/autopilot.js +160 -0
  226. package/dist/cli/autopilot.js.map +1 -0
  227. package/dist/cli/codex-feature-probe.d.ts +16 -0
  228. package/dist/cli/codex-feature-probe.d.ts.map +1 -1
  229. package/dist/cli/codex-feature-probe.js +44 -0
  230. package/dist/cli/codex-feature-probe.js.map +1 -1
  231. package/dist/cli/codex-home.d.ts.map +1 -1
  232. package/dist/cli/codex-home.js +7 -4
  233. package/dist/cli/codex-home.js.map +1 -1
  234. package/dist/cli/doctor.d.ts +29 -0
  235. package/dist/cli/doctor.d.ts.map +1 -1
  236. package/dist/cli/doctor.js +433 -19
  237. package/dist/cli/doctor.js.map +1 -1
  238. package/dist/cli/index.d.ts +231 -12
  239. package/dist/cli/index.d.ts.map +1 -1
  240. package/dist/cli/index.js +2354 -724
  241. package/dist/cli/index.js.map +1 -1
  242. package/dist/cli/mcp-parity.d.ts.map +1 -1
  243. package/dist/cli/mcp-parity.js +7 -3
  244. package/dist/cli/mcp-parity.js.map +1 -1
  245. package/dist/cli/native-assets.d.ts +26 -1
  246. package/dist/cli/native-assets.d.ts.map +1 -1
  247. package/dist/cli/native-assets.js +448 -51
  248. package/dist/cli/native-assets.js.map +1 -1
  249. package/dist/cli/native-hook-claim-journal.d.ts +8 -4
  250. package/dist/cli/native-hook-claim-journal.d.ts.map +1 -1
  251. package/dist/cli/native-hook-claim-journal.js +30 -8
  252. package/dist/cli/native-hook-claim-journal.js.map +1 -1
  253. package/dist/cli/package-manager-ownership.d.ts +69 -0
  254. package/dist/cli/package-manager-ownership.d.ts.map +1 -0
  255. package/dist/cli/package-manager-ownership.js +338 -0
  256. package/dist/cli/package-manager-ownership.js.map +1 -0
  257. package/dist/cli/plugin-marketplace.d.ts +1 -0
  258. package/dist/cli/plugin-marketplace.d.ts.map +1 -1
  259. package/dist/cli/plugin-marketplace.js +123 -55
  260. package/dist/cli/plugin-marketplace.js.map +1 -1
  261. package/dist/cli/ralph.js +2 -2
  262. package/dist/cli/ralph.js.map +1 -1
  263. package/dist/cli/ralplan.d.ts +6 -2
  264. package/dist/cli/ralplan.d.ts.map +1 -1
  265. package/dist/cli/ralplan.js +134 -34
  266. package/dist/cli/ralplan.js.map +1 -1
  267. package/dist/cli/session-search.d.ts.map +1 -1
  268. package/dist/cli/session-search.js +38 -1
  269. package/dist/cli/session-search.js.map +1 -1
  270. package/dist/cli/setup-preferences.d.ts +15 -0
  271. package/dist/cli/setup-preferences.d.ts.map +1 -1
  272. package/dist/cli/setup-preferences.js +27 -1
  273. package/dist/cli/setup-preferences.js.map +1 -1
  274. package/dist/cli/setup.d.ts +6 -0
  275. package/dist/cli/setup.d.ts.map +1 -1
  276. package/dist/cli/setup.js +425 -215
  277. package/dist/cli/setup.js.map +1 -1
  278. package/dist/cli/sparkshell.d.ts.map +1 -1
  279. package/dist/cli/sparkshell.js +52 -9
  280. package/dist/cli/sparkshell.js.map +1 -1
  281. package/dist/cli/ultragoal.d.ts +1 -1
  282. package/dist/cli/ultragoal.d.ts.map +1 -1
  283. package/dist/cli/ultragoal.js +10 -7
  284. package/dist/cli/ultragoal.js.map +1 -1
  285. package/dist/cli/uninstall.d.ts.map +1 -1
  286. package/dist/cli/uninstall.js +27 -28
  287. package/dist/cli/uninstall.js.map +1 -1
  288. package/dist/cli/update-worker.d.ts +2 -0
  289. package/dist/cli/update-worker.d.ts.map +1 -0
  290. package/dist/cli/update-worker.js +142 -0
  291. package/dist/cli/update-worker.js.map +1 -0
  292. package/dist/cli/update.d.ts +16 -23
  293. package/dist/cli/update.d.ts.map +1 -1
  294. package/dist/cli/update.js +212 -241
  295. package/dist/cli/update.js.map +1 -1
  296. package/dist/compat/__tests__/upgrade-from-0-20.test.d.ts +2 -0
  297. package/dist/compat/__tests__/upgrade-from-0-20.test.d.ts.map +1 -0
  298. package/dist/compat/__tests__/upgrade-from-0-20.test.js +109 -0
  299. package/dist/compat/__tests__/upgrade-from-0-20.test.js.map +1 -0
  300. package/dist/compat/upgrade-from-0-20.d.ts +64 -0
  301. package/dist/compat/upgrade-from-0-20.d.ts.map +1 -0
  302. package/dist/compat/upgrade-from-0-20.js +276 -0
  303. package/dist/compat/upgrade-from-0-20.js.map +1 -0
  304. package/dist/config/__tests__/generator-idempotent.test.js +173 -29
  305. package/dist/config/__tests__/generator-idempotent.test.js.map +1 -1
  306. package/dist/config/__tests__/generator-notify.test.js +3 -1
  307. package/dist/config/__tests__/generator-notify.test.js.map +1 -1
  308. package/dist/config/generator.d.ts +25 -3
  309. package/dist/config/generator.d.ts.map +1 -1
  310. package/dist/config/generator.js +113 -35
  311. package/dist/config/generator.js.map +1 -1
  312. package/dist/goal-workflows/__tests__/codex-goal-snapshot.test.js +25 -0
  313. package/dist/goal-workflows/__tests__/codex-goal-snapshot.test.js.map +1 -1
  314. package/dist/goal-workflows/codex-goal-snapshot.d.ts +1 -1
  315. package/dist/goal-workflows/codex-goal-snapshot.d.ts.map +1 -1
  316. package/dist/goal-workflows/codex-goal-snapshot.js +2 -0
  317. package/dist/goal-workflows/codex-goal-snapshot.js.map +1 -1
  318. package/dist/hooks/__tests__/agents-overlay.test.js +3 -1
  319. package/dist/hooks/__tests__/agents-overlay.test.js.map +1 -1
  320. package/dist/hooks/__tests__/analyze-skill-contract.test.js +24 -31
  321. package/dist/hooks/__tests__/analyze-skill-contract.test.js.map +1 -1
  322. package/dist/hooks/__tests__/anti-slop-workflow.test.js +66 -135
  323. package/dist/hooks/__tests__/anti-slop-workflow.test.js.map +1 -1
  324. package/dist/hooks/__tests__/autopilot-skill-contract.test.js +47 -106
  325. package/dist/hooks/__tests__/autopilot-skill-contract.test.js.map +1 -1
  326. package/dist/hooks/__tests__/code-review-skill-contract.test.js +16 -18
  327. package/dist/hooks/__tests__/code-review-skill-contract.test.js.map +1 -1
  328. package/dist/hooks/__tests__/consensus-execution-handoff.test.d.ts +2 -14
  329. package/dist/hooks/__tests__/consensus-execution-handoff.test.d.ts.map +1 -1
  330. package/dist/hooks/__tests__/consensus-execution-handoff.test.js +56 -234
  331. package/dist/hooks/__tests__/consensus-execution-handoff.test.js.map +1 -1
  332. package/dist/hooks/__tests__/deep-interview-contract.test.js +1 -3
  333. package/dist/hooks/__tests__/deep-interview-contract.test.js.map +1 -1
  334. package/dist/hooks/__tests__/design-skill.test.js +18 -16
  335. package/dist/hooks/__tests__/design-skill.test.js.map +1 -1
  336. package/dist/hooks/__tests__/explore-sparkshell-guidance-contract.test.js +24 -20
  337. package/dist/hooks/__tests__/explore-sparkshell-guidance-contract.test.js.map +1 -1
  338. package/dist/hooks/__tests__/issue-3293-autopilot-handoff.test.d.ts +2 -0
  339. package/dist/hooks/__tests__/issue-3293-autopilot-handoff.test.d.ts.map +1 -0
  340. package/dist/hooks/__tests__/issue-3293-autopilot-handoff.test.js +164 -0
  341. package/dist/hooks/__tests__/issue-3293-autopilot-handoff.test.js.map +1 -0
  342. package/dist/hooks/__tests__/issue-3497-hook-simplification.test.d.ts +2 -0
  343. package/dist/hooks/__tests__/issue-3497-hook-simplification.test.d.ts.map +1 -0
  344. package/dist/hooks/__tests__/issue-3497-hook-simplification.test.js +169 -0
  345. package/dist/hooks/__tests__/issue-3497-hook-simplification.test.js.map +1 -0
  346. package/dist/hooks/__tests__/keyword-detector.test.js +226 -858
  347. package/dist/hooks/__tests__/keyword-detector.test.js.map +1 -1
  348. package/dist/hooks/__tests__/notify-fallback-watcher.test.js +176 -14
  349. package/dist/hooks/__tests__/notify-fallback-watcher.test.js.map +1 -1
  350. package/dist/hooks/__tests__/notify-hook-all-workers-idle.test.js +1 -0
  351. package/dist/hooks/__tests__/notify-hook-all-workers-idle.test.js.map +1 -1
  352. package/dist/hooks/__tests__/notify-hook-auto-nudge.test.js +33 -21
  353. package/dist/hooks/__tests__/notify-hook-auto-nudge.test.js.map +1 -1
  354. package/dist/hooks/__tests__/notify-hook-cross-worktree-heartbeat.test.js +27 -0
  355. package/dist/hooks/__tests__/notify-hook-cross-worktree-heartbeat.test.js.map +1 -1
  356. package/dist/hooks/__tests__/notify-hook-managed-tmux.test.js +10 -118
  357. package/dist/hooks/__tests__/notify-hook-managed-tmux.test.js.map +1 -1
  358. package/dist/hooks/__tests__/notify-hook-ralph-resume.test.js +14 -904
  359. package/dist/hooks/__tests__/notify-hook-ralph-resume.test.js.map +1 -1
  360. package/dist/hooks/__tests__/notify-hook-regression-205.test.js +7 -0
  361. package/dist/hooks/__tests__/notify-hook-regression-205.test.js.map +1 -1
  362. package/dist/hooks/__tests__/notify-hook-session-scope.test.js +5 -5
  363. package/dist/hooks/__tests__/notify-hook-session-scope.test.js.map +1 -1
  364. package/dist/hooks/__tests__/notify-hook-team-dispatch.test.js +7 -7
  365. package/dist/hooks/__tests__/notify-hook-team-dispatch.test.js.map +1 -1
  366. package/dist/hooks/__tests__/notify-hook-team-leader-nudge.test.js +11 -8
  367. package/dist/hooks/__tests__/notify-hook-team-leader-nudge.test.js.map +1 -1
  368. package/dist/hooks/__tests__/notify-hook-team-tmux-guard.test.js +7 -6
  369. package/dist/hooks/__tests__/notify-hook-team-tmux-guard.test.js.map +1 -1
  370. package/dist/hooks/__tests__/notify-hook-team-worker-fail-closed.test.js +2 -1
  371. package/dist/hooks/__tests__/notify-hook-team-worker-fail-closed.test.js.map +1 -1
  372. package/dist/hooks/__tests__/notify-hook-worker-idle.test.js +1 -0
  373. package/dist/hooks/__tests__/notify-hook-worker-idle.test.js.map +1 -1
  374. package/dist/hooks/__tests__/pre-context-gate-skills.test.js +19 -21
  375. package/dist/hooks/__tests__/pre-context-gate-skills.test.js.map +1 -1
  376. package/dist/hooks/__tests__/prompt-guidance-contract.test.js +23 -3
  377. package/dist/hooks/__tests__/prompt-guidance-contract.test.js.map +1 -1
  378. package/dist/hooks/__tests__/prompt-guidance-wave-two.test.js +23 -28
  379. package/dist/hooks/__tests__/prompt-guidance-wave-two.test.js.map +1 -1
  380. package/dist/hooks/__tests__/research-workflow-boundaries.test.js +9 -8
  381. package/dist/hooks/__tests__/research-workflow-boundaries.test.js.map +1 -1
  382. package/dist/hooks/__tests__/session.test.js +3656 -656
  383. package/dist/hooks/__tests__/session.test.js.map +1 -1
  384. package/dist/hooks/__tests__/skill-catalog-hygiene.test.js +13 -22
  385. package/dist/hooks/__tests__/skill-catalog-hygiene.test.js.map +1 -1
  386. package/dist/hooks/__tests__/skill-guidance-contract.test.js +3 -13
  387. package/dist/hooks/__tests__/skill-guidance-contract.test.js.map +1 -1
  388. package/dist/hooks/__tests__/sunset-routing-contract.test.d.ts +2 -0
  389. package/dist/hooks/__tests__/sunset-routing-contract.test.d.ts.map +1 -0
  390. package/dist/hooks/__tests__/sunset-routing-contract.test.js +172 -0
  391. package/dist/hooks/__tests__/sunset-routing-contract.test.js.map +1 -0
  392. package/dist/hooks/__tests__/team-runtime-gating-docs-contract.test.js +2 -2
  393. package/dist/hooks/__tests__/team-runtime-gating-docs-contract.test.js.map +1 -1
  394. package/dist/hooks/__tests__/tmux-hook-engine.test.js +18 -3
  395. package/dist/hooks/__tests__/tmux-hook-engine.test.js.map +1 -1
  396. package/dist/hooks/__tests__/visual-ralph-skill.test.js +20 -17
  397. package/dist/hooks/__tests__/visual-ralph-skill.test.js.map +1 -1
  398. package/dist/hooks/__tests__/visual-verdict-loop.test.js +17 -18
  399. package/dist/hooks/__tests__/visual-verdict-loop.test.js.map +1 -1
  400. package/dist/hooks/agents-overlay.js +1 -1
  401. package/dist/hooks/agents-overlay.js.map +1 -1
  402. package/dist/hooks/extensibility/__tests__/dispatcher.test.js +108 -2
  403. package/dist/hooks/extensibility/__tests__/dispatcher.test.js.map +1 -1
  404. package/dist/hooks/extensibility/__tests__/sdk.test.js +151 -1
  405. package/dist/hooks/extensibility/__tests__/sdk.test.js.map +1 -1
  406. package/dist/hooks/extensibility/dispatcher.d.ts.map +1 -1
  407. package/dist/hooks/extensibility/dispatcher.js +20 -3
  408. package/dist/hooks/extensibility/dispatcher.js.map +1 -1
  409. package/dist/hooks/extensibility/plugin-runner.js +1 -0
  410. package/dist/hooks/extensibility/plugin-runner.js.map +1 -1
  411. package/dist/hooks/extensibility/runtime.d.ts.map +1 -1
  412. package/dist/hooks/extensibility/runtime.js.map +1 -1
  413. package/dist/hooks/extensibility/sdk/paths.d.ts +1 -1
  414. package/dist/hooks/extensibility/sdk/paths.d.ts.map +1 -1
  415. package/dist/hooks/extensibility/sdk/paths.js +2 -2
  416. package/dist/hooks/extensibility/sdk/paths.js.map +1 -1
  417. package/dist/hooks/extensibility/sdk/runtime-state.d.ts +1 -1
  418. package/dist/hooks/extensibility/sdk/runtime-state.d.ts.map +1 -1
  419. package/dist/hooks/extensibility/sdk/runtime-state.js +55 -15
  420. package/dist/hooks/extensibility/sdk/runtime-state.js.map +1 -1
  421. package/dist/hooks/extensibility/sdk.d.ts +1 -0
  422. package/dist/hooks/extensibility/sdk.d.ts.map +1 -1
  423. package/dist/hooks/extensibility/sdk.js +1 -1
  424. package/dist/hooks/extensibility/sdk.js.map +1 -1
  425. package/dist/hooks/extensibility/types.d.ts +7 -0
  426. package/dist/hooks/extensibility/types.d.ts.map +1 -1
  427. package/dist/hooks/keyword-detector.d.ts +6 -47
  428. package/dist/hooks/keyword-detector.d.ts.map +1 -1
  429. package/dist/hooks/keyword-detector.js +156 -268
  430. package/dist/hooks/keyword-detector.js.map +1 -1
  431. package/dist/hooks/keyword-registry.d.ts.map +1 -1
  432. package/dist/hooks/keyword-registry.js +14 -26
  433. package/dist/hooks/keyword-registry.js.map +1 -1
  434. package/dist/hooks/native/capability-warnings.d.ts +16 -0
  435. package/dist/hooks/native/capability-warnings.d.ts.map +1 -0
  436. package/dist/hooks/native/capability-warnings.js +40 -0
  437. package/dist/hooks/native/capability-warnings.js.map +1 -0
  438. package/dist/hooks/native/index.d.ts +17 -0
  439. package/dist/hooks/native/index.d.ts.map +1 -0
  440. package/dist/hooks/native/index.js +17 -0
  441. package/dist/hooks/native/index.js.map +1 -0
  442. package/dist/hooks/native/pre-tool-use-advisory.d.ts +17 -0
  443. package/dist/hooks/native/pre-tool-use-advisory.d.ts.map +1 -0
  444. package/dist/hooks/native/pre-tool-use-advisory.js +112 -0
  445. package/dist/hooks/native/pre-tool-use-advisory.js.map +1 -0
  446. package/dist/hooks/native/types.d.ts +30 -0
  447. package/dist/hooks/native/types.d.ts.map +1 -0
  448. package/dist/hooks/native/types.js +22 -0
  449. package/dist/hooks/native/types.js.map +1 -0
  450. package/dist/hooks/prompt-guidance-contract.d.ts.map +1 -1
  451. package/dist/hooks/prompt-guidance-contract.js +46 -93
  452. package/dist/hooks/prompt-guidance-contract.js.map +1 -1
  453. package/dist/hooks/session.d.ts +197 -18
  454. package/dist/hooks/session.d.ts.map +1 -1
  455. package/dist/hooks/session.js +1939 -271
  456. package/dist/hooks/session.js.map +1 -1
  457. package/dist/hooks/sunset-stub.d.ts +17 -0
  458. package/dist/hooks/sunset-stub.d.ts.map +1 -0
  459. package/dist/hooks/sunset-stub.js +125 -0
  460. package/dist/hooks/sunset-stub.js.map +1 -0
  461. package/dist/hud/__tests__/hud-tmux-injection.test.js +6 -6
  462. package/dist/hud/__tests__/hud-tmux-injection.test.js.map +1 -1
  463. package/dist/hud/__tests__/index.test.js +26 -16
  464. package/dist/hud/__tests__/index.test.js.map +1 -1
  465. package/dist/hud/__tests__/reconcile.test.js +83 -78
  466. package/dist/hud/__tests__/reconcile.test.js.map +1 -1
  467. package/dist/hud/__tests__/state.test.js +36 -31
  468. package/dist/hud/__tests__/state.test.js.map +1 -1
  469. package/dist/hud/__tests__/tmux-split-realtmux.test.d.ts +2 -0
  470. package/dist/hud/__tests__/tmux-split-realtmux.test.d.ts.map +1 -0
  471. package/dist/hud/__tests__/tmux-split-realtmux.test.js +112 -0
  472. package/dist/hud/__tests__/tmux-split-realtmux.test.js.map +1 -0
  473. package/dist/hud/__tests__/tmux.test.js +217 -198
  474. package/dist/hud/__tests__/tmux.test.js.map +1 -1
  475. package/dist/hud/tmux.d.ts +43 -4
  476. package/dist/hud/tmux.d.ts.map +1 -1
  477. package/dist/hud/tmux.js +1025 -139
  478. package/dist/hud/tmux.js.map +1 -1
  479. package/dist/leader/__tests__/contract.test.js +71 -34
  480. package/dist/leader/__tests__/contract.test.js.map +1 -1
  481. package/dist/leader/contract.d.ts +1 -6
  482. package/dist/leader/contract.d.ts.map +1 -1
  483. package/dist/leader/contract.js +25 -24
  484. package/dist/leader/contract.js.map +1 -1
  485. package/dist/mcp/__tests__/hermes-bridge.test.js +17 -0
  486. package/dist/mcp/__tests__/hermes-bridge.test.js.map +1 -1
  487. package/dist/mcp/__tests__/server-lifecycle.test.js +4 -4
  488. package/dist/mcp/__tests__/state-paths.test.js +735 -4
  489. package/dist/mcp/__tests__/state-paths.test.js.map +1 -1
  490. package/dist/mcp/__tests__/state-server-ralph-phase.test.js +29 -4
  491. package/dist/mcp/__tests__/state-server-ralph-phase.test.js.map +1 -1
  492. package/dist/mcp/__tests__/state-server-schema.test.js +13 -3
  493. package/dist/mcp/__tests__/state-server-schema.test.js.map +1 -1
  494. package/dist/mcp/__tests__/state-server.test.js +11 -1033
  495. package/dist/mcp/__tests__/state-server.test.js.map +1 -1
  496. package/dist/mcp/__tests__/trace-server.test.js +2 -2
  497. package/dist/mcp/__tests__/trace-server.test.js.map +1 -1
  498. package/dist/mcp/hermes-bridge.d.ts.map +1 -1
  499. package/dist/mcp/hermes-bridge.js +2 -2
  500. package/dist/mcp/hermes-bridge.js.map +1 -1
  501. package/dist/mcp/state-paths.d.ts +78 -4
  502. package/dist/mcp/state-paths.d.ts.map +1 -1
  503. package/dist/mcp/state-paths.js +543 -40
  504. package/dist/mcp/state-paths.js.map +1 -1
  505. package/dist/mcp/state-server.d.ts +68 -97
  506. package/dist/mcp/state-server.d.ts.map +1 -1
  507. package/dist/mcp/state-server.js +116 -67
  508. package/dist/mcp/state-server.js.map +1 -1
  509. package/dist/modes/__tests__/base-autopilot-gates.test.js +1 -168
  510. package/dist/modes/__tests__/base-autopilot-gates.test.js.map +1 -1
  511. package/dist/modes/__tests__/base-autoresearch-contract.test.js +1 -85
  512. package/dist/modes/__tests__/base-autoresearch-contract.test.js.map +1 -1
  513. package/dist/modes/__tests__/base-multi-state-compat.test.js +1 -35
  514. package/dist/modes/__tests__/base-multi-state-compat.test.js.map +1 -1
  515. package/dist/modes/__tests__/base-session-scope.test.js +10 -158
  516. package/dist/modes/__tests__/base-session-scope.test.js.map +1 -1
  517. package/dist/modes/base.d.ts +6 -6
  518. package/dist/modes/base.d.ts.map +1 -1
  519. package/dist/modes/base.js +116 -90
  520. package/dist/modes/base.js.map +1 -1
  521. package/dist/native-assets/archive.d.ts +17 -0
  522. package/dist/native-assets/archive.d.ts.map +1 -0
  523. package/dist/native-assets/archive.js +238 -0
  524. package/dist/native-assets/archive.js.map +1 -0
  525. package/dist/native-assets/policy.d.ts +42 -0
  526. package/dist/native-assets/policy.d.ts.map +1 -0
  527. package/dist/native-assets/policy.js +155 -0
  528. package/dist/native-assets/policy.js.map +1 -0
  529. package/dist/notifications/__tests__/formatter.test.js +8 -0
  530. package/dist/notifications/__tests__/formatter.test.js.map +1 -1
  531. package/dist/notifications/__tests__/template-engine.test.js +8 -0
  532. package/dist/notifications/__tests__/template-engine.test.js.map +1 -1
  533. package/dist/notifications/formatter.js +1 -1
  534. package/dist/notifications/formatter.js.map +1 -1
  535. package/dist/notifications/template-engine.js +1 -1
  536. package/dist/notifications/template-engine.js.map +1 -1
  537. package/dist/planning/__tests__/approved-execution-lifecycle-matrix.test.js +0 -21
  538. package/dist/planning/__tests__/approved-execution-lifecycle-matrix.test.js.map +1 -1
  539. package/dist/question/autopilot-wait.d.ts.map +1 -1
  540. package/dist/question/autopilot-wait.js +2 -2
  541. package/dist/question/autopilot-wait.js.map +1 -1
  542. package/dist/question/deep-interview.d.ts +1 -2
  543. package/dist/question/deep-interview.d.ts.map +1 -1
  544. package/dist/question/deep-interview.js +4 -4
  545. package/dist/question/deep-interview.js.map +1 -1
  546. package/dist/ralplan/__tests__/documented-leader-preflight.test.js +328 -39
  547. package/dist/ralplan/__tests__/documented-leader-preflight.test.js.map +1 -1
  548. package/dist/ralplan/__tests__/runtime.test.js +56 -821
  549. package/dist/ralplan/__tests__/runtime.test.js.map +1 -1
  550. package/dist/ralplan/documented-leader-preflight.d.ts +15 -8
  551. package/dist/ralplan/documented-leader-preflight.d.ts.map +1 -1
  552. package/dist/ralplan/documented-leader-preflight.js +356 -54
  553. package/dist/ralplan/documented-leader-preflight.js.map +1 -1
  554. package/dist/ralplan/runtime.d.ts +2 -2
  555. package/dist/ralplan/runtime.d.ts.map +1 -1
  556. package/dist/ralplan/runtime.js +145 -163
  557. package/dist/ralplan/runtime.js.map +1 -1
  558. package/dist/runtime/bridge.d.ts.map +1 -1
  559. package/dist/runtime/bridge.js +45 -0
  560. package/dist/runtime/bridge.js.map +1 -1
  561. package/dist/runtime/run-state.d.ts +5 -1
  562. package/dist/runtime/run-state.d.ts.map +1 -1
  563. package/dist/runtime/run-state.js +15 -8
  564. package/dist/runtime/run-state.js.map +1 -1
  565. package/dist/scripts/__tests__/codex-native-hook.test.js +12490 -19414
  566. package/dist/scripts/__tests__/codex-native-hook.test.js.map +1 -1
  567. package/dist/scripts/__tests__/docs-site-contract.test.js +2 -2
  568. package/dist/scripts/__tests__/docs-site-contract.test.js.map +1 -1
  569. package/dist/scripts/__tests__/generated-artifact-drift.test.d.ts +2 -0
  570. package/dist/scripts/__tests__/generated-artifact-drift.test.d.ts.map +1 -0
  571. package/dist/scripts/__tests__/generated-artifact-drift.test.js +84 -0
  572. package/dist/scripts/__tests__/generated-artifact-drift.test.js.map +1 -0
  573. package/dist/scripts/__tests__/issue-3293-callsite-parity.test.d.ts +2 -0
  574. package/dist/scripts/__tests__/issue-3293-callsite-parity.test.d.ts.map +1 -0
  575. package/dist/scripts/__tests__/issue-3293-callsite-parity.test.js +235 -0
  576. package/dist/scripts/__tests__/issue-3293-callsite-parity.test.js.map +1 -0
  577. package/dist/scripts/__tests__/issue-3293-cancel-transaction.test.d.ts +2 -0
  578. package/dist/scripts/__tests__/issue-3293-cancel-transaction.test.d.ts.map +1 -0
  579. package/dist/scripts/__tests__/issue-3293-cancel-transaction.test.js +188 -0
  580. package/dist/scripts/__tests__/issue-3293-cancel-transaction.test.js.map +1 -0
  581. package/dist/scripts/__tests__/issue-3293-hook-owned-cancel.test.d.ts +2 -0
  582. package/dist/scripts/__tests__/issue-3293-hook-owned-cancel.test.d.ts.map +1 -0
  583. package/dist/scripts/__tests__/issue-3293-hook-owned-cancel.test.js +207 -0
  584. package/dist/scripts/__tests__/issue-3293-hook-owned-cancel.test.js.map +1 -0
  585. package/dist/scripts/__tests__/issue-3311-ultragoal-native-outside-tmux.test.d.ts +2 -0
  586. package/dist/scripts/__tests__/issue-3311-ultragoal-native-outside-tmux.test.d.ts.map +1 -0
  587. package/dist/scripts/__tests__/issue-3311-ultragoal-native-outside-tmux.test.js +156 -0
  588. package/dist/scripts/__tests__/issue-3311-ultragoal-native-outside-tmux.test.js.map +1 -0
  589. package/dist/scripts/__tests__/issue-3358-ultragoal-cancel-lifecycle.test.d.ts +2 -0
  590. package/dist/scripts/__tests__/issue-3358-ultragoal-cancel-lifecycle.test.d.ts.map +1 -0
  591. package/dist/scripts/__tests__/issue-3358-ultragoal-cancel-lifecycle.test.js +191 -0
  592. package/dist/scripts/__tests__/issue-3358-ultragoal-cancel-lifecycle.test.js.map +1 -0
  593. package/dist/scripts/__tests__/issue-3369-autopilot-ralplan-state-machine.test.d.ts +2 -0
  594. package/dist/scripts/__tests__/issue-3369-autopilot-ralplan-state-machine.test.d.ts.map +1 -0
  595. package/dist/scripts/__tests__/issue-3369-autopilot-ralplan-state-machine.test.js +40 -0
  596. package/dist/scripts/__tests__/issue-3369-autopilot-ralplan-state-machine.test.js.map +1 -0
  597. package/dist/scripts/__tests__/issue-3481-conductor-deadlock.test.d.ts +2 -0
  598. package/dist/scripts/__tests__/issue-3481-conductor-deadlock.test.d.ts.map +1 -0
  599. package/dist/scripts/__tests__/issue-3481-conductor-deadlock.test.js +63 -0
  600. package/dist/scripts/__tests__/issue-3481-conductor-deadlock.test.js.map +1 -0
  601. package/dist/scripts/__tests__/issue-3486-pretooluse-lock-regression.test.d.ts +2 -0
  602. package/dist/scripts/__tests__/issue-3486-pretooluse-lock-regression.test.d.ts.map +1 -0
  603. package/dist/scripts/__tests__/issue-3486-pretooluse-lock-regression.test.js +65 -0
  604. package/dist/scripts/__tests__/issue-3486-pretooluse-lock-regression.test.js.map +1 -0
  605. package/dist/scripts/__tests__/issue-3536-external-team-state-root.test.d.ts +2 -0
  606. package/dist/scripts/__tests__/issue-3536-external-team-state-root.test.d.ts.map +1 -0
  607. package/dist/scripts/__tests__/issue-3536-external-team-state-root.test.js +189 -0
  608. package/dist/scripts/__tests__/issue-3536-external-team-state-root.test.js.map +1 -0
  609. package/dist/scripts/__tests__/notify-state-io.test.js +26 -0
  610. package/dist/scripts/__tests__/notify-state-io.test.js.map +1 -1
  611. package/dist/scripts/__tests__/notify-tmux-injection.test.js +23 -1
  612. package/dist/scripts/__tests__/notify-tmux-injection.test.js.map +1 -1
  613. package/dist/scripts/__tests__/postinstall.test.js +19 -1
  614. package/dist/scripts/__tests__/postinstall.test.js.map +1 -1
  615. package/dist/scripts/__tests__/prompt-inventory.test.js +54 -1
  616. package/dist/scripts/__tests__/prompt-inventory.test.js.map +1 -1
  617. package/dist/scripts/__tests__/reasoning-artifact-contract.test.js +2 -2
  618. package/dist/scripts/__tests__/reasoning-artifact-contract.test.js.map +1 -1
  619. package/dist/scripts/__tests__/role-intent-bootstrap-e2e-3181.test.js +46 -60
  620. package/dist/scripts/__tests__/role-intent-bootstrap-e2e-3181.test.js.map +1 -1
  621. package/dist/scripts/__tests__/role-intent-durable-recovery-3181.test.js +60 -25
  622. package/dist/scripts/__tests__/role-intent-durable-recovery-3181.test.js.map +1 -1
  623. package/dist/scripts/__tests__/smoke-packed-install.test.js +168 -20
  624. package/dist/scripts/__tests__/smoke-packed-install.test.js.map +1 -1
  625. package/dist/scripts/__tests__/verify-native-release-assets.test.d.ts +2 -0
  626. package/dist/scripts/__tests__/verify-native-release-assets.test.d.ts.map +1 -0
  627. package/dist/scripts/__tests__/verify-native-release-assets.test.js +235 -0
  628. package/dist/scripts/__tests__/verify-native-release-assets.test.js.map +1 -0
  629. package/dist/scripts/codex-native-hook.d.ts +51 -2
  630. package/dist/scripts/codex-native-hook.d.ts.map +1 -1
  631. package/dist/scripts/codex-native-hook.js +3648 -1332
  632. package/dist/scripts/codex-native-hook.js.map +1 -1
  633. package/dist/scripts/codex-native-pre-post.d.ts +9 -0
  634. package/dist/scripts/codex-native-pre-post.d.ts.map +1 -1
  635. package/dist/scripts/codex-native-pre-post.js +33 -22
  636. package/dist/scripts/codex-native-pre-post.js.map +1 -1
  637. package/dist/scripts/generate-native-release-manifest.js +80 -51
  638. package/dist/scripts/generate-native-release-manifest.js.map +1 -1
  639. package/dist/scripts/notify-fallback-watcher.js +28 -39
  640. package/dist/scripts/notify-fallback-watcher.js.map +1 -1
  641. package/dist/scripts/notify-hook/auto-nudge.d.ts +2 -48
  642. package/dist/scripts/notify-hook/auto-nudge.d.ts.map +1 -1
  643. package/dist/scripts/notify-hook/auto-nudge.js +1 -0
  644. package/dist/scripts/notify-hook/auto-nudge.js.map +1 -1
  645. package/dist/scripts/notify-hook/state-io.d.ts.map +1 -1
  646. package/dist/scripts/notify-hook/state-io.js +5 -0
  647. package/dist/scripts/notify-hook/state-io.js.map +1 -1
  648. package/dist/scripts/notify-hook/team-dispatch.d.ts.map +1 -1
  649. package/dist/scripts/notify-hook/team-dispatch.js +8 -5
  650. package/dist/scripts/notify-hook/team-dispatch.js.map +1 -1
  651. package/dist/scripts/notify-hook/team-leader-nudge.d.ts.map +1 -1
  652. package/dist/scripts/notify-hook/team-leader-nudge.js +32 -3
  653. package/dist/scripts/notify-hook/team-leader-nudge.js.map +1 -1
  654. package/dist/scripts/notify-hook/team-tmux-guard.d.ts +1 -1
  655. package/dist/scripts/notify-hook/team-tmux-guard.d.ts.map +1 -1
  656. package/dist/scripts/notify-hook/team-tmux-guard.js +3 -2
  657. package/dist/scripts/notify-hook/team-tmux-guard.js.map +1 -1
  658. package/dist/scripts/notify-hook/team-worker-stop.d.ts.map +1 -1
  659. package/dist/scripts/notify-hook/team-worker-stop.js +23 -3
  660. package/dist/scripts/notify-hook/team-worker-stop.js.map +1 -1
  661. package/dist/scripts/notify-hook/team-worker.d.ts.map +1 -1
  662. package/dist/scripts/notify-hook/team-worker.js +45 -5
  663. package/dist/scripts/notify-hook/team-worker.js.map +1 -1
  664. package/dist/scripts/notify-hook/tmux-injection.d.ts +1 -0
  665. package/dist/scripts/notify-hook/tmux-injection.d.ts.map +1 -1
  666. package/dist/scripts/notify-hook/tmux-injection.js +4 -1
  667. package/dist/scripts/notify-hook/tmux-injection.js.map +1 -1
  668. package/dist/scripts/notify-hook.d.ts.map +1 -1
  669. package/dist/scripts/notify-hook.js +71 -124
  670. package/dist/scripts/notify-hook.js.map +1 -1
  671. package/dist/scripts/postinstall-advisory.d.ts +14 -0
  672. package/dist/scripts/postinstall-advisory.d.ts.map +1 -0
  673. package/dist/scripts/postinstall-advisory.js +49 -0
  674. package/dist/scripts/postinstall-advisory.js.map +1 -0
  675. package/dist/scripts/postinstall.d.ts +1 -1
  676. package/dist/scripts/postinstall.d.ts.map +1 -1
  677. package/dist/scripts/postinstall.js +1 -1
  678. package/dist/scripts/postinstall.js.map +1 -1
  679. package/dist/scripts/prompt-inventory.d.ts +39 -0
  680. package/dist/scripts/prompt-inventory.d.ts.map +1 -1
  681. package/dist/scripts/prompt-inventory.js +108 -3
  682. package/dist/scripts/prompt-inventory.js.map +1 -1
  683. package/dist/scripts/run-compiled-ci.js +2 -0
  684. package/dist/scripts/run-compiled-ci.js.map +1 -1
  685. package/dist/scripts/run-test-files.js +1 -0
  686. package/dist/scripts/run-test-files.js.map +1 -1
  687. package/dist/scripts/smoke-packed-install.d.ts +32 -1
  688. package/dist/scripts/smoke-packed-install.d.ts.map +1 -1
  689. package/dist/scripts/smoke-packed-install.js +407 -60
  690. package/dist/scripts/smoke-packed-install.js.map +1 -1
  691. package/dist/scripts/sync-plugin-mirror.d.ts.map +1 -1
  692. package/dist/scripts/sync-plugin-mirror.js +16 -1
  693. package/dist/scripts/sync-plugin-mirror.js.map +1 -1
  694. package/dist/scripts/sync-prompt-guidance-fragments.js +62 -20
  695. package/dist/scripts/sync-prompt-guidance-fragments.js.map +1 -1
  696. package/dist/scripts/tmux-hook-engine.d.ts +1 -1
  697. package/dist/scripts/tmux-hook-engine.d.ts.map +1 -1
  698. package/dist/scripts/tmux-hook-engine.js +3 -3
  699. package/dist/scripts/tmux-hook-engine.js.map +1 -1
  700. package/dist/scripts/verify-capabilities-lock.d.ts +3 -0
  701. package/dist/scripts/verify-capabilities-lock.d.ts.map +1 -0
  702. package/dist/scripts/verify-capabilities-lock.js +22 -0
  703. package/dist/scripts/verify-capabilities-lock.js.map +1 -0
  704. package/dist/scripts/verify-native-release-assets.js +37 -23
  705. package/dist/scripts/verify-native-release-assets.js.map +1 -1
  706. package/dist/state/__tests__/authority-decreasing.test.d.ts +2 -0
  707. package/dist/state/__tests__/authority-decreasing.test.d.ts.map +1 -0
  708. package/dist/state/__tests__/authority-decreasing.test.js +78 -0
  709. package/dist/state/__tests__/authority-decreasing.test.js.map +1 -0
  710. package/dist/state/__tests__/handoff-carrier.test.d.ts +2 -0
  711. package/dist/state/__tests__/handoff-carrier.test.d.ts.map +1 -0
  712. package/dist/state/__tests__/handoff-carrier.test.js +96 -0
  713. package/dist/state/__tests__/handoff-carrier.test.js.map +1 -0
  714. package/dist/state/__tests__/mode-state-context.test.js +12 -5
  715. package/dist/state/__tests__/mode-state-context.test.js.map +1 -1
  716. package/dist/state/__tests__/operations.test.js +870 -2517
  717. package/dist/state/__tests__/operations.test.js.map +1 -1
  718. package/dist/state/__tests__/single-writer-invariant.test.d.ts +2 -0
  719. package/dist/state/__tests__/single-writer-invariant.test.d.ts.map +1 -0
  720. package/dist/state/__tests__/single-writer-invariant.test.js +342 -0
  721. package/dist/state/__tests__/single-writer-invariant.test.js.map +1 -0
  722. package/dist/state/__tests__/skill-active.test.js +400 -2
  723. package/dist/state/__tests__/skill-active.test.js.map +1 -1
  724. package/dist/state/__tests__/stale-state-neutralization.test.d.ts +2 -0
  725. package/dist/state/__tests__/stale-state-neutralization.test.d.ts.map +1 -0
  726. package/dist/state/__tests__/stale-state-neutralization.test.js +84 -0
  727. package/dist/state/__tests__/stale-state-neutralization.test.js.map +1 -0
  728. package/dist/state/__tests__/workflow-transition.test.js +20 -33
  729. package/dist/state/__tests__/workflow-transition.test.js.map +1 -1
  730. package/dist/state/handoff-carrier.d.ts +46 -0
  731. package/dist/state/handoff-carrier.d.ts.map +1 -0
  732. package/dist/state/handoff-carrier.js +85 -0
  733. package/dist/state/handoff-carrier.js.map +1 -0
  734. package/dist/state/mode-state-context.d.ts +3 -0
  735. package/dist/state/mode-state-context.d.ts.map +1 -1
  736. package/dist/state/mode-state-context.js +27 -13
  737. package/dist/state/mode-state-context.js.map +1 -1
  738. package/dist/state/namespace-owners.d.ts +34 -0
  739. package/dist/state/namespace-owners.d.ts.map +1 -0
  740. package/dist/state/namespace-owners.js +127 -0
  741. package/dist/state/namespace-owners.js.map +1 -0
  742. package/dist/state/operations.d.ts +9 -4
  743. package/dist/state/operations.d.ts.map +1 -1
  744. package/dist/state/operations.js +194 -235
  745. package/dist/state/operations.js.map +1 -1
  746. package/dist/state/skill-active.d.ts +18 -1
  747. package/dist/state/skill-active.d.ts.map +1 -1
  748. package/dist/state/skill-active.js +421 -31
  749. package/dist/state/skill-active.js.map +1 -1
  750. package/dist/state/workflow-transition-reconcile.d.ts +3 -0
  751. package/dist/state/workflow-transition-reconcile.d.ts.map +1 -1
  752. package/dist/state/workflow-transition-reconcile.js +11 -31
  753. package/dist/state/workflow-transition-reconcile.js.map +1 -1
  754. package/dist/state/workflow-transition.d.ts +1 -27
  755. package/dist/state/workflow-transition.d.ts.map +1 -1
  756. package/dist/state/workflow-transition.js +9 -790
  757. package/dist/state/workflow-transition.js.map +1 -1
  758. package/dist/subagents/__tests__/leader-bootstrap-3181.test.js +27 -0
  759. package/dist/subagents/__tests__/leader-bootstrap-3181.test.js.map +1 -1
  760. package/dist/subagents/__tests__/tracker.test.js +837 -1048
  761. package/dist/subagents/__tests__/tracker.test.js.map +1 -1
  762. package/dist/subagents/tracker.d.ts +64 -60
  763. package/dist/subagents/tracker.d.ts.map +1 -1
  764. package/dist/subagents/tracker.js +809 -358
  765. package/dist/subagents/tracker.js.map +1 -1
  766. package/dist/team/__tests__/api-interop.test.js +1 -0
  767. package/dist/team/__tests__/api-interop.test.js.map +1 -1
  768. package/dist/team/__tests__/delivery-e2e-smoke.test.js +6 -7
  769. package/dist/team/__tests__/delivery-e2e-smoke.test.js.map +1 -1
  770. package/dist/team/__tests__/mcp-comm.test.js +104 -3
  771. package/dist/team/__tests__/mcp-comm.test.js.map +1 -1
  772. package/dist/team/__tests__/notice-ledger.test.d.ts +2 -0
  773. package/dist/team/__tests__/notice-ledger.test.d.ts.map +1 -0
  774. package/dist/team/__tests__/notice-ledger.test.js +131 -0
  775. package/dist/team/__tests__/notice-ledger.test.js.map +1 -0
  776. package/dist/team/__tests__/pane-teardown-residue-e2e.test.d.ts +2 -0
  777. package/dist/team/__tests__/pane-teardown-residue-e2e.test.d.ts.map +1 -0
  778. package/dist/team/__tests__/pane-teardown-residue-e2e.test.js +410 -0
  779. package/dist/team/__tests__/pane-teardown-residue-e2e.test.js.map +1 -0
  780. package/dist/team/__tests__/phase-controller.test.js +20 -0
  781. package/dist/team/__tests__/phase-controller.test.js.map +1 -1
  782. package/dist/team/__tests__/runtime.test.js +725 -59
  783. package/dist/team/__tests__/runtime.test.js.map +1 -1
  784. package/dist/team/__tests__/scaling.test.js +269 -245
  785. package/dist/team/__tests__/scaling.test.js.map +1 -1
  786. package/dist/team/__tests__/state-root.test.js +117 -2
  787. package/dist/team/__tests__/state-root.test.js.map +1 -1
  788. package/dist/team/__tests__/team-ops-contract.test.js +1 -0
  789. package/dist/team/__tests__/team-ops-contract.test.js.map +1 -1
  790. package/dist/team/__tests__/tmux-session.test.js +1093 -101
  791. package/dist/team/__tests__/tmux-session.test.js.map +1 -1
  792. package/dist/team/__tests__/tmux-source-authority-realtmux.test.d.ts +2 -0
  793. package/dist/team/__tests__/tmux-source-authority-realtmux.test.d.ts.map +1 -0
  794. package/dist/team/__tests__/tmux-source-authority-realtmux.test.js +159 -0
  795. package/dist/team/__tests__/tmux-source-authority-realtmux.test.js.map +1 -0
  796. package/dist/team/__tests__/tmux-test-fixture.d.ts +69 -0
  797. package/dist/team/__tests__/tmux-test-fixture.d.ts.map +1 -1
  798. package/dist/team/__tests__/tmux-test-fixture.js +242 -15
  799. package/dist/team/__tests__/tmux-test-fixture.js.map +1 -1
  800. package/dist/team/__tests__/tmux-test-fixture.test.js +177 -1
  801. package/dist/team/__tests__/tmux-test-fixture.test.js.map +1 -1
  802. package/dist/team/__tests__/worker-bootstrap.test.js +11 -13
  803. package/dist/team/__tests__/worker-bootstrap.test.js.map +1 -1
  804. package/dist/team/__tests__/worker-provenance.test.d.ts +2 -0
  805. package/dist/team/__tests__/worker-provenance.test.d.ts.map +1 -0
  806. package/dist/team/__tests__/worker-provenance.test.js +251 -0
  807. package/dist/team/__tests__/worker-provenance.test.js.map +1 -0
  808. package/dist/team/__tests__/worker-runtime-identity.test.js +45 -72
  809. package/dist/team/__tests__/worker-runtime-identity.test.js.map +1 -1
  810. package/dist/team/api-interop.d.ts.map +1 -1
  811. package/dist/team/api-interop.js +14 -8
  812. package/dist/team/api-interop.js.map +1 -1
  813. package/dist/team/mcp-comm.d.ts.map +1 -1
  814. package/dist/team/mcp-comm.js +3 -8
  815. package/dist/team/mcp-comm.js.map +1 -1
  816. package/dist/team/notice-ledger.d.ts +54 -0
  817. package/dist/team/notice-ledger.d.ts.map +1 -0
  818. package/dist/team/notice-ledger.js +259 -0
  819. package/dist/team/notice-ledger.js.map +1 -0
  820. package/dist/team/phase-controller.d.ts.map +1 -1
  821. package/dist/team/phase-controller.js +2 -0
  822. package/dist/team/phase-controller.js.map +1 -1
  823. package/dist/team/runtime.d.ts +3 -1
  824. package/dist/team/runtime.d.ts.map +1 -1
  825. package/dist/team/runtime.js +314 -142
  826. package/dist/team/runtime.js.map +1 -1
  827. package/dist/team/scaling.d.ts.map +1 -1
  828. package/dist/team/scaling.js +161 -91
  829. package/dist/team/scaling.js.map +1 -1
  830. package/dist/team/state/monitor.d.ts +10 -0
  831. package/dist/team/state/monitor.d.ts.map +1 -1
  832. package/dist/team/state/monitor.js +13 -0
  833. package/dist/team/state/monitor.js.map +1 -1
  834. package/dist/team/state/types.d.ts +15 -0
  835. package/dist/team/state/types.d.ts.map +1 -1
  836. package/dist/team/state/types.js.map +1 -1
  837. package/dist/team/state-root.d.ts +12 -0
  838. package/dist/team/state-root.d.ts.map +1 -1
  839. package/dist/team/state-root.js +177 -0
  840. package/dist/team/state-root.js.map +1 -1
  841. package/dist/team/state.d.ts +19 -0
  842. package/dist/team/state.d.ts.map +1 -1
  843. package/dist/team/state.js +47 -1
  844. package/dist/team/state.js.map +1 -1
  845. package/dist/team/team-ops.d.ts +1 -0
  846. package/dist/team/team-ops.d.ts.map +1 -1
  847. package/dist/team/team-ops.js +1 -0
  848. package/dist/team/team-ops.js.map +1 -1
  849. package/dist/team/tmux-session.d.ts +42 -4
  850. package/dist/team/tmux-session.d.ts.map +1 -1
  851. package/dist/team/tmux-session.js +572 -196
  852. package/dist/team/tmux-session.js.map +1 -1
  853. package/dist/team/ultragoal-context.d.ts +1 -0
  854. package/dist/team/ultragoal-context.d.ts.map +1 -1
  855. package/dist/team/ultragoal-context.js +2 -0
  856. package/dist/team/ultragoal-context.js.map +1 -1
  857. package/dist/team/worker-provenance.d.ts +35 -0
  858. package/dist/team/worker-provenance.d.ts.map +1 -0
  859. package/dist/team/worker-provenance.js +213 -0
  860. package/dist/team/worker-provenance.js.map +1 -0
  861. package/dist/ultragoal/__tests__/artifacts.test.js +1053 -2
  862. package/dist/ultragoal/__tests__/artifacts.test.js.map +1 -1
  863. package/dist/ultragoal/__tests__/docs-contract.test.js +89 -59
  864. package/dist/ultragoal/__tests__/docs-contract.test.js.map +1 -1
  865. package/dist/ultragoal/artifacts.d.ts +36 -1
  866. package/dist/ultragoal/artifacts.d.ts.map +1 -1
  867. package/dist/ultragoal/artifacts.js +267 -51
  868. package/dist/ultragoal/artifacts.js.map +1 -1
  869. package/dist/url-reader/__tests__/url-reader.test.js +14 -0
  870. package/dist/url-reader/__tests__/url-reader.test.js.map +1 -1
  871. package/dist/url-reader/index.js +5 -3
  872. package/dist/url-reader/index.js.map +1 -1
  873. package/dist/utils/__tests__/file-durability.test.js +58 -1
  874. package/dist/utils/__tests__/file-durability.test.js.map +1 -1
  875. package/dist/utils/__tests__/paths.test.js +21 -1
  876. package/dist/utils/__tests__/paths.test.js.map +1 -1
  877. package/dist/utils/__tests__/version.test.js +17 -0
  878. package/dist/utils/__tests__/version.test.js.map +1 -1
  879. package/dist/utils/file-durability.d.ts +10 -0
  880. package/dist/utils/file-durability.d.ts.map +1 -1
  881. package/dist/utils/file-durability.js +34 -4
  882. package/dist/utils/file-durability.js.map +1 -1
  883. package/dist/utils/paths.d.ts.map +1 -1
  884. package/dist/utils/paths.js +14 -6
  885. package/dist/utils/paths.js.map +1 -1
  886. package/dist/utils/version.d.ts.map +1 -1
  887. package/dist/utils/version.js +2 -1
  888. package/dist/utils/version.js.map +1 -1
  889. package/dist/verification/__tests__/ci-rust-gates.test.js +49 -4
  890. package/dist/verification/__tests__/ci-rust-gates.test.js.map +1 -1
  891. package/dist/verification/__tests__/explore-harness-release-workflow.test.js +60 -0
  892. package/dist/verification/__tests__/explore-harness-release-workflow.test.js.map +1 -1
  893. package/dist/verification/__tests__/native-release-manifest.test.js +157 -4
  894. package/dist/verification/__tests__/native-release-manifest.test.js.map +1 -1
  895. package/package.json +17 -6
  896. package/plugins/oh-my-codex/.codex-plugin/plugin.json +1 -1
  897. package/plugins/oh-my-codex/hooks/codex-native-hook.mjs +81 -17
  898. package/plugins/oh-my-codex/skills/ai-slop-cleaner/SKILL.md +60 -135
  899. package/plugins/oh-my-codex/skills/analyze/SKILL.md +35 -111
  900. package/plugins/oh-my-codex/skills/autopilot/SKILL.md +72 -163
  901. package/plugins/oh-my-codex/skills/cancel/SKILL.md +33 -392
  902. package/plugins/oh-my-codex/skills/code-review/SKILL.md +66 -256
  903. package/plugins/oh-my-codex/skills/configure-notifications/SKILL.md +30 -216
  904. package/plugins/oh-my-codex/skills/deep-interview/SKILL.md +11 -10
  905. package/plugins/oh-my-codex/skills/design/SKILL.md +32 -132
  906. package/plugins/oh-my-codex/skills/doctor/SKILL.md +56 -177
  907. package/plugins/oh-my-codex/skills/omx-setup/SKILL.md +35 -116
  908. package/plugins/oh-my-codex/skills/plan/SKILL.md +45 -228
  909. package/plugins/oh-my-codex/skills/ralplan/SKILL.md +20 -22
  910. package/plugins/oh-my-codex/skills/skill/SKILL.md +82 -807
  911. package/plugins/oh-my-codex/skills/team/SKILL.md +32 -520
  912. package/plugins/oh-my-codex/skills/ultragoal/SKILL.md +47 -150
  913. package/plugins/oh-my-codex/skills/ultraqa/SKILL.md +75 -252
  914. package/plugins/oh-my-codex/skills/visual-ralph/SKILL.md +33 -111
  915. package/plugins/oh-my-codex/skills/worker/SKILL.md +40 -113
  916. package/prompts/analyst.md +27 -107
  917. package/prompts/api-reviewer.md +17 -84
  918. package/prompts/architect.md +31 -70
  919. package/prompts/code-reviewer.md +18 -107
  920. package/prompts/code-simplifier.md +32 -110
  921. package/prompts/critic.md +32 -54
  922. package/prompts/dependency-expert.md +37 -107
  923. package/prompts/designer.md +40 -107
  924. package/prompts/executor.md +26 -75
  925. package/prompts/information-architect.md +51 -200
  926. package/prompts/performance-reviewer.md +17 -81
  927. package/prompts/planner.md +33 -76
  928. package/prompts/product-analyst.md +65 -280
  929. package/prompts/product-manager.md +48 -216
  930. package/prompts/qa-tester.md +33 -104
  931. package/prompts/quality-reviewer.md +17 -93
  932. package/prompts/quality-strategist.md +31 -255
  933. package/prompts/researcher.md +32 -89
  934. package/prompts/security-reviewer.md +12 -98
  935. package/prompts/test-engineer.md +29 -110
  936. package/prompts/ux-researcher.md +46 -300
  937. package/prompts/verifier.md +23 -51
  938. package/skills/ai-slop-cleaner/SKILL.md +60 -135
  939. package/skills/analyze/SKILL.md +35 -111
  940. package/skills/autopilot/SKILL.md +72 -163
  941. package/skills/autoresearch-goal/SKILL.md +6 -31
  942. package/skills/cancel/SKILL.md +33 -392
  943. package/skills/code-review/SKILL.md +66 -256
  944. package/skills/configure-notifications/SKILL.md +30 -216
  945. package/skills/deep-interview/SKILL.md +11 -10
  946. package/skills/design/SKILL.md +32 -132
  947. package/skills/doctor/SKILL.md +56 -177
  948. package/skills/omx-setup/SKILL.md +35 -116
  949. package/skills/pipeline/SKILL.md +6 -89
  950. package/skills/plan/SKILL.md +45 -228
  951. package/skills/ralph/SKILL.md +6 -290
  952. package/skills/ralplan/SKILL.md +20 -22
  953. package/skills/skill/SKILL.md +82 -807
  954. package/skills/team/SKILL.md +32 -520
  955. package/skills/ultragoal/SKILL.md +47 -150
  956. package/skills/ultraqa/SKILL.md +75 -252
  957. package/skills/ultrawork/SKILL.md +6 -182
  958. package/skills/visual-ralph/SKILL.md +33 -111
  959. package/skills/worker/SKILL.md +40 -113
  960. package/src/scripts/__tests__/codex-native-hook.test.ts +18888 -28799
  961. package/src/scripts/__tests__/docs-site-contract.test.ts +2 -2
  962. package/src/scripts/__tests__/generated-artifact-drift.test.ts +97 -0
  963. package/src/scripts/__tests__/issue-3293-callsite-parity.test.ts +197 -0
  964. package/src/scripts/__tests__/issue-3293-cancel-transaction.test.ts +189 -0
  965. package/src/scripts/__tests__/issue-3293-hook-owned-cancel.test.ts +186 -0
  966. package/src/scripts/__tests__/issue-3311-ultragoal-native-outside-tmux.test.ts +177 -0
  967. package/src/scripts/__tests__/issue-3358-ultragoal-cancel-lifecycle.test.ts +201 -0
  968. package/src/scripts/__tests__/issue-3369-autopilot-ralplan-state-machine.test.ts +43 -0
  969. package/src/scripts/__tests__/issue-3481-conductor-deadlock.test.ts +77 -0
  970. package/src/scripts/__tests__/issue-3486-pretooluse-lock-regression.test.ts +95 -0
  971. package/src/scripts/__tests__/issue-3536-external-team-state-root.test.ts +216 -0
  972. package/src/scripts/__tests__/notify-state-io.test.ts +29 -0
  973. package/src/scripts/__tests__/notify-tmux-injection.test.ts +26 -1
  974. package/src/scripts/__tests__/postinstall.test.ts +23 -1
  975. package/src/scripts/__tests__/prompt-inventory.test.ts +62 -1
  976. package/src/scripts/__tests__/reasoning-artifact-contract.test.ts +1 -2
  977. package/src/scripts/__tests__/role-intent-bootstrap-e2e-3181.test.ts +49 -59
  978. package/src/scripts/__tests__/role-intent-durable-recovery-3181.test.ts +61 -27
  979. package/src/scripts/__tests__/smoke-packed-install.test.ts +223 -23
  980. package/src/scripts/__tests__/verify-native-release-assets.test.ts +240 -0
  981. package/src/scripts/codex-native-hook.ts +3618 -1383
  982. package/src/scripts/codex-native-pre-post.ts +50 -24
  983. package/src/scripts/generate-native-release-manifest.ts +83 -79
  984. package/src/scripts/notify-fallback-watcher.ts +38 -38
  985. package/src/scripts/notify-hook/auto-nudge.ts +1 -0
  986. package/src/scripts/notify-hook/state-io.ts +11 -0
  987. package/src/scripts/notify-hook/team-dispatch.ts +8 -5
  988. package/src/scripts/notify-hook/team-leader-nudge.ts +32 -4
  989. package/src/scripts/notify-hook/team-tmux-guard.ts +3 -1
  990. package/src/scripts/notify-hook/team-worker-stop.ts +23 -4
  991. package/src/scripts/notify-hook/team-worker.ts +43 -5
  992. package/src/scripts/notify-hook/tmux-injection.ts +5 -1
  993. package/src/scripts/notify-hook.ts +76 -124
  994. package/src/scripts/postinstall-advisory.ts +67 -0
  995. package/src/scripts/postinstall.ts +1 -1
  996. package/src/scripts/prompt-inventory.ts +142 -3
  997. package/src/scripts/run-compiled-ci.ts +2 -0
  998. package/src/scripts/run-test-files.ts +1 -0
  999. package/src/scripts/smoke-packed-install.ts +440 -59
  1000. package/src/scripts/sync-plugin-mirror.ts +17 -1
  1001. package/src/scripts/sync-prompt-guidance-fragments.ts +59 -22
  1002. package/src/scripts/tmux-hook-engine.ts +3 -3
  1003. package/src/scripts/verify-capabilities-lock.ts +23 -0
  1004. package/src/scripts/verify-native-release-assets.ts +39 -31
  1005. package/templates/AGENTS.md +45 -9
  1006. package/templates/catalog-manifest.json +6 -146
  1007. package/dist/autopilot/__tests__/deep-interview-gate.test.d.ts +0 -2
  1008. package/dist/autopilot/__tests__/deep-interview-gate.test.d.ts.map +0 -1
  1009. package/dist/autopilot/__tests__/deep-interview-gate.test.js +0 -215
  1010. package/dist/autopilot/__tests__/deep-interview-gate.test.js.map +0 -1
  1011. package/dist/autopilot/__tests__/ralplan-gate.test.d.ts +0 -2
  1012. package/dist/autopilot/__tests__/ralplan-gate.test.d.ts.map +0 -1
  1013. package/dist/autopilot/__tests__/ralplan-gate.test.js +0 -1361
  1014. package/dist/autopilot/__tests__/ralplan-gate.test.js.map +0 -1
  1015. package/dist/autopilot/deep-interview-gate.d.ts +0 -18
  1016. package/dist/autopilot/deep-interview-gate.d.ts.map +0 -1
  1017. package/dist/autopilot/deep-interview-gate.js +0 -396
  1018. package/dist/autopilot/deep-interview-gate.js.map +0 -1
  1019. package/dist/autopilot/ralplan-gate.d.ts +0 -18
  1020. package/dist/autopilot/ralplan-gate.d.ts.map +0 -1
  1021. package/dist/autopilot/ralplan-gate.js +0 -148
  1022. package/dist/autopilot/ralplan-gate.js.map +0 -1
  1023. package/dist/hooks/__tests__/prometheus-strict-contract.test.d.ts +0 -2
  1024. package/dist/hooks/__tests__/prometheus-strict-contract.test.d.ts.map +0 -1
  1025. package/dist/hooks/__tests__/prometheus-strict-contract.test.js +0 -320
  1026. package/dist/hooks/__tests__/prometheus-strict-contract.test.js.map +0 -1
  1027. package/dist/pipeline/__tests__/orchestrator.test.d.ts +0 -2
  1028. package/dist/pipeline/__tests__/orchestrator.test.d.ts.map +0 -1
  1029. package/dist/pipeline/__tests__/orchestrator.test.js +0 -770
  1030. package/dist/pipeline/__tests__/orchestrator.test.js.map +0 -1
  1031. package/dist/pipeline/__tests__/stages.test.d.ts +0 -2
  1032. package/dist/pipeline/__tests__/stages.test.d.ts.map +0 -1
  1033. package/dist/pipeline/__tests__/stages.test.js +0 -1778
  1034. package/dist/pipeline/__tests__/stages.test.js.map +0 -1
  1035. package/dist/pipeline/index.d.ts +0 -25
  1036. package/dist/pipeline/index.d.ts.map +0 -1
  1037. package/dist/pipeline/index.js +0 -17
  1038. package/dist/pipeline/index.js.map +0 -1
  1039. package/dist/pipeline/orchestrator.d.ts +0 -52
  1040. package/dist/pipeline/orchestrator.d.ts.map +0 -1
  1041. package/dist/pipeline/orchestrator.js +0 -399
  1042. package/dist/pipeline/orchestrator.js.map +0 -1
  1043. package/dist/pipeline/review-verdict.d.ts +0 -3
  1044. package/dist/pipeline/review-verdict.d.ts.map +0 -1
  1045. package/dist/pipeline/review-verdict.js +0 -14
  1046. package/dist/pipeline/review-verdict.js.map +0 -1
  1047. package/dist/pipeline/stages/code-review.d.ts +0 -35
  1048. package/dist/pipeline/stages/code-review.d.ts.map +0 -1
  1049. package/dist/pipeline/stages/code-review.js +0 -55
  1050. package/dist/pipeline/stages/code-review.js.map +0 -1
  1051. package/dist/pipeline/stages/deep-interview.d.ts +0 -15
  1052. package/dist/pipeline/stages/deep-interview.d.ts.map +0 -1
  1053. package/dist/pipeline/stages/deep-interview.js +0 -32
  1054. package/dist/pipeline/stages/deep-interview.js.map +0 -1
  1055. package/dist/pipeline/stages/ralph-verify.d.ts +0 -53
  1056. package/dist/pipeline/stages/ralph-verify.d.ts.map +0 -1
  1057. package/dist/pipeline/stages/ralph-verify.js +0 -89
  1058. package/dist/pipeline/stages/ralph-verify.js.map +0 -1
  1059. package/dist/pipeline/stages/ralplan.d.ts +0 -27
  1060. package/dist/pipeline/stages/ralplan.d.ts.map +0 -1
  1061. package/dist/pipeline/stages/ralplan.js +0 -152
  1062. package/dist/pipeline/stages/ralplan.js.map +0 -1
  1063. package/dist/pipeline/stages/team-exec.d.ts +0 -52
  1064. package/dist/pipeline/stages/team-exec.d.ts.map +0 -1
  1065. package/dist/pipeline/stages/team-exec.js +0 -266
  1066. package/dist/pipeline/stages/team-exec.js.map +0 -1
  1067. package/dist/pipeline/stages/ultragoal.d.ts +0 -23
  1068. package/dist/pipeline/stages/ultragoal.d.ts.map +0 -1
  1069. package/dist/pipeline/stages/ultragoal.js +0 -53
  1070. package/dist/pipeline/stages/ultragoal.js.map +0 -1
  1071. package/dist/pipeline/stages/ultraqa.d.ts +0 -33
  1072. package/dist/pipeline/stages/ultraqa.d.ts.map +0 -1
  1073. package/dist/pipeline/stages/ultraqa.js +0 -49
  1074. package/dist/pipeline/stages/ultraqa.js.map +0 -1
  1075. package/dist/pipeline/types.d.ts +0 -124
  1076. package/dist/pipeline/types.d.ts.map +0 -1
  1077. package/dist/pipeline/types.js +0 -8
  1078. package/dist/pipeline/types.js.map +0 -1
  1079. package/dist/ralplan/__tests__/consensus-gate.test.d.ts +0 -2
  1080. package/dist/ralplan/__tests__/consensus-gate.test.d.ts.map +0 -1
  1081. package/dist/ralplan/__tests__/consensus-gate.test.js +0 -1455
  1082. package/dist/ralplan/__tests__/consensus-gate.test.js.map +0 -1
  1083. package/dist/ralplan/consensus-gate.d.ts +0 -62
  1084. package/dist/ralplan/consensus-gate.d.ts.map +0 -1
  1085. package/dist/ralplan/consensus-gate.js +0 -760
  1086. package/dist/ralplan/consensus-gate.js.map +0 -1
  1087. package/dist/scripts/notify-hook/ralph-session-resume.d.ts +0 -25
  1088. package/dist/scripts/notify-hook/ralph-session-resume.d.ts.map +0 -1
  1089. package/dist/scripts/notify-hook/ralph-session-resume.js +0 -377
  1090. package/dist/scripts/notify-hook/ralph-session-resume.js.map +0 -1
  1091. package/dist/state/__tests__/planning-gate.test.d.ts +0 -2
  1092. package/dist/state/__tests__/planning-gate.test.d.ts.map +0 -1
  1093. package/dist/state/__tests__/planning-gate.test.js +0 -629
  1094. package/dist/state/__tests__/planning-gate.test.js.map +0 -1
  1095. package/plugins/oh-my-codex/skills/autoresearch-goal/SKILL.md +0 -39
  1096. package/plugins/oh-my-codex/skills/pipeline/SKILL.md +0 -97
  1097. package/plugins/oh-my-codex/skills/prometheus-strict/README.md +0 -35
  1098. package/plugins/oh-my-codex/skills/prometheus-strict/SKILL.md +0 -219
  1099. package/plugins/oh-my-codex/skills/ralph/SKILL.md +0 -298
  1100. package/plugins/oh-my-codex/skills/ultrawork/SKILL.md +0 -190
  1101. package/plugins/oh-my-codex/skills/ultrawork/references/agent-tiers.md +0 -62
  1102. package/prompts/prometheus-strict-metis.md +0 -274
  1103. package/prompts/prometheus-strict-momus.md +0 -82
  1104. package/prompts/prometheus-strict-oracle.md +0 -107
  1105. package/prompts/scholastic.md +0 -11
  1106. package/prompts/sisyphus-lite.md +0 -111
  1107. package/skills/ask-claude/SKILL.md +0 -12
  1108. package/skills/ask-gemini/SKILL.md +0 -12
  1109. package/skills/build-fix/SKILL.md +0 -10
  1110. package/skills/deepsearch/SKILL.md +0 -10
  1111. package/skills/ecomode/SKILL.md +0 -114
  1112. package/skills/ecomode/references/agent-tiers.md +0 -61
  1113. package/skills/frontend-ui-ux/SKILL.md +0 -16
  1114. package/skills/help/SKILL.md +0 -10
  1115. package/skills/note/SKILL.md +0 -10
  1116. package/skills/prometheus-strict/README.md +0 -35
  1117. package/skills/prometheus-strict/SKILL.md +0 -219
  1118. package/skills/ralph-init/SKILL.md +0 -10
  1119. package/skills/review/SKILL.md +0 -10
  1120. package/skills/security-review/SKILL.md +0 -10
  1121. package/skills/swarm/SKILL.md +0 -12
  1122. package/skills/tdd/SKILL.md +0 -104
  1123. package/skills/trace/SKILL.md +0 -10
  1124. package/skills/visual-verdict/SKILL.md +0 -10
  1125. package/skills/web-clone/SKILL.md +0 -357
  1126. package/src/scripts/notify-hook/ralph-session-resume.ts +0 -466
package/dist/cli/index.js CHANGED
@@ -3,9 +3,9 @@
3
3
  * Multi-agent orchestration for OpenAI Codex CLI
4
4
  */
5
5
  import { execFileSync, spawn } from "child_process";
6
- import { basename, dirname, join, posix, resolve, win32 } from "path";
7
- import { chmodSync, existsSync, lstatSync, mkdirSync, readFileSync, realpathSync, rmSync, statSync, writeFileSync } from "fs";
8
- import { copyFile, cp, lstat, mkdir, readFile, readdir, rm, stat, symlink, utimes, writeFile } from "fs/promises";
6
+ import { basename, dirname, isAbsolute, join, posix, relative, resolve, win32 } from "path";
7
+ import { appendFileSync, chmodSync, closeSync, constants as fsConstants, existsSync, fsyncSync, lstatSync, mkdirSync, openSync, readFileSync, realpathSync, renameSync, rmSync, statSync, unlinkSync, writeFileSync } from "fs";
8
+ import { copyFile, cp, lstat, mkdir, open, readFile, readdir, rm, stat, symlink, utimes, writeFile } from "fs/promises";
9
9
  import { constants as osConstants, homedir } from "os";
10
10
  import { createHash, randomUUID } from "crypto";
11
11
  import { setup, SETUP_MCP_MODES, SETUP_SCOPES, SETUP_TEAM_MODES, } from "./setup.js";
@@ -18,6 +18,7 @@ import { sidecarCommand } from "../sidecar/index.js";
18
18
  import { teamCommand } from "./team.js";
19
19
  import { ralphCommand } from "./ralph.js";
20
20
  import { ralplanCommand } from "./ralplan.js";
21
+ import { autopilotCommand } from "./autopilot.js";
21
22
  import { ultragoalCommand } from "./ultragoal.js";
22
23
  import { performanceGoalCommand } from "./performance-goal.js";
23
24
  import { askCommand } from "./ask.js";
@@ -41,7 +42,7 @@ import { authCommand } from "./auth.js";
41
42
  import { missionCommand } from "./mission.js";
42
43
  import { resolveCodexGlobalOptionValue, runAuthHotswap, } from "../auth/hotswap.js";
43
44
  import { MADMAX_FLAG, CODEX_BYPASS_FLAG, HIGH_REASONING_FLAG, XHIGH_REASONING_FLAG, SPARK_FLAG, MADMAX_SPARK_FLAG, CONFIG_FLAG, LONG_CONFIG_FLAG, } from "./constants.js";
44
- import { getBaseStateDir, getBaseStateDirWithSource, getStateDir, listModeStateFilesWithScopePreference, resolveWritableStateScope, } from "../mcp/state-paths.js";
45
+ import { getBaseStateDir, getBaseStateDirWithSource, getStateDir, isModeStateFilename, listModeStateFilesWithScopePreference, resolveWritableStateScope, } from "../mcp/state-paths.js";
45
46
  import { evaluateRalphCompletionAuditEvidence, isRalphCompletePhase } from "../ralph/completion-audit.js";
46
47
  import { normalizeTerminalWorkflowState } from "../state/terminal-normalization.js";
47
48
  import { readPersistedSetupPreferences, resolveCodexConfigPathForLaunch, resolveCodexHomeForLaunch, resolveProjectLocalCodexHomeForLaunch, } from "./codex-home.js";
@@ -50,14 +51,14 @@ import { discoverOmxPluginCacheDirs, hasLocalOmxPluginEnablement, materializePac
50
51
  import { escapeTomlString, readTopLevelTomlString, upsertTopLevelTomlString } from "../utils/toml.js";
51
52
  import { ROOT_REASONING_EFFORTS, isUnsupportedRootReasoningEffort, normalizeUnsupportedRootReasoningEffort, } from "../config/models.js";
52
53
  export { readPersistedSetupPreferences, readPersistedSetupScope, resolveCodexConfigPathForLaunch, resolveCodexHomeForLaunch, resolveProjectLocalCodexHomeForLaunch, } from "./codex-home.js";
53
- import { SKILL_ACTIVE_STATE_MODE, extractSessionIdFromInitializedStatePath, getSkillActiveStatePathsForStateDir, listActiveSkills, readSkillActiveState, syncCanonicalSkillStateForMode, } from "../state/skill-active.js";
54
+ import { SKILL_ACTIVE_STATE_MODE, extractSessionIdFromInitializedStatePath, listActiveSkills, syncCanonicalSkillStateForMode, updateRootSkillActiveStateForStateDir, } from "../state/skill-active.js";
54
55
  import { isTrackedWorkflowMode } from "../state/workflow-transition.js";
55
56
  import { maybeCheckAndPromptUpdate, runImmediateUpdate } from "./update.js";
56
57
  import { maybePromptGithubStar } from "./star-prompt.js";
57
58
  import { generateOverlay, removeSessionModelInstructionsFile, resolveSessionOrchestrationMode, sessionModelInstructionsPath, writeSessionModelInstructionsFile, } from "../hooks/agents-overlay.js";
58
- import { isSessionPointerLaunchAbort, normalizeSessionId, readSessionState, writeSessionStart, writeSessionEnd, resetSessionMetrics, } from "../hooks/session.js";
59
+ import { closeLaunchSessionBindingOnce, establishLaunchSessionBinding, readNativeSessionOwnerEvidence, readSessionPointer, readSessionState, resolveSessionPointerContext, finalizeBoundOnce, isSessionPointerLaunchAbort, normalizeSessionId, resetSessionMetrics, } from "../hooks/session.js";
59
60
  import { probeActualTmuxInstanceEvidence, tmuxEvidenceBindsCandidate } from "../scripts/notify-hook/managed-tmux.js";
60
- import { buildClientAttachedReconcileHookName, buildReconcileHudResizeArgs, buildRegisterClientAttachedReconcileArgs, buildRegisterResizeHookArgs, buildResizeHookName, buildResizeHookTarget, buildScheduleDelayedHudResizeArgs, buildUnregisterClientAttachedReconcileArgs, buildUnregisterResizeHookArgs, enableMouseScrolling, isMsysOrGitBash, isNativeWindows, isTmuxAvailable, mitigateCopyModeUnderlineArtifacts, } from "../team/tmux-session.js";
61
+ import { buildClientAttachedReconcileHookName, buildReconcileHudResizeArgs, buildRegisterClientAttachedReconcileArgs, buildRegisterResizeHookArgs, buildResizeHookName, buildResizeHookTarget, buildScheduleDelayedHudResizeArgs, buildUnregisterClientAttachedReconcileArgs, buildUnregisterResizeHookArgs, enableMouseScrolling, isMsysOrGitBash, isNativeWindows, isTmuxAvailable, } from "../team/tmux-session.js";
61
62
  import { getPackageRoot } from "../utils/package.js";
62
63
  import { codexConfigPath, omxRoot, rememberOmxLaunchContext, resolveOmxCliEntryPath } from "../utils/paths.js";
63
64
  import { cleanCodexModelAvailabilityNuxIfNeeded, extractSharedMcpRegistryServersFromConfig, repairConfigIfNeeded, repairProjectScopeTrustStateForLaunch, syncProjectScopeTrustStateFromRuntime } from "../config/generator.js";
@@ -73,6 +74,7 @@ import { buildHookEvent } from "../hooks/extensibility/events.js";
73
74
  import { dispatchHookEvent } from "../hooks/extensibility/dispatcher.js";
74
75
  import { collectInheritableTeamWorkerArgs as collectInheritableTeamWorkerArgsShared, parseTeamWorkerLaunchArgs, resolveTeamWorkerLaunchArgs, resolveTeamLowComplexityDefaultModel, serializeTeamWorkerLaunchArgs, TEAM_WORKER_INHERITED_MODEL_ENV, } from "../team/model-contract.js";
75
76
  import { parseWorktreeMode, planWorktreeTarget, ensureWorktree, } from "../team/worktree.js";
77
+ import { resolveVerifiedDetachedTeamContext } from "../team/state-root.js";
76
78
  import { ensureReusableNodeModules } from "../utils/repo-deps.js";
77
79
  import { resolveWorktreeToolContext, worktreeToolContextEnv } from "../utils/worktree-tool-context.js";
78
80
  import { OMX_NOTIFY_TEMP_CONTRACT_ENV, parseNotifyTempContractFromArgs, serializeNotifyTempContract, } from "../notifications/temp-contract.js";
@@ -123,7 +125,7 @@ Usage:
123
125
  omx explore DEPRECATED compatibility command; use normal repo inspection or omx sparkshell
124
126
  omx api Run native omx-api localhost gateway commands (serve|status|stop|generate)
125
127
  omx session Search and summarize local session history (--codex-home <path> escape hatch)
126
- Includes session lock inspect/recover diagnostics
128
+ Includes session lock diagnostics and verified-dead pointer recovery
127
129
  omx url Passive URL reader (read <url> --json)
128
130
  omx capabilities
129
131
  Lock/check deterministic configured tool, skill, agent, and observation surfaces
@@ -134,7 +136,8 @@ Usage:
134
136
  Alias for agents-init (lightweight AGENTS bootstrap only)
135
137
  omx team Spawn parallel worker panes in tmux and bootstrap inbox/task state
136
138
  omx ralph Launch Codex with ralph persistence mode active
137
- omx ralplan Record validated role intents for adapted native subagent spawns
139
+ omx ralplan Adapted Ralplan authority support; preflight applies only when native role routing is unavailable
140
+ and adapted Ralplan authority is requested; ordinary work remains under its own workflow gates
138
141
  omx ultragoal Create, resume, and checkpoint durable multi-goal plans over Codex goal mode
139
142
  omx performance-goal
140
143
  Create, hand off, and gate evaluator-backed performance goals
@@ -196,6 +199,9 @@ Options:
196
199
  Clear the persisted AGENTS merge policy for this project root
197
200
  --dry-run Show what would be done without doing it
198
201
  --plugin Use Codex plugin delivery for omx setup and remove legacy OMX-managed user/project components
202
+ --disable-hooks
203
+ Disable only OMX-owned hook registrations and related enablement; preserve foreign hooks and .omx artifacts
204
+ --repair-state Archive stale state projections under .omx/archive/ during omx doctor
199
205
  --legacy Use legacy setup delivery for omx setup, overriding persisted plugin mode
200
206
  --install-mode <legacy|plugin>
201
207
  Explicit setup install mode (canonical form; --legacy/--plugin are aliases)
@@ -262,7 +268,6 @@ const ALLOWED_SHELLS = new Set([
262
268
  "/opt/local/bin/zsh",
263
269
  "/opt/homebrew/bin/zsh",
264
270
  ]);
265
- const WINDOWS_DETACHED_BOOTSTRAP_DELAY_MS = 2500;
266
271
  const CODEX_VERSION_FLAGS = new Set(["--version", "-V"]);
267
272
  const TMUX_EXTENDED_KEYS_MODE = "always";
268
273
  const TMUX_EXTENDED_KEYS_FALLBACK_MODE = "off";
@@ -270,6 +275,7 @@ const TMUX_EXTENDED_KEYS_LEASE_DIR = "tmux-extended-keys";
270
275
  const TMUX_EXTENDED_KEYS_LOCK_RETRY_MS = 20;
271
276
  const TMUX_EXTENDED_KEYS_LOCK_MAX_ATTEMPTS = 100;
272
277
  const TMUX_EXTENDED_KEYS_LOCK_STALE_MS = 30_000;
278
+ export const DETACHED_LEADER_READY_TIMEOUT_MS = 120_000;
273
279
  const RESUME_HELP = `Usage: omx resume [--project] [--codex-home <path>] [codex resume options]
274
280
 
275
281
  Read-only help does not prepare or launch a Codex session.`;
@@ -520,6 +526,9 @@ export function resolveCliInvocation(args) {
520
526
  if (firstArg === "resume") {
521
527
  return { command: "resume", launchArgs: args.slice(1) };
522
528
  }
529
+ if (firstArg === "__detached-session-leader") {
530
+ return { command: firstArg, launchArgs: args.slice(1) };
531
+ }
523
532
  return { command: firstArg, launchArgs: [] };
524
533
  }
525
534
  export function resolveUpdateChannelArg(args) {
@@ -823,6 +832,10 @@ export async function prepareRuntimeCodexHomeForProjectLaunch(cwd, sessionId, pr
823
832
  await writeFile(destination, launchConfig, "utf-8");
824
833
  continue;
825
834
  }
835
+ if (entry.name === "plugins" && entry.isDirectory()) {
836
+ await cp(source, destination, { recursive: true });
837
+ continue;
838
+ }
826
839
  await linkOrCopyCodexHomeEntry(source, destination);
827
840
  }
828
841
  await ensureProjectLaunchRuntimeHistoryLinks(runtimeCodexHome, projectCodexHome);
@@ -1042,6 +1055,247 @@ function execTmuxFileSync(args, options) {
1042
1055
  ...(process.platform === "win32" ? { windowsHide: true } : {}),
1043
1056
  });
1044
1057
  }
1058
+ function captureDetachedLeaderAuthority(leaderPaneId, expectedSessionName, ownerId) {
1059
+ if (!/^%[0-9]+$/.test(leaderPaneId) || !ownerId)
1060
+ throw new Error("invalid detached leader authority target");
1061
+ const output = execTmuxFileSync([
1062
+ "display-message", "-p", "-t", leaderPaneId,
1063
+ "#{session_name}\t#{session_id}\t#{session_created}\t#{window_index}\t#{window_id}\t#{pane_id}\t#{pane_pid}",
1064
+ ], { encoding: "utf-8", stdio: ["ignore", "pipe", "ignore"] }).trim();
1065
+ const fields = output.split("\t");
1066
+ if (fields.length !== 7)
1067
+ throw new Error("malformed detached leader authority");
1068
+ const [sessionName, sessionId, sessionCreated, windowIndex, windowId, paneId, panePid] = fields;
1069
+ if (sessionName !== expectedSessionName || paneId !== leaderPaneId || !/^\$[0-9]+$/.test(sessionId)
1070
+ || !/^[0-9]+$/.test(sessionCreated) || !/^[0-9]+$/.test(windowIndex)
1071
+ || !/^@[0-9]+$/.test(windowId) || !/^[1-9][0-9]*$/.test(panePid)) {
1072
+ throw new Error("malformed detached leader authority");
1073
+ }
1074
+ const parsedPid = Number(panePid);
1075
+ if (!Number.isSafeInteger(parsedPid) || parsedPid <= 0)
1076
+ throw new Error("malformed detached leader authority");
1077
+ return { paneId, panePid: parsedPid, sessionName, sessionId, sessionCreated, windowId, windowIndex, ownerId };
1078
+ }
1079
+ /** Internal detached-launch seam. Exported solely for deterministic CLI tests. */
1080
+ export function parseDetachedLeaderPaneIdByPid(snapshot, leaderPid) {
1081
+ if (!Number.isSafeInteger(leaderPid) || leaderPid <= 0)
1082
+ throw new Error("invalid detached leader pid");
1083
+ const matches = snapshot
1084
+ .split("\n")
1085
+ .map((line) => line.trim())
1086
+ .filter(Boolean)
1087
+ .map((line) => line.split("\t"))
1088
+ .filter(([paneId, paneDead, panePid]) => /^%[0-9]+$/.test(paneId ?? "") && paneDead === "0" && Number(panePid) === leaderPid)
1089
+ .map(([paneId]) => paneId);
1090
+ if (matches.length !== 1)
1091
+ throw new Error("detached leader pane identity is unavailable");
1092
+ return matches[0];
1093
+ }
1094
+ function detachedLeaderAuthorityCondition(authority, requireOwner = true) {
1095
+ const conditions = [
1096
+ "#{==:#{pane_dead},0}",
1097
+ `#{==:#{pane_id},${authority.paneId}}`,
1098
+ `#{==:#{pane_pid},${authority.panePid}}`,
1099
+ `#{==:#{session_name},${authority.sessionName}}`,
1100
+ `#{==:#{session_id},${authority.sessionId}}`,
1101
+ `#{==:#{session_created},${authority.sessionCreated}}`,
1102
+ `#{==:#{window_id},${authority.windowId}}`,
1103
+ ...(requireOwner ? [`#{==:#{@omx_instance_id},${authority.ownerId}}`] : []),
1104
+ ];
1105
+ return conditions.reduce((combined, condition) => `#{&&:${combined},${condition}}`);
1106
+ }
1107
+ function detachedHudAuthorityCondition(authority, requireMarker = true) {
1108
+ const conditions = [
1109
+ "#{==:#{pane_dead},0}",
1110
+ `#{==:#{pane_id},${authority.paneId}}`,
1111
+ `#{==:#{pane_pid},${authority.panePid}}`,
1112
+ `#{==:#{session_id},${authority.sessionId}}`,
1113
+ `#{==:#{window_id},${authority.windowId}}`,
1114
+ ...(requireMarker ? [`#{m:*OMX_DETACHED_HUD_OPERATION=${authority.operationMarker}*,#{pane_start_command}}`] : []),
1115
+ ];
1116
+ return conditions.reduce((combined, condition) => `#{&&:${combined},${condition}}`);
1117
+ }
1118
+ function detachedAuthorityReceipt() {
1119
+ return `omx_detached_${randomUUID()}`;
1120
+ }
1121
+ function describeDetachedLeaderAuthorityMismatch(authority) {
1122
+ const output = execTmuxFileSync([
1123
+ "display-message", "-p", "-t", authority.paneId,
1124
+ "#{pane_dead}\t#{pane_id}\t#{pane_pid}\t#{session_name}\t#{session_id}\t#{session_created}\t#{window_id}\t#{@omx_instance_id}",
1125
+ ], { encoding: "utf-8", stdio: ["ignore", "pipe", "ignore"] }).trim();
1126
+ const observed = output.split("\t");
1127
+ if (observed.length !== 8)
1128
+ return "authority observation was malformed";
1129
+ const expected = ["0", authority.paneId, String(authority.panePid), authority.sessionName, authority.sessionId, authority.sessionCreated, authority.windowId, authority.ownerId];
1130
+ const labels = ["pane_dead", "pane_id", "pane_pid", "session_name", "session_id", "session_created", "window_id", "owner"];
1131
+ const mismatches = observed.flatMap((value, index) => value === expected[index] ? [] : [`${labels[index]} expected ${JSON.stringify(expected[index])} observed ${JSON.stringify(value)}`]);
1132
+ return mismatches.length > 0 ? mismatches.join(", ") : "authority condition was not observable";
1133
+ }
1134
+ function runDetachedLeaderMutation(authority, args, requireOwner = true, retryMissingReceipt = false) {
1135
+ const attempts = retryMissingReceipt ? 2 : 1;
1136
+ for (let attempt = 1; attempt <= attempts; attempt += 1) {
1137
+ const receipt = detachedAuthorityReceipt();
1138
+ const success = `${args.map(quoteShellArg).join(" ")} ; display-message -p ${quoteShellArg(receipt)}`;
1139
+ const output = execTmuxFileSync([
1140
+ "if-shell", "-F", "-t", authority.paneId, detachedLeaderAuthorityCondition(authority, requireOwner),
1141
+ success, "display-message -p ''",
1142
+ ], { encoding: "utf-8", stdio: ["ignore", "pipe", "ignore"] }).trim();
1143
+ if (output === receipt)
1144
+ return;
1145
+ }
1146
+ const mutation = args.length >= 4 && args[0] === "set-option" ? `${args[0]} ${args.at(-2)}` : args[0] ?? "unknown";
1147
+ throw new Error(`detached leader authority blocked tmux mutation ${mutation}: ${describeDetachedLeaderAuthorityMismatch(authority)}`);
1148
+ }
1149
+ function detachedPreReportCleanupCondition(authority) {
1150
+ const conditions = [
1151
+ "#{==:#{pane_dead},1}",
1152
+ `#{==:#{pane_id},${authority.paneId}}`,
1153
+ `#{==:#{session_name},${authority.sessionName}}`,
1154
+ `#{==:#{session_id},${authority.sessionId}}`,
1155
+ `#{==:#{session_created},${authority.sessionCreated}}`,
1156
+ `#{==:#{window_id},${authority.windowId}}`,
1157
+ ];
1158
+ return conditions.reduce((combined, condition) => `#{&&:${combined},${condition}}`);
1159
+ }
1160
+ export function cleanupDetachedPreReportSession(authority) {
1161
+ const receipt = detachedAuthorityReceipt();
1162
+ const success = `kill-session -t ${quoteShellArg(authority.sessionName)} ; display-message -p ${quoteShellArg(receipt)}`;
1163
+ const output = execTmuxFileSync([
1164
+ "if-shell", "-F", "-t", authority.paneId, detachedPreReportCleanupCondition(authority),
1165
+ success, "display-message -p ''",
1166
+ ], { encoding: "utf-8", stdio: ["ignore", "pipe", "ignore"] }).trim();
1167
+ if (output !== receipt)
1168
+ throw new Error("detached pre-report topology changed before cleanup");
1169
+ }
1170
+ function runDetachedHudMutation(leaderAuthority, hudAuthority, args, requireMarker = true) {
1171
+ const receipt = detachedAuthorityReceipt();
1172
+ const success = `${args.map(quoteShellArg).join(" ")} ; display-message -p ${quoteShellArg(receipt)}`;
1173
+ const hudGuard = `if-shell -F -t ${quoteShellArg(hudAuthority.paneId)} ${quoteShellArg(detachedHudAuthorityCondition(hudAuthority, requireMarker))} ${quoteShellArg(success)} ${quoteShellArg("display-message -p ''")}`;
1174
+ const output = execTmuxFileSync([
1175
+ "if-shell", "-F", "-t", leaderAuthority.paneId, detachedLeaderAuthorityCondition(leaderAuthority),
1176
+ hudGuard, "display-message -p ''",
1177
+ ], { encoding: "utf-8", stdio: ["ignore", "pipe", "ignore"] }).trim();
1178
+ if (output !== receipt)
1179
+ throw new Error("detached leader or HUD authority changed before tmux mutation");
1180
+ }
1181
+ export function cleanupDetachedHudPane(authority, ownerId) {
1182
+ if (ownerId) {
1183
+ execTmuxFileSync(["kill-pane", "-t", authority.paneId], { encoding: "utf-8", stdio: ["ignore", "pipe", "ignore"] });
1184
+ }
1185
+ else {
1186
+ execTmuxFileSync([
1187
+ "if-shell", "-F", "-t", authority.paneId, detachedHudAuthorityCondition(authority, false),
1188
+ `kill-pane -t ${quoteShellArg(authority.paneId)}`, "",
1189
+ ], { encoding: "utf-8", stdio: ["ignore", "pipe", "ignore"] });
1190
+ }
1191
+ for (let attempt = 0; attempt < 50; attempt += 1) {
1192
+ const panes = execTmuxFileSync(["list-panes", "-a", "-F", "#{pane_id}"], {
1193
+ encoding: "utf-8", stdio: ["ignore", "pipe", "ignore"],
1194
+ }).split("\n").map((value) => value.trim()).filter(Boolean);
1195
+ if (!panes.includes(authority.paneId))
1196
+ return;
1197
+ blockMs(20);
1198
+ }
1199
+ throw new Error("detached HUD topology changed before teardown");
1200
+ }
1201
+ function tagDetachedHudPane(leaderAuthority, authority, ownerId) {
1202
+ runDetachedHudMutation(leaderAuthority, authority, [
1203
+ "set-option", "-pq", "-t", authority.paneId, "@omx_hud_owner", ownerId,
1204
+ ], false);
1205
+ }
1206
+ export function discoverDetachedHudAuthority(sessionName, ownerId) {
1207
+ const rows = execTmuxFileSync([
1208
+ "list-panes", "-a", "-F",
1209
+ "#{pane_id}\t#{pane_dead}\t#{pane_pid}\t#{session_name}\t#{session_id}\t#{session_created}\t#{window_id}\t#{@omx_hud_owner}",
1210
+ ], { encoding: "utf-8", stdio: ["ignore", "pipe", "ignore"] });
1211
+ const matches = rows.split("\n").map((line) => line.trim()).filter(Boolean).flatMap((line) => {
1212
+ const [paneId, paneDead, panePidRaw, discoveredSessionName, sessionId, sessionCreated, windowId, owner] = line.split("\t");
1213
+ const panePid = Number(panePidRaw);
1214
+ if (paneDead !== "0" || discoveredSessionName !== sessionName || owner !== ownerId || !/^%\d+$/.test(paneId ?? "") || !/^\$\d+$/.test(sessionId ?? "") || !/^\d+$/.test(sessionCreated ?? "") || !/^@\d+$/.test(windowId ?? "") || !Number.isSafeInteger(panePid) || panePid <= 0)
1215
+ return [];
1216
+ return [{ paneId: paneId, panePid, sessionId: sessionId, sessionCreated: sessionCreated, windowId: windowId, operationMarker: randomUUID() }];
1217
+ });
1218
+ if (matches.length > 1)
1219
+ throw new Error("multiple detached HUD ownership tags found");
1220
+ return matches[0];
1221
+ }
1222
+ function buildDeferredDetachedHudGuard(leaderAuthority, hudAuthority, command) {
1223
+ const deferredReceipt = detachedAuthorityReceipt();
1224
+ let guardedSinkCount = 0;
1225
+ const guardedResize = (_match, sink) => {
1226
+ guardedSinkCount += 1;
1227
+ // The stored hook payload is consumed by one outer tmux format pass when
1228
+ // run-shell executes it. The injected authority conditions must survive that
1229
+ // pass so they are evaluated at execution time against the guarded panes;
1230
+ // unescaped, tmux pre-expands them against the hook's own pane and collapses
1231
+ // them to constants, denying the resize before the sink is parsed (#3292).
1232
+ const outerFormatEscaped = (condition) => condition.replaceAll("#{", "##{");
1233
+ const success = `${sink} ; display-message -p ${quoteShellArg(deferredReceipt)}`;
1234
+ const hudGuard = `if-shell -F -t ${quoteShellArg(hudAuthority.paneId)} ${quoteShellArg(outerFormatEscaped(detachedHudAuthorityCondition(hudAuthority)))} ${quoteShellArg(success)} ${quoteShellArg("")}`;
1235
+ return `tmux if-shell -F -t ${quoteShellArg(leaderAuthority.paneId)} ${quoteShellArg(outerFormatEscaped(detachedLeaderAuthorityCondition(leaderAuthority)))} ${quoteShellArg(hudGuard)} ${quoteShellArg("")}`;
1236
+ };
1237
+ const outerFormatEscapedCommand = command
1238
+ .replaceAll('#{pane_id}', '##{pane_id}')
1239
+ .replaceAll('#{pane_dead}', '##{pane_dead}')
1240
+ .replaceAll('#{pane_pid}', '##{pane_pid}');
1241
+ const guardedCommand = outerFormatEscapedCommand.replace(/tmux (resize-pane -t %[0-9]+ -y [1-9][0-9]*)/g, guardedResize);
1242
+ if (guardedSinkCount === 0) {
1243
+ throw new Error("detached deferred HUD mutation lacks a recognized resize sink");
1244
+ }
1245
+ return guardedCommand;
1246
+ }
1247
+ export function guardDetachedHudDeferredMutation(leaderAuthority, hudAuthority, args) {
1248
+ if (args[0] === "set-hook" && typeof args.at(-1) === "string") {
1249
+ const hookCommand = args.at(-1);
1250
+ const runShellPrefix = "run-shell -b ";
1251
+ if (!hookCommand.startsWith(runShellPrefix)) {
1252
+ throw new Error("detached deferred HUD hook lacks a run-shell command");
1253
+ }
1254
+ const quotedPayload = hookCommand.slice(runShellPrefix.length);
1255
+ if (!quotedPayload.startsWith("'") || !quotedPayload.endsWith("'")) {
1256
+ throw new Error("detached deferred HUD hook has an invalid run-shell payload");
1257
+ }
1258
+ const payload = quotedPayload
1259
+ .slice(1, -1)
1260
+ .replaceAll(`'"'"'`, "'")
1261
+ .replaceAll("'\\''", "'");
1262
+ const guardedPayload = buildDeferredDetachedHudGuard(leaderAuthority, hudAuthority, payload);
1263
+ return [...args.slice(0, -1), `${runShellPrefix}${quoteShellArg(guardedPayload)}`];
1264
+ }
1265
+ if (args[0] === "run-shell") {
1266
+ const commandIndex = args.length - 1;
1267
+ return [...args.slice(0, commandIndex), buildDeferredDetachedHudGuard(leaderAuthority, hudAuthority, args[commandIndex])];
1268
+ }
1269
+ return args;
1270
+ }
1271
+ function runDetachedLeaderSplit(authority, args) {
1272
+ const receipt = detachedAuthorityReceipt();
1273
+ const operationMarker = randomUUID();
1274
+ const splitArgs = [...args];
1275
+ const targetIndex = splitArgs.indexOf("-t");
1276
+ if (targetIndex < 0 || splitArgs[targetIndex + 1] !== authority.sessionName)
1277
+ throw new Error("invalid detached HUD split target");
1278
+ splitArgs[targetIndex + 1] = authority.paneId;
1279
+ const formatIndex = splitArgs.indexOf("-F");
1280
+ if (formatIndex < 0 || splitArgs[formatIndex + 1] !== "#{pane_id}")
1281
+ throw new Error("invalid detached HUD split receipt format");
1282
+ const commandIndex = splitArgs.length - 1;
1283
+ splitArgs[commandIndex] = `env OMX_DETACHED_HUD_OPERATION=${operationMarker} ${splitArgs[commandIndex]}`;
1284
+ splitArgs[formatIndex + 1] = `#{pane_id}\t#{pane_pid}\t#{session_id}\t#{window_id}\t${receipt}`;
1285
+ const output = execTmuxFileSync([
1286
+ "if-shell", "-F", "-t", authority.paneId, detachedLeaderAuthorityCondition(authority),
1287
+ splitArgs.map(quoteShellArg).join(" "), "display-message -p ''",
1288
+ ], { encoding: "utf-8", stdio: ["ignore", "pipe", "ignore"] }).trim();
1289
+ const [paneId, panePid, sessionId, windowId, observedReceipt, ...extra] = output.split("\t");
1290
+ if (!/^%[0-9]+$/.test(paneId) || !/^[1-9][0-9]*$/.test(panePid) || !/^\$[0-9]+$/.test(sessionId)
1291
+ || !/^@[0-9]+$/.test(windowId) || observedReceipt !== receipt || extra.length !== 0) {
1292
+ throw new Error("detached leader authority changed before HUD split");
1293
+ }
1294
+ const parsedPid = Number(panePid);
1295
+ if (!Number.isSafeInteger(parsedPid) || parsedPid <= 0)
1296
+ throw new Error("malformed detached HUD authority");
1297
+ return { paneId, panePid: parsedPid, sessionId, windowId, operationMarker };
1298
+ }
1045
1299
  function readTmuxEnvValueForTarget(targetPaneId) {
1046
1300
  if (!targetPaneId.startsWith("%"))
1047
1301
  return undefined;
@@ -1129,29 +1383,6 @@ function clearDetachedTmuxSessionHistoryIfUnattached(sessionName, leaderPaneId)
1129
1383
  logCliOperationFailure(err);
1130
1384
  }
1131
1385
  }
1132
- function readTmuxSessionInstanceId(sessionName) {
1133
- try {
1134
- return execTmuxFileSync(["show-options", "-qv", "-t", sessionName, OMX_INSTANCE_OPTION], {
1135
- stdio: ["ignore", "pipe", "ignore"],
1136
- encoding: "utf-8",
1137
- }).trim();
1138
- }
1139
- catch {
1140
- return null;
1141
- }
1142
- }
1143
- function tmuxPaneBelongsToSession(paneId, sessionName) {
1144
- try {
1145
- const paneSessionName = execTmuxFileSync(["display-message", "-p", "-t", paneId, "#{session_name}"], {
1146
- stdio: ["ignore", "pipe", "ignore"],
1147
- encoding: "utf-8",
1148
- }).trim();
1149
- return paneSessionName === sessionName;
1150
- }
1151
- catch {
1152
- return false;
1153
- }
1154
- }
1155
1386
  function buildDetachedHistoryPruneHookCommand(leaderPaneId) {
1156
1387
  // The leader pane can be gone by the time the hook fires (e.g. crashed
1157
1388
  // leader with a lingering session); suppress errors so tmux does not queue
@@ -1193,6 +1424,12 @@ function logCliOperationFailure(error) {
1193
1424
  process.stderr.write(`[cli/index] operation failed: ${error}
1194
1425
  `);
1195
1426
  }
1427
+ function logEstablishmentCleanupEvidence(error) {
1428
+ const cleanup = error?.establishmentCleanup;
1429
+ if (!cleanup)
1430
+ return;
1431
+ process.stderr.write(`[omx] establishment cleanup evidence: ${JSON.stringify(cleanup)}\n`);
1432
+ }
1196
1433
  function tmuxFailureMessage(error) {
1197
1434
  if (!error || typeof error !== "object")
1198
1435
  return String(error);
@@ -1207,77 +1444,18 @@ function isUnsupportedTmuxExtendedKeysFailure(error) {
1207
1444
  return (message.includes("extended-keys") &&
1208
1445
  /(?:invalid|unknown|unsupported) (?:option|flag|argument)|no such option|unknown option/.test(message));
1209
1446
  }
1210
- function isBenignMissingTmuxServerMessage(message) {
1211
- return (/no server running/i.test(message) ||
1212
- /error connecting to .*\(No such file or directory\)/i.test(message));
1213
- }
1214
- export function checkDetachedTmuxLaunchHealth() {
1215
- try {
1216
- execTmuxFileSync(["list-sessions"], {
1217
- stdio: ["ignore", "pipe", "pipe"],
1218
- encoding: "utf-8",
1219
- });
1220
- return { usable: true };
1221
- }
1222
- catch (err) {
1223
- const reason = tmuxFailureMessage(err);
1224
- if (isBenignMissingTmuxServerMessage(reason)) {
1225
- return { usable: true };
1226
- }
1227
- return { usable: false, reason };
1228
- }
1229
- }
1230
- function warnDetachedTmuxFallback(reason) {
1231
- const suffix = reason ? ` (${reason})` : "";
1232
- console.warn(`[omx] warning: tmux is installed but its server/socket is unusable${suffix}. Falling back to direct Codex launch.`);
1233
- }
1234
- const QUICK_ATTACH_NOOP_THRESHOLD_MS = 2_000;
1235
- function isWslWindowsTerminalEnvironment(env) {
1236
- return Boolean(env.WT_SESSION?.trim() &&
1237
- (env.WSL_INTEROP?.trim() ||
1238
- env.WSL_DISTRO_NAME?.trim() ||
1239
- env.WSLENV?.trim()));
1240
- }
1241
- function readDetachedSessionAttachedClientCount(sessionName) {
1242
- try {
1243
- const output = execTmuxFileSync(["display-message", "-p", "-t", sessionName, "#{session_attached}"], {
1244
- stdio: ["ignore", "pipe", "pipe"],
1245
- encoding: "utf-8",
1246
- }).trim();
1247
- const parsed = Number.parseInt(output, 10);
1248
- return Number.isFinite(parsed) ? parsed : null;
1249
- }
1250
- catch (err) {
1251
- logCliOperationFailure(err);
1252
- return null;
1447
+ function probeDetachedTmuxTransport() {
1448
+ const { result } = spawnPlatformCommandSync("tmux", ["-V"], {
1449
+ encoding: "utf-8",
1450
+ stdio: ["ignore", "pipe", "pipe"],
1451
+ });
1452
+ if (result.error || result.status !== 0) {
1453
+ throw new DetachedTransportUnavailable();
1253
1454
  }
1254
1455
  }
1255
- function assertDetachedAttachDidNotNoop(sessionName, elapsedMs, env) {
1256
- if (!isWslWindowsTerminalEnvironment(env))
1257
- return;
1258
- if (elapsedMs >= QUICK_ATTACH_NOOP_THRESHOLD_MS)
1259
- return;
1260
- const attachedClients = readDetachedSessionAttachedClientCount(sessionName);
1261
- if (attachedClients === null || attachedClients > 0)
1262
- return;
1263
- throw new Error([
1264
- "tmux attach-session returned immediately without attaching a client",
1265
- `(session=${sessionName}).`,
1266
- "This can happen on WSL2 under Windows Terminal.",
1267
- "Falling back to direct Codex launch.",
1268
- ].join(" "));
1269
- }
1270
1456
  function resolveTmuxAwareLaunchPolicy(explicitLaunchPolicy, nativeWindows) {
1271
- const launchPolicy = resolveCodexLaunchPolicy(process.env, process.platform, undefined, nativeWindows, undefined, undefined, explicitLaunchPolicy);
1272
- if (launchPolicy !== "detached-tmux") {
1273
- return { launchPolicy, effectiveExplicitLaunchPolicy: explicitLaunchPolicy };
1274
- }
1275
- const tmuxHealth = checkDetachedTmuxLaunchHealth();
1276
- if (tmuxHealth.usable) {
1277
- return { launchPolicy, effectiveExplicitLaunchPolicy: explicitLaunchPolicy };
1278
- }
1279
- warnDetachedTmuxFallback(tmuxHealth.reason);
1280
- return { launchPolicy: "direct", effectiveExplicitLaunchPolicy: "direct" };
1457
+ const launchPolicy = resolveCodexLaunchPolicy(process.env, process.platform, Boolean(resolveTmuxBinaryForPlatform()), nativeWindows, undefined, undefined, explicitLaunchPolicy);
1458
+ return { launchPolicy, effectiveExplicitLaunchPolicy: explicitLaunchPolicy };
1281
1459
  }
1282
1460
  export function resolveSignalExitCode(signal) {
1283
1461
  if (!signal)
@@ -1507,15 +1685,6 @@ export function resolveOmxRootForLaunch(cwd, env = process.env) {
1507
1685
  function resolveLaunchPath(cwd, raw) {
1508
1686
  return isCrossPlatformAbsolutePath(raw) ? raw : join(cwd, raw);
1509
1687
  }
1510
- function resolveHudRuntimeRootSource(omxRootOverride, env = process.env) {
1511
- if (env.OMX_TEAM_STATE_ROOT?.trim())
1512
- return 'team-env';
1513
- if (env.OMX_ROOT?.trim() || omxRootOverride)
1514
- return 'omx-root-env';
1515
- if (env.OMX_STATE_ROOT?.trim())
1516
- return 'omx-state-root-env';
1517
- return 'cwd-default';
1518
- }
1519
1688
  export function resolveHudRuntimeRootForLaunch(cwd, env = process.env) {
1520
1689
  const omxTeamStateRoot = env.OMX_TEAM_STATE_ROOT?.trim();
1521
1690
  if (omxTeamStateRoot) {
@@ -1574,6 +1743,8 @@ export function captureMadmaxWorktreeRuntimeContext(options) {
1574
1743
  const inheritedRoot = resolveInheritedMadmaxRoot(env);
1575
1744
  if (!inheritedRoot)
1576
1745
  return undefined;
1746
+ if (!madmaxInheritedContextMatchesLaunch(options.sourceCwd, options.originalLaunchArgs, env))
1747
+ return undefined;
1577
1748
  const sourceCwd = env.OMX_SOURCE_CWD?.trim() || options.sourceCwd;
1578
1749
  const worktreeCwd = options.worktreeCwd?.trim();
1579
1750
  const omxStateRoot = env.OMX_STATE_ROOT?.trim();
@@ -1673,7 +1844,28 @@ const MADMAX_DETACHED_LOCK_RETRY_MS = 50;
1673
1844
  const MADMAX_DETACHED_LOCK_MAX_ATTEMPTS = 100;
1674
1845
  const OMX_MADMAX_DETACHED_CONTEXT_ENV = "OMX_MADMAX_DETACHED_CONTEXT";
1675
1846
  function resolveMadmaxRunsRoot(env = process.env) {
1676
- return env.OMX_RUNS_DIR || join(homedir(), ".omx-runs");
1847
+ const configured = env.OMX_RUNS_DIR || join(homedir(), ".omx-runs");
1848
+ // Canonicalize, because the detached active-record path is derived from this root and macOS reaches
1849
+ // the same directory as both `/var/...` and `/private/var/...`. Without this, a launch that recorded
1850
+ // its active detached session under one alias could not be found under the other, so the reuse guard
1851
+ // silently started a DUPLICATE detached launch instead of attaching to the live one.
1852
+ try {
1853
+ return realpathSync(configured);
1854
+ }
1855
+ catch {
1856
+ // Not created yet: the raw path is correct for the first launch, which creates it.
1857
+ return configured;
1858
+ }
1859
+ }
1860
+ function canonicalizeExistingPath(target) {
1861
+ if (!target)
1862
+ return target;
1863
+ try {
1864
+ return realpathSync(target);
1865
+ }
1866
+ catch {
1867
+ return target;
1868
+ }
1677
1869
  }
1678
1870
  function canonicalizeLaunchCwd(cwd) {
1679
1871
  try {
@@ -1681,10 +1873,14 @@ function canonicalizeLaunchCwd(cwd) {
1681
1873
  cwd,
1682
1874
  encoding: "utf-8",
1683
1875
  stdio: ["ignore", "pipe", "ignore"],
1684
- }).trim() || cwd;
1876
+ }).trim() || canonicalizeExistingPath(cwd);
1685
1877
  }
1686
1878
  catch {
1687
- return cwd;
1879
+ // Outside a git checkout realpath is the only canonical form available. Returning the raw value let
1880
+ // the macOS /var vs /private/var alias survive into the context key, so the same directory reached
1881
+ // by two aliases hashed to two different keys and a legitimate inherited context failed its own
1882
+ // verification.
1883
+ return canonicalizeExistingPath(cwd);
1688
1884
  }
1689
1885
  }
1690
1886
  function normalizeMadmaxDetachedLaunchArgv(argv) {
@@ -1730,7 +1926,7 @@ export function buildMadmaxDetachedLaunchContextKey(sourceCwd, argv, runIdentity
1730
1926
  const payload = JSON.stringify({
1731
1927
  source_cwd: canonicalizeLaunchCwd(sourceCwd),
1732
1928
  argv: normalizeMadmaxDetachedLaunchArgv(argv),
1733
- run_identity: runIdentity,
1929
+ run_identity: canonicalizeExistingPath(runIdentity),
1734
1930
  });
1735
1931
  return createHash("sha256").update(payload).digest("hex").slice(0, 32);
1736
1932
  }
@@ -1761,31 +1957,77 @@ function readMadmaxDetachedActiveRecord(recordPath) {
1761
1957
  tmux_session_name: parsed.tmux_session_name,
1762
1958
  ...(typeof parsed.session_id === "string" ? { session_id: parsed.session_id } : {}),
1763
1959
  ...(typeof parsed.tmux_pane_id === "string" ? { tmux_pane_id: parsed.tmux_pane_id } : {}),
1960
+ ...(typeof parsed.tmux_internal_session_id === "string" ? { tmux_internal_session_id: parsed.tmux_internal_session_id } : {}),
1961
+ ...(typeof parsed.tmux_session_created === "string" ? { tmux_session_created: parsed.tmux_session_created } : {}),
1962
+ ...(typeof parsed.tmux_pane_pid === "number" ? { tmux_pane_pid: parsed.tmux_pane_pid } : {}),
1764
1963
  ...(typeof parsed.worktree_cwd === "string" ? { worktree_cwd: parsed.worktree_cwd } : {}),
1964
+ ...(typeof parsed.launch_nonce === "string" ? { launch_nonce: parsed.launch_nonce } : {}),
1965
+ ...(typeof parsed.leader_pid === "number" && Number.isSafeInteger(parsed.leader_pid) && parsed.leader_pid > 0 ? { leader_pid: parsed.leader_pid } : {}),
1966
+ ...(typeof parsed.base_state_root === "string" ? { base_state_root: parsed.base_state_root } : {}),
1967
+ ...(parsed.lifecycle_phase === "ready" || parsed.lifecycle_phase === "terminal" ? { lifecycle_phase: parsed.lifecycle_phase } : {}),
1765
1968
  };
1766
1969
  }
1767
1970
  catch {
1768
1971
  return null;
1769
1972
  }
1770
1973
  }
1974
+ function queryMadmaxDetachedRuntimeBinding(record) {
1975
+ if (!record.tmux_pane_id || !record.session_id)
1976
+ return null;
1977
+ try {
1978
+ const output = execTmuxFileSync([
1979
+ "list-panes", "-a", "-F",
1980
+ "#{pane_id}\t#{pane_dead}\t#{pane_pid}\t#{session_name}\t#{session_id}\t#{session_created}\t#{@omx_instance_id}",
1981
+ ], { encoding: "utf-8", stdio: ["ignore", "pipe", "ignore"] });
1982
+ let binding = null;
1983
+ const seen = new Set();
1984
+ for (const line of output.split("\n").filter(Boolean)) {
1985
+ const fields = line.split("\t");
1986
+ if (fields.length !== 7)
1987
+ return null;
1988
+ const [paneId, dead, panePidRaw, sessionName, internalSessionId, sessionCreated, omxInstanceId] = fields;
1989
+ const panePid = Number(panePidRaw);
1990
+ if (!/^%[0-9]+$/.test(paneId) || seen.has(paneId) || (dead !== "0" && dead !== "1")
1991
+ || !Number.isSafeInteger(panePid) || panePid <= 0 || !sessionName
1992
+ || !/^\$[0-9]+$/.test(internalSessionId) || !/^[0-9]+$/.test(sessionCreated))
1993
+ return null;
1994
+ seen.add(paneId);
1995
+ if (paneId !== record.tmux_pane_id)
1996
+ continue;
1997
+ if (dead !== "0" || sessionName !== record.tmux_session_name || omxInstanceId !== record.session_id)
1998
+ return null;
1999
+ binding = { paneId, panePid, sessionName, internalSessionId, sessionCreated, omxInstanceId };
2000
+ }
2001
+ return binding;
2002
+ }
2003
+ catch {
2004
+ return null;
2005
+ }
2006
+ }
2007
+ function hasMatchingMadmaxDetachedRuntimeBinding(record) {
2008
+ const binding = queryMadmaxDetachedRuntimeBinding(record);
2009
+ return Boolean(binding
2010
+ && binding.panePid === record.tmux_pane_pid
2011
+ && binding.internalSessionId === record.tmux_internal_session_id
2012
+ && binding.sessionCreated === record.tmux_session_created);
2013
+ }
1771
2014
  function isReusableMadmaxDetachedActiveRecord(record) {
1772
- if (!detachedTmuxSessionExists(record.tmux_session_name))
2015
+ if (record.lifecycle_phase !== "ready" || !record.leader_pid || !record.base_state_root || !record.launch_nonce)
1773
2016
  return false;
1774
- if (!record.session_id || !record.tmux_pane_id)
1775
- return false;
1776
- if (readTmuxSessionInstanceId(record.tmux_session_name) !== record.session_id) {
2017
+ try {
2018
+ process.kill(record.leader_pid, 0);
2019
+ }
2020
+ catch {
1777
2021
  return false;
1778
2022
  }
1779
- return tmuxPaneBelongsToSession(record.tmux_pane_id, record.tmux_session_name);
1780
- }
1781
- function detachedTmuxSessionExists(sessionName) {
1782
2023
  try {
1783
- execTmuxFileSync(["has-session", "-t", sessionName], { stdio: "ignore" });
1784
- return true;
2024
+ if (realpathSync(record.base_state_root) !== record.base_state_root)
2025
+ return false;
1785
2026
  }
1786
2027
  catch {
1787
2028
  return false;
1788
2029
  }
2030
+ return hasMatchingMadmaxDetachedRuntimeBinding(record);
1789
2031
  }
1790
2032
  function readMadmaxDetachedLockOwner(lockPath) {
1791
2033
  try {
@@ -1854,7 +2096,7 @@ function inspectMadmaxDetachedContextLock(lockPath) {
1854
2096
  diagnostic: `lock has no readable owner pid yet; lock age ${Math.round(ageMs)}ms`,
1855
2097
  };
1856
2098
  }
1857
- export function withMadmaxDetachedContextLock(runsRoot, contextKey, run, options = {}) {
2099
+ export async function withMadmaxDetachedContextLock(runsRoot, contextKey, run, options = {}) {
1858
2100
  const lockPath = join(runsRoot, MADMAX_DETACHED_ACTIVE_DIR, `${contextKey}.lock`);
1859
2101
  const maxAttempts = options.maxAttempts ?? MADMAX_DETACHED_LOCK_MAX_ATTEMPTS;
1860
2102
  const retryMs = options.retryMs ?? MADMAX_DETACHED_LOCK_RETRY_MS;
@@ -1872,7 +2114,7 @@ export function withMadmaxDetachedContextLock(runsRoot, contextKey, run, options
1872
2114
  };
1873
2115
  writeFileSync(join(lockPath, "owner.json"), `${JSON.stringify(owner, null, 2)}\n`, { mode: 0o600 });
1874
2116
  writeFileSync(join(lockPath, "pid"), String(process.pid));
1875
- return run();
2117
+ return await run();
1876
2118
  }
1877
2119
  finally {
1878
2120
  rmSync(lockPath, { recursive: true, force: true });
@@ -1900,6 +2142,7 @@ function readMadmaxRunMetadata(runRoot) {
1900
2142
  const parsed = JSON.parse(readFileSync(join(runRoot, ".omxbox-run.json"), "utf-8"));
1901
2143
  return {
1902
2144
  ...(typeof parsed.cwd === "string" ? { cwd: parsed.cwd } : {}),
2145
+ ...(typeof parsed.source_cwd === "string" ? { source_cwd: parsed.source_cwd } : {}),
1903
2146
  ...(typeof parsed.detached_launch_context === "string"
1904
2147
  ? { detached_launch_context: parsed.detached_launch_context }
1905
2148
  : {}),
@@ -1927,13 +2170,27 @@ function madmaxInheritedContextMatchesLaunch(cwd, launchArgs, env) {
1927
2170
  return false;
1928
2171
  if (metadata.cwd && metadata.cwd !== inheritedRoot)
1929
2172
  return false;
2173
+ const sourceCwd = env.OMX_SOURCE_CWD?.trim();
2174
+ if (!sourceCwd || !metadata.source_cwd || metadata.source_cwd !== sourceCwd)
2175
+ return false;
1930
2176
  if (metadata.detached_launch_context !== context)
1931
2177
  return false;
1932
2178
  const expectedContext = buildMadmaxDetachedLaunchContextKey(cwd, [...launchArgs], inheritedRoot);
1933
2179
  return expectedContext === context;
1934
2180
  }
1935
- function isMadmaxDetachedGuardEnabled(env) {
1936
- return env.OMXBOX_ACTIVE === "1" && typeof env[OMX_MADMAX_DETACHED_CONTEXT_ENV] === "string";
2181
+ function resolveVerifiedMadmaxDetachedContext(cwd, launchArgs, env) {
2182
+ if (!madmaxInheritedContextMatchesLaunch(cwd, launchArgs, env))
2183
+ return undefined;
2184
+ const inheritedRoot = resolveInheritedMadmaxRoot(env);
2185
+ const context = env[OMX_MADMAX_DETACHED_CONTEXT_ENV]?.trim();
2186
+ if (!inheritedRoot || !context)
2187
+ return undefined;
2188
+ return {
2189
+ root: resolveLaunchPath(cwd, inheritedRoot),
2190
+ sourceCwd: env.OMX_SOURCE_CWD?.trim() || cwd,
2191
+ context,
2192
+ runsRoot: resolveMadmaxRunsRoot(env),
2193
+ };
1937
2194
  }
1938
2195
  function cleanupCurrentMadmaxReuseRunRoot(env, runsRoot) {
1939
2196
  const runRoot = env.OMX_ROOT;
@@ -1945,18 +2202,243 @@ function cleanupCurrentMadmaxReuseRunRoot(env, runsRoot) {
1945
2202
  rmSync(runRoot, { recursive: true, force: true });
1946
2203
  }
1947
2204
  function writeMadmaxDetachedActiveRecord(recordPath, record) {
1948
- mkdirSync(dirname(recordPath), { recursive: true });
1949
- writeFileSync(recordPath, `${JSON.stringify(record, null, 2)}\n`, { mode: 0o600 });
2205
+ const bytes = `${JSON.stringify(record, null, 2)}\n`;
2206
+ const tempPath = `${recordPath}.${process.pid}.${randomUUID()}.tmp`;
2207
+ let fd;
2208
+ mkdirSync(dirname(recordPath), { recursive: true, mode: 0o700 });
2209
+ try {
2210
+ fd = openSync(tempPath, "wx", 0o600);
2211
+ writeFileSync(fd, bytes, "utf-8");
2212
+ try {
2213
+ fsyncSync(fd);
2214
+ }
2215
+ catch (error) {
2216
+ if (!(process.platform === "win32" && error.code === "EPERM"))
2217
+ throw error;
2218
+ }
2219
+ closeSync(fd);
2220
+ fd = undefined;
2221
+ renameSync(tempPath, recordPath);
2222
+ const directoryFd = openSync(dirname(recordPath), "r");
2223
+ try {
2224
+ try {
2225
+ fsyncSync(directoryFd);
2226
+ }
2227
+ catch (error) {
2228
+ if (!(process.platform === "win32" && error.code === "EPERM"))
2229
+ throw error;
2230
+ }
2231
+ }
2232
+ finally {
2233
+ closeSync(directoryFd);
2234
+ }
2235
+ return {
2236
+ bytes,
2237
+ digest: createHash("sha256").update(bytes).digest("hex"),
2238
+ nonce: record.launch_nonce ?? "",
2239
+ };
2240
+ }
2241
+ finally {
2242
+ if (fd !== undefined)
2243
+ closeSync(fd);
2244
+ try {
2245
+ unlinkSync(tempPath);
2246
+ }
2247
+ catch { }
2248
+ }
1950
2249
  }
1951
- class MadmaxDetachedReuseError extends Error {
1952
- failClosed = true;
2250
+ function detachedFailureCode(error) {
2251
+ const code = error && typeof error === "object" && "code" in error
2252
+ ? error.code
2253
+ : undefined;
2254
+ return typeof code === "string" && /^(?:EACCES|EAGAIN|EBUSY|ECANCELED|ECONNREFUSED|EEXIST|EINTR|EINVAL|EIO|EISDIR|EMFILE|ENFILE|ENOENT|ENOSPC|ENOTDIR|EPERM|EPIPE|EROFS|ESRCH|ETIMEDOUT)$/.test(code) ? code : "unknown";
2255
+ }
2256
+ export function describeDetachedLeaderFailure(error) {
2257
+ const describe = (value, depth) => {
2258
+ if (depth > 4)
2259
+ return "nested failure";
2260
+ if (value instanceof AggregateError) {
2261
+ return [value.message, ...[...value.errors].map((child) => describe(child, depth + 1))]
2262
+ .filter(Boolean)
2263
+ .join(": ");
2264
+ }
2265
+ if (value instanceof Error)
2266
+ return value.message;
2267
+ return String(value);
2268
+ };
2269
+ return describe(error, 0).replace(/[\r\n\t]+/g, " ").slice(0, 1_024);
2270
+ }
2271
+ export class DetachedLaunchSafetyError extends Error {
2272
+ phase;
2273
+ cause;
2274
+ report;
2275
+ name = "DetachedLaunchSafetyError";
2276
+ constructor(phase, cause, report) {
2277
+ super(`detached launch safety failure during ${phase}${cause instanceof Error ? `: ${describeDetachedLeaderFailure(cause)}` : ""}`);
2278
+ this.phase = phase;
2279
+ this.cause = cause;
2280
+ this.report = report;
2281
+ }
2282
+ }
2283
+ export class DetachedTransportUnavailable extends Error {
2284
+ reason;
2285
+ name = "DetachedTransportUnavailable";
2286
+ noSideEffects = true;
2287
+ constructor(reason) {
2288
+ super("detached transport is unavailable before launch side effects");
2289
+ this.reason = reason;
2290
+ }
2291
+ }
2292
+ /** Internal detached-launch seam. Exported solely for deterministic CLI tests. */
2293
+ export async function executeDetachedLaunchStateMachine(input, deps) {
2294
+ const report = input.preflight.report;
2295
+ const transition = (name) => { report.transitions.push(name); };
2296
+ let binding;
2297
+ let ownedRecord;
2298
+ let released = false;
2299
+ let retainedAuthority = false;
2300
+ let rollbackAuthorized = false;
2301
+ try {
2302
+ transition("D1");
2303
+ binding = await deps.establish();
2304
+ retainedAuthority = true;
2305
+ transition("D2");
2306
+ const completion = await deps.complete(binding);
2307
+ if (completion.kind === "failure") {
2308
+ const failures = [completion.error];
2309
+ try {
2310
+ report.rollback.attempted.push("setup-finalization");
2311
+ await deps.finalizeSetupFailure(binding);
2312
+ }
2313
+ catch (finalizationError) {
2314
+ report.rollback.failures.push({ step: "setup-finalization", status: "failed", code: detachedFailureCode(finalizationError) });
2315
+ failures.push(finalizationError);
2316
+ }
2317
+ throw new DetachedLaunchSafetyError("completion", new AggregateError(failures, `preLaunch ${completion.operation} failed: ${describeDetachedLeaderFailure(completion.error)}`), report);
2318
+ }
2319
+ transition("D3");
2320
+ const inertSession = await deps.createInertSession();
2321
+ transition("D4");
2322
+ const pane = await deps.capturePane(inertSession);
2323
+ transition("D5");
2324
+ if (await deps.updateNameMetadata(binding, inertSession) !== "committed-released") {
2325
+ throw new DetachedLaunchSafetyError("name-metadata", new Error("detached name metadata did not release"), report);
2326
+ }
2327
+ transition("D6");
2328
+ if (await deps.updatePaneMetadata(binding, pane) !== "committed-released") {
2329
+ throw new DetachedLaunchSafetyError("pane-metadata", new Error("detached pane metadata did not release"), report);
2330
+ }
2331
+ transition("D7");
2332
+ ownedRecord = await deps.publishActiveRecord(inertSession, pane);
2333
+ transition("D9");
2334
+ try {
2335
+ await deps.releaseBarrier();
2336
+ released = true;
2337
+ }
2338
+ catch (releaseError) {
2339
+ // A failed publication must not let the outer launcher destroy the
2340
+ // leader's finalization capability. Request a nonce-bound abort and
2341
+ // preserve all artifacts on timeout, malformed, or foreign reports.
2342
+ let resolution;
2343
+ try {
2344
+ resolution = await deps.abortAndAwaitFinalization?.(releaseError);
2345
+ }
2346
+ catch (abortError) {
2347
+ released = true;
2348
+ throw new AggregateError([releaseError, abortError], "detached release and abort publication both failed");
2349
+ }
2350
+ rollbackAuthorized = resolution?.acknowledged === true && Boolean(resolution.nonce && resolution.sessionId && resolution.sessionName &&
2351
+ Number.isSafeInteger(resolution.leaderPid) && resolution.kind);
2352
+ if (!rollbackAuthorized)
2353
+ released = true;
2354
+ throw releaseError;
2355
+ }
2356
+ transition("D10");
2357
+ await deps.attachOrReturn(inertSession, input.preflight.shouldAttach);
2358
+ return { kind: input.preflight.shouldAttach ? "attached" : "returned", released: true, report };
2359
+ }
2360
+ catch (error) {
2361
+ deps.diagnostic?.(error);
2362
+ if (retainedAuthority && !rollbackAuthorized && !released) {
2363
+ try {
2364
+ const resolution = await deps.abortAndAwaitFinalization?.(error);
2365
+ rollbackAuthorized = resolution?.rollbackAuthorized === true || (resolution?.acknowledged === true && Boolean(resolution.nonce && resolution.sessionId && resolution.sessionName &&
2366
+ Number.isSafeInteger(resolution.leaderPid) && resolution.kind));
2367
+ if (!rollbackAuthorized)
2368
+ released = true;
2369
+ }
2370
+ catch (abortError) {
2371
+ released = true;
2372
+ deps.diagnostic?.(abortError);
2373
+ }
2374
+ }
2375
+ if (!released) {
2376
+ try {
2377
+ report.rollback.attempted.push("rollback");
2378
+ await deps.rollback(ownedRecord, report);
2379
+ }
2380
+ catch (rollbackError) {
2381
+ report.rollback.failures.push({ step: "rollback", status: "failed", code: detachedFailureCode(rollbackError) });
2382
+ deps.diagnostic?.(rollbackError);
2383
+ }
2384
+ }
2385
+ if (error instanceof DetachedLaunchSafetyError) {
2386
+ throw new DetachedLaunchSafetyError(error.phase, error.cause, report);
2387
+ }
2388
+ const phase = report.transitions.at(-1);
2389
+ const phaseName = phase === "D1" ? "establishment"
2390
+ : phase === "D2" ? "completion"
2391
+ : phase === "D3" ? "inert-session"
2392
+ : phase === "D4" ? "pane-id"
2393
+ : phase === "D5" ? "name-metadata"
2394
+ : phase === "D6" ? "pane-metadata"
2395
+ : phase === "D7" ? "active-record"
2396
+ : phase === "D9" ? "barrier-release"
2397
+ : "post-release-attach";
2398
+ throw new DetachedLaunchSafetyError(phaseName, error, report);
2399
+ }
1953
2400
  }
1954
2401
  class MadmaxDetachedGuardError extends Error {
1955
2402
  failClosed = true;
1956
2403
  }
2404
+ function preflightDetachedLaunch() {
2405
+ // D0 must remain transport-only: `tmux -V` neither connects to nor creates a server.
2406
+ probeDetachedTmuxTransport();
2407
+ const report = { transitions: ["D0"], rollback: { attempted: [], failures: [] } };
2408
+ return Object.freeze({
2409
+ kind: "available",
2410
+ shouldAttach: shouldAttachDetachedTmuxSession(process.env),
2411
+ report,
2412
+ });
2413
+ }
2414
+ function handleMadmaxDetachedReuse(runtimeContext, preflight) {
2415
+ const contextKey = runtimeContext?.madmaxDetachedContext ?? process.env[OMX_MADMAX_DETACHED_CONTEXT_ENV]?.trim();
2416
+ const activeRecordPath = contextKey ? madmaxDetachedActiveRecordPath(resolveMadmaxRunsRoot(process.env), contextKey) : null;
2417
+ const activeRecord = activeRecordPath ? readMadmaxDetachedActiveRecord(activeRecordPath) : null;
2418
+ if (!activeRecord || activeRecord.context_key !== contextKey || !isReusableMadmaxDetachedActiveRecord(activeRecord))
2419
+ return false;
2420
+ cleanupCurrentMadmaxReuseRunRoot(process.env, resolveMadmaxRunsRoot(process.env));
2421
+ setDetachedTmuxSessionHistoryLimit(activeRecord.tmux_session_name, activeRecord.tmux_pane_id);
2422
+ if (!preflight.shouldAttach) {
2423
+ clearDetachedTmuxSessionHistoryIfUnattached(activeRecord.tmux_session_name, activeRecord.tmux_pane_id);
2424
+ return true;
2425
+ }
2426
+ process.stderr.write(`[omx] madmax detached launch already active for this context; attaching ${activeRecord.tmux_session_name} instead of starting a duplicate.\n`);
2427
+ try {
2428
+ execTmuxFileSync(["attach-session", "-t", activeRecord.tmux_session_name], { stdio: "inherit" });
2429
+ }
2430
+ catch (error) {
2431
+ throw new DetachedLaunchSafetyError("post-release-attach", error, preflight.report);
2432
+ }
2433
+ return true;
2434
+ }
1957
2435
  export function createMadmaxIsolatedRoot(sourceCwd, argv, env = process.env) {
1958
- const runsRoot = resolveMadmaxRunsRoot(env);
2436
+ let runsRoot = resolveMadmaxRunsRoot(env);
1959
2437
  mkdirSync(runsRoot, { recursive: true });
2438
+ // Re-resolve after creating it: on a first launch the directory does not exist yet, so the realpath
2439
+ // inside resolveMadmaxRunsRoot falls back to the raw value and the macOS /var vs /private/var alias
2440
+ // survives into the run directory and the active-record path.
2441
+ runsRoot = resolveMadmaxRunsRoot(env);
1960
2442
  const stamp = new Date().toISOString().replace(/[-:TZ.]/g, "").slice(0, 14);
1961
2443
  const suffix = Math.random().toString(16).slice(2, 6);
1962
2444
  const runDir = join(runsRoot, sanitizeRunIdSegment(`run-${stamp}-${suffix}`));
@@ -1984,6 +2466,24 @@ function activateMadmaxIsolationIfNeeded(command, launchArgs, cwd, env = process
1984
2466
  env.OMX_SOURCE_CWD = cwd;
1985
2467
  process.stderr.write(`[omx] madmax isolated state: ${runDir} (source: ${cwd})\n`);
1986
2468
  }
2469
+ function renderDetachedLaunchSafetyReport(error) {
2470
+ const establishmentCleanup = error.report.establishmentCleanup?.capability.map((entry) => ({
2471
+ role: entry.role,
2472
+ phase: entry.phase,
2473
+ status: entry.status,
2474
+ ...(entry.error ? { failureCode: detachedFailureCode(entry.error) } : {}),
2475
+ }));
2476
+ return JSON.stringify({
2477
+ phase: error.phase,
2478
+ failure: describeDetachedLeaderFailure(error.cause),
2479
+ transitions: error.report.transitions,
2480
+ ...(establishmentCleanup ? { establishmentCleanup } : {}),
2481
+ rollback: {
2482
+ attempted: error.report.rollback.attempted,
2483
+ failures: error.report.rollback.failures.map(({ step, status, code }) => ({ step, status, code })),
2484
+ },
2485
+ });
2486
+ }
1987
2487
  export async function main(args) {
1988
2488
  const knownCommands = new Set([
1989
2489
  "launch",
@@ -1992,6 +2492,7 @@ export async function main(args) {
1992
2492
  "imagegen",
1993
2493
  "capabilities",
1994
2494
  "setup",
2495
+ "__detached-session-leader",
1995
2496
  "update",
1996
2497
  "list",
1997
2498
  "agents",
@@ -2009,6 +2510,7 @@ export async function main(args) {
2009
2510
  "api",
2010
2511
  "sparkshell",
2011
2512
  "team",
2513
+ "autopilot",
2012
2514
  "ralph",
2013
2515
  "ralplan",
2014
2516
  "ultragoal",
@@ -2038,11 +2540,15 @@ export async function main(args) {
2038
2540
  dryRun: flags.has("--dry-run"),
2039
2541
  verbose: flags.has("--verbose"),
2040
2542
  team: flags.has("--team"),
2543
+ repairState: flags.has("--repair-state"),
2041
2544
  };
2042
2545
  if (flags.has("--help") && !commandOwnsLocalHelp(command)) {
2043
2546
  console.log(HELP);
2044
2547
  return;
2045
2548
  }
2549
+ if (command !== "launch" && command !== "resume") {
2550
+ activateMadmaxIsolationIfNeeded(command, launchArgs, process.cwd(), process.env);
2551
+ }
2046
2552
  try {
2047
2553
  if (command === "session") {
2048
2554
  await sessionCommand(args.slice(1));
@@ -2052,8 +2558,13 @@ export async function main(args) {
2052
2558
  console.log(RESUME_HELP);
2053
2559
  return;
2054
2560
  }
2055
- activateMadmaxIsolationIfNeeded(command, launchArgs, process.cwd(), process.env);
2056
2561
  switch (command) {
2562
+ case "__detached-session-leader": {
2563
+ const payload = decodeDetachedLeaderPayload(launchArgs[0]);
2564
+ await runDetachedSessionLeader(payload);
2565
+ process.exit(typeof process.exitCode === "number" ? process.exitCode : 0);
2566
+ break;
2567
+ }
2057
2568
  case "launch":
2058
2569
  if (launchArgsRequestAuthHotswap(launchArgs)) {
2059
2570
  await launchWithAuthHotswap(launchArgs);
@@ -2067,6 +2578,7 @@ export async function main(args) {
2067
2578
  break;
2068
2579
  case "setup":
2069
2580
  await setup({
2581
+ disableHooks: flags.has("--disable-hooks"),
2070
2582
  force: options.force,
2071
2583
  mergeAgents: options.mergeAgents,
2072
2584
  mergeAgentsPolicy: resolveSetupAgentsMergePolicyArg(args.slice(1)),
@@ -2166,6 +2678,9 @@ export async function main(args) {
2166
2678
  case "team":
2167
2679
  await teamCommand(args.slice(1), options);
2168
2680
  break;
2681
+ case "autopilot":
2682
+ await autopilotCommand(args.slice(1));
2683
+ break;
2169
2684
  case "url":
2170
2685
  await urlCommand(args.slice(1));
2171
2686
  break;
@@ -2249,7 +2764,13 @@ export async function main(args) {
2249
2764
  }
2250
2765
  }
2251
2766
  catch (err) {
2252
- console.error(`Error: ${err instanceof Error ? err.message : err}`);
2767
+ if (err instanceof DetachedLaunchSafetyError) {
2768
+ console.error(`Error: ${err.message}`);
2769
+ console.error(`[omx] detached launch safety report: ${renderDetachedLaunchSafetyReport(err)}`);
2770
+ }
2771
+ else {
2772
+ console.error(`Error: ${err instanceof Error ? err.message : err}`);
2773
+ }
2253
2774
  process.exit(1);
2254
2775
  }
2255
2776
  }
@@ -2480,38 +3001,29 @@ export async function launchWithAuthHotswap(args) {
2480
3001
  process.exitCode = status;
2481
3002
  }
2482
3003
  export async function launchWithHud(args) {
2483
- if (isNativeWindows()) {
2484
- const { result } = spawnPlatformCommandSync("tmux", ["-V"], {
2485
- encoding: "utf-8",
2486
- stdio: ["pipe", "pipe", "pipe"],
2487
- });
2488
- if (result.error) {
2489
- const errno = result.error;
2490
- const kind = classifySpawnError(errno);
2491
- if (kind === "missing") {
2492
- console.warn("[omx] warning: tmux was not found on native Windows. Continuing without tmux/HUD.\n" +
2493
- "[omx] To enable tmux-backed features, install psmux:\n" +
2494
- "[omx] winget install psmux\n" +
2495
- "[omx] See: https://github.com/marlocarlo/psmux");
2496
- }
2497
- else {
2498
- console.warn(`[omx] warning: tmux probe failed on native Windows (${errno.code || errno.message}). Continuing without tmux/HUD.`);
2499
- }
2500
- }
2501
- else if (result.status !== 0 && !isTmuxAvailable()) {
2502
- const stderr = (result.stderr || "").trim();
2503
- console.warn(`[omx] warning: tmux reported an error on native Windows${stderr ? ` (${stderr})` : ""}. Continuing without tmux/HUD.`);
2504
- }
2505
- }
2506
3004
  const launchCwd = process.cwd();
2507
3005
  const { omxArgs, suffix } = splitOmxArgsAtEndOfOptions(args);
2508
3006
  const parsedWorktree = parseWorktreeMode(omxArgs);
2509
3007
  const notifyTempResult = resolveNotifyTempContract(parsedWorktree.remainingArgs, process.env);
2510
3008
  const passthroughArgs = [...notifyTempResult.passthroughArgs, ...suffix];
2511
3009
  const explicitLaunchPolicy = resolveEffectiveLeaderLaunchPolicyOverride(passthroughArgs, process.env);
2512
- const persistentCodexHomeForLaunch = resolveCodexHomeForLaunch(launchCwd, process.env);
2513
- const { launchPolicy, effectiveExplicitLaunchPolicy } = resolveTmuxAwareLaunchPolicy(explicitLaunchPolicy, isNativeWindows());
3010
+ let { launchPolicy } = resolveTmuxAwareLaunchPolicy(explicitLaunchPolicy, isNativeWindows());
3011
+ let detachedPreflight;
3012
+ if (launchPolicy === "detached-tmux") {
3013
+ try {
3014
+ detachedPreflight = preflightDetachedLaunch();
3015
+ }
3016
+ catch (error) {
3017
+ if (!(error instanceof DetachedTransportUnavailable))
3018
+ throw error;
3019
+ console.warn("[omx] warning: detached tmux transport is unavailable. Falling back to direct Codex launch.");
3020
+ launchPolicy = "direct";
3021
+ }
3022
+ }
2514
3023
  const enableNotifyFallbackAuthority = launchPolicy === "direct";
3024
+ activateMadmaxIsolationIfNeeded("launch", args, launchCwd, process.env);
3025
+ const verifiedMadmaxLaunchContext = resolveVerifiedMadmaxDetachedContext(launchCwd, args, process.env);
3026
+ const persistentCodexHomeForLaunch = resolveCodexHomeForLaunch(launchCwd, process.env);
2515
3027
  const workerSparkModel = resolveWorkerSparkModel(passthroughArgs, persistentCodexHomeForLaunch);
2516
3028
  let normalizedArgs = normalizeCodexLaunchArgs(passthroughArgs);
2517
3029
  let cwd = launchCwd;
@@ -2551,6 +3063,8 @@ export async function launchWithHud(args) {
2551
3063
  clearInheritedMadmaxRootForDisposableWorktreeLaunch(parsedWorktree.remainingArgs);
2552
3064
  applyDisposableWorktreeOmxRootForLaunch(ensuredLaunchWorktree);
2553
3065
  applyWorktreeToolContextForLaunch(cwd, ensuredLaunchWorktree);
3066
+ if (launchPolicy === "detached-tmux" && detachedPreflight && handleMadmaxDetachedReuse(madmaxWorktreeRuntimeContext, detachedPreflight))
3067
+ return;
2554
3068
  const sessionId = `omx-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
2555
3069
  try {
2556
3070
  await maybeCheckAndPromptUpdate(cwd);
@@ -2592,37 +3106,77 @@ export async function launchWithHud(args) {
2592
3106
  const codexHomeOverride = preparedCodexHome.codexHomeOverride;
2593
3107
  const sqliteHomeOverride = preparedCodexHome.sqliteHomeOverride;
2594
3108
  const projectLocalCodexHomeForCleanup = preparedCodexHome.projectLocalCodexHomeForCleanup;
2595
- // ── Phase 1: preLaunch ──────────────────────────────────────────────────
2596
- try {
2597
- await preLaunch(cwd, sessionId, notifyTempResult.contract, codexHomeOverride, enableNotifyFallbackAuthority, worktreeDirty);
3109
+ let insideTmuxControlPlane;
3110
+ if (launchPolicy === "inside-tmux") {
3111
+ let verifiedTeamContext;
3112
+ try {
3113
+ verifiedTeamContext = await resolveVerifiedDetachedTeamContext(cwd, process.env);
3114
+ }
3115
+ catch {
3116
+ verifiedTeamContext = undefined;
3117
+ }
3118
+ insideTmuxControlPlane = buildDetachedLaunchControlPlane({
3119
+ cwd,
3120
+ sessionId,
3121
+ env: process.env,
3122
+ omxRootOverride: resolveOmxRootForLaunch(cwd, process.env),
3123
+ runtimeContext: madmaxWorktreeRuntimeContext,
3124
+ verifiedMadmaxContext: verifiedMadmaxLaunchContext,
3125
+ leaderPaneId: process.env.TMUX_PANE,
3126
+ verifiedTeamContext,
3127
+ });
2598
3128
  }
2599
- catch (err) {
2600
- if (isSessionPointerLaunchAbort(err)) {
2601
- console.error(`[omx] session pointer launch aborted: ${err.code}`);
2602
- reportOrdinaryLaunchRootConflict(err, cwd, cwd === launchCwd && !parsedWorktree.mode.enabled && !isResumeCodexLaunch(normalizedArgs));
3129
+ const restoreInsideTmuxControlPlane = insideTmuxControlPlane
3130
+ ? applyLaunchOwnedControlPlane(insideTmuxControlPlane)
3131
+ : undefined;
3132
+ let launch;
3133
+ if (launchPolicy !== "detached-tmux") {
3134
+ try {
3135
+ launch = await preLaunch(cwd, sessionId, notifyTempResult.contract, codexHomeOverride, enableNotifyFallbackAuthority, worktreeDirty);
3136
+ }
3137
+ catch (err) {
3138
+ logEstablishmentCleanupEvidence(err);
3139
+ if (isSessionPointerLaunchAbort(err)) {
3140
+ console.error(`[omx] session pointer launch aborted: ${err.code}`);
3141
+ reportOrdinaryLaunchRootConflict(err, cwd, cwd === launchCwd && !parsedWorktree.mode.enabled && !isResumeCodexLaunch(normalizedArgs));
3142
+ }
3143
+ restoreInsideTmuxControlPlane?.();
2603
3144
  await cleanupRuntimeCodexHome(preparedCodexHome.runtimeCodexHomeForCleanup, projectLocalCodexHomeForCleanup).catch((cleanupErr) => {
2604
3145
  console.error(`[omx] preLaunch abort cleanup warning: ${cleanupErr instanceof Error ? cleanupErr.message : cleanupErr}`);
2605
3146
  });
2606
3147
  throw err;
2607
3148
  }
2608
- // preLaunch errors after pointer commit must not prevent Codex from starting.
2609
- console.error(`[omx] preLaunch warning: ${err instanceof Error ? err.message : err}`);
2610
3149
  }
2611
- // ── Phase 2: run ────────────────────────────────────────────────────────
2612
3150
  let postLaunchHandledExternally = false;
2613
3151
  try {
2614
3152
  const notifyTempContractRaw = notifyTempResult.contract.active
2615
3153
  ? serializeNotifyTempContract(notifyTempResult.contract)
2616
3154
  : null;
2617
- const launchResult = runCodex(cwd, normalizedArgs, sessionId, workerSparkModel, codexHomeOverride, sqliteHomeOverride, notifyTempContractRaw, effectiveExplicitLaunchPolicy, projectLocalCodexHomeForCleanup, preparedCodexHome.runtimeCodexHomeForCleanup, madmaxWorktreeRuntimeContext);
3155
+ const launchResult = await runCodex(cwd, normalizedArgs, sessionId, workerSparkModel, codexHomeOverride, sqliteHomeOverride, notifyTempContractRaw, {
3156
+ notifyTempContract: notifyTempResult.contract,
3157
+ enableNotifyFallbackAuthority,
3158
+ worktreeDirty,
3159
+ }, launchPolicy, detachedPreflight, projectLocalCodexHomeForCleanup, preparedCodexHome.runtimeCodexHomeForCleanup, madmaxWorktreeRuntimeContext, verifiedMadmaxLaunchContext, insideTmuxControlPlane);
2618
3160
  postLaunchHandledExternally = launchResult.postLaunchHandledExternally;
2619
3161
  }
3162
+ catch (error) {
3163
+ if (error instanceof DetachedLaunchSafetyError)
3164
+ postLaunchHandledExternally = true;
3165
+ throw error;
3166
+ }
2620
3167
  finally {
2621
- // ── Phase 3: postLaunch ─────────────────────────────────────────────
2622
- if (!postLaunchHandledExternally) {
2623
- await postLaunch(cwd, sessionId, codexHomeOverride, enableNotifyFallbackAuthority, projectLocalCodexHomeForCleanup);
3168
+ if (!postLaunchHandledExternally && launch) {
3169
+ try {
3170
+ await postLaunch(cwd, sessionId, launch.binding, codexHomeOverride, enableNotifyFallbackAuthority, projectLocalCodexHomeForCleanup);
3171
+ }
3172
+ finally {
3173
+ restoreInsideTmuxControlPlane?.();
3174
+ }
2624
3175
  await cleanupRuntimeCodexHome(preparedCodexHome.runtimeCodexHomeForCleanup, projectLocalCodexHomeForCleanup).catch(logCliOperationFailure);
2625
3176
  }
3177
+ else {
3178
+ restoreInsideTmuxControlPlane?.();
3179
+ }
2626
3180
  }
2627
3181
  }
2628
3182
  export async function execWithOverlay(args) {
@@ -2689,18 +3243,19 @@ export async function execWithOverlay(args) {
2689
3243
  const codexHomeOverride = preparedCodexHome.codexHomeOverride;
2690
3244
  const sqliteHomeOverride = preparedCodexHome.sqliteHomeOverride;
2691
3245
  const projectLocalCodexHomeForCleanup = preparedCodexHome.projectLocalCodexHomeForCleanup;
3246
+ let launch;
2692
3247
  try {
2693
- await preLaunch(cwd, sessionId, notifyTempResult.contract, codexHomeOverride, true, worktreeDirty);
3248
+ launch = await preLaunch(cwd, sessionId, notifyTempResult.contract, codexHomeOverride, true, worktreeDirty);
2694
3249
  }
2695
3250
  catch (err) {
3251
+ logEstablishmentCleanupEvidence(err);
2696
3252
  if (isSessionPointerLaunchAbort(err)) {
2697
3253
  console.error(`[omx] session pointer launch aborted: ${err.code}`);
2698
- await cleanupRuntimeCodexHome(preparedCodexHome.runtimeCodexHomeForCleanup, projectLocalCodexHomeForCleanup).catch((cleanupErr) => {
2699
- console.error(`[omx] preLaunch abort cleanup warning: ${cleanupErr instanceof Error ? cleanupErr.message : cleanupErr}`);
2700
- });
2701
- throw err;
2702
3254
  }
2703
- console.error(`[omx] preLaunch warning: ${err instanceof Error ? err.message : err}`);
3255
+ await cleanupRuntimeCodexHome(preparedCodexHome.runtimeCodexHomeForCleanup, projectLocalCodexHomeForCleanup).catch((cleanupErr) => {
3256
+ console.error(`[omx] preLaunch abort cleanup warning: ${cleanupErr instanceof Error ? cleanupErr.message : cleanupErr}`);
3257
+ });
3258
+ throw err;
2704
3259
  }
2705
3260
  try {
2706
3261
  const notifyTempContractRaw = notifyTempResult.contract.active
@@ -2708,22 +3263,28 @@ export async function execWithOverlay(args) {
2708
3263
  : null;
2709
3264
  const codexArgs = injectModelInstructionsBypassArgs(cwd, ["exec", ...normalizedArgs], process.env, sessionModelInstructionsPath(cwd, sessionId));
2710
3265
  const omxRootOverride = resolveOmxRootForLaunch(cwd, process.env);
2711
- const codexEnvBase = {
2712
- ...process.env,
3266
+ const omxBin = resolveOmxCliEntryPath({ argv1: process.argv[1], cwd, env: process.env });
3267
+ if (!omxBin)
3268
+ throw new Error("Unable to resolve OMX launcher path for exec");
3269
+ const hudRuntimeRoot = resolveHudRuntimeRootForLaunch(cwd, process.env);
3270
+ const codexEnvBase = prependOmxRuntimeCommandShimToEnv(cwd, {
3271
+ ...stripHermesMcpBridgeEnv(process.env),
2713
3272
  ...(codexHomeOverride ? { CODEX_HOME: codexHomeOverride } : {}),
2714
3273
  ...(sqliteHomeOverride ? { [CODEX_SQLITE_HOME_ENV]: sqliteHomeOverride } : {}),
2715
3274
  ...(omxRootOverride ? { OMX_ROOT: omxRootOverride } : {}),
3275
+ }, omxBin);
3276
+ // Correlates plugin hook routing only; it is not an authority token.
3277
+ const pluginHookRoutingLaunchId = randomUUID();
3278
+ const codexEnv = {
3279
+ ...codexEnvBase,
3280
+ OMX_CODEX_LAUNCH_ID: pluginHookRoutingLaunchId,
3281
+ ...buildHudRuntimeEnv({ sessionId, ...hudRuntimeRoot }).env,
3282
+ ...(notifyTempContractRaw ? { [OMX_NOTIFY_TEMP_CONTRACT_ENV]: notifyTempContractRaw } : {}),
2716
3283
  };
2717
- const codexEnv = notifyTempContractRaw
2718
- ? {
2719
- ...codexEnvBase,
2720
- [OMX_NOTIFY_TEMP_CONTRACT_ENV]: notifyTempContractRaw,
2721
- }
2722
- : codexEnvBase;
2723
3284
  runCodexBlocking(cwd, codexArgs, codexEnv);
2724
3285
  }
2725
3286
  finally {
2726
- await postLaunch(cwd, sessionId, codexHomeOverride, true, projectLocalCodexHomeForCleanup);
3287
+ await postLaunch(cwd, sessionId, launch.binding, codexHomeOverride, true, projectLocalCodexHomeForCleanup);
2727
3288
  await cleanupRuntimeCodexHome(preparedCodexHome.runtimeCodexHomeForCleanup, projectLocalCodexHomeForCleanup).catch(logCliOperationFailure);
2728
3289
  }
2729
3290
  }
@@ -3005,9 +3566,6 @@ export function detectDetachedSessionWindowIndex(sessionName) {
3005
3566
  return null;
3006
3567
  }
3007
3568
  }
3008
- function escapeShellDoubleQuotedValue(value) {
3009
- return value.replace(/["\\$`]/g, "\\$&");
3010
- }
3011
3569
  function sanitizeTmuxLeaseKey(value) {
3012
3570
  const cleaned = value
3013
3571
  .toLowerCase()
@@ -3193,106 +3751,173 @@ function withTmuxExtendedKeysLeaseLock(cwd, socketPath, run) {
3193
3751
  }
3194
3752
  throw new Error(`timed out waiting for tmux extended-keys lease lock: ${lockPath}`);
3195
3753
  }
3196
- function buildDetachedSessionLeaderCommand(cwd, sessionName, codexCmd, sessionId, codexHomeOverride, projectLocalCodexHomeForCleanup, runtimeCodexHomeForCleanup, parentEnvFilePath) {
3197
- const detachedPostLaunchHelper = sessionId
3198
- ? `${buildDetachedSessionPostLaunchHelperCommand(cwd, sessionId, codexHomeOverride, projectLocalCodexHomeForCleanup, runtimeCodexHomeForCleanup)} >/dev/null 2>&1 || true;`
3199
- : "";
3200
- const parentEnvSource = parentEnvFilePath && parentEnvFilePath.trim()
3201
- ? `if [ -r ${quoteShellArg(parentEnvFilePath)} ]; then . ${quoteShellArg(parentEnvFilePath)}; rm -f ${quoteShellArg(parentEnvFilePath)}; fi;`
3202
- : "";
3203
- const parentEnvCleanup = parentEnvFilePath && parentEnvFilePath.trim()
3204
- ? `rm -f ${quoteShellArg(parentEnvFilePath)} 2>/dev/null || true;`
3205
- : "";
3206
- const wrapped = [
3207
- buildTmuxExtendedKeysAcquireShellSnippet(cwd),
3208
- 'exec 3<&0;',
3209
- 'omx_codex_pid="";',
3210
- "omx_detached_session_cleanup() {",
3211
- "status=$?;",
3212
- "trap - 0 INT TERM HUP;",
3213
- 'if [ -n "$omx_codex_pid" ] && kill -0 "$omx_codex_pid" 2>/dev/null; then',
3214
- 'kill -TERM "$omx_codex_pid" 2>/dev/null || true;',
3215
- 'wait "$omx_codex_pid" 2>/dev/null || true;',
3216
- "fi;",
3217
- 'exec 3<&- 2>/dev/null || true;',
3218
- buildTmuxExtendedKeysReleaseShellSnippet(cwd),
3219
- parentEnvCleanup,
3220
- detachedPostLaunchHelper,
3221
- 'if [ "$status" -eq 0 ]; then',
3222
- `tmux kill-session -t "${escapeShellDoubleQuotedValue(sessionName)}" >/dev/null 2>&1 || true;`,
3223
- "fi;",
3224
- "exit $status;",
3225
- "};",
3226
- "trap omx_detached_session_cleanup 0 INT TERM HUP;",
3227
- parentEnvSource,
3228
- "unset OMX_HERMES_MCP_BRIDGE;",
3229
- "omx_codex_started_at=$(date +%s 2>/dev/null || printf 0);",
3230
- `${codexCmd} <&3 &`,
3231
- "omx_codex_pid=$!;",
3232
- 'wait "$omx_codex_pid";',
3233
- "omx_codex_status=$?;",
3234
- "omx_codex_finished_at=$(date +%s 2>/dev/null || printf 0);",
3235
- 'omx_codex_elapsed=$((omx_codex_finished_at - omx_codex_started_at));',
3236
- 'if [ "$omx_codex_status" -eq 0 ] && [ "$omx_codex_elapsed" -le 2 ]; then',
3237
- 'printf "\\n[omx] codex exited immediately with code 0 during startup. The detached tmux session is being kept open so any output above remains visible. Press Enter to close this OMX session.\\n" >&2;',
3238
- 'IFS= read -r _omx_close || true;',
3239
- 'elif [ "$omx_codex_status" -gt 0 ] && [ "$omx_codex_status" -lt 128 ] && [ "$omx_codex_elapsed" -le 2 ]; then',
3240
- 'printf "\\n[omx] codex exited with code %s during startup. The detached tmux session is being kept open so the error above remains visible. Press Enter to close this OMX session.\\n" "$omx_codex_status" >&2;',
3241
- 'IFS= read -r _omx_close || true;',
3242
- 'elif [ "$omx_codex_status" -gt 0 ] && [ "$omx_codex_status" -lt 128 ]; then',
3243
- 'printf "\\n[omx] codex exited with code %s. The detached tmux session is being kept open so the error above remains visible. Press Enter to close this OMX session.\\n" "$omx_codex_status" >&2;',
3244
- 'IFS= read -r _omx_close || true;',
3245
- "fi;",
3246
- 'exit "$omx_codex_status";',
3247
- ].join(" ");
3248
- return `/bin/sh -c ${quoteShellArg(wrapped)}`;
3249
- }
3250
- function buildDetachedSessionPostLaunchHelperCommand(cwd, sessionId, codexHomeOverride, projectLocalCodexHomeForCleanup, runtimeCodexHomeForCleanup) {
3251
- const cwdLiteral = JSON.stringify(cwd);
3252
- const sessionIdLiteral = JSON.stringify(sessionId);
3253
- const codexHomeLiteral = typeof codexHomeOverride === "string" && codexHomeOverride.length > 0
3254
- ? JSON.stringify(codexHomeOverride)
3255
- : "undefined";
3256
- const projectLocalCleanupLiteral = typeof projectLocalCodexHomeForCleanup === "string" &&
3257
- projectLocalCodexHomeForCleanup.length > 0
3258
- ? JSON.stringify(projectLocalCodexHomeForCleanup)
3259
- : "undefined";
3260
- const runtimeCodexHomeCleanupLiteral = typeof runtimeCodexHomeForCleanup === "string" &&
3261
- runtimeCodexHomeForCleanup.length > 0
3262
- ? JSON.stringify(runtimeCodexHomeForCleanup)
3263
- : "undefined";
3264
- const moduleUrlLiteral = JSON.stringify(import.meta.url);
3265
- const script = [
3266
- `const mod = await import(${moduleUrlLiteral});`,
3267
- `await mod.runDetachedSessionPostLaunch(${cwdLiteral}, ${sessionIdLiteral}, ${codexHomeLiteral}, ${projectLocalCleanupLiteral}, ${runtimeCodexHomeCleanupLiteral});`,
3268
- ].join(" ");
3269
- return `${quoteShellArg(process.execPath)} --input-type=module -e ${quoteShellArg(script)}`;
3270
- }
3271
- function execTmuxSync(args, execFileSyncImpl = (file, tmuxArgs) => execFileSync(file, tmuxArgs, {
3272
- encoding: "utf-8",
3273
- ...(process.platform === "win32" ? { windowsHide: true } : {}),
3274
- })) {
3275
- return execFileSyncImpl(resolveTmuxExecutableForLaunch(), [...args]).trim();
3754
+ function buildDetachedSessionLeaderCommand(cwd, sessionName, codexCmd, sessionId, codexHomeOverride, projectLocalCodexHomeForCleanup, runtimeCodexHomeForCleanup, parentEnvFilePath, readyPath, preLaunchOptions, omxBin = process.argv[1]) {
3755
+ const payload = Buffer.from(JSON.stringify({
3756
+ cwd, sessionName, sessionId, codexCmd, codexHomeOverride,
3757
+ projectLocalCodexHomeForCleanup, runtimeCodexHomeForCleanup, parentEnvFilePath,
3758
+ readyPath, preLaunchOptions,
3759
+ })).toString("base64url");
3760
+ return `/bin/sh -c ${quoteShellArg(`${parentEnvFilePath ? `if [ -r ${quoteShellArg(parentEnvFilePath)} ]; then . ${quoteShellArg(parentEnvFilePath)}; rm -f ${quoteShellArg(parentEnvFilePath)}; fi; ` : ""}exec ${quoteShellArg(process.execPath)} ${quoteShellArg(omxBin)} __detached-session-leader ${quoteShellArg(payload)}`)}`;
3761
+ }
3762
+ function buildDetachedWindowsLeaderCommand(cwd, sessionName, codexCmd, sessionId, codexHomeOverride, projectLocalCodexHomeForCleanup, runtimeCodexHomeForCleanup, parentEnv, readyPath, preLaunchOptions, omxBin = process.argv[1]) {
3763
+ const payload = Buffer.from(JSON.stringify({
3764
+ cwd, sessionName, sessionId, codexCmd, codexHomeOverride,
3765
+ projectLocalCodexHomeForCleanup, runtimeCodexHomeForCleanup,
3766
+ ...(parentEnv ? { parentEnv: detachedSessionParentEnvironment(parentEnv, true) } : {}),
3767
+ readyPath, preLaunchOptions,
3768
+ })).toString("base64url");
3769
+ const invocation = `& ${quotePowerShellArg(process.execPath)} ${quotePowerShellArg(omxBin)} __detached-session-leader ${quotePowerShellArg(payload)}`;
3770
+ return `powershell.exe -NoLogo -NoProfile -NonInteractive -Command ${quotePowerShellArg(invocation)}`;
3771
+ }
3772
+ export function isDetachedReadyReportAuthorized(report, expected) {
3773
+ if (report?.kind !== "ready" || report.nonce !== expected.nonce
3774
+ || report.sessionId !== expected.sessionId || report.sessionName !== expected.sessionName
3775
+ || typeof report.leaderPid !== "number" || report.leaderPid <= 0)
3776
+ return false;
3777
+ if (expected.shouldAttach && expected.leaderPanePid !== null) {
3778
+ return report.leaderPid === expected.leaderPanePid;
3779
+ }
3780
+ return report.paneId === expected.leaderPaneId;
3276
3781
  }
3277
- export function acquireTmuxExtendedKeysLease(cwd, execFileSyncImpl = (file, tmuxArgs) => execFileSync(file, tmuxArgs, {
3278
- encoding: "utf-8",
3279
- ...(process.platform === "win32" ? { windowsHide: true } : {}),
3280
- }), ownerPid = process.pid) {
3782
+ function writeDetachedLeaderReport(path, report) {
3783
+ mkdirSync(dirname(path), { recursive: true, mode: 0o700 });
3784
+ const tempPath = `${path}.${process.pid}.${randomUUID()}.tmp`;
3785
+ let fd;
3281
3786
  try {
3282
- const socketPath = resolveTmuxSocketPath(execFileSyncImpl);
3283
- const leasePath = tmuxExtendedKeysLeasePath(cwd, socketPath);
3284
- const holderPid = Number.isSafeInteger(ownerPid) && ownerPid > 0 ? ownerPid : process.pid;
3285
- const leaseId = `${holderPid}-${Date.now()}-${Math.random().toString(16).slice(2)}`;
3286
- withTmuxExtendedKeysLeaseLock(cwd, socketPath, () => {
3287
- const stateRaw = readTmuxExtendedKeysLeaseState(leasePath);
3288
- const state = stateRaw ? reapDeadTmuxExtendedKeysLeaseHolders(stateRaw) : null;
3289
- if (stateRaw && state?.holders.length === 0) {
3290
- execTmuxSync(["set-option", "-sq", "extended-keys", state.originalMode], execFileSyncImpl);
3291
- rmSync(leasePath, { force: true });
3787
+ fd = openSync(tempPath, "wx", 0o600);
3788
+ writeFileSync(fd, `${JSON.stringify(report)}\n`, "utf-8");
3789
+ try {
3790
+ fsyncSync(fd);
3791
+ }
3792
+ catch (error) {
3793
+ if (!(process.platform === "win32" && error.code === "EPERM"))
3794
+ throw error;
3795
+ }
3796
+ closeSync(fd);
3797
+ fd = undefined;
3798
+ renameSync(tempPath, path);
3799
+ const directoryFd = openSync(dirname(path), "r");
3800
+ try {
3801
+ try {
3802
+ fsyncSync(directoryFd);
3292
3803
  }
3293
- if (!state || state.holders.length === 0) {
3294
- const previousMode = execTmuxSync(["show-options", "-sv", "extended-keys"], execFileSyncImpl) ||
3295
- TMUX_EXTENDED_KEYS_FALLBACK_MODE;
3804
+ catch (error) {
3805
+ if (!(process.platform === "win32" && error.code === "EPERM"))
3806
+ throw error;
3807
+ }
3808
+ }
3809
+ finally {
3810
+ closeSync(directoryFd);
3811
+ }
3812
+ }
3813
+ finally {
3814
+ if (fd !== undefined)
3815
+ closeSync(fd);
3816
+ try {
3817
+ unlinkSync(tempPath);
3818
+ }
3819
+ catch { }
3820
+ }
3821
+ }
3822
+ function readDetachedLeaderReport(path) {
3823
+ try {
3824
+ const value = JSON.parse(readFileSync(path, "utf8"));
3825
+ if (!value || typeof value !== "object")
3826
+ return undefined;
3827
+ const report = value;
3828
+ if (report.version !== 1 || !["ready", "failed", "terminal", "release", "abort"].includes(String(report.kind)) ||
3829
+ typeof report.nonce !== "string" || typeof report.sessionId !== "string" || typeof report.sessionName !== "string")
3830
+ return undefined;
3831
+ if (report.paneId !== undefined && typeof report.paneId !== "string")
3832
+ return undefined;
3833
+ if (report.leaderPid !== undefined && (!Number.isSafeInteger(report.leaderPid) || Number(report.leaderPid) <= 0))
3834
+ return undefined;
3835
+ if (report.exitStatus !== undefined && (!Number.isSafeInteger(report.exitStatus) || Number(report.exitStatus) < 0 || Number(report.exitStatus) > 255))
3836
+ return undefined;
3837
+ return report;
3838
+ }
3839
+ catch {
3840
+ return undefined;
3841
+ }
3842
+ }
3843
+ /** Internal detached-launch seam. Exported solely for deterministic CLI tests. */
3844
+ export function publishDetachedReleaseMarker(releaseMarkerPath, nonce, sessionId, sessionName, leaderPid, hud) {
3845
+ writeDetachedLeaderReport(`${releaseMarkerPath}.release`, {
3846
+ version: 1, kind: "release", nonce, sessionId, sessionName, leaderPid, ...(hud ? { hud } : {}),
3847
+ });
3848
+ }
3849
+ /** Internal detached-launch seam. Exported solely for deterministic CLI tests. */
3850
+ export function applyDetachedTerminalExitStatus(releaseMarkerPath, expected) {
3851
+ const report = readDetachedLeaderReport(releaseMarkerPath);
3852
+ if (!report || report.kind !== "terminal" || report.finalized !== true)
3853
+ return false;
3854
+ if (report.nonce !== expected.nonce || report.sessionId !== expected.sessionId ||
3855
+ report.sessionName !== expected.sessionName || report.leaderPid !== expected.leaderPid)
3856
+ return false;
3857
+ if (typeof report.exitStatus !== "number")
3858
+ return false;
3859
+ process.exitCode = report.exitStatus;
3860
+ return true;
3861
+ }
3862
+ /** Internal detached-launch seam. Exported solely for deterministic CLI tests. */
3863
+ export function probeExactDetachedSessionExists(authority) {
3864
+ if (!authority)
3865
+ return undefined;
3866
+ try {
3867
+ // A retained dead pane (remain-on-exit) keeps its pid/topology, so prove
3868
+ // liveness first: a dead leader with no valid terminal report is a failure,
3869
+ // never a manual detach.
3870
+ const paneDead = execTmuxFileSync(["display-message", "-p", "-t", authority.paneId, "#{pane_dead}"], { encoding: "utf-8", stdio: ["ignore", "pipe", "ignore"] }).trim();
3871
+ if (paneDead === "1")
3872
+ return false;
3873
+ if (paneDead !== "0")
3874
+ return undefined;
3875
+ // Re-capture the exact leader-pane topology the bootstrap authority recorded.
3876
+ // A matching recapture proves the owned session and its leader survive (manual
3877
+ // detach); a destroyed session, recycled pane id, or tmux error fails closed.
3878
+ const current = captureDetachedLeaderAuthority(authority.paneId, authority.sessionName, authority.ownerId);
3879
+ return current.panePid === authority.panePid && current.sessionId === authority.sessionId
3880
+ && current.sessionCreated === authority.sessionCreated && current.windowId === authority.windowId
3881
+ && current.windowIndex === authority.windowIndex;
3882
+ }
3883
+ catch {
3884
+ return undefined;
3885
+ }
3886
+ }
3887
+ /** Internal detached-launch seam. Exported solely for deterministic CLI tests. */
3888
+ export function resolveDetachedAttachExitStatus(releaseMarkerPath, expected, probe) {
3889
+ if (applyDetachedTerminalExitStatus(releaseMarkerPath, expected))
3890
+ return "applied";
3891
+ return probe.exactSessionExists() === true ? "manual-detach" : "protocol-failure";
3892
+ }
3893
+ function publishDetachedAbortMarker(releaseMarkerPath, nonce, sessionId, sessionName, leaderPid) {
3894
+ writeDetachedLeaderReport(`${releaseMarkerPath}.abort`, { version: 1, kind: "abort", nonce, sessionId, sessionName, leaderPid });
3895
+ }
3896
+ function execTmuxSync(args, execFileSyncImpl = (file, tmuxArgs) => execFileSync(file, tmuxArgs, {
3897
+ encoding: "utf-8",
3898
+ ...(process.platform === "win32" ? { windowsHide: true } : {}),
3899
+ })) {
3900
+ return execFileSyncImpl(resolveTmuxExecutableForLaunch(), [...args]).trim();
3901
+ }
3902
+ export function acquireTmuxExtendedKeysLease(cwd, execFileSyncImpl = (file, tmuxArgs) => execFileSync(file, tmuxArgs, {
3903
+ encoding: "utf-8",
3904
+ ...(process.platform === "win32" ? { windowsHide: true } : {}),
3905
+ }), ownerPid = process.pid) {
3906
+ try {
3907
+ const socketPath = resolveTmuxSocketPath(execFileSyncImpl);
3908
+ const leasePath = tmuxExtendedKeysLeasePath(cwd, socketPath);
3909
+ const holderPid = Number.isSafeInteger(ownerPid) && ownerPid > 0 ? ownerPid : process.pid;
3910
+ const leaseId = `${holderPid}-${Date.now()}-${Math.random().toString(16).slice(2)}`;
3911
+ withTmuxExtendedKeysLeaseLock(cwd, socketPath, () => {
3912
+ const stateRaw = readTmuxExtendedKeysLeaseState(leasePath);
3913
+ const state = stateRaw ? reapDeadTmuxExtendedKeysLeaseHolders(stateRaw) : null;
3914
+ if (stateRaw && state?.holders.length === 0) {
3915
+ execTmuxSync(["set-option", "-sq", "extended-keys", state.originalMode], execFileSyncImpl);
3916
+ rmSync(leasePath, { force: true });
3917
+ }
3918
+ if (!state || state.holders.length === 0) {
3919
+ const previousMode = execTmuxSync(["show-options", "-sv", "extended-keys"], execFileSyncImpl) ||
3920
+ TMUX_EXTENDED_KEYS_FALLBACK_MODE;
3296
3921
  execTmuxSync(["set-option", "-sq", "extended-keys", TMUX_EXTENDED_KEYS_MODE], execFileSyncImpl);
3297
3922
  writeTmuxExtendedKeysLeaseState(leasePath, {
3298
3923
  originalMode: previousMode,
@@ -3352,19 +3977,123 @@ export function releaseTmuxExtendedKeysLease(cwd, leaseHandle, execFileSyncImpl
3352
3977
  }
3353
3978
  }
3354
3979
  }
3355
- function buildTmuxExtendedKeysHelperCommand(cwd, operation) {
3356
- const cwdLiteral = JSON.stringify(cwd);
3357
- const moduleUrlLiteral = JSON.stringify(import.meta.url);
3358
- const script = operation === "acquire"
3359
- ? `const mod = await import(${moduleUrlLiteral}); const ownerPid = Number.parseInt(process.argv[1] ?? "", 10); const lease = mod.acquireTmuxExtendedKeysLease(${cwdLiteral}, undefined, Number.isSafeInteger(ownerPid) && ownerPid > 0 ? ownerPid : undefined); if (lease) process.stdout.write(lease);`
3360
- : `const mod = await import(${moduleUrlLiteral}); mod.releaseTmuxExtendedKeysLease(${cwdLiteral}, process.argv[1] ?? "");`;
3361
- return `${quoteShellArg(process.execPath)} --input-type=module -e ${quoteShellArg(script)}`;
3362
- }
3363
- function buildTmuxExtendedKeysAcquireShellSnippet(cwd) {
3364
- return `OMX_TMUX_EXTENDED_KEYS_LEASE=$(${buildTmuxExtendedKeysHelperCommand(cwd, "acquire")} "$$" 2>/dev/null || true);`;
3980
+ export const DETACHED_LAUNCH_CONTROL_PLANE_KEYS = [
3981
+ "OMX_SESSION_ID",
3982
+ "CODEX_SESSION_ID",
3983
+ "SESSION_ID",
3984
+ "OMX_TMUX_HUD_OWNER",
3985
+ "OMX_TMUX_HUD_LEADER_PANE",
3986
+ "OMX_ROOT",
3987
+ "OMX_STATE_ROOT",
3988
+ "OMX_TEAM_STATE_ROOT",
3989
+ "OMXBOX_ACTIVE",
3990
+ "OMX_SOURCE_CWD",
3991
+ "OMX_MADMAX_DETACHED_CONTEXT",
3992
+ "OMX_RUNS_DIR",
3993
+ "OMX_TEAM_LEADER_CWD",
3994
+ "OMX_TEAM_WORKER",
3995
+ "OMX_TEAM_INTERNAL_WORKER",
3996
+ ];
3997
+ const DETACHED_LAUNCH_CONTROL_PLANE_KEY_SET = new Set(DETACHED_LAUNCH_CONTROL_PLANE_KEYS);
3998
+ function isDetachedLaunchControlPlaneKey(key, caseInsensitive = false) {
3999
+ return caseInsensitive
4000
+ ? DETACHED_LAUNCH_CONTROL_PLANE_KEY_SET.has(key.toUpperCase())
4001
+ : DETACHED_LAUNCH_CONTROL_PLANE_KEY_SET.has(key);
4002
+ }
4003
+ function emptyDetachedLaunchControlPlaneValues() {
4004
+ return Object.fromEntries(DETACHED_LAUNCH_CONTROL_PLANE_KEYS.map((key) => [key, ""]));
4005
+ }
4006
+ function resolveDetachedRootValue(cwd, raw) {
4007
+ return isCrossPlatformAbsolutePath(raw) ? raw : resolve(cwd, raw);
4008
+ }
4009
+ export function buildDetachedLaunchControlPlane(options) {
4010
+ const env = options.env ?? process.env;
4011
+ const values = emptyDetachedLaunchControlPlaneValues();
4012
+ values.OMX_SESSION_ID = options.sessionId;
4013
+ values.OMX_TMUX_HUD_OWNER = "1";
4014
+ values.OMX_TMUX_HUD_LEADER_PANE = options.leaderPaneId ?? "";
4015
+ const runtimeContext = options.runtimeContext;
4016
+ const verifiedMadmaxContext = options.verifiedMadmaxContext;
4017
+ if (runtimeContext && verifiedMadmaxContext) {
4018
+ values.OMX_ROOT = verifiedMadmaxContext.root;
4019
+ values.OMXBOX_ACTIVE = "1";
4020
+ values.OMX_SOURCE_CWD = verifiedMadmaxContext.sourceCwd;
4021
+ values.OMX_MADMAX_DETACHED_CONTEXT = verifiedMadmaxContext.context;
4022
+ values.OMX_RUNS_DIR = verifiedMadmaxContext.runsRoot;
4023
+ }
4024
+ else {
4025
+ const madmaxGuardPresent = env.OMXBOX_ACTIVE === "1";
4026
+ const teamRoot = env.OMX_TEAM_STATE_ROOT?.trim();
4027
+ const root = env.OMX_ROOT?.trim();
4028
+ const stateRoot = env.OMX_STATE_ROOT?.trim();
4029
+ const resolvedStateRoot = stateRoot ? resolveDetachedRootValue(options.cwd, stateRoot) : undefined;
4030
+ const explicitOverrideWins = Boolean(options.omxRootOverride?.trim()
4031
+ && !teamRoot
4032
+ && !root
4033
+ && (!resolvedStateRoot || resolveDetachedRootValue(options.cwd, options.omxRootOverride.trim()) !== resolvedStateRoot));
4034
+ if (teamRoot)
4035
+ values.OMX_TEAM_STATE_ROOT = resolveDetachedRootValue(options.cwd, teamRoot);
4036
+ if (!madmaxGuardPresent) {
4037
+ if (explicitOverrideWins)
4038
+ values.OMX_ROOT = resolveDetachedRootValue(options.cwd, options.omxRootOverride.trim());
4039
+ else if (!teamRoot && root)
4040
+ values.OMX_ROOT = resolveDetachedRootValue(options.cwd, root);
4041
+ else if (!teamRoot && stateRoot)
4042
+ values.OMX_STATE_ROOT = resolvedStateRoot;
4043
+ else if (!teamRoot && options.omxRootOverride?.trim())
4044
+ values.OMX_ROOT = resolveDetachedRootValue(options.cwd, options.omxRootOverride.trim());
4045
+ }
4046
+ if (verifiedMadmaxContext) {
4047
+ values.OMX_ROOT = verifiedMadmaxContext.root;
4048
+ values.OMX_STATE_ROOT = "";
4049
+ values.OMX_TEAM_STATE_ROOT = "";
4050
+ values.OMXBOX_ACTIVE = "1";
4051
+ values.OMX_SOURCE_CWD = verifiedMadmaxContext.sourceCwd;
4052
+ values.OMX_MADMAX_DETACHED_CONTEXT = verifiedMadmaxContext.context;
4053
+ values.OMX_RUNS_DIR = verifiedMadmaxContext.runsRoot;
4054
+ }
4055
+ }
4056
+ const verifiedTeam = options.verifiedTeamContext;
4057
+ if (verifiedTeam && !verifiedMadmaxContext) {
4058
+ values.OMX_ROOT = "";
4059
+ values.OMX_STATE_ROOT = "";
4060
+ values.OMXBOX_ACTIVE = "";
4061
+ values.OMX_SOURCE_CWD = "";
4062
+ values.OMX_MADMAX_DETACHED_CONTEXT = "";
4063
+ values.OMX_RUNS_DIR = "";
4064
+ values.OMX_TEAM_STATE_ROOT = verifiedTeam.stateRoot;
4065
+ values.OMX_TEAM_LEADER_CWD = verifiedTeam.leaderCwd;
4066
+ values.OMX_TEAM_WORKER = verifiedTeam.worker;
4067
+ values.OMX_TEAM_INTERNAL_WORKER = verifiedTeam.internalWorker;
4068
+ }
4069
+ return Object.freeze({
4070
+ values: Object.freeze(values),
4071
+ keys: DETACHED_LAUNCH_CONTROL_PLANE_KEYS,
4072
+ });
3365
4073
  }
3366
- function buildTmuxExtendedKeysReleaseShellSnippet(cwd) {
3367
- return `if [ -n "\${OMX_TMUX_EXTENDED_KEYS_LEASE:-}" ]; then ${buildTmuxExtendedKeysHelperCommand(cwd, "release")} "\${OMX_TMUX_EXTENDED_KEYS_LEASE}" >/dev/null 2>&1 || true; fi;`;
4074
+ export const resolveDetachedLaunchControlPlane = buildDetachedLaunchControlPlane;
4075
+ function detachedLaunchControlPlaneEnv(controlPlane) {
4076
+ return Object.fromEntries(DETACHED_LAUNCH_CONTROL_PLANE_KEYS.map((key) => [key, controlPlane.values[key]]));
4077
+ }
4078
+ function applyLaunchOwnedControlPlane(controlPlane) {
4079
+ const previous = new Map();
4080
+ for (const key of DETACHED_LAUNCH_CONTROL_PLANE_KEYS) {
4081
+ previous.set(key, process.env[key]);
4082
+ const value = controlPlane.values[key];
4083
+ if (value === "")
4084
+ delete process.env[key];
4085
+ else
4086
+ process.env[key] = value;
4087
+ }
4088
+ return () => {
4089
+ for (const key of DETACHED_LAUNCH_CONTROL_PLANE_KEYS) {
4090
+ const value = previous.get(key);
4091
+ if (typeof value === "string")
4092
+ process.env[key] = value;
4093
+ else
4094
+ delete process.env[key];
4095
+ }
4096
+ };
3368
4097
  }
3369
4098
  const SHELL_ENV_NAME_PATTERN = /^[A-Za-z_][A-Za-z0-9_]*$/;
3370
4099
  const DETACHED_SESSION_PANE_ENV_KEYS = new Set([
@@ -3376,21 +4105,23 @@ const DETACHED_SESSION_PANE_ENV_KEYS = new Set([
3376
4105
  "COLUMNS",
3377
4106
  "LINES",
3378
4107
  ]);
3379
- export function serializeDetachedSessionParentEnv(env) {
3380
- const lines = [];
4108
+ function detachedSessionParentEnvironment(env, caseInsensitive = false) {
4109
+ const values = {};
3381
4110
  for (const key of Object.keys(env).sort()) {
3382
- if (!SHELL_ENV_NAME_PATTERN.test(key))
3383
- continue;
3384
- if (DETACHED_SESSION_PANE_ENV_KEYS.has(key))
4111
+ if (!SHELL_ENV_NAME_PATTERN.test(key)
4112
+ || DETACHED_SESSION_PANE_ENV_KEYS.has(key)
4113
+ || isDetachedLaunchControlPlaneKey(key, caseInsensitive))
3385
4114
  continue;
3386
4115
  const value = env[key];
3387
- if (typeof value !== "string")
3388
- continue;
3389
- if (value.includes("\0"))
3390
- continue;
3391
- lines.push(`export ${key}=${quoteShellArg(value)}`);
4116
+ if (typeof value === "string" && !value.includes("\0"))
4117
+ values[key] = value;
3392
4118
  }
3393
- return `${lines.join("\n")}\n`;
4119
+ return values;
4120
+ }
4121
+ export function serializeDetachedSessionParentEnv(env) {
4122
+ return `${Object.entries(detachedSessionParentEnvironment(env))
4123
+ .map(([key, value]) => `export ${key}=${quoteShellArg(value)}`)
4124
+ .join("\n")}\n`;
3394
4125
  }
3395
4126
  export function detachedSessionParentEnvFilePath(cwd, sessionId) {
3396
4127
  const safeSessionId = sessionId.replace(/[^A-Za-z0-9_.-]/g, "_");
@@ -3418,27 +4149,21 @@ export function withTmuxExtendedKeys(cwd, run, execFileSyncImpl = (file, tmuxArg
3418
4149
  releaseTmuxExtendedKeysLease(cwd, leaseHandle, execFileSyncImpl);
3419
4150
  }
3420
4151
  }
3421
- export function buildDetachedSessionBootstrapSteps(sessionName, cwd, codexCmd, hudCmd, workerLaunchArgs, codexHomeOverride, notifyTempContractRaw, nativeWindows = false, sessionId, projectLocalCodexHomeForCleanup, runtimeCodexHomeForCleanup, omxRootOverride, env = process.env, sqliteHomeOverride, parentEnvFilePath, inheritedWorkerModel) {
4152
+ export function buildDetachedSessionBootstrapSteps(sessionName, cwd, codexCmd, hudCmd, workerLaunchArgs, codexHomeOverride, notifyTempContractRaw, nativeWindows = false, sessionId, projectLocalCodexHomeForCleanup, runtimeCodexHomeForCleanup, omxRootOverride, env = process.env, sqliteHomeOverride, parentEnvFilePath, inheritedWorkerModel, releaseMarkerPath, omxBin = process.argv[1], preLaunchOptions = {
4153
+ enableNotifyFallbackAuthority: false,
4154
+ worktreeDirty: false,
4155
+ shouldAttach: true,
4156
+ }, controlPlane) {
3422
4157
  const detachedLeaderCmd = nativeWindows
3423
- ? "powershell.exe"
3424
- : buildDetachedSessionLeaderCommand(cwd, sessionName, codexCmd, sessionId, codexHomeOverride, projectLocalCodexHomeForCleanup, runtimeCodexHomeForCleanup, parentEnvFilePath);
3425
- const resolvedEnvStateRoot = env.OMX_STATE_ROOT?.trim()
3426
- ? resolveLaunchPath(cwd, env.OMX_STATE_ROOT.trim())
3427
- : undefined;
3428
- const hasExplicitRootOverride = Boolean(env.OMX_ROOT?.trim()
3429
- || (omxRootOverride && omxRootOverride !== resolvedEnvStateRoot));
3430
- const hudRuntimeRoot = env.OMX_TEAM_STATE_ROOT?.trim()
3431
- ? resolveHudRuntimeRootForLaunch(cwd, env)
3432
- : hasExplicitRootOverride
3433
- ? {
3434
- omxRoot: omxRootOverride,
3435
- rootSource: resolveHudRuntimeRootSource(omxRootOverride, env),
3436
- }
3437
- : resolveHudRuntimeRootForLaunch(cwd, env);
3438
- const hudRuntimeEnv = buildHudRuntimeEnv({
3439
- sessionId,
3440
- ...hudRuntimeRoot,
3441
- }).env;
4158
+ ? buildDetachedWindowsLeaderCommand(cwd, sessionName, codexCmd, sessionId, codexHomeOverride, projectLocalCodexHomeForCleanup, runtimeCodexHomeForCleanup, env, releaseMarkerPath, preLaunchOptions, omxBin)
4159
+ : buildDetachedSessionLeaderCommand(cwd, sessionName, codexCmd, sessionId, codexHomeOverride, projectLocalCodexHomeForCleanup, runtimeCodexHomeForCleanup, parentEnvFilePath, releaseMarkerPath, preLaunchOptions, omxBin);
4160
+ const launchControlPlane = controlPlane ?? buildDetachedLaunchControlPlane({
4161
+ cwd,
4162
+ sessionId: sessionId ?? "",
4163
+ env,
4164
+ omxRootOverride,
4165
+ });
4166
+ const controlPlaneEnv = detachedLaunchControlPlaneEnv(launchControlPlane);
3442
4167
  const newSessionArgs = [
3443
4168
  "new-session",
3444
4169
  "-d",
@@ -3449,15 +4174,10 @@ export function buildDetachedSessionBootstrapSteps(sessionName, cwd, codexCmd, h
3449
4174
  sessionName,
3450
4175
  "-c",
3451
4176
  cwd,
4177
+ ...DETACHED_LAUNCH_CONTROL_PLANE_KEYS.flatMap((key) => ["-e", `${key}=${controlPlaneEnv[key]}`]),
3452
4178
  ...(workerLaunchArgs ? ["-e", `${TEAM_WORKER_LAUNCH_ARGS_ENV}=${workerLaunchArgs}`] : []),
3453
- ...Object.entries(hudRuntimeEnv).map(([key, value]) => ["-e", `${key}=${value}`]).flat(),
3454
4179
  ...(codexHomeOverride ? ["-e", `CODEX_HOME=${codexHomeOverride}`] : []),
3455
4180
  ...(sqliteHomeOverride ? ["-e", `${CODEX_SQLITE_HOME_ENV}=${sqliteHomeOverride}`] : []),
3456
- ...(env.OMXBOX_ACTIVE ? ["-e", `OMXBOX_ACTIVE=${env.OMXBOX_ACTIVE}`] : []),
3457
- ...(env.OMX_SOURCE_CWD ? ["-e", `OMX_SOURCE_CWD=${env.OMX_SOURCE_CWD}`] : []),
3458
- ...(env[OMX_MADMAX_DETACHED_CONTEXT_ENV]
3459
- ? ["-e", `${OMX_MADMAX_DETACHED_CONTEXT_ENV}=${env[OMX_MADMAX_DETACHED_CONTEXT_ENV]}`]
3460
- : []),
3461
4181
  ...(notifyTempContractRaw
3462
4182
  ? ["-e", `${OMX_NOTIFY_TEMP_CONTRACT_ENV}=${notifyTempContractRaw}`]
3463
4183
  : []),
@@ -3717,41 +4437,40 @@ function isAutopilotReviewPendingPostLaunchState(state) {
3717
4437
  function postLaunchUniqueStrings(values) {
3718
4438
  return [...new Set(values.map((value) => value.trim()).filter(Boolean))];
3719
4439
  }
3720
- async function scrubPostLaunchRootSkillActiveForSession(stateDir, sessionId, nowIso, writeFileFn, rootStateBeforeCleanup) {
4440
+ async function scrubPostLaunchRootSkillActiveForSession(stateDir, sessionId, nowIso, writeRootStateFn) {
3721
4441
  const normalizedSessionId = cleanPostLaunchString(sessionId);
3722
4442
  if (!normalizedSessionId)
3723
4443
  return;
3724
- const { rootPath } = getSkillActiveStatePathsForStateDir(stateDir);
3725
- const rootState = rootStateBeforeCleanup ?? await readSkillActiveState(rootPath);
3726
- if (!rootState)
3727
- return;
3728
- const rootSessionIds = postLaunchUniqueStrings([
3729
- cleanPostLaunchString(rootState.session_id),
3730
- cleanPostLaunchString(extractSessionIdFromInitializedStatePath(rootState.initialized_state_path)),
3731
- ]);
3732
- const rootBelongsToSession = rootSessionIds.includes(normalizedSessionId);
3733
- const entries = listActiveSkills(rootState);
3734
- const keptEntries = entries.filter((entry) => {
3735
- const entrySessionId = cleanPostLaunchString(entry.session_id);
3736
- if (entrySessionId)
3737
- return entrySessionId !== normalizedSessionId;
3738
- return !rootBelongsToSession;
4444
+ await writeRootStateFn(stateDir, (rootState) => {
4445
+ if (!rootState)
4446
+ return null;
4447
+ const rootSessionIds = postLaunchUniqueStrings([
4448
+ cleanPostLaunchString(rootState.session_id),
4449
+ cleanPostLaunchString(extractSessionIdFromInitializedStatePath(rootState.initialized_state_path)),
4450
+ ]);
4451
+ const rootBelongsToSession = rootSessionIds.includes(normalizedSessionId);
4452
+ const entries = listActiveSkills(rootState);
4453
+ const keptEntries = entries.filter((entry) => {
4454
+ const entrySessionId = cleanPostLaunchString(entry.session_id);
4455
+ if (entrySessionId)
4456
+ return entrySessionId !== normalizedSessionId;
4457
+ return !rootBelongsToSession;
4458
+ });
4459
+ if (keptEntries.length === entries.length && rootState.active !== true)
4460
+ return null;
4461
+ if (keptEntries.length === entries.length && !rootBelongsToSession)
4462
+ return null;
4463
+ return {
4464
+ ...rootState,
4465
+ active: keptEntries.length > 0,
4466
+ skill: keptEntries[0]?.skill ?? (keptEntries.length > 0 ? cleanPostLaunchString(rootState.skill) : ""),
4467
+ phase: keptEntries[0]?.phase ?? (keptEntries.length > 0 ? cleanPostLaunchString(rootState.phase) : "complete"),
4468
+ updated_at: nowIso,
4469
+ active_skills: keptEntries,
4470
+ post_launch_reconciled_at: nowIso,
4471
+ post_launch_reconciliation_reason: "terminal_session_cleanup",
4472
+ };
3739
4473
  });
3740
- if (keptEntries.length === entries.length && rootState.active !== true)
3741
- return;
3742
- if (keptEntries.length === entries.length && !rootBelongsToSession)
3743
- return;
3744
- const nextRoot = {
3745
- ...rootState,
3746
- active: keptEntries.length > 0,
3747
- skill: keptEntries[0]?.skill ?? (keptEntries.length > 0 ? cleanPostLaunchString(rootState.skill) : ""),
3748
- phase: keptEntries[0]?.phase ?? (keptEntries.length > 0 ? cleanPostLaunchString(rootState.phase) : "complete"),
3749
- updated_at: nowIso,
3750
- active_skills: keptEntries,
3751
- post_launch_reconciled_at: nowIso,
3752
- post_launch_reconciliation_reason: "terminal_session_cleanup",
3753
- };
3754
- await writeFileFn(rootPath, JSON.stringify(nextRoot, null, 2));
3755
4474
  }
3756
4475
  function buildRecoveredPostLaunchModeState(mode, completedAt) {
3757
4476
  return {
@@ -3798,9 +4517,14 @@ export async function cleanupPostLaunchModeStateFiles(cwd, sessionId, dependenci
3798
4517
  ? [getStateDir(cwd, sessionId)]
3799
4518
  : [getBaseStateDir(cwd)];
3800
4519
  const rootStateDir = getBaseStateDir(cwd);
3801
- const rootSkillActiveStateBeforeCleanup = sessionId
3802
- ? await readSkillActiveState(getSkillActiveStatePathsForStateDir(rootStateDir).rootPath)
3803
- : null;
4520
+ const writeRootState = dependencies.writeRootState ?? ((stateDir, update) => (updateRootSkillActiveStateForStateDir(stateDir, update)));
4521
+ const writeModeState = async (stateDir, mode, path, state) => {
4522
+ if (stateDir === rootStateDir && mode === SKILL_ACTIVE_STATE_MODE) {
4523
+ await writeRootState(stateDir, () => state);
4524
+ return;
4525
+ }
4526
+ await writeFile(path, JSON.stringify(state, null, 2));
4527
+ };
3804
4528
  let preserveSkillActiveForReviewPendingAutopilot = false;
3805
4529
  for (const stateDir of scopedDirs) {
3806
4530
  const files = await readdir(stateDir).catch(() => []);
@@ -3812,7 +4536,7 @@ export async function cleanupPostLaunchModeStateFiles(cwd, sessionId, dependenci
3812
4536
  && isAutopilotReviewPendingPostLaunchState(autopilotPrecheck.state);
3813
4537
  preserveSkillActiveForReviewPendingAutopilot ||= preserveReviewPendingAutopilot;
3814
4538
  for (const file of files) {
3815
- if (!file.endsWith("-state.json") || file === "session.json")
4539
+ if (!isModeStateFilename(file))
3816
4540
  continue;
3817
4541
  const path = join(stateDir, file);
3818
4542
  const mode = file.slice(0, -"-state.json".length);
@@ -3821,9 +4545,10 @@ export async function cleanupPostLaunchModeStateFiles(cwd, sessionId, dependenci
3821
4545
  if (result.kind === "recoverable") {
3822
4546
  try {
3823
4547
  const completedAt = now().toISOString();
3824
- await writeFile(path, JSON.stringify(mode === SKILL_ACTIVE_STATE_MODE
4548
+ const recoveredState = mode === SKILL_ACTIVE_STATE_MODE
3825
4549
  ? buildRecoveredPostLaunchSkillActiveState(completedAt)
3826
- : buildRecoveredPostLaunchModeState(mode, completedAt), null, 2));
4550
+ : buildRecoveredPostLaunchModeState(mode, completedAt);
4551
+ await writeModeState(stateDir, mode, path, recoveredState);
3827
4552
  if (isTrackedWorkflowMode(mode)) {
3828
4553
  await syncCanonicalSkillStateForMode({
3829
4554
  cwd,
@@ -3856,12 +4581,12 @@ export async function cleanupPostLaunchModeStateFiles(cwd, sessionId, dependenci
3856
4581
  : normalizeTerminalWorkflowState(result.state, { mode, nowIso: completedAt });
3857
4582
  if (normalized.changed) {
3858
4583
  result.state = normalized.state;
3859
- await writeFile(path, JSON.stringify(result.state, null, 2));
4584
+ await writeModeState(stateDir, mode, path, result.state);
3860
4585
  }
3861
4586
  if (mode === "ralph") {
3862
4587
  const completedAt = now().toISOString();
3863
4588
  if (markRalphCompletionAuditBlockedForPostLaunch(result.state, cwd, completedAt)) {
3864
- await writeFile(path, JSON.stringify(result.state, null, 2));
4589
+ await writeModeState(stateDir, mode, path, result.state);
3865
4590
  await syncCanonicalSkillStateForMode({
3866
4591
  cwd,
3867
4592
  baseStateDir: rootStateDir,
@@ -3887,7 +4612,7 @@ export async function cleanupPostLaunchModeStateFiles(cwd, sessionId, dependenci
3887
4612
  result.state.phase = "complete";
3888
4613
  result.state.updated_at = completedAt;
3889
4614
  result.state.active_skills = [];
3890
- await writeFile(path, JSON.stringify(result.state, null, 2));
4615
+ await writeModeState(stateDir, mode, path, result.state);
3891
4616
  continue;
3892
4617
  }
3893
4618
  result.state.active = false;
@@ -3898,7 +4623,7 @@ export async function cleanupPostLaunchModeStateFiles(cwd, sessionId, dependenci
3898
4623
  result.state.interrupted_at = completedAt;
3899
4624
  result.state.stop_reason = cleanPostLaunchString(result.state.stop_reason) || "session_exit";
3900
4625
  }
3901
- await writeFile(path, JSON.stringify(result.state, null, 2));
4626
+ await writeModeState(stateDir, mode, path, result.state);
3902
4627
  if (isTrackedWorkflowMode(mode)) {
3903
4628
  await syncCanonicalSkillStateForMode({
3904
4629
  cwd,
@@ -3920,7 +4645,7 @@ export async function cleanupPostLaunchModeStateFiles(cwd, sessionId, dependenci
3920
4645
  if (sessionId) {
3921
4646
  try {
3922
4647
  if (!preserveSkillActiveForReviewPendingAutopilot) {
3923
- await scrubPostLaunchRootSkillActiveForSession(rootStateDir, sessionId, now().toISOString(), writeFile, rootSkillActiveStateBeforeCleanup);
4648
+ await scrubPostLaunchRootSkillActiveForSession(rootStateDir, sessionId, now().toISOString(), writeRootState);
3924
4649
  }
3925
4650
  }
3926
4651
  catch (err) {
@@ -3946,115 +4671,140 @@ export async function reapPostLaunchOrphanedMcpProcesses(dependencies = {}) {
3946
4671
  writeError(`[cli/index] postLaunch MCP cleanup failed: ${err}\n`);
3947
4672
  }
3948
4673
  }
3949
- /**
3950
- * preLaunch: Prepare environment before Codex starts.
3951
- * 1. Best-effort launch-safe orphan cleanup for detached OMX MCP processes
3952
- * 2. Establish the canonical session pointer
3953
- * 3. Generate session-scoped launch artifacts and start best-effort helpers
3954
- *
3955
- * Automatic broad stale-session cleanup remains disabled here. Only detached
3956
- * OMX MCP processes without a live Codex ancestor are reaped so new launches
3957
- * do not accumulate stale processes from prior crashed/closed sessions.
3958
- */
3959
- export async function preLaunch(cwd, sessionId, notifyTempContract, codexHomeOverride, enableNotifyFallbackAuthority = false, worktreeDirty = false) {
3960
- // 1. Best-effort launch-safe orphan cleanup
4674
+ async function establishPreLaunchBinding(cwd, sessionId, metadata = {}) {
4675
+ const pointerOptions = await resolvePreLaunchSessionPointerOptions();
4676
+ const established = await establishLaunchSessionBinding(cwd, sessionId, { ...pointerOptions, ...metadata });
4677
+ if (established.kind === "committed-released")
4678
+ return { binding: established.binding, cleanup: established.cleanup };
4679
+ const error = established.kind === "precommit-aborted" ? established.abort : established.error;
4680
+ Object.assign(error, { establishmentCleanup: established.cleanup });
4681
+ throw error;
4682
+ }
4683
+ async function failPreLaunchSetup(binding) {
4684
+ const failures = [];
4685
+ try {
4686
+ await finalizeBoundOnce(binding, "setup-failure");
4687
+ }
4688
+ catch (error) {
4689
+ failures.push(error);
4690
+ }
4691
+ finally {
4692
+ try {
4693
+ const close = await closeLaunchSessionBindingOnce(binding);
4694
+ if (close.status === "failed")
4695
+ failures.push(close.error ?? new Error("bound session close failed"));
4696
+ }
4697
+ catch (error) {
4698
+ failures.push(error);
4699
+ }
4700
+ }
4701
+ if (failures.length > 0)
4702
+ throw new AggregateError(failures, "preLaunch setup finalization failed");
4703
+ }
4704
+ async function completePreLaunchSetup(cwd, sessionId, notifyTempContract, codexHomeOverride, enableNotifyFallbackAuthority, worktreeDirty) {
4705
+ const degraded = [];
3961
4706
  try {
3962
4707
  const cleanup = await cleanupLaunchOrphanedMcpProcesses();
3963
- if (cleanup.terminatedCount > 0) {
4708
+ if (cleanup.terminatedCount > 0)
3964
4709
  console.log(`[omx] Reaped ${cleanup.terminatedCount} orphaned OMX MCP process(es) before launch.`);
3965
- }
3966
- if (cleanup.failedPids.length > 0) {
4710
+ if (cleanup.failedPids.length > 0)
3967
4711
  console.warn(`[omx] Failed to reap ${cleanup.failedPids.length} orphaned OMX MCP process(es); continuing launch.`);
3968
- }
3969
4712
  }
3970
- catch (err) {
3971
- logCliOperationFailure(err);
3972
- // Non-fatal
4713
+ catch (error) {
4714
+ degraded.push("orphan-reaping");
4715
+ logCliOperationFailure(error);
4716
+ }
4717
+ let instructions;
4718
+ try {
4719
+ const orchestrationMode = await resolveSessionOrchestrationMode(cwd, sessionId);
4720
+ const overlay = await generateOverlay(cwd, sessionId, { orchestrationMode });
4721
+ const launchAppendix = await readLaunchAppendInstructions();
4722
+ const dirtyWorktreeGuidance = worktreeDirty
4723
+ ? `\n\n## Session start: dirty worktree detected\n\nThis worktree has uncommitted changes that were present when the session launched.\nBefore executing the requested task, resolve the dirty state first:\n1. Review uncommitted changes with \`git status\` and \`git diff\`.\n2. Commit, stash, or discard changes as appropriate.\n3. Then proceed with the original task.`
4724
+ : "";
4725
+ instructions = launchAppendix.trim().length > 0 ? `${overlay}\n\n${launchAppendix}${dirtyWorktreeGuidance}` : `${overlay}${dirtyWorktreeGuidance}`;
4726
+ }
4727
+ catch (error) {
4728
+ return { kind: "failure", operation: "overlay", error };
4729
+ }
4730
+ try {
4731
+ await writeSessionModelInstructionsFile(cwd, sessionId, instructions);
4732
+ }
4733
+ catch (error) {
4734
+ return { kind: "failure", operation: "session-instructions", error };
4735
+ }
4736
+ try {
4737
+ await resetSessionMetrics(cwd, sessionId);
4738
+ tagCurrentTmuxSessionWithInstance(sessionId);
4739
+ }
4740
+ catch (error) {
4741
+ return { kind: "failure", operation: "session-metrics", error };
3973
4742
  }
3974
- // 2. Establish the canonical pointer before any session-scoped launch artifact.
3975
- await writeSessionStart(cwd, sessionId, await resolvePreLaunchSessionPointerOptions());
3976
- // 3. Generate runtime overlay + write session-scoped model instructions file
3977
- const orchestrationMode = await resolveSessionOrchestrationMode(cwd, sessionId);
3978
- const overlay = await generateOverlay(cwd, sessionId, { orchestrationMode });
3979
- const launchAppendix = await readLaunchAppendInstructions();
3980
- const dirtyWorktreeGuidance = worktreeDirty
3981
- ? `\n\n## Session start: dirty worktree detected\n\nThis worktree has uncommitted changes that were present when the session launched.\nBefore executing the requested task, resolve the dirty state first:\n1. Review uncommitted changes with \`git status\` and \`git diff\`.\n2. Commit, stash, or discard changes as appropriate.\n3. Then proceed with the original task.`
3982
- : "";
3983
- const sessionInstructions = launchAppendix.trim().length > 0
3984
- ? `${overlay}
3985
-
3986
- ${launchAppendix}${dirtyWorktreeGuidance}`
3987
- : `${overlay}${dirtyWorktreeGuidance}`;
3988
- await writeSessionModelInstructionsFile(cwd, sessionId, sessionInstructions);
3989
- // 4. Reset session metrics and tag the established session.
3990
- await resetSessionMetrics(cwd, sessionId);
3991
- tagCurrentTmuxSessionWithInstance(sessionId);
3992
- // 5. Start notify fallback watcher (best effort)
3993
4743
  try {
3994
4744
  await startNotifyFallbackWatcher(cwd, { codexHomeOverride, enableAuthority: enableNotifyFallbackAuthority, sessionId });
3995
4745
  }
3996
- catch (err) {
3997
- logCliOperationFailure(err);
3998
- // Non-fatal
4746
+ catch (error) {
4747
+ degraded.push("notify-watcher");
4748
+ logCliOperationFailure(error);
3999
4749
  }
4000
- // 6. Start derived watcher (best effort, opt-in)
4001
4750
  try {
4002
4751
  await startHookDerivedWatcher(cwd);
4003
4752
  }
4004
- catch (err) {
4005
- logCliOperationFailure(err);
4006
- // Non-fatal
4753
+ catch (error) {
4754
+ degraded.push("derived-watcher");
4755
+ logCliOperationFailure(error);
4007
4756
  }
4008
- // 7. Emit temp notification startup summary + warnings, then send session-start lifecycle notification (best effort)
4009
4757
  try {
4010
4758
  if (notifyTempContract?.active) {
4011
- process.env[OMX_NOTIFY_TEMP_CONTRACT_ENV] =
4012
- serializeNotifyTempContract(notifyTempContract);
4759
+ process.env[OMX_NOTIFY_TEMP_CONTRACT_ENV] = serializeNotifyTempContract(notifyTempContract);
4013
4760
  const { getNotificationConfig } = await import("../notifications/config.js");
4014
- const resolved = getNotificationConfig();
4015
- const startup = buildNotifyTempStartupMessages(notifyTempContract, Boolean(resolved?.enabled));
4016
- for (const info of startup.infoLines) {
4761
+ const startup = buildNotifyTempStartupMessages(notifyTempContract, Boolean(getNotificationConfig()?.enabled));
4762
+ for (const info of startup.infoLines)
4017
4763
  console.log(`[omx] ${info}`);
4018
- }
4019
- for (const warning of startup.warningLines) {
4764
+ for (const warning of startup.warningLines)
4020
4765
  console.warn(`[omx] ${warning}`);
4021
- }
4022
4766
  }
4023
- else {
4767
+ else
4024
4768
  delete process.env[OMX_NOTIFY_TEMP_CONTRACT_ENV];
4025
- }
4026
4769
  const { notifyLifecycle } = await import("../notifications/index.js");
4027
- await notifyLifecycle("session-start", {
4028
- sessionId,
4029
- projectPath: cwd,
4030
- projectName: basename(cwd),
4031
- });
4770
+ await notifyLifecycle("session-start", { sessionId, projectPath: cwd, projectName: basename(cwd) });
4032
4771
  }
4033
- catch (err) {
4034
- logCliOperationFailure(err);
4035
- // Non-fatal: notification failures must never block launch
4772
+ catch (error) {
4773
+ degraded.push("notification");
4774
+ logCliOperationFailure(error);
4036
4775
  }
4037
- // 8. Dispatch native hook event (best effort)
4038
4776
  try {
4039
- await emitNativeHookEvent(cwd, "session-start", {
4040
- session_id: sessionId,
4041
- context: buildNativeHookBaseContext(cwd, sessionId, "started", {
4042
- project_path: cwd,
4043
- project_name: basename(cwd),
4044
- status: "started",
4045
- }),
4046
- });
4777
+ await emitNativeHookEvent(cwd, "session-start", { session_id: sessionId, context: buildNativeHookBaseContext(cwd, sessionId, "started", { project_path: cwd, project_name: basename(cwd), status: "started" }) });
4047
4778
  }
4048
- catch (err) {
4049
- logCliOperationFailure(err);
4050
- // Non-fatal
4779
+ catch (error) {
4780
+ degraded.push("native-lifecycle-event");
4781
+ logCliOperationFailure(error);
4782
+ }
4783
+ return degraded.length === 0 ? { kind: "success" } : { kind: "degraded-success", operations: degraded };
4784
+ }
4785
+ export async function preLaunch(cwd, sessionId, notifyTempContract, codexHomeOverride, enableNotifyFallbackAuthority = false, worktreeDirty = false) {
4786
+ const established = await establishPreLaunchBinding(cwd, sessionId);
4787
+ const completion = {
4788
+ ...await completePreLaunchSetup(cwd, sessionId, notifyTempContract, codexHomeOverride, enableNotifyFallbackAuthority, worktreeDirty),
4789
+ establishmentCleanup: established.cleanup,
4790
+ };
4791
+ if (completion.kind === "failure") {
4792
+ try {
4793
+ await failPreLaunchSetup(established.binding);
4794
+ }
4795
+ catch (cleanupError) {
4796
+ throw new AggregateError([completion.error, cleanupError], `preLaunch ${completion.operation} failed`);
4797
+ }
4798
+ throw completion.error;
4051
4799
  }
4800
+ return { binding: established.binding, completion };
4052
4801
  }
4053
4802
  /**
4054
4803
  * runCodex: Launch Codex CLI (blocks until exit).
4055
4804
  * All 3 paths (new tmux, existing tmux, no tmux) block via execSync/execFileSync.
4056
4805
  */
4057
- function runCodex(cwd, args, sessionId, workerDefaultModel, codexHomeOverride, sqliteHomeOverride, notifyTempContractRaw, explicitLaunchPolicy, projectLocalCodexHomeForCleanup, runtimeCodexHomeForCleanup, runtimeContext) {
4806
+ async function runCodex(cwd, args, sessionId, workerDefaultModel, codexHomeOverride, sqliteHomeOverride, notifyTempContractRaw, preLaunchOptions, launchPolicy = "direct", detachedPreflight, projectLocalCodexHomeForCleanup, runtimeCodexHomeForCleanup, runtimeContext, verifiedMadmaxContext, launchControlPlane) {
4807
+ void preLaunchOptions;
4058
4808
  const launchArgs = injectModelInstructionsBypassArgs(cwd, args, process.env, sessionModelInstructionsPath(cwd, sessionId));
4059
4809
  const nativeWindows = isNativeWindows();
4060
4810
  const omxBin = resolveOmxCliEntryPath({ argv1: process.argv[1], cwd, env: process.env });
@@ -4062,19 +4812,59 @@ function runCodex(cwd, args, sessionId, workerDefaultModel, codexHomeOverride, s
4062
4812
  throw new Error("Unable to resolve OMX launcher path for tmux HUD bootstrap");
4063
4813
  }
4064
4814
  const runtimeEnvOverlay = buildMadmaxWorktreeRuntimeEnvOverlay(runtimeContext);
4815
+ let verifiedTeamContext;
4816
+ if (launchPolicy === "detached-tmux") {
4817
+ try {
4818
+ verifiedTeamContext = await resolveVerifiedDetachedTeamContext(cwd, process.env);
4819
+ }
4820
+ catch {
4821
+ verifiedTeamContext = undefined;
4822
+ }
4823
+ }
4824
+ const verifiedMadmaxContextForLaunch = verifiedMadmaxContext ?? (runtimeContext?.madmaxDetachedContext
4825
+ ? {
4826
+ root: runtimeContext.omxRoot,
4827
+ sourceCwd: runtimeContext.sourceCwd,
4828
+ context: runtimeContext.madmaxDetachedContext,
4829
+ runsRoot: resolveMadmaxRunsRoot(process.env),
4830
+ }
4831
+ : resolveVerifiedMadmaxDetachedContext(cwd, launchArgs, process.env));
4832
+ const detachedControlPlane = launchPolicy === "detached-tmux"
4833
+ ? buildDetachedLaunchControlPlane({
4834
+ cwd,
4835
+ sessionId,
4836
+ env: process.env,
4837
+ omxRootOverride: runtimeContext?.omxRoot ?? resolveOmxRootForLaunch(cwd, process.env),
4838
+ runtimeContext,
4839
+ verifiedMadmaxContext: verifiedMadmaxContextForLaunch,
4840
+ verifiedTeamContext,
4841
+ })
4842
+ : undefined;
4843
+ const launchOwnedControlPlane = detachedControlPlane ?? launchControlPlane;
4844
+ const launchOwnedControlPlaneEnv = launchOwnedControlPlane
4845
+ ? detachedLaunchControlPlaneEnv(launchOwnedControlPlane)
4846
+ : {};
4847
+ const detachedSelectedStateEnv = detachedControlPlane
4848
+ ? { ...process.env, ...detachedLaunchControlPlaneEnv(detachedControlPlane) }
4849
+ : undefined;
4065
4850
  const omxRootOverride = runtimeContext?.omxRoot ?? resolveOmxRootForLaunch(cwd, process.env);
4851
+ const selectedOmxRootOverride = launchOwnedControlPlane
4852
+ ? launchOwnedControlPlane.values.OMX_ROOT || undefined
4853
+ : omxRootOverride;
4066
4854
  const currentPaneId = process.env.TMUX_PANE;
4067
4855
  const hudRuntimeRoot = runtimeContext
4068
4856
  ? { omxRoot: runtimeContext.omxRoot, rootSource: 'omx-root-env' }
4069
4857
  : resolveHudRuntimeRootForLaunch(cwd, process.env);
4070
- const hudRuntimeEnv = {
4071
- ...buildHudRuntimeEnv({
4072
- sessionId,
4073
- leaderPaneId: currentPaneId,
4074
- ...hudRuntimeRoot,
4075
- }).env,
4076
- ...runtimeEnvOverlay,
4077
- };
4858
+ const hudRuntimeEnv = launchOwnedControlPlane
4859
+ ? Object.fromEntries(Object.entries(launchOwnedControlPlaneEnv))
4860
+ : {
4861
+ ...buildHudRuntimeEnv({
4862
+ sessionId,
4863
+ leaderPaneId: currentPaneId,
4864
+ ...hudRuntimeRoot,
4865
+ }).env,
4866
+ ...runtimeEnvOverlay,
4867
+ };
4078
4868
  const hudEnvArgs = Object.entries(hudRuntimeEnv).map(([key, value]) => `${key}=${value}`);
4079
4869
  const hudCmd = nativeWindows
4080
4870
  ? buildWindowsPromptCommand("node", [omxBin, "hud", "--watch"])
@@ -4089,13 +4879,17 @@ function runCodex(cwd, args, sessionId, workerDefaultModel, codexHomeOverride, s
4089
4879
  ...stripHermesMcpBridgeEnv(process.env),
4090
4880
  ...(codexHomeOverride ? { CODEX_HOME: codexHomeOverride } : {}),
4091
4881
  ...(sqliteHomeOverride ? { [CODEX_SQLITE_HOME_ENV]: sqliteHomeOverride } : {}),
4092
- ...(omxRootOverride ? { OMX_ROOT: omxRootOverride } : {}),
4882
+ ...(!launchOwnedControlPlane && omxRootOverride ? { OMX_ROOT: omxRootOverride } : {}),
4093
4883
  ...runtimeEnvOverlay,
4884
+ ...launchOwnedControlPlaneEnv,
4094
4885
  }, omxBin);
4886
+ // Correlates plugin hook routing only; it is not an authority token.
4887
+ const pluginHookRoutingLaunchId = randomUUID();
4095
4888
  const codexEnvWithSession = {
4096
4889
  ...codexBaseEnv,
4097
- OMX_CODEX_LAUNCH_ID: randomUUID(),
4890
+ OMX_CODEX_LAUNCH_ID: pluginHookRoutingLaunchId,
4098
4891
  ...buildHudRuntimeEnv({ sessionId }).env,
4892
+ ...launchOwnedControlPlaneEnv,
4099
4893
  };
4100
4894
  const codexEnv = workerLaunchArgs
4101
4895
  ? {
@@ -4107,8 +4901,9 @@ function runCodex(cwd, args, sessionId, workerDefaultModel, codexHomeOverride, s
4107
4901
  const codexEnvWithNotify = notifyTempContractRaw
4108
4902
  ? { ...codexEnv, [OMX_NOTIFY_TEMP_CONTRACT_ENV]: notifyTempContractRaw }
4109
4903
  : codexEnv;
4110
- const runtimeHookEnv = { ...process.env, ...runtimeEnvOverlay };
4111
- const { launchPolicy } = resolveTmuxAwareLaunchPolicy(explicitLaunchPolicy, nativeWindows);
4904
+ const runtimeHookEnv = launchOwnedControlPlane
4905
+ ? { ...process.env, ...runtimeEnvOverlay, ...launchOwnedControlPlaneEnv }
4906
+ : { ...process.env, ...runtimeEnvOverlay };
4112
4907
  if (isCodexVersionRequest(launchArgs)) {
4113
4908
  runCodexBlocking(cwd, launchArgs, codexEnvWithNotify);
4114
4909
  return { postLaunchHandledExternally: false };
@@ -4144,7 +4939,7 @@ function runCodex(cwd, args, sessionId, workerDefaultModel, codexHomeOverride, s
4144
4939
  currentPaneId,
4145
4940
  cwd,
4146
4941
  sessionId,
4147
- omxRootOverride,
4942
+ omxRootOverride: selectedOmxRootOverride,
4148
4943
  baseEnv: runtimeHookEnv,
4149
4944
  });
4150
4945
  }
@@ -4171,7 +4966,7 @@ function runCodex(cwd, args, sessionId, workerDefaultModel, codexHomeOverride, s
4171
4966
  currentPaneId,
4172
4967
  cwd,
4173
4968
  sessionId,
4174
- omxRootOverride,
4969
+ omxRootOverride: selectedOmxRootOverride,
4175
4970
  baseEnv: runtimeHookEnv,
4176
4971
  });
4177
4972
  }
@@ -4232,214 +5027,251 @@ function runCodex(cwd, args, sessionId, workerDefaultModel, codexHomeOverride, s
4232
5027
  return { postLaunchHandledExternally: false };
4233
5028
  }
4234
5029
  else {
4235
- // Not in tmux: create a new tmux session with codex + HUD pane
5030
+ if (!detachedPreflight) {
5031
+ throw new DetachedLaunchSafetyError("establishment", new Error("detached launch requires a frozen available preflight"), { transitions: ["D0"], rollback: { attempted: [], failures: [] } });
5032
+ }
4236
5033
  const codexCmd = buildTmuxPaneCommand("codex", launchArgs);
4237
5034
  const detachedWindowsCodexCmd = nativeWindows
4238
- ? buildWindowsPromptCommand("codex", launchArgs)
5035
+ ? buildWindowsDetachedChildCommand("codex", launchArgs)
4239
5036
  : null;
4240
5037
  const sessionName = buildDetachedTmuxSessionName(cwd, sessionId);
4241
- const launchDetachedSession = () => {
4242
- const contextKey = runtimeContext?.madmaxDetachedContext ?? process.env[OMX_MADMAX_DETACHED_CONTEXT_ENV]?.trim();
4243
- const runsRoot = resolveMadmaxRunsRoot(process.env);
4244
- const activeRecordPath = contextKey
4245
- ? madmaxDetachedActiveRecordPath(runsRoot, contextKey)
4246
- : null;
4247
- const activeRecord = activeRecordPath
4248
- ? readMadmaxDetachedActiveRecord(activeRecordPath)
4249
- : null;
4250
- if (activeRecord &&
4251
- activeRecord.context_key === contextKey &&
4252
- isReusableMadmaxDetachedActiveRecord(activeRecord)) {
4253
- cleanupCurrentMadmaxReuseRunRoot(process.env, runsRoot);
4254
- setDetachedTmuxSessionHistoryLimit(activeRecord.tmux_session_name, activeRecord.tmux_pane_id);
4255
- if (!shouldAttachDetachedTmuxSession(process.env)) {
4256
- clearDetachedTmuxSessionHistoryIfUnattached(activeRecord.tmux_session_name, activeRecord.tmux_pane_id);
4257
- process.stderr.write(`[omx] madmax detached launch already active for this context; reusing ${activeRecord.tmux_session_name} without attaching because this launch is a Hermes MCP bridge.\n`);
4258
- return { postLaunchHandledExternally: true };
4259
- }
4260
- process.stderr.write(`[omx] madmax detached launch already active for this context; attaching ${activeRecord.tmux_session_name} instead of starting a duplicate.\n`);
4261
- try {
4262
- execTmuxFileSync(["attach-session", "-t", activeRecord.tmux_session_name], {
4263
- stdio: "inherit",
4264
- });
4265
- }
4266
- catch (err) {
4267
- logCliOperationFailure(err);
4268
- throw new MadmaxDetachedReuseError(`refusing duplicate madmax detached launch: existing session ${activeRecord.tmux_session_name} is active but attach failed`);
4269
- }
4270
- return { postLaunchHandledExternally: true };
4271
- }
4272
- if (activeRecordPath && activeRecord) {
4273
- rmSync(activeRecordPath, { force: true });
4274
- }
4275
- let detachedSessionBindingWrite = Promise.resolve();
4276
- const writeDetachedSessionBinding = (tmuxPaneId) => {
4277
- detachedSessionBindingWrite = detachedSessionBindingWrite
4278
- .catch((err) => {
4279
- logCliOperationFailure(err);
4280
- })
4281
- .then(() => writeSessionStart(cwd, sessionId, {
4282
- tmuxSessionName: sessionName,
4283
- ...(tmuxPaneId ? { tmuxPaneId } : {}),
4284
- }));
4285
- void detachedSessionBindingWrite.catch((err) => {
4286
- logCliOperationFailure(err);
4287
- // Non-fatal: managed tmux recovery can still use compatibility fallback.
4288
- });
4289
- };
4290
- writeDetachedSessionBinding();
4291
- let createdDetachedSession = false;
4292
- let registeredHookTarget = null;
4293
- let registeredHookName = null;
4294
- let registeredClientAttachedHookName = null;
4295
- let detachedParentEnvFilePath;
4296
- let detachedLeaderPaneId = null;
4297
- try {
4298
- // This path is the user-shell interactive launch: OMX creates a tmux
4299
- // session and immediately attaches the user's terminal to it. If a tmux
4300
- // server already exists, `new-session -e` only forwards explicit values,
4301
- // so provider-specific parent-shell keys would disappear. Source a
4302
- // private env file inside the leader shell instead of putting every
4303
- // parent env value on the tmux command line or in logs.
4304
- if (!nativeWindows) {
5038
+ const contextKey = detachedControlPlane?.values.OMX_MADMAX_DETACHED_CONTEXT.trim() || undefined;
5039
+ const runsRoot = detachedControlPlane?.values.OMX_RUNS_DIR || resolveMadmaxRunsRoot(process.env);
5040
+ const detachedLaunchNonce = randomUUID();
5041
+ const releaseMarkerPath = join(omxRoot(cwd), "runtime", "detached-release", `${sessionId}.${detachedLaunchNonce}.release`);
5042
+ let detachedParentEnvFilePath;
5043
+ let detachedLeaderPaneId = null;
5044
+ let detachedLeaderAuthority = null;
5045
+ let detachedHudAuthority = null;
5046
+ let detachedLeaderPid = null;
5047
+ let rollbackFromPreReportAuthority = false;
5048
+ let attachStep = null;
5049
+ const launchDetachedSession = async () => {
5050
+ let createdSession = false;
5051
+ const bootstrapLeader = async () => {
5052
+ if (!nativeWindows)
4305
5053
  detachedParentEnvFilePath = writeDetachedSessionParentEnvFile(cwd, sessionId, codexEnvWithNotify);
4306
- }
4307
- const bootstrapSteps = buildDetachedSessionBootstrapSteps(sessionName, cwd, codexCmd, hudCmd, workerLaunchArgs, codexHomeOverride, notifyTempContractRaw, nativeWindows, sessionId, projectLocalCodexHomeForCleanup, runtimeCodexHomeForCleanup, omxRootOverride, runtimeHookEnv, sqliteHomeOverride, detachedParentEnvFilePath, inheritedWorkerModel);
5054
+ const bootstrapSteps = buildDetachedSessionBootstrapSteps(sessionName, cwd, nativeWindows && detachedWindowsCodexCmd ? detachedWindowsCodexCmd : codexCmd, hudCmd, workerLaunchArgs, codexHomeOverride, notifyTempContractRaw, nativeWindows, sessionId, projectLocalCodexHomeForCleanup, runtimeCodexHomeForCleanup, omxRootOverride, codexEnvWithNotify, sqliteHomeOverride, detachedParentEnvFilePath, inheritedWorkerModel, releaseMarkerPath, omxBin, {
5055
+ ...(preLaunchOptions.notifyTempContract.active ? { notifyTempContract: preLaunchOptions.notifyTempContract } : {}),
5056
+ enableNotifyFallbackAuthority: preLaunchOptions.enableNotifyFallbackAuthority,
5057
+ worktreeDirty: preLaunchOptions.worktreeDirty,
5058
+ shouldAttach: detachedPreflight.shouldAttach,
5059
+ }, detachedControlPlane);
4308
5060
  for (const step of bootstrapSteps) {
4309
- const output = execTmuxFileSync(step.args, {
4310
- stdio: "pipe",
4311
- encoding: "utf-8",
4312
- });
4313
- if (step.name === "new-session") {
4314
- createdDetachedSession = true;
4315
- const leaderPaneId = parsePaneIdFromTmuxOutput(output || "");
4316
- if (leaderPaneId) {
5061
+ try {
5062
+ if (step.name === "new-session") {
5063
+ const output = execTmuxFileSync(step.args, { stdio: "pipe", encoding: "utf-8" });
5064
+ createdSession = true;
5065
+ const leaderPaneId = parsePaneIdFromTmuxOutput(output || "");
5066
+ if (!leaderPaneId)
5067
+ throw new Error("detached session did not report a leader pane id");
4317
5068
  detachedLeaderPaneId = leaderPaneId;
4318
- setDetachedTmuxSessionHistoryLimit(sessionName, leaderPaneId);
4319
- if (activeRecordPath && contextKey) {
4320
- writeMadmaxDetachedActiveRecord(activeRecordPath, {
4321
- version: 1,
4322
- context_key: contextKey,
4323
- created_at: new Date().toISOString(),
4324
- source_cwd: runtimeContext?.sourceCwd ?? process.env.OMX_SOURCE_CWD ?? cwd,
4325
- ...(runtimeContext?.worktreeCwd ? { worktree_cwd: runtimeContext.worktreeCwd } : {}),
4326
- argv: args,
4327
- run_dir: runtimeContext?.omxRoot ?? process.env.OMX_ROOT ?? cwd,
4328
- tmux_session_name: sessionName,
4329
- session_id: sessionId,
4330
- tmux_pane_id: leaderPaneId,
4331
- });
4332
- }
4333
- writeDetachedSessionBinding(leaderPaneId);
5069
+ detachedLeaderAuthority = captureDetachedLeaderAuthority(leaderPaneId, sessionName, sessionId);
5070
+ continue;
4334
5071
  }
4335
- }
4336
- if (step.name === "split-and-capture-hud-pane") {
4337
- const hudPaneId = parsePaneIdFromTmuxOutput(output || "");
4338
- const hookWindowIndex = hudPaneId
4339
- ? detectDetachedSessionWindowIndex(sessionName)
4340
- : null;
4341
- const hookTarget = hudPaneId && hookWindowIndex
4342
- ? buildResizeHookTarget(sessionName, hookWindowIndex)
4343
- : null;
4344
- const hookName = hudPaneId && hookWindowIndex
4345
- ? buildResizeHookName("launch", sessionName, hookWindowIndex, hudPaneId)
4346
- : null;
4347
- const clientAttachedHookName = hudPaneId && hookWindowIndex
4348
- ? buildClientAttachedReconcileHookName("launch", sessionName, hookWindowIndex, hudPaneId)
4349
- : null;
4350
- const finalizeSteps = buildDetachedSessionFinalizeSteps(sessionName, hudPaneId, hookWindowIndex, process.env.OMX_MOUSE !== "0", nativeWindows, shouldAttachDetachedTmuxSession(process.env), detachedLeaderPaneId);
4351
- if (nativeWindows && detachedWindowsCodexCmd) {
4352
- scheduleDetachedWindowsCodexLaunch(sessionName, detachedWindowsCodexCmd);
5072
+ const authority = detachedLeaderAuthority;
5073
+ if (!authority)
5074
+ throw new Error("detached leader authority missing before tmux mutation");
5075
+ if (step.name === "tag-session") {
5076
+ runDetachedLeaderMutation(authority, step.args, false);
5077
+ // tmux can acknowledge the session tag before the first owner-format
5078
+ // evaluation sees it. Retrying this idempotent first owner-guarded
5079
+ // mutation once gives the server a command boundary without relaxing
5080
+ // any part of the captured session, window, pane, PID, or owner fence.
5081
+ runDetachedLeaderMutation(authority, ["set-option", "-q", "-t", authority.sessionName, "history-limit", String(DETACHED_TMUX_HISTORY_LIMIT)], true, true);
5082
+ runDetachedLeaderMutation(authority, ["set-option", "-pq", "-t", authority.paneId, "history-limit", String(DETACHED_TMUX_HISTORY_LIMIT)]);
5083
+ // #3266: the owned leader pane must close with its process so a normal
5084
+ // child exit destroys the session naturally even when the user's tmux
5085
+ // config inherits remain-on-exit=on/failed.
5086
+ runDetachedLeaderMutation(authority, ["set-option", "-pq", "-t", authority.paneId, "remain-on-exit", "off"]);
5087
+ continue;
4353
5088
  }
4354
- for (const finalizeStep of finalizeSteps) {
4355
- if (finalizeStep.name === "sanitize-copy-mode-style") {
4356
- try {
4357
- mitigateCopyModeUnderlineArtifacts(sessionName);
4358
- }
4359
- catch (err) {
4360
- logCliOperationFailure(err);
4361
- }
5089
+ if (step.name !== "split-and-capture-hud-pane") {
5090
+ throw new Error(`unexpected detached bootstrap mutation: ${step.name}`);
5091
+ }
5092
+ detachedHudAuthority = runDetachedLeaderSplit(authority, step.args);
5093
+ tagDetachedHudPane(authority, detachedHudAuthority, sessionId);
5094
+ for (const finalizeStep of buildDetachedSessionFinalizeSteps(authority.sessionName, detachedHudAuthority.paneId, authority.windowIndex, process.env.OMX_MOUSE !== "0", nativeWindows, detachedPreflight.shouldAttach, authority.paneId)) {
5095
+ if (finalizeStep.name === "attach-session") {
5096
+ attachStep = finalizeStep;
4362
5097
  continue;
4363
5098
  }
4364
- const stdio = finalizeStep.name === "attach-session" ? "inherit" : "ignore";
4365
- try {
4366
- const startedAtMs = Date.now();
4367
- execTmuxFileSync(finalizeStep.args, { stdio });
4368
- if (finalizeStep.name === "attach-session") {
4369
- assertDetachedAttachDidNotNoop(sessionName, Date.now() - startedAtMs, process.env);
4370
- }
4371
- }
4372
- catch (err) {
4373
- logCliOperationFailure(err);
4374
- if (finalizeStep.name === "attach-session")
4375
- throw new Error("failed to attach detached tmux session");
5099
+ if (finalizeStep.name === "sanitize-copy-mode-style")
4376
5100
  continue;
4377
- }
4378
- if (finalizeStep.name === "register-resize-hook" &&
4379
- hookTarget &&
4380
- hookName) {
4381
- registeredHookTarget = hookTarget;
4382
- registeredHookName = hookName;
4383
- }
4384
- if (finalizeStep.name === "register-client-attached-reconcile" &&
4385
- clientAttachedHookName) {
4386
- registeredClientAttachedHookName = clientAttachedHookName;
4387
- }
4388
- if (finalizeStep.name === "reconcile-hud-resize") {
4389
- registerDetachedHudLayoutReconcileHook({
4390
- hudPaneId,
4391
- detachedLeaderPaneId,
4392
- cwd,
4393
- sessionId,
4394
- omxBin,
4395
- omxRootOverride,
4396
- baseEnv: runtimeHookEnv,
4397
- });
4398
- }
5101
+ const targetsHudPane = new Set([
5102
+ "register-resize-hook",
5103
+ "register-client-attached-reconcile",
5104
+ "schedule-delayed-resize",
5105
+ "reconcile-hud-resize",
5106
+ ]).has(finalizeStep.name);
5107
+ if (targetsHudPane)
5108
+ runDetachedHudMutation(authority, detachedHudAuthority, guardDetachedHudDeferredMutation(authority, detachedHudAuthority, finalizeStep.args));
5109
+ else
5110
+ runDetachedLeaderMutation(authority, finalizeStep.args);
4399
5111
  }
4400
5112
  }
5113
+ catch (error) {
5114
+ throw new DetachedLaunchSafetyError(step.name === "new-session" ? "inert-session" : "pane-id", error, detachedPreflight.report);
5115
+ }
4401
5116
  }
4402
- return { postLaunchHandledExternally: !nativeWindows };
4403
- }
4404
- catch (err) {
4405
- if (detachedParentEnvFilePath) {
4406
- rmSync(detachedParentEnvFilePath, { force: true });
4407
- }
4408
- if (activeRecordPath) {
4409
- rmSync(activeRecordPath, { force: true });
4410
- }
4411
- if (createdDetachedSession) {
4412
- const rollbackSteps = buildDetachedSessionRollbackSteps(sessionName, registeredHookTarget, registeredHookName, registeredClientAttachedHookName);
4413
- for (const rollbackStep of rollbackSteps) {
5117
+ return undefined;
5118
+ };
5119
+ const launchTerminal = await executeDetachedLaunchStateMachine({ preflight: detachedPreflight }, {
5120
+ establish: bootstrapLeader,
5121
+ complete: async () => {
5122
+ const readyDeadline = Date.now() + DETACHED_LEADER_READY_TIMEOUT_MS;
5123
+ while (true) {
5124
+ const report = readDetachedLeaderReport(releaseMarkerPath);
5125
+ if (isDetachedReadyReportAuthorized(report, {
5126
+ nonce: detachedLaunchNonce,
5127
+ sessionId,
5128
+ sessionName,
5129
+ shouldAttach: detachedPreflight.shouldAttach,
5130
+ leaderPaneId: detachedLeaderPaneId,
5131
+ leaderPanePid: detachedLeaderAuthority?.panePid ?? null,
5132
+ })) {
5133
+ detachedLeaderPid = report.leaderPid;
5134
+ return { kind: "success" };
5135
+ }
5136
+ if (report?.nonce === detachedLaunchNonce && report.sessionId === sessionId && report.sessionName === sessionName && report.leaderPid && report.kind === "failed") {
5137
+ detachedLeaderPid = report.leaderPid;
5138
+ return { kind: "failure", operation: "session-instructions", error: new Error(report.error || "detached leader setup failed") };
5139
+ }
5140
+ if (Date.now() >= readyDeadline)
5141
+ return { kind: "failure", operation: "session-instructions", error: new Error("detached leader readiness timed out") };
5142
+ blockMs(20);
5143
+ }
5144
+ },
5145
+ createInertSession: async () => {
5146
+ const report = readDetachedLeaderReport(releaseMarkerPath);
5147
+ if (report?.kind !== "ready" || report.nonce !== detachedLaunchNonce || report.sessionId !== sessionId || report.sessionName !== sessionName || report.leaderPid !== detachedLeaderPid) {
5148
+ throw new Error("detached ready report does not bind the inert session");
5149
+ }
5150
+ return sessionName;
5151
+ },
5152
+ capturePane: async () => {
5153
+ const report = readDetachedLeaderReport(releaseMarkerPath);
5154
+ if (!detachedLeaderPaneId || report?.kind !== "ready" || report.paneId !== detachedLeaderPaneId || report.leaderPid !== detachedLeaderPid) {
5155
+ throw new Error("detached ready report does not bind the leader pane");
5156
+ }
5157
+ return detachedLeaderPaneId;
5158
+ },
5159
+ updateNameMetadata: async () => {
5160
+ const state = await readSessionState(cwd, detachedSelectedStateEnv);
5161
+ if (state?.session_id !== sessionId || state.tmux_session_name !== sessionName) {
5162
+ throw new Error("detached session-name metadata was not committed by the leader");
5163
+ }
5164
+ return "committed-released";
5165
+ },
5166
+ updatePaneMetadata: async (_binding, pane) => {
5167
+ const state = await readSessionState(cwd, detachedSelectedStateEnv);
5168
+ if (state?.session_id !== sessionId || state.tmux_pane_id !== pane) {
5169
+ throw new Error("detached pane metadata was not committed by the leader");
5170
+ }
5171
+ return "committed-released";
5172
+ },
5173
+ publishActiveRecord: async () => {
5174
+ const activeRecordPath = contextKey ? madmaxDetachedActiveRecordPath(runsRoot, contextKey) : join(omxRoot(cwd), "state", "detached-active-record.json");
5175
+ const record = readMadmaxDetachedActiveRecord(activeRecordPath);
5176
+ if (!record || record.launch_nonce !== detachedLaunchNonce || record.leader_pid !== detachedLeaderPid || record.session_id !== sessionId || record.tmux_session_name !== sessionName || record.tmux_pane_id !== detachedLeaderPaneId)
5177
+ throw new Error("detached active record does not bind the ready leader");
5178
+ const bytes = readFileSync(activeRecordPath, "utf-8");
5179
+ return { bytes, digest: createHash("sha256").update(bytes).digest("hex"), nonce: detachedLaunchNonce };
5180
+ },
5181
+ finalizeSetupFailure: async () => { },
5182
+ releaseBarrier: async () => {
5183
+ if (!detachedLeaderPid)
5184
+ throw new Error("detached leader PID missing before release");
5185
+ publishDetachedReleaseMarker(releaseMarkerPath, detachedLaunchNonce, sessionId, sessionName, detachedLeaderPid, detachedHudAuthority ?? undefined);
5186
+ },
5187
+ abortAndAwaitFinalization: async () => {
5188
+ // The leader has the retained binding. Publishing abort is the only
5189
+ // outer action permitted after a D9 write failure; a mismatched or
5190
+ // missing report leaves every artifact and the tmux session intact.
5191
+ if (!detachedLeaderPid) {
5192
+ rollbackFromPreReportAuthority = detachedLeaderAuthority !== null;
5193
+ return { acknowledged: false, rollbackAuthorized: rollbackFromPreReportAuthority };
5194
+ }
5195
+ const current = readDetachedLeaderReport(releaseMarkerPath);
5196
+ if (current?.nonce === detachedLaunchNonce && current.sessionId === sessionId &&
5197
+ current.sessionName === sessionName && current.leaderPid === detachedLeaderPid &&
5198
+ current.finalized === true && (current.kind === "failed" || current.kind === "terminal"))
5199
+ return {
5200
+ acknowledged: true,
5201
+ nonce: current.nonce,
5202
+ sessionId: current.sessionId,
5203
+ sessionName: current.sessionName,
5204
+ leaderPid: current.leaderPid,
5205
+ kind: current.kind,
5206
+ };
5207
+ publishDetachedAbortMarker(releaseMarkerPath, detachedLaunchNonce, sessionId, sessionName, detachedLeaderPid);
5208
+ const deadline = Date.now() + 30_000;
5209
+ while (Date.now() < deadline) {
5210
+ const report = readDetachedLeaderReport(releaseMarkerPath);
5211
+ if (report?.nonce === detachedLaunchNonce && report.sessionId === sessionId &&
5212
+ report.sessionName === sessionName && report.leaderPid === detachedLeaderPid &&
5213
+ report.finalized === true && (report.kind === "failed" || report.kind === "terminal"))
5214
+ return {
5215
+ acknowledged: true,
5216
+ nonce: report.nonce,
5217
+ sessionId: report.sessionId,
5218
+ sessionName: report.sessionName,
5219
+ leaderPid: report.leaderPid,
5220
+ kind: report.kind,
5221
+ };
5222
+ blockMs(20);
5223
+ }
5224
+ return { acknowledged: false };
5225
+ },
5226
+ attachOrReturn: async () => { if (attachStep)
5227
+ execTmuxFileSync(attachStep.args, { stdio: "inherit" }); },
5228
+ rollback: async (_ownedRecord, report) => {
5229
+ const attempt = async (step, operation) => {
5230
+ report.rollback.attempted.push(step);
4414
5231
  try {
4415
- execTmuxFileSync(rollbackStep.args, { stdio: "ignore" });
5232
+ await operation();
5233
+ }
5234
+ catch (error) {
5235
+ report.rollback.failures.push({ step, status: "failed", code: detachedFailureCode(error) });
4416
5236
  }
4417
- catch (rollbackErr) {
4418
- logCliOperationFailure(rollbackErr);
4419
- // best-effort rollback only
5237
+ };
5238
+ await attempt("parent-env", async () => { if (detachedParentEnvFilePath)
5239
+ await rm(detachedParentEnvFilePath, { force: true }); });
5240
+ await attempt("runtime-codex-home", () => cleanupRuntimeCodexHome(runtimeCodexHomeForCleanup, projectLocalCodexHomeForCleanup));
5241
+ await attempt("rollback", () => { rmSync(releaseMarkerPath, { force: true }); rmSync(`${releaseMarkerPath}.release`, { force: true }); rmSync(`${releaseMarkerPath}.abort`, { force: true }); });
5242
+ if (createdSession) {
5243
+ for (const step of buildDetachedSessionRollbackSteps(sessionName, null, null, null)) {
5244
+ await attempt(`session:${step.name}`, () => {
5245
+ if (!detachedLeaderAuthority)
5246
+ throw new Error("detached leader authority missing before rollback");
5247
+ if (rollbackFromPreReportAuthority && step.name === "kill-session") {
5248
+ cleanupDetachedPreReportSession(detachedLeaderAuthority);
5249
+ return;
5250
+ }
5251
+ runDetachedLeaderMutation(detachedLeaderAuthority, step.args);
5252
+ });
4420
5253
  }
4421
5254
  }
5255
+ },
5256
+ });
5257
+ if (launchTerminal.kind === "attached" && detachedLeaderPid) {
5258
+ // #3266: propagate the finalized detached child's exit status to the invoking shell.
5259
+ // A rejected status protocol is only benign when the exact owned session
5260
+ // provably survives (manual detach); a destroyed or ambiguous session with
5261
+ // no authenticated finalized status is a protocol failure, never silent success.
5262
+ const attachResolution = resolveDetachedAttachExitStatus(releaseMarkerPath, { nonce: detachedLaunchNonce, sessionId, sessionName, leaderPid: detachedLeaderPid }, { exactSessionExists: () => probeExactDetachedSessionExists(detachedLeaderAuthority) });
5263
+ if (attachResolution === "protocol-failure") {
5264
+ process.stderr.write("[omx] detached session ended without an authenticated finalized terminal status; treating the launch as failed.\n");
5265
+ if (!process.exitCode)
5266
+ process.exitCode = 1;
4422
5267
  }
4423
- throw err;
4424
5268
  }
5269
+ return { postLaunchHandledExternally: true };
4425
5270
  };
4426
- const contextKey = process.env[OMX_MADMAX_DETACHED_CONTEXT_ENV]?.trim();
4427
- const runsRoot = resolveMadmaxRunsRoot(process.env);
4428
- try {
4429
- if (isMadmaxDetachedGuardEnabled(process.env) && contextKey) {
4430
- return withMadmaxDetachedContextLock(runsRoot, contextKey, launchDetachedSession);
4431
- }
4432
- return launchDetachedSession();
4433
- }
4434
- catch (err) {
4435
- if (err instanceof MadmaxDetachedReuseError || err instanceof MadmaxDetachedGuardError) {
4436
- throw err;
4437
- }
4438
- logCliOperationFailure(err);
4439
- // tmux not available or failed, just run codex directly
4440
- runCodexBlocking(cwd, launchArgs, codexEnvWithNotify);
4441
- return { postLaunchHandledExternally: false };
5271
+ if (contextKey) {
5272
+ return await withMadmaxDetachedContextLock(runsRoot, contextKey, launchDetachedSession);
4442
5273
  }
5274
+ return await launchDetachedSession();
4443
5275
  }
4444
5276
  }
4445
5277
  function listHudWatchPaneIdsInCurrentWindow(currentPaneId, owner = {}) {
@@ -4498,6 +5330,24 @@ export function buildWindowsPromptCommand(command, args) {
4498
5330
  ].join("; ");
4499
5331
  return `powershell.exe -NoLogo -NoExit -EncodedCommand ${encodePowerShellCommand(wrappedCommand)}`;
4500
5332
  }
5333
+ /**
5334
+ * Build a Windows child command for detached ownership. Unlike the interactive
5335
+ * prompt wrapper, this process exits as soon as Codex does so the outer barrier
5336
+ * can run its post-launch cleanup.
5337
+ */
5338
+ export function buildWindowsDetachedChildCommand(command, args) {
5339
+ const invocation = [
5340
+ "&",
5341
+ quotePowerShellArg(command),
5342
+ ...args.map(quotePowerShellArg),
5343
+ ].join(" ");
5344
+ const wrappedCommand = [
5345
+ "$ErrorActionPreference = 'Stop'",
5346
+ invocation,
5347
+ "exit $LASTEXITCODE",
5348
+ ].join("; ");
5349
+ return `powershell.exe -NoLogo -NoProfile -NonInteractive -EncodedCommand ${encodePowerShellCommand(wrappedCommand)}`;
5350
+ }
4501
5351
  /**
4502
5352
  * Wrap a command for tmux pane execution while preserving the tmux pane cwd.
4503
5353
  * tmux already starts the pane at `-c <cwd>`; using a login shell here can
@@ -4534,45 +5384,323 @@ function quoteShellArg(value) {
4534
5384
  function quotePowerShellArg(value) {
4535
5385
  return `'${value.replace(/'/g, "''")}'`;
4536
5386
  }
4537
- export function buildDetachedWindowsBootstrapScript(sessionName, commandText, delayMs = WINDOWS_DETACHED_BOOTSTRAP_DELAY_MS, tmuxCommand = resolveTmuxExecutableForLaunch()) {
4538
- const delay = Number.isFinite(delayMs) && delayMs > 0
4539
- ? Math.floor(delayMs)
4540
- : WINDOWS_DETACHED_BOOTSTRAP_DELAY_MS;
4541
- const targetLiteral = JSON.stringify(`${sessionName}:0.0`);
4542
- const commandLiteral = JSON.stringify(commandText);
4543
- const tmuxCommandLiteral = JSON.stringify(tmuxCommand);
4544
- return [
4545
- "const { execFileSync } = require('child_process');",
4546
- `const tmuxCommand = ${tmuxCommandLiteral};`,
4547
- `setTimeout(() => {`,
4548
- `try { execFileSync(tmuxCommand, ['send-keys', '-t', ${targetLiteral}, '-l', '--', ${commandLiteral}], { stdio: 'ignore' }); } catch {}`,
4549
- `try { execFileSync(tmuxCommand, ['send-keys', '-t', ${targetLiteral}, 'C-m'], { stdio: 'ignore' }); } catch {}`,
4550
- `}, ${delay});`,
4551
- ].join("");
5387
+ export function decodeDetachedLeaderPayload(encoded) {
5388
+ if (!encoded || !/^[A-Za-z0-9_-]+$/.test(encoded))
5389
+ throw new Error("invalid detached leader payload");
5390
+ let value;
5391
+ try {
5392
+ value = JSON.parse(Buffer.from(encoded, "base64url").toString("utf8"));
5393
+ }
5394
+ catch {
5395
+ throw new Error("invalid detached leader payload");
5396
+ }
5397
+ if (!value || typeof value !== "object")
5398
+ throw new Error("invalid detached leader payload");
5399
+ const payload = value;
5400
+ const options = payload.preLaunchOptions;
5401
+ if (typeof payload.cwd !== "string" || typeof payload.sessionName !== "string" || typeof payload.sessionId !== "string" ||
5402
+ typeof payload.codexCmd !== "string" || !options || typeof options !== "object" ||
5403
+ typeof options.enableNotifyFallbackAuthority !== "boolean" ||
5404
+ typeof options.worktreeDirty !== "boolean" ||
5405
+ typeof options.shouldAttach !== "boolean")
5406
+ throw new Error("invalid detached leader payload");
5407
+ const notifyTempContract = options.notifyTempContract;
5408
+ if (notifyTempContract !== undefined && (!notifyTempContract || typeof notifyTempContract !== "object"))
5409
+ throw new Error("invalid detached leader payload");
5410
+ const parentEnv = payload.parentEnv;
5411
+ if (parentEnv !== undefined && (!parentEnv || typeof parentEnv !== "object" ||
5412
+ Object.entries(parentEnv).some(([key, value]) => !SHELL_ENV_NAME_PATTERN.test(key) || DETACHED_SESSION_PANE_ENV_KEYS.has(key) || typeof value !== "string" || value.includes("\0")))) {
5413
+ throw new Error("invalid detached leader parent environment");
5414
+ }
5415
+ return {
5416
+ cwd: payload.cwd, sessionName: payload.sessionName, sessionId: payload.sessionId, codexCmd: payload.codexCmd,
5417
+ ...(typeof payload.codexHomeOverride === "string" ? { codexHomeOverride: payload.codexHomeOverride } : {}),
5418
+ ...(typeof payload.projectLocalCodexHomeForCleanup === "string" ? { projectLocalCodexHomeForCleanup: payload.projectLocalCodexHomeForCleanup } : {}),
5419
+ ...(typeof payload.runtimeCodexHomeForCleanup === "string" ? { runtimeCodexHomeForCleanup: payload.runtimeCodexHomeForCleanup } : {}),
5420
+ ...(parentEnv ? { parentEnv: parentEnv } : {}),
5421
+ ...(typeof payload.readyPath === "string" ? { readyPath: payload.readyPath } : {}),
5422
+ preLaunchOptions: {
5423
+ ...(notifyTempContract ? { notifyTempContract: notifyTempContract } : {}),
5424
+ enableNotifyFallbackAuthority: options.enableNotifyFallbackAuthority,
5425
+ worktreeDirty: options.worktreeDirty,
5426
+ shouldAttach: options.shouldAttach,
5427
+ },
5428
+ };
4552
5429
  }
4553
- function scheduleDetachedWindowsCodexLaunch(sessionName, commandText) {
4554
- const child = spawn(process.execPath, ["-e", buildDetachedWindowsBootstrapScript(sessionName, commandText)], {
4555
- detached: true,
4556
- stdio: "ignore",
4557
- windowsHide: true,
5430
+ function parseDetachedHudAuthorityProof(value) {
5431
+ if (!value || typeof value !== "object")
5432
+ return undefined;
5433
+ const proof = value;
5434
+ if (typeof proof.paneId !== "string" || !/^%[0-9]+$/.test(proof.paneId))
5435
+ return undefined;
5436
+ if (typeof proof.panePid !== "number" || !Number.isSafeInteger(proof.panePid) || proof.panePid <= 0)
5437
+ return undefined;
5438
+ if (typeof proof.sessionId !== "string" || !/^\$[0-9]+$/.test(proof.sessionId))
5439
+ return undefined;
5440
+ if (typeof proof.windowId !== "string" || !/^@[0-9]+$/.test(proof.windowId))
5441
+ return undefined;
5442
+ if (typeof proof.operationMarker !== "string" || !/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/.test(proof.operationMarker))
5443
+ return undefined;
5444
+ return { paneId: proof.paneId, panePid: proof.panePid, sessionId: proof.sessionId, windowId: proof.windowId, operationMarker: proof.operationMarker };
5445
+ }
5446
+ function teardownDetachedOwnedHudPane(leaderPaneId, payload, hudProof) {
5447
+ // #3266: on a normal child exit, kill exactly the bootstrap-proven OMX HUD pane so
5448
+ // the leader pane becomes the last live pane and the owned session closes naturally,
5449
+ // returning the attached client to its shell. Fail-closed: any doubt means zero mutations.
5450
+ // Contract: only the proven HUD is removed. A pane the user added to the detached
5451
+ // session is deliberately preserved, which intentionally keeps that session (and any
5452
+ // attached client) alive; natural closure and shell return happen only when no other
5453
+ // panes remain.
5454
+ const discovered = discoverDetachedHudAuthority(payload.sessionName, payload.sessionId);
5455
+ const proof = discovered ?? hudProof;
5456
+ if (!proof)
5457
+ return;
5458
+ try {
5459
+ cleanupDetachedHudPane(proof, discovered ? payload.sessionId : undefined);
5460
+ execTmuxFileSync(["kill-pane", "-t", leaderPaneId], { encoding: "utf-8", stdio: ["ignore", "pipe", "ignore"] });
5461
+ }
5462
+ catch (error) {
5463
+ traceDetachedLeaderPhase(`hud-teardown-error:${describeDetachedLeaderFailure(error)}`);
5464
+ logCliOperationFailure(error);
5465
+ }
5466
+ }
5467
+ function traceDetachedLeaderPhase(phase) {
5468
+ if (process.env.OMX_TEST_DETACHED_TRACE !== "1")
5469
+ return;
5470
+ const line = `[omx-test-detached] ${phase}\n`;
5471
+ process.stderr.write(line);
5472
+ const tracePath = process.env.OMX_TEST_DETACHED_TRACE_PATH;
5473
+ if (tracePath)
5474
+ appendFileSync(tracePath, line);
5475
+ }
5476
+ async function runDetachedSessionLeader(payload) {
5477
+ for (const [key, value] of Object.entries(payload.parentEnv ?? {})) {
5478
+ if (isDetachedLaunchControlPlaneKey(key, process.platform === "win32"))
5479
+ continue;
5480
+ process.env[key] = value;
5481
+ }
5482
+ let pane;
5483
+ const inheritedPane = process.env.TMUX_PANE?.trim();
5484
+ if (payload.preLaunchOptions.shouldAttach === false) {
5485
+ if (!inheritedPane || !/^%[0-9]+$/.test(inheritedPane)) {
5486
+ throw new Error("detached Hermes leader has no tmux pane identity");
5487
+ }
5488
+ pane = inheritedPane;
5489
+ }
5490
+ else {
5491
+ const paneSnapshot = execTmuxFileSync(["list-panes", "-t", payload.sessionName, "-F", "#{pane_id}\t#{pane_dead}\t#{pane_pid}"], { encoding: "utf-8", stdio: ["ignore", "pipe", "ignore"] });
5492
+ pane = parseDetachedLeaderPaneIdByPid(paneSnapshot, process.pid);
5493
+ }
5494
+ process.env.TMUX_PANE = pane;
5495
+ const established = await establishPreLaunchBinding(payload.cwd, payload.sessionId, {
5496
+ tmuxSessionName: payload.sessionName,
5497
+ tmuxPaneId: pane,
4558
5498
  });
4559
- child.unref();
5499
+ const binding = established.binding;
5500
+ let ownedRecord;
5501
+ let detachedHudProof;
5502
+ const nonce = payload.readyPath?.split(".").at(-2) || payload.sessionId;
5503
+ const contextKey = process.env[OMX_MADMAX_DETACHED_CONTEXT_ENV]?.trim();
5504
+ const activeRecordPath = contextKey
5505
+ ? madmaxDetachedActiveRecordPath(resolveMadmaxRunsRoot(process.env), contextKey)
5506
+ : join(omxRoot(payload.cwd), "state", "detached-active-record.json");
5507
+ let externalInterrupt;
5508
+ try {
5509
+ const completion = await completePreLaunchSetup(payload.cwd, payload.sessionId, payload.preLaunchOptions.notifyTempContract, payload.codexHomeOverride, payload.preLaunchOptions.enableNotifyFallbackAuthority, payload.preLaunchOptions.worktreeDirty);
5510
+ if (completion.kind === "failure") {
5511
+ const finalization = await finalizeBoundOnce(binding, "setup-failure", payload.cwd);
5512
+ if (!finalization.finalized)
5513
+ throw new Error(`bound setup finalization denied: ${finalization.cleanup.comparison?.reason ?? "unknown reason"}`);
5514
+ throw completion.error;
5515
+ }
5516
+ const record = {
5517
+ version: 1, context_key: contextKey ?? payload.sessionId, created_at: new Date().toISOString(),
5518
+ source_cwd: payload.cwd, argv: [payload.codexCmd], run_dir: process.env.OMX_ROOT ?? payload.cwd,
5519
+ tmux_session_name: payload.sessionName, session_id: payload.sessionId, tmux_pane_id: pane,
5520
+ launch_nonce: nonce, leader_pid: process.pid, base_state_root: binding.context.baseStateDir,
5521
+ lifecycle_phase: "ready",
5522
+ };
5523
+ const runtimeBinding = queryMadmaxDetachedRuntimeBinding(record);
5524
+ ownedRecord = writeMadmaxDetachedActiveRecord(activeRecordPath, {
5525
+ ...record,
5526
+ ...(runtimeBinding ? {
5527
+ tmux_internal_session_id: runtimeBinding.internalSessionId,
5528
+ tmux_session_created: runtimeBinding.sessionCreated,
5529
+ tmux_pane_pid: runtimeBinding.panePid,
5530
+ } : {}),
5531
+ });
5532
+ if (payload.readyPath) {
5533
+ writeDetachedLeaderReport(payload.readyPath, { version: 1, kind: "ready", nonce, sessionId: payload.sessionId, sessionName: payload.sessionName, paneId: pane, leaderPid: process.pid });
5534
+ const releasePath = `${payload.readyPath}.release`;
5535
+ const abortPath = `${payload.readyPath}.abort`;
5536
+ const releaseDeadline = Date.now() + 30_000;
5537
+ let interrupted;
5538
+ const interrupt = (signal) => { interrupted = signal; };
5539
+ process.once("SIGINT", () => interrupt("SIGINT"));
5540
+ process.once("SIGTERM", () => interrupt("SIGTERM"));
5541
+ process.once("SIGHUP", () => interrupt("SIGHUP"));
5542
+ while (true) {
5543
+ if (interrupted)
5544
+ throw new Error(`detached leader interrupted before release: ${interrupted}`);
5545
+ const release = readDetachedLeaderReport(releasePath);
5546
+ if (release?.kind === "release" && release.nonce === nonce && release.sessionId === payload.sessionId && release.sessionName === payload.sessionName && release.leaderPid === process.pid) {
5547
+ detachedHudProof = parseDetachedHudAuthorityProof(release.hud);
5548
+ break;
5549
+ }
5550
+ const abort = readDetachedLeaderReport(abortPath);
5551
+ if (abort?.kind === "abort" && abort.nonce === nonce && abort.sessionId === payload.sessionId && abort.sessionName === payload.sessionName && abort.leaderPid === process.pid) {
5552
+ throw new Error("detached launch aborted before release");
5553
+ }
5554
+ if (Date.now() >= releaseDeadline)
5555
+ throw new Error("detached leader release timed out");
5556
+ blockMs(20);
5557
+ }
5558
+ rmSync(releasePath, { force: true });
5559
+ rmSync(abortPath, { force: true });
5560
+ }
5561
+ const child = spawn(process.platform === "win32" ? "powershell.exe" : "/bin/sh", process.platform === "win32" ? ["-NoProfile", "-Command", payload.codexCmd] : ["-c", payload.codexCmd], {
5562
+ cwd: payload.cwd,
5563
+ stdio: "inherit",
5564
+ ...(process.platform === "win32" ? {} : { detached: true }),
5565
+ });
5566
+ let escalation;
5567
+ const signalChildTree = (signal, force = false) => {
5568
+ if (!child.pid)
5569
+ return;
5570
+ try {
5571
+ if (process.platform === "win32")
5572
+ execFileSync("taskkill", ["/PID", String(child.pid), "/T", ...(force ? ["/F"] : [])], { stdio: "ignore" });
5573
+ else
5574
+ process.kill(-child.pid, signal);
5575
+ }
5576
+ catch { }
5577
+ };
5578
+ const forward = (signal) => {
5579
+ externalInterrupt ??= signal;
5580
+ signalChildTree(signal);
5581
+ if (!escalation) {
5582
+ escalation = setTimeout(() => signalChildTree("SIGKILL", true), 5_000);
5583
+ escalation.unref();
5584
+ }
5585
+ };
5586
+ process.once("SIGINT", () => forward("SIGINT"));
5587
+ process.once("SIGTERM", () => forward("SIGTERM"));
5588
+ process.once("SIGHUP", () => forward("SIGHUP"));
5589
+ traceDetachedLeaderPhase("child-wait-start");
5590
+ const outcome = await new Promise((resolveChild, rejectChild) => {
5591
+ child.once("error", rejectChild);
5592
+ child.once("exit", (code, signal) => resolveChild({ code, signal }));
5593
+ });
5594
+ traceDetachedLeaderPhase(`child-exit:${String(outcome.code)}:${String(outcome.signal)}`);
5595
+ if (escalation)
5596
+ clearTimeout(escalation);
5597
+ if (process.platform !== "win32" && child.pid) {
5598
+ const waitForGroupExit = async (deadline) => {
5599
+ while (Date.now() < deadline) {
5600
+ try {
5601
+ process.kill(-child.pid, 0);
5602
+ await new Promise((resolveWait) => setTimeout(resolveWait, 20));
5603
+ }
5604
+ catch {
5605
+ return true;
5606
+ }
5607
+ }
5608
+ return false;
5609
+ };
5610
+ let groupGone = await waitForGroupExit(Date.now() + 5_000);
5611
+ if (!groupGone) {
5612
+ signalChildTree("SIGKILL", true);
5613
+ groupGone = await waitForGroupExit(Date.now() + 5_000);
5614
+ }
5615
+ if (!groupGone)
5616
+ throw new Error("detached child process group did not disappear after forced termination");
5617
+ }
5618
+ const signalExitCodes = { SIGHUP: 129, SIGINT: 130, SIGTERM: 143, SIGKILL: 137 };
5619
+ process.exitCode = externalInterrupt
5620
+ ? signalExitCodes[externalInterrupt] ?? 1
5621
+ : outcome.code ?? (outcome.signal ? signalExitCodes[outcome.signal] ?? 1 : 1);
5622
+ traceDetachedLeaderPhase("post-launch-start");
5623
+ await postLaunch(payload.cwd, payload.sessionId, binding, payload.codexHomeOverride, payload.preLaunchOptions.enableNotifyFallbackAuthority, payload.projectLocalCodexHomeForCleanup, async () => {
5624
+ if (payload.readyPath)
5625
+ writeDetachedLeaderReport(payload.readyPath, {
5626
+ version: 1, kind: "terminal", nonce, sessionId: payload.sessionId, sessionName: payload.sessionName,
5627
+ paneId: pane, leaderPid: process.pid, finalized: true,
5628
+ ...(typeof process.exitCode === "number" ? { exitStatus: process.exitCode } : {}),
5629
+ });
5630
+ traceDetachedLeaderPhase("terminal-report-written");
5631
+ if (ownedRecord) {
5632
+ const bytes = readFileSync(activeRecordPath, "utf-8");
5633
+ const digest = createHash("sha256").update(bytes).digest("hex");
5634
+ if (bytes === ownedRecord.bytes && digest === ownedRecord.digest)
5635
+ rmSync(activeRecordPath, { force: true });
5636
+ }
5637
+ if (!externalInterrupt && outcome.code !== null) {
5638
+ traceDetachedLeaderPhase("hud-teardown-start");
5639
+ teardownDetachedOwnedHudPane(pane, payload, detachedHudProof);
5640
+ traceDetachedLeaderPhase("hud-teardown-done");
5641
+ }
5642
+ });
5643
+ traceDetachedLeaderPhase("post-launch-done");
5644
+ await cleanupRuntimeCodexHome(payload.runtimeCodexHomeForCleanup, payload.projectLocalCodexHomeForCleanup);
5645
+ traceDetachedLeaderPhase("leader-return");
5646
+ }
5647
+ catch (error) {
5648
+ let failure = error;
5649
+ let finalized = false;
5650
+ try {
5651
+ await postLaunch(payload.cwd, payload.sessionId, binding, payload.codexHomeOverride, payload.preLaunchOptions.enableNotifyFallbackAuthority, payload.projectLocalCodexHomeForCleanup);
5652
+ finalized = true;
5653
+ await cleanupRuntimeCodexHome(payload.runtimeCodexHomeForCleanup, payload.projectLocalCodexHomeForCleanup);
5654
+ }
5655
+ catch (lifecycleError) {
5656
+ failure = new AggregateError([error, lifecycleError], `detached leader failure finalization did not complete: launch=${describeDetachedLeaderFailure(error)}; lifecycle=${describeDetachedLeaderFailure(lifecycleError)}`);
5657
+ }
5658
+ if (finalized && ownedRecord) {
5659
+ try {
5660
+ const bytes = readFileSync(activeRecordPath, "utf-8");
5661
+ const digest = createHash("sha256").update(bytes).digest("hex");
5662
+ if (bytes === ownedRecord.bytes && digest === ownedRecord.digest)
5663
+ rmSync(activeRecordPath, { force: true });
5664
+ }
5665
+ catch { }
5666
+ }
5667
+ if (payload.readyPath)
5668
+ writeDetachedLeaderReport(payload.readyPath, {
5669
+ version: 1, kind: "failed", nonce, sessionId: payload.sessionId, sessionName: payload.sessionName,
5670
+ leaderPid: process.pid, finalized, error: failure instanceof Error ? failure.message : String(failure),
5671
+ });
5672
+ throw failure;
5673
+ }
5674
+ finally {
5675
+ await closeLaunchSessionBindingOnce(binding);
5676
+ }
4560
5677
  }
4561
5678
  /**
4562
5679
  * postLaunch: Clean up after Codex exits.
4563
5680
  * Each step is independently fault-tolerant (try/catch per step).
4564
5681
  */
4565
- export async function postLaunch(cwd, sessionId, codexHomeOverride, enableNotifyFallbackAuthority = false, projectLocalCodexHomeForCleanup) {
4566
- // Capture session start time before cleanup (writeSessionEnd deletes session.json)
4567
- let sessionStartedAt;
5682
+ export async function postLaunch(cwd, sessionId, binding, codexHomeOverride, enableNotifyFallbackAuthority = false, projectLocalCodexHomeForCleanup, onFinalized) {
5683
+ const sessionStartedAt = binding.startedAt;
5684
+ // Pointer authority is consumed before any terminal cleanup. A denied or
5685
+ // unproven binding must leave every cleanup target untouched.
5686
+ let terminalError;
4568
5687
  try {
4569
- const sessionState = await readSessionState(cwd);
4570
- sessionStartedAt = sessionState?.started_at;
5688
+ const finalization = await finalizeBoundOnce(binding, "post-launch", cwd);
5689
+ if (!finalization.finalized)
5690
+ terminalError = new Error(`bound session finalization denied: ${finalization.cleanup.comparison?.reason ?? "unknown reason"}`);
5691
+ const closeFailure = finalization.cleanup.capability.find((evidence) => evidence.status === "failed");
5692
+ if (closeFailure)
5693
+ terminalError = new Error(`bound session retained close failed: ${closeFailure.error?.message ?? "unknown error"}`);
4571
5694
  }
4572
- catch (err) {
4573
- logCliOperationFailure(err);
4574
- // Non-fatal
5695
+ catch (error) {
5696
+ terminalError = error;
4575
5697
  }
5698
+ if (terminalError) {
5699
+ // Denial diagnostics are complete; do not mutate ordinary lifecycle state.
5700
+ await closeLaunchSessionBindingOnce(binding);
5701
+ throw terminalError;
5702
+ }
5703
+ await onFinalized?.();
4576
5704
  // 0. Reap MCP orphans left behind by the session that just exited.
4577
5705
  await reapPostLaunchOrphanedMcpProcesses();
4578
5706
  // 0. Flush fallback watcher once to reduce race with fast codex exit.
@@ -4623,13 +5751,6 @@ export async function postLaunch(cwd, sessionId, codexHomeOverride, enableNotify
4623
5751
  catch (err) {
4624
5752
  console.error(`[omx] postLaunch: model instructions cleanup failed: ${err instanceof Error ? err.message : err}`);
4625
5753
  }
4626
- // 2. Archive session (write history, delete session.json)
4627
- try {
4628
- await writeSessionEnd(cwd, sessionId);
4629
- }
4630
- catch (err) {
4631
- console.error(`[omx] postLaunch: session archive failed: ${err instanceof Error ? err.message : err}`);
4632
- }
4633
5754
  // 2.5. Best-effort wiki session capture
4634
5755
  try {
4635
5756
  const { onSessionEnd } = await import("../wiki/lifecycle.js");
@@ -4701,10 +5822,59 @@ export async function postLaunch(cwd, sessionId, codexHomeOverride, enableNotify
4701
5822
  logCliOperationFailure(err);
4702
5823
  // Non-fatal
4703
5824
  }
5825
+ const closeEvidence = await closeLaunchSessionBindingOnce(binding);
5826
+ if (closeEvidence.status === "failed") {
5827
+ throw new Error(`bound session retained close failed: ${closeEvidence.error?.message ?? "unknown error"}`);
5828
+ }
4704
5829
  }
4705
- export async function runDetachedSessionPostLaunch(cwd, sessionId, codexHomeOverride, projectLocalCodexHomeForCleanup, runtimeCodexHomeForCleanup) {
4706
- await postLaunch(cwd, sessionId, codexHomeOverride, false, projectLocalCodexHomeForCleanup);
4707
- await cleanupRuntimeCodexHome(runtimeCodexHomeForCleanup, projectLocalCodexHomeForCleanup).catch(logCliOperationFailure);
5830
+ export async function runDetachedSessionPostLaunch(cwd, sessionId, projectLocalCodexHomeForCleanup, runtimeCodexHomeForCleanup, releaseMarkerPath) {
5831
+ // The detached child is intentionally unbound: it must never perform parent
5832
+ // pointer finalization or cleanup after the release barrier. It still owns
5833
+ // every non-pointer terminal lifecycle responsibility.
5834
+ const failures = [];
5835
+ const strict = async (operation) => {
5836
+ try {
5837
+ await operation();
5838
+ }
5839
+ catch (error) {
5840
+ failures.push(error);
5841
+ }
5842
+ };
5843
+ await strict(() => reapPostLaunchOrphanedMcpProcesses());
5844
+ await strict(() => flushNotifyFallbackOnce(cwd, { sessionId }));
5845
+ await strict(() => stopNotifyFallbackWatcher(cwd));
5846
+ await strict(() => flushHookDerivedWatcherOnce(cwd));
5847
+ await strict(() => stopHookDerivedWatcher(cwd));
5848
+ if (projectLocalCodexHomeForCleanup) {
5849
+ await strict(() => cleanCodexModelAvailabilityNuxIfNeeded(join(projectLocalCodexHomeForCleanup, "config.toml")));
5850
+ }
5851
+ await strict(() => removeSessionModelInstructionsFile(cwd, sessionId));
5852
+ await strict(async () => {
5853
+ const { onSessionEnd } = await import("../wiki/lifecycle.js");
5854
+ onSessionEnd({ cwd, session_id: sessionId });
5855
+ });
5856
+ await strict(() => cleanupPostLaunchModeStateFiles(cwd, sessionId));
5857
+ await strict(async () => {
5858
+ const { notifyLifecycle } = await import("../notifications/index.js");
5859
+ await notifyLifecycle("session-end", {
5860
+ sessionId, projectPath: cwd, projectName: basename(cwd), reason: "session_exit",
5861
+ });
5862
+ });
5863
+ await strict(async () => {
5864
+ const { markOwnedTeamsLeaderSessionStopped } = await import("../team/state.js");
5865
+ await markOwnedTeamsLeaderSessionStopped(cwd, sessionId);
5866
+ });
5867
+ await strict(() => emitNativeHookEvent(cwd, "session-end", {
5868
+ session_id: sessionId,
5869
+ context: buildNativeHookBaseContext(cwd, sessionId, "finished", {
5870
+ project_path: cwd, project_name: basename(cwd), reason: "session_exit", status: "finished",
5871
+ }),
5872
+ }));
5873
+ await strict(() => cleanupRuntimeCodexHome(runtimeCodexHomeForCleanup, projectLocalCodexHomeForCleanup));
5874
+ if (releaseMarkerPath)
5875
+ await strict(() => rm(releaseMarkerPath, { force: true }));
5876
+ if (failures.length > 0)
5877
+ throw new AggregateError(failures, "detached post-launch lifecycle cleanup failed");
4708
5878
  }
4709
5879
  async function emitNativeHookEvent(cwd, event, opts = {}) {
4710
5880
  const payload = buildHookEvent(event, {
@@ -5143,14 +6313,13 @@ async function listHookVisibleRunDirStateRefs(cwd) {
5143
6313
  if (!sourceCwd || !runDir)
5144
6314
  return;
5145
6315
  try {
5146
- if (canonicalizePathForRunDirMatch(sourceCwd) !== canonicalCwd &&
5147
- (!worktreeCwd || canonicalizePathForRunDirMatch(worktreeCwd) !== canonicalCwd))
6316
+ if (canonicalizePathForRunDirMatch(sourceCwd) !== canonicalCwd
6317
+ && (!worktreeCwd || canonicalizePathForRunDirMatch(worktreeCwd) !== canonicalCwd))
5148
6318
  return;
5149
6319
  const resolvedRunDir = resolve(runDir);
5150
6320
  if (resolvedRunDir !== canonicalRunsRoot
5151
- && !resolvedRunDir.startsWith(`${canonicalRunsRoot}/`)) {
6321
+ && !resolvedRunDir.startsWith(`${canonicalRunsRoot}/`))
5152
6322
  return;
5153
- }
5154
6323
  runDirs.add(resolvedRunDir);
5155
6324
  }
5156
6325
  catch {
@@ -5203,7 +6372,7 @@ async function listHookVisibleRunDirStateRefs(cwd) {
5203
6372
  for (const dir of candidateDirs) {
5204
6373
  const files = await readdir(dir).catch(() => []);
5205
6374
  for (const file of files) {
5206
- if (!file.endsWith("-state.json") || file === "session.json")
6375
+ if (!isModeStateFilename(file))
5207
6376
  continue;
5208
6377
  const path = join(dir, file);
5209
6378
  if (seenPaths.has(path))
@@ -5219,41 +6388,362 @@ async function listHookVisibleRunDirStateRefs(cwd) {
5219
6388
  }
5220
6389
  return refs.sort((a, b) => a.mode.localeCompare(b.mode));
5221
6390
  }
5222
- async function cancelModes(args = []) {
5223
- const { writeFile, readFile } = await import("fs/promises");
5224
- const cwd = process.cwd();
6391
+ function isCanonicalPathWithin(parent, child, allowEqual = false) {
6392
+ const rel = relative(parent, child);
6393
+ if (rel === "")
6394
+ return allowEqual;
6395
+ return rel !== ".." && !rel.startsWith(`..${process.platform === "win32" ? "\\" : "/"}`) && !isAbsolute(rel);
6396
+ }
6397
+ async function selectAuthorizedHookVisibleRunDirState(cwd) {
6398
+ const runsRoot = resolveMadmaxRunsRoot(process.env);
6399
+ const activeDir = join(runsRoot, MADMAX_DETACHED_ACTIVE_DIR);
6400
+ const canonicalCwd = canonicalizePathForRunDirMatch(cwd);
6401
+ let canonicalRunsRoot;
6402
+ try {
6403
+ canonicalRunsRoot = realpathSync(resolve(runsRoot));
6404
+ }
6405
+ catch {
6406
+ return null;
6407
+ }
6408
+ const candidates = [];
6409
+ const files = await readdir(activeDir).catch(() => []);
6410
+ for (const file of files) {
6411
+ if (!file.endsWith(".json"))
6412
+ continue;
6413
+ const record = readMadmaxDetachedActiveRecord(join(activeDir, file));
6414
+ if (!record || file !== `${record.context_key}.json`)
6415
+ continue;
6416
+ if (!record.session_id || normalizeSessionId(record.session_id) !== record.session_id)
6417
+ continue;
6418
+ if (!hasMatchingMadmaxDetachedRuntimeBinding(record))
6419
+ continue;
6420
+ if (canonicalizePathForRunDirMatch(record.source_cwd) !== canonicalCwd
6421
+ && (!record.worktree_cwd || canonicalizePathForRunDirMatch(record.worktree_cwd) !== canonicalCwd))
6422
+ continue;
6423
+ try {
6424
+ const canonicalRunDir = realpathSync(resolve(record.run_dir));
6425
+ if (!isCanonicalPathWithin(canonicalRunsRoot, canonicalRunDir)) {
6426
+ throw new Error("run directory escapes the authorized runs root");
6427
+ }
6428
+ const stateDir = realpathSync(join(canonicalRunDir, ".omx", "state"));
6429
+ if (!isCanonicalPathWithin(canonicalRunDir, stateDir)) {
6430
+ throw new Error("state directory escapes the authorized run directory");
6431
+ }
6432
+ const session = JSON.parse(await readFile(join(stateDir, "session.json"), "utf-8"));
6433
+ if (session.session_id !== record.session_id)
6434
+ throw new Error("run session pointer changed");
6435
+ const sessionDir = realpathSync(join(stateDir, "sessions", record.session_id));
6436
+ if (!isCanonicalPathWithin(stateDir, sessionDir)) {
6437
+ throw new Error("session directory escapes the authorized state directory");
6438
+ }
6439
+ candidates.push({ sessionDir, sessionId: record.session_id, record });
6440
+ }
6441
+ catch (err) {
6442
+ throw new Error(`Refusing cancellation because detached run authority is invalid: ${record.run_dir}.`, { cause: err });
6443
+ }
6444
+ }
6445
+ if (candidates.length > 1)
6446
+ throw new Error("Refusing cancellation because multiple detached run authorities match.");
6447
+ if (candidates.length === 0)
6448
+ return null;
6449
+ const [{ sessionDir, sessionId, record }] = candidates;
6450
+ const refs = [];
6451
+ const stateFiles = await readdir(sessionDir).catch(() => []);
6452
+ for (const file of stateFiles) {
6453
+ if (!isModeStateFilename(file))
6454
+ continue;
6455
+ const path = join(sessionDir, file);
6456
+ try {
6457
+ const fileStat = lstatSync(path);
6458
+ if (!fileStat.isFile() || fileStat.isSymbolicLink()) {
6459
+ throw new Error(`Refusing cancellation through non-regular run state target: ${path}.`);
6460
+ }
6461
+ const canonicalFile = realpathSync(path);
6462
+ if (!isCanonicalPathWithin(sessionDir, canonicalFile)) {
6463
+ throw new Error(`Refusing cancellation outside authorized run session: ${path}.`);
6464
+ }
6465
+ refs.push({
6466
+ mode: file.slice(0, -"-state.json".length),
6467
+ path: canonicalFile,
6468
+ scope: "session",
6469
+ });
6470
+ }
6471
+ catch (err) {
6472
+ throw new Error(`Refusing cancellation because detached run state authority is invalid: ${path}.`, { cause: err });
6473
+ }
6474
+ }
6475
+ return { refs: refs.sort((a, b) => a.mode.localeCompare(b.mode)), sessionId, sessionDir, record };
6476
+ }
6477
+ function assertCancellationAuthorityPath(baseStateDir, authorityRoot) {
6478
+ const resolvedBase = resolve(baseStateDir);
6479
+ const resolvedAuthority = resolve(authorityRoot);
6480
+ const baseStat = lstatSync(resolvedBase);
6481
+ if (!baseStat.isDirectory() || baseStat.isSymbolicLink() || realpathSync(resolvedBase) !== resolvedBase) {
6482
+ throw new Error(`Refusing cancellation through symlinked state authority root: ${baseStateDir}.`);
6483
+ }
6484
+ if (!isCanonicalPathWithin(resolvedBase, resolvedAuthority, true)) {
6485
+ throw new Error(`Refusing cancellation outside base state authority: ${authorityRoot}.`);
6486
+ }
6487
+ let current = resolvedBase;
6488
+ const rel = relative(resolvedBase, resolvedAuthority);
6489
+ for (const component of rel ? rel.split(/[\\/]+/) : []) {
6490
+ current = join(current, component);
6491
+ const componentStat = lstatSync(current);
6492
+ if (!componentStat.isDirectory() || componentStat.isSymbolicLink() || realpathSync(current) !== current) {
6493
+ throw new Error(`Refusing cancellation through symlinked authority component: ${current}.`);
6494
+ }
6495
+ }
6496
+ return resolvedAuthority;
6497
+ }
6498
+ function collectCancellationOwnerIds(baseStateDir, sessionId) {
6499
+ const ownerIds = new Set();
6500
+ if (sessionId)
6501
+ ownerIds.add(sessionId);
6502
+ try {
6503
+ const pointer = JSON.parse(readFileSync(join(baseStateDir, "session.json"), "utf-8"));
6504
+ for (const field of [
6505
+ "session_id",
6506
+ "native_session_id",
6507
+ "codex_session_id",
6508
+ "owner_omx_session_id",
6509
+ "owner_codex_session_id",
6510
+ ]) {
6511
+ const normalized = normalizeSessionId(pointer[field]);
6512
+ if (normalized)
6513
+ ownerIds.add(normalized);
6514
+ }
6515
+ }
6516
+ catch { }
6517
+ return ownerIds;
6518
+ }
6519
+ function assertCancellationOwnerMetadata(value, ownerIds, path) {
6520
+ if (Object.prototype.hasOwnProperty.call(value, "session_id")) {
6521
+ const sessionId = normalizeSessionId(value.session_id);
6522
+ if (!sessionId || !ownerIds.has(sessionId)) {
6523
+ throw new Error(`Refusing contradictory session_id in ${path}.`);
6524
+ }
6525
+ }
6526
+ for (const ownerField of ["owner_omx_session_id", "owner_codex_session_id"]) {
6527
+ if (!Object.prototype.hasOwnProperty.call(value, ownerField))
6528
+ continue;
6529
+ const rawOwner = value[ownerField];
6530
+ const blankOwner = typeof rawOwner === "string" && rawOwner.trim() === "";
6531
+ const owner = normalizeSessionId(rawOwner);
6532
+ if (!blankOwner && (!owner || !ownerIds.has(owner))) {
6533
+ throw new Error(`Refusing contradictory ${ownerField} in ${path}.`);
6534
+ }
6535
+ }
6536
+ }
6537
+ async function resolveExactSessionCancellationAuthority(cwd, baseStateDir, sessionId) {
6538
+ if (!sessionId)
6539
+ return undefined;
6540
+ let pointer;
6541
+ let authorityCwd = cwd;
6542
+ try {
6543
+ const rawPointer = JSON.parse(await readFile(join(baseStateDir, "session.json"), "utf-8"));
6544
+ if (typeof rawPointer.cwd === "string" && rawPointer.cwd.trim())
6545
+ authorityCwd = rawPointer.cwd.trim();
6546
+ pointer = await readSessionPointer(resolveSessionPointerContext(authorityCwd));
6547
+ }
6548
+ catch {
6549
+ return undefined;
6550
+ }
6551
+ const nativeSessionId = normalizeSessionId(pointer.state?.native_session_id);
6552
+ if (pointer.status !== "usable" || pointer.state?.session_id !== sessionId || !nativeSessionId)
6553
+ return undefined;
6554
+ try {
6555
+ const ownerEvidence = await readNativeSessionOwnerEvidence(authorityCwd, nativeSessionId);
6556
+ if (ownerEvidence.status !== "usable")
6557
+ return undefined;
6558
+ }
6559
+ catch {
6560
+ return undefined;
6561
+ }
6562
+ return { sessionId, nativeSessionId, authorityCwd };
6563
+ }
6564
+ function assertExactSkillOwner(value, authority, path, allowStaleTopCodexOwner = false) {
6565
+ if (Object.prototype.hasOwnProperty.call(value, "session_id")
6566
+ && normalizeSessionId(value.session_id) !== authority.sessionId) {
6567
+ throw new Error(`Refusing contradictory session_id in ${path}.`);
6568
+ }
6569
+ if (Object.prototype.hasOwnProperty.call(value, "owner_omx_session_id")) {
6570
+ const rawOwner = value.owner_omx_session_id;
6571
+ const blankOwner = typeof rawOwner === "string" && rawOwner.trim() === "";
6572
+ const owner = normalizeSessionId(rawOwner);
6573
+ if (!blankOwner && (!owner || owner !== authority.sessionId)) {
6574
+ throw new Error(`Refusing contradictory owner_omx_session_id in ${path}.`);
6575
+ }
6576
+ }
6577
+ if (Object.prototype.hasOwnProperty.call(value, "owner_codex_session_id")) {
6578
+ const rawOwner = value.owner_codex_session_id;
6579
+ const blankOwner = typeof rawOwner === "string" && rawOwner.trim() === "";
6580
+ const owner = normalizeSessionId(rawOwner);
6581
+ if (!blankOwner && (!owner || (!allowStaleTopCodexOwner && owner !== authority.nativeSessionId))) {
6582
+ throw new Error(`Refusing contradictory owner_codex_session_id in ${path}.`);
6583
+ }
6584
+ }
6585
+ }
6586
+ function assertCompatibleNestedSkillOwners(value, authority, path) {
6587
+ if (!Object.prototype.hasOwnProperty.call(value, "active_skills"))
6588
+ return;
6589
+ if (!Array.isArray(value.active_skills)) {
6590
+ throw new Error(`Refusing malformed active_skills in ${path}.`);
6591
+ }
6592
+ for (const [index, skill] of value.active_skills.entries()) {
6593
+ const entryPath = `${path} active_skills[${index}]`;
6594
+ if (!skill || typeof skill !== "object" || Array.isArray(skill)) {
6595
+ throw new Error(`Refusing malformed active_skills entry ${index} in ${path}.`);
6596
+ }
6597
+ const entry = skill;
6598
+ if (typeof entry.skill !== "string" || entry.skill.trim() === "") {
6599
+ throw new Error(`Refusing malformed skill in ${entryPath}.`);
6600
+ }
6601
+ for (const field of ["active"]) {
6602
+ if (Object.prototype.hasOwnProperty.call(entry, field) && typeof entry[field] !== "boolean") {
6603
+ throw new Error(`Refusing malformed ${field} in ${entryPath}.`);
6604
+ }
6605
+ }
6606
+ if (Object.prototype.hasOwnProperty.call(entry, "phase") && typeof entry.phase !== "string") {
6607
+ throw new Error(`Refusing malformed phase in ${entryPath}.`);
6608
+ }
6609
+ for (const [field, fieldValue] of Object.entries(entry)) {
6610
+ if (field.endsWith("_at") && typeof fieldValue !== "string") {
6611
+ throw new Error(`Refusing malformed ${field} in ${entryPath}.`);
6612
+ }
6613
+ }
6614
+ assertExactSkillOwner(entry, authority, entryPath);
6615
+ }
6616
+ }
6617
+ async function cancelModes(args = [], options = {}) {
6618
+ const cwd = options.cwd ?? process.cwd();
5225
6619
  const nowIso = new Date().toISOString();
5226
- const force = args.includes("--force");
6620
+ if (args.length > 1 || args.some((arg) => arg !== "--force")) {
6621
+ const unsupported = args.find((arg) => arg !== "--force") ?? args[1] ?? "";
6622
+ throw new Error(unsupported === "--all"
6623
+ ? "omx cancel --all is not supported; broad workspace cancellation requires a separate command."
6624
+ : `Unknown cancel argument: ${unsupported}`);
6625
+ }
6626
+ const force = args.length === 1;
5227
6627
  try {
5228
6628
  const writableScope = await resolveWritableStateScope(cwd);
5229
- const loadStates = async (refs) => {
6629
+ const baseStateDir = getBaseStateDir(cwd);
6630
+ const exactAuthority = writableScope.source === "session"
6631
+ ? await resolveExactSessionCancellationAuthority(cwd, baseStateDir, writableScope.sessionId)
6632
+ : undefined;
6633
+ const expectedOwnerIds = collectCancellationOwnerIds(baseStateDir, writableScope.sessionId);
6634
+ const loadStates = async (refs, authorityRoot, ownerIds = expectedOwnerIds) => {
5230
6635
  const loaded = new Map();
6636
+ if (refs.length === 0)
6637
+ return loaded;
6638
+ const canonicalAuthorityRoot = assertCancellationAuthorityPath(authorityRoot === writableScope.stateDir ? baseStateDir : authorityRoot, authorityRoot);
5231
6639
  for (const ref of refs) {
5232
- const content = await readFile(ref.path, "utf-8");
6640
+ const fileStat = lstatSync(ref.path);
6641
+ if (!fileStat.isFile() || fileStat.isSymbolicLink()) {
6642
+ throw new Error(`Refusing cancellation through non-regular state target ${ref.path}.`);
6643
+ }
6644
+ const canonicalPath = realpathSync(ref.path);
6645
+ const canonicalParent = realpathSync(dirname(ref.path));
6646
+ if (!isCanonicalPathWithin(canonicalAuthorityRoot, canonicalParent, true)
6647
+ || !isCanonicalPathWithin(canonicalAuthorityRoot, canonicalPath)) {
6648
+ throw new Error(`Refusing cancellation outside authorized state root: ${ref.path}.`);
6649
+ }
6650
+ const content = await readFile(canonicalPath, "utf-8");
5233
6651
  let parsedState;
5234
6652
  try {
5235
- parsedState = JSON.parse(content);
6653
+ const parsed = JSON.parse(content);
6654
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
6655
+ throw new Error("state must be a JSON object");
6656
+ }
6657
+ parsedState = parsed;
5236
6658
  }
5237
6659
  catch (err) {
5238
6660
  logCliOperationFailure(err);
5239
- continue;
6661
+ throw new Error(`Refusing partial cancellation because ${ref.path} is malformed.`, { cause: err });
6662
+ }
6663
+ if (typeof parsedState.mode === "string" && parsedState.mode !== ref.mode) {
6664
+ throw new Error(`Refusing contradictory mode state in ${ref.path}.`);
6665
+ }
6666
+ if (ref.mode === SKILL_ACTIVE_STATE_MODE && exactAuthority) {
6667
+ const hasTopCodexOwner = Object.prototype.hasOwnProperty.call(parsedState, "owner_codex_session_id");
6668
+ const displacedOwnerId = hasTopCodexOwner
6669
+ ? normalizeSessionId(parsedState.owner_codex_session_id)
6670
+ : undefined;
6671
+ const blankTopCodexOwner = typeof parsedState.owner_codex_session_id === "string"
6672
+ && parsedState.owner_codex_session_id.trim() === "";
6673
+ if (hasTopCodexOwner && !blankTopCodexOwner && !displacedOwnerId) {
6674
+ throw new Error(`Refusing contradictory owner_codex_session_id in ${ref.path}.`);
6675
+ }
6676
+ assertExactSkillOwner(parsedState, exactAuthority, ref.path, hasTopCodexOwner);
6677
+ assertCompatibleNestedSkillOwners(parsedState, exactAuthority, ref.path);
6678
+ if (displacedOwnerId && displacedOwnerId !== exactAuthority.nativeSessionId) {
6679
+ let displacedEvidence;
6680
+ try {
6681
+ displacedEvidence = await readNativeSessionOwnerEvidence(exactAuthority.authorityCwd, displacedOwnerId);
6682
+ }
6683
+ catch (error) {
6684
+ throw new Error(`Refusing ambiguous displaced owner evidence for ${ref.path}.`, { cause: error });
6685
+ }
6686
+ if (displacedEvidence.status !== "absent" && displacedEvidence.status !== "stale-dead") {
6687
+ throw new Error(`Refusing non-stale displaced owner evidence for ${ref.path}.`);
6688
+ }
6689
+ parsedState.owner_codex_session_id = exactAuthority.nativeSessionId;
6690
+ }
6691
+ }
6692
+ else {
6693
+ assertCancellationOwnerMetadata(parsedState, ownerIds, ref.path);
6694
+ if (Object.prototype.hasOwnProperty.call(parsedState, "active_skills")) {
6695
+ if (!Array.isArray(parsedState.active_skills)) {
6696
+ throw new Error(`Refusing malformed active_skills in ${ref.path}.`);
6697
+ }
6698
+ for (const [index, skill] of parsedState.active_skills.entries()) {
6699
+ if (!skill || typeof skill !== "object" || Array.isArray(skill)) {
6700
+ throw new Error(`Refusing malformed active_skills entry ${index} in ${ref.path}.`);
6701
+ }
6702
+ assertCancellationOwnerMetadata(skill, ownerIds, `${ref.path} active_skills[${index}]`);
6703
+ }
6704
+ }
5240
6705
  }
5241
6706
  loaded.set(ref.mode, {
5242
- path: ref.path,
6707
+ path: canonicalPath,
5243
6708
  scope: ref.scope,
5244
6709
  state: parsedState,
6710
+ originalContent: content,
6711
+ dev: fileStat.dev,
6712
+ ino: fileStat.ino,
5245
6713
  });
5246
6714
  }
5247
6715
  return loaded;
5248
6716
  };
5249
- let states = await loadStates(await listModeStateFilesWithScopePreference(cwd));
6717
+ const preferredRefs = writableScope.source === "root"
6718
+ ? (await readdir(writableScope.stateDir).catch(() => []))
6719
+ .filter((file) => isModeStateFilename(file))
6720
+ .map((file) => ({
6721
+ mode: file.slice(0, -"-state.json".length),
6722
+ path: join(writableScope.stateDir, file),
6723
+ scope: "root",
6724
+ }))
6725
+ : await listModeStateFilesWithScopePreference(cwd, writableScope.sessionId);
6726
+ const hasPreferredSessionStateFiles = preferredRefs.some((ref) => ref.scope === "session");
6727
+ const targetRefs = writableScope.source === "session"
6728
+ ? preferredRefs.filter((ref) => ref.scope === "session")
6729
+ : preferredRefs;
6730
+ let runSelection = null;
6731
+ let states = await loadStates(targetRefs, writableScope.stateDir, expectedOwnerIds);
5250
6732
  const hasActiveWorkflowMode = (entries) => [...entries.entries()].some(([mode, entry]) => mode !== SKILL_ACTIVE_STATE_MODE && entry.state.active === true);
5251
- if (!hasActiveWorkflowMode(states)) {
5252
- const runDirStates = await loadStates(await listHookVisibleRunDirStateRefs(cwd));
5253
- if (hasActiveWorkflowMode(runDirStates))
5254
- states = runDirStates;
6733
+ if (writableScope.source === "root"
6734
+ && !hasActiveWorkflowMode(states)
6735
+ && !hasPreferredSessionStateFiles) {
6736
+ runSelection = await selectAuthorizedHookVisibleRunDirState(cwd);
6737
+ if (runSelection) {
6738
+ const runOwnerIds = new Set([runSelection.sessionId]);
6739
+ const runDirStates = await loadStates(runSelection.refs, runSelection.sessionDir, runOwnerIds);
6740
+ if (hasActiveWorkflowMode(runDirStates))
6741
+ states = runDirStates;
6742
+ else
6743
+ runSelection = null;
6744
+ }
5255
6745
  }
5256
- const currentSessionId = writableScope.sessionId ?? "";
6746
+ const currentSessionId = writableScope.sessionId ?? runSelection?.sessionId ?? "";
5257
6747
  const changed = new Set();
5258
6748
  const reported = new Set();
5259
6749
  const cancelMode = (mode, phase = "cancelled", reportIfWasActive = true) => {
@@ -5284,13 +6774,74 @@ async function cancelModes(args = []) {
5284
6774
  if (reportIfWasActive && wasActive && mode !== SKILL_ACTIVE_STATE_MODE)
5285
6775
  reported.add(mode);
5286
6776
  };
5287
- const ralphLinksUltrawork = (state) => state.linked_ultrawork === true || state.linked_mode === "ultrawork";
6777
+ const linkedRalphMode = (state) => {
6778
+ if (state.linked_ultrawork === true || state.linked_mode === "ultrawork")
6779
+ return "ultrawork";
6780
+ if (state.linked_ecomode === true || state.linked_mode === "ecomode")
6781
+ return "ecomode";
6782
+ return undefined;
6783
+ };
6784
+ const cancelRalphSkillEntries = (linkedMode) => {
6785
+ const entry = states.get(SKILL_ACTIVE_STATE_MODE);
6786
+ if (!entry)
6787
+ return;
6788
+ const matchingSkills = new Set(["ralph", ...(linkedMode ? [linkedMode] : [])]);
6789
+ const activeSkills = Array.isArray(entry.state.active_skills)
6790
+ ? entry.state.active_skills
6791
+ : [];
6792
+ let changedSkillEntries = false;
6793
+ const nextSkills = activeSkills.map((skill) => {
6794
+ if (!skill || typeof skill !== "object" || Array.isArray(skill))
6795
+ return skill;
6796
+ const skillEntry = skill;
6797
+ if (skillEntry.active === false || typeof skillEntry.skill !== "string" || !matchingSkills.has(skillEntry.skill)) {
6798
+ return skill;
6799
+ }
6800
+ changedSkillEntries = true;
6801
+ return { ...skillEntry, active: false, phase: "cancelled" };
6802
+ });
6803
+ const remainingActiveSkills = nextSkills.filter((skill) => (!!skill && typeof skill === "object" && !Array.isArray(skill)
6804
+ && skill.active !== false));
6805
+ if (!changedSkillEntries) {
6806
+ if (entry.state.active !== true)
6807
+ return;
6808
+ if (typeof entry.state.skill !== "string" || !matchingSkills.has(entry.state.skill))
6809
+ return;
6810
+ }
6811
+ entry.state.active_skills = nextSkills;
6812
+ entry.state.updated_at = nowIso;
6813
+ entry.state.last_turn_at = nowIso;
6814
+ if (remainingActiveSkills.length > 0) {
6815
+ const primary = remainingActiveSkills[0];
6816
+ entry.state.active = true;
6817
+ entry.state.skill = primary.skill;
6818
+ if (typeof primary.phase === "string") {
6819
+ entry.state.phase = primary.phase;
6820
+ entry.state.current_phase = primary.phase;
6821
+ }
6822
+ else {
6823
+ delete entry.state.phase;
6824
+ delete entry.state.current_phase;
6825
+ }
6826
+ delete entry.state.completed_at;
6827
+ }
6828
+ else {
6829
+ entry.state.active = false;
6830
+ entry.state.skill = "";
6831
+ entry.state.phase = "cancelled";
6832
+ entry.state.current_phase = "cancelled";
6833
+ entry.state.completed_at = nowIso;
6834
+ }
6835
+ changed.add(SKILL_ACTIVE_STATE_MODE);
6836
+ };
5288
6837
  const ralph = states.get("ralph");
5289
6838
  const hadActiveRalph = !!(ralph && ralph.state.active === true);
5290
6839
  if (ralph && ralph.state.active === true) {
6840
+ const linkedMode = linkedRalphMode(ralph.state);
6841
+ if (linkedMode)
6842
+ cancelMode(linkedMode, "cancelled", true);
5291
6843
  cancelMode("ralph", "cancelled", true);
5292
- if (ralphLinksUltrawork(ralph.state))
5293
- cancelMode("ultrawork", "cancelled", true);
6844
+ cancelRalphSkillEntries(linkedMode);
5294
6845
  }
5295
6846
  if (!hadActiveRalph) {
5296
6847
  for (const [mode, entry] of states.entries()) {
@@ -5298,11 +6849,11 @@ async function cancelModes(args = []) {
5298
6849
  cancelMode(mode, "cancelled", true);
5299
6850
  }
5300
6851
  }
5301
- for (const [mode, entry] of states.entries()) {
5302
- if (!changed.has(mode))
5303
- continue;
5304
- await writeFile(entry.path, JSON.stringify(entry.state, null, 2));
5305
- }
6852
+ const assertRunAuthority = () => {
6853
+ if (runSelection && !hasMatchingMadmaxDetachedRuntimeBinding(runSelection.record)) {
6854
+ throw new Error("Refusing cancellation because detached run authority changed.");
6855
+ }
6856
+ };
5306
6857
  if (force && currentSessionId) {
5307
6858
  const stopStateEntries = [...states.entries()].filter(([mode]) => mode === "native-stop");
5308
6859
  for (const [, entry] of stopStateEntries) {
@@ -5313,10 +6864,85 @@ async function cancelModes(args = []) {
5313
6864
  continue;
5314
6865
  delete sessions[currentSessionId];
5315
6866
  entry.state.sessions = sessions;
5316
- await writeFile(entry.path, JSON.stringify(entry.state, null, 2));
5317
6867
  changed.add("native-stop");
5318
6868
  }
5319
6869
  }
6870
+ const orderedChanges = [...changed]
6871
+ .sort((left, right) => {
6872
+ if (left === "ralph")
6873
+ return 1;
6874
+ if (right === "ralph")
6875
+ return -1;
6876
+ return left.localeCompare(right);
6877
+ })
6878
+ .map((mode) => {
6879
+ const entry = states.get(mode);
6880
+ if (!entry)
6881
+ throw new Error(`Missing frozen cancellation entry for ${mode}.`);
6882
+ return { mode, entry, nextContent: JSON.stringify(entry.state, null, 2) };
6883
+ });
6884
+ const opened = [];
6885
+ try {
6886
+ for (const change of orderedChanges) {
6887
+ assertRunAuthority();
6888
+ const handle = await open(change.entry.path, fsConstants.O_RDWR | fsConstants.O_NOFOLLOW);
6889
+ const currentStat = await handle.stat();
6890
+ const currentContent = await handle.readFile({ encoding: "utf-8" });
6891
+ if (!currentStat.isFile() || currentStat.dev !== change.entry.dev || currentStat.ino !== change.entry.ino) {
6892
+ await handle.close();
6893
+ throw new Error(`Refusing cancellation because state identity changed: ${change.entry.path}.`);
6894
+ }
6895
+ if (currentContent !== change.entry.originalContent) {
6896
+ await handle.close();
6897
+ throw new Error(`Refusing cancellation because state content changed: ${change.entry.path}.`);
6898
+ }
6899
+ opened.push({ ...change, handle });
6900
+ }
6901
+ const committed = [];
6902
+ const cancellationTestWriteFailureMode = options.testFaults?.writeFailureMode;
6903
+ const cancellationTestRollbackFailureMode = options.testFaults?.rollbackFailureMode;
6904
+ try {
6905
+ for (const openedEntry of opened) {
6906
+ assertRunAuthority();
6907
+ committed.push(openedEntry);
6908
+ if (cancellationTestWriteFailureMode === openedEntry.mode) {
6909
+ throw new Error(`Injected cancellation write failure for ${openedEntry.mode}.`);
6910
+ }
6911
+ await openedEntry.handle.truncate(0);
6912
+ await openedEntry.handle.write(openedEntry.nextContent, 0, "utf-8");
6913
+ await openedEntry.handle.sync();
6914
+ }
6915
+ }
6916
+ catch (writeError) {
6917
+ let rollbackFailureCount = 0;
6918
+ const rollbackFailureSample = [];
6919
+ for (const committedEntry of committed.reverse()) {
6920
+ try {
6921
+ if (cancellationTestRollbackFailureMode === committedEntry.mode) {
6922
+ throw new Error(`Injected cancellation rollback failure for ${committedEntry.mode}.`);
6923
+ }
6924
+ await committedEntry.handle.truncate(0);
6925
+ await committedEntry.handle.write(committedEntry.entry.originalContent, 0, "utf-8");
6926
+ await committedEntry.handle.sync();
6927
+ }
6928
+ catch {
6929
+ rollbackFailureCount += 1;
6930
+ if (rollbackFailureSample.length < 3) {
6931
+ rollbackFailureSample.push(committedEntry.mode.replace(/[^A-Za-z0-9_-]/g, "_"));
6932
+ }
6933
+ }
6934
+ }
6935
+ if (rollbackFailureCount > 0) {
6936
+ const primaryMessage = writeError instanceof Error ? writeError.message : String(writeError);
6937
+ const sample = rollbackFailureSample.length > 0 ? `:${rollbackFailureSample.join(",")}` : "";
6938
+ throw new Error(`${primaryMessage} (cancellation_rollback_failed:${rollbackFailureCount}${sample}).`, { cause: writeError });
6939
+ }
6940
+ throw writeError;
6941
+ }
6942
+ }
6943
+ finally {
6944
+ await Promise.all(opened.map(({ handle }) => handle.close().catch(() => undefined)));
6945
+ }
5320
6946
  for (const mode of reported) {
5321
6947
  console.log(`Cancelled: ${mode}`);
5322
6948
  }
@@ -5326,7 +6952,11 @@ async function cancelModes(args = []) {
5326
6952
  }
5327
6953
  catch (err) {
5328
6954
  logCliOperationFailure(err);
5329
- console.log("No active modes to cancel.");
6955
+ process.exitCode = 1;
5330
6956
  }
5331
6957
  }
6958
+ /** Test-only entrypoint for deterministic transaction fault injection without runtime environment controls. */
6959
+ export async function cancelModesForTest(cwd, args, testFaults) {
6960
+ await cancelModes(args, { cwd, testFaults });
6961
+ }
5332
6962
  //# sourceMappingURL=index.js.map