oh-my-codex 0.18.1 → 0.18.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (204) hide show
  1. package/Cargo.lock +6 -6
  2. package/Cargo.toml +1 -1
  3. package/README.md +4 -2
  4. package/dist/agents/__tests__/definitions.test.js +14 -0
  5. package/dist/agents/__tests__/definitions.test.js.map +1 -1
  6. package/dist/agents/__tests__/native-config.test.js +19 -0
  7. package/dist/agents/__tests__/native-config.test.js.map +1 -1
  8. package/dist/agents/definitions.d.ts.map +1 -1
  9. package/dist/agents/definitions.js +30 -0
  10. package/dist/agents/definitions.js.map +1 -1
  11. package/dist/agents/native-config.d.ts +1 -0
  12. package/dist/agents/native-config.d.ts.map +1 -1
  13. package/dist/agents/native-config.js +4 -0
  14. package/dist/agents/native-config.js.map +1 -1
  15. package/dist/catalog/__tests__/generator.test.js +4 -0
  16. package/dist/catalog/__tests__/generator.test.js.map +1 -1
  17. package/dist/cli/__tests__/doctor-warning-copy.test.js +61 -5
  18. package/dist/cli/__tests__/doctor-warning-copy.test.js.map +1 -1
  19. package/dist/cli/__tests__/index.test.js +161 -21
  20. package/dist/cli/__tests__/index.test.js.map +1 -1
  21. package/dist/cli/__tests__/launch-fallback.test.js +51 -3
  22. package/dist/cli/__tests__/launch-fallback.test.js.map +1 -1
  23. package/dist/cli/__tests__/question.test.js +2 -2
  24. package/dist/cli/__tests__/question.test.js.map +1 -1
  25. package/dist/cli/doctor.d.ts.map +1 -1
  26. package/dist/cli/doctor.js +178 -7
  27. package/dist/cli/doctor.js.map +1 -1
  28. package/dist/cli/index.d.ts +7 -1
  29. package/dist/cli/index.d.ts.map +1 -1
  30. package/dist/cli/index.js +143 -43
  31. package/dist/cli/index.js.map +1 -1
  32. package/dist/config/__tests__/codex-hooks.test.js +3 -3
  33. package/dist/config/__tests__/codex-hooks.test.js.map +1 -1
  34. package/dist/config/codex-hooks.d.ts +1 -0
  35. package/dist/config/codex-hooks.d.ts.map +1 -1
  36. package/dist/config/codex-hooks.js +2 -4
  37. package/dist/config/codex-hooks.js.map +1 -1
  38. package/dist/config/generator.d.ts +14 -0
  39. package/dist/config/generator.d.ts.map +1 -1
  40. package/dist/config/generator.js +100 -1
  41. package/dist/config/generator.js.map +1 -1
  42. package/dist/goal-workflows/__tests__/codex-goal-snapshot.test.js +21 -0
  43. package/dist/goal-workflows/__tests__/codex-goal-snapshot.test.js.map +1 -1
  44. package/dist/goal-workflows/codex-goal-snapshot.d.ts +3 -0
  45. package/dist/goal-workflows/codex-goal-snapshot.d.ts.map +1 -1
  46. package/dist/goal-workflows/codex-goal-snapshot.js +45 -2
  47. package/dist/goal-workflows/codex-goal-snapshot.js.map +1 -1
  48. package/dist/hooks/__tests__/autopilot-skill-contract.test.js +17 -0
  49. package/dist/hooks/__tests__/autopilot-skill-contract.test.js.map +1 -1
  50. package/dist/hooks/__tests__/keyword-detector.test.js +170 -15
  51. package/dist/hooks/__tests__/keyword-detector.test.js.map +1 -1
  52. package/dist/hooks/__tests__/prometheus-strict-contract.test.d.ts +2 -0
  53. package/dist/hooks/__tests__/prometheus-strict-contract.test.d.ts.map +1 -0
  54. package/dist/hooks/__tests__/prometheus-strict-contract.test.js +320 -0
  55. package/dist/hooks/__tests__/prometheus-strict-contract.test.js.map +1 -0
  56. package/dist/hooks/__tests__/prompt-guidance-wave-two.test.js +12 -0
  57. package/dist/hooks/__tests__/prompt-guidance-wave-two.test.js.map +1 -1
  58. package/dist/hooks/__tests__/research-workflow-boundaries.test.d.ts +2 -0
  59. package/dist/hooks/__tests__/research-workflow-boundaries.test.d.ts.map +1 -0
  60. package/dist/hooks/__tests__/research-workflow-boundaries.test.js +35 -0
  61. package/dist/hooks/__tests__/research-workflow-boundaries.test.js.map +1 -0
  62. package/dist/hooks/keyword-detector.d.ts +1 -1
  63. package/dist/hooks/keyword-detector.d.ts.map +1 -1
  64. package/dist/hooks/keyword-detector.js +28 -6
  65. package/dist/hooks/keyword-detector.js.map +1 -1
  66. package/dist/hooks/keyword-registry.d.ts.map +1 -1
  67. package/dist/hooks/keyword-registry.js +1 -0
  68. package/dist/hooks/keyword-registry.js.map +1 -1
  69. package/dist/hooks/prompt-guidance-contract.d.ts.map +1 -1
  70. package/dist/hooks/prompt-guidance-contract.js +11 -0
  71. package/dist/hooks/prompt-guidance-contract.js.map +1 -1
  72. package/dist/hud/__tests__/hud-tmux-injection.test.js +22 -0
  73. package/dist/hud/__tests__/hud-tmux-injection.test.js.map +1 -1
  74. package/dist/hud/__tests__/reconcile.test.js +121 -10
  75. package/dist/hud/__tests__/reconcile.test.js.map +1 -1
  76. package/dist/hud/__tests__/render.test.js +84 -0
  77. package/dist/hud/__tests__/render.test.js.map +1 -1
  78. package/dist/hud/__tests__/state.test.js +51 -1
  79. package/dist/hud/__tests__/state.test.js.map +1 -1
  80. package/dist/hud/__tests__/tmux.test.js +69 -23
  81. package/dist/hud/__tests__/tmux.test.js.map +1 -1
  82. package/dist/hud/index.d.ts +1 -1
  83. package/dist/hud/index.d.ts.map +1 -1
  84. package/dist/hud/index.js +8 -3
  85. package/dist/hud/index.js.map +1 -1
  86. package/dist/hud/reconcile.d.ts.map +1 -1
  87. package/dist/hud/reconcile.js +6 -3
  88. package/dist/hud/reconcile.js.map +1 -1
  89. package/dist/hud/render.d.ts.map +1 -1
  90. package/dist/hud/render.js +26 -0
  91. package/dist/hud/render.js.map +1 -1
  92. package/dist/hud/state.d.ts +2 -1
  93. package/dist/hud/state.d.ts.map +1 -1
  94. package/dist/hud/state.js +62 -1
  95. package/dist/hud/state.js.map +1 -1
  96. package/dist/hud/tmux.d.ts +10 -3
  97. package/dist/hud/tmux.d.ts.map +1 -1
  98. package/dist/hud/tmux.js +59 -10
  99. package/dist/hud/tmux.js.map +1 -1
  100. package/dist/hud/types.d.ts +22 -0
  101. package/dist/hud/types.d.ts.map +1 -1
  102. package/dist/hud/types.js.map +1 -1
  103. package/dist/pipeline/__tests__/orchestrator.test.js +63 -1
  104. package/dist/pipeline/__tests__/orchestrator.test.js.map +1 -1
  105. package/dist/pipeline/__tests__/stages.test.js +410 -4
  106. package/dist/pipeline/__tests__/stages.test.js.map +1 -1
  107. package/dist/pipeline/orchestrator.d.ts.map +1 -1
  108. package/dist/pipeline/orchestrator.js +29 -2
  109. package/dist/pipeline/orchestrator.js.map +1 -1
  110. package/dist/pipeline/stages/ralplan.d.ts.map +1 -1
  111. package/dist/pipeline/stages/ralplan.js +41 -6
  112. package/dist/pipeline/stages/ralplan.js.map +1 -1
  113. package/dist/question/__tests__/ui.test.js +43 -10
  114. package/dist/question/__tests__/ui.test.js.map +1 -1
  115. package/dist/question/ui.d.ts +12 -0
  116. package/dist/question/ui.d.ts.map +1 -1
  117. package/dist/question/ui.js +83 -46
  118. package/dist/question/ui.js.map +1 -1
  119. package/dist/ralplan/__tests__/runtime.test.js +200 -10
  120. package/dist/ralplan/__tests__/runtime.test.js.map +1 -1
  121. package/dist/ralplan/consensus-gate.d.ts +23 -0
  122. package/dist/ralplan/consensus-gate.d.ts.map +1 -0
  123. package/dist/ralplan/consensus-gate.js +212 -0
  124. package/dist/ralplan/consensus-gate.js.map +1 -0
  125. package/dist/ralplan/runtime.d.ts +25 -0
  126. package/dist/ralplan/runtime.d.ts.map +1 -1
  127. package/dist/ralplan/runtime.js +144 -8
  128. package/dist/ralplan/runtime.js.map +1 -1
  129. package/dist/scripts/__tests__/codex-native-hook.test.js +626 -7
  130. package/dist/scripts/__tests__/codex-native-hook.test.js.map +1 -1
  131. package/dist/scripts/__tests__/docs-site-contract.test.d.ts +2 -0
  132. package/dist/scripts/__tests__/docs-site-contract.test.d.ts.map +1 -0
  133. package/dist/scripts/__tests__/docs-site-contract.test.js +42 -0
  134. package/dist/scripts/__tests__/docs-site-contract.test.js.map +1 -0
  135. package/dist/scripts/__tests__/notify-dispatcher.test.js +115 -2
  136. package/dist/scripts/__tests__/notify-dispatcher.test.js.map +1 -1
  137. package/dist/scripts/__tests__/run-test-files.test.js +57 -0
  138. package/dist/scripts/__tests__/run-test-files.test.js.map +1 -1
  139. package/dist/scripts/__tests__/verify-native-agents.test.js +2 -2
  140. package/dist/scripts/__tests__/verify-native-agents.test.js.map +1 -1
  141. package/dist/scripts/codex-native-hook.d.ts.map +1 -1
  142. package/dist/scripts/codex-native-hook.js +214 -34
  143. package/dist/scripts/codex-native-hook.js.map +1 -1
  144. package/dist/scripts/notify-dispatcher.js +188 -4
  145. package/dist/scripts/notify-dispatcher.js.map +1 -1
  146. package/dist/scripts/run-test-files.js +13 -0
  147. package/dist/scripts/run-test-files.js.map +1 -1
  148. package/dist/state/__tests__/workflow-transition.test.js +6 -0
  149. package/dist/state/__tests__/workflow-transition.test.js.map +1 -1
  150. package/dist/state/workflow-transition.d.ts +1 -1
  151. package/dist/state/workflow-transition.d.ts.map +1 -1
  152. package/dist/state/workflow-transition.js +7 -0
  153. package/dist/state/workflow-transition.js.map +1 -1
  154. package/dist/subagents/tracker.d.ts.map +1 -1
  155. package/dist/subagents/tracker.js +4 -3
  156. package/dist/subagents/tracker.js.map +1 -1
  157. package/dist/team/__tests__/runtime.test.js +36 -44
  158. package/dist/team/__tests__/runtime.test.js.map +1 -1
  159. package/dist/team/__tests__/tmux-session.test.js +58 -18
  160. package/dist/team/__tests__/tmux-session.test.js.map +1 -1
  161. package/dist/team/runtime.d.ts.map +1 -1
  162. package/dist/team/runtime.js +10 -20
  163. package/dist/team/runtime.js.map +1 -1
  164. package/dist/team/tmux-session.d.ts.map +1 -1
  165. package/dist/team/tmux-session.js +15 -6
  166. package/dist/team/tmux-session.js.map +1 -1
  167. package/dist/ultragoal/__tests__/artifacts.test.js +50 -0
  168. package/dist/ultragoal/__tests__/artifacts.test.js.map +1 -1
  169. package/dist/ultragoal/artifacts.d.ts.map +1 -1
  170. package/dist/ultragoal/artifacts.js +28 -2
  171. package/dist/ultragoal/artifacts.js.map +1 -1
  172. package/package.json +1 -1
  173. package/plugins/oh-my-codex/.codex-plugin/plugin.json +1 -1
  174. package/plugins/oh-my-codex/skills/autopilot/SKILL.md +16 -4
  175. package/plugins/oh-my-codex/skills/autoresearch/SKILL.md +4 -0
  176. package/plugins/oh-my-codex/skills/autoresearch-goal/SKILL.md +1 -1
  177. package/plugins/oh-my-codex/skills/best-practice-research/SKILL.md +1 -1
  178. package/plugins/oh-my-codex/skills/pipeline/SKILL.md +1 -1
  179. package/plugins/oh-my-codex/skills/plan/SKILL.md +1 -1
  180. package/plugins/oh-my-codex/skills/prometheus-strict/README.md +35 -0
  181. package/plugins/oh-my-codex/skills/prometheus-strict/SKILL.md +219 -0
  182. package/plugins/oh-my-codex/skills/ralplan/SKILL.md +18 -3
  183. package/prompts/prometheus-strict-metis.md +274 -0
  184. package/prompts/prometheus-strict-momus.md +82 -0
  185. package/prompts/prometheus-strict-oracle.md +107 -0
  186. package/prompts/researcher.md +22 -3
  187. package/skills/autopilot/SKILL.md +16 -4
  188. package/skills/autoresearch/SKILL.md +4 -0
  189. package/skills/autoresearch-goal/SKILL.md +1 -1
  190. package/skills/best-practice-research/SKILL.md +1 -1
  191. package/skills/pipeline/SKILL.md +1 -1
  192. package/skills/plan/SKILL.md +1 -1
  193. package/skills/prometheus-strict/README.md +35 -0
  194. package/skills/prometheus-strict/SKILL.md +219 -0
  195. package/skills/ralplan/SKILL.md +18 -3
  196. package/src/scripts/__tests__/codex-native-hook.test.ts +769 -8
  197. package/src/scripts/__tests__/docs-site-contract.test.ts +47 -0
  198. package/src/scripts/__tests__/notify-dispatcher.test.ts +132 -3
  199. package/src/scripts/__tests__/run-test-files.test.ts +67 -0
  200. package/src/scripts/__tests__/verify-native-agents.test.ts +2 -2
  201. package/src/scripts/codex-native-hook.ts +237 -30
  202. package/src/scripts/notify-dispatcher.ts +202 -4
  203. package/src/scripts/run-test-files.ts +13 -0
  204. package/templates/catalog-manifest.json +22 -0
@@ -194,6 +194,11 @@ describe('keyword detector team compatibility', () => {
194
194
  assert.equal(detectPrimaryKeyword('cleanup stale deep-interview state after session clear'), null);
195
195
  assert.equal(detectPrimaryKeyword('remove the stale deep interview lock from .omx/state'), null);
196
196
  });
197
+ it('does not trigger deep-interview from casual discussion mentions', () => {
198
+ assert.equal(detectPrimaryKeyword('the deep interview report is useful context for the next plan'), null);
199
+ assert.equal(detectPrimaryKeyword('we already did a deep interview and should not reactivate it'), null);
200
+ assert.equal(detectPrimaryKeyword('this interview transcript says implementation is ready'), null);
201
+ });
197
202
  it('maps "gather requirements" to deep-interview skill', () => {
198
203
  const match = detectPrimaryKeyword('let us gather requirements first');
199
204
  assert.ok(match);
@@ -285,6 +290,13 @@ describe('explicit skill-name invocation requirement', () => {
285
290
  it('does not trigger ralplan from bare skill-name usage', () => {
286
291
  assert.equal(detectPrimaryKeyword('please do ralplan first'), null);
287
292
  });
293
+ it('detects explicit prometheus-strict invocation only', () => {
294
+ const match = detectPrimaryKeyword('please run $prometheus-strict before implementation');
295
+ assert.ok(match);
296
+ assert.equal(match.skill, 'prometheus-strict');
297
+ assert.equal(match.keyword.toLowerCase(), '$prometheus-strict');
298
+ assert.equal(detectPrimaryKeyword('please use prometheus-strict planning here'), null);
299
+ });
288
300
  });
289
301
  describe('keyword registry coverage', () => {
290
302
  it('includes key team aliases in runtime keyword registry', () => {
@@ -304,6 +316,7 @@ describe('keyword registry coverage', () => {
304
316
  assert.ok(registryKeywords.has('wiki lint'));
305
317
  assert.ok(registryKeywords.has('$autoresearch'));
306
318
  assert.ok(registryKeywords.has('$ultragoal'));
319
+ assert.ok(registryKeywords.has('$prometheus-strict'));
307
320
  assert.ok(registryKeywords.has('ultragoal'));
308
321
  });
309
322
  });
@@ -398,7 +411,7 @@ describe('keyword detector skill-active-state lifecycle', () => {
398
411
  await rm(root, { recursive: true, force: true });
399
412
  }
400
413
  });
401
- it('writes skill-active-state.json with ralplan phase when autopilot keyword activates', async () => {
414
+ it('writes skill-active-state.json with deep-interview phase when autopilot keyword activates', async () => {
402
415
  const cwd = await mkdtemp(join(tmpdir(), 'omx-keyword-state-'));
403
416
  const stateDir = join(cwd, '.omx', 'state');
404
417
  try {
@@ -413,11 +426,11 @@ describe('keyword detector skill-active-state lifecycle', () => {
413
426
  });
414
427
  assert.ok(result);
415
428
  assert.equal(result.skill, 'autopilot');
416
- assert.equal(result.phase, 'ralplan');
429
+ assert.equal(result.phase, 'deep-interview');
417
430
  assert.equal(result.active, true);
418
431
  assert.deepEqual(result.active_skills, [{
419
432
  skill: 'autopilot',
420
- phase: 'ralplan',
433
+ phase: 'deep-interview',
421
434
  active: true,
422
435
  activated_at: '2026-02-25T00:00:00.000Z',
423
436
  updated_at: '2026-02-25T00:00:00.000Z',
@@ -439,7 +452,27 @@ describe('keyword detector skill-active-state lifecycle', () => {
439
452
  assert.equal(modeState.review_cycle, 0);
440
453
  assert.equal(modeState.max_iterations, 10);
441
454
  assert.deepEqual(modeState.state.phase_cycle, ['deep-interview', 'ralplan', 'ultragoal', 'code-review', 'ultraqa']);
442
- assert.deepEqual(modeState.state.handoff_artifacts, { deep_interview: null, ralplan: null, ultragoal: null, code_review: null, ultraqa: null });
455
+ assert.deepEqual(modeState.state.deep_interview_gate, {
456
+ status: 'required',
457
+ skip_reason: null,
458
+ rationale: 'Autopilot starts at the deep-interview gate by default; clear bounded tasks may skip only with an explicit persisted skip reason.',
459
+ });
460
+ assert.deepEqual(modeState.state.handoff_artifacts, {
461
+ deep_interview: null,
462
+ ralplan: null,
463
+ ralplan_consensus_gate: {
464
+ required: true,
465
+ sequence: ['architect-review', 'critic-review'],
466
+ planning_artifacts_are_not_consensus: true,
467
+ required_review_roles: ['architect', 'critic'],
468
+ ralplan_architect_review: null,
469
+ ralplan_critic_review: null,
470
+ complete: false,
471
+ },
472
+ ultragoal: null,
473
+ code_review: null,
474
+ ultraqa: null,
475
+ });
443
476
  assert.equal(modeState.state.review_verdict, null);
444
477
  assert.equal(modeState.state.qa_verdict, null);
445
478
  assert.equal(modeState.state.return_to_ralplan_reason, null);
@@ -792,18 +825,56 @@ describe('keyword detector skill-active-state lifecycle', () => {
792
825
  }, null, 2));
793
826
  const result = await recordSkillActivation({
794
827
  stateDir,
795
- text: '$ralplan implement the approved contract',
828
+ text: '$ultragoal turn the clarified spec into goals',
796
829
  sessionId: 'sess-handoff',
797
830
  nowIso: '2026-04-10T00:00:00.000Z',
798
831
  });
799
832
  assert.equal(result?.transition_error, undefined);
800
- assert.equal(result?.transition_message, 'mode transiting: deep-interview -> ralplan');
833
+ assert.equal(result?.skill, 'ultragoal');
834
+ assert.equal(result?.initialized_mode, 'ultragoal');
835
+ assert.equal(result?.initialized_state_path, '.omx/state/sessions/sess-handoff/ultragoal-state.json');
836
+ assert.equal(result?.transition_message, 'mode transiting: deep-interview -> ultragoal');
801
837
  const completed = JSON.parse(await readFile(join(stateDir, 'sessions', 'sess-handoff', 'deep-interview-state.json'), 'utf-8'));
802
838
  assert.equal(completed.active, false);
803
839
  assert.equal(completed.current_phase, 'completed');
804
840
  assert.equal(completed.question_enforcement?.status, 'cleared');
805
841
  assert.equal(completed.question_enforcement?.clear_reason, 'handoff');
806
842
  assert.ok(completed.question_enforcement?.cleared_at);
843
+ const ultragoal = JSON.parse(await readFile(join(stateDir, 'sessions', 'sess-handoff', 'ultragoal-state.json'), 'utf-8'));
844
+ assert.equal(ultragoal.active, true);
845
+ assert.equal(ultragoal.mode, 'ultragoal');
846
+ assert.equal(ultragoal.current_phase, 'planning');
847
+ }
848
+ finally {
849
+ await rm(cwd, { recursive: true, force: true });
850
+ }
851
+ });
852
+ it('keeps ralplan as an allowlisted deep-interview forward handoff', async () => {
853
+ const cwd = await mkdtemp(join(tmpdir(), 'omx-keyword-ralplan-handoff-'));
854
+ const stateDir = join(cwd, '.omx', 'state');
855
+ try {
856
+ await mkdir(join(stateDir, 'sessions', 'sess-ralplan-handoff'), { recursive: true });
857
+ await writeFile(join(stateDir, 'sessions', 'sess-ralplan-handoff', SKILL_ACTIVE_STATE_FILE), JSON.stringify({
858
+ version: 1,
859
+ active: true,
860
+ skill: 'deep-interview',
861
+ phase: 'planning',
862
+ session_id: 'sess-ralplan-handoff',
863
+ active_skills: [{ skill: 'deep-interview', phase: 'planning', active: true, session_id: 'sess-ralplan-handoff' }],
864
+ }, null, 2));
865
+ await writeFile(join(stateDir, 'sessions', 'sess-ralplan-handoff', 'deep-interview-state.json'), JSON.stringify({ active: true, mode: 'deep-interview', current_phase: 'intent-first' }, null, 2));
866
+ const result = await recordSkillActivation({
867
+ stateDir,
868
+ text: '$ralplan implement the approved contract',
869
+ sessionId: 'sess-ralplan-handoff',
870
+ nowIso: '2026-04-10T00:00:00.000Z',
871
+ });
872
+ assert.equal(result?.transition_error, undefined);
873
+ assert.equal(result?.skill, 'ralplan');
874
+ assert.equal(result?.transition_message, 'mode transiting: deep-interview -> ralplan');
875
+ const completed = JSON.parse(await readFile(join(stateDir, 'sessions', 'sess-ralplan-handoff', 'deep-interview-state.json'), 'utf-8'));
876
+ assert.equal(completed.active, false);
877
+ assert.equal(completed.current_phase, 'completed');
807
878
  }
808
879
  finally {
809
880
  await rm(cwd, { recursive: true, force: true });
@@ -1151,7 +1222,7 @@ describe('keyword detector skill-active-state lifecycle', () => {
1151
1222
  await rm(cwd, { recursive: true, force: true });
1152
1223
  }
1153
1224
  });
1154
- it('records ultragoal as a prompt skill without seeding unrelated mode state', async () => {
1225
+ it('records ultragoal as a prompt skill with first-class mode state', async () => {
1155
1226
  const cwd = await mkdtemp(join(tmpdir(), 'omx-keyword-state-ultragoal-'));
1156
1227
  const stateDir = join(cwd, '.omx', 'state');
1157
1228
  try {
@@ -1163,9 +1234,12 @@ describe('keyword detector skill-active-state lifecycle', () => {
1163
1234
  assert.ok(result);
1164
1235
  assert.equal(result.skill, 'ultragoal');
1165
1236
  assert.equal(result.keyword, '$ultragoal');
1166
- assert.equal(result.initialized_mode, undefined);
1167
- assert.equal(result.initialized_state_path, undefined);
1168
- assert.equal(existsSync(join(stateDir, 'ultragoal-state.json')), false);
1237
+ assert.equal(result.initialized_mode, 'ultragoal');
1238
+ assert.equal(result.initialized_state_path, '.omx/state/ultragoal-state.json');
1239
+ const modeState = JSON.parse(await readFile(join(stateDir, 'ultragoal-state.json'), 'utf-8'));
1240
+ assert.equal(modeState.active, true);
1241
+ assert.equal(modeState.mode, 'ultragoal');
1242
+ assert.equal(modeState.current_phase, 'planning');
1169
1243
  }
1170
1244
  finally {
1171
1245
  await rm(cwd, { recursive: true, force: true });
@@ -1696,7 +1770,7 @@ describe('isUnderspecifiedForExecution', () => {
1696
1770
  });
1697
1771
  });
1698
1772
  describe('applyRalplanGate', () => {
1699
- it('does not re-enter ralplan for a short approved team follow-up', async () => {
1773
+ it('gates short team follow-up when only PRD/test-spec artifacts exist', async () => {
1700
1774
  const cwd = await mkdtemp(join(tmpdir(), 'omx-keyword-gate-followup-'));
1701
1775
  try {
1702
1776
  const plansDir = join(cwd, '.omx', 'plans');
@@ -1704,20 +1778,32 @@ describe('applyRalplanGate', () => {
1704
1778
  await writeFile(join(plansDir, 'prd-issue-831.md'), '# Approved plan\n\nLaunch hint: omx team 3:executor "Execute approved issue 831 plan"\n');
1705
1779
  await writeFile(join(plansDir, 'test-spec-issue-831.md'), '# Test spec\n');
1706
1780
  const result = applyRalplanGate(['team'], 'team', { cwd });
1707
- assert.equal(result.gateApplied, false);
1708
- assert.deepEqual(result.keywords, ['team']);
1781
+ assert.equal(result.gateApplied, true);
1782
+ assert.deepEqual(result.keywords, ['ralplan']);
1709
1783
  }
1710
1784
  finally {
1711
1785
  await rm(cwd, { recursive: true, force: true });
1712
1786
  }
1713
1787
  });
1714
- it('does not re-enter ralplan for a short approved Korean team follow-up', async () => {
1788
+ it('does not re-enter ralplan for a short approved team follow-up with durable consensus', async () => {
1715
1789
  const cwd = await mkdtemp(join(tmpdir(), 'omx-keyword-gate-followup-ko-'));
1716
1790
  try {
1717
1791
  const plansDir = join(cwd, '.omx', 'plans');
1792
+ const stateDir = join(cwd, '.omx', 'state');
1718
1793
  await mkdir(plansDir, { recursive: true });
1794
+ await mkdir(stateDir, { recursive: true });
1719
1795
  await writeFile(join(plansDir, 'prd-issue-831.md'), '# Approved plan\n\nLaunch hint: omx team 3:executor "Execute approved issue 831 plan"\n');
1720
1796
  await writeFile(join(plansDir, 'test-spec-issue-831.md'), '# Test spec\n');
1797
+ await writeFile(join(stateDir, 'ralplan-state.json'), JSON.stringify({
1798
+ current_phase: 'complete',
1799
+ planning_complete: true,
1800
+ ralplan_consensus_gate: {
1801
+ complete: true,
1802
+ sequence: ['architect-review', 'critic-review'],
1803
+ ralplan_architect_review: { agent_role: 'architect', verdict: 'approve', iteration: 1 },
1804
+ ralplan_critic_review: { agent_role: 'critic', verdict: 'approve', iteration: 1 },
1805
+ },
1806
+ }));
1721
1807
  const result = applyRalplanGate(['team'], 'team으로 해줘', { cwd });
1722
1808
  assert.equal(result.gateApplied, false);
1723
1809
  assert.deepEqual(result.keywords, ['team']);
@@ -1726,13 +1812,25 @@ describe('applyRalplanGate', () => {
1726
1812
  await rm(cwd, { recursive: true, force: true });
1727
1813
  }
1728
1814
  });
1729
- it('does not re-enter ralplan for a short approved ralph follow-up', async () => {
1815
+ it('does not re-enter ralplan for a short approved ralph follow-up with durable consensus', async () => {
1730
1816
  const cwd = await mkdtemp(join(tmpdir(), 'omx-keyword-gate-followup-ralph-'));
1731
1817
  try {
1732
1818
  const plansDir = join(cwd, '.omx', 'plans');
1819
+ const stateDir = join(cwd, '.omx', 'state');
1733
1820
  await mkdir(plansDir, { recursive: true });
1821
+ await mkdir(stateDir, { recursive: true });
1734
1822
  await writeFile(join(plansDir, 'prd-issue-832.md'), '# Approved plan\n\nLaunch hint: omx ralph "Execute approved issue 832 plan"\n');
1735
1823
  await writeFile(join(plansDir, 'test-spec-issue-832.md'), '# Test spec\n');
1824
+ await writeFile(join(stateDir, 'ralplan-state.json'), JSON.stringify({
1825
+ current_phase: 'complete',
1826
+ planning_complete: true,
1827
+ ralplan_consensus_gate: {
1828
+ complete: true,
1829
+ sequence: ['architect-review', 'critic-review'],
1830
+ ralplan_architect_review: { agent_role: 'architect', verdict: 'approve', iteration: 1 },
1831
+ ralplan_critic_review: { agent_role: 'critic', verdict: 'approve', iteration: 1 },
1832
+ },
1833
+ }));
1736
1834
  const result = applyRalplanGate(['ralph'], 'ralph please', { cwd, priorSkill: 'ralplan' });
1737
1835
  assert.equal(result.gateApplied, false);
1738
1836
  assert.deepEqual(result.keywords, ['ralph']);
@@ -1741,6 +1839,63 @@ describe('applyRalplanGate', () => {
1741
1839
  await rm(cwd, { recursive: true, force: true });
1742
1840
  }
1743
1841
  });
1842
+ it('ignores ambient OMX_ROOT consensus state for local PRD/test-spec-only follow-up gating', async () => {
1843
+ const cwd = await mkdtemp(join(tmpdir(), 'omx-keyword-gate-local-'));
1844
+ const ambientRoot = await mkdtemp(join(tmpdir(), 'omx-keyword-gate-ambient-'));
1845
+ const previousOmxRoot = process.env.OMX_ROOT;
1846
+ try {
1847
+ const plansDir = join(cwd, '.omx', 'plans');
1848
+ await mkdir(plansDir, { recursive: true });
1849
+ await writeFile(join(plansDir, 'prd-local.md'), '# Plan\n');
1850
+ await writeFile(join(plansDir, 'test-spec-local.md'), '# Test spec\n');
1851
+ const ambientStateDir = join(ambientRoot, '.omx', 'state');
1852
+ await mkdir(ambientStateDir, { recursive: true });
1853
+ await writeFile(join(ambientStateDir, 'ralplan-state.json'), JSON.stringify({
1854
+ current_phase: 'complete',
1855
+ planning_complete: true,
1856
+ ralplan_consensus_gate: {
1857
+ complete: true,
1858
+ ralplan_architect_review: { agent_role: 'architect', verdict: 'approve', iteration: 1 },
1859
+ ralplan_critic_review: { agent_role: 'critic', verdict: 'approve', iteration: 1 },
1860
+ },
1861
+ }));
1862
+ process.env.OMX_ROOT = ambientRoot;
1863
+ const result = applyRalplanGate(['team'], 'team', { cwd });
1864
+ assert.equal(result.gateApplied, true);
1865
+ assert.deepEqual(result.keywords, ['ralplan']);
1866
+ }
1867
+ finally {
1868
+ if (previousOmxRoot === undefined)
1869
+ delete process.env.OMX_ROOT;
1870
+ else
1871
+ process.env.OMX_ROOT = previousOmxRoot;
1872
+ await rm(cwd, { recursive: true, force: true });
1873
+ await rm(ambientRoot, { recursive: true, force: true });
1874
+ }
1875
+ });
1876
+ it('gates short follow-up when local state only has latest verdict fields', async () => {
1877
+ const cwd = await mkdtemp(join(tmpdir(), 'omx-keyword-gate-latest-only-'));
1878
+ try {
1879
+ const plansDir = join(cwd, '.omx', 'plans');
1880
+ const stateDir = join(cwd, '.omx', 'state');
1881
+ await mkdir(plansDir, { recursive: true });
1882
+ await mkdir(stateDir, { recursive: true });
1883
+ await writeFile(join(plansDir, 'prd-local.md'), '# Plan\n\nLaunch hint: omx team 3:executor "Execute approved local plan"\n');
1884
+ await writeFile(join(plansDir, 'test-spec-local.md'), '# Test spec\n');
1885
+ await writeFile(join(stateDir, 'ralplan-state.json'), JSON.stringify({
1886
+ current_phase: 'complete',
1887
+ planning_complete: true,
1888
+ latest_architect_verdict: 'approve',
1889
+ latest_critic_verdict: 'approve',
1890
+ }));
1891
+ const result = applyRalplanGate(['team'], 'team', { cwd });
1892
+ assert.equal(result.gateApplied, true);
1893
+ assert.deepEqual(result.keywords, ['ralplan']);
1894
+ }
1895
+ finally {
1896
+ await rm(cwd, { recursive: true, force: true });
1897
+ }
1898
+ });
1744
1899
  it('redirects underspecified execution keywords to ralplan', () => {
1745
1900
  const result = applyRalplanGate(['ralph'], 'ralph fix this');
1746
1901
  assert.equal(result.gateApplied, true);