oh-my-codex 0.16.3 → 0.17.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (339) hide show
  1. package/Cargo.lock +5 -5
  2. package/Cargo.toml +1 -1
  3. package/README.md +3 -3
  4. package/dist/catalog/__tests__/generator.test.js +2 -0
  5. package/dist/catalog/__tests__/generator.test.js.map +1 -1
  6. package/dist/catalog/__tests__/plugin-bundle-ssot.test.js +9 -0
  7. package/dist/catalog/__tests__/plugin-bundle-ssot.test.js.map +1 -1
  8. package/dist/cli/__tests__/cleanup.test.js +27 -0
  9. package/dist/cli/__tests__/cleanup.test.js.map +1 -1
  10. package/dist/cli/__tests__/codex-plugin-layout.test.js +7 -5
  11. package/dist/cli/__tests__/codex-plugin-layout.test.js.map +1 -1
  12. package/dist/cli/__tests__/doctor-warning-copy.test.js +175 -7
  13. package/dist/cli/__tests__/doctor-warning-copy.test.js.map +1 -1
  14. package/dist/cli/__tests__/index.test.js +147 -12
  15. package/dist/cli/__tests__/index.test.js.map +1 -1
  16. package/dist/cli/__tests__/mcp-serve.test.js +4 -0
  17. package/dist/cli/__tests__/mcp-serve.test.js.map +1 -1
  18. package/dist/cli/__tests__/ralph-goal-mode-contract.test.js +2 -0
  19. package/dist/cli/__tests__/ralph-goal-mode-contract.test.js.map +1 -1
  20. package/dist/cli/__tests__/ralph.test.js +47 -0
  21. package/dist/cli/__tests__/ralph.test.js.map +1 -1
  22. package/dist/cli/__tests__/setup-hooks-shared-ownership.test.js +10 -5
  23. package/dist/cli/__tests__/setup-hooks-shared-ownership.test.js.map +1 -1
  24. package/dist/cli/__tests__/setup-install-mode.test.js +299 -27
  25. package/dist/cli/__tests__/setup-install-mode.test.js.map +1 -1
  26. package/dist/cli/__tests__/setup-refresh.test.js +85 -3
  27. package/dist/cli/__tests__/setup-refresh.test.js.map +1 -1
  28. package/dist/cli/__tests__/setup-scope.test.js +1 -1
  29. package/dist/cli/__tests__/setup-scope.test.js.map +1 -1
  30. package/dist/cli/__tests__/setup-skills-overwrite.test.js +2 -1
  31. package/dist/cli/__tests__/setup-skills-overwrite.test.js.map +1 -1
  32. package/dist/cli/__tests__/team.test.js +108 -0
  33. package/dist/cli/__tests__/team.test.js.map +1 -1
  34. package/dist/cli/__tests__/ultragoal.test.js +91 -0
  35. package/dist/cli/__tests__/ultragoal.test.js.map +1 -1
  36. package/dist/cli/__tests__/uninstall.test.js +54 -8
  37. package/dist/cli/__tests__/uninstall.test.js.map +1 -1
  38. package/dist/cli/cleanup.d.ts.map +1 -1
  39. package/dist/cli/cleanup.js +8 -4
  40. package/dist/cli/cleanup.js.map +1 -1
  41. package/dist/cli/codex-feature-probe.d.ts +9 -0
  42. package/dist/cli/codex-feature-probe.d.ts.map +1 -0
  43. package/dist/cli/codex-feature-probe.js +28 -0
  44. package/dist/cli/codex-feature-probe.js.map +1 -0
  45. package/dist/cli/doctor.d.ts +1 -0
  46. package/dist/cli/doctor.d.ts.map +1 -1
  47. package/dist/cli/doctor.js +214 -23
  48. package/dist/cli/doctor.js.map +1 -1
  49. package/dist/cli/index.d.ts +17 -4
  50. package/dist/cli/index.d.ts.map +1 -1
  51. package/dist/cli/index.js +152 -24
  52. package/dist/cli/index.js.map +1 -1
  53. package/dist/cli/mcp-parity.js +8 -8
  54. package/dist/cli/mcp-parity.js.map +1 -1
  55. package/dist/cli/mcp-serve.d.ts.map +1 -1
  56. package/dist/cli/mcp-serve.js +4 -0
  57. package/dist/cli/mcp-serve.js.map +1 -1
  58. package/dist/cli/plugin-marketplace.d.ts +23 -0
  59. package/dist/cli/plugin-marketplace.d.ts.map +1 -1
  60. package/dist/cli/plugin-marketplace.js +203 -1
  61. package/dist/cli/plugin-marketplace.js.map +1 -1
  62. package/dist/cli/ralph.d.ts.map +1 -1
  63. package/dist/cli/ralph.js +21 -0
  64. package/dist/cli/ralph.js.map +1 -1
  65. package/dist/cli/setup-preferences.d.ts +4 -0
  66. package/dist/cli/setup-preferences.d.ts.map +1 -1
  67. package/dist/cli/setup-preferences.js +7 -0
  68. package/dist/cli/setup-preferences.js.map +1 -1
  69. package/dist/cli/setup.d.ts +5 -3
  70. package/dist/cli/setup.d.ts.map +1 -1
  71. package/dist/cli/setup.js +140 -51
  72. package/dist/cli/setup.js.map +1 -1
  73. package/dist/cli/ultragoal.d.ts +1 -1
  74. package/dist/cli/ultragoal.d.ts.map +1 -1
  75. package/dist/cli/ultragoal.js +70 -5
  76. package/dist/cli/ultragoal.js.map +1 -1
  77. package/dist/cli/uninstall.d.ts +2 -0
  78. package/dist/cli/uninstall.d.ts.map +1 -1
  79. package/dist/cli/uninstall.js +12 -3
  80. package/dist/cli/uninstall.js.map +1 -1
  81. package/dist/config/__tests__/codex-feature-flags.test.d.ts +2 -0
  82. package/dist/config/__tests__/codex-feature-flags.test.d.ts.map +1 -0
  83. package/dist/config/__tests__/codex-feature-flags.test.js +35 -0
  84. package/dist/config/__tests__/codex-feature-flags.test.js.map +1 -0
  85. package/dist/config/__tests__/codex-hooks.test.js +143 -9
  86. package/dist/config/__tests__/codex-hooks.test.js.map +1 -1
  87. package/dist/config/__tests__/generator-idempotent.test.js +85 -9
  88. package/dist/config/__tests__/generator-idempotent.test.js.map +1 -1
  89. package/dist/config/__tests__/generator-notify.test.js +116 -11
  90. package/dist/config/__tests__/generator-notify.test.js.map +1 -1
  91. package/dist/config/__tests__/wiki-config-contract.test.js +6 -3
  92. package/dist/config/__tests__/wiki-config-contract.test.js.map +1 -1
  93. package/dist/config/codex-feature-flags.d.ts +21 -0
  94. package/dist/config/codex-feature-flags.d.ts.map +1 -0
  95. package/dist/config/codex-feature-flags.js +56 -0
  96. package/dist/config/codex-feature-flags.js.map +1 -0
  97. package/dist/config/codex-hooks.d.ts +14 -13
  98. package/dist/config/codex-hooks.d.ts.map +1 -1
  99. package/dist/config/codex-hooks.js +108 -8
  100. package/dist/config/codex-hooks.js.map +1 -1
  101. package/dist/config/generator.d.ts +15 -3
  102. package/dist/config/generator.d.ts.map +1 -1
  103. package/dist/config/generator.js +233 -129
  104. package/dist/config/generator.js.map +1 -1
  105. package/dist/config/omx-first-party-mcp.d.ts +3 -1
  106. package/dist/config/omx-first-party-mcp.d.ts.map +1 -1
  107. package/dist/config/omx-first-party-mcp.js +9 -2
  108. package/dist/config/omx-first-party-mcp.js.map +1 -1
  109. package/dist/hooks/__tests__/design-skill.test.d.ts +2 -0
  110. package/dist/hooks/__tests__/design-skill.test.d.ts.map +1 -0
  111. package/dist/hooks/__tests__/design-skill.test.js +55 -0
  112. package/dist/hooks/__tests__/design-skill.test.js.map +1 -0
  113. package/dist/hooks/__tests__/keyword-detector.test.js +92 -2
  114. package/dist/hooks/__tests__/keyword-detector.test.js.map +1 -1
  115. package/dist/hooks/__tests__/notify-hook-non-omx-guard.test.js +125 -1
  116. package/dist/hooks/__tests__/notify-hook-non-omx-guard.test.js.map +1 -1
  117. package/dist/hooks/__tests__/notify-hook-tmux-heal.test.js +265 -0
  118. package/dist/hooks/__tests__/notify-hook-tmux-heal.test.js.map +1 -1
  119. package/dist/hooks/__tests__/skill-catalog-hygiene.test.d.ts +2 -0
  120. package/dist/hooks/__tests__/skill-catalog-hygiene.test.d.ts.map +1 -0
  121. package/dist/hooks/__tests__/skill-catalog-hygiene.test.js +84 -0
  122. package/dist/hooks/__tests__/skill-catalog-hygiene.test.js.map +1 -0
  123. package/dist/hooks/__tests__/skill-guidance-contract.test.js +41 -0
  124. package/dist/hooks/__tests__/skill-guidance-contract.test.js.map +1 -1
  125. package/dist/hooks/agents-overlay.js +2 -2
  126. package/dist/hooks/agents-overlay.js.map +1 -1
  127. package/dist/hooks/keyword-detector.d.ts +1 -0
  128. package/dist/hooks/keyword-detector.d.ts.map +1 -1
  129. package/dist/hooks/keyword-detector.js +12 -6
  130. package/dist/hooks/keyword-detector.js.map +1 -1
  131. package/dist/hooks/keyword-registry.d.ts.map +1 -1
  132. package/dist/hooks/keyword-registry.js +2 -0
  133. package/dist/hooks/keyword-registry.js.map +1 -1
  134. package/dist/hooks/prompt-guidance-contract.d.ts.map +1 -1
  135. package/dist/hooks/prompt-guidance-contract.js +47 -2
  136. package/dist/hooks/prompt-guidance-contract.js.map +1 -1
  137. package/dist/hud/__tests__/state.test.js +164 -0
  138. package/dist/hud/__tests__/state.test.js.map +1 -1
  139. package/dist/hud/state.d.ts.map +1 -1
  140. package/dist/hud/state.js +4 -5
  141. package/dist/hud/state.js.map +1 -1
  142. package/dist/mcp/__tests__/bootstrap.test.js +3 -0
  143. package/dist/mcp/__tests__/bootstrap.test.js.map +1 -1
  144. package/dist/mcp/__tests__/hermes-bridge.test.d.ts +2 -0
  145. package/dist/mcp/__tests__/hermes-bridge.test.d.ts.map +1 -0
  146. package/dist/mcp/__tests__/hermes-bridge.test.js +374 -0
  147. package/dist/mcp/__tests__/hermes-bridge.test.js.map +1 -0
  148. package/dist/mcp/__tests__/state-paths.test.js +155 -11
  149. package/dist/mcp/__tests__/state-paths.test.js.map +1 -1
  150. package/dist/mcp/__tests__/state-server.test.js +166 -0
  151. package/dist/mcp/__tests__/state-server.test.js.map +1 -1
  152. package/dist/mcp/bootstrap.d.ts +1 -1
  153. package/dist/mcp/bootstrap.d.ts.map +1 -1
  154. package/dist/mcp/bootstrap.js +2 -0
  155. package/dist/mcp/bootstrap.js.map +1 -1
  156. package/dist/mcp/hermes-bridge.d.ts +81 -0
  157. package/dist/mcp/hermes-bridge.d.ts.map +1 -0
  158. package/dist/mcp/hermes-bridge.js +400 -0
  159. package/dist/mcp/hermes-bridge.js.map +1 -0
  160. package/dist/mcp/hermes-server.d.ts +269 -0
  161. package/dist/mcp/hermes-server.d.ts.map +1 -0
  162. package/dist/mcp/hermes-server.js +121 -0
  163. package/dist/mcp/hermes-server.js.map +1 -0
  164. package/dist/mcp/state-paths.d.ts.map +1 -1
  165. package/dist/mcp/state-paths.js +64 -11
  166. package/dist/mcp/state-paths.js.map +1 -1
  167. package/dist/modes/__tests__/base-session-scope.test.js +22 -0
  168. package/dist/modes/__tests__/base-session-scope.test.js.map +1 -1
  169. package/dist/modes/__tests__/base-tmux-pane.test.js +88 -27
  170. package/dist/modes/__tests__/base-tmux-pane.test.js.map +1 -1
  171. package/dist/modes/base.d.ts.map +1 -1
  172. package/dist/modes/base.js +5 -0
  173. package/dist/modes/base.js.map +1 -1
  174. package/dist/planning/__tests__/approved-execution-lifecycle-matrix.test.d.ts +2 -0
  175. package/dist/planning/__tests__/approved-execution-lifecycle-matrix.test.d.ts.map +1 -0
  176. package/dist/planning/__tests__/approved-execution-lifecycle-matrix.test.js +316 -0
  177. package/dist/planning/__tests__/approved-execution-lifecycle-matrix.test.js.map +1 -0
  178. package/dist/planning/__tests__/approved-launch-hint-lineage-matrix.test.d.ts +2 -0
  179. package/dist/planning/__tests__/approved-launch-hint-lineage-matrix.test.d.ts.map +1 -0
  180. package/dist/planning/__tests__/approved-launch-hint-lineage-matrix.test.js +481 -0
  181. package/dist/planning/__tests__/approved-launch-hint-lineage-matrix.test.js.map +1 -0
  182. package/dist/planning/__tests__/artifacts.test.js +533 -4
  183. package/dist/planning/__tests__/artifacts.test.js.map +1 -1
  184. package/dist/planning/__tests__/context-pack-status.test.js +524 -0
  185. package/dist/planning/__tests__/context-pack-status.test.js.map +1 -1
  186. package/dist/planning/__tests__/markdown-structure.test.d.ts +2 -0
  187. package/dist/planning/__tests__/markdown-structure.test.d.ts.map +1 -0
  188. package/dist/planning/__tests__/markdown-structure.test.js +459 -0
  189. package/dist/planning/__tests__/markdown-structure.test.js.map +1 -0
  190. package/dist/planning/__tests__/ready-context-pack-role-refs.test.js +523 -1
  191. package/dist/planning/__tests__/ready-context-pack-role-refs.test.js.map +1 -1
  192. package/dist/planning/artifacts.d.ts +1 -1
  193. package/dist/planning/artifacts.d.ts.map +1 -1
  194. package/dist/planning/artifacts.js +227 -28
  195. package/dist/planning/artifacts.js.map +1 -1
  196. package/dist/planning/context-pack-status.d.ts +25 -0
  197. package/dist/planning/context-pack-status.d.ts.map +1 -1
  198. package/dist/planning/context-pack-status.js +272 -31
  199. package/dist/planning/context-pack-status.js.map +1 -1
  200. package/dist/planning/markdown-structure.d.ts +20 -0
  201. package/dist/planning/markdown-structure.d.ts.map +1 -0
  202. package/dist/planning/markdown-structure.js +137 -0
  203. package/dist/planning/markdown-structure.js.map +1 -0
  204. package/dist/ralph/__tests__/completion-audit.test.d.ts +2 -0
  205. package/dist/ralph/__tests__/completion-audit.test.d.ts.map +1 -0
  206. package/dist/ralph/__tests__/completion-audit.test.js +121 -0
  207. package/dist/ralph/__tests__/completion-audit.test.js.map +1 -0
  208. package/dist/ralph/completion-audit.d.ts +8 -0
  209. package/dist/ralph/completion-audit.d.ts.map +1 -0
  210. package/dist/ralph/completion-audit.js +99 -0
  211. package/dist/ralph/completion-audit.js.map +1 -0
  212. package/dist/scripts/__tests__/codex-native-hook.test.js +407 -15
  213. package/dist/scripts/__tests__/codex-native-hook.test.js.map +1 -1
  214. package/dist/scripts/__tests__/notify-dispatcher.test.d.ts +2 -0
  215. package/dist/scripts/__tests__/notify-dispatcher.test.d.ts.map +1 -0
  216. package/dist/scripts/__tests__/notify-dispatcher.test.js +126 -0
  217. package/dist/scripts/__tests__/notify-dispatcher.test.js.map +1 -0
  218. package/dist/scripts/codex-native-hook.d.ts +1 -0
  219. package/dist/scripts/codex-native-hook.d.ts.map +1 -1
  220. package/dist/scripts/codex-native-hook.js +177 -71
  221. package/dist/scripts/codex-native-hook.js.map +1 -1
  222. package/dist/scripts/codex-native-pre-post.d.ts.map +1 -1
  223. package/dist/scripts/codex-native-pre-post.js +4 -2
  224. package/dist/scripts/codex-native-pre-post.js.map +1 -1
  225. package/dist/scripts/notify-dispatcher.js +30 -1
  226. package/dist/scripts/notify-dispatcher.js.map +1 -1
  227. package/dist/scripts/notify-hook/tmux-injection.d.ts.map +1 -1
  228. package/dist/scripts/notify-hook/tmux-injection.js +91 -2
  229. package/dist/scripts/notify-hook/tmux-injection.js.map +1 -1
  230. package/dist/scripts/notify-hook.js +3 -1
  231. package/dist/scripts/notify-hook.js.map +1 -1
  232. package/dist/state/__tests__/workflow-transition.test.js +102 -27
  233. package/dist/state/__tests__/workflow-transition.test.js.map +1 -1
  234. package/dist/state/mode-state-context.d.ts +2 -0
  235. package/dist/state/mode-state-context.d.ts.map +1 -1
  236. package/dist/state/mode-state-context.js +21 -0
  237. package/dist/state/mode-state-context.js.map +1 -1
  238. package/dist/state/operations.d.ts.map +1 -1
  239. package/dist/state/operations.js +9 -3
  240. package/dist/state/operations.js.map +1 -1
  241. package/dist/state/skill-active.d.ts +7 -0
  242. package/dist/state/skill-active.d.ts.map +1 -1
  243. package/dist/state/skill-active.js +25 -8
  244. package/dist/state/skill-active.js.map +1 -1
  245. package/dist/state/workflow-transition-reconcile.d.ts +1 -0
  246. package/dist/state/workflow-transition-reconcile.d.ts.map +1 -1
  247. package/dist/state/workflow-transition-reconcile.js +22 -15
  248. package/dist/state/workflow-transition-reconcile.js.map +1 -1
  249. package/dist/state/workflow-transition.js +3 -3
  250. package/dist/state/workflow-transition.js.map +1 -1
  251. package/dist/team/__tests__/approved-execution.test.js +39 -0
  252. package/dist/team/__tests__/approved-execution.test.js.map +1 -1
  253. package/dist/team/__tests__/runtime.test.js +5 -0
  254. package/dist/team/__tests__/runtime.test.js.map +1 -1
  255. package/dist/team/__tests__/scaling.test.js +497 -2
  256. package/dist/team/__tests__/scaling.test.js.map +1 -1
  257. package/dist/team/__tests__/state-root.test.js +1 -1
  258. package/dist/team/__tests__/state-root.test.js.map +1 -1
  259. package/dist/team/__tests__/worker-bootstrap.test.js +8 -0
  260. package/dist/team/__tests__/worker-bootstrap.test.js.map +1 -1
  261. package/dist/team/approved-execution.d.ts.map +1 -1
  262. package/dist/team/approved-execution.js +3 -0
  263. package/dist/team/approved-execution.js.map +1 -1
  264. package/dist/team/scaling.d.ts.map +1 -1
  265. package/dist/team/scaling.js +43 -0
  266. package/dist/team/scaling.js.map +1 -1
  267. package/dist/team/state-root.d.ts.map +1 -1
  268. package/dist/team/state-root.js +4 -0
  269. package/dist/team/state-root.js.map +1 -1
  270. package/dist/team/state.d.ts.map +1 -1
  271. package/dist/team/state.js +2 -6
  272. package/dist/team/state.js.map +1 -1
  273. package/dist/ultragoal/__tests__/artifacts.test.js +245 -1
  274. package/dist/ultragoal/__tests__/artifacts.test.js.map +1 -1
  275. package/dist/ultragoal/__tests__/docs-contract.test.js +21 -0
  276. package/dist/ultragoal/__tests__/docs-contract.test.js.map +1 -1
  277. package/dist/ultragoal/artifacts.d.ts +52 -2
  278. package/dist/ultragoal/artifacts.d.ts.map +1 -1
  279. package/dist/ultragoal/artifacts.js +301 -15
  280. package/dist/ultragoal/artifacts.js.map +1 -1
  281. package/dist/utils/__tests__/paths.test.js +31 -1
  282. package/dist/utils/__tests__/paths.test.js.map +1 -1
  283. package/dist/utils/paths.d.ts +6 -0
  284. package/dist/utils/paths.d.ts.map +1 -1
  285. package/dist/utils/paths.js +18 -0
  286. package/dist/utils/paths.js.map +1 -1
  287. package/dist/wiki/lifecycle.js +4 -4
  288. package/dist/wiki/lifecycle.js.map +1 -1
  289. package/package.json +1 -1
  290. package/plugins/oh-my-codex/.codex-plugin/plugin.json +1 -1
  291. package/plugins/oh-my-codex/.mcp.json +13 -5
  292. package/plugins/oh-my-codex/skills/analyze/SKILL.md +0 -2
  293. package/plugins/oh-my-codex/skills/autopilot/SKILL.md +2 -2
  294. package/plugins/oh-my-codex/skills/code-review/SKILL.md +1 -3
  295. package/plugins/oh-my-codex/skills/deep-interview/SKILL.md +5 -7
  296. package/plugins/oh-my-codex/skills/design/SKILL.md +180 -0
  297. package/plugins/oh-my-codex/skills/doctor/SKILL.md +2 -2
  298. package/plugins/oh-my-codex/skills/omx-setup/SKILL.md +3 -3
  299. package/plugins/oh-my-codex/skills/pipeline/SKILL.md +3 -3
  300. package/plugins/oh-my-codex/skills/plan/SKILL.md +3 -6
  301. package/plugins/oh-my-codex/skills/ralph/SKILL.md +9 -10
  302. package/plugins/oh-my-codex/skills/skill/SKILL.md +2 -1
  303. package/plugins/oh-my-codex/skills/ultragoal/SKILL.md +36 -3
  304. package/plugins/oh-my-codex/skills/ultraqa/SKILL.md +175 -64
  305. package/plugins/oh-my-codex/skills/ultrawork/SKILL.md +8 -8
  306. package/plugins/oh-my-codex/skills/visual-ralph/SKILL.md +2 -2
  307. package/plugins/oh-my-codex/skills/wiki/SKILL.md +13 -13
  308. package/skills/analyze/SKILL.md +0 -2
  309. package/skills/ask-claude/SKILL.md +5 -3
  310. package/skills/ask-gemini/SKILL.md +5 -3
  311. package/skills/autopilot/SKILL.md +2 -2
  312. package/skills/code-review/SKILL.md +1 -3
  313. package/skills/deep-interview/SKILL.md +5 -7
  314. package/skills/design/SKILL.md +180 -0
  315. package/skills/doctor/SKILL.md +2 -2
  316. package/skills/ecomode/SKILL.md +105 -1
  317. package/skills/frontend-ui-ux/SKILL.md +6 -24
  318. package/skills/git-master/SKILL.md +2 -4
  319. package/skills/omx-setup/SKILL.md +3 -3
  320. package/skills/pipeline/SKILL.md +3 -3
  321. package/skills/plan/SKILL.md +3 -6
  322. package/skills/ralph/SKILL.md +9 -10
  323. package/skills/skill/SKILL.md +2 -1
  324. package/skills/swarm/SKILL.md +5 -3
  325. package/skills/tdd/SKILL.md +95 -1
  326. package/skills/ultragoal/SKILL.md +36 -3
  327. package/skills/ultraqa/SKILL.md +175 -64
  328. package/skills/ultrawork/SKILL.md +8 -8
  329. package/skills/visual-ralph/SKILL.md +2 -2
  330. package/skills/web-clone/SKILL.md +348 -1
  331. package/skills/wiki/SKILL.md +13 -13
  332. package/src/scripts/__tests__/codex-native-hook.test.ts +437 -14
  333. package/src/scripts/__tests__/notify-dispatcher.test.ts +153 -0
  334. package/src/scripts/codex-native-hook.ts +205 -61
  335. package/src/scripts/codex-native-pre-post.ts +4 -1
  336. package/src/scripts/notify-dispatcher.ts +40 -1
  337. package/src/scripts/notify-hook/tmux-injection.ts +110 -3
  338. package/src/scripts/notify-hook.ts +3 -1
  339. package/templates/catalog-manifest.json +9 -2
@@ -68,6 +68,25 @@ async function writeContextPack(slug, prdPath, testSpecPath, roles) {
68
68
  })),
69
69
  }, null, 2));
70
70
  }
71
+ async function writeContextPackWithEntries(slug, prdPath, testSpecPath, entries) {
72
+ const packPath = join(tempDir, canonicalContextPackRelativePath(slug));
73
+ const prdActual = await readFile(prdPath, 'utf-8');
74
+ const testSpecActual = await readFile(testSpecPath, 'utf-8');
75
+ await writeFile(packPath, JSON.stringify({
76
+ slug,
77
+ basis: {
78
+ prd: {
79
+ path: relativeToRepo(prdPath),
80
+ sha1: computeGitBlobSha1(prdActual),
81
+ },
82
+ testSpecs: [{
83
+ path: relativeToRepo(testSpecPath),
84
+ sha1: computeGitBlobSha1(testSpecActual),
85
+ }],
86
+ },
87
+ entries,
88
+ }, null, 2));
89
+ }
71
90
  describe('context pack handoff status', () => {
72
91
  beforeEach(async () => { await setup(); });
73
92
  afterEach(async () => { await cleanup(); });
@@ -126,6 +145,8 @@ describe('context pack handoff status', () => {
126
145
  assert.equal(status.contextPackStatus, 'plan-only');
127
146
  assert.equal(status.baselineState, 'present');
128
147
  assert.equal(status.outcomeState, 'absent');
148
+ assert.equal(status.declarationState, 'unknown');
149
+ assert.equal(status.contextPackRoleRefs, null);
129
150
  assert.equal(status.roleCoverage, 'unknown');
130
151
  assert.deepEqual(status.missingRequiredContextPackRoles, []);
131
152
  assert.deepEqual(status.contextPackIssues, []);
@@ -143,11 +164,313 @@ describe('context pack handoff status', () => {
143
164
  assert.deepEqual(status.contextPack, { path: packPath });
144
165
  assert.equal(status.contextPackStatus, 'ready');
145
166
  assert.equal(status.packState, 'valid');
167
+ assert.equal(status.declarationState, 'matching');
146
168
  assert.equal(status.roleCoverage, 'covered');
147
169
  assert.equal(status.basisState, 'fresh');
170
+ assert.deepEqual(status.contextPackRoleRefs, {
171
+ scope: ['src/scope-0.ts'],
172
+ build: ['src/build-1.ts'],
173
+ verify: ['src/verify-2.ts'],
174
+ });
148
175
  assert.deepEqual(status.missingRequiredContextPackRoles, []);
149
176
  assert.deepEqual(status.contextPackIssues, []);
150
177
  });
178
+ it('keeps the handoff state machine invariant under private metadata variations and counterfactuals', async () => {
179
+ const cases = [
180
+ {
181
+ slug: 'ready-valid-private-metadata',
182
+ entries: [
183
+ { path: 'src/scope.ts', roles: ['scope'] },
184
+ {
185
+ path: 'src/build.ts',
186
+ roles: ['build'],
187
+ label: 'Runtime Contract',
188
+ tags: ['runtime', 'build'],
189
+ selector: { type: 'heading', value: '## Runtime Contract', maxWords: 120 },
190
+ relationPath: [
191
+ { tag: 'plan', target: 'ready-valid-private-metadata' },
192
+ { tag: 'implements', target: 'src/build.ts#runtime-contract' },
193
+ ],
194
+ },
195
+ {
196
+ path: 'src/verify.ts',
197
+ roles: ['verify'],
198
+ selector: { type: 'lines', start: 2, end: 4 },
199
+ },
200
+ ],
201
+ expected: {
202
+ status: 'ready',
203
+ packState: 'valid',
204
+ declarationState: 'matching',
205
+ basisState: 'fresh',
206
+ roleCoverage: 'covered',
207
+ roleRefs: {
208
+ scope: ['src/scope.ts'],
209
+ build: ['src/build.ts'],
210
+ verify: ['src/verify.ts'],
211
+ },
212
+ },
213
+ },
214
+ {
215
+ slug: 'ready-invalid-private-metadata',
216
+ entries: [
217
+ { path: 'src/scope.ts', roles: ['scope'] },
218
+ {
219
+ path: 'src/build.ts',
220
+ roles: ['build'],
221
+ selector: { type: 'heading', value: 'Runtime Contract', maxWords: 20 },
222
+ },
223
+ { path: 'src/verify.ts', roles: ['verify'] },
224
+ ],
225
+ expected: {
226
+ status: 'ready',
227
+ packState: 'valid',
228
+ declarationState: 'matching',
229
+ basisState: 'fresh',
230
+ roleCoverage: 'covered',
231
+ roleRefs: {
232
+ scope: ['src/scope.ts'],
233
+ build: ['src/build.ts'],
234
+ verify: ['src/verify.ts'],
235
+ },
236
+ },
237
+ },
238
+ {
239
+ slug: 'incomplete-invalid-private-metadata',
240
+ entries: [
241
+ { path: 'src/scope.ts', roles: ['scope'] },
242
+ {
243
+ path: 'src/build.ts',
244
+ roles: ['build'],
245
+ relationPath: [
246
+ { tag: 'plan', target: 'alpha' },
247
+ { tag: 'evidence', target: 'beta' },
248
+ { tag: 'implements', target: 'gamma' },
249
+ { tag: 'dependency', target: 'delta' },
250
+ { tag: 'bounds', target: 'epsilon' },
251
+ { tag: 'verifies', target: 'zeta' },
252
+ ],
253
+ },
254
+ ],
255
+ expected: {
256
+ status: 'incomplete',
257
+ packState: 'valid',
258
+ declarationState: 'matching',
259
+ basisState: 'fresh',
260
+ roleCoverage: 'missing-required-roles',
261
+ roleRefs: null,
262
+ missingRoles: ['verify'],
263
+ },
264
+ },
265
+ {
266
+ slug: 'invalid-stale-private-metadata',
267
+ entries: [
268
+ { path: 'src/scope.ts', roles: ['scope'] },
269
+ {
270
+ path: 'src/build.ts',
271
+ roles: ['build'],
272
+ tags: ['runtime', '', 'verify'],
273
+ },
274
+ { path: 'src/verify.ts', roles: ['verify'] },
275
+ ],
276
+ mutate: async ({ testSpecPath }) => {
277
+ await writeFile(testSpecPath, '# Drifted Test Spec\n');
278
+ },
279
+ expected: {
280
+ status: 'invalid',
281
+ packState: 'valid',
282
+ declarationState: 'matching',
283
+ basisState: 'stale',
284
+ roleCoverage: 'covered',
285
+ roleRefs: null,
286
+ issue: 'basis test-spec hash',
287
+ },
288
+ },
289
+ ];
290
+ for (const testCase of cases) {
291
+ const fixture = await writeApprovedPlan(testCase.slug, [
292
+ '# PRD',
293
+ '',
294
+ buildContextPackOutcome(canonicalContextPackRelativePath(testCase.slug)),
295
+ '',
296
+ `Launch via omx ralph ${JSON.stringify(`Execute ${testCase.slug} plan`)}`,
297
+ ]);
298
+ await writeContextPackWithEntries(testCase.slug, fixture.prdPath, fixture.testSpecPath, testCase.entries);
299
+ if (testCase.mutate) {
300
+ await testCase.mutate(fixture);
301
+ }
302
+ const status = readContextPackHandoffStatus(tempDir, fixture.prdPath);
303
+ assert.equal(status.contextPackStatus, testCase.expected.status, `status mismatch for ${testCase.slug}`);
304
+ assert.equal(status.packState, testCase.expected.packState, `packState mismatch for ${testCase.slug}`);
305
+ assert.equal(status.declarationState, testCase.expected.declarationState, `declarationState mismatch for ${testCase.slug}`);
306
+ assert.equal(status.basisState, testCase.expected.basisState, `basisState mismatch for ${testCase.slug}`);
307
+ assert.equal(status.roleCoverage, testCase.expected.roleCoverage, `roleCoverage mismatch for ${testCase.slug}`);
308
+ assert.deepEqual(status.contextPackRoleRefs, testCase.expected.roleRefs, `role refs mismatch for ${testCase.slug}`);
309
+ assert.deepEqual(status.missingRequiredContextPackRoles, testCase.expected.missingRoles ?? [], `missing roles mismatch for ${testCase.slug}`);
310
+ if (testCase.expected.issue) {
311
+ assert.ok(status.contextPackIssues.some((issue) => issue.includes(testCase.expected.issue ?? '')), `expected issue containing ${testCase.expected.issue} for ${testCase.slug}`);
312
+ }
313
+ else {
314
+ assert.deepEqual(status.contextPackIssues, [], `issues should stay empty for ${testCase.slug}`);
315
+ }
316
+ }
317
+ });
318
+ it('keeps the public handoff state machine invariant across exhaustive private metadata modes', async () => {
319
+ const metadataVariants = [
320
+ {
321
+ name: 'none',
322
+ buildMetadata: () => ({}),
323
+ },
324
+ {
325
+ name: 'valid-heading',
326
+ buildMetadata: (slug) => ({
327
+ label: 'Runtime Contract',
328
+ tags: ['runtime', 'build'],
329
+ selector: { type: 'heading', value: ' ## Runtime Contract ', maxWords: 120 },
330
+ relationPath: [
331
+ { tag: 'Plan', target: ` ${slug} ` },
332
+ { tag: 'Implements', target: ' src/build.ts#runtime-contract ' },
333
+ ],
334
+ }),
335
+ },
336
+ {
337
+ name: 'valid-lines',
338
+ buildMetadata: (slug) => ({
339
+ label: 'Verify Slice',
340
+ selector: { type: 'lines', start: 2, end: 5 },
341
+ relationPath: [
342
+ { tag: 'Plan', target: ` ${slug} ` },
343
+ { tag: 'Verifies', target: ' src/build.ts:2-5 ' },
344
+ ],
345
+ }),
346
+ },
347
+ {
348
+ name: 'invalid-label',
349
+ buildMetadata: () => ({ label: '---' }),
350
+ },
351
+ {
352
+ name: 'invalid-tags',
353
+ buildMetadata: () => ({ tags: ['runtime', '', 'verify'] }),
354
+ },
355
+ {
356
+ name: 'invalid-selector-extra-key',
357
+ buildMetadata: () => ({
358
+ selector: { type: 'heading', value: 'Runtime Contract', extra: true },
359
+ }),
360
+ },
361
+ {
362
+ name: 'invalid-relation-too-many-steps',
363
+ buildMetadata: () => ({
364
+ relationPath: [
365
+ { tag: 'plan', target: 'alpha' },
366
+ { tag: 'evidence', target: 'beta' },
367
+ { tag: 'dependency', target: 'gamma' },
368
+ { tag: 'links-to', target: 'delta' },
369
+ { tag: 'bounds', target: 'epsilon' },
370
+ { tag: 'verifies', target: 'zeta' },
371
+ ],
372
+ }),
373
+ },
374
+ {
375
+ name: 'invalid-relation-step-extra-key',
376
+ buildMetadata: () => ({
377
+ relationPath: [{ tag: 'plan', target: 'alpha', extra: true }],
378
+ }),
379
+ },
380
+ {
381
+ name: 'invalid-entry-extra-key',
382
+ buildMetadata: () => ({ unexpected: true }),
383
+ },
384
+ ];
385
+ const scenarios = [
386
+ {
387
+ name: 'ready',
388
+ buildEntries: (metadata) => [
389
+ { path: 'src/scope.ts', roles: ['scope'] },
390
+ { path: 'src/build.ts', roles: ['build'], ...metadata },
391
+ { path: 'src/verify.ts', roles: ['verify'] },
392
+ ],
393
+ expected: {
394
+ status: 'ready',
395
+ packState: 'valid',
396
+ declarationState: 'matching',
397
+ basisState: 'fresh',
398
+ roleCoverage: 'covered',
399
+ roleRefs: {
400
+ scope: ['src/scope.ts'],
401
+ build: ['src/build.ts'],
402
+ verify: ['src/verify.ts'],
403
+ },
404
+ },
405
+ },
406
+ {
407
+ name: 'incomplete',
408
+ buildEntries: (metadata) => [
409
+ { path: 'src/scope.ts', roles: ['scope'] },
410
+ { path: 'src/build.ts', roles: ['build'], ...metadata },
411
+ ],
412
+ expected: {
413
+ status: 'incomplete',
414
+ packState: 'valid',
415
+ declarationState: 'matching',
416
+ basisState: 'fresh',
417
+ roleCoverage: 'missing-required-roles',
418
+ roleRefs: null,
419
+ missingRoles: ['verify'],
420
+ },
421
+ },
422
+ {
423
+ name: 'stale',
424
+ buildEntries: (metadata) => [
425
+ { path: 'src/scope.ts', roles: ['scope'] },
426
+ { path: 'src/build.ts', roles: ['build'], ...metadata },
427
+ { path: 'src/verify.ts', roles: ['verify'] },
428
+ ],
429
+ mutate: async ({ testSpecPath }) => {
430
+ await writeFile(testSpecPath, '# Drifted Test Spec\n');
431
+ },
432
+ expected: {
433
+ status: 'invalid',
434
+ packState: 'valid',
435
+ declarationState: 'matching',
436
+ basisState: 'stale',
437
+ roleCoverage: 'covered',
438
+ roleRefs: null,
439
+ issue: 'basis test-spec hash',
440
+ },
441
+ },
442
+ ];
443
+ for (const scenario of scenarios) {
444
+ for (const metadataVariant of metadataVariants) {
445
+ const slug = `${scenario.name}-${metadataVariant.name}`;
446
+ const fixture = await writeApprovedPlan(slug, [
447
+ '# PRD',
448
+ '',
449
+ buildContextPackOutcome(canonicalContextPackRelativePath(slug)),
450
+ '',
451
+ `Launch via omx ralph ${JSON.stringify(`Execute ${slug} plan`)}`,
452
+ ]);
453
+ await writeContextPackWithEntries(slug, fixture.prdPath, fixture.testSpecPath, scenario.buildEntries(metadataVariant.buildMetadata(slug)));
454
+ if (scenario.mutate) {
455
+ await scenario.mutate(fixture);
456
+ }
457
+ const status = readContextPackHandoffStatus(tempDir, fixture.prdPath);
458
+ assert.equal(status.contextPackStatus, scenario.expected.status, `status mismatch for ${slug}`);
459
+ assert.equal(status.packState, scenario.expected.packState, `packState mismatch for ${slug}`);
460
+ assert.equal(status.declarationState, scenario.expected.declarationState, `declarationState mismatch for ${slug}`);
461
+ assert.equal(status.basisState, scenario.expected.basisState, `basisState mismatch for ${slug}`);
462
+ assert.equal(status.roleCoverage, scenario.expected.roleCoverage, `roleCoverage mismatch for ${slug}`);
463
+ assert.deepEqual(status.contextPackRoleRefs, scenario.expected.roleRefs, `role refs mismatch for ${slug}`);
464
+ assert.deepEqual(status.missingRequiredContextPackRoles, scenario.expected.missingRoles ?? [], `missing roles mismatch for ${slug}`);
465
+ if (scenario.expected.issue) {
466
+ assert.ok(status.contextPackIssues.some((issue) => issue.includes(scenario.expected.issue ?? '')), `expected issue containing ${scenario.expected.issue} for ${slug}`);
467
+ }
468
+ else {
469
+ assert.deepEqual(status.contextPackIssues, [], `issues should stay empty for ${slug}`);
470
+ }
471
+ }
472
+ }
473
+ });
151
474
  it('reports incomplete when the declared pack omits required execution roles', async () => {
152
475
  const { prdPath, testSpecPath } = await writeApprovedPlan('gamma', [
153
476
  '# PRD',
@@ -159,6 +482,9 @@ describe('context pack handoff status', () => {
159
482
  await writeContextPack('gamma', prdPath, testSpecPath, ['scope']);
160
483
  const status = readContextPackHandoffStatus(tempDir);
161
484
  assert.equal(status.contextPackStatus, 'incomplete');
485
+ assert.equal(status.roleCoverage, 'missing-required-roles');
486
+ assert.equal(status.contextPackRoleRefs, null);
487
+ assert.equal(status.roleCoverage, 'missing-required-roles');
162
488
  assert.deepEqual(status.missingRequiredContextPackRoles, ['build', 'verify']);
163
489
  });
164
490
  it('reports invalid when the declared pack basis drifts from the approved test spec', async () => {
@@ -174,6 +500,9 @@ describe('context pack handoff status', () => {
174
500
  const status = readContextPackHandoffStatus(tempDir);
175
501
  assert.equal(status.contextPackStatus, 'invalid');
176
502
  assert.equal(status.roleCoverage, 'covered');
503
+ assert.equal(status.basisState, 'stale');
504
+ assert.equal(status.contextPackRoleRefs, null);
505
+ assert.equal(status.basisState, 'stale');
177
506
  assert.deepEqual(status.missingRequiredContextPackRoles, []);
178
507
  assert.ok(status.contextPackIssues.some((issue) => issue.includes('basis test-spec hash')));
179
508
  });
@@ -190,6 +519,7 @@ describe('context pack handoff status', () => {
190
519
  const status = readContextPackHandoffStatus(tempDir);
191
520
  assert.equal(status.contextPackStatus, 'invalid');
192
521
  assert.equal(status.roleCoverage, 'missing-required-roles');
522
+ assert.equal(status.contextPackRoleRefs, null);
193
523
  assert.deepEqual(status.missingRequiredContextPackRoles, ['build', 'verify']);
194
524
  assert.ok(status.contextPackIssues.some((issue) => issue.includes('basis test-spec hash')));
195
525
  });
@@ -205,6 +535,7 @@ describe('context pack handoff status', () => {
205
535
  const status = readContextPackHandoffStatus(tempDir);
206
536
  assert.equal(status.contextPackStatus, 'invalid');
207
537
  assert.equal(status.packState, 'invalid');
538
+ assert.equal(status.contextPackRoleRefs, null);
208
539
  assert.equal(status.roleCoverage, 'unknown');
209
540
  assert.deepEqual(status.missingRequiredContextPackRoles, []);
210
541
  assert.ok(status.contextPackIssues.some((issue) => issue.includes('invalid JSON')));
@@ -225,6 +556,97 @@ describe('context pack handoff status', () => {
225
556
  assert.equal(status.contextPackStatus, 'plan-only');
226
557
  assert.equal(status.outcomeState, 'absent');
227
558
  });
559
+ it('ignores indented outcome declarations and keeps the plan in plan-only status', async () => {
560
+ await writeApprovedPlan('epsilon-indented', [
561
+ '# PRD',
562
+ '',
563
+ ' ## Context Pack Outcome',
564
+ '',
565
+ ` - pack: created \`${canonicalContextPackRelativePath('epsilon-indented')}\``,
566
+ '',
567
+ 'Launch via omx ralph "Execute epsilon indented plan"',
568
+ ]);
569
+ const status = readContextPackHandoffStatus(tempDir);
570
+ assert.equal(status.contextPackStatus, 'plan-only');
571
+ assert.equal(status.outcomeState, 'absent');
572
+ });
573
+ it('ignores commented outcome declarations and keeps the plan in plan-only status', async () => {
574
+ await writeApprovedPlan('epsilon-commented', [
575
+ '# PRD',
576
+ '',
577
+ '<!--',
578
+ '## Context Pack Outcome',
579
+ '',
580
+ `- pack: created \`${canonicalContextPackRelativePath('epsilon-commented-hidden')}\``,
581
+ '<!--',
582
+ `- pack: created \`${canonicalContextPackRelativePath('epsilon-commented-hidden-deeper')}\``,
583
+ '-->',
584
+ '-->',
585
+ '',
586
+ 'Launch via omx ralph "Execute epsilon commented plan"',
587
+ ]);
588
+ const status = readContextPackHandoffStatus(tempDir);
589
+ assert.equal(status.contextPackStatus, 'plan-only');
590
+ assert.equal(status.outcomeState, 'absent');
591
+ });
592
+ it('ignores adversarial hidden outcome declarations and still reads the visible declaration', async () => {
593
+ const { prdPath, testSpecPath } = await writeApprovedPlan('epsilon-adversarial', [
594
+ '# PRD',
595
+ '',
596
+ '```md',
597
+ '## Context Pack Outcome',
598
+ '',
599
+ `- pack: created \`${canonicalContextPackRelativePath('sample-hidden')}\``,
600
+ ' ```',
601
+ '```still-open',
602
+ '~~~',
603
+ '## Context Pack Outcome',
604
+ '',
605
+ `- pack: created \`${canonicalContextPackRelativePath('other-hidden')}\``,
606
+ '```',
607
+ '',
608
+ buildContextPackOutcome(canonicalContextPackRelativePath('epsilon-adversarial')),
609
+ '',
610
+ 'Launch via omx ralph "Execute epsilon adversarial plan"',
611
+ ]);
612
+ await writeContextPack('epsilon-adversarial', prdPath, testSpecPath, ['scope', 'build', 'verify']);
613
+ const status = readContextPackHandoffStatus(tempDir);
614
+ assert.equal(status.contextPackStatus, 'ready');
615
+ assert.equal(status.outcomeState, 'declared');
616
+ assert.deepEqual(status.contextPack, {
617
+ path: join(tempDir, canonicalContextPackRelativePath('epsilon-adversarial')),
618
+ });
619
+ assert.deepEqual(status.contextPackIssues, []);
620
+ });
621
+ it('keeps the visible outcome section valid when nested hidden blocks appear inside it', async () => {
622
+ const { prdPath, testSpecPath } = await writeApprovedPlan('epsilon-inner-hidden', [
623
+ '# PRD',
624
+ '',
625
+ '## Context Pack Outcome',
626
+ '',
627
+ '<!--',
628
+ `- pack: created \`${canonicalContextPackRelativePath('epsilon-inner-hidden-comment')}\``,
629
+ '<!--',
630
+ `- pack: created \`${canonicalContextPackRelativePath('epsilon-inner-hidden-comment-deeper')}\``,
631
+ '-->',
632
+ '-->',
633
+ '```md',
634
+ `- pack: created \`${canonicalContextPackRelativePath('epsilon-inner-hidden-fenced')}\``,
635
+ '```',
636
+ ` - pack: created \`${canonicalContextPackRelativePath('epsilon-inner-hidden-indented')}\``,
637
+ `- pack: created \`${canonicalContextPackRelativePath('epsilon-inner-hidden')}\``,
638
+ '',
639
+ 'Launch via omx ralph "Execute epsilon inner hidden plan"',
640
+ ]);
641
+ await writeContextPack('epsilon-inner-hidden', prdPath, testSpecPath, ['scope', 'build', 'verify']);
642
+ const status = readContextPackHandoffStatus(tempDir);
643
+ assert.equal(status.contextPackStatus, 'ready');
644
+ assert.equal(status.outcomeState, 'declared');
645
+ assert.deepEqual(status.contextPack, {
646
+ path: join(tempDir, canonicalContextPackRelativePath('epsilon-inner-hidden')),
647
+ });
648
+ assert.deepEqual(status.contextPackIssues, []);
649
+ });
228
650
  it('rejects nested outcome paths that are not canonical context-pack files', async () => {
229
651
  await writeApprovedPlan('eta', [
230
652
  '# PRD',
@@ -249,6 +671,7 @@ describe('context pack handoff status', () => {
249
671
  const status = readContextPackHandoffStatus(tempDir);
250
672
  assert.equal(status.contextPackStatus, 'invalid');
251
673
  assert.equal(status.outcomeState, 'declared');
674
+ assert.equal(status.declarationState, 'mismatched');
252
675
  assert.equal(status.packState, 'invalid');
253
676
  assert.ok(status.contextPackIssues.some((issue) => issue.includes('does not match approved plan slug theta')));
254
677
  });
@@ -267,5 +690,106 @@ describe('context pack handoff status', () => {
267
690
  assert.equal(status.outcomeState, 'ambiguous');
268
691
  assert.ok(status.contextPackIssues.some((issue) => issue.includes('multiple Context Pack Outcome sections')));
269
692
  });
693
+ it('distinguishes missing, unreadable, invalid, stale, mismatched, and missing-role handoffs', async () => {
694
+ const cases = [
695
+ {
696
+ slug: 'missing-pack',
697
+ mutate: async () => { },
698
+ expected: {
699
+ status: 'incomplete',
700
+ packState: 'missing',
701
+ declarationState: 'matching',
702
+ basisState: 'stale',
703
+ roleCoverage: 'unknown',
704
+ issue: 'file is missing',
705
+ },
706
+ },
707
+ {
708
+ slug: 'unreadable-pack',
709
+ mutate: async ({ packPath }) => { await mkdir(packPath, { recursive: true }); },
710
+ expected: {
711
+ status: 'invalid',
712
+ packState: 'unreadable',
713
+ declarationState: 'matching',
714
+ basisState: 'stale',
715
+ roleCoverage: 'unknown',
716
+ issue: 'could not be read',
717
+ },
718
+ },
719
+ {
720
+ slug: 'invalid-pack',
721
+ mutate: async ({ packPath }) => { await writeFile(packPath, '{bad json'); },
722
+ expected: {
723
+ status: 'invalid',
724
+ packState: 'invalid',
725
+ declarationState: 'matching',
726
+ basisState: 'stale',
727
+ roleCoverage: 'unknown',
728
+ issue: 'invalid JSON',
729
+ },
730
+ },
731
+ {
732
+ slug: 'stale-basis',
733
+ mutate: async ({ prdPath, testSpecPath }) => {
734
+ await writeContextPack('stale-basis', prdPath, testSpecPath, ['scope', 'build', 'verify']);
735
+ await writeFile(testSpecPath, '# Drifted Test Spec\n');
736
+ },
737
+ expected: {
738
+ status: 'invalid',
739
+ packState: 'valid',
740
+ declarationState: 'matching',
741
+ basisState: 'stale',
742
+ roleCoverage: 'covered',
743
+ issue: 'basis test-spec hash',
744
+ },
745
+ },
746
+ {
747
+ slug: 'missing-role',
748
+ mutate: async ({ prdPath, testSpecPath }) => {
749
+ await writeContextPack('missing-role', prdPath, testSpecPath, ['scope']);
750
+ },
751
+ expected: {
752
+ status: 'incomplete',
753
+ packState: 'valid',
754
+ declarationState: 'matching',
755
+ basisState: 'fresh',
756
+ roleCoverage: 'missing-required-roles',
757
+ missingRoles: ['build', 'verify'],
758
+ },
759
+ },
760
+ ];
761
+ for (const testCase of cases) {
762
+ const fixture = await writeApprovedPlan(testCase.slug, [
763
+ '# PRD',
764
+ '',
765
+ buildContextPackOutcome(canonicalContextPackRelativePath(testCase.slug)),
766
+ '',
767
+ `Launch via omx ralph "Execute ${testCase.slug} plan"`,
768
+ ]);
769
+ await testCase.mutate(fixture);
770
+ const status = readContextPackHandoffStatus(tempDir, fixture.prdPath);
771
+ assert.equal(status.contextPackStatus, testCase.expected.status);
772
+ assert.equal(status.packState, testCase.expected.packState);
773
+ assert.equal(status.declarationState, testCase.expected.declarationState);
774
+ assert.equal(status.basisState, testCase.expected.basisState);
775
+ assert.equal(status.roleCoverage, testCase.expected.roleCoverage);
776
+ assert.deepEqual(status.missingRequiredContextPackRoles, testCase.expected.missingRoles ?? []);
777
+ if (testCase.expected.issue) {
778
+ assert.ok(status.contextPackIssues.some((issue) => issue.includes(testCase.expected.issue ?? '')), `expected issue containing ${testCase.expected.issue}`);
779
+ }
780
+ }
781
+ const mismatch = await writeApprovedPlan('declared-mismatch', [
782
+ '# PRD',
783
+ '',
784
+ buildContextPackOutcome(canonicalContextPackRelativePath('other')),
785
+ '',
786
+ 'Launch via omx ralph "Execute mismatch plan"',
787
+ ]);
788
+ const mismatchStatus = readContextPackHandoffStatus(tempDir, mismatch.prdPath);
789
+ assert.equal(mismatchStatus.contextPackStatus, 'invalid');
790
+ assert.equal(mismatchStatus.declarationState, 'mismatched');
791
+ assert.equal(mismatchStatus.packState, 'invalid');
792
+ assert.ok(mismatchStatus.contextPackIssues.some((issue) => issue.includes('does not match approved plan slug')));
793
+ });
270
794
  });
271
795
  //# sourceMappingURL=context-pack-status.test.js.map