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
@@ -97,12 +97,13 @@ describe('RALPLAN Stage', () => {
97
97
  const stage = createRalplanStage();
98
98
  assert.equal(stage.name, 'ralplan');
99
99
  });
100
- it('runs successfully and produces artifacts', async () => {
100
+ it('fails closed without planning artifacts and consensus evidence', async () => {
101
101
  const stage = createRalplanStage();
102
102
  const result = await stage.run(makeCtx());
103
- assert.equal(result.status, 'completed');
103
+ assert.equal(result.status, 'failed');
104
104
  assert.equal(result.artifacts.stage, 'ralplan');
105
105
  assert.ok(result.artifacts.instruction);
106
+ assert.equal(result.error, 'ralplan_planning_artifacts_missing');
106
107
  });
107
108
  it('canSkip returns false when no plans directory exists', () => {
108
109
  const stage = createRalplanStage();
@@ -121,13 +122,329 @@ describe('RALPLAN Stage', () => {
121
122
  const stage = createRalplanStage();
122
123
  assert.equal(stage.canSkip(makeCtx()), false);
123
124
  });
124
- it('canSkip returns true when both prd and test spec plan files exist', async () => {
125
+ it('canSkip returns false when only prd and test spec plan files exist without consensus evidence', async () => {
126
+ const plansDir = join(tempDir, '.omx', 'plans');
127
+ await mkdir(plansDir, { recursive: true });
128
+ await writeFile(join(plansDir, 'prd-my-feature.md'), '# Plan\n');
129
+ await writeFile(join(plansDir, 'test-spec-my-feature.md'), '# Test Spec\n');
130
+ const stage = createRalplanStage();
131
+ assert.equal(stage.canSkip(makeCtx()), false);
132
+ });
133
+ it('run fails with consensus-specific artifact error when consensus exists but planning artifacts are missing', async () => {
134
+ const stage = createRalplanStage();
135
+ const result = await stage.run(makeCtx({
136
+ artifacts: {
137
+ ralplan: {
138
+ ralplanConsensusGate: {
139
+ complete: true,
140
+ sequence: ['architect-review', 'critic-review'],
141
+ ralplan_architect_review: { agent_role: 'architect', verdict: 'approve' },
142
+ ralplan_critic_review: { agent_role: 'critic', verdict: 'approve' },
143
+ },
144
+ },
145
+ },
146
+ }));
147
+ assert.equal(result.status, 'failed');
148
+ assert.equal(result.error, 'ralplan_planning_artifacts_missing_after_consensus');
149
+ assert.equal(result.artifacts.planningComplete, false);
150
+ });
151
+ it('canSkip returns true only when planning artifacts have sequential Architect and Critic approval evidence', async () => {
152
+ const plansDir = join(tempDir, '.omx', 'plans');
153
+ await mkdir(plansDir, { recursive: true });
154
+ await writeFile(join(plansDir, 'prd-my-feature.md'), '# Plan\n');
155
+ await writeFile(join(plansDir, 'test-spec-my-feature.md'), '# Test Spec\n');
156
+ const stage = createRalplanStage();
157
+ assert.equal(stage.canSkip(makeCtx({
158
+ artifacts: {
159
+ ralplan: {
160
+ ralplanConsensusGate: {
161
+ complete: true,
162
+ ralplan_architect_review: { agent_role: 'architect', verdict: 'approve', summary: 'architect approved' },
163
+ ralplan_critic_review: { agent_role: 'critic', verdict: 'approve', summary: 'critic approved after architect' },
164
+ },
165
+ },
166
+ },
167
+ })), true);
168
+ });
169
+ it('canSkip honors explicit session-scoped consensus state before root state', async () => {
170
+ const plansDir = join(tempDir, '.omx', 'plans');
171
+ const stateDir = join(tempDir, '.omx', 'state');
172
+ const sessionDir = join(stateDir, 'sessions', 'sess-explicit');
173
+ await mkdir(plansDir, { recursive: true });
174
+ await mkdir(sessionDir, { recursive: true });
175
+ await writeFile(join(plansDir, 'prd-my-feature.md'), '# Plan\n');
176
+ await writeFile(join(plansDir, 'test-spec-my-feature.md'), '# Test Spec\n');
177
+ await writeFile(join(stateDir, 'autopilot-state.json'), JSON.stringify({
178
+ state: {
179
+ handoff_artifacts: {
180
+ ralplan_architect_review: { agent_role: 'architect', verdict: 'reject', approved: true },
181
+ ralplan_critic_review: { agent_role: 'critic', verdict: 'approve' },
182
+ },
183
+ },
184
+ }));
185
+ await writeFile(join(sessionDir, 'autopilot-state.json'), JSON.stringify({
186
+ state: {
187
+ handoff_artifacts: {
188
+ ralplan_architect_review: { agent_role: 'architect', verdict: 'approve' },
189
+ ralplan_critic_review: { agent_role: 'critic', verdict: 'approve' },
190
+ },
191
+ },
192
+ }));
193
+ const stage = createRalplanStage();
194
+ assert.equal(stage.canSkip(makeCtx({ sessionId: 'sess-explicit' })), true);
195
+ });
196
+ it('canSkip fails closed when explicit session state is missing despite root consensus', async () => {
197
+ const plansDir = join(tempDir, '.omx', 'plans');
198
+ const stateDir = join(tempDir, '.omx', 'state');
199
+ await mkdir(plansDir, { recursive: true });
200
+ await mkdir(stateDir, { recursive: true });
201
+ await writeFile(join(plansDir, 'prd-my-feature.md'), '# Plan\n');
202
+ await writeFile(join(plansDir, 'test-spec-my-feature.md'), '# Test Spec\n');
203
+ await writeFile(join(stateDir, 'autopilot-state.json'), JSON.stringify({
204
+ state: {
205
+ handoff_artifacts: {
206
+ ralplan_architect_review: { agent_role: 'architect', verdict: 'approve' },
207
+ ralplan_critic_review: { agent_role: 'critic', verdict: 'approve' },
208
+ },
209
+ },
210
+ }));
211
+ const stage = createRalplanStage();
212
+ assert.equal(stage.canSkip(makeCtx({ sessionId: 'sess-missing' })), false);
213
+ });
214
+ it('canSkip fails closed for malformed explicit session ids instead of falling back to root consensus', async () => {
215
+ const plansDir = join(tempDir, '.omx', 'plans');
216
+ const stateDir = join(tempDir, '.omx', 'state');
217
+ await mkdir(plansDir, { recursive: true });
218
+ await mkdir(stateDir, { recursive: true });
219
+ await writeFile(join(plansDir, 'prd-my-feature.md'), '# Plan\n');
220
+ await writeFile(join(plansDir, 'test-spec-my-feature.md'), '# Test Spec\n');
221
+ await writeFile(join(stateDir, 'ralplan-state.json'), JSON.stringify({
222
+ ralplanConsensusGate: {
223
+ complete: true,
224
+ sequence: ['architect-review', 'critic-review'],
225
+ ralplan_architect_review: { agent_role: 'architect', verdict: 'approve' },
226
+ ralplan_critic_review: { agent_role: 'critic', verdict: 'approve' },
227
+ },
228
+ }));
229
+ const stage = createRalplanStage();
230
+ for (const sessionId of ['../bad', 'a'.repeat(65), '']) {
231
+ assert.equal(stage.canSkip(makeCtx({ sessionId })), false);
232
+ }
233
+ });
234
+ it('canSkip rejects blocker aliases even with approval-shaped booleans', async () => {
235
+ const plansDir = join(tempDir, '.omx', 'plans');
236
+ await mkdir(plansDir, { recursive: true });
237
+ await writeFile(join(plansDir, 'prd-my-feature.md'), '# Plan\n');
238
+ await writeFile(join(plansDir, 'test-spec-my-feature.md'), '# Test Spec\n');
239
+ const stage = createRalplanStage();
240
+ for (const blocker of [
241
+ { blocking: true },
242
+ { request_changes: true },
243
+ { requestChanges: true },
244
+ { status: 'request changes' },
245
+ { recommendation: 'changes-requested' },
246
+ ]) {
247
+ assert.equal(stage.canSkip(makeCtx({
248
+ artifacts: {
249
+ ralplan: {
250
+ ralplanConsensusGate: {
251
+ complete: true,
252
+ ralplan_architect_review: { agent_role: 'architect', verdict: 'approve' },
253
+ ralplan_critic_review: { agent_role: 'critic', approved: true, clean: true, ...blocker },
254
+ },
255
+ },
256
+ },
257
+ })), false);
258
+ }
259
+ });
260
+ it('canSkip returns false when Critic evidence is recorded before Architect evidence', async () => {
261
+ const plansDir = join(tempDir, '.omx', 'plans');
262
+ await mkdir(plansDir, { recursive: true });
263
+ await writeFile(join(plansDir, 'prd-my-feature.md'), '# Plan\n');
264
+ await writeFile(join(plansDir, 'test-spec-my-feature.md'), '# Test Spec\n');
265
+ const stage = createRalplanStage();
266
+ assert.equal(stage.canSkip(makeCtx({
267
+ artifacts: {
268
+ ralplan: {
269
+ ralplanConsensusGate: {
270
+ complete: true,
271
+ sequence: ['critic-review', 'architect-review'],
272
+ ralplan_architect_review: { agent_role: 'architect', verdict: 'approve' },
273
+ ralplan_critic_review: { agent_role: 'critic', verdict: 'approve' },
274
+ },
275
+ },
276
+ },
277
+ })), false);
278
+ });
279
+ it('canSkip returns false when Critic timestamp predates Architect timestamp', async () => {
280
+ const plansDir = join(tempDir, '.omx', 'plans');
281
+ await mkdir(plansDir, { recursive: true });
282
+ await writeFile(join(plansDir, 'prd-my-feature.md'), '# Plan\n');
283
+ await writeFile(join(plansDir, 'test-spec-my-feature.md'), '# Test Spec\n');
284
+ const stage = createRalplanStage();
285
+ assert.equal(stage.canSkip(makeCtx({
286
+ artifacts: {
287
+ ralplan: {
288
+ ralplanConsensusGate: {
289
+ complete: true,
290
+ sequence: ['architect-review', 'critic-review'],
291
+ ralplan_architect_review: {
292
+ agent_role: 'architect',
293
+ verdict: 'approve',
294
+ completed_at: '2026-05-21T10:05:00.000Z',
295
+ },
296
+ ralplan_critic_review: {
297
+ agent_role: 'critic',
298
+ verdict: 'approve',
299
+ completed_at: '2026-05-21T10:00:00.000Z',
300
+ },
301
+ },
302
+ },
303
+ },
304
+ })), false);
305
+ });
306
+ it('canSkip ignores ambient OMX_ROOT consensus state for local PRD/test-spec-only artifacts', async () => {
307
+ const ambientRoot = await mkdtemp(join(tmpdir(), 'omx-ralplan-ambient-'));
308
+ const previousOmxRoot = process.env.OMX_ROOT;
309
+ try {
310
+ const plansDir = join(tempDir, '.omx', 'plans');
311
+ await mkdir(plansDir, { recursive: true });
312
+ await writeFile(join(plansDir, 'prd-local.md'), '# Plan\n');
313
+ await writeFile(join(plansDir, 'test-spec-local.md'), '# Test Spec\n');
314
+ const ambientStateDir = join(ambientRoot, '.omx', 'state');
315
+ await mkdir(ambientStateDir, { recursive: true });
316
+ await writeFile(join(ambientStateDir, 'ralplan-state.json'), JSON.stringify({
317
+ current_phase: 'complete',
318
+ planning_complete: true,
319
+ ralplan_consensus_gate: {
320
+ complete: true,
321
+ ralplan_architect_review: { agent_role: 'architect', verdict: 'approve', iteration: 1 },
322
+ ralplan_critic_review: { agent_role: 'critic', verdict: 'approve', iteration: 1 },
323
+ },
324
+ }));
325
+ process.env.OMX_ROOT = ambientRoot;
326
+ const stage = createRalplanStage();
327
+ assert.equal(stage.canSkip(makeCtx()), false);
328
+ }
329
+ finally {
330
+ if (previousOmxRoot === undefined)
331
+ delete process.env.OMX_ROOT;
332
+ else
333
+ process.env.OMX_ROOT = previousOmxRoot;
334
+ await rm(ambientRoot, { recursive: true, force: true });
335
+ }
336
+ });
337
+ it('canSkip returns false for rejected consensus objects with approval-shaped booleans', async () => {
338
+ const plansDir = join(tempDir, '.omx', 'plans');
339
+ await mkdir(plansDir, { recursive: true });
340
+ await writeFile(join(plansDir, 'prd-my-feature.md'), '# Plan\n');
341
+ await writeFile(join(plansDir, 'test-spec-my-feature.md'), '# Test Spec\n');
342
+ const stage = createRalplanStage();
343
+ assert.equal(stage.canSkip(makeCtx({
344
+ artifacts: {
345
+ ralplan: {
346
+ ralplanConsensusGate: {
347
+ complete: true,
348
+ ralplan_architect_review: {
349
+ agent_role: 'architect',
350
+ verdict: 'reject',
351
+ approved: true,
352
+ clean: true,
353
+ },
354
+ ralplan_critic_review: {
355
+ agent_role: 'critic',
356
+ verdict: 'approve',
357
+ approved: true,
358
+ clean: true,
359
+ },
360
+ },
361
+ },
362
+ },
363
+ })), false);
364
+ });
365
+ it('canSkip returns false when consensus-shaped reviews do not record agent roles', async () => {
366
+ const plansDir = join(tempDir, '.omx', 'plans');
367
+ await mkdir(plansDir, { recursive: true });
368
+ await writeFile(join(plansDir, 'prd-my-feature.md'), '# Plan\n');
369
+ await writeFile(join(plansDir, 'test-spec-my-feature.md'), '# Test Spec\n');
370
+ const stage = createRalplanStage();
371
+ assert.equal(stage.canSkip(makeCtx({
372
+ artifacts: {
373
+ ralplan: {
374
+ ralplanConsensusGate: {
375
+ complete: true,
376
+ ralplan_architect_review: { verdict: 'approve', summary: 'role missing' },
377
+ ralplan_critic_review: { verdict: 'approve', summary: 'role missing' },
378
+ },
379
+ },
380
+ },
381
+ })), false);
382
+ });
383
+ it('canSkip returns false when review history entries do not record agent roles', async () => {
384
+ const plansDir = join(tempDir, '.omx', 'plans');
385
+ await mkdir(plansDir, { recursive: true });
386
+ await writeFile(join(plansDir, 'prd-my-feature.md'), '# Plan\n');
387
+ await writeFile(join(plansDir, 'test-spec-my-feature.md'), '# Test Spec\n');
388
+ const stage = createRalplanStage();
389
+ assert.equal(stage.canSkip(makeCtx({
390
+ artifacts: {
391
+ ralplan: {
392
+ review_history: [{
393
+ architect_review: { verdict: 'approve', summary: 'role missing' },
394
+ critic_review: { verdict: 'approve', summary: 'role missing' },
395
+ }],
396
+ },
397
+ },
398
+ })), false);
399
+ });
400
+ it('canSkip returns false when review arrays do not record agent roles', async () => {
401
+ const plansDir = join(tempDir, '.omx', 'plans');
402
+ await mkdir(plansDir, { recursive: true });
403
+ await writeFile(join(plansDir, 'prd-my-feature.md'), '# Plan\n');
404
+ await writeFile(join(plansDir, 'test-spec-my-feature.md'), '# Test Spec\n');
405
+ const stage = createRalplanStage();
406
+ assert.equal(stage.canSkip(makeCtx({
407
+ artifacts: {
408
+ ralplan: {
409
+ architectReviews: [{ verdict: 'approve', summary: 'role missing' }],
410
+ criticReviews: [{ verdict: 'approve', summary: 'role missing' }],
411
+ },
412
+ },
413
+ })), false);
414
+ });
415
+ it('canSkip returns false when local state only has latest verdict fields', async () => {
416
+ const plansDir = join(tempDir, '.omx', 'plans');
417
+ const stateDir = join(tempDir, '.omx', 'state');
418
+ await mkdir(plansDir, { recursive: true });
419
+ await mkdir(stateDir, { recursive: true });
420
+ await writeFile(join(plansDir, 'prd-my-feature.md'), '# Plan\n');
421
+ await writeFile(join(plansDir, 'test-spec-my-feature.md'), '# Test Spec\n');
422
+ await writeFile(join(stateDir, 'ralplan-state.json'), JSON.stringify({
423
+ current_phase: 'complete',
424
+ planning_complete: true,
425
+ latest_architect_verdict: 'approve',
426
+ latest_critic_verdict: 'approve',
427
+ }));
428
+ const stage = createRalplanStage();
429
+ assert.equal(stage.canSkip(makeCtx()), false);
430
+ });
431
+ it('canSkip returns false when Architect and Critic roles are swapped', async () => {
125
432
  const plansDir = join(tempDir, '.omx', 'plans');
126
433
  await mkdir(plansDir, { recursive: true });
127
434
  await writeFile(join(plansDir, 'prd-my-feature.md'), '# Plan\n');
128
435
  await writeFile(join(plansDir, 'test-spec-my-feature.md'), '# Test Spec\n');
129
436
  const stage = createRalplanStage();
130
- assert.equal(stage.canSkip(makeCtx()), true);
437
+ assert.equal(stage.canSkip(makeCtx({
438
+ artifacts: {
439
+ ralplan: {
440
+ ralplanConsensusGate: {
441
+ complete: true,
442
+ ralplan_architect_review: { agent_role: 'critic', verdict: 'approve' },
443
+ ralplan_critic_review: { agent_role: 'architect', verdict: 'approve' },
444
+ },
445
+ },
446
+ },
447
+ })), false);
131
448
  });
132
449
  it('canSkip returns false after non-clean code-review loopback even when plans exist', async () => {
133
450
  const plansDir = join(tempDir, '.omx', 'plans');
@@ -189,11 +506,100 @@ describe('RALPLAN Stage', () => {
189
506
  const result = await stage.run(makeCtx({ task: 'live ralplan run' }));
190
507
  const artifacts = result.artifacts;
191
508
  assert.equal(result.status, 'completed');
509
+ assert.equal(result.error, undefined);
192
510
  assert.equal(artifacts.runtime, true);
193
511
  assert.equal(artifacts.planningComplete, true);
512
+ assert.deepEqual(artifacts.ralplanConsensusGate, {
513
+ complete: true,
514
+ sequence: ['architect-review', 'critic-review'],
515
+ ralplan_architect_review: { agent_role: 'architect', iteration: 1, verdict: 'approve', summary: 'architect ok' },
516
+ ralplan_critic_review: { agent_role: 'critic', iteration: 1, verdict: 'approve', summary: 'critic ok' },
517
+ source: 'runtime-result',
518
+ blockedReason: null,
519
+ });
194
520
  assert.equal(artifacts.iteration, 1);
195
521
  assert.equal(artifacts.runtimeDrafted, true);
196
522
  });
523
+ it('fails runtime handoff when consensus approves but test spec does not match selected PRD', async () => {
524
+ const stage = createRalplanStage({
525
+ executor: {
526
+ async draft() {
527
+ const plansDir = join(tempDir, '.omx', 'plans');
528
+ await mkdir(plansDir, { recursive: true });
529
+ const prdPath = join(plansDir, 'prd-new.md');
530
+ await writeFile(prdPath, '# New runtime plan\n');
531
+ await writeFile(join(plansDir, 'test-spec-old.md'), '# Stale runtime tests\n');
532
+ return { summary: 'drafted mismatched artifacts', planPath: prdPath };
533
+ },
534
+ async architectReview() {
535
+ return { verdict: 'approve', summary: 'architect ok' };
536
+ },
537
+ async criticReview() {
538
+ return { verdict: 'approve', summary: 'critic ok' };
539
+ },
540
+ },
541
+ });
542
+ const result = await stage.run(makeCtx({ task: 'live ralplan mismatched artifacts' }));
543
+ const artifacts = result.artifacts;
544
+ assert.equal(result.status, 'failed');
545
+ assert.equal(result.error, 'ralplan_planning_artifacts_missing_after_consensus');
546
+ assert.equal(artifacts.planningComplete, false);
547
+ assert.equal(artifacts.ralplanConsensusGate.complete, true);
548
+ });
549
+ it('fails runtime handoff when consensus approves but required planning artifacts are missing', async () => {
550
+ const stage = createRalplanStage({
551
+ executor: {
552
+ async draft() {
553
+ return { summary: 'draft without files' };
554
+ },
555
+ async architectReview() {
556
+ return { verdict: 'approve', summary: 'architect ok' };
557
+ },
558
+ async criticReview() {
559
+ return { verdict: 'approve', summary: 'critic ok' };
560
+ },
561
+ },
562
+ });
563
+ const result = await stage.run(makeCtx({ task: 'live ralplan no artifacts' }));
564
+ const artifacts = result.artifacts;
565
+ assert.equal(result.status, 'failed');
566
+ assert.equal(result.error, 'ralplan_planning_artifacts_missing_after_consensus');
567
+ assert.equal(artifacts.planningComplete, false);
568
+ assert.equal(artifacts.ralplanConsensusGate.complete, true);
569
+ });
570
+ it('fails runtime handoff when Critic has not approved after Architect', async () => {
571
+ const stage = createRalplanStage({
572
+ executor: {
573
+ async draft() {
574
+ const plansDir = join(tempDir, '.omx', 'plans');
575
+ await mkdir(plansDir, { recursive: true });
576
+ const prdPath = join(plansDir, 'prd-runtime.md');
577
+ await writeFile(prdPath, '# Runtime Plan\n');
578
+ await writeFile(join(plansDir, 'test-spec-runtime.md'), '# Runtime Tests\n');
579
+ return { summary: 'drafted', planPath: prdPath };
580
+ },
581
+ async architectReview() {
582
+ return { verdict: 'approve', summary: 'architect ok' };
583
+ },
584
+ async criticReview() {
585
+ return { verdict: 'iterate', summary: 'critic needs changes' };
586
+ },
587
+ },
588
+ maxIterations: 1,
589
+ });
590
+ const result = await stage.run(makeCtx({ task: 'live ralplan run' }));
591
+ const artifacts = result.artifacts;
592
+ assert.equal(result.status, 'failed');
593
+ assert.equal(result.error, 'ralplan_consensus_not_reached_after_1_iterations');
594
+ assert.deepEqual(artifacts.ralplanConsensusGate, {
595
+ complete: false,
596
+ sequence: ['architect-review', 'critic-review'],
597
+ ralplan_architect_review: null,
598
+ ralplan_critic_review: null,
599
+ source: null,
600
+ blockedReason: 'missing_sequential_architect_then_critic_approval',
601
+ });
602
+ });
197
603
  it('canSkip returns false for non-prd plan files', async () => {
198
604
  const plansDir = join(tempDir, '.omx', 'plans');
199
605
  await mkdir(plansDir, { recursive: true });