oh-my-codex 0.18.6 → 0.18.8

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 (444) hide show
  1. package/Cargo.lock +6 -6
  2. package/Cargo.toml +1 -1
  3. package/README.md +59 -10
  4. package/crates/omx-sparkshell/tests/execution.rs +1 -1
  5. package/dist/agents/__tests__/definitions.test.js +11 -0
  6. package/dist/agents/__tests__/definitions.test.js.map +1 -1
  7. package/dist/agents/__tests__/native-config.test.js +56 -6
  8. package/dist/agents/__tests__/native-config.test.js.map +1 -1
  9. package/dist/agents/definitions.d.ts +10 -0
  10. package/dist/agents/definitions.d.ts.map +1 -1
  11. package/dist/agents/definitions.js +5 -1
  12. package/dist/agents/definitions.js.map +1 -1
  13. package/dist/agents/native-config.d.ts +5 -1
  14. package/dist/agents/native-config.d.ts.map +1 -1
  15. package/dist/agents/native-config.js +19 -4
  16. package/dist/agents/native-config.js.map +1 -1
  17. package/dist/autopilot/__tests__/fsm.test.d.ts +2 -0
  18. package/dist/autopilot/__tests__/fsm.test.d.ts.map +1 -0
  19. package/dist/autopilot/__tests__/fsm.test.js +75 -0
  20. package/dist/autopilot/__tests__/fsm.test.js.map +1 -0
  21. package/dist/autopilot/__tests__/ralplan-gate.test.d.ts +2 -0
  22. package/dist/autopilot/__tests__/ralplan-gate.test.d.ts.map +1 -0
  23. package/dist/autopilot/__tests__/ralplan-gate.test.js +79 -0
  24. package/dist/autopilot/__tests__/ralplan-gate.test.js.map +1 -0
  25. package/dist/autopilot/deep-interview-gate.d.ts +18 -0
  26. package/dist/autopilot/deep-interview-gate.d.ts.map +1 -0
  27. package/dist/autopilot/deep-interview-gate.js +256 -0
  28. package/dist/autopilot/deep-interview-gate.js.map +1 -0
  29. package/dist/autopilot/fsm.d.ts +13 -0
  30. package/dist/autopilot/fsm.d.ts.map +1 -0
  31. package/dist/autopilot/fsm.js +70 -0
  32. package/dist/autopilot/fsm.js.map +1 -0
  33. package/dist/autopilot/ralplan-gate.d.ts +17 -0
  34. package/dist/autopilot/ralplan-gate.d.ts.map +1 -0
  35. package/dist/autopilot/ralplan-gate.js +61 -0
  36. package/dist/autopilot/ralplan-gate.js.map +1 -0
  37. package/dist/cli/__tests__/codex-plugin-layout.test.js +512 -1
  38. package/dist/cli/__tests__/codex-plugin-layout.test.js.map +1 -1
  39. package/dist/cli/__tests__/doctor-warning-copy.test.js +39 -0
  40. package/dist/cli/__tests__/doctor-warning-copy.test.js.map +1 -1
  41. package/dist/cli/__tests__/index.test.js +83 -7
  42. package/dist/cli/__tests__/index.test.js.map +1 -1
  43. package/dist/cli/__tests__/launch-fallback.test.js +175 -6
  44. package/dist/cli/__tests__/launch-fallback.test.js.map +1 -1
  45. package/dist/cli/__tests__/package-bin-contract.test.js +8 -4
  46. package/dist/cli/__tests__/package-bin-contract.test.js.map +1 -1
  47. package/dist/cli/__tests__/question.test.js +100 -0
  48. package/dist/cli/__tests__/question.test.js.map +1 -1
  49. package/dist/cli/__tests__/ralph-goal-mode-contract.test.js +13 -0
  50. package/dist/cli/__tests__/ralph-goal-mode-contract.test.js.map +1 -1
  51. package/dist/cli/__tests__/ralph.test.js +14 -0
  52. package/dist/cli/__tests__/ralph.test.js.map +1 -1
  53. package/dist/cli/__tests__/setup-install-mode.test.js +89 -0
  54. package/dist/cli/__tests__/setup-install-mode.test.js.map +1 -1
  55. package/dist/cli/__tests__/setup-refresh.test.js +83 -0
  56. package/dist/cli/__tests__/setup-refresh.test.js.map +1 -1
  57. package/dist/cli/__tests__/state.test.js +21 -0
  58. package/dist/cli/__tests__/state.test.js.map +1 -1
  59. package/dist/cli/__tests__/team.test.js +2 -2
  60. package/dist/cli/__tests__/team.test.js.map +1 -1
  61. package/dist/cli/__tests__/update.test.js +110 -2
  62. package/dist/cli/__tests__/update.test.js.map +1 -1
  63. package/dist/cli/doctor.d.ts.map +1 -1
  64. package/dist/cli/doctor.js +8 -1
  65. package/dist/cli/doctor.js.map +1 -1
  66. package/dist/cli/index.d.ts +14 -3
  67. package/dist/cli/index.d.ts.map +1 -1
  68. package/dist/cli/index.js +298 -50
  69. package/dist/cli/index.js.map +1 -1
  70. package/dist/cli/plugin-marketplace.d.ts +14 -2
  71. package/dist/cli/plugin-marketplace.d.ts.map +1 -1
  72. package/dist/cli/plugin-marketplace.js +62 -15
  73. package/dist/cli/plugin-marketplace.js.map +1 -1
  74. package/dist/cli/question.d.ts.map +1 -1
  75. package/dist/cli/question.js +36 -5
  76. package/dist/cli/question.js.map +1 -1
  77. package/dist/cli/ralph.d.ts.map +1 -1
  78. package/dist/cli/ralph.js +3 -1
  79. package/dist/cli/ralph.js.map +1 -1
  80. package/dist/cli/setup-preferences.d.ts +2 -0
  81. package/dist/cli/setup-preferences.d.ts.map +1 -1
  82. package/dist/cli/setup-preferences.js +4 -0
  83. package/dist/cli/setup-preferences.js.map +1 -1
  84. package/dist/cli/setup.d.ts +3 -0
  85. package/dist/cli/setup.d.ts.map +1 -1
  86. package/dist/cli/setup.js +166 -27
  87. package/dist/cli/setup.js.map +1 -1
  88. package/dist/cli/state.d.ts.map +1 -1
  89. package/dist/cli/state.js +8 -1
  90. package/dist/cli/state.js.map +1 -1
  91. package/dist/cli/tmux-hook.d.ts.map +1 -1
  92. package/dist/cli/tmux-hook.js +16 -0
  93. package/dist/cli/tmux-hook.js.map +1 -1
  94. package/dist/cli/update.d.ts +2 -0
  95. package/dist/cli/update.d.ts.map +1 -1
  96. package/dist/cli/update.js +47 -3
  97. package/dist/cli/update.js.map +1 -1
  98. package/dist/config/__tests__/deep-interview.test.js +7 -6
  99. package/dist/config/__tests__/deep-interview.test.js.map +1 -1
  100. package/dist/config/__tests__/generator-notify.test.js +1 -0
  101. package/dist/config/__tests__/generator-notify.test.js.map +1 -1
  102. package/dist/config/deep-interview.d.ts.map +1 -1
  103. package/dist/config/deep-interview.js +14 -4
  104. package/dist/config/deep-interview.js.map +1 -1
  105. package/dist/config/generator.d.ts +2 -2
  106. package/dist/config/generator.d.ts.map +1 -1
  107. package/dist/config/generator.js +2 -2
  108. package/dist/config/generator.js.map +1 -1
  109. package/dist/config/team-mode.d.ts +12 -0
  110. package/dist/config/team-mode.d.ts.map +1 -0
  111. package/dist/config/team-mode.js +91 -0
  112. package/dist/config/team-mode.js.map +1 -0
  113. package/dist/hooks/__tests__/agents-overlay.test.js +88 -0
  114. package/dist/hooks/__tests__/agents-overlay.test.js.map +1 -1
  115. package/dist/hooks/__tests__/autopilot-skill-contract.test.js +8 -0
  116. package/dist/hooks/__tests__/autopilot-skill-contract.test.js.map +1 -1
  117. package/dist/hooks/__tests__/code-review-skill-contract.test.js +8 -0
  118. package/dist/hooks/__tests__/code-review-skill-contract.test.js.map +1 -1
  119. package/dist/hooks/__tests__/deep-interview-contract.test.js +10 -0
  120. package/dist/hooks/__tests__/deep-interview-contract.test.js.map +1 -1
  121. package/dist/hooks/__tests__/keyword-detector.test.js +1072 -14
  122. package/dist/hooks/__tests__/keyword-detector.test.js.map +1 -1
  123. package/dist/hooks/__tests__/notify-fallback-watcher.test.js +64 -1
  124. package/dist/hooks/__tests__/notify-fallback-watcher.test.js.map +1 -1
  125. package/dist/hooks/__tests__/notify-hook-auto-nudge.test.js +189 -0
  126. package/dist/hooks/__tests__/notify-hook-auto-nudge.test.js.map +1 -1
  127. package/dist/hooks/__tests__/notify-hook-team-leader-nudge.test.js +35 -2
  128. package/dist/hooks/__tests__/notify-hook-team-leader-nudge.test.js.map +1 -1
  129. package/dist/hooks/__tests__/notify-hook-tmux-heal.test.js +3 -3
  130. package/dist/hooks/__tests__/notify-hook-tmux-heal.test.js.map +1 -1
  131. package/dist/hooks/__tests__/session.test.js +25 -0
  132. package/dist/hooks/__tests__/session.test.js.map +1 -1
  133. package/dist/hooks/__tests__/skill-guidance-contract.test.js +21 -0
  134. package/dist/hooks/__tests__/skill-guidance-contract.test.js.map +1 -1
  135. package/dist/hooks/agents-overlay.d.ts.map +1 -1
  136. package/dist/hooks/agents-overlay.js +36 -50
  137. package/dist/hooks/agents-overlay.js.map +1 -1
  138. package/dist/hooks/deep-interview-config-instruction.js +1 -1
  139. package/dist/hooks/deep-interview-config-instruction.js.map +1 -1
  140. package/dist/hooks/extensibility/__tests__/plugin-runner.test.js +31 -0
  141. package/dist/hooks/extensibility/__tests__/plugin-runner.test.js.map +1 -1
  142. package/dist/hooks/extensibility/plugin-runner.js +17 -21
  143. package/dist/hooks/extensibility/plugin-runner.js.map +1 -1
  144. package/dist/hooks/keyword-detector.d.ts +1 -0
  145. package/dist/hooks/keyword-detector.d.ts.map +1 -1
  146. package/dist/hooks/keyword-detector.js +428 -32
  147. package/dist/hooks/keyword-detector.js.map +1 -1
  148. package/dist/hooks/keyword-registry.d.ts.map +1 -1
  149. package/dist/hooks/keyword-registry.js +1 -0
  150. package/dist/hooks/keyword-registry.js.map +1 -1
  151. package/dist/hooks/prompt-guidance-contract.d.ts.map +1 -1
  152. package/dist/hooks/prompt-guidance-contract.js +6 -0
  153. package/dist/hooks/prompt-guidance-contract.js.map +1 -1
  154. package/dist/hooks/session.d.ts +3 -0
  155. package/dist/hooks/session.d.ts.map +1 -1
  156. package/dist/hooks/session.js +13 -5
  157. package/dist/hooks/session.js.map +1 -1
  158. package/dist/hud/__tests__/authority.test.js +469 -31
  159. package/dist/hud/__tests__/authority.test.js.map +1 -1
  160. package/dist/hud/__tests__/hud-tmux-injection.test.js +2 -1
  161. package/dist/hud/__tests__/hud-tmux-injection.test.js.map +1 -1
  162. package/dist/hud/__tests__/index.test.js +210 -2
  163. package/dist/hud/__tests__/index.test.js.map +1 -1
  164. package/dist/hud/__tests__/reconcile.test.js +588 -28
  165. package/dist/hud/__tests__/reconcile.test.js.map +1 -1
  166. package/dist/hud/__tests__/render.test.js +61 -0
  167. package/dist/hud/__tests__/render.test.js.map +1 -1
  168. package/dist/hud/__tests__/state.test.js +208 -0
  169. package/dist/hud/__tests__/state.test.js.map +1 -1
  170. package/dist/hud/__tests__/tmux.test.js +314 -22
  171. package/dist/hud/__tests__/tmux.test.js.map +1 -1
  172. package/dist/hud/authority.d.ts +5 -0
  173. package/dist/hud/authority.d.ts.map +1 -1
  174. package/dist/hud/authority.js +337 -30
  175. package/dist/hud/authority.js.map +1 -1
  176. package/dist/hud/index.d.ts +20 -2
  177. package/dist/hud/index.d.ts.map +1 -1
  178. package/dist/hud/index.js +103 -26
  179. package/dist/hud/index.js.map +1 -1
  180. package/dist/hud/reconcile.d.ts +3 -3
  181. package/dist/hud/reconcile.d.ts.map +1 -1
  182. package/dist/hud/reconcile.js +129 -20
  183. package/dist/hud/reconcile.js.map +1 -1
  184. package/dist/hud/render.d.ts.map +1 -1
  185. package/dist/hud/render.js +35 -0
  186. package/dist/hud/render.js.map +1 -1
  187. package/dist/hud/state.d.ts.map +1 -1
  188. package/dist/hud/state.js +64 -50
  189. package/dist/hud/state.js.map +1 -1
  190. package/dist/hud/tmux.d.ts +26 -6
  191. package/dist/hud/tmux.d.ts.map +1 -1
  192. package/dist/hud/tmux.js +173 -38
  193. package/dist/hud/tmux.js.map +1 -1
  194. package/dist/hud/types.d.ts +11 -0
  195. package/dist/hud/types.d.ts.map +1 -1
  196. package/dist/hud/types.js.map +1 -1
  197. package/dist/mcp/__tests__/hermes-bridge.test.js +203 -7
  198. package/dist/mcp/__tests__/hermes-bridge.test.js.map +1 -1
  199. package/dist/mcp/__tests__/state-paths.test.js +71 -1
  200. package/dist/mcp/__tests__/state-paths.test.js.map +1 -1
  201. package/dist/mcp/__tests__/state-server.test.js +13 -1
  202. package/dist/mcp/__tests__/state-server.test.js.map +1 -1
  203. package/dist/mcp/hermes-bridge.d.ts +12 -2
  204. package/dist/mcp/hermes-bridge.d.ts.map +1 -1
  205. package/dist/mcp/hermes-bridge.js +83 -9
  206. package/dist/mcp/hermes-bridge.js.map +1 -1
  207. package/dist/mcp/state-paths.d.ts +32 -0
  208. package/dist/mcp/state-paths.d.ts.map +1 -1
  209. package/dist/mcp/state-paths.js +113 -17
  210. package/dist/mcp/state-paths.js.map +1 -1
  211. package/dist/mcp/state-server.d.ts +4 -4
  212. package/dist/modes/__tests__/base-autoresearch-contract.test.js +7 -1
  213. package/dist/modes/__tests__/base-autoresearch-contract.test.js.map +1 -1
  214. package/dist/pipeline/__tests__/stages.test.js +130 -0
  215. package/dist/pipeline/__tests__/stages.test.js.map +1 -1
  216. package/dist/pipeline/orchestrator.js +1 -1
  217. package/dist/pipeline/orchestrator.js.map +1 -1
  218. package/dist/pipeline/stages/ralplan.d.ts +1 -0
  219. package/dist/pipeline/stages/ralplan.d.ts.map +1 -1
  220. package/dist/pipeline/stages/ralplan.js +14 -5
  221. package/dist/pipeline/stages/ralplan.js.map +1 -1
  222. package/dist/question/__tests__/deep-interview.test.js +160 -2
  223. package/dist/question/__tests__/deep-interview.test.js.map +1 -1
  224. package/dist/question/__tests__/policy.test.js +63 -3
  225. package/dist/question/__tests__/policy.test.js.map +1 -1
  226. package/dist/question/__tests__/renderer.test.js +191 -2
  227. package/dist/question/__tests__/renderer.test.js.map +1 -1
  228. package/dist/question/__tests__/state.test.js +94 -3
  229. package/dist/question/__tests__/state.test.js.map +1 -1
  230. package/dist/question/__tests__/ui.test.js +4 -0
  231. package/dist/question/__tests__/ui.test.js.map +1 -1
  232. package/dist/question/autopilot-wait.d.ts +12 -2
  233. package/dist/question/autopilot-wait.d.ts.map +1 -1
  234. package/dist/question/autopilot-wait.js +158 -47
  235. package/dist/question/autopilot-wait.js.map +1 -1
  236. package/dist/question/deep-interview.d.ts.map +1 -1
  237. package/dist/question/deep-interview.js +22 -6
  238. package/dist/question/deep-interview.js.map +1 -1
  239. package/dist/question/policy.d.ts.map +1 -1
  240. package/dist/question/policy.js +2 -5
  241. package/dist/question/policy.js.map +1 -1
  242. package/dist/question/renderer.d.ts +12 -0
  243. package/dist/question/renderer.d.ts.map +1 -1
  244. package/dist/question/renderer.js +87 -3
  245. package/dist/question/renderer.js.map +1 -1
  246. package/dist/question/state.d.ts +8 -1
  247. package/dist/question/state.d.ts.map +1 -1
  248. package/dist/question/state.js +54 -14
  249. package/dist/question/state.js.map +1 -1
  250. package/dist/question/types.d.ts +1 -1
  251. package/dist/question/types.d.ts.map +1 -1
  252. package/dist/question/ui.d.ts +1 -0
  253. package/dist/question/ui.d.ts.map +1 -1
  254. package/dist/question/ui.js +1 -0
  255. package/dist/question/ui.js.map +1 -1
  256. package/dist/ralplan/__tests__/runtime.test.js +191 -0
  257. package/dist/ralplan/__tests__/runtime.test.js.map +1 -1
  258. package/dist/ralplan/consensus-gate.d.ts +9 -1
  259. package/dist/ralplan/consensus-gate.d.ts.map +1 -1
  260. package/dist/ralplan/consensus-gate.js +84 -2
  261. package/dist/ralplan/consensus-gate.js.map +1 -1
  262. package/dist/ralplan/runtime.d.ts +9 -0
  263. package/dist/ralplan/runtime.d.ts.map +1 -1
  264. package/dist/ralplan/runtime.js +32 -11
  265. package/dist/ralplan/runtime.js.map +1 -1
  266. package/dist/scripts/__tests__/codex-native-hook.test.js +2315 -280
  267. package/dist/scripts/__tests__/codex-native-hook.test.js.map +1 -1
  268. package/dist/scripts/__tests__/notify-state-io.test.js +72 -1
  269. package/dist/scripts/__tests__/notify-state-io.test.js.map +1 -1
  270. package/dist/scripts/__tests__/notify-tmux-injection.test.d.ts +2 -0
  271. package/dist/scripts/__tests__/notify-tmux-injection.test.d.ts.map +1 -0
  272. package/dist/scripts/__tests__/notify-tmux-injection.test.js +57 -0
  273. package/dist/scripts/__tests__/notify-tmux-injection.test.js.map +1 -0
  274. package/dist/scripts/__tests__/run-test-files.test.js +74 -0
  275. package/dist/scripts/__tests__/run-test-files.test.js.map +1 -1
  276. package/dist/scripts/__tests__/verify-native-agents.test.js +65 -0
  277. package/dist/scripts/__tests__/verify-native-agents.test.js.map +1 -1
  278. package/dist/scripts/codex-native-hook.d.ts.map +1 -1
  279. package/dist/scripts/codex-native-hook.js +431 -56
  280. package/dist/scripts/codex-native-hook.js.map +1 -1
  281. package/dist/scripts/codex-native-pre-post.d.ts.map +1 -1
  282. package/dist/scripts/codex-native-pre-post.js +79 -1
  283. package/dist/scripts/codex-native-pre-post.js.map +1 -1
  284. package/dist/scripts/eval/eval-parity-smoke.js +1 -1
  285. package/dist/scripts/eval/eval-parity-smoke.js.map +1 -1
  286. package/dist/scripts/hook-payload-guard.d.ts +9 -0
  287. package/dist/scripts/hook-payload-guard.d.ts.map +1 -0
  288. package/dist/scripts/hook-payload-guard.js +111 -0
  289. package/dist/scripts/hook-payload-guard.js.map +1 -0
  290. package/dist/scripts/notify-fallback-watcher.js +8 -1
  291. package/dist/scripts/notify-fallback-watcher.js.map +1 -1
  292. package/dist/scripts/notify-hook/__tests__/payload-guard.test.d.ts +2 -0
  293. package/dist/scripts/notify-hook/__tests__/payload-guard.test.d.ts.map +1 -0
  294. package/dist/scripts/notify-hook/__tests__/payload-guard.test.js +39 -0
  295. package/dist/scripts/notify-hook/__tests__/payload-guard.test.js.map +1 -0
  296. package/dist/scripts/notify-hook/auto-nudge.d.ts.map +1 -1
  297. package/dist/scripts/notify-hook/auto-nudge.js +3 -1
  298. package/dist/scripts/notify-hook/auto-nudge.js.map +1 -1
  299. package/dist/scripts/notify-hook/ralph-session-resume.d.ts.map +1 -1
  300. package/dist/scripts/notify-hook/ralph-session-resume.js +3 -10
  301. package/dist/scripts/notify-hook/ralph-session-resume.js.map +1 -1
  302. package/dist/scripts/notify-hook/state-io.d.ts.map +1 -1
  303. package/dist/scripts/notify-hook/state-io.js +62 -38
  304. package/dist/scripts/notify-hook/state-io.js.map +1 -1
  305. package/dist/scripts/notify-hook/team-leader-nudge.d.ts.map +1 -1
  306. package/dist/scripts/notify-hook/team-leader-nudge.js +7 -0
  307. package/dist/scripts/notify-hook/team-leader-nudge.js.map +1 -1
  308. package/dist/scripts/notify-hook/team-worker-stop.d.ts.map +1 -1
  309. package/dist/scripts/notify-hook/team-worker-stop.js +234 -86
  310. package/dist/scripts/notify-hook/team-worker-stop.js.map +1 -1
  311. package/dist/scripts/notify-hook/tmux-injection.d.ts +7 -0
  312. package/dist/scripts/notify-hook/tmux-injection.d.ts.map +1 -1
  313. package/dist/scripts/notify-hook/tmux-injection.js +24 -18
  314. package/dist/scripts/notify-hook/tmux-injection.js.map +1 -1
  315. package/dist/scripts/notify-hook.js +86 -13
  316. package/dist/scripts/notify-hook.js.map +1 -1
  317. package/dist/scripts/run-test-files.js +193 -22
  318. package/dist/scripts/run-test-files.js.map +1 -1
  319. package/dist/scripts/sync-plugin-mirror.d.ts.map +1 -1
  320. package/dist/scripts/sync-plugin-mirror.js +61 -3
  321. package/dist/scripts/sync-plugin-mirror.js.map +1 -1
  322. package/dist/scripts/verify-native-agents.d.ts.map +1 -1
  323. package/dist/scripts/verify-native-agents.js +58 -1
  324. package/dist/scripts/verify-native-agents.js.map +1 -1
  325. package/dist/state/__tests__/operations.test.js +1125 -1
  326. package/dist/state/__tests__/operations.test.js.map +1 -1
  327. package/dist/state/__tests__/skill-active.test.js +46 -1
  328. package/dist/state/__tests__/skill-active.test.js.map +1 -1
  329. package/dist/state/__tests__/workflow-transition.test.js +98 -7
  330. package/dist/state/__tests__/workflow-transition.test.js.map +1 -1
  331. package/dist/state/operations.d.ts.map +1 -1
  332. package/dist/state/operations.js +159 -2
  333. package/dist/state/operations.js.map +1 -1
  334. package/dist/state/skill-active.js +6 -8
  335. package/dist/state/skill-active.js.map +1 -1
  336. package/dist/state/workflow-transition-reconcile.d.ts +6 -0
  337. package/dist/state/workflow-transition-reconcile.d.ts.map +1 -1
  338. package/dist/state/workflow-transition-reconcile.js +38 -15
  339. package/dist/state/workflow-transition-reconcile.js.map +1 -1
  340. package/dist/state/workflow-transition.d.ts.map +1 -1
  341. package/dist/state/workflow-transition.js +10 -3
  342. package/dist/state/workflow-transition.js.map +1 -1
  343. package/dist/subagents/__tests__/tracker.test.js +139 -0
  344. package/dist/subagents/__tests__/tracker.test.js.map +1 -1
  345. package/dist/subagents/tracker.d.ts +3 -0
  346. package/dist/subagents/tracker.d.ts.map +1 -1
  347. package/dist/subagents/tracker.js +41 -4
  348. package/dist/subagents/tracker.js.map +1 -1
  349. package/dist/team/__tests__/coordination-protocol.test.d.ts +2 -0
  350. package/dist/team/__tests__/coordination-protocol.test.d.ts.map +1 -0
  351. package/dist/team/__tests__/coordination-protocol.test.js +173 -0
  352. package/dist/team/__tests__/coordination-protocol.test.js.map +1 -0
  353. package/dist/team/__tests__/runtime.test.js +52 -3
  354. package/dist/team/__tests__/runtime.test.js.map +1 -1
  355. package/dist/team/__tests__/scaling.test.js +9 -4
  356. package/dist/team/__tests__/scaling.test.js.map +1 -1
  357. package/dist/team/__tests__/state.test.js +83 -0
  358. package/dist/team/__tests__/state.test.js.map +1 -1
  359. package/dist/team/__tests__/tmux-session.test.js +240 -2
  360. package/dist/team/__tests__/tmux-session.test.js.map +1 -1
  361. package/dist/team/__tests__/worker-bootstrap.test.js +84 -0
  362. package/dist/team/__tests__/worker-bootstrap.test.js.map +1 -1
  363. package/dist/team/__tests__/worker-runtime-identity.test.js +4 -2
  364. package/dist/team/__tests__/worker-runtime-identity.test.js.map +1 -1
  365. package/dist/team/coordination-protocol.d.ts +14 -0
  366. package/dist/team/coordination-protocol.d.ts.map +1 -0
  367. package/dist/team/coordination-protocol.js +244 -0
  368. package/dist/team/coordination-protocol.js.map +1 -0
  369. package/dist/team/runtime.d.ts +1 -0
  370. package/dist/team/runtime.d.ts.map +1 -1
  371. package/dist/team/runtime.js +19 -3
  372. package/dist/team/runtime.js.map +1 -1
  373. package/dist/team/scaling.d.ts.map +1 -1
  374. package/dist/team/scaling.js +3 -2
  375. package/dist/team/scaling.js.map +1 -1
  376. package/dist/team/state/tasks.d.ts.map +1 -1
  377. package/dist/team/state/tasks.js +24 -0
  378. package/dist/team/state/tasks.js.map +1 -1
  379. package/dist/team/state/types.d.ts +21 -1
  380. package/dist/team/state/types.d.ts.map +1 -1
  381. package/dist/team/state/types.js.map +1 -1
  382. package/dist/team/state.d.ts +17 -1
  383. package/dist/team/state.d.ts.map +1 -1
  384. package/dist/team/state.js +12 -5
  385. package/dist/team/state.js.map +1 -1
  386. package/dist/team/team-ops.d.ts +1 -1
  387. package/dist/team/team-ops.d.ts.map +1 -1
  388. package/dist/team/team-ops.js.map +1 -1
  389. package/dist/team/tmux-session.d.ts +2 -0
  390. package/dist/team/tmux-session.d.ts.map +1 -1
  391. package/dist/team/tmux-session.js +161 -13
  392. package/dist/team/tmux-session.js.map +1 -1
  393. package/dist/team/worker-bootstrap.d.ts.map +1 -1
  394. package/dist/team/worker-bootstrap.js +63 -0
  395. package/dist/team/worker-bootstrap.js.map +1 -1
  396. package/dist/utils/__tests__/agents-model-table.test.js +4 -2
  397. package/dist/utils/__tests__/agents-model-table.test.js.map +1 -1
  398. package/dist/utils/agents-model-table.d.ts.map +1 -1
  399. package/dist/utils/agents-model-table.js +3 -0
  400. package/dist/utils/agents-model-table.js.map +1 -1
  401. package/dist/verification/__tests__/ci-rust-gates.test.js +81 -1
  402. package/dist/verification/__tests__/ci-rust-gates.test.js.map +1 -1
  403. package/package.json +8 -8
  404. package/plugins/oh-my-codex/.codex-plugin/plugin.json +1 -1
  405. package/plugins/oh-my-codex/hooks/codex-native-hook.mjs +334 -21
  406. package/plugins/oh-my-codex/hooks/hooks.json +1 -2
  407. package/plugins/oh-my-codex/skills/autopilot/SKILL.md +13 -6
  408. package/plugins/oh-my-codex/skills/code-review/SKILL.md +7 -7
  409. package/plugins/oh-my-codex/skills/deep-interview/SKILL.md +9 -4
  410. package/plugins/oh-my-codex/skills/ralph/SKILL.md +22 -22
  411. package/plugins/oh-my-codex/skills/ralplan/SKILL.md +12 -0
  412. package/plugins/oh-my-codex/skills/team/SKILL.md +16 -0
  413. package/plugins/oh-my-codex/skills/ultraqa/SKILL.md +9 -0
  414. package/plugins/oh-my-codex/skills/worker/SKILL.md +14 -0
  415. package/skills/autopilot/SKILL.md +13 -6
  416. package/skills/code-review/SKILL.md +7 -7
  417. package/skills/deep-interview/SKILL.md +9 -4
  418. package/skills/ralph/SKILL.md +22 -22
  419. package/skills/ralplan/SKILL.md +12 -0
  420. package/skills/team/SKILL.md +16 -0
  421. package/skills/ultraqa/SKILL.md +9 -0
  422. package/skills/worker/SKILL.md +14 -0
  423. package/src/scripts/__tests__/codex-native-hook.test.ts +4435 -2083
  424. package/src/scripts/__tests__/notify-state-io.test.ts +95 -0
  425. package/src/scripts/__tests__/notify-tmux-injection.test.ts +82 -0
  426. package/src/scripts/__tests__/run-test-files.test.ts +102 -0
  427. package/src/scripts/__tests__/verify-native-agents.test.ts +75 -0
  428. package/src/scripts/codex-native-hook.ts +536 -51
  429. package/src/scripts/codex-native-pre-post.ts +80 -0
  430. package/src/scripts/demo-team-e2e.sh +10 -7
  431. package/src/scripts/eval/eval-parity-smoke.ts +1 -1
  432. package/src/scripts/hook-payload-guard.ts +113 -0
  433. package/src/scripts/notify-fallback-watcher.ts +8 -1
  434. package/src/scripts/notify-hook/__tests__/payload-guard.test.ts +41 -0
  435. package/src/scripts/notify-hook/auto-nudge.ts +3 -1
  436. package/src/scripts/notify-hook/ralph-session-resume.ts +2 -8
  437. package/src/scripts/notify-hook/state-io.ts +75 -37
  438. package/src/scripts/notify-hook/team-leader-nudge.ts +7 -0
  439. package/src/scripts/notify-hook/team-worker-stop.ts +193 -52
  440. package/src/scripts/notify-hook/tmux-injection.ts +35 -19
  441. package/src/scripts/notify-hook.ts +105 -6
  442. package/src/scripts/run-test-files.ts +192 -22
  443. package/src/scripts/sync-plugin-mirror.ts +98 -9
  444. package/src/scripts/verify-native-agents.ts +65 -1
@@ -1,7 +1,7 @@
1
1
  import { describe, it, mock } from 'node:test';
2
2
  import assert from 'node:assert/strict';
3
3
  import { existsSync } from 'node:fs';
4
- import { mkdtemp, mkdir, readFile, rm, writeFile } from 'node:fs/promises';
4
+ import { mkdtemp, mkdir, readFile, rm, symlink, writeFile } from 'node:fs/promises';
5
5
  import { tmpdir } from 'node:os';
6
6
  import { join } from 'node:path';
7
7
  import { detectKeywords, detectPrimaryKeyword, recordSkillActivation, DEEP_INTERVIEW_STATE_FILE, DEEP_INTERVIEW_BLOCKED_APPROVAL_INPUTS, DEEP_INTERVIEW_INPUT_LOCK_MESSAGE, persistDeepInterviewModeState, } from '../keyword-detector.js';
@@ -23,6 +23,54 @@ async function withIsolatedHome(prefix, run) {
23
23
  await rm(homeDir, { recursive: true, force: true });
24
24
  }
25
25
  }
26
+ const AUTOPILOT_TEST_NOW = '2026-05-30T00:00:00.000Z';
27
+ const AUTOPILOT_TEST_STARTED_AT = '2026-05-29T00:00:00.000Z';
28
+ const AUTOPILOT_TEST_UPDATED_AT = '2026-05-29T00:10:00.000Z';
29
+ async function writeActiveAutopilotSkillState(stateDir, sessionId, phase = 'ralplan') {
30
+ await mkdir(join(stateDir, 'sessions', sessionId), { recursive: true });
31
+ await writeFile(join(stateDir, 'sessions', sessionId, SKILL_ACTIVE_STATE_FILE), JSON.stringify({
32
+ version: 1,
33
+ active: true,
34
+ skill: 'autopilot',
35
+ keyword: '$autopilot',
36
+ phase,
37
+ activated_at: AUTOPILOT_TEST_STARTED_AT,
38
+ updated_at: AUTOPILOT_TEST_UPDATED_AT,
39
+ session_id: sessionId,
40
+ active_skills: [{ skill: 'autopilot', active: true, phase, session_id: sessionId }],
41
+ }, null, 2));
42
+ }
43
+ async function readAutopilotModeState(stateDir, sessionId) {
44
+ return JSON.parse(await readFile(join(stateDir, 'sessions', sessionId, 'autopilot-state.json'), 'utf-8'));
45
+ }
46
+ async function continueAutopilotTestState(stateDir, cwd, sessionId, suffix, text = 'continue') {
47
+ await recordSkillActivation({
48
+ stateDir,
49
+ sourceCwd: cwd,
50
+ text,
51
+ sessionId,
52
+ threadId: `thread-${suffix}`,
53
+ turnId: `turn-${suffix}`,
54
+ nowIso: AUTOPILOT_TEST_NOW,
55
+ });
56
+ }
57
+ async function assertAutopilotRecoverySnapshot(cwd, modeState, expectedPath, expectedReason) {
58
+ const snapshotPath = modeState.state?.handoff_artifacts?.context_snapshot_path ?? '';
59
+ if (typeof expectedPath === 'string')
60
+ assert.equal(snapshotPath, expectedPath);
61
+ else
62
+ assert.match(snapshotPath, expectedPath);
63
+ assert.equal(modeState.state?.handoff_artifacts?.context_snapshot?.kind, 'recovery');
64
+ assert.equal(modeState.state?.handoff_artifacts?.context_snapshot?.recovery?.reason, expectedReason);
65
+ assert.equal(modeState.state?.context_snapshot_recovery?.status, 'degraded');
66
+ assert.equal(modeState.state?.context_snapshot_recovery?.reason, expectedReason);
67
+ const recoverySnapshot = await readFile(join(cwd, snapshotPath), 'utf-8');
68
+ assert.match(recoverySnapshot, /recovery status: degraded/);
69
+ assert.match(recoverySnapshot, new RegExp(`recovery reason: ${expectedReason}`));
70
+ assert.match(recoverySnapshot, /do not treat the continuation input as the task seed/);
71
+ assert.doesNotMatch(recoverySnapshot, /task seed: continue/);
72
+ return snapshotPath;
73
+ }
26
74
  describe('keyword detector team compatibility', () => {
27
75
  it('keeps explicit $skill order in detectKeywords results (left-to-right)', () => {
28
76
  const matches = detectKeywords('$analyze $ultraqa $code-review now');
@@ -133,6 +181,28 @@ describe('keyword detector team compatibility', () => {
133
181
  const pathOnly = detectPrimaryKeyword('inspect .omx/ultragoal/goals.json');
134
182
  assert.notEqual(pathOnly?.skill, 'ultragoal');
135
183
  });
184
+ it('maps bare and command-style autopilot invocations to autopilot', () => {
185
+ for (const prompt of ['autopilot', 'run autopilot', 'autopilot this', 'autopilot mode']) {
186
+ const match = detectPrimaryKeyword(prompt);
187
+ assert.ok(match, `expected autopilot match for ${prompt}`);
188
+ assert.equal(match.skill, 'autopilot');
189
+ assert.equal(match.keyword.toLowerCase(), 'autopilot');
190
+ }
191
+ });
192
+ it('does not trigger autopilot from management/debug prose mentions', () => {
193
+ assert.equal(detectPrimaryKeyword('inspect autopilot state before continuing'), null);
194
+ assert.equal(detectPrimaryKeyword('fix the autopilot bug in the detector'), null);
195
+ assert.equal(detectPrimaryKeyword('why did autopilot fail?'), null);
196
+ assert.equal(detectPrimaryKeyword('run autopilot tests'), null);
197
+ assert.equal(detectPrimaryKeyword('run autopilot regression tests'), null);
198
+ assert.equal(detectPrimaryKeyword('continue autopilot debugging'), null);
199
+ assert.equal(detectPrimaryKeyword('start autopilot bug investigation'), null);
200
+ });
201
+ it('keeps higher-priority workflow keywords ahead of autopilot mentions', () => {
202
+ const match = detectPrimaryKeyword('autopilot this after consensus plan');
203
+ assert.ok(match);
204
+ assert.equal(match.skill, 'ralplan');
205
+ });
136
206
  it('maps code-review keyword variants to code-review skill', () => {
137
207
  const hyphen = detectPrimaryKeyword('run $code-review before merge');
138
208
  assert.ok(hyphen);
@@ -333,6 +403,7 @@ describe('keyword registry coverage', () => {
333
403
  assert.ok(registryKeywords.has('$ultragoal'));
334
404
  assert.ok(registryKeywords.has('$prometheus-strict'));
335
405
  assert.ok(registryKeywords.has('ultragoal'));
406
+ assert.ok(registryKeywords.has('autopilot'));
336
407
  });
337
408
  });
338
409
  describe('keyword detector skill-active-state lifecycle', () => {
@@ -473,6 +544,12 @@ describe('keyword detector skill-active-state lifecycle', () => {
473
544
  rationale: 'Autopilot starts at the deep-interview gate by default; clear bounded tasks may skip only with an explicit persisted skip reason.',
474
545
  });
475
546
  assert.deepEqual(modeState.state.handoff_artifacts, {
547
+ context_snapshot_path: '.omx/context/please-run-and-keep-going-20260225T000000Z.md',
548
+ context_snapshot: {
549
+ path: '.omx/context/please-run-and-keep-going-20260225T000000Z.md',
550
+ kind: 'canonical',
551
+ original_task_status: 'activation-prompt',
552
+ },
476
553
  deep_interview: null,
477
554
  ralplan: null,
478
555
  ralplan_consensus_gate: {
@@ -491,11 +568,445 @@ describe('keyword detector skill-active-state lifecycle', () => {
491
568
  assert.equal(modeState.state.review_verdict, null);
492
569
  assert.equal(modeState.state.qa_verdict, null);
493
570
  assert.equal(modeState.state.return_to_ralplan_reason, null);
571
+ const snapshot = await readFile(join(cwd, '.omx', 'context', 'please-run-and-keep-going-20260225T000000Z.md'), 'utf-8');
572
+ assert.match(snapshot, /activation prompt \/ task seed: please run \$autopilot and keep going/);
573
+ assert.match(snapshot, /scope note: this seed captures the Autopilot activation prompt/);
574
+ }
575
+ finally {
576
+ await rm(cwd, { recursive: true, force: true });
577
+ }
578
+ });
579
+ it('migrates legacy Autopilot context snapshot paths into handoff artifacts', async () => {
580
+ const cwd = await mkdtemp(join(tmpdir(), 'omx-keyword-autopilot-legacy-context-'));
581
+ const stateDir = join(cwd, '.omx', 'state');
582
+ const sessionId = 'sess-autopilot-legacy-context';
583
+ try {
584
+ await writeActiveAutopilotSkillState(stateDir, sessionId, 'deep-interview');
585
+ await writeFile(join(stateDir, 'sessions', sessionId, 'autopilot-state.json'), JSON.stringify({
586
+ active: true,
587
+ mode: 'autopilot',
588
+ current_phase: 'deep-interview',
589
+ started_at: AUTOPILOT_TEST_STARTED_AT,
590
+ context_snapshot_path: '.omx/context/legacy-task-20260529T000000Z.md',
591
+ state: { handoff_artifacts: { deep_interview: null } },
592
+ }, null, 2));
593
+ await mkdir(join(cwd, '.omx', 'context'), { recursive: true });
594
+ await writeFile(join(cwd, '.omx', 'context', 'legacy-task-20260529T000000Z.md'), '# legacy task');
595
+ await continueAutopilotTestState(stateDir, cwd, sessionId, 'legacy');
596
+ const modeState = await readAutopilotModeState(stateDir, sessionId);
597
+ assert.equal(modeState.state?.handoff_artifacts?.context_snapshot_path, '.omx/context/legacy-task-20260529T000000Z.md');
598
+ assert.deepEqual(modeState.state?.handoff_artifacts?.context_snapshot, {
599
+ path: '.omx/context/legacy-task-20260529T000000Z.md',
600
+ kind: 'legacy',
601
+ original_task_status: 'legacy-unverified',
602
+ });
603
+ assert.equal(existsSync(join(cwd, '.omx', 'context', 'continue-20260530T000000Z.md')), false);
604
+ }
605
+ finally {
606
+ await rm(cwd, { recursive: true, force: true });
607
+ }
608
+ });
609
+ it('rejects unsafe legacy Autopilot context snapshot paths without writing outside .omx/context', async () => {
610
+ const cwd = await mkdtemp(join(tmpdir(), 'omx-keyword-autopilot-unsafe-context-'));
611
+ const stateDir = join(cwd, '.omx', 'state');
612
+ const sessionId = 'sess-autopilot-unsafe-context';
613
+ try {
614
+ await writeActiveAutopilotSkillState(stateDir, sessionId, 'deep-interview');
615
+ await writeFile(join(stateDir, 'sessions', sessionId, 'autopilot-state.json'), JSON.stringify({
616
+ active: true,
617
+ mode: 'autopilot',
618
+ current_phase: 'deep-interview',
619
+ started_at: AUTOPILOT_TEST_STARTED_AT,
620
+ context_snapshot_path: '.omx/context/../../escape.md',
621
+ state: { handoff_artifacts: { deep_interview: null } },
622
+ }, null, 2));
623
+ const result = await recordSkillActivation({
624
+ stateDir,
625
+ text: 'continue',
626
+ sessionId,
627
+ threadId: 'thread-unsafe',
628
+ turnId: 'turn-unsafe',
629
+ nowIso: '2026-05-30T00:00:00.000Z',
630
+ });
631
+ assert.ok(result);
632
+ assert.equal(existsSync(join(cwd, '.omx', 'escape.md')), false);
633
+ const modeState = await readAutopilotModeState(stateDir, sessionId);
634
+ assert.equal(modeState.context_snapshot_path, undefined);
635
+ await assertAutopilotRecoverySnapshot(cwd, modeState, '.omx/context/autopilot-recovery-20260530T000000Z.md', 'missing-or-unsafe-legacy-context-snapshot');
636
+ }
637
+ finally {
638
+ await rm(cwd, { recursive: true, force: true });
639
+ }
640
+ });
641
+ it('does not snapshot bare continuation text when active Autopilot mode state is corrupt', async () => {
642
+ const expectedReasons = {
643
+ 'missing-current-phase': 'nonpreservable-autopilot-mode-state-missing-current-phase',
644
+ 'malformed-json': 'malformed-autopilot-mode-state',
645
+ 'array-json': 'malformed-autopilot-mode-state',
646
+ };
647
+ for (const fixture of ['missing-current-phase', 'malformed-json', 'array-json']) {
648
+ const cwd = await mkdtemp(join(tmpdir(), `omx-keyword-autopilot-corrupt-continuation-${fixture}-`));
649
+ const stateDir = join(cwd, '.omx', 'state');
650
+ const sessionId = `sess-autopilot-corrupt-continuation-${fixture}`;
651
+ try {
652
+ await writeActiveAutopilotSkillState(stateDir, sessionId);
653
+ const modeStatePath = join(stateDir, 'sessions', sessionId, 'autopilot-state.json');
654
+ if (fixture === 'missing-current-phase') {
655
+ await writeFile(modeStatePath, JSON.stringify({
656
+ active: true,
657
+ mode: 'autopilot',
658
+ started_at: AUTOPILOT_TEST_STARTED_AT,
659
+ state: { handoff_artifacts: {} },
660
+ }, null, 2));
661
+ }
662
+ else if (fixture === 'malformed-json') {
663
+ await writeFile(modeStatePath, '{ "active": true, "mode": "autopilot",');
664
+ }
665
+ else {
666
+ await writeFile(modeStatePath, '[]');
667
+ }
668
+ await continueAutopilotTestState(stateDir, cwd, sessionId, fixture);
669
+ assert.equal(existsSync(join(cwd, '.omx', 'context', 'continue-20260530T000000Z.md')), false);
670
+ await assertAutopilotRecoverySnapshot(cwd, JSON.parse(await readFile(modeStatePath, 'utf-8')), /^\.omx\/context\/autopilot-recovery-20260530T000000Z(?:-\d+)?\.md$/, expectedReasons[fixture]);
671
+ }
672
+ finally {
673
+ await rm(cwd, { recursive: true, force: true });
674
+ }
675
+ }
676
+ });
677
+ it('rejects nested symlink Autopilot context snapshot candidates during reuse', async () => {
678
+ const cwd = await mkdtemp(join(tmpdir(), 'omx-keyword-autopilot-nested-symlink-context-'));
679
+ const outside = await mkdtemp(join(tmpdir(), 'omx-keyword-autopilot-nested-symlink-outside-'));
680
+ const stateDir = join(cwd, '.omx', 'state');
681
+ const sessionId = 'sess-autopilot-nested-symlink-context';
682
+ try {
683
+ await mkdir(join(cwd, '.omx', 'context'), { recursive: true });
684
+ await symlink(outside, join(cwd, '.omx', 'context', 'link'));
685
+ await writeFile(join(outside, 'exfil.md'), '# outside context');
686
+ await writeActiveAutopilotSkillState(stateDir, sessionId);
687
+ await writeFile(join(stateDir, 'sessions', sessionId, 'autopilot-state.json'), JSON.stringify({
688
+ active: true,
689
+ mode: 'autopilot',
690
+ current_phase: 'ralplan',
691
+ started_at: AUTOPILOT_TEST_STARTED_AT,
692
+ state: { handoff_artifacts: { context_snapshot_path: '.omx/context/link/exfil.md' } },
693
+ }, null, 2));
694
+ await continueAutopilotTestState(stateDir, cwd, sessionId, 'nested-symlink');
695
+ await assertAutopilotRecoverySnapshot(cwd, await readAutopilotModeState(stateDir, sessionId), '.omx/context/autopilot-recovery-20260530T000000Z.md', 'missing-or-unsafe-legacy-context-snapshot');
696
+ assert.equal(existsSync(join(outside, 'exfil.md')), true);
697
+ }
698
+ finally {
699
+ await rm(cwd, { recursive: true, force: true });
700
+ await rm(outside, { recursive: true, force: true });
701
+ }
702
+ });
703
+ it('rejects typed canonical Autopilot recovery snapshot candidates during reuse', async () => {
704
+ const cwd = await mkdtemp(join(tmpdir(), 'omx-keyword-autopilot-typed-recovery-context-'));
705
+ const stateDir = join(cwd, '.omx', 'state');
706
+ const sessionId = 'sess-autopilot-typed-recovery-context';
707
+ try {
708
+ await mkdir(join(cwd, '.omx', 'context'), { recursive: true });
709
+ await writeFile(join(cwd, '.omx', 'context', 'autopilot-recovery-20260529T000000Z.md'), '# stale degraded recovery');
710
+ await writeActiveAutopilotSkillState(stateDir, sessionId);
711
+ await writeFile(join(stateDir, 'sessions', sessionId, 'autopilot-state.json'), JSON.stringify({
712
+ active: true,
713
+ mode: 'autopilot',
714
+ current_phase: 'ralplan',
715
+ started_at: AUTOPILOT_TEST_STARTED_AT,
716
+ state: {
717
+ handoff_artifacts: {
718
+ context_snapshot: {
719
+ path: '.omx/context/autopilot-recovery-20260529T000000Z.md',
720
+ kind: 'canonical',
721
+ },
722
+ },
723
+ },
724
+ }, null, 2));
725
+ await continueAutopilotTestState(stateDir, cwd, sessionId, 'typed-recovery');
726
+ await assertAutopilotRecoverySnapshot(cwd, await readAutopilotModeState(stateDir, sessionId), '.omx/context/autopilot-recovery-20260530T000000Z.md', 'missing-or-unsafe-legacy-context-snapshot');
727
+ assert.equal(existsSync(join(cwd, '.omx', 'context', 'autopilot-recovery-20260529T000000Z.md')), true);
728
+ }
729
+ finally {
730
+ await rm(cwd, { recursive: true, force: true });
731
+ }
732
+ });
733
+ it('rejects oversized Autopilot context snapshot candidates during reuse', async () => {
734
+ const cwd = await mkdtemp(join(tmpdir(), 'omx-keyword-autopilot-oversized-context-'));
735
+ const stateDir = join(cwd, '.omx', 'state');
736
+ const sessionId = 'sess-autopilot-oversized-context';
737
+ try {
738
+ await mkdir(join(cwd, '.omx', 'context'), { recursive: true });
739
+ await writeFile(join(cwd, '.omx', 'context', 'oversized-legacy-20260529T000000Z.md'), 'x'.repeat((1024 * 1024) + 1));
740
+ await writeActiveAutopilotSkillState(stateDir, sessionId);
741
+ await writeFile(join(stateDir, 'sessions', sessionId, 'autopilot-state.json'), JSON.stringify({
742
+ active: true,
743
+ mode: 'autopilot',
744
+ current_phase: 'ralplan',
745
+ started_at: AUTOPILOT_TEST_STARTED_AT,
746
+ state: {
747
+ handoff_artifacts: {
748
+ context_snapshot_path: '.omx/context/oversized-legacy-20260529T000000Z.md',
749
+ },
750
+ },
751
+ }, null, 2));
752
+ await continueAutopilotTestState(stateDir, cwd, sessionId, 'oversized-context');
753
+ await assertAutopilotRecoverySnapshot(cwd, await readAutopilotModeState(stateDir, sessionId), '.omx/context/autopilot-recovery-20260530T000000Z.md', 'missing-or-unsafe-legacy-context-snapshot');
754
+ assert.equal(existsSync(join(cwd, '.omx', 'context', 'oversized-legacy-20260529T000000Z.md')), true);
755
+ }
756
+ finally {
757
+ await rm(cwd, { recursive: true, force: true });
758
+ }
759
+ });
760
+ it('does not promote degraded recovery snapshots to canonical context on reactivation', async () => {
761
+ const cwd = await mkdtemp(join(tmpdir(), 'omx-keyword-autopilot-recovery-reactivation-'));
762
+ const stateDir = join(cwd, '.omx', 'state');
763
+ const sessionId = 'sess-autopilot-recovery-reactivation';
764
+ try {
765
+ await mkdir(join(cwd, '.omx', 'context'), { recursive: true });
766
+ await writeFile(join(cwd, '.omx', 'context', 'autopilot-recovery-20260529T000000Z.md'), '# degraded recovery');
767
+ await writeActiveAutopilotSkillState(stateDir, sessionId, 'complete');
768
+ await writeFile(join(stateDir, 'sessions', sessionId, 'autopilot-state.json'), JSON.stringify({
769
+ active: true,
770
+ mode: 'autopilot',
771
+ current_phase: 'complete',
772
+ completed_at: AUTOPILOT_TEST_UPDATED_AT,
773
+ state: {
774
+ handoff_artifacts: {
775
+ context_snapshot_path: '.omx/context/autopilot-recovery-20260529T000000Z.md',
776
+ context_snapshot: {
777
+ path: '.omx/context/autopilot-recovery-20260529T000000Z.md',
778
+ kind: 'recovery',
779
+ recovery: { status: 'degraded', reason: 'missing-or-unsafe-legacy-context-snapshot' },
780
+ },
781
+ },
782
+ context_snapshot_recovery: { status: 'degraded', reason: 'missing-or-unsafe-legacy-context-snapshot' },
783
+ },
784
+ }, null, 2));
785
+ await continueAutopilotTestState(stateDir, cwd, sessionId, 'recovery-reactivation', '$autopilot implement the real task');
786
+ const modeState = await readAutopilotModeState(stateDir, sessionId);
787
+ assert.equal(modeState.state?.handoff_artifacts?.context_snapshot_path, '.omx/context/implement-the-real-task-20260530T000000Z.md');
788
+ assert.deepEqual(modeState.state?.handoff_artifacts?.context_snapshot, {
789
+ path: '.omx/context/implement-the-real-task-20260530T000000Z.md',
790
+ kind: 'canonical',
791
+ original_task_status: 'activation-prompt',
792
+ });
793
+ assert.equal(modeState.state?.context_snapshot_recovery, undefined);
794
+ const snapshot = await readFile(join(cwd, '.omx', 'context', 'implement-the-real-task-20260530T000000Z.md'), 'utf-8');
795
+ assert.match(snapshot, /activation prompt \/ task seed: \$autopilot implement the real task/);
494
796
  }
495
797
  finally {
496
798
  await rm(cwd, { recursive: true, force: true });
497
799
  }
498
800
  });
801
+ it('does not follow symlinked Autopilot context directories when writing snapshots', async () => {
802
+ const cwd = await mkdtemp(join(tmpdir(), 'omx-keyword-autopilot-symlink-context-'));
803
+ const outside = await mkdtemp(join(tmpdir(), 'omx-keyword-autopilot-symlink-outside-'));
804
+ const stateDir = join(cwd, '.omx', 'state');
805
+ try {
806
+ await mkdir(join(cwd, '.omx'), { recursive: true });
807
+ await symlink(outside, join(cwd, '.omx', 'context'));
808
+ await mkdir(stateDir, { recursive: true });
809
+ const warnings = [];
810
+ mock.method(console, 'warn', (...args) => {
811
+ warnings.push(args);
812
+ });
813
+ await recordSkillActivation({
814
+ stateDir,
815
+ sourceCwd: cwd,
816
+ text: '$autopilot symlink escape',
817
+ sessionId: 'sess-autopilot-symlink-context',
818
+ threadId: 'thread-symlink-context',
819
+ turnId: 'turn-symlink-context',
820
+ nowIso: '2026-05-30T00:00:00.000Z',
821
+ });
822
+ assert.equal(warnings.length, 1);
823
+ assert.match(String(warnings[0][1]), /symbolic link/);
824
+ assert.equal(existsSync(join(outside, 'symlink-escape-20260530T000000Z.md')), false);
825
+ assert.equal(existsSync(join(stateDir, 'sessions', 'sess-autopilot-symlink-context', 'autopilot-state.json')), false);
826
+ }
827
+ finally {
828
+ await rm(cwd, { recursive: true, force: true });
829
+ await rm(outside, { recursive: true, force: true });
830
+ }
831
+ });
832
+ it('allocates unique Autopilot context snapshot paths for same-second matching slugs', async () => {
833
+ const cwd = await mkdtemp(join(tmpdir(), 'omx-keyword-autopilot-context-collision-'));
834
+ const stateDir = join(cwd, '.omx', 'state');
835
+ try {
836
+ await mkdir(stateDir, { recursive: true });
837
+ await recordSkillActivation({
838
+ stateDir,
839
+ sourceCwd: cwd,
840
+ text: '$autopilot same task',
841
+ sessionId: 'sess-autopilot-collision-a',
842
+ threadId: 'thread-collision',
843
+ turnId: 'turn-collision-a',
844
+ nowIso: '2026-05-30T00:00:00.000Z',
845
+ });
846
+ await recordSkillActivation({
847
+ stateDir,
848
+ sourceCwd: cwd,
849
+ text: '$autopilot same task',
850
+ sessionId: 'sess-autopilot-collision-b',
851
+ threadId: 'thread-collision',
852
+ turnId: 'turn-collision-b',
853
+ nowIso: '2026-05-30T00:00:00.000Z',
854
+ });
855
+ const first = JSON.parse(await readFile(join(stateDir, 'sessions', 'sess-autopilot-collision-a', 'autopilot-state.json'), 'utf-8'));
856
+ const second = JSON.parse(await readFile(join(stateDir, 'sessions', 'sess-autopilot-collision-b', 'autopilot-state.json'), 'utf-8'));
857
+ assert.equal(first.state?.handoff_artifacts?.context_snapshot_path, '.omx/context/same-task-20260530T000000Z.md');
858
+ assert.equal(second.state?.handoff_artifacts?.context_snapshot_path, '.omx/context/same-task-20260530T000000Z-2.md');
859
+ }
860
+ finally {
861
+ await rm(cwd, { recursive: true, force: true });
862
+ }
863
+ });
864
+ it('fully resets terminal Autopilot mode state when reactivated', async () => {
865
+ const cwd = await mkdtemp(join(tmpdir(), 'omx-keyword-autopilot-terminal-reset-'));
866
+ const stateDir = join(cwd, '.omx', 'state');
867
+ const sessionId = 'sess-autopilot-terminal-reset';
868
+ try {
869
+ await mkdir(join(stateDir, 'sessions', sessionId), { recursive: true });
870
+ await writeFile(join(stateDir, 'sessions', sessionId, SKILL_ACTIVE_STATE_FILE), JSON.stringify({
871
+ version: 1,
872
+ active: true,
873
+ skill: 'autopilot',
874
+ keyword: '$autopilot',
875
+ phase: 'complete',
876
+ activated_at: '2026-05-29T00:00:00.000Z',
877
+ updated_at: '2026-05-29T00:00:00.000Z',
878
+ session_id: sessionId,
879
+ active_skills: [{ skill: 'autopilot', active: true, phase: 'complete', session_id: sessionId }],
880
+ }, null, 2));
881
+ await writeFile(join(stateDir, 'sessions', sessionId, 'autopilot-state.json'), JSON.stringify({
882
+ active: true,
883
+ mode: 'autopilot',
884
+ current_phase: 'complete',
885
+ started_at: '2026-05-29T00:00:00.000Z',
886
+ completed_at: '2026-05-29T00:10:00.000Z',
887
+ iteration: 10,
888
+ max_iterations: 10,
889
+ review_cycle: 3,
890
+ lifecycle_outcome: 'finished',
891
+ run_outcome: 'finish',
892
+ handoff_artifacts: {
893
+ code_review: { verdict: 'APPROVE / CLEAR' },
894
+ ultraqa: { verdict: 'pass' },
895
+ },
896
+ state: {
897
+ handoff_artifacts: {
898
+ ralplan_consensus_gate: { complete: false },
899
+ code_review: { verdict: 'stale' },
900
+ },
901
+ },
902
+ }, null, 2));
903
+ const result = await recordSkillActivation({
904
+ stateDir,
905
+ text: '$autopilot investigate the next issue',
906
+ sessionId,
907
+ threadId: 'thread-reactivated',
908
+ turnId: 'turn-reactivated',
909
+ nowIso: '2026-05-30T00:00:00.000Z',
910
+ });
911
+ assert.ok(result);
912
+ assert.equal(result.skill, 'autopilot');
913
+ assert.equal(result.phase, 'deep-interview');
914
+ assert.equal(result.activated_at, '2026-05-30T00:00:00.000Z');
915
+ assert.equal(result.active_skills?.[0]?.phase, 'deep-interview');
916
+ assert.equal(result.active_skills?.[0]?.activated_at, '2026-05-30T00:00:00.000Z');
917
+ const skillState = JSON.parse(await readFile(join(stateDir, 'sessions', sessionId, SKILL_ACTIVE_STATE_FILE), 'utf-8'));
918
+ assert.equal(skillState.phase, 'deep-interview');
919
+ assert.equal(skillState.activated_at, '2026-05-30T00:00:00.000Z');
920
+ assert.equal(skillState.active_skills?.[0]?.phase, 'deep-interview');
921
+ assert.equal(skillState.active_skills?.[0]?.activated_at, '2026-05-30T00:00:00.000Z');
922
+ const modeState = JSON.parse(await readFile(join(stateDir, 'sessions', sessionId, 'autopilot-state.json'), 'utf-8'));
923
+ assert.equal(modeState.active, true);
924
+ assert.equal(modeState.current_phase, 'deep-interview');
925
+ assert.equal(modeState.started_at, '2026-05-30T00:00:00.000Z');
926
+ assert.equal(modeState.completed_at, undefined);
927
+ assert.equal(modeState.iteration, 1);
928
+ assert.equal(modeState.max_iterations, 10);
929
+ assert.equal(modeState.review_cycle, 0);
930
+ assert.equal(modeState.lifecycle_outcome, undefined);
931
+ assert.equal(modeState.run_outcome, undefined);
932
+ assert.equal(modeState.handoff_artifacts, undefined);
933
+ assert.deepEqual(modeState.state?.handoff_artifacts, {
934
+ context_snapshot_path: '.omx/context/investigate-the-next-issue-20260530T000000Z.md',
935
+ context_snapshot: {
936
+ path: '.omx/context/investigate-the-next-issue-20260530T000000Z.md',
937
+ kind: 'canonical',
938
+ original_task_status: 'activation-prompt',
939
+ },
940
+ deep_interview: null,
941
+ ralplan: null,
942
+ ralplan_consensus_gate: {
943
+ required: true,
944
+ sequence: ['architect-review', 'critic-review'],
945
+ planning_artifacts_are_not_consensus: true,
946
+ required_review_roles: ['architect', 'critic'],
947
+ ralplan_architect_review: null,
948
+ ralplan_critic_review: null,
949
+ complete: false,
950
+ },
951
+ ultragoal: null,
952
+ code_review: null,
953
+ ultraqa: null,
954
+ });
955
+ }
956
+ finally {
957
+ await rm(cwd, { recursive: true, force: true });
958
+ }
959
+ });
960
+ it('resets stopped Autopilot mode state when reactivated', async () => {
961
+ for (const phase of ['stopped', 'user-stopped']) {
962
+ const cwd = await mkdtemp(join(tmpdir(), `omx-keyword-autopilot-${phase}-reset-`));
963
+ const stateDir = join(cwd, '.omx', 'state');
964
+ const sessionId = `sess-autopilot-${phase}-reset`;
965
+ try {
966
+ await mkdir(join(stateDir, 'sessions', sessionId), { recursive: true });
967
+ await writeFile(join(stateDir, 'sessions', sessionId, SKILL_ACTIVE_STATE_FILE), JSON.stringify({
968
+ version: 1,
969
+ active: true,
970
+ skill: 'autopilot',
971
+ keyword: '$autopilot',
972
+ phase,
973
+ activated_at: '2026-05-29T00:00:00.000Z',
974
+ updated_at: '2026-05-29T00:00:00.000Z',
975
+ source: 'keyword-detector',
976
+ session_id: sessionId,
977
+ active_skills: [{ skill: 'autopilot', active: true, phase, session_id: sessionId }],
978
+ }, null, 2));
979
+ await writeFile(join(stateDir, 'sessions', sessionId, 'autopilot-state.json'), JSON.stringify({
980
+ active: true,
981
+ mode: 'autopilot',
982
+ current_phase: phase,
983
+ started_at: '2026-05-29T00:00:00.000Z',
984
+ completed_at: '2026-05-29T00:10:00.000Z',
985
+ iteration: 10,
986
+ max_iterations: 10,
987
+ review_cycle: 3,
988
+ state: { handoff_artifacts: { code_review: { verdict: 'stale' } } },
989
+ }, null, 2));
990
+ const result = await recordSkillActivation({
991
+ stateDir,
992
+ text: '$autopilot new task after stop',
993
+ sessionId,
994
+ nowIso: '2026-05-30T00:00:00.000Z',
995
+ });
996
+ assert.ok(result);
997
+ assert.equal(result.phase, 'deep-interview');
998
+ assert.equal(result.activated_at, '2026-05-30T00:00:00.000Z');
999
+ const modeState = JSON.parse(await readFile(join(stateDir, 'sessions', sessionId, 'autopilot-state.json'), 'utf-8'));
1000
+ assert.equal(modeState.current_phase, 'deep-interview');
1001
+ assert.equal(modeState.iteration, 1);
1002
+ assert.equal(modeState.review_cycle, 0);
1003
+ assert.equal(modeState.state?.handoff_artifacts?.code_review, null);
1004
+ }
1005
+ finally {
1006
+ await rm(cwd, { recursive: true, force: true });
1007
+ }
1008
+ }
1009
+ });
499
1010
  it('adds approved workflow overlaps without deleting the existing canonical state', async () => {
500
1011
  const cwd = await mkdtemp(join(tmpdir(), 'omx-keyword-state-overlap-'));
501
1012
  const stateDir = join(cwd, '.omx', 'state');
@@ -864,7 +1375,7 @@ describe('keyword detector skill-active-state lifecycle', () => {
864
1375
  await rm(cwd, { recursive: true, force: true });
865
1376
  }
866
1377
  });
867
- it('keeps ralplan as an allowlisted deep-interview forward handoff', async () => {
1378
+ it('denies ralplan handoff from deep-interview without completion or explicit skip evidence', async () => {
868
1379
  const cwd = await mkdtemp(join(tmpdir(), 'omx-keyword-ralplan-handoff-'));
869
1380
  const stateDir = join(cwd, '.omx', 'state');
870
1381
  try {
@@ -884,10 +1395,49 @@ describe('keyword detector skill-active-state lifecycle', () => {
884
1395
  sessionId: 'sess-ralplan-handoff',
885
1396
  nowIso: '2026-04-10T00:00:00.000Z',
886
1397
  });
1398
+ assert.equal(result?.skill, 'deep-interview');
1399
+ assert.match(String(result?.transition_error), /missing deep-interview completion\/skip gate/i);
1400
+ const preserved = JSON.parse(await readFile(join(stateDir, 'sessions', 'sess-ralplan-handoff', 'deep-interview-state.json'), 'utf-8'));
1401
+ assert.equal(preserved.active, true);
1402
+ assert.equal(preserved.current_phase, 'intent-first');
1403
+ assert.equal(existsSync(join(stateDir, 'sessions', 'sess-ralplan-handoff', 'ralplan-state.json')), false);
1404
+ }
1405
+ finally {
1406
+ await rm(cwd, { recursive: true, force: true });
1407
+ }
1408
+ });
1409
+ it('allows ralplan handoff from deep-interview with a durable completion gate', async () => {
1410
+ const cwd = await mkdtemp(join(tmpdir(), 'omx-keyword-ralplan-handoff-complete-'));
1411
+ const stateDir = join(cwd, '.omx', 'state');
1412
+ try {
1413
+ await mkdir(join(stateDir, 'sessions', 'sess-ralplan-handoff-complete'), { recursive: true });
1414
+ await writeFile(join(stateDir, 'sessions', 'sess-ralplan-handoff-complete', SKILL_ACTIVE_STATE_FILE), JSON.stringify({
1415
+ version: 1,
1416
+ active: true,
1417
+ skill: 'deep-interview',
1418
+ phase: 'planning',
1419
+ session_id: 'sess-ralplan-handoff-complete',
1420
+ active_skills: [{ skill: 'deep-interview', phase: 'planning', active: true, session_id: 'sess-ralplan-handoff-complete' }],
1421
+ }, null, 2));
1422
+ await writeFile(join(stateDir, 'sessions', 'sess-ralplan-handoff-complete', 'deep-interview-state.json'), JSON.stringify({
1423
+ active: true,
1424
+ mode: 'deep-interview',
1425
+ current_phase: 'intent-first',
1426
+ deep_interview_gate: {
1427
+ status: 'complete',
1428
+ rationale: 'Requirements are clarified and ready for ralplan consensus.',
1429
+ },
1430
+ }, null, 2));
1431
+ const result = await recordSkillActivation({
1432
+ stateDir,
1433
+ text: '$ralplan implement the approved contract',
1434
+ sessionId: 'sess-ralplan-handoff-complete',
1435
+ nowIso: '2026-04-10T00:00:00.000Z',
1436
+ });
887
1437
  assert.equal(result?.transition_error, undefined);
888
1438
  assert.equal(result?.skill, 'ralplan');
889
1439
  assert.equal(result?.transition_message, 'mode transiting: deep-interview -> ralplan');
890
- const completed = JSON.parse(await readFile(join(stateDir, 'sessions', 'sess-ralplan-handoff', 'deep-interview-state.json'), 'utf-8'));
1440
+ const completed = JSON.parse(await readFile(join(stateDir, 'sessions', 'sess-ralplan-handoff-complete', 'deep-interview-state.json'), 'utf-8'));
891
1441
  assert.equal(completed.active, false);
892
1442
  assert.equal(completed.current_phase, 'completed');
893
1443
  }
@@ -965,6 +1515,72 @@ describe('keyword detector skill-active-state lifecycle', () => {
965
1515
  await rm(cwd, { recursive: true, force: true });
966
1516
  }
967
1517
  });
1518
+ it('does not activate team state when persisted Team mode is disabled', async () => {
1519
+ const cwd = await mkdtemp(join(tmpdir(), 'omx-keyword-state-team-disabled-'));
1520
+ const stateDir = join(cwd, '.omx', 'state');
1521
+ try {
1522
+ await mkdir(join(cwd, '.omx'), { recursive: true });
1523
+ await mkdir(stateDir, { recursive: true });
1524
+ await writeFile(join(cwd, '.omx', 'setup-scope.json'), JSON.stringify({ scope: 'project', teamMode: 'disabled' }, null, 2));
1525
+ const result = await recordSkillActivation({
1526
+ stateDir,
1527
+ text: '$team coordinate the hotfix',
1528
+ sessionId: 'sess-team-disabled',
1529
+ nowIso: '2026-04-08T00:00:00.000Z',
1530
+ });
1531
+ assert.equal(result, null);
1532
+ assert.equal(existsSync(join(stateDir, 'team-state.json')), false);
1533
+ assert.equal(existsSync(join(stateDir, 'sessions', 'sess-team-disabled', SKILL_ACTIVE_STATE_FILE)), false);
1534
+ }
1535
+ finally {
1536
+ await rm(cwd, { recursive: true, force: true });
1537
+ }
1538
+ });
1539
+ it('ignores disabled Team when selecting the primary workflow', async () => {
1540
+ const cwd = await mkdtemp(join(tmpdir(), 'omx-keyword-state-team-disabled-primary-'));
1541
+ const stateDir = join(cwd, '.omx', 'state');
1542
+ try {
1543
+ await mkdir(join(cwd, '.omx'), { recursive: true });
1544
+ await mkdir(stateDir, { recursive: true });
1545
+ await writeFile(join(cwd, '.omx', 'setup-scope.json'), JSON.stringify({ scope: 'project', teamMode: 'disabled' }, null, 2));
1546
+ const result = await recordSkillActivation({
1547
+ stateDir,
1548
+ text: '$team $ralph ship this fix',
1549
+ sessionId: 'sess-team-disabled-primary',
1550
+ nowIso: '2026-04-10T01:00:00.000Z',
1551
+ });
1552
+ assert.equal(result?.skill, 'ralph');
1553
+ assert.deepEqual(result?.active_skills?.map((entry) => entry.skill), ['ralph']);
1554
+ assert.equal(existsSync(join(stateDir, 'team-state.json')), false);
1555
+ assert.equal(existsSync(join(stateDir, 'sessions', 'sess-team-disabled-primary', 'ralph-state.json')), true);
1556
+ }
1557
+ finally {
1558
+ await rm(cwd, { recursive: true, force: true });
1559
+ }
1560
+ });
1561
+ it('filters deferred team handoffs when persisted Team mode is disabled', async () => {
1562
+ const cwd = await mkdtemp(join(tmpdir(), 'omx-keyword-state-team-disabled-deferred-'));
1563
+ const stateDir = join(cwd, '.omx', 'state');
1564
+ try {
1565
+ await mkdir(join(cwd, '.omx'), { recursive: true });
1566
+ await mkdir(stateDir, { recursive: true });
1567
+ await writeFile(join(cwd, '.omx', 'setup-scope.json'), JSON.stringify({ scope: 'project', teamMode: 'disabled' }, null, 2));
1568
+ const result = await recordSkillActivation({
1569
+ stateDir,
1570
+ text: '$ralplan $team $ralph ship this fix',
1571
+ sessionId: 'sess-team-disabled-deferred',
1572
+ nowIso: '2026-04-10T00:00:00.000Z',
1573
+ });
1574
+ assert.equal(result?.skill, 'ralplan');
1575
+ assert.deepEqual(result?.active_skills?.map((entry) => entry.skill), ['ralplan']);
1576
+ assert.deepEqual(result?.deferred_skills, ['ralph']);
1577
+ assert.equal(existsSync(join(stateDir, 'team-state.json')), false);
1578
+ assert.equal(existsSync(join(stateDir, 'sessions', 'sess-team-disabled-deferred', 'team-state.json')), false);
1579
+ }
1580
+ finally {
1581
+ await rm(cwd, { recursive: true, force: true });
1582
+ }
1583
+ });
968
1584
  it('preserves active team root state when $team is re-entered from prompt routing', async () => {
969
1585
  const cwd = await mkdtemp(join(tmpdir(), 'omx-keyword-state-team-preserve-'));
970
1586
  const stateDir = join(cwd, '.omx', 'state');
@@ -1034,6 +1650,57 @@ describe('keyword detector skill-active-state lifecycle', () => {
1034
1650
  await rm(cwd, { recursive: true, force: true });
1035
1651
  }
1036
1652
  });
1653
+ it('emits terminal ralplan state before explicit ultragoal execution handoff', async () => {
1654
+ const cwd = await mkdtemp(join(tmpdir(), 'omx-keyword-ralplan-ultragoal-handoff-'));
1655
+ const stateDir = join(cwd, '.omx', 'state');
1656
+ try {
1657
+ await mkdir(join(stateDir, 'sessions', 'sess-ralplan-ultragoal'), { recursive: true });
1658
+ await writeFile(join(stateDir, 'sessions', 'sess-ralplan-ultragoal', SKILL_ACTIVE_STATE_FILE), JSON.stringify({
1659
+ version: 1,
1660
+ active: true,
1661
+ skill: 'ralplan',
1662
+ keyword: '$ralplan',
1663
+ phase: 'planning',
1664
+ session_id: 'sess-ralplan-ultragoal',
1665
+ active_skills: [{ skill: 'ralplan', phase: 'planning', active: true, session_id: 'sess-ralplan-ultragoal' }],
1666
+ }, null, 2));
1667
+ await writeFile(join(stateDir, 'sessions', 'sess-ralplan-ultragoal', 'ralplan-state.json'), JSON.stringify({
1668
+ active: true,
1669
+ mode: 'ralplan',
1670
+ current_phase: 'complete',
1671
+ planning_complete: true,
1672
+ ralplan_consensus_gate: {
1673
+ complete: true,
1674
+ sequence: ['architect-review', 'critic-review'],
1675
+ ralplan_architect_review: { agent_role: 'architect', verdict: 'approve', approved: true },
1676
+ ralplan_critic_review: { agent_role: 'critic', verdict: 'approve', approved: true },
1677
+ },
1678
+ }, null, 2));
1679
+ const result = await recordSkillActivation({
1680
+ stateDir,
1681
+ sourceCwd: cwd,
1682
+ text: '$ultragoal execute the approved ralplan',
1683
+ sessionId: 'sess-ralplan-ultragoal',
1684
+ nowIso: '2026-04-10T00:20:00.000Z',
1685
+ });
1686
+ assert.equal(result?.transition_error, undefined);
1687
+ assert.equal(result?.skill, 'ultragoal');
1688
+ assert.equal(result?.transition_message, 'mode transiting: ralplan -> ultragoal');
1689
+ assert.deepEqual(result?.active_skills?.map((entry) => entry.skill), ['ultragoal']);
1690
+ const ralplan = JSON.parse(await readFile(join(stateDir, 'sessions', 'sess-ralplan-ultragoal', 'ralplan-state.json'), 'utf-8'));
1691
+ assert.equal(ralplan.active, false);
1692
+ assert.equal(ralplan.current_phase, 'completed');
1693
+ assert.equal(ralplan.auto_completed_reason, 'mode transiting: ralplan -> ultragoal');
1694
+ assert.ok(ralplan.completed_at);
1695
+ const ultragoal = JSON.parse(await readFile(join(stateDir, 'sessions', 'sess-ralplan-ultragoal', 'ultragoal-state.json'), 'utf-8'));
1696
+ assert.equal(ultragoal.active, true);
1697
+ assert.equal(ultragoal.mode, 'ultragoal');
1698
+ assert.equal(ultragoal.current_phase, 'planning');
1699
+ }
1700
+ finally {
1701
+ await rm(cwd, { recursive: true, force: true });
1702
+ }
1703
+ });
1037
1704
  it('keeps root team state out of the session-scoped Ralph canonical state', async () => {
1038
1705
  const cwd = await mkdtemp(join(tmpdir(), 'omx-keyword-state-team-ralph-'));
1039
1706
  const stateDir = join(cwd, '.omx', 'state');
@@ -1523,6 +2190,211 @@ deepMaxRounds = 21
1523
2190
  await rm(cwd, { recursive: true, force: true });
1524
2191
  }
1525
2192
  });
2193
+ it('keeps Autopilot visible when a supervised code-review child keyword appears', async () => {
2194
+ const cwd = await mkdtemp(join(tmpdir(), 'omx-keyword-state-autopilot-child-code-review-'));
2195
+ const stateDir = join(cwd, '.omx', 'state');
2196
+ const sessionId = 'sess-autopilot-child-code-review';
2197
+ try {
2198
+ await mkdir(join(stateDir, 'sessions', sessionId), { recursive: true });
2199
+ await writeFile(join(stateDir, 'sessions', sessionId, SKILL_ACTIVE_STATE_FILE), JSON.stringify({
2200
+ version: 1,
2201
+ active: true,
2202
+ skill: 'autopilot',
2203
+ keyword: '$autopilot',
2204
+ phase: 'ralplan',
2205
+ activated_at: '2026-05-30T00:00:00.000Z',
2206
+ updated_at: '2026-05-30T00:01:00.000Z',
2207
+ source: 'keyword-detector',
2208
+ session_id: sessionId,
2209
+ active_skills: [{ skill: 'autopilot', phase: 'ralplan', active: true, session_id: sessionId }],
2210
+ }, null, 2));
2211
+ const result = await recordSkillActivation({
2212
+ stateDir,
2213
+ text: 'CODE REVIEW the current diff before continuing',
2214
+ sessionId,
2215
+ threadId: 'thread-autopilot-child-code-review',
2216
+ turnId: 'turn-autopilot-child-code-review',
2217
+ nowIso: '2026-05-30T00:02:00.000Z',
2218
+ });
2219
+ assert.ok(result);
2220
+ assert.equal(result.skill, 'autopilot');
2221
+ assert.equal(result.phase, 'ralplan');
2222
+ assert.equal(result.supervised_child_skill, 'code-review');
2223
+ const persisted = JSON.parse(await readFile(join(stateDir, 'sessions', sessionId, SKILL_ACTIVE_STATE_FILE), 'utf-8'));
2224
+ assert.equal(persisted.skill, 'autopilot');
2225
+ assert.equal(persisted.phase, 'ralplan');
2226
+ assert.deepEqual(persisted.active_skills?.map((entry) => entry.skill), ['autopilot']);
2227
+ assert.equal(existsSync(join(stateDir, 'sessions', sessionId, 'code-review-state.json')), false);
2228
+ }
2229
+ finally {
2230
+ await rm(cwd, { recursive: true, force: true });
2231
+ }
2232
+ });
2233
+ it('keeps tracked Autopilot child keywords supervised and completes stale child mode state', async () => {
2234
+ const cwd = await mkdtemp(join(tmpdir(), 'omx-keyword-state-autopilot-child-ultraqa-'));
2235
+ const stateDir = join(cwd, '.omx', 'state');
2236
+ const sessionId = 'sess-autopilot-child-ultraqa';
2237
+ try {
2238
+ await mkdir(join(stateDir, 'sessions', sessionId), { recursive: true });
2239
+ await writeFile(join(stateDir, 'sessions', sessionId, SKILL_ACTIVE_STATE_FILE), JSON.stringify({
2240
+ version: 1,
2241
+ active: true,
2242
+ skill: 'autopilot',
2243
+ keyword: '$autopilot',
2244
+ phase: 'ultraqa',
2245
+ activated_at: '2026-05-30T00:00:00.000Z',
2246
+ updated_at: '2026-05-30T00:01:00.000Z',
2247
+ source: 'keyword-detector',
2248
+ session_id: sessionId,
2249
+ active_skills: [{ skill: 'autopilot', phase: 'ultraqa', active: true, session_id: sessionId }],
2250
+ }, null, 2));
2251
+ await writeFile(join(stateDir, 'sessions', sessionId, 'ultragoal-state.json'), JSON.stringify({
2252
+ active: true,
2253
+ mode: 'ultragoal',
2254
+ current_phase: 'planning',
2255
+ session_id: sessionId,
2256
+ started_at: '2026-05-29T23:00:00.000Z',
2257
+ updated_at: '2026-05-29T23:05:00.000Z',
2258
+ }, null, 2));
2259
+ const result = await recordSkillActivation({
2260
+ stateDir,
2261
+ text: '$ultraqa run adversarial checks',
2262
+ sessionId,
2263
+ threadId: 'thread-autopilot-child-ultraqa',
2264
+ turnId: 'turn-autopilot-child-ultraqa',
2265
+ nowIso: '2026-05-30T00:02:00.000Z',
2266
+ });
2267
+ assert.ok(result);
2268
+ assert.equal(result.skill, 'autopilot');
2269
+ assert.equal(result.phase, 'ultraqa');
2270
+ assert.equal(result.supervised_child_skill, 'ultraqa');
2271
+ assert.equal(result.transition_error, undefined);
2272
+ assert.equal(existsSync(join(stateDir, 'sessions', sessionId, 'ultraqa-state.json')), false);
2273
+ const ultragoal = JSON.parse(await readFile(join(stateDir, 'sessions', sessionId, 'ultragoal-state.json'), 'utf-8'));
2274
+ assert.equal(ultragoal.active, false);
2275
+ assert.equal(ultragoal.current_phase, 'completed');
2276
+ assert.match(ultragoal.auto_completed_reason || '', /mode transiting: ultragoal -> ultraqa/);
2277
+ }
2278
+ finally {
2279
+ await rm(cwd, { recursive: true, force: true });
2280
+ }
2281
+ });
2282
+ it('denies supervised Autopilot child rollback without clearing stale execution state', async () => {
2283
+ const cwd = await mkdtemp(join(tmpdir(), 'omx-keyword-state-autopilot-child-rollback-'));
2284
+ const stateDir = join(cwd, '.omx', 'state');
2285
+ const sessionId = 'sess-autopilot-child-rollback';
2286
+ try {
2287
+ await mkdir(join(stateDir, 'sessions', sessionId), { recursive: true });
2288
+ await writeFile(join(stateDir, 'sessions', sessionId, SKILL_ACTIVE_STATE_FILE), JSON.stringify({
2289
+ version: 1,
2290
+ active: true,
2291
+ skill: 'autopilot',
2292
+ keyword: '$autopilot',
2293
+ phase: 'ultragoal',
2294
+ session_id: sessionId,
2295
+ active_skills: [{ skill: 'autopilot', phase: 'ultragoal', active: true, session_id: sessionId }],
2296
+ }, null, 2));
2297
+ await writeFile(join(stateDir, 'sessions', sessionId, 'ultragoal-state.json'), JSON.stringify({
2298
+ active: true,
2299
+ mode: 'ultragoal',
2300
+ current_phase: 'executing',
2301
+ session_id: sessionId,
2302
+ }, null, 2));
2303
+ const result = await recordSkillActivation({
2304
+ stateDir,
2305
+ text: '$deep-interview go back and re-plan',
2306
+ sessionId,
2307
+ nowIso: '2026-05-30T00:03:00.000Z',
2308
+ });
2309
+ assert.equal(result?.skill, 'autopilot');
2310
+ assert.match(String(result?.transition_error), /Execution-to-planning rollback auto-complete is not allowed/i);
2311
+ assert.equal(result?.supervised_child_skill, undefined);
2312
+ assert.equal(existsSync(join(stateDir, 'sessions', sessionId, 'deep-interview-state.json')), false);
2313
+ const ultragoal = JSON.parse(await readFile(join(stateDir, 'sessions', sessionId, 'ultragoal-state.json'), 'utf-8'));
2314
+ assert.equal(ultragoal.active, true);
2315
+ assert.equal(ultragoal.current_phase, 'executing');
2316
+ }
2317
+ finally {
2318
+ await rm(cwd, { recursive: true, force: true });
2319
+ }
2320
+ });
2321
+ it('surfaces supervised Autopilot deep-interview to ralplan gate failures', async () => {
2322
+ const cwd = await mkdtemp(join(tmpdir(), 'omx-keyword-state-autopilot-child-gate-'));
2323
+ const stateDir = join(cwd, '.omx', 'state');
2324
+ const sessionId = 'sess-autopilot-child-gate';
2325
+ try {
2326
+ await mkdir(join(stateDir, 'sessions', sessionId), { recursive: true });
2327
+ await writeFile(join(stateDir, 'sessions', sessionId, SKILL_ACTIVE_STATE_FILE), JSON.stringify({
2328
+ version: 1,
2329
+ active: true,
2330
+ skill: 'autopilot',
2331
+ keyword: '$autopilot',
2332
+ phase: 'deep-interview',
2333
+ session_id: sessionId,
2334
+ active_skills: [{ skill: 'autopilot', phase: 'deep-interview', active: true, session_id: sessionId }],
2335
+ }, null, 2));
2336
+ await writeFile(join(stateDir, 'sessions', sessionId, 'deep-interview-state.json'), JSON.stringify({
2337
+ active: true,
2338
+ mode: 'deep-interview',
2339
+ current_phase: 'intent-first',
2340
+ session_id: sessionId,
2341
+ }, null, 2));
2342
+ const result = await recordSkillActivation({
2343
+ stateDir,
2344
+ text: '$ralplan continue without interview completion evidence',
2345
+ sessionId,
2346
+ nowIso: '2026-05-30T00:04:00.000Z',
2347
+ });
2348
+ assert.equal(result?.skill, 'autopilot');
2349
+ assert.match(String(result?.transition_error), /missing deep-interview completion\/skip gate/i);
2350
+ assert.equal(result?.supervised_child_skill, undefined);
2351
+ assert.equal(existsSync(join(stateDir, 'sessions', sessionId, 'ralplan-state.json')), false);
2352
+ const deepInterview = JSON.parse(await readFile(join(stateDir, 'sessions', sessionId, 'deep-interview-state.json'), 'utf-8'));
2353
+ assert.equal(deepInterview.active, true);
2354
+ assert.equal(deepInterview.current_phase, 'intent-first');
2355
+ }
2356
+ finally {
2357
+ await rm(cwd, { recursive: true, force: true });
2358
+ }
2359
+ });
2360
+ it('ignores stale root child mode state during session-scoped Autopilot child reconciliation', async () => {
2361
+ const cwd = await mkdtemp(join(tmpdir(), 'omx-keyword-state-autopilot-child-session-root-'));
2362
+ const stateDir = join(cwd, '.omx', 'state');
2363
+ const sessionId = 'sess-autopilot-child-session-root';
2364
+ try {
2365
+ await mkdir(join(stateDir, 'sessions', sessionId), { recursive: true });
2366
+ await writeFile(join(stateDir, 'sessions', sessionId, SKILL_ACTIVE_STATE_FILE), JSON.stringify({
2367
+ version: 1,
2368
+ active: true,
2369
+ skill: 'autopilot',
2370
+ keyword: '$autopilot',
2371
+ phase: 'deep-interview',
2372
+ session_id: sessionId,
2373
+ active_skills: [{ skill: 'autopilot', phase: 'deep-interview', active: true, session_id: sessionId }],
2374
+ }, null, 2));
2375
+ await writeFile(join(stateDir, 'ultragoal-state.json'), JSON.stringify({
2376
+ active: true,
2377
+ mode: 'ultragoal',
2378
+ current_phase: 'executing',
2379
+ }, null, 2));
2380
+ const result = await recordSkillActivation({
2381
+ stateDir,
2382
+ text: '$deep-interview continue scoped interview',
2383
+ sessionId,
2384
+ nowIso: '2026-05-30T00:05:00.000Z',
2385
+ });
2386
+ assert.equal(result?.skill, 'autopilot');
2387
+ assert.equal(result?.supervised_child_skill, 'deep-interview');
2388
+ assert.equal(result?.transition_error, undefined);
2389
+ const rootUltragoal = JSON.parse(await readFile(join(stateDir, 'ultragoal-state.json'), 'utf-8'));
2390
+ assert.equal(rootUltragoal.active, true);
2391
+ assert.equal(rootUltragoal.current_phase, 'executing');
2392
+ assert.equal(existsSync(join(stateDir, 'sessions', sessionId, 'deep-interview-state.json')), false);
2393
+ }
2394
+ finally {
2395
+ await rm(cwd, { recursive: true, force: true });
2396
+ }
2397
+ });
1526
2398
  it('records ultragoal as a prompt skill with first-class mode state', async () => {
1527
2399
  const cwd = await mkdtemp(join(tmpdir(), 'omx-keyword-state-ultragoal-'));
1528
2400
  const stateDir = join(cwd, '.omx', 'state');
@@ -1547,19 +2419,27 @@ deepMaxRounds = 21
1547
2419
  }
1548
2420
  });
1549
2421
  it('emits a warning when skill-active-state persistence fails', async () => {
2422
+ const cwd = await mkdtemp(join(tmpdir(), 'omx-keyword-state-persist-fail-'));
1550
2423
  const warnings = [];
1551
2424
  mock.method(console, 'warn', (...args) => {
1552
2425
  warnings.push(args);
1553
2426
  });
1554
- const result = await recordSkillActivation({
1555
- stateDir: join('/definitely-missing', 'nested', 'state-dir'),
1556
- text: 'please run $autopilot',
1557
- nowIso: '2026-02-25T00:00:00.000Z',
1558
- });
1559
- assert.ok(result);
1560
- assert.equal(result.skill, 'autopilot');
1561
- assert.equal(warnings.length, 1);
1562
- assert.match(String(warnings[0][0]), /failed to persist keyword activation state/);
2427
+ try {
2428
+ const blockingFile = join(cwd, 'state-root-file');
2429
+ await writeFile(blockingFile, 'not a directory');
2430
+ const result = await recordSkillActivation({
2431
+ stateDir: join(blockingFile, 'nested', 'state-dir'),
2432
+ text: 'please run $autopilot',
2433
+ nowIso: '2026-02-25T00:00:00.000Z',
2434
+ });
2435
+ assert.ok(result);
2436
+ assert.equal(result.skill, 'autopilot');
2437
+ assert.equal(warnings.length, 1);
2438
+ assert.match(String(warnings[0][0]), /failed to persist keyword activation state/);
2439
+ }
2440
+ finally {
2441
+ await rm(cwd, { recursive: true, force: true });
2442
+ }
1563
2443
  });
1564
2444
  it('preserves activated_at for same-skill continuation', async () => {
1565
2445
  const cwd = await mkdtemp(join(tmpdir(), 'omx-keyword-state-continuation-'));
@@ -1618,6 +2498,8 @@ deepMaxRounds = 21
1618
2498
  session_id: 'sess-autopilot',
1619
2499
  state: { context_snapshot_path: '.omx/context/existing.md' },
1620
2500
  }));
2501
+ await mkdir(join(cwd, '.omx', 'context'), { recursive: true });
2502
+ await writeFile(join(cwd, '.omx', 'context', 'existing.md'), '# existing context');
1621
2503
  const result = await recordSkillActivation({
1622
2504
  stateDir,
1623
2505
  text: 'autopilot keep going',
@@ -1631,7 +2513,8 @@ deepMaxRounds = 21
1631
2513
  const modeState = JSON.parse(await readFile(join(stateDir, 'sessions', 'sess-autopilot', 'autopilot-state.json'), 'utf-8'));
1632
2514
  assert.equal(modeState.current_phase, 'code-review');
1633
2515
  assert.equal(modeState.started_at, '2026-02-25T00:00:00.000Z');
1634
- assert.equal(modeState.state?.context_snapshot_path, '.omx/context/existing.md');
2516
+ assert.equal(modeState.state?.context_snapshot_path, undefined);
2517
+ assert.equal(modeState.state?.handoff_artifacts?.context_snapshot_path, '.omx/context/existing.md');
1635
2518
  }
1636
2519
  finally {
1637
2520
  await rm(cwd, { recursive: true, force: true });
@@ -1801,6 +2684,8 @@ deepMaxRounds = 21
1801
2684
  session_id: 'sess-autopilot-bare',
1802
2685
  state: { context_snapshot_path: '.omx/context/autopilot.md' },
1803
2686
  }, null, 2));
2687
+ await mkdir(join(cwd, '.omx', 'context'), { recursive: true });
2688
+ await writeFile(join(cwd, '.omx', 'context', 'autopilot.md'), '# autopilot context');
1804
2689
  const result = await recordSkillActivation({
1805
2690
  stateDir,
1806
2691
  text: '\\ keep going now',
@@ -1813,13 +2698,149 @@ deepMaxRounds = 21
1813
2698
  assert.equal(result.transition_error, undefined);
1814
2699
  const modeState = JSON.parse(await readFile(join(stateDir, 'sessions', 'sess-autopilot-bare', 'autopilot-state.json'), 'utf-8'));
1815
2700
  assert.equal(modeState.current_phase, 'code-review');
1816
- assert.equal(modeState.state?.context_snapshot_path, '.omx/context/autopilot.md');
2701
+ assert.equal(modeState.state?.context_snapshot_path, undefined);
2702
+ assert.equal(modeState.state?.handoff_artifacts?.context_snapshot_path, '.omx/context/autopilot.md');
1817
2703
  assert.equal(existsSync(join(stateDir, 'sessions', 'sess-autopilot-bare', 'ralph-state.json')), false);
1818
2704
  }
1819
2705
  finally {
1820
2706
  await rm(cwd, { recursive: true, force: true });
1821
2707
  }
1822
2708
  });
2709
+ it('preserves active Autopilot question-wait state on bare continuation', async () => {
2710
+ const cwd = await mkdtemp(join(tmpdir(), 'omx-keyword-state-autopilot-question-wait-'));
2711
+ const stateDir = join(cwd, '.omx', 'state');
2712
+ const sessionId = 'sess-autopilot-question-wait';
2713
+ try {
2714
+ await mkdir(join(stateDir, 'sessions', sessionId), { recursive: true });
2715
+ await writeFile(join(stateDir, 'sessions', sessionId, SKILL_ACTIVE_STATE_FILE), JSON.stringify({
2716
+ version: 1,
2717
+ active: true,
2718
+ skill: 'autopilot',
2719
+ keyword: '$autopilot',
2720
+ phase: 'waiting-for-user',
2721
+ activated_at: '2026-04-19T00:00:00.000Z',
2722
+ updated_at: '2026-04-19T00:10:00.000Z',
2723
+ source: 'keyword-detector',
2724
+ session_id: sessionId,
2725
+ active_skills: [
2726
+ {
2727
+ skill: 'autopilot',
2728
+ phase: 'waiting-for-user',
2729
+ active: true,
2730
+ activated_at: '2026-04-19T00:00:00.000Z',
2731
+ updated_at: '2026-04-19T00:10:00.000Z',
2732
+ session_id: sessionId,
2733
+ },
2734
+ ],
2735
+ }, null, 2));
2736
+ await writeFile(join(stateDir, 'sessions', sessionId, 'autopilot-state.json'), JSON.stringify({
2737
+ active: true,
2738
+ mode: 'autopilot',
2739
+ current_phase: 'waiting-for-user',
2740
+ started_at: '2026-04-19T00:00:00.000Z',
2741
+ updated_at: '2026-04-19T00:10:00.000Z',
2742
+ session_id: sessionId,
2743
+ iteration: 4,
2744
+ max_iterations: 10,
2745
+ review_cycle: 2,
2746
+ run_outcome: 'blocked_on_user',
2747
+ lifecycle_outcome: 'askuserQuestion',
2748
+ state: {
2749
+ deep_interview_question: {
2750
+ status: 'waiting_for_user',
2751
+ obligation_id: 'obligation-question-wait',
2752
+ previous_phase: 'deep-interview',
2753
+ },
2754
+ },
2755
+ }, null, 2));
2756
+ const result = await recordSkillActivation({
2757
+ stateDir,
2758
+ text: '\\ keep going now',
2759
+ sessionId,
2760
+ nowIso: '2026-04-19T00:15:00.000Z',
2761
+ });
2762
+ assert.ok(result);
2763
+ assert.equal(result.skill, 'autopilot');
2764
+ const modeState = JSON.parse(await readFile(join(stateDir, 'sessions', sessionId, 'autopilot-state.json'), 'utf-8'));
2765
+ assert.equal(modeState.current_phase, 'waiting-for-user');
2766
+ assert.equal(modeState.iteration, 4);
2767
+ assert.equal(modeState.max_iterations, 10);
2768
+ assert.equal(modeState.review_cycle, 2);
2769
+ assert.equal(modeState.lifecycle_outcome, 'askuserQuestion');
2770
+ assert.equal(modeState.state?.deep_interview_question?.status, 'waiting_for_user');
2771
+ assert.equal(modeState.state?.deep_interview_question?.obligation_id, 'obligation-question-wait');
2772
+ }
2773
+ finally {
2774
+ await rm(cwd, { recursive: true, force: true });
2775
+ }
2776
+ });
2777
+ it('resets terminal Ralph blocked_on_user state when reactivated', async () => {
2778
+ const cases = [
2779
+ { name: 'phase', phase: 'blocked_on_user', run_outcome: undefined },
2780
+ { name: 'outcome', phase: 'executing', run_outcome: 'blocked_on_user' },
2781
+ ];
2782
+ for (const testCase of cases) {
2783
+ const cwd = await mkdtemp(join(tmpdir(), `omx-keyword-state-ralph-terminal-${testCase.name}-reactivation-`));
2784
+ const stateDir = join(cwd, '.omx', 'state');
2785
+ const sessionId = `sess-ralph-terminal-${testCase.name}`;
2786
+ try {
2787
+ await mkdir(join(stateDir, 'sessions', sessionId), { recursive: true });
2788
+ await writeFile(join(stateDir, 'sessions', sessionId, SKILL_ACTIVE_STATE_FILE), JSON.stringify({
2789
+ version: 1,
2790
+ active: true,
2791
+ skill: 'ralph',
2792
+ keyword: '$ralph',
2793
+ phase: testCase.phase,
2794
+ activated_at: '2026-04-19T00:00:00.000Z',
2795
+ updated_at: '2026-04-19T00:10:00.000Z',
2796
+ source: 'keyword-detector',
2797
+ session_id: sessionId,
2798
+ active_skills: [
2799
+ {
2800
+ skill: 'ralph',
2801
+ phase: testCase.phase,
2802
+ active: true,
2803
+ activated_at: '2026-04-19T00:00:00.000Z',
2804
+ updated_at: '2026-04-19T00:10:00.000Z',
2805
+ session_id: sessionId,
2806
+ },
2807
+ ],
2808
+ }, null, 2));
2809
+ await writeFile(join(stateDir, 'sessions', sessionId, 'ralph-state.json'), JSON.stringify({
2810
+ active: false,
2811
+ mode: 'ralph',
2812
+ current_phase: testCase.phase,
2813
+ started_at: '2026-04-19T00:00:00.000Z',
2814
+ completed_at: '2026-04-19T00:10:00.000Z',
2815
+ iteration: 50,
2816
+ max_iterations: 50,
2817
+ ...(testCase.run_outcome ? { run_outcome: testCase.run_outcome } : {}),
2818
+ }, null, 2));
2819
+ const result = await recordSkillActivation({
2820
+ stateDir,
2821
+ text: '\\ keep going now',
2822
+ sessionId,
2823
+ nowIso: '2026-04-19T00:15:00.000Z',
2824
+ });
2825
+ assert.ok(result);
2826
+ assert.equal(result.skill, 'ralph');
2827
+ assert.equal(result.phase, 'planning');
2828
+ assert.equal(result.activated_at, '2026-04-19T00:15:00.000Z');
2829
+ assert.equal(result.active_skills?.[0]?.phase, 'planning');
2830
+ assert.equal(result.active_skills?.[0]?.activated_at, '2026-04-19T00:15:00.000Z');
2831
+ const modeState = JSON.parse(await readFile(join(stateDir, 'sessions', sessionId, 'ralph-state.json'), 'utf-8'));
2832
+ assert.equal(modeState.active, true);
2833
+ assert.equal(modeState.current_phase, 'starting');
2834
+ assert.equal(modeState.started_at, '2026-04-19T00:15:00.000Z');
2835
+ assert.equal(modeState.completed_at, undefined);
2836
+ assert.equal(modeState.iteration, 0);
2837
+ assert.equal(modeState.max_iterations, 50);
2838
+ }
2839
+ finally {
2840
+ await rm(cwd, { recursive: true, force: true });
2841
+ }
2842
+ }
2843
+ });
1823
2844
  it('routes bare keep-going continuation to the active ralph skill instead of resetting through generic keep-going detection', async () => {
1824
2845
  const cwd = await mkdtemp(join(tmpdir(), 'omx-keyword-state-ralph-bare-continuation-'));
1825
2846
  const stateDir = join(cwd, '.omx', 'state');
@@ -2113,6 +3134,43 @@ describe('applyRalplanGate', () => {
2113
3134
  await rm(cwd, { recursive: true, force: true });
2114
3135
  }
2115
3136
  });
3137
+ it('keeps native-proof execution follow-ups gated when consensus is artifact-only', async () => {
3138
+ const cwd = await mkdtemp(join(tmpdir(), 'omx-keyword-gate-native-required-'));
3139
+ try {
3140
+ const plansDir = join(cwd, '.omx', 'plans');
3141
+ const stateDir = join(cwd, '.omx', 'state');
3142
+ await mkdir(plansDir, { recursive: true });
3143
+ await mkdir(stateDir, { recursive: true });
3144
+ await writeFile(join(plansDir, 'prd-issue-833.md'), '# Approved plan\n\nLaunch hint: omx team 3:executor "Execute approved issue 833 plan"\n');
3145
+ await writeFile(join(plansDir, 'test-spec-issue-833.md'), '# Test spec\n');
3146
+ await writeFile(join(stateDir, 'ralplan-state.json'), JSON.stringify({
3147
+ current_phase: 'complete',
3148
+ planning_complete: true,
3149
+ ralplan_consensus_gate: {
3150
+ complete: true,
3151
+ sequence: ['architect-review', 'critic-review'],
3152
+ ralplan_architect_review: {
3153
+ agent_role: 'architect',
3154
+ verdict: 'approve',
3155
+ iteration: 1,
3156
+ provenance_kind: 'codex_exec',
3157
+ },
3158
+ ralplan_critic_review: {
3159
+ agent_role: 'critic',
3160
+ verdict: 'approve',
3161
+ iteration: 1,
3162
+ provenance_kind: 'codex_exec',
3163
+ },
3164
+ },
3165
+ }));
3166
+ const result = applyRalplanGate(['team'], 'team', { cwd, requireNativeSubagents: true });
3167
+ assert.equal(result.gateApplied, true);
3168
+ assert.deepEqual(result.keywords, ['ralplan']);
3169
+ }
3170
+ finally {
3171
+ await rm(cwd, { recursive: true, force: true });
3172
+ }
3173
+ });
2116
3174
  it('does not re-enter ralplan for a short approved ralph follow-up with durable consensus', async () => {
2117
3175
  const cwd = await mkdtemp(join(tmpdir(), 'omx-keyword-gate-followup-ralph-'));
2118
3176
  try {