oh-my-codex 0.18.6 → 0.18.7

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 (278) hide show
  1. package/Cargo.lock +6 -6
  2. package/Cargo.toml +1 -1
  3. package/README.md +56 -7
  4. package/dist/agents/__tests__/definitions.test.js +11 -0
  5. package/dist/agents/__tests__/definitions.test.js.map +1 -1
  6. package/dist/agents/__tests__/native-config.test.js +14 -5
  7. package/dist/agents/__tests__/native-config.test.js.map +1 -1
  8. package/dist/agents/definitions.d.ts +2 -0
  9. package/dist/agents/definitions.d.ts.map +1 -1
  10. package/dist/agents/definitions.js +4 -1
  11. package/dist/agents/definitions.js.map +1 -1
  12. package/dist/agents/native-config.js +2 -2
  13. package/dist/agents/native-config.js.map +1 -1
  14. package/dist/autopilot/__tests__/fsm.test.d.ts +2 -0
  15. package/dist/autopilot/__tests__/fsm.test.d.ts.map +1 -0
  16. package/dist/autopilot/__tests__/fsm.test.js +75 -0
  17. package/dist/autopilot/__tests__/fsm.test.js.map +1 -0
  18. package/dist/autopilot/__tests__/ralplan-gate.test.d.ts +2 -0
  19. package/dist/autopilot/__tests__/ralplan-gate.test.d.ts.map +1 -0
  20. package/dist/autopilot/__tests__/ralplan-gate.test.js +79 -0
  21. package/dist/autopilot/__tests__/ralplan-gate.test.js.map +1 -0
  22. package/dist/autopilot/deep-interview-gate.d.ts +18 -0
  23. package/dist/autopilot/deep-interview-gate.d.ts.map +1 -0
  24. package/dist/autopilot/deep-interview-gate.js +256 -0
  25. package/dist/autopilot/deep-interview-gate.js.map +1 -0
  26. package/dist/autopilot/fsm.d.ts +13 -0
  27. package/dist/autopilot/fsm.d.ts.map +1 -0
  28. package/dist/autopilot/fsm.js +70 -0
  29. package/dist/autopilot/fsm.js.map +1 -0
  30. package/dist/autopilot/ralplan-gate.d.ts +17 -0
  31. package/dist/autopilot/ralplan-gate.d.ts.map +1 -0
  32. package/dist/autopilot/ralplan-gate.js +61 -0
  33. package/dist/autopilot/ralplan-gate.js.map +1 -0
  34. package/dist/cli/__tests__/index.test.js +24 -4
  35. package/dist/cli/__tests__/index.test.js.map +1 -1
  36. package/dist/cli/__tests__/launch-fallback.test.js +175 -6
  37. package/dist/cli/__tests__/launch-fallback.test.js.map +1 -1
  38. package/dist/cli/__tests__/question.test.js +100 -0
  39. package/dist/cli/__tests__/question.test.js.map +1 -1
  40. package/dist/cli/__tests__/setup-refresh.test.js +18 -0
  41. package/dist/cli/__tests__/setup-refresh.test.js.map +1 -1
  42. package/dist/cli/__tests__/team.test.js +2 -2
  43. package/dist/cli/__tests__/team.test.js.map +1 -1
  44. package/dist/cli/index.d.ts +3 -1
  45. package/dist/cli/index.d.ts.map +1 -1
  46. package/dist/cli/index.js +191 -36
  47. package/dist/cli/index.js.map +1 -1
  48. package/dist/cli/question.d.ts.map +1 -1
  49. package/dist/cli/question.js +36 -5
  50. package/dist/cli/question.js.map +1 -1
  51. package/dist/config/__tests__/deep-interview.test.js +7 -6
  52. package/dist/config/__tests__/deep-interview.test.js.map +1 -1
  53. package/dist/config/deep-interview.d.ts.map +1 -1
  54. package/dist/config/deep-interview.js +14 -4
  55. package/dist/config/deep-interview.js.map +1 -1
  56. package/dist/hooks/__tests__/autopilot-skill-contract.test.js +8 -0
  57. package/dist/hooks/__tests__/autopilot-skill-contract.test.js.map +1 -1
  58. package/dist/hooks/__tests__/deep-interview-contract.test.js +10 -0
  59. package/dist/hooks/__tests__/deep-interview-contract.test.js.map +1 -1
  60. package/dist/hooks/__tests__/keyword-detector.test.js +649 -11
  61. package/dist/hooks/__tests__/keyword-detector.test.js.map +1 -1
  62. package/dist/hooks/__tests__/notify-fallback-watcher.test.js +63 -0
  63. package/dist/hooks/__tests__/notify-fallback-watcher.test.js.map +1 -1
  64. package/dist/hooks/__tests__/session.test.js +25 -0
  65. package/dist/hooks/__tests__/session.test.js.map +1 -1
  66. package/dist/hooks/deep-interview-config-instruction.js +1 -1
  67. package/dist/hooks/deep-interview-config-instruction.js.map +1 -1
  68. package/dist/hooks/keyword-detector.d.ts +1 -0
  69. package/dist/hooks/keyword-detector.d.ts.map +1 -1
  70. package/dist/hooks/keyword-detector.js +171 -21
  71. package/dist/hooks/keyword-detector.js.map +1 -1
  72. package/dist/hooks/keyword-registry.d.ts.map +1 -1
  73. package/dist/hooks/keyword-registry.js +1 -0
  74. package/dist/hooks/keyword-registry.js.map +1 -1
  75. package/dist/hooks/session.d.ts +2 -0
  76. package/dist/hooks/session.d.ts.map +1 -1
  77. package/dist/hooks/session.js +13 -5
  78. package/dist/hooks/session.js.map +1 -1
  79. package/dist/hud/__tests__/authority.test.js +35 -0
  80. package/dist/hud/__tests__/authority.test.js.map +1 -1
  81. package/dist/hud/__tests__/index.test.js +168 -2
  82. package/dist/hud/__tests__/index.test.js.map +1 -1
  83. package/dist/hud/__tests__/reconcile.test.js +67 -13
  84. package/dist/hud/__tests__/reconcile.test.js.map +1 -1
  85. package/dist/hud/__tests__/state.test.js +80 -0
  86. package/dist/hud/__tests__/state.test.js.map +1 -1
  87. package/dist/hud/__tests__/tmux.test.js +134 -1
  88. package/dist/hud/__tests__/tmux.test.js.map +1 -1
  89. package/dist/hud/authority.d.ts.map +1 -1
  90. package/dist/hud/authority.js +13 -2
  91. package/dist/hud/authority.js.map +1 -1
  92. package/dist/hud/index.d.ts +17 -0
  93. package/dist/hud/index.d.ts.map +1 -1
  94. package/dist/hud/index.js +64 -10
  95. package/dist/hud/index.js.map +1 -1
  96. package/dist/hud/reconcile.js +1 -1
  97. package/dist/hud/reconcile.js.map +1 -1
  98. package/dist/hud/state.d.ts.map +1 -1
  99. package/dist/hud/state.js +16 -1
  100. package/dist/hud/state.js.map +1 -1
  101. package/dist/hud/tmux.d.ts +2 -0
  102. package/dist/hud/tmux.d.ts.map +1 -1
  103. package/dist/hud/tmux.js +39 -2
  104. package/dist/hud/tmux.js.map +1 -1
  105. package/dist/mcp/__tests__/hermes-bridge.test.js +203 -7
  106. package/dist/mcp/__tests__/hermes-bridge.test.js.map +1 -1
  107. package/dist/mcp/__tests__/state-server.test.js +13 -1
  108. package/dist/mcp/__tests__/state-server.test.js.map +1 -1
  109. package/dist/mcp/hermes-bridge.d.ts +12 -2
  110. package/dist/mcp/hermes-bridge.d.ts.map +1 -1
  111. package/dist/mcp/hermes-bridge.js +83 -9
  112. package/dist/mcp/hermes-bridge.js.map +1 -1
  113. package/dist/modes/__tests__/base-autoresearch-contract.test.js +7 -1
  114. package/dist/modes/__tests__/base-autoresearch-contract.test.js.map +1 -1
  115. package/dist/pipeline/__tests__/stages.test.js +130 -0
  116. package/dist/pipeline/__tests__/stages.test.js.map +1 -1
  117. package/dist/pipeline/orchestrator.js +1 -1
  118. package/dist/pipeline/orchestrator.js.map +1 -1
  119. package/dist/pipeline/stages/ralplan.d.ts +1 -0
  120. package/dist/pipeline/stages/ralplan.d.ts.map +1 -1
  121. package/dist/pipeline/stages/ralplan.js +14 -5
  122. package/dist/pipeline/stages/ralplan.js.map +1 -1
  123. package/dist/question/__tests__/deep-interview.test.js +160 -2
  124. package/dist/question/__tests__/deep-interview.test.js.map +1 -1
  125. package/dist/question/__tests__/policy.test.js +63 -3
  126. package/dist/question/__tests__/policy.test.js.map +1 -1
  127. package/dist/question/__tests__/renderer.test.js +191 -2
  128. package/dist/question/__tests__/renderer.test.js.map +1 -1
  129. package/dist/question/__tests__/state.test.js +94 -3
  130. package/dist/question/__tests__/state.test.js.map +1 -1
  131. package/dist/question/__tests__/ui.test.js +4 -0
  132. package/dist/question/__tests__/ui.test.js.map +1 -1
  133. package/dist/question/autopilot-wait.d.ts +12 -2
  134. package/dist/question/autopilot-wait.d.ts.map +1 -1
  135. package/dist/question/autopilot-wait.js +158 -47
  136. package/dist/question/autopilot-wait.js.map +1 -1
  137. package/dist/question/deep-interview.d.ts.map +1 -1
  138. package/dist/question/deep-interview.js +22 -6
  139. package/dist/question/deep-interview.js.map +1 -1
  140. package/dist/question/policy.d.ts.map +1 -1
  141. package/dist/question/policy.js +2 -5
  142. package/dist/question/policy.js.map +1 -1
  143. package/dist/question/renderer.d.ts +12 -0
  144. package/dist/question/renderer.d.ts.map +1 -1
  145. package/dist/question/renderer.js +87 -3
  146. package/dist/question/renderer.js.map +1 -1
  147. package/dist/question/state.d.ts +8 -1
  148. package/dist/question/state.d.ts.map +1 -1
  149. package/dist/question/state.js +54 -14
  150. package/dist/question/state.js.map +1 -1
  151. package/dist/question/types.d.ts +1 -1
  152. package/dist/question/types.d.ts.map +1 -1
  153. package/dist/question/ui.d.ts +1 -0
  154. package/dist/question/ui.d.ts.map +1 -1
  155. package/dist/question/ui.js +1 -0
  156. package/dist/question/ui.js.map +1 -1
  157. package/dist/ralplan/__tests__/runtime.test.js +191 -0
  158. package/dist/ralplan/__tests__/runtime.test.js.map +1 -1
  159. package/dist/ralplan/consensus-gate.d.ts +9 -1
  160. package/dist/ralplan/consensus-gate.d.ts.map +1 -1
  161. package/dist/ralplan/consensus-gate.js +84 -2
  162. package/dist/ralplan/consensus-gate.js.map +1 -1
  163. package/dist/ralplan/runtime.d.ts +9 -0
  164. package/dist/ralplan/runtime.d.ts.map +1 -1
  165. package/dist/ralplan/runtime.js +32 -11
  166. package/dist/ralplan/runtime.js.map +1 -1
  167. package/dist/scripts/__tests__/codex-native-hook.test.js +1487 -34
  168. package/dist/scripts/__tests__/codex-native-hook.test.js.map +1 -1
  169. package/dist/scripts/codex-native-hook.d.ts.map +1 -1
  170. package/dist/scripts/codex-native-hook.js +356 -38
  171. package/dist/scripts/codex-native-hook.js.map +1 -1
  172. package/dist/scripts/codex-native-pre-post.d.ts.map +1 -1
  173. package/dist/scripts/codex-native-pre-post.js +79 -1
  174. package/dist/scripts/codex-native-pre-post.js.map +1 -1
  175. package/dist/scripts/hook-payload-guard.d.ts +9 -0
  176. package/dist/scripts/hook-payload-guard.d.ts.map +1 -0
  177. package/dist/scripts/hook-payload-guard.js +111 -0
  178. package/dist/scripts/hook-payload-guard.js.map +1 -0
  179. package/dist/scripts/notify-fallback-watcher.js +8 -1
  180. package/dist/scripts/notify-fallback-watcher.js.map +1 -1
  181. package/dist/scripts/notify-hook/__tests__/payload-guard.test.d.ts +2 -0
  182. package/dist/scripts/notify-hook/__tests__/payload-guard.test.d.ts.map +1 -0
  183. package/dist/scripts/notify-hook/__tests__/payload-guard.test.js +39 -0
  184. package/dist/scripts/notify-hook/__tests__/payload-guard.test.js.map +1 -0
  185. package/dist/scripts/notify-hook/team-worker-stop.d.ts.map +1 -1
  186. package/dist/scripts/notify-hook/team-worker-stop.js +234 -86
  187. package/dist/scripts/notify-hook/team-worker-stop.js.map +1 -1
  188. package/dist/scripts/notify-hook.js +11 -2
  189. package/dist/scripts/notify-hook.js.map +1 -1
  190. package/dist/state/__tests__/operations.test.js +1012 -1
  191. package/dist/state/__tests__/operations.test.js.map +1 -1
  192. package/dist/state/__tests__/skill-active.test.js +59 -1
  193. package/dist/state/__tests__/skill-active.test.js.map +1 -1
  194. package/dist/state/__tests__/workflow-transition.test.js +73 -7
  195. package/dist/state/__tests__/workflow-transition.test.js.map +1 -1
  196. package/dist/state/operations.d.ts.map +1 -1
  197. package/dist/state/operations.js +102 -0
  198. package/dist/state/operations.js.map +1 -1
  199. package/dist/state/skill-active.d.ts.map +1 -1
  200. package/dist/state/skill-active.js +33 -3
  201. package/dist/state/skill-active.js.map +1 -1
  202. package/dist/state/workflow-transition-reconcile.d.ts +6 -0
  203. package/dist/state/workflow-transition-reconcile.d.ts.map +1 -1
  204. package/dist/state/workflow-transition-reconcile.js +28 -1
  205. package/dist/state/workflow-transition-reconcile.js.map +1 -1
  206. package/dist/state/workflow-transition.d.ts.map +1 -1
  207. package/dist/state/workflow-transition.js +10 -3
  208. package/dist/state/workflow-transition.js.map +1 -1
  209. package/dist/subagents/__tests__/tracker.test.js +139 -0
  210. package/dist/subagents/__tests__/tracker.test.js.map +1 -1
  211. package/dist/subagents/tracker.d.ts +3 -0
  212. package/dist/subagents/tracker.d.ts.map +1 -1
  213. package/dist/subagents/tracker.js +41 -4
  214. package/dist/subagents/tracker.js.map +1 -1
  215. package/dist/team/__tests__/coordination-protocol.test.d.ts +2 -0
  216. package/dist/team/__tests__/coordination-protocol.test.d.ts.map +1 -0
  217. package/dist/team/__tests__/coordination-protocol.test.js +173 -0
  218. package/dist/team/__tests__/coordination-protocol.test.js.map +1 -0
  219. package/dist/team/__tests__/runtime.test.js +51 -2
  220. package/dist/team/__tests__/runtime.test.js.map +1 -1
  221. package/dist/team/__tests__/state.test.js +83 -0
  222. package/dist/team/__tests__/state.test.js.map +1 -1
  223. package/dist/team/__tests__/tmux-session.test.js +45 -0
  224. package/dist/team/__tests__/tmux-session.test.js.map +1 -1
  225. package/dist/team/__tests__/worker-bootstrap.test.js +84 -0
  226. package/dist/team/__tests__/worker-bootstrap.test.js.map +1 -1
  227. package/dist/team/coordination-protocol.d.ts +14 -0
  228. package/dist/team/coordination-protocol.d.ts.map +1 -0
  229. package/dist/team/coordination-protocol.js +244 -0
  230. package/dist/team/coordination-protocol.js.map +1 -0
  231. package/dist/team/runtime.d.ts +1 -0
  232. package/dist/team/runtime.d.ts.map +1 -1
  233. package/dist/team/runtime.js +19 -3
  234. package/dist/team/runtime.js.map +1 -1
  235. package/dist/team/state/tasks.d.ts.map +1 -1
  236. package/dist/team/state/tasks.js +24 -0
  237. package/dist/team/state/tasks.js.map +1 -1
  238. package/dist/team/state/types.d.ts +21 -1
  239. package/dist/team/state/types.d.ts.map +1 -1
  240. package/dist/team/state/types.js.map +1 -1
  241. package/dist/team/state.d.ts +17 -1
  242. package/dist/team/state.d.ts.map +1 -1
  243. package/dist/team/state.js +12 -5
  244. package/dist/team/state.js.map +1 -1
  245. package/dist/team/team-ops.d.ts +1 -1
  246. package/dist/team/team-ops.d.ts.map +1 -1
  247. package/dist/team/team-ops.js.map +1 -1
  248. package/dist/team/tmux-session.d.ts.map +1 -1
  249. package/dist/team/tmux-session.js +19 -1
  250. package/dist/team/tmux-session.js.map +1 -1
  251. package/dist/team/worker-bootstrap.d.ts.map +1 -1
  252. package/dist/team/worker-bootstrap.js +63 -0
  253. package/dist/team/worker-bootstrap.js.map +1 -1
  254. package/dist/utils/__tests__/agents-model-table.test.js +4 -2
  255. package/dist/utils/__tests__/agents-model-table.test.js.map +1 -1
  256. package/dist/utils/agents-model-table.d.ts.map +1 -1
  257. package/dist/utils/agents-model-table.js +3 -0
  258. package/dist/utils/agents-model-table.js.map +1 -1
  259. package/package.json +1 -1
  260. package/plugins/oh-my-codex/.codex-plugin/plugin.json +1 -1
  261. package/plugins/oh-my-codex/skills/autopilot/SKILL.md +10 -5
  262. package/plugins/oh-my-codex/skills/deep-interview/SKILL.md +9 -4
  263. package/plugins/oh-my-codex/skills/ralplan/SKILL.md +12 -0
  264. package/plugins/oh-my-codex/skills/team/SKILL.md +16 -0
  265. package/plugins/oh-my-codex/skills/worker/SKILL.md +14 -0
  266. package/skills/autopilot/SKILL.md +10 -5
  267. package/skills/deep-interview/SKILL.md +9 -4
  268. package/skills/ralplan/SKILL.md +12 -0
  269. package/skills/team/SKILL.md +16 -0
  270. package/skills/worker/SKILL.md +14 -0
  271. package/src/scripts/__tests__/codex-native-hook.test.ts +2202 -523
  272. package/src/scripts/codex-native-hook.ts +444 -36
  273. package/src/scripts/codex-native-pre-post.ts +80 -0
  274. package/src/scripts/hook-payload-guard.ts +113 -0
  275. package/src/scripts/notify-fallback-watcher.ts +8 -1
  276. package/src/scripts/notify-hook/__tests__/payload-guard.test.ts +41 -0
  277. package/src/scripts/notify-hook/team-worker-stop.ts +193 -52
  278. package/src/scripts/notify-hook.ts +14 -2
@@ -133,6 +133,28 @@ describe('keyword detector team compatibility', () => {
133
133
  const pathOnly = detectPrimaryKeyword('inspect .omx/ultragoal/goals.json');
134
134
  assert.notEqual(pathOnly?.skill, 'ultragoal');
135
135
  });
136
+ it('maps bare and command-style autopilot invocations to autopilot', () => {
137
+ for (const prompt of ['autopilot', 'run autopilot', 'autopilot this', 'autopilot mode']) {
138
+ const match = detectPrimaryKeyword(prompt);
139
+ assert.ok(match, `expected autopilot match for ${prompt}`);
140
+ assert.equal(match.skill, 'autopilot');
141
+ assert.equal(match.keyword.toLowerCase(), 'autopilot');
142
+ }
143
+ });
144
+ it('does not trigger autopilot from management/debug prose mentions', () => {
145
+ assert.equal(detectPrimaryKeyword('inspect autopilot state before continuing'), null);
146
+ assert.equal(detectPrimaryKeyword('fix the autopilot bug in the detector'), null);
147
+ assert.equal(detectPrimaryKeyword('why did autopilot fail?'), null);
148
+ assert.equal(detectPrimaryKeyword('run autopilot tests'), null);
149
+ assert.equal(detectPrimaryKeyword('run autopilot regression tests'), null);
150
+ assert.equal(detectPrimaryKeyword('continue autopilot debugging'), null);
151
+ assert.equal(detectPrimaryKeyword('start autopilot bug investigation'), null);
152
+ });
153
+ it('keeps higher-priority workflow keywords ahead of autopilot mentions', () => {
154
+ const match = detectPrimaryKeyword('autopilot this after consensus plan');
155
+ assert.ok(match);
156
+ assert.equal(match.skill, 'ralplan');
157
+ });
136
158
  it('maps code-review keyword variants to code-review skill', () => {
137
159
  const hyphen = detectPrimaryKeyword('run $code-review before merge');
138
160
  assert.ok(hyphen);
@@ -333,6 +355,7 @@ describe('keyword registry coverage', () => {
333
355
  assert.ok(registryKeywords.has('$ultragoal'));
334
356
  assert.ok(registryKeywords.has('$prometheus-strict'));
335
357
  assert.ok(registryKeywords.has('ultragoal'));
358
+ assert.ok(registryKeywords.has('autopilot'));
336
359
  });
337
360
  });
338
361
  describe('keyword detector skill-active-state lifecycle', () => {
@@ -496,6 +519,146 @@ describe('keyword detector skill-active-state lifecycle', () => {
496
519
  await rm(cwd, { recursive: true, force: true });
497
520
  }
498
521
  });
522
+ it('fully resets terminal Autopilot mode state when reactivated', async () => {
523
+ const cwd = await mkdtemp(join(tmpdir(), 'omx-keyword-autopilot-terminal-reset-'));
524
+ const stateDir = join(cwd, '.omx', 'state');
525
+ const sessionId = 'sess-autopilot-terminal-reset';
526
+ try {
527
+ await mkdir(join(stateDir, 'sessions', sessionId), { recursive: true });
528
+ await writeFile(join(stateDir, 'sessions', sessionId, SKILL_ACTIVE_STATE_FILE), JSON.stringify({
529
+ version: 1,
530
+ active: true,
531
+ skill: 'autopilot',
532
+ keyword: '$autopilot',
533
+ phase: 'complete',
534
+ activated_at: '2026-05-29T00:00:00.000Z',
535
+ updated_at: '2026-05-29T00:00:00.000Z',
536
+ session_id: sessionId,
537
+ active_skills: [{ skill: 'autopilot', active: true, phase: 'complete', session_id: sessionId }],
538
+ }, null, 2));
539
+ await writeFile(join(stateDir, 'sessions', sessionId, 'autopilot-state.json'), JSON.stringify({
540
+ active: true,
541
+ mode: 'autopilot',
542
+ current_phase: 'complete',
543
+ started_at: '2026-05-29T00:00:00.000Z',
544
+ completed_at: '2026-05-29T00:10:00.000Z',
545
+ iteration: 10,
546
+ max_iterations: 10,
547
+ review_cycle: 3,
548
+ lifecycle_outcome: 'finished',
549
+ run_outcome: 'finish',
550
+ handoff_artifacts: {
551
+ code_review: { verdict: 'APPROVE / CLEAR' },
552
+ ultraqa: { verdict: 'pass' },
553
+ },
554
+ state: {
555
+ handoff_artifacts: {
556
+ ralplan_consensus_gate: { complete: false },
557
+ code_review: { verdict: 'stale' },
558
+ },
559
+ },
560
+ }, null, 2));
561
+ const result = await recordSkillActivation({
562
+ stateDir,
563
+ text: '$autopilot investigate the next issue',
564
+ sessionId,
565
+ threadId: 'thread-reactivated',
566
+ turnId: 'turn-reactivated',
567
+ nowIso: '2026-05-30T00:00:00.000Z',
568
+ });
569
+ assert.ok(result);
570
+ assert.equal(result.skill, 'autopilot');
571
+ assert.equal(result.phase, 'deep-interview');
572
+ assert.equal(result.activated_at, '2026-05-30T00:00:00.000Z');
573
+ assert.equal(result.active_skills?.[0]?.phase, 'deep-interview');
574
+ assert.equal(result.active_skills?.[0]?.activated_at, '2026-05-30T00:00:00.000Z');
575
+ const skillState = JSON.parse(await readFile(join(stateDir, 'sessions', sessionId, SKILL_ACTIVE_STATE_FILE), 'utf-8'));
576
+ assert.equal(skillState.phase, 'deep-interview');
577
+ assert.equal(skillState.activated_at, '2026-05-30T00:00:00.000Z');
578
+ assert.equal(skillState.active_skills?.[0]?.phase, 'deep-interview');
579
+ assert.equal(skillState.active_skills?.[0]?.activated_at, '2026-05-30T00:00:00.000Z');
580
+ const modeState = JSON.parse(await readFile(join(stateDir, 'sessions', sessionId, 'autopilot-state.json'), 'utf-8'));
581
+ assert.equal(modeState.active, true);
582
+ assert.equal(modeState.current_phase, 'deep-interview');
583
+ assert.equal(modeState.started_at, '2026-05-30T00:00:00.000Z');
584
+ assert.equal(modeState.completed_at, undefined);
585
+ assert.equal(modeState.iteration, 1);
586
+ assert.equal(modeState.max_iterations, 10);
587
+ assert.equal(modeState.review_cycle, 0);
588
+ assert.equal(modeState.lifecycle_outcome, undefined);
589
+ assert.equal(modeState.run_outcome, undefined);
590
+ assert.equal(modeState.handoff_artifacts, undefined);
591
+ assert.deepEqual(modeState.state?.handoff_artifacts, {
592
+ deep_interview: null,
593
+ ralplan: null,
594
+ ralplan_consensus_gate: {
595
+ required: true,
596
+ sequence: ['architect-review', 'critic-review'],
597
+ planning_artifacts_are_not_consensus: true,
598
+ required_review_roles: ['architect', 'critic'],
599
+ ralplan_architect_review: null,
600
+ ralplan_critic_review: null,
601
+ complete: false,
602
+ },
603
+ ultragoal: null,
604
+ code_review: null,
605
+ ultraqa: null,
606
+ });
607
+ }
608
+ finally {
609
+ await rm(cwd, { recursive: true, force: true });
610
+ }
611
+ });
612
+ it('resets stopped Autopilot mode state when reactivated', async () => {
613
+ for (const phase of ['stopped', 'user-stopped']) {
614
+ const cwd = await mkdtemp(join(tmpdir(), `omx-keyword-autopilot-${phase}-reset-`));
615
+ const stateDir = join(cwd, '.omx', 'state');
616
+ const sessionId = `sess-autopilot-${phase}-reset`;
617
+ try {
618
+ await mkdir(join(stateDir, 'sessions', sessionId), { recursive: true });
619
+ await writeFile(join(stateDir, 'sessions', sessionId, SKILL_ACTIVE_STATE_FILE), JSON.stringify({
620
+ version: 1,
621
+ active: true,
622
+ skill: 'autopilot',
623
+ keyword: '$autopilot',
624
+ phase,
625
+ activated_at: '2026-05-29T00:00:00.000Z',
626
+ updated_at: '2026-05-29T00:00:00.000Z',
627
+ source: 'keyword-detector',
628
+ session_id: sessionId,
629
+ active_skills: [{ skill: 'autopilot', active: true, phase, session_id: sessionId }],
630
+ }, null, 2));
631
+ await writeFile(join(stateDir, 'sessions', sessionId, 'autopilot-state.json'), JSON.stringify({
632
+ active: true,
633
+ mode: 'autopilot',
634
+ current_phase: phase,
635
+ started_at: '2026-05-29T00:00:00.000Z',
636
+ completed_at: '2026-05-29T00:10:00.000Z',
637
+ iteration: 10,
638
+ max_iterations: 10,
639
+ review_cycle: 3,
640
+ state: { handoff_artifacts: { code_review: { verdict: 'stale' } } },
641
+ }, null, 2));
642
+ const result = await recordSkillActivation({
643
+ stateDir,
644
+ text: '$autopilot new task after stop',
645
+ sessionId,
646
+ nowIso: '2026-05-30T00:00:00.000Z',
647
+ });
648
+ assert.ok(result);
649
+ assert.equal(result.phase, 'deep-interview');
650
+ assert.equal(result.activated_at, '2026-05-30T00:00:00.000Z');
651
+ const modeState = JSON.parse(await readFile(join(stateDir, 'sessions', sessionId, 'autopilot-state.json'), 'utf-8'));
652
+ assert.equal(modeState.current_phase, 'deep-interview');
653
+ assert.equal(modeState.iteration, 1);
654
+ assert.equal(modeState.review_cycle, 0);
655
+ assert.equal(modeState.state?.handoff_artifacts?.code_review, null);
656
+ }
657
+ finally {
658
+ await rm(cwd, { recursive: true, force: true });
659
+ }
660
+ }
661
+ });
499
662
  it('adds approved workflow overlaps without deleting the existing canonical state', async () => {
500
663
  const cwd = await mkdtemp(join(tmpdir(), 'omx-keyword-state-overlap-'));
501
664
  const stateDir = join(cwd, '.omx', 'state');
@@ -864,7 +1027,7 @@ describe('keyword detector skill-active-state lifecycle', () => {
864
1027
  await rm(cwd, { recursive: true, force: true });
865
1028
  }
866
1029
  });
867
- it('keeps ralplan as an allowlisted deep-interview forward handoff', async () => {
1030
+ it('denies ralplan handoff from deep-interview without completion or explicit skip evidence', async () => {
868
1031
  const cwd = await mkdtemp(join(tmpdir(), 'omx-keyword-ralplan-handoff-'));
869
1032
  const stateDir = join(cwd, '.omx', 'state');
870
1033
  try {
@@ -884,10 +1047,49 @@ describe('keyword detector skill-active-state lifecycle', () => {
884
1047
  sessionId: 'sess-ralplan-handoff',
885
1048
  nowIso: '2026-04-10T00:00:00.000Z',
886
1049
  });
1050
+ assert.equal(result?.skill, 'deep-interview');
1051
+ assert.match(String(result?.transition_error), /missing deep-interview completion\/skip gate/i);
1052
+ const preserved = JSON.parse(await readFile(join(stateDir, 'sessions', 'sess-ralplan-handoff', 'deep-interview-state.json'), 'utf-8'));
1053
+ assert.equal(preserved.active, true);
1054
+ assert.equal(preserved.current_phase, 'intent-first');
1055
+ assert.equal(existsSync(join(stateDir, 'sessions', 'sess-ralplan-handoff', 'ralplan-state.json')), false);
1056
+ }
1057
+ finally {
1058
+ await rm(cwd, { recursive: true, force: true });
1059
+ }
1060
+ });
1061
+ it('allows ralplan handoff from deep-interview with a durable completion gate', async () => {
1062
+ const cwd = await mkdtemp(join(tmpdir(), 'omx-keyword-ralplan-handoff-complete-'));
1063
+ const stateDir = join(cwd, '.omx', 'state');
1064
+ try {
1065
+ await mkdir(join(stateDir, 'sessions', 'sess-ralplan-handoff-complete'), { recursive: true });
1066
+ await writeFile(join(stateDir, 'sessions', 'sess-ralplan-handoff-complete', SKILL_ACTIVE_STATE_FILE), JSON.stringify({
1067
+ version: 1,
1068
+ active: true,
1069
+ skill: 'deep-interview',
1070
+ phase: 'planning',
1071
+ session_id: 'sess-ralplan-handoff-complete',
1072
+ active_skills: [{ skill: 'deep-interview', phase: 'planning', active: true, session_id: 'sess-ralplan-handoff-complete' }],
1073
+ }, null, 2));
1074
+ await writeFile(join(stateDir, 'sessions', 'sess-ralplan-handoff-complete', 'deep-interview-state.json'), JSON.stringify({
1075
+ active: true,
1076
+ mode: 'deep-interview',
1077
+ current_phase: 'intent-first',
1078
+ deep_interview_gate: {
1079
+ status: 'complete',
1080
+ rationale: 'Requirements are clarified and ready for ralplan consensus.',
1081
+ },
1082
+ }, null, 2));
1083
+ const result = await recordSkillActivation({
1084
+ stateDir,
1085
+ text: '$ralplan implement the approved contract',
1086
+ sessionId: 'sess-ralplan-handoff-complete',
1087
+ nowIso: '2026-04-10T00:00:00.000Z',
1088
+ });
887
1089
  assert.equal(result?.transition_error, undefined);
888
1090
  assert.equal(result?.skill, 'ralplan');
889
1091
  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'));
1092
+ const completed = JSON.parse(await readFile(join(stateDir, 'sessions', 'sess-ralplan-handoff-complete', 'deep-interview-state.json'), 'utf-8'));
891
1093
  assert.equal(completed.active, false);
892
1094
  assert.equal(completed.current_phase, 'completed');
893
1095
  }
@@ -1034,6 +1236,57 @@ describe('keyword detector skill-active-state lifecycle', () => {
1034
1236
  await rm(cwd, { recursive: true, force: true });
1035
1237
  }
1036
1238
  });
1239
+ it('emits terminal ralplan state before explicit ultragoal execution handoff', async () => {
1240
+ const cwd = await mkdtemp(join(tmpdir(), 'omx-keyword-ralplan-ultragoal-handoff-'));
1241
+ const stateDir = join(cwd, '.omx', 'state');
1242
+ try {
1243
+ await mkdir(join(stateDir, 'sessions', 'sess-ralplan-ultragoal'), { recursive: true });
1244
+ await writeFile(join(stateDir, 'sessions', 'sess-ralplan-ultragoal', SKILL_ACTIVE_STATE_FILE), JSON.stringify({
1245
+ version: 1,
1246
+ active: true,
1247
+ skill: 'ralplan',
1248
+ keyword: '$ralplan',
1249
+ phase: 'planning',
1250
+ session_id: 'sess-ralplan-ultragoal',
1251
+ active_skills: [{ skill: 'ralplan', phase: 'planning', active: true, session_id: 'sess-ralplan-ultragoal' }],
1252
+ }, null, 2));
1253
+ await writeFile(join(stateDir, 'sessions', 'sess-ralplan-ultragoal', 'ralplan-state.json'), JSON.stringify({
1254
+ active: true,
1255
+ mode: 'ralplan',
1256
+ current_phase: 'complete',
1257
+ planning_complete: true,
1258
+ ralplan_consensus_gate: {
1259
+ complete: true,
1260
+ sequence: ['architect-review', 'critic-review'],
1261
+ ralplan_architect_review: { agent_role: 'architect', verdict: 'approve', approved: true },
1262
+ ralplan_critic_review: { agent_role: 'critic', verdict: 'approve', approved: true },
1263
+ },
1264
+ }, null, 2));
1265
+ const result = await recordSkillActivation({
1266
+ stateDir,
1267
+ sourceCwd: cwd,
1268
+ text: '$ultragoal execute the approved ralplan',
1269
+ sessionId: 'sess-ralplan-ultragoal',
1270
+ nowIso: '2026-04-10T00:20:00.000Z',
1271
+ });
1272
+ assert.equal(result?.transition_error, undefined);
1273
+ assert.equal(result?.skill, 'ultragoal');
1274
+ assert.equal(result?.transition_message, 'mode transiting: ralplan -> ultragoal');
1275
+ assert.deepEqual(result?.active_skills?.map((entry) => entry.skill), ['ultragoal']);
1276
+ const ralplan = JSON.parse(await readFile(join(stateDir, 'sessions', 'sess-ralplan-ultragoal', 'ralplan-state.json'), 'utf-8'));
1277
+ assert.equal(ralplan.active, false);
1278
+ assert.equal(ralplan.current_phase, 'completed');
1279
+ assert.equal(ralplan.auto_completed_reason, 'mode transiting: ralplan -> ultragoal');
1280
+ assert.ok(ralplan.completed_at);
1281
+ const ultragoal = JSON.parse(await readFile(join(stateDir, 'sessions', 'sess-ralplan-ultragoal', 'ultragoal-state.json'), 'utf-8'));
1282
+ assert.equal(ultragoal.active, true);
1283
+ assert.equal(ultragoal.mode, 'ultragoal');
1284
+ assert.equal(ultragoal.current_phase, 'planning');
1285
+ }
1286
+ finally {
1287
+ await rm(cwd, { recursive: true, force: true });
1288
+ }
1289
+ });
1037
1290
  it('keeps root team state out of the session-scoped Ralph canonical state', async () => {
1038
1291
  const cwd = await mkdtemp(join(tmpdir(), 'omx-keyword-state-team-ralph-'));
1039
1292
  const stateDir = join(cwd, '.omx', 'state');
@@ -1523,6 +1776,211 @@ deepMaxRounds = 21
1523
1776
  await rm(cwd, { recursive: true, force: true });
1524
1777
  }
1525
1778
  });
1779
+ it('keeps Autopilot visible when a supervised code-review child keyword appears', async () => {
1780
+ const cwd = await mkdtemp(join(tmpdir(), 'omx-keyword-state-autopilot-child-code-review-'));
1781
+ const stateDir = join(cwd, '.omx', 'state');
1782
+ const sessionId = 'sess-autopilot-child-code-review';
1783
+ try {
1784
+ await mkdir(join(stateDir, 'sessions', sessionId), { recursive: true });
1785
+ await writeFile(join(stateDir, 'sessions', sessionId, SKILL_ACTIVE_STATE_FILE), JSON.stringify({
1786
+ version: 1,
1787
+ active: true,
1788
+ skill: 'autopilot',
1789
+ keyword: '$autopilot',
1790
+ phase: 'ralplan',
1791
+ activated_at: '2026-05-30T00:00:00.000Z',
1792
+ updated_at: '2026-05-30T00:01:00.000Z',
1793
+ source: 'keyword-detector',
1794
+ session_id: sessionId,
1795
+ active_skills: [{ skill: 'autopilot', phase: 'ralplan', active: true, session_id: sessionId }],
1796
+ }, null, 2));
1797
+ const result = await recordSkillActivation({
1798
+ stateDir,
1799
+ text: 'CODE REVIEW the current diff before continuing',
1800
+ sessionId,
1801
+ threadId: 'thread-autopilot-child-code-review',
1802
+ turnId: 'turn-autopilot-child-code-review',
1803
+ nowIso: '2026-05-30T00:02:00.000Z',
1804
+ });
1805
+ assert.ok(result);
1806
+ assert.equal(result.skill, 'autopilot');
1807
+ assert.equal(result.phase, 'ralplan');
1808
+ assert.equal(result.supervised_child_skill, 'code-review');
1809
+ const persisted = JSON.parse(await readFile(join(stateDir, 'sessions', sessionId, SKILL_ACTIVE_STATE_FILE), 'utf-8'));
1810
+ assert.equal(persisted.skill, 'autopilot');
1811
+ assert.equal(persisted.phase, 'ralplan');
1812
+ assert.deepEqual(persisted.active_skills?.map((entry) => entry.skill), ['autopilot']);
1813
+ assert.equal(existsSync(join(stateDir, 'sessions', sessionId, 'code-review-state.json')), false);
1814
+ }
1815
+ finally {
1816
+ await rm(cwd, { recursive: true, force: true });
1817
+ }
1818
+ });
1819
+ it('keeps tracked Autopilot child keywords supervised and completes stale child mode state', async () => {
1820
+ const cwd = await mkdtemp(join(tmpdir(), 'omx-keyword-state-autopilot-child-ultraqa-'));
1821
+ const stateDir = join(cwd, '.omx', 'state');
1822
+ const sessionId = 'sess-autopilot-child-ultraqa';
1823
+ try {
1824
+ await mkdir(join(stateDir, 'sessions', sessionId), { recursive: true });
1825
+ await writeFile(join(stateDir, 'sessions', sessionId, SKILL_ACTIVE_STATE_FILE), JSON.stringify({
1826
+ version: 1,
1827
+ active: true,
1828
+ skill: 'autopilot',
1829
+ keyword: '$autopilot',
1830
+ phase: 'ultraqa',
1831
+ activated_at: '2026-05-30T00:00:00.000Z',
1832
+ updated_at: '2026-05-30T00:01:00.000Z',
1833
+ source: 'keyword-detector',
1834
+ session_id: sessionId,
1835
+ active_skills: [{ skill: 'autopilot', phase: 'ultraqa', active: true, session_id: sessionId }],
1836
+ }, null, 2));
1837
+ await writeFile(join(stateDir, 'sessions', sessionId, 'ultragoal-state.json'), JSON.stringify({
1838
+ active: true,
1839
+ mode: 'ultragoal',
1840
+ current_phase: 'planning',
1841
+ session_id: sessionId,
1842
+ started_at: '2026-05-29T23:00:00.000Z',
1843
+ updated_at: '2026-05-29T23:05:00.000Z',
1844
+ }, null, 2));
1845
+ const result = await recordSkillActivation({
1846
+ stateDir,
1847
+ text: '$ultraqa run adversarial checks',
1848
+ sessionId,
1849
+ threadId: 'thread-autopilot-child-ultraqa',
1850
+ turnId: 'turn-autopilot-child-ultraqa',
1851
+ nowIso: '2026-05-30T00:02:00.000Z',
1852
+ });
1853
+ assert.ok(result);
1854
+ assert.equal(result.skill, 'autopilot');
1855
+ assert.equal(result.phase, 'ultraqa');
1856
+ assert.equal(result.supervised_child_skill, 'ultraqa');
1857
+ assert.equal(result.transition_error, undefined);
1858
+ assert.equal(existsSync(join(stateDir, 'sessions', sessionId, 'ultraqa-state.json')), false);
1859
+ const ultragoal = JSON.parse(await readFile(join(stateDir, 'sessions', sessionId, 'ultragoal-state.json'), 'utf-8'));
1860
+ assert.equal(ultragoal.active, false);
1861
+ assert.equal(ultragoal.current_phase, 'completed');
1862
+ assert.match(ultragoal.auto_completed_reason || '', /mode transiting: ultragoal -> ultraqa/);
1863
+ }
1864
+ finally {
1865
+ await rm(cwd, { recursive: true, force: true });
1866
+ }
1867
+ });
1868
+ it('denies supervised Autopilot child rollback without clearing stale execution state', async () => {
1869
+ const cwd = await mkdtemp(join(tmpdir(), 'omx-keyword-state-autopilot-child-rollback-'));
1870
+ const stateDir = join(cwd, '.omx', 'state');
1871
+ const sessionId = 'sess-autopilot-child-rollback';
1872
+ try {
1873
+ await mkdir(join(stateDir, 'sessions', sessionId), { recursive: true });
1874
+ await writeFile(join(stateDir, 'sessions', sessionId, SKILL_ACTIVE_STATE_FILE), JSON.stringify({
1875
+ version: 1,
1876
+ active: true,
1877
+ skill: 'autopilot',
1878
+ keyword: '$autopilot',
1879
+ phase: 'ultragoal',
1880
+ session_id: sessionId,
1881
+ active_skills: [{ skill: 'autopilot', phase: 'ultragoal', active: true, session_id: sessionId }],
1882
+ }, null, 2));
1883
+ await writeFile(join(stateDir, 'sessions', sessionId, 'ultragoal-state.json'), JSON.stringify({
1884
+ active: true,
1885
+ mode: 'ultragoal',
1886
+ current_phase: 'executing',
1887
+ session_id: sessionId,
1888
+ }, null, 2));
1889
+ const result = await recordSkillActivation({
1890
+ stateDir,
1891
+ text: '$deep-interview go back and re-plan',
1892
+ sessionId,
1893
+ nowIso: '2026-05-30T00:03:00.000Z',
1894
+ });
1895
+ assert.equal(result?.skill, 'autopilot');
1896
+ assert.match(String(result?.transition_error), /Execution-to-planning rollback auto-complete is not allowed/i);
1897
+ assert.equal(result?.supervised_child_skill, undefined);
1898
+ assert.equal(existsSync(join(stateDir, 'sessions', sessionId, 'deep-interview-state.json')), false);
1899
+ const ultragoal = JSON.parse(await readFile(join(stateDir, 'sessions', sessionId, 'ultragoal-state.json'), 'utf-8'));
1900
+ assert.equal(ultragoal.active, true);
1901
+ assert.equal(ultragoal.current_phase, 'executing');
1902
+ }
1903
+ finally {
1904
+ await rm(cwd, { recursive: true, force: true });
1905
+ }
1906
+ });
1907
+ it('surfaces supervised Autopilot deep-interview to ralplan gate failures', async () => {
1908
+ const cwd = await mkdtemp(join(tmpdir(), 'omx-keyword-state-autopilot-child-gate-'));
1909
+ const stateDir = join(cwd, '.omx', 'state');
1910
+ const sessionId = 'sess-autopilot-child-gate';
1911
+ try {
1912
+ await mkdir(join(stateDir, 'sessions', sessionId), { recursive: true });
1913
+ await writeFile(join(stateDir, 'sessions', sessionId, SKILL_ACTIVE_STATE_FILE), JSON.stringify({
1914
+ version: 1,
1915
+ active: true,
1916
+ skill: 'autopilot',
1917
+ keyword: '$autopilot',
1918
+ phase: 'deep-interview',
1919
+ session_id: sessionId,
1920
+ active_skills: [{ skill: 'autopilot', phase: 'deep-interview', active: true, session_id: sessionId }],
1921
+ }, null, 2));
1922
+ await writeFile(join(stateDir, 'sessions', sessionId, 'deep-interview-state.json'), JSON.stringify({
1923
+ active: true,
1924
+ mode: 'deep-interview',
1925
+ current_phase: 'intent-first',
1926
+ session_id: sessionId,
1927
+ }, null, 2));
1928
+ const result = await recordSkillActivation({
1929
+ stateDir,
1930
+ text: '$ralplan continue without interview completion evidence',
1931
+ sessionId,
1932
+ nowIso: '2026-05-30T00:04:00.000Z',
1933
+ });
1934
+ assert.equal(result?.skill, 'autopilot');
1935
+ assert.match(String(result?.transition_error), /missing deep-interview completion\/skip gate/i);
1936
+ assert.equal(result?.supervised_child_skill, undefined);
1937
+ assert.equal(existsSync(join(stateDir, 'sessions', sessionId, 'ralplan-state.json')), false);
1938
+ const deepInterview = JSON.parse(await readFile(join(stateDir, 'sessions', sessionId, 'deep-interview-state.json'), 'utf-8'));
1939
+ assert.equal(deepInterview.active, true);
1940
+ assert.equal(deepInterview.current_phase, 'intent-first');
1941
+ }
1942
+ finally {
1943
+ await rm(cwd, { recursive: true, force: true });
1944
+ }
1945
+ });
1946
+ it('ignores stale root child mode state during session-scoped Autopilot child reconciliation', async () => {
1947
+ const cwd = await mkdtemp(join(tmpdir(), 'omx-keyword-state-autopilot-child-session-root-'));
1948
+ const stateDir = join(cwd, '.omx', 'state');
1949
+ const sessionId = 'sess-autopilot-child-session-root';
1950
+ try {
1951
+ await mkdir(join(stateDir, 'sessions', sessionId), { recursive: true });
1952
+ await writeFile(join(stateDir, 'sessions', sessionId, SKILL_ACTIVE_STATE_FILE), JSON.stringify({
1953
+ version: 1,
1954
+ active: true,
1955
+ skill: 'autopilot',
1956
+ keyword: '$autopilot',
1957
+ phase: 'deep-interview',
1958
+ session_id: sessionId,
1959
+ active_skills: [{ skill: 'autopilot', phase: 'deep-interview', active: true, session_id: sessionId }],
1960
+ }, null, 2));
1961
+ await writeFile(join(stateDir, 'ultragoal-state.json'), JSON.stringify({
1962
+ active: true,
1963
+ mode: 'ultragoal',
1964
+ current_phase: 'executing',
1965
+ }, null, 2));
1966
+ const result = await recordSkillActivation({
1967
+ stateDir,
1968
+ text: '$deep-interview continue scoped interview',
1969
+ sessionId,
1970
+ nowIso: '2026-05-30T00:05:00.000Z',
1971
+ });
1972
+ assert.equal(result?.skill, 'autopilot');
1973
+ assert.equal(result?.supervised_child_skill, 'deep-interview');
1974
+ assert.equal(result?.transition_error, undefined);
1975
+ const rootUltragoal = JSON.parse(await readFile(join(stateDir, 'ultragoal-state.json'), 'utf-8'));
1976
+ assert.equal(rootUltragoal.active, true);
1977
+ assert.equal(rootUltragoal.current_phase, 'executing');
1978
+ assert.equal(existsSync(join(stateDir, 'sessions', sessionId, 'deep-interview-state.json')), false);
1979
+ }
1980
+ finally {
1981
+ await rm(cwd, { recursive: true, force: true });
1982
+ }
1983
+ });
1526
1984
  it('records ultragoal as a prompt skill with first-class mode state', async () => {
1527
1985
  const cwd = await mkdtemp(join(tmpdir(), 'omx-keyword-state-ultragoal-'));
1528
1986
  const stateDir = join(cwd, '.omx', 'state');
@@ -1547,19 +2005,27 @@ deepMaxRounds = 21
1547
2005
  }
1548
2006
  });
1549
2007
  it('emits a warning when skill-active-state persistence fails', async () => {
2008
+ const cwd = await mkdtemp(join(tmpdir(), 'omx-keyword-state-persist-fail-'));
1550
2009
  const warnings = [];
1551
2010
  mock.method(console, 'warn', (...args) => {
1552
2011
  warnings.push(args);
1553
2012
  });
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/);
2013
+ try {
2014
+ const blockingFile = join(cwd, 'state-root-file');
2015
+ await writeFile(blockingFile, 'not a directory');
2016
+ const result = await recordSkillActivation({
2017
+ stateDir: join(blockingFile, 'nested', 'state-dir'),
2018
+ text: 'please run $autopilot',
2019
+ nowIso: '2026-02-25T00:00:00.000Z',
2020
+ });
2021
+ assert.ok(result);
2022
+ assert.equal(result.skill, 'autopilot');
2023
+ assert.equal(warnings.length, 1);
2024
+ assert.match(String(warnings[0][0]), /failed to persist keyword activation state/);
2025
+ }
2026
+ finally {
2027
+ await rm(cwd, { recursive: true, force: true });
2028
+ }
1563
2029
  });
1564
2030
  it('preserves activated_at for same-skill continuation', async () => {
1565
2031
  const cwd = await mkdtemp(join(tmpdir(), 'omx-keyword-state-continuation-'));
@@ -1820,6 +2286,141 @@ deepMaxRounds = 21
1820
2286
  await rm(cwd, { recursive: true, force: true });
1821
2287
  }
1822
2288
  });
2289
+ it('preserves active Autopilot question-wait state on bare continuation', async () => {
2290
+ const cwd = await mkdtemp(join(tmpdir(), 'omx-keyword-state-autopilot-question-wait-'));
2291
+ const stateDir = join(cwd, '.omx', 'state');
2292
+ const sessionId = 'sess-autopilot-question-wait';
2293
+ try {
2294
+ await mkdir(join(stateDir, 'sessions', sessionId), { recursive: true });
2295
+ await writeFile(join(stateDir, 'sessions', sessionId, SKILL_ACTIVE_STATE_FILE), JSON.stringify({
2296
+ version: 1,
2297
+ active: true,
2298
+ skill: 'autopilot',
2299
+ keyword: '$autopilot',
2300
+ phase: 'waiting-for-user',
2301
+ activated_at: '2026-04-19T00:00:00.000Z',
2302
+ updated_at: '2026-04-19T00:10:00.000Z',
2303
+ source: 'keyword-detector',
2304
+ session_id: sessionId,
2305
+ active_skills: [
2306
+ {
2307
+ skill: 'autopilot',
2308
+ phase: 'waiting-for-user',
2309
+ active: true,
2310
+ activated_at: '2026-04-19T00:00:00.000Z',
2311
+ updated_at: '2026-04-19T00:10:00.000Z',
2312
+ session_id: sessionId,
2313
+ },
2314
+ ],
2315
+ }, null, 2));
2316
+ await writeFile(join(stateDir, 'sessions', sessionId, 'autopilot-state.json'), JSON.stringify({
2317
+ active: true,
2318
+ mode: 'autopilot',
2319
+ current_phase: 'waiting-for-user',
2320
+ started_at: '2026-04-19T00:00:00.000Z',
2321
+ updated_at: '2026-04-19T00:10:00.000Z',
2322
+ session_id: sessionId,
2323
+ iteration: 4,
2324
+ max_iterations: 10,
2325
+ review_cycle: 2,
2326
+ run_outcome: 'blocked_on_user',
2327
+ lifecycle_outcome: 'askuserQuestion',
2328
+ state: {
2329
+ deep_interview_question: {
2330
+ status: 'waiting_for_user',
2331
+ obligation_id: 'obligation-question-wait',
2332
+ previous_phase: 'deep-interview',
2333
+ },
2334
+ },
2335
+ }, null, 2));
2336
+ const result = await recordSkillActivation({
2337
+ stateDir,
2338
+ text: '\\ keep going now',
2339
+ sessionId,
2340
+ nowIso: '2026-04-19T00:15:00.000Z',
2341
+ });
2342
+ assert.ok(result);
2343
+ assert.equal(result.skill, 'autopilot');
2344
+ const modeState = JSON.parse(await readFile(join(stateDir, 'sessions', sessionId, 'autopilot-state.json'), 'utf-8'));
2345
+ assert.equal(modeState.current_phase, 'waiting-for-user');
2346
+ assert.equal(modeState.iteration, 4);
2347
+ assert.equal(modeState.max_iterations, 10);
2348
+ assert.equal(modeState.review_cycle, 2);
2349
+ assert.equal(modeState.lifecycle_outcome, 'askuserQuestion');
2350
+ assert.equal(modeState.state?.deep_interview_question?.status, 'waiting_for_user');
2351
+ assert.equal(modeState.state?.deep_interview_question?.obligation_id, 'obligation-question-wait');
2352
+ }
2353
+ finally {
2354
+ await rm(cwd, { recursive: true, force: true });
2355
+ }
2356
+ });
2357
+ it('resets terminal Ralph blocked_on_user state when reactivated', async () => {
2358
+ const cases = [
2359
+ { name: 'phase', phase: 'blocked_on_user', run_outcome: undefined },
2360
+ { name: 'outcome', phase: 'executing', run_outcome: 'blocked_on_user' },
2361
+ ];
2362
+ for (const testCase of cases) {
2363
+ const cwd = await mkdtemp(join(tmpdir(), `omx-keyword-state-ralph-terminal-${testCase.name}-reactivation-`));
2364
+ const stateDir = join(cwd, '.omx', 'state');
2365
+ const sessionId = `sess-ralph-terminal-${testCase.name}`;
2366
+ try {
2367
+ await mkdir(join(stateDir, 'sessions', sessionId), { recursive: true });
2368
+ await writeFile(join(stateDir, 'sessions', sessionId, SKILL_ACTIVE_STATE_FILE), JSON.stringify({
2369
+ version: 1,
2370
+ active: true,
2371
+ skill: 'ralph',
2372
+ keyword: '$ralph',
2373
+ phase: testCase.phase,
2374
+ activated_at: '2026-04-19T00:00:00.000Z',
2375
+ updated_at: '2026-04-19T00:10:00.000Z',
2376
+ source: 'keyword-detector',
2377
+ session_id: sessionId,
2378
+ active_skills: [
2379
+ {
2380
+ skill: 'ralph',
2381
+ phase: testCase.phase,
2382
+ active: true,
2383
+ activated_at: '2026-04-19T00:00:00.000Z',
2384
+ updated_at: '2026-04-19T00:10:00.000Z',
2385
+ session_id: sessionId,
2386
+ },
2387
+ ],
2388
+ }, null, 2));
2389
+ await writeFile(join(stateDir, 'sessions', sessionId, 'ralph-state.json'), JSON.stringify({
2390
+ active: false,
2391
+ mode: 'ralph',
2392
+ current_phase: testCase.phase,
2393
+ started_at: '2026-04-19T00:00:00.000Z',
2394
+ completed_at: '2026-04-19T00:10:00.000Z',
2395
+ iteration: 50,
2396
+ max_iterations: 50,
2397
+ ...(testCase.run_outcome ? { run_outcome: testCase.run_outcome } : {}),
2398
+ }, null, 2));
2399
+ const result = await recordSkillActivation({
2400
+ stateDir,
2401
+ text: '\\ keep going now',
2402
+ sessionId,
2403
+ nowIso: '2026-04-19T00:15:00.000Z',
2404
+ });
2405
+ assert.ok(result);
2406
+ assert.equal(result.skill, 'ralph');
2407
+ assert.equal(result.phase, 'planning');
2408
+ assert.equal(result.activated_at, '2026-04-19T00:15:00.000Z');
2409
+ assert.equal(result.active_skills?.[0]?.phase, 'planning');
2410
+ assert.equal(result.active_skills?.[0]?.activated_at, '2026-04-19T00:15:00.000Z');
2411
+ const modeState = JSON.parse(await readFile(join(stateDir, 'sessions', sessionId, 'ralph-state.json'), 'utf-8'));
2412
+ assert.equal(modeState.active, true);
2413
+ assert.equal(modeState.current_phase, 'starting');
2414
+ assert.equal(modeState.started_at, '2026-04-19T00:15:00.000Z');
2415
+ assert.equal(modeState.completed_at, undefined);
2416
+ assert.equal(modeState.iteration, 0);
2417
+ assert.equal(modeState.max_iterations, 50);
2418
+ }
2419
+ finally {
2420
+ await rm(cwd, { recursive: true, force: true });
2421
+ }
2422
+ }
2423
+ });
1823
2424
  it('routes bare keep-going continuation to the active ralph skill instead of resetting through generic keep-going detection', async () => {
1824
2425
  const cwd = await mkdtemp(join(tmpdir(), 'omx-keyword-state-ralph-bare-continuation-'));
1825
2426
  const stateDir = join(cwd, '.omx', 'state');
@@ -2113,6 +2714,43 @@ describe('applyRalplanGate', () => {
2113
2714
  await rm(cwd, { recursive: true, force: true });
2114
2715
  }
2115
2716
  });
2717
+ it('keeps native-proof execution follow-ups gated when consensus is artifact-only', async () => {
2718
+ const cwd = await mkdtemp(join(tmpdir(), 'omx-keyword-gate-native-required-'));
2719
+ try {
2720
+ const plansDir = join(cwd, '.omx', 'plans');
2721
+ const stateDir = join(cwd, '.omx', 'state');
2722
+ await mkdir(plansDir, { recursive: true });
2723
+ await mkdir(stateDir, { recursive: true });
2724
+ await writeFile(join(plansDir, 'prd-issue-833.md'), '# Approved plan\n\nLaunch hint: omx team 3:executor "Execute approved issue 833 plan"\n');
2725
+ await writeFile(join(plansDir, 'test-spec-issue-833.md'), '# Test spec\n');
2726
+ await writeFile(join(stateDir, 'ralplan-state.json'), JSON.stringify({
2727
+ current_phase: 'complete',
2728
+ planning_complete: true,
2729
+ ralplan_consensus_gate: {
2730
+ complete: true,
2731
+ sequence: ['architect-review', 'critic-review'],
2732
+ ralplan_architect_review: {
2733
+ agent_role: 'architect',
2734
+ verdict: 'approve',
2735
+ iteration: 1,
2736
+ provenance_kind: 'codex_exec',
2737
+ },
2738
+ ralplan_critic_review: {
2739
+ agent_role: 'critic',
2740
+ verdict: 'approve',
2741
+ iteration: 1,
2742
+ provenance_kind: 'codex_exec',
2743
+ },
2744
+ },
2745
+ }));
2746
+ const result = applyRalplanGate(['team'], 'team', { cwd, requireNativeSubagents: true });
2747
+ assert.equal(result.gateApplied, true);
2748
+ assert.deepEqual(result.keywords, ['ralplan']);
2749
+ }
2750
+ finally {
2751
+ await rm(cwd, { recursive: true, force: true });
2752
+ }
2753
+ });
2116
2754
  it('does not re-enter ralplan for a short approved ralph follow-up with durable consensus', async () => {
2117
2755
  const cwd = await mkdtemp(join(tmpdir(), 'omx-keyword-gate-followup-ralph-'));
2118
2756
  try {