oh-my-codex 0.18.0 → 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 (410) hide show
  1. package/Cargo.lock +6 -6
  2. package/Cargo.toml +1 -1
  3. package/README.md +45 -19
  4. package/crates/omx-api/src/lib.rs +66 -9
  5. package/crates/omx-sparkshell/src/exec.rs +125 -3
  6. package/crates/omx-sparkshell/src/main.rs +126 -36
  7. package/crates/omx-sparkshell/tests/execution.rs +225 -1
  8. package/dist/agents/__tests__/definitions.test.js +14 -0
  9. package/dist/agents/__tests__/definitions.test.js.map +1 -1
  10. package/dist/agents/__tests__/native-config.test.js +19 -0
  11. package/dist/agents/__tests__/native-config.test.js.map +1 -1
  12. package/dist/agents/definitions.d.ts.map +1 -1
  13. package/dist/agents/definitions.js +30 -0
  14. package/dist/agents/definitions.js.map +1 -1
  15. package/dist/agents/native-config.d.ts +1 -0
  16. package/dist/agents/native-config.d.ts.map +1 -1
  17. package/dist/agents/native-config.js +4 -0
  18. package/dist/agents/native-config.js.map +1 -1
  19. package/dist/catalog/__tests__/generator.test.js +4 -0
  20. package/dist/catalog/__tests__/generator.test.js.map +1 -1
  21. package/dist/cli/__tests__/codex-plugin-layout.test.js +15 -7
  22. package/dist/cli/__tests__/codex-plugin-layout.test.js.map +1 -1
  23. package/dist/cli/__tests__/doctor-warning-copy.test.js +137 -8
  24. package/dist/cli/__tests__/doctor-warning-copy.test.js.map +1 -1
  25. package/dist/cli/__tests__/index.test.js +203 -15
  26. package/dist/cli/__tests__/index.test.js.map +1 -1
  27. package/dist/cli/__tests__/install-docs-contract.test.d.ts +2 -0
  28. package/dist/cli/__tests__/install-docs-contract.test.d.ts.map +1 -0
  29. package/dist/cli/__tests__/install-docs-contract.test.js +55 -0
  30. package/dist/cli/__tests__/install-docs-contract.test.js.map +1 -0
  31. package/dist/cli/__tests__/launch-fallback.test.js +163 -0
  32. package/dist/cli/__tests__/launch-fallback.test.js.map +1 -1
  33. package/dist/cli/__tests__/question.test.js +29 -43
  34. package/dist/cli/__tests__/question.test.js.map +1 -1
  35. package/dist/cli/__tests__/setup-install-mode.test.js +94 -35
  36. package/dist/cli/__tests__/setup-install-mode.test.js.map +1 -1
  37. package/dist/cli/__tests__/sparkshell-cli.test.js +20 -1
  38. package/dist/cli/__tests__/sparkshell-cli.test.js.map +1 -1
  39. package/dist/cli/__tests__/sparkshell-packaging.test.js +1 -0
  40. package/dist/cli/__tests__/sparkshell-packaging.test.js.map +1 -1
  41. package/dist/cli/__tests__/ultragoal.test.js +227 -4
  42. package/dist/cli/__tests__/ultragoal.test.js.map +1 -1
  43. package/dist/cli/__tests__/update.test.js +72 -1
  44. package/dist/cli/__tests__/update.test.js.map +1 -1
  45. package/dist/cli/codex-feature-probe.d.ts +5 -0
  46. package/dist/cli/codex-feature-probe.d.ts.map +1 -1
  47. package/dist/cli/codex-feature-probe.js +13 -7
  48. package/dist/cli/codex-feature-probe.js.map +1 -1
  49. package/dist/cli/doctor.d.ts +7 -0
  50. package/dist/cli/doctor.d.ts.map +1 -1
  51. package/dist/cli/doctor.js +297 -17
  52. package/dist/cli/doctor.js.map +1 -1
  53. package/dist/cli/index.d.ts +9 -1
  54. package/dist/cli/index.d.ts.map +1 -1
  55. package/dist/cli/index.js +465 -110
  56. package/dist/cli/index.js.map +1 -1
  57. package/dist/cli/plugin-marketplace.d.ts +2 -0
  58. package/dist/cli/plugin-marketplace.d.ts.map +1 -1
  59. package/dist/cli/plugin-marketplace.js +15 -1
  60. package/dist/cli/plugin-marketplace.js.map +1 -1
  61. package/dist/cli/setup.d.ts.map +1 -1
  62. package/dist/cli/setup.js +71 -11
  63. package/dist/cli/setup.js.map +1 -1
  64. package/dist/cli/sparkshell.d.ts +7 -1
  65. package/dist/cli/sparkshell.d.ts.map +1 -1
  66. package/dist/cli/sparkshell.js +13 -3
  67. package/dist/cli/sparkshell.js.map +1 -1
  68. package/dist/cli/ultragoal.d.ts +1 -1
  69. package/dist/cli/ultragoal.d.ts.map +1 -1
  70. package/dist/cli/ultragoal.js +184 -10
  71. package/dist/cli/ultragoal.js.map +1 -1
  72. package/dist/cli/update.d.ts +2 -0
  73. package/dist/cli/update.d.ts.map +1 -1
  74. package/dist/cli/update.js +14 -3
  75. package/dist/cli/update.js.map +1 -1
  76. package/dist/compat/__tests__/doctor-contract.test.js +3 -0
  77. package/dist/compat/__tests__/doctor-contract.test.js.map +1 -1
  78. package/dist/config/__tests__/codex-feature-flags.test.js +11 -1
  79. package/dist/config/__tests__/codex-feature-flags.test.js.map +1 -1
  80. package/dist/config/__tests__/codex-hooks.test.js +22 -11
  81. package/dist/config/__tests__/codex-hooks.test.js.map +1 -1
  82. package/dist/config/__tests__/commit-lore-guard.test.d.ts +2 -0
  83. package/dist/config/__tests__/commit-lore-guard.test.d.ts.map +1 -0
  84. package/dist/config/__tests__/commit-lore-guard.test.js +20 -0
  85. package/dist/config/__tests__/commit-lore-guard.test.js.map +1 -0
  86. package/dist/config/codex-feature-flags.d.ts +4 -0
  87. package/dist/config/codex-feature-flags.d.ts.map +1 -1
  88. package/dist/config/codex-feature-flags.js +4 -0
  89. package/dist/config/codex-feature-flags.js.map +1 -1
  90. package/dist/config/codex-hooks.d.ts +1 -0
  91. package/dist/config/codex-hooks.d.ts.map +1 -1
  92. package/dist/config/codex-hooks.js +8 -10
  93. package/dist/config/codex-hooks.js.map +1 -1
  94. package/dist/config/commit-lore-guard.d.ts +1 -0
  95. package/dist/config/commit-lore-guard.d.ts.map +1 -1
  96. package/dist/config/commit-lore-guard.js +29 -3
  97. package/dist/config/commit-lore-guard.js.map +1 -1
  98. package/dist/config/generator.d.ts +17 -1
  99. package/dist/config/generator.d.ts.map +1 -1
  100. package/dist/config/generator.js +124 -11
  101. package/dist/config/generator.js.map +1 -1
  102. package/dist/goal-workflows/__tests__/codex-goal-snapshot.test.js +21 -0
  103. package/dist/goal-workflows/__tests__/codex-goal-snapshot.test.js.map +1 -1
  104. package/dist/goal-workflows/codex-goal-snapshot.d.ts +4 -0
  105. package/dist/goal-workflows/codex-goal-snapshot.d.ts.map +1 -1
  106. package/dist/goal-workflows/codex-goal-snapshot.js +50 -3
  107. package/dist/goal-workflows/codex-goal-snapshot.js.map +1 -1
  108. package/dist/hooks/__tests__/autopilot-skill-contract.test.js +27 -6
  109. package/dist/hooks/__tests__/autopilot-skill-contract.test.js.map +1 -1
  110. package/dist/hooks/__tests__/consensus-execution-handoff.test.d.ts +1 -1
  111. package/dist/hooks/__tests__/consensus-execution-handoff.test.js +13 -11
  112. package/dist/hooks/__tests__/consensus-execution-handoff.test.js.map +1 -1
  113. package/dist/hooks/__tests__/deep-interview-contract.test.js +4 -3
  114. package/dist/hooks/__tests__/deep-interview-contract.test.js.map +1 -1
  115. package/dist/hooks/__tests__/keyword-detector.test.js +173 -17
  116. package/dist/hooks/__tests__/keyword-detector.test.js.map +1 -1
  117. package/dist/hooks/__tests__/notify-hook-team-tmux-guard.test.js +33 -0
  118. package/dist/hooks/__tests__/notify-hook-team-tmux-guard.test.js.map +1 -1
  119. package/dist/hooks/__tests__/prometheus-strict-contract.test.d.ts +2 -0
  120. package/dist/hooks/__tests__/prometheus-strict-contract.test.d.ts.map +1 -0
  121. package/dist/hooks/__tests__/prometheus-strict-contract.test.js +320 -0
  122. package/dist/hooks/__tests__/prometheus-strict-contract.test.js.map +1 -0
  123. package/dist/hooks/__tests__/prompt-guidance-wave-two.test.js +12 -0
  124. package/dist/hooks/__tests__/prompt-guidance-wave-two.test.js.map +1 -1
  125. package/dist/hooks/__tests__/research-workflow-boundaries.test.d.ts +2 -0
  126. package/dist/hooks/__tests__/research-workflow-boundaries.test.d.ts.map +1 -0
  127. package/dist/hooks/__tests__/research-workflow-boundaries.test.js +35 -0
  128. package/dist/hooks/__tests__/research-workflow-boundaries.test.js.map +1 -0
  129. package/dist/hooks/extensibility/__tests__/dispatcher.test.js +26 -3
  130. package/dist/hooks/extensibility/__tests__/dispatcher.test.js.map +1 -1
  131. package/dist/hooks/extensibility/dispatcher.d.ts.map +1 -1
  132. package/dist/hooks/extensibility/dispatcher.js +29 -14
  133. package/dist/hooks/extensibility/dispatcher.js.map +1 -1
  134. package/dist/hooks/keyword-detector.d.ts +1 -1
  135. package/dist/hooks/keyword-detector.d.ts.map +1 -1
  136. package/dist/hooks/keyword-detector.js +36 -9
  137. package/dist/hooks/keyword-detector.js.map +1 -1
  138. package/dist/hooks/keyword-registry.d.ts.map +1 -1
  139. package/dist/hooks/keyword-registry.js +1 -0
  140. package/dist/hooks/keyword-registry.js.map +1 -1
  141. package/dist/hooks/prompt-guidance-contract.d.ts.map +1 -1
  142. package/dist/hooks/prompt-guidance-contract.js +14 -2
  143. package/dist/hooks/prompt-guidance-contract.js.map +1 -1
  144. package/dist/hud/__tests__/hud-tmux-injection.test.js +36 -8
  145. package/dist/hud/__tests__/hud-tmux-injection.test.js.map +1 -1
  146. package/dist/hud/__tests__/reconcile.test.js +122 -11
  147. package/dist/hud/__tests__/reconcile.test.js.map +1 -1
  148. package/dist/hud/__tests__/render.test.js +84 -0
  149. package/dist/hud/__tests__/render.test.js.map +1 -1
  150. package/dist/hud/__tests__/resource-leak-watch.test.d.ts +2 -0
  151. package/dist/hud/__tests__/resource-leak-watch.test.d.ts.map +1 -0
  152. package/dist/hud/__tests__/resource-leak-watch.test.js +28 -0
  153. package/dist/hud/__tests__/resource-leak-watch.test.js.map +1 -0
  154. package/dist/hud/__tests__/state.test.js +51 -1
  155. package/dist/hud/__tests__/state.test.js.map +1 -1
  156. package/dist/hud/__tests__/tmux.test.js +69 -23
  157. package/dist/hud/__tests__/tmux.test.js.map +1 -1
  158. package/dist/hud/index.d.ts +2 -2
  159. package/dist/hud/index.d.ts.map +1 -1
  160. package/dist/hud/index.js +17 -6
  161. package/dist/hud/index.js.map +1 -1
  162. package/dist/hud/reconcile.d.ts.map +1 -1
  163. package/dist/hud/reconcile.js +6 -3
  164. package/dist/hud/reconcile.js.map +1 -1
  165. package/dist/hud/render.d.ts.map +1 -1
  166. package/dist/hud/render.js +26 -0
  167. package/dist/hud/render.js.map +1 -1
  168. package/dist/hud/state.d.ts +2 -1
  169. package/dist/hud/state.d.ts.map +1 -1
  170. package/dist/hud/state.js +62 -1
  171. package/dist/hud/state.js.map +1 -1
  172. package/dist/hud/tmux.d.ts +10 -3
  173. package/dist/hud/tmux.d.ts.map +1 -1
  174. package/dist/hud/tmux.js +60 -11
  175. package/dist/hud/tmux.js.map +1 -1
  176. package/dist/hud/types.d.ts +22 -0
  177. package/dist/hud/types.d.ts.map +1 -1
  178. package/dist/hud/types.js.map +1 -1
  179. package/dist/notifications/__tests__/http-client-resource.test.d.ts +2 -0
  180. package/dist/notifications/__tests__/http-client-resource.test.d.ts.map +1 -0
  181. package/dist/notifications/__tests__/http-client-resource.test.js +41 -0
  182. package/dist/notifications/__tests__/http-client-resource.test.js.map +1 -0
  183. package/dist/notifications/__tests__/verbosity.test.js +20 -0
  184. package/dist/notifications/__tests__/verbosity.test.js.map +1 -1
  185. package/dist/notifications/config.d.ts.map +1 -1
  186. package/dist/notifications/config.js +6 -3
  187. package/dist/notifications/config.js.map +1 -1
  188. package/dist/notifications/http-client.d.ts.map +1 -1
  189. package/dist/notifications/http-client.js +78 -27
  190. package/dist/notifications/http-client.js.map +1 -1
  191. package/dist/notifications/types.d.ts +2 -0
  192. package/dist/notifications/types.d.ts.map +1 -1
  193. package/dist/openclaw/__tests__/dispatcher.test.js +49 -1
  194. package/dist/openclaw/__tests__/dispatcher.test.js.map +1 -1
  195. package/dist/openclaw/dispatcher.d.ts +7 -4
  196. package/dist/openclaw/dispatcher.d.ts.map +1 -1
  197. package/dist/openclaw/dispatcher.js +32 -69
  198. package/dist/openclaw/dispatcher.js.map +1 -1
  199. package/dist/pipeline/__tests__/orchestrator.test.js +128 -4
  200. package/dist/pipeline/__tests__/orchestrator.test.js.map +1 -1
  201. package/dist/pipeline/__tests__/stages.test.js +460 -9
  202. package/dist/pipeline/__tests__/stages.test.js.map +1 -1
  203. package/dist/pipeline/index.d.ts +8 -2
  204. package/dist/pipeline/index.d.ts.map +1 -1
  205. package/dist/pipeline/index.js +5 -2
  206. package/dist/pipeline/index.js.map +1 -1
  207. package/dist/pipeline/orchestrator.d.ts +5 -4
  208. package/dist/pipeline/orchestrator.d.ts.map +1 -1
  209. package/dist/pipeline/orchestrator.js +85 -17
  210. package/dist/pipeline/orchestrator.js.map +1 -1
  211. package/dist/pipeline/stages/code-review.d.ts +2 -2
  212. package/dist/pipeline/stages/code-review.d.ts.map +1 -1
  213. package/dist/pipeline/stages/code-review.js +5 -3
  214. package/dist/pipeline/stages/code-review.js.map +1 -1
  215. package/dist/pipeline/stages/deep-interview.d.ts +15 -0
  216. package/dist/pipeline/stages/deep-interview.d.ts.map +1 -0
  217. package/dist/pipeline/stages/deep-interview.js +32 -0
  218. package/dist/pipeline/stages/deep-interview.js.map +1 -0
  219. package/dist/pipeline/stages/ralph-verify.d.ts +5 -5
  220. package/dist/pipeline/stages/ralph-verify.d.ts.map +1 -1
  221. package/dist/pipeline/stages/ralph-verify.js +2 -2
  222. package/dist/pipeline/stages/ralph-verify.js.map +1 -1
  223. package/dist/pipeline/stages/ralplan.d.ts.map +1 -1
  224. package/dist/pipeline/stages/ralplan.js +41 -6
  225. package/dist/pipeline/stages/ralplan.js.map +1 -1
  226. package/dist/pipeline/stages/ultragoal.d.ts +19 -0
  227. package/dist/pipeline/stages/ultragoal.d.ts.map +1 -0
  228. package/dist/pipeline/stages/ultragoal.js +38 -0
  229. package/dist/pipeline/stages/ultragoal.js.map +1 -0
  230. package/dist/pipeline/stages/ultraqa.d.ts +30 -0
  231. package/dist/pipeline/stages/ultraqa.d.ts.map +1 -0
  232. package/dist/pipeline/stages/ultraqa.js +46 -0
  233. package/dist/pipeline/stages/ultraqa.js.map +1 -0
  234. package/dist/pipeline/types.d.ts +8 -6
  235. package/dist/pipeline/types.d.ts.map +1 -1
  236. package/dist/pipeline/types.js +2 -2
  237. package/dist/question/__tests__/ui.test.js +43 -10
  238. package/dist/question/__tests__/ui.test.js.map +1 -1
  239. package/dist/question/ui.d.ts +12 -0
  240. package/dist/question/ui.d.ts.map +1 -1
  241. package/dist/question/ui.js +83 -46
  242. package/dist/question/ui.js.map +1 -1
  243. package/dist/ralplan/__tests__/runtime.test.js +200 -10
  244. package/dist/ralplan/__tests__/runtime.test.js.map +1 -1
  245. package/dist/ralplan/consensus-gate.d.ts +23 -0
  246. package/dist/ralplan/consensus-gate.d.ts.map +1 -0
  247. package/dist/ralplan/consensus-gate.js +212 -0
  248. package/dist/ralplan/consensus-gate.js.map +1 -0
  249. package/dist/ralplan/runtime.d.ts +25 -0
  250. package/dist/ralplan/runtime.d.ts.map +1 -1
  251. package/dist/ralplan/runtime.js +144 -8
  252. package/dist/ralplan/runtime.js.map +1 -1
  253. package/dist/scripts/__tests__/codex-native-hook.test.js +1358 -79
  254. package/dist/scripts/__tests__/codex-native-hook.test.js.map +1 -1
  255. package/dist/scripts/__tests__/docs-site-contract.test.d.ts +2 -0
  256. package/dist/scripts/__tests__/docs-site-contract.test.d.ts.map +1 -0
  257. package/dist/scripts/__tests__/docs-site-contract.test.js +42 -0
  258. package/dist/scripts/__tests__/docs-site-contract.test.js.map +1 -0
  259. package/dist/scripts/__tests__/notify-dispatcher.test.js +115 -2
  260. package/dist/scripts/__tests__/notify-dispatcher.test.js.map +1 -1
  261. package/dist/scripts/__tests__/run-test-files.test.js +57 -0
  262. package/dist/scripts/__tests__/run-test-files.test.js.map +1 -1
  263. package/dist/scripts/__tests__/smoke-packed-install.test.js +23 -1
  264. package/dist/scripts/__tests__/smoke-packed-install.test.js.map +1 -1
  265. package/dist/scripts/__tests__/verify-native-agents.test.js +18 -3
  266. package/dist/scripts/__tests__/verify-native-agents.test.js.map +1 -1
  267. package/dist/scripts/cleanup-explore-harness.js +1 -0
  268. package/dist/scripts/cleanup-explore-harness.js.map +1 -1
  269. package/dist/scripts/codex-native-hook.d.ts.map +1 -1
  270. package/dist/scripts/codex-native-hook.js +372 -44
  271. package/dist/scripts/codex-native-hook.js.map +1 -1
  272. package/dist/scripts/codex-native-pre-post.d.ts.map +1 -1
  273. package/dist/scripts/codex-native-pre-post.js +9 -1
  274. package/dist/scripts/codex-native-pre-post.js.map +1 -1
  275. package/dist/scripts/notify-dispatcher.js +188 -4
  276. package/dist/scripts/notify-dispatcher.js.map +1 -1
  277. package/dist/scripts/notify-hook/process-runner.d.ts.map +1 -1
  278. package/dist/scripts/notify-hook/process-runner.js +39 -17
  279. package/dist/scripts/notify-hook/process-runner.js.map +1 -1
  280. package/dist/scripts/notify-hook/team-dispatch.d.ts.map +1 -1
  281. package/dist/scripts/notify-hook/team-dispatch.js +9 -5
  282. package/dist/scripts/notify-hook/team-dispatch.js.map +1 -1
  283. package/dist/scripts/notify-hook/team-tmux-guard.d.ts +1 -1
  284. package/dist/scripts/notify-hook/team-tmux-guard.d.ts.map +1 -1
  285. package/dist/scripts/notify-hook/team-tmux-guard.js +7 -1
  286. package/dist/scripts/notify-hook/team-tmux-guard.js.map +1 -1
  287. package/dist/scripts/run-test-files.js +13 -0
  288. package/dist/scripts/run-test-files.js.map +1 -1
  289. package/dist/scripts/smoke-packed-install.d.ts +3 -0
  290. package/dist/scripts/smoke-packed-install.d.ts.map +1 -1
  291. package/dist/scripts/smoke-packed-install.js +99 -1
  292. package/dist/scripts/smoke-packed-install.js.map +1 -1
  293. package/dist/scripts/sync-plugin-mirror.js +2 -2
  294. package/dist/scripts/sync-plugin-mirror.js.map +1 -1
  295. package/dist/scripts/verify-native-agents.js +2 -2
  296. package/dist/scripts/verify-native-agents.js.map +1 -1
  297. package/dist/sidecar/__tests__/resource-leak-watch.test.d.ts +2 -0
  298. package/dist/sidecar/__tests__/resource-leak-watch.test.d.ts.map +1 -0
  299. package/dist/sidecar/__tests__/resource-leak-watch.test.js +38 -0
  300. package/dist/sidecar/__tests__/resource-leak-watch.test.js.map +1 -0
  301. package/dist/sidecar/index.d.ts +1 -1
  302. package/dist/sidecar/index.d.ts.map +1 -1
  303. package/dist/sidecar/index.js +29 -12
  304. package/dist/sidecar/index.js.map +1 -1
  305. package/dist/state/__tests__/operations-ralph-phase.test.js +88 -1
  306. package/dist/state/__tests__/operations-ralph-phase.test.js.map +1 -1
  307. package/dist/state/__tests__/workflow-transition.test.js +6 -0
  308. package/dist/state/__tests__/workflow-transition.test.js.map +1 -1
  309. package/dist/state/operations.d.ts.map +1 -1
  310. package/dist/state/operations.js +11 -0
  311. package/dist/state/operations.js.map +1 -1
  312. package/dist/state/workflow-transition.d.ts +1 -1
  313. package/dist/state/workflow-transition.d.ts.map +1 -1
  314. package/dist/state/workflow-transition.js +7 -0
  315. package/dist/state/workflow-transition.js.map +1 -1
  316. package/dist/subagents/tracker.d.ts.map +1 -1
  317. package/dist/subagents/tracker.js +4 -3
  318. package/dist/subagents/tracker.js.map +1 -1
  319. package/dist/team/__tests__/runtime.test.js +36 -44
  320. package/dist/team/__tests__/runtime.test.js.map +1 -1
  321. package/dist/team/__tests__/tmux-session.test.js +163 -15
  322. package/dist/team/__tests__/tmux-session.test.js.map +1 -1
  323. package/dist/team/runtime.d.ts.map +1 -1
  324. package/dist/team/runtime.js +10 -20
  325. package/dist/team/runtime.js.map +1 -1
  326. package/dist/team/tmux-session.d.ts.map +1 -1
  327. package/dist/team/tmux-session.js +51 -21
  328. package/dist/team/tmux-session.js.map +1 -1
  329. package/dist/ultragoal/__tests__/artifacts.test.js +764 -10
  330. package/dist/ultragoal/__tests__/artifacts.test.js.map +1 -1
  331. package/dist/ultragoal/__tests__/docs-contract.test.js +57 -1
  332. package/dist/ultragoal/__tests__/docs-contract.test.js.map +1 -1
  333. package/dist/ultragoal/__tests__/steering-fixtures.d.ts +68 -0
  334. package/dist/ultragoal/__tests__/steering-fixtures.d.ts.map +1 -0
  335. package/dist/ultragoal/__tests__/steering-fixtures.js +259 -0
  336. package/dist/ultragoal/__tests__/steering-fixtures.js.map +1 -0
  337. package/dist/ultragoal/__tests__/steering-fixtures.test.d.ts +2 -0
  338. package/dist/ultragoal/__tests__/steering-fixtures.test.d.ts.map +1 -0
  339. package/dist/ultragoal/__tests__/steering-fixtures.test.js +65 -0
  340. package/dist/ultragoal/__tests__/steering-fixtures.test.js.map +1 -0
  341. package/dist/ultragoal/artifacts.d.ts +97 -2
  342. package/dist/ultragoal/artifacts.d.ts.map +1 -1
  343. package/dist/ultragoal/artifacts.js +837 -256
  344. package/dist/ultragoal/artifacts.js.map +1 -1
  345. package/dist/utils/__tests__/sleep-resource.test.d.ts +2 -0
  346. package/dist/utils/__tests__/sleep-resource.test.d.ts.map +1 -0
  347. package/dist/utils/__tests__/sleep-resource.test.js +39 -0
  348. package/dist/utils/__tests__/sleep-resource.test.js.map +1 -0
  349. package/dist/utils/sleep.d.ts.map +1 -1
  350. package/dist/utils/sleep.js +17 -6
  351. package/dist/utils/sleep.js.map +1 -1
  352. package/package.json +2 -1
  353. package/plugins/oh-my-codex/.codex-plugin/plugin.json +4 -3
  354. package/plugins/oh-my-codex/hooks/codex-native-hook.mjs +56 -0
  355. package/plugins/oh-my-codex/hooks/hooks.json +77 -0
  356. package/plugins/oh-my-codex/skills/autopilot/SKILL.md +92 -50
  357. package/plugins/oh-my-codex/skills/autoresearch/SKILL.md +4 -0
  358. package/plugins/oh-my-codex/skills/autoresearch-goal/SKILL.md +1 -1
  359. package/plugins/oh-my-codex/skills/best-practice-research/SKILL.md +1 -1
  360. package/plugins/oh-my-codex/skills/cancel/SKILL.md +2 -2
  361. package/plugins/oh-my-codex/skills/deep-interview/SKILL.md +8 -8
  362. package/plugins/oh-my-codex/skills/omx-setup/SKILL.md +1 -1
  363. package/plugins/oh-my-codex/skills/pipeline/SKILL.md +23 -12
  364. package/plugins/oh-my-codex/skills/plan/SKILL.md +8 -8
  365. package/plugins/oh-my-codex/skills/prometheus-strict/README.md +35 -0
  366. package/plugins/oh-my-codex/skills/prometheus-strict/SKILL.md +219 -0
  367. package/plugins/oh-my-codex/skills/ralph/SKILL.md +7 -0
  368. package/plugins/oh-my-codex/skills/ralplan/SKILL.md +22 -7
  369. package/plugins/oh-my-codex/skills/team/SKILL.md +1 -1
  370. package/plugins/oh-my-codex/skills/ultragoal/SKILL.md +38 -4
  371. package/plugins/oh-my-codex/skills/ultrawork/SKILL.md +1 -1
  372. package/prompts/planner.md +1 -1
  373. package/prompts/prometheus-strict-metis.md +274 -0
  374. package/prompts/prometheus-strict-momus.md +82 -0
  375. package/prompts/prometheus-strict-oracle.md +107 -0
  376. package/prompts/researcher.md +22 -3
  377. package/skills/autopilot/SKILL.md +92 -50
  378. package/skills/autoresearch/SKILL.md +4 -0
  379. package/skills/autoresearch-goal/SKILL.md +1 -1
  380. package/skills/best-practice-research/SKILL.md +1 -1
  381. package/skills/cancel/SKILL.md +2 -2
  382. package/skills/deep-interview/SKILL.md +8 -8
  383. package/skills/omx-setup/SKILL.md +1 -1
  384. package/skills/pipeline/SKILL.md +23 -12
  385. package/skills/plan/SKILL.md +8 -8
  386. package/skills/prometheus-strict/README.md +35 -0
  387. package/skills/prometheus-strict/SKILL.md +219 -0
  388. package/skills/ralph/SKILL.md +7 -0
  389. package/skills/ralplan/SKILL.md +22 -7
  390. package/skills/team/SKILL.md +1 -1
  391. package/skills/ultragoal/SKILL.md +38 -4
  392. package/skills/ultrawork/SKILL.md +1 -1
  393. package/src/scripts/__tests__/codex-native-hook.test.ts +1757 -210
  394. package/src/scripts/__tests__/docs-site-contract.test.ts +47 -0
  395. package/src/scripts/__tests__/notify-dispatcher.test.ts +132 -3
  396. package/src/scripts/__tests__/run-test-files.test.ts +67 -0
  397. package/src/scripts/__tests__/smoke-packed-install.test.ts +31 -0
  398. package/src/scripts/__tests__/verify-native-agents.test.ts +23 -3
  399. package/src/scripts/cleanup-explore-harness.ts +1 -0
  400. package/src/scripts/codex-native-hook.ts +393 -40
  401. package/src/scripts/codex-native-pre-post.ts +16 -1
  402. package/src/scripts/notify-dispatcher.ts +202 -4
  403. package/src/scripts/notify-hook/process-runner.ts +40 -16
  404. package/src/scripts/notify-hook/team-dispatch.ts +9 -5
  405. package/src/scripts/notify-hook/team-tmux-guard.ts +7 -0
  406. package/src/scripts/run-test-files.ts +13 -0
  407. package/src/scripts/smoke-packed-install.ts +105 -0
  408. package/src/scripts/sync-plugin-mirror.ts +3 -3
  409. package/src/scripts/verify-native-agents.ts +2 -2
  410. package/templates/catalog-manifest.json +22 -0
@@ -1,11 +1,25 @@
1
1
  import { existsSync } from 'node:fs';
2
- import { appendFile, mkdir, readFile, writeFile } from 'node:fs/promises';
2
+ import { appendFile, mkdir, open, readFile, rename, rm, writeFile } from 'node:fs/promises';
3
3
  import { join, relative } from 'node:path';
4
4
  import { formatCodexGoalReconciliation, parseCodexGoalSnapshot, reconcileCodexGoalSnapshot, } from '../goal-workflows/codex-goal-snapshot.js';
5
5
  export const ULTRAGOAL_DIR = '.omx/ultragoal';
6
6
  export const ULTRAGOAL_BRIEF = 'brief.md';
7
7
  export const ULTRAGOAL_GOALS = 'goals.json';
8
8
  export const ULTRAGOAL_LEDGER = 'ledger.jsonl';
9
+ const ULTRAGOAL_MUTATION_LOCK = '.mutation.lock';
10
+ export const ULTRAGOAL_STEERING_MUTATION_KINDS = [
11
+ 'add_subgoal',
12
+ 'split_subgoal',
13
+ 'reorder_pending',
14
+ 'revise_pending_wording',
15
+ 'annotate_ledger',
16
+ 'mark_blocked_superseded',
17
+ ];
18
+ export const ULTRAGOAL_STEERING_SOURCES = [
19
+ 'user_prompt_submit',
20
+ 'finding',
21
+ 'cli',
22
+ ];
9
23
  export class UltragoalError extends Error {
10
24
  }
11
25
  function iso(now = new Date()) {
@@ -32,6 +46,51 @@ function cleanLine(line) {
32
46
  function normalizeObjective(value) {
33
47
  return value.replace(/\s+/g, ' ').trim();
34
48
  }
49
+ function normalizeBlockerEvidence(value) {
50
+ return (value ?? '')
51
+ .toLowerCase()
52
+ .replace(/https?:\/\/\S+/g, ' ')
53
+ .replace(/[`"'()[\]{}:,;]/g, ' ')
54
+ .replace(/\s+/g, ' ')
55
+ .trim();
56
+ }
57
+ function classifyExternalAuthorizationBlocker(evidence) {
58
+ const normalized = normalizeBlockerEvidence(evidence);
59
+ if (!normalized)
60
+ return null;
61
+ const mentionsAuthorization = /\b(auth|authorization|credential|credentials|token|permission|permissions|scope|scopes|access|unauthorized|forbidden|401|403)\b/.test(normalized);
62
+ const mentionsMissingAuthority = /\b(unset|missing|required|requires|without|omit|omits|not set|not available|no read packages|read packages)\b/.test(normalized);
63
+ if (!mentionsAuthorization || !mentionsMissingAuthority)
64
+ return null;
65
+ const mentionsGhcr = /\b(ghcr|github container registry|read packages|imagepullsecret|package api|anonymous image|container image)\b/.test(normalized);
66
+ if (mentionsGhcr) {
67
+ const has401 = /\b(401|unauthorized|anonymous pull|authentication required)\b/.test(normalized);
68
+ const has403 = /\b(403|forbidden|read packages|package api)\b/.test(normalized);
69
+ const status = [has401 ? 'HTTP_401_ANONYMOUS' : null, has403 ? 'HTTP_403_NO_READ_PACKAGES' : null]
70
+ .filter((part) => Boolean(part))
71
+ .join('+') || 'AUTHORIZATION_REQUIRED';
72
+ return {
73
+ signature: `GHCR_PULL_ACCESS:${status}:GHCR_VISIBILITY_OR_CREDENTIAL_REQUIRED`,
74
+ requiredDecision: 'make the GHCR package public, or provide/authorize a least-privilege read:packages credential and imagePullSecret/SOPS path',
75
+ };
76
+ }
77
+ return {
78
+ signature: 'EXTERNAL_AUTHORIZATION_REQUIRED',
79
+ requiredDecision: 'provide the missing external authorization/credential, or explicitly choose a different unblock path',
80
+ };
81
+ }
82
+ function sameBlockerOccurrences(entries, goalId, signature) {
83
+ return entries.filter((entry) => (entry.goalId === goalId
84
+ && (entry.event === 'goal_failed' || entry.event === 'goal_needs_user_decision')
85
+ && entry.blockerSignature === signature)).length;
86
+ }
87
+ function clearGoalBlockerFields(goal) {
88
+ goal.blockedReason = undefined;
89
+ goal.blockerSignature = undefined;
90
+ goal.blockerOccurrenceCount = undefined;
91
+ goal.requiredExternalDecision = undefined;
92
+ goal.nonRetriable = undefined;
93
+ }
35
94
  function textMentionsUltragoalPlanArtifact(value) {
36
95
  const normalized = (value ?? '').toLowerCase();
37
96
  return normalized.includes(ULTRAGOAL_DIR.toLowerCase())
@@ -82,7 +141,14 @@ function buildCompletedLegacyGoalRemediation(goal) {
82
141
  return [
83
142
  'If get_goal returns a different completed legacy/thread objective, do not repeat --status complete in this thread.',
84
143
  `Record a non-terminal blocker with: omx ultragoal checkpoint --goal-id ${goal.id} --status blocked --evidence "<completed legacy Codex goal blocks create_goal in this thread>" --codex-goal-json "<different completed get_goal JSON or path>".`,
85
- 'Then continue this ultragoal in a fresh Codex thread in the same repo/worktree and create the intended goal there.',
144
+ 'Then continue only from a Codex goal context with no active/completed conflicting goal, in the same repo/worktree, and create the intended goal there.',
145
+ ].join(' ');
146
+ }
147
+ function buildUnavailableCodexGoalRemediation(goal) {
148
+ return [
149
+ 'If get_goal itself is unavailable due to a Codex DB/schema/context error, such as "no such table: thread_goals", do not repeat --status complete or mark the Codex goal complete from shell state.',
150
+ `Record an auditable non-terminal blocker with: omx ultragoal checkpoint --goal-id ${goal.id} --status blocked --evidence "<get_goal unavailable due to Codex DB/schema/context error; safe recovery requires a working Codex goal context>" --codex-goal-json "<unavailable get_goal error JSON or path>".`,
151
+ 'Then continue from a Codex goal context where get_goal works and strict completion reconciliation can be proven.',
86
152
  ].join(' ');
87
153
  }
88
154
  function codexGoalMode(plan) {
@@ -91,35 +157,78 @@ function codexGoalMode(plan) {
91
157
  function isResolvedStatus(status) {
92
158
  return status === 'complete' || status === 'review_blocked';
93
159
  }
94
- function aggregateCodexObjective(goals) {
95
- const prefix = `Complete all ultragoal stories in ${ULTRAGOAL_DIR}/${ULTRAGOAL_GOALS}: `;
96
- const suffix = goals.map((goal) => `${goal.id} ${goal.title}`).join('; ');
97
- const full = `${prefix}${suffix}`;
98
- if (full.length <= 4000)
99
- return full;
100
- const fallback = `Complete all ultragoal stories listed in ${ULTRAGOAL_DIR}/${ULTRAGOAL_GOALS}. Use ${ULTRAGOAL_DIR}/${ULTRAGOAL_LEDGER} as the durable audit trail.`;
101
- if (fallback.length <= 4000)
102
- return fallback;
160
+ function isScheduleEligibleGoal(goal) {
161
+ return goal.steeringStatus !== 'superseded' && goal.steeringStatus !== 'blocked';
162
+ }
163
+ export const ULTRAGOAL_AGGREGATE_CODEX_OBJECTIVE = `Complete the durable ultragoal plan in ${ULTRAGOAL_DIR}/${ULTRAGOAL_GOALS}, including later accepted/appended stories, under the original brief constraints; use ${ULTRAGOAL_DIR}/${ULTRAGOAL_LEDGER} as the audit trail.`;
164
+ function aggregateCodexObjective(_goals) {
165
+ if (ULTRAGOAL_AGGREGATE_CODEX_OBJECTIVE.length <= 4000)
166
+ return ULTRAGOAL_AGGREGATE_CODEX_OBJECTIVE;
103
167
  throw new UltragoalError('Generated aggregate Codex objective exceeds the 4,000 character goal limit.');
104
168
  }
169
+ function isLegacyEnumeratedAggregateObjective(objective) {
170
+ if (!objective)
171
+ return false;
172
+ return (objective.startsWith(`Complete all ultragoal stories in ${ULTRAGOAL_DIR}/${ULTRAGOAL_GOALS}: `)
173
+ || objective === `Complete all ultragoal stories listed in ${ULTRAGOAL_DIR}/${ULTRAGOAL_GOALS}. Use ${ULTRAGOAL_DIR}/${ULTRAGOAL_LEDGER} as the durable audit trail.`);
174
+ }
175
+ function compatibleCodexObjectives(plan) {
176
+ return (plan.codexObjectiveAliases ?? [])
177
+ .filter((objective) => isLegacyEnumeratedAggregateObjective(objective));
178
+ }
105
179
  function expectedCodexObjective(plan, goal) {
106
180
  return codexGoalMode(plan) === 'aggregate'
107
181
  ? (plan.codexObjective ?? aggregateCodexObjective(plan.goals))
108
182
  : goal.objective;
109
183
  }
184
+ function isSupersededResolved(goal, plan) {
185
+ if (goal.steeringStatus !== 'superseded')
186
+ return false;
187
+ const replacements = goal.supersededBy ?? [];
188
+ if (replacements.length === 0)
189
+ return false;
190
+ return replacements.every((id) => {
191
+ const replacement = plan.goals.find((candidate) => candidate.id === id);
192
+ return replacement !== undefined && isResolvedStatus(replacement.status);
193
+ });
194
+ }
195
+ function isCompletionBlocking(goal, plan) {
196
+ if (goal.steeringStatus === 'superseded')
197
+ return !isSupersededResolved(goal, plan);
198
+ if (goal.steeringStatus === 'blocked')
199
+ return true;
200
+ return !isResolvedStatus(goal.status);
201
+ }
202
+ function isCompletionBlockingForFinalCandidate(candidate, finalCandidate, plan) {
203
+ if (candidate.id === finalCandidate.id)
204
+ return false;
205
+ if (candidate.steeringStatus === 'superseded') {
206
+ const replacements = candidate.supersededBy ?? [];
207
+ if (replacements.length === 0)
208
+ return true;
209
+ return !replacements.every((id) => {
210
+ if (id === finalCandidate.id)
211
+ return true;
212
+ const replacement = plan.goals.find((goal) => goal.id === id);
213
+ return replacement !== undefined && isResolvedStatus(replacement.status);
214
+ });
215
+ }
216
+ return isCompletionBlocking(candidate, plan);
217
+ }
218
+ function isScheduleEligible(goal) {
219
+ return goal.steeringStatus !== 'superseded' && goal.steeringStatus !== 'blocked';
220
+ }
110
221
  export function isFinalRunCompletionCandidate(plan, goal) {
111
- return plan.goals.every((candidate) => candidate.id === goal.id || isResolvedStatus(candidate.status));
222
+ return plan.goals.every((candidate) => !isCompletionBlockingForFinalCandidate(candidate, goal, plan));
112
223
  }
113
224
  export function isUltragoalDone(plan) {
114
225
  if (plan.aggregateCompletion?.status === 'complete')
115
226
  return true;
116
227
  if (plan.goals.length === 0)
117
228
  return true;
118
- if (plan.goals.some((goal) => goal.status === 'pending' || goal.status === 'in_progress' || goal.status === 'failed'))
229
+ if (plan.goals.some((goal) => isCompletionBlocking(goal, plan)))
119
230
  return false;
120
- if (!plan.goals.every((goal) => isResolvedStatus(goal.status)))
121
- return false;
122
- const latestNonReviewBlocked = [...plan.goals].reverse().find((goal) => goal.status !== 'review_blocked');
231
+ const latestNonReviewBlocked = [...plan.goals].reverse().find((goal) => goal.status !== 'review_blocked' && goal.steeringStatus !== 'superseded');
123
232
  return latestNonReviewBlocked?.status === 'complete';
124
233
  }
125
234
  function titleFromObjective(objective, fallback) {
@@ -155,6 +264,37 @@ function normalizeGoalId(title, index) {
155
264
  .replace(/-+$/g, '');
156
265
  return `G${String(index + 1).padStart(3, '0')}${slug ? `-${slug}` : ''}`;
157
266
  }
267
+ function sleep(ms) {
268
+ return new Promise((resolve) => setTimeout(resolve, ms));
269
+ }
270
+ async function withUltragoalMutationLock(cwd, operation) {
271
+ await mkdir(ultragoalDir(cwd), { recursive: true });
272
+ const lockPath = join(ultragoalDir(cwd), ULTRAGOAL_MUTATION_LOCK);
273
+ let handle;
274
+ for (let attempt = 0; attempt < 100; attempt += 1) {
275
+ try {
276
+ handle = await open(lockPath, 'wx');
277
+ await handle.writeFile(JSON.stringify({ pid: process.pid, createdAt: iso() }));
278
+ break;
279
+ }
280
+ catch (error) {
281
+ const code = error.code;
282
+ if (code !== 'EEXIST')
283
+ throw error;
284
+ await sleep(Math.min(25 + attempt * 5, 250));
285
+ }
286
+ }
287
+ if (!handle) {
288
+ throw new UltragoalError(`Timed out waiting for ultragoal mutation lock at ${repoRelative(cwd, lockPath)}.`);
289
+ }
290
+ try {
291
+ return await operation();
292
+ }
293
+ finally {
294
+ await handle.close().catch(() => undefined);
295
+ await rm(lockPath, { force: true }).catch(() => undefined);
296
+ }
297
+ }
158
298
  async function appendLedger(cwd, entry) {
159
299
  await mkdir(ultragoalDir(cwd), { recursive: true });
160
300
  const path = ultragoalLedgerPath(cwd);
@@ -173,49 +313,69 @@ export async function readUltragoalPlan(cwd) {
173
313
  if (parsed.version !== 1 || !Array.isArray(parsed.goals)) {
174
314
  throw new UltragoalError(`Invalid ultragoal plan at ${repoRelative(cwd, path)}.`);
175
315
  }
316
+ if (codexGoalMode(parsed) === 'aggregate' && isLegacyEnumeratedAggregateObjective(parsed.codexObjective)) {
317
+ const previousObjective = parsed.codexObjective;
318
+ const now = iso();
319
+ parsed.codexObjective = aggregateCodexObjective(parsed.goals);
320
+ parsed.codexObjectiveAliases = Array.from(new Set([...(parsed.codexObjectiveAliases ?? []), previousObjective].filter((value) => typeof value === 'string' && value.length > 0)));
321
+ parsed.updatedAt = now;
322
+ await writePlan(cwd, parsed);
323
+ await appendLedger(cwd, {
324
+ ts: now,
325
+ event: 'aggregate_objective_migrated',
326
+ message: 'Migrated legacy enumerated aggregate Codex objective to the stable pointer objective.',
327
+ before: { codexObjective: previousObjective },
328
+ after: { codexObjective: parsed.codexObjective },
329
+ });
330
+ }
176
331
  return parsed;
177
332
  }
178
333
  async function writePlan(cwd, plan) {
179
334
  await mkdir(ultragoalDir(cwd), { recursive: true });
180
- await writeFile(ultragoalGoalsPath(cwd), `${JSON.stringify(plan, null, 2)}\n`);
335
+ const path = ultragoalGoalsPath(cwd);
336
+ const tmpPath = `${path}.${process.pid}.${Date.now()}.tmp`;
337
+ await writeFile(tmpPath, `${JSON.stringify(plan, null, 2)}\n`);
338
+ await rename(tmpPath, path);
181
339
  }
182
340
  export async function createUltragoalPlan(cwd, options) {
183
- if (!options.force && existsSync(ultragoalGoalsPath(cwd))) {
184
- throw new UltragoalError(`Refusing to overwrite existing ${ULTRAGOAL_DIR}/${ULTRAGOAL_GOALS}; pass --force to recreate it.`);
185
- }
186
- const now = iso(options.now);
187
- const sourceGoals = options.goals?.length
188
- ? options.goals
189
- : deriveGoalCandidates(options.brief);
190
- const candidates = sourceGoals
191
- .map((goal, index) => ({
192
- id: normalizeGoalId(goal.title ?? titleFromObjective(goal.objective, `Goal ${index + 1}`), index),
193
- title: goal.title ?? titleFromObjective(goal.objective, `Goal ${index + 1}`),
194
- objective: goal.objective.trim(),
195
- status: 'pending',
196
- tokenBudget: goal.tokenBudget,
197
- attempt: 0,
198
- createdAt: now,
199
- updatedAt: now,
200
- }));
201
- const plan = {
202
- version: 1,
203
- createdAt: now,
204
- updatedAt: now,
205
- briefPath: `${ULTRAGOAL_DIR}/${ULTRAGOAL_BRIEF}`,
206
- goalsPath: `${ULTRAGOAL_DIR}/${ULTRAGOAL_GOALS}`,
207
- ledgerPath: `${ULTRAGOAL_DIR}/${ULTRAGOAL_LEDGER}`,
208
- codexGoalMode: options.codexGoalMode ?? 'aggregate',
209
- goals: candidates,
210
- };
211
- if (plan.codexGoalMode === 'aggregate')
212
- plan.codexObjective = aggregateCodexObjective(candidates);
213
- await mkdir(ultragoalDir(cwd), { recursive: true });
214
- await writeFile(ultragoalBriefPath(cwd), options.brief.endsWith('\n') ? options.brief : `${options.brief}\n`);
215
- await writePlan(cwd, plan);
216
- await writeFile(ultragoalLedgerPath(cwd), '');
217
- await appendLedger(cwd, { ts: now, event: 'plan_created', message: `${candidates.length} goal(s) created` });
218
- return plan;
341
+ return withUltragoalMutationLock(cwd, async () => {
342
+ if (!options.force && existsSync(ultragoalGoalsPath(cwd))) {
343
+ throw new UltragoalError(`Refusing to overwrite existing ${ULTRAGOAL_DIR}/${ULTRAGOAL_GOALS}; pass --force to recreate it.`);
344
+ }
345
+ const now = iso(options.now);
346
+ const sourceGoals = options.goals?.length
347
+ ? options.goals
348
+ : deriveGoalCandidates(options.brief);
349
+ const candidates = sourceGoals
350
+ .map((goal, index) => ({
351
+ id: normalizeGoalId(goal.title ?? titleFromObjective(goal.objective, `Goal ${index + 1}`), index),
352
+ title: goal.title ?? titleFromObjective(goal.objective, `Goal ${index + 1}`),
353
+ objective: goal.objective.trim(),
354
+ status: 'pending',
355
+ tokenBudget: goal.tokenBudget,
356
+ attempt: 0,
357
+ createdAt: now,
358
+ updatedAt: now,
359
+ }));
360
+ const plan = {
361
+ version: 1,
362
+ createdAt: now,
363
+ updatedAt: now,
364
+ briefPath: `${ULTRAGOAL_DIR}/${ULTRAGOAL_BRIEF}`,
365
+ goalsPath: `${ULTRAGOAL_DIR}/${ULTRAGOAL_GOALS}`,
366
+ ledgerPath: `${ULTRAGOAL_DIR}/${ULTRAGOAL_LEDGER}`,
367
+ codexGoalMode: options.codexGoalMode ?? 'aggregate',
368
+ goals: candidates,
369
+ };
370
+ if (plan.codexGoalMode === 'aggregate')
371
+ plan.codexObjective = aggregateCodexObjective(candidates);
372
+ await mkdir(ultragoalDir(cwd), { recursive: true });
373
+ await writeFile(ultragoalBriefPath(cwd), options.brief.endsWith('\n') ? options.brief : `${options.brief}\n`);
374
+ await writePlan(cwd, plan);
375
+ await writeFile(ultragoalLedgerPath(cwd), '');
376
+ await appendLedger(cwd, { ts: now, event: 'plan_created', message: `${candidates.length} goal(s) created` });
377
+ return plan;
378
+ });
219
379
  }
220
380
  export function summarizeUltragoalPlan(plan) {
221
381
  return {
@@ -225,6 +385,9 @@ export function summarizeUltragoalPlan(plan) {
225
385
  complete: plan.goals.filter((goal) => goal.status === 'complete').length,
226
386
  failed: plan.goals.filter((goal) => goal.status === 'failed').length,
227
387
  reviewBlocked: plan.goals.filter((goal) => goal.status === 'review_blocked').length,
388
+ needsUserDecision: plan.goals.filter((goal) => goal.status === 'needs_user_decision').length,
389
+ superseded: plan.goals.filter((goal) => goal.steeringStatus === 'superseded').length,
390
+ steeringBlocked: plan.goals.filter((goal) => goal.steeringStatus === 'blocked').length,
228
391
  aggregateComplete: plan.aggregateCompletion?.status === 'complete',
229
392
  activeGoalId: plan.activeGoalId,
230
393
  };
@@ -235,7 +398,34 @@ function assertNonEmpty(value, label) {
235
398
  throw new UltragoalError(`Missing ${label}.`);
236
399
  return trimmed;
237
400
  }
238
- function appendGoalToPlan(plan, options, now) {
401
+ export function parseUltragoalSteeringDirective(raw) {
402
+ const trimmed = raw.trim();
403
+ if (!trimmed || trimmed.length < 5)
404
+ return null;
405
+ try {
406
+ const parsed = JSON.parse(trimmed);
407
+ if (!parsed || typeof parsed !== 'object')
408
+ return null;
409
+ if (!parsed.kind || typeof parsed.kind !== 'string')
410
+ return null;
411
+ if (!parsed.source || typeof parsed.source !== 'string')
412
+ return null;
413
+ if (!parsed.evidence || typeof parsed.evidence !== 'string')
414
+ return null;
415
+ if (!parsed.rationale || typeof parsed.rationale !== 'string')
416
+ return null;
417
+ if (!ULTRAGOAL_STEERING_MUTATION_KINDS.includes(parsed.kind))
418
+ return null;
419
+ if (!ULTRAGOAL_STEERING_SOURCES.includes(parsed.source))
420
+ return null;
421
+ return parsed;
422
+ }
423
+ catch {
424
+ return null;
425
+ }
426
+ }
427
+ function appendGoalToPlan(plan, options, nowOverride) {
428
+ const now = nowOverride ?? iso(options.now);
239
429
  const title = assertNonEmpty(options.title, '--title');
240
430
  const objective = assertNonEmpty(options.objective, '--objective');
241
431
  const goal = {
@@ -253,19 +443,360 @@ function appendGoalToPlan(plan, options, now) {
253
443
  return goal;
254
444
  }
255
445
  export async function addUltragoalGoal(cwd, options) {
256
- const plan = await readUltragoalPlan(cwd);
257
- const now = iso(options.now);
258
- const goal = appendGoalToPlan(plan, options, now);
259
- await writePlan(cwd, plan);
260
- await appendLedger(cwd, {
261
- ts: now,
262
- event: 'goal_added',
263
- goalId: goal.id,
264
- status: goal.status,
265
- evidence: options.evidence,
266
- message: goal.title,
446
+ return withUltragoalMutationLock(cwd, async () => {
447
+ const plan = await readUltragoalPlan(cwd);
448
+ const now = iso(options.now);
449
+ const goal = appendGoalToPlan(plan, options);
450
+ await writePlan(cwd, plan);
451
+ await appendLedger(cwd, {
452
+ ts: now,
453
+ event: 'goal_added',
454
+ goalId: goal.id,
455
+ status: goal.status,
456
+ evidence: options.evidence,
457
+ message: goal.title,
458
+ });
459
+ return { plan, goal };
460
+ });
461
+ }
462
+ function proposalTargetIds(proposal) {
463
+ return proposal.targetGoalIds?.length ? proposal.targetGoalIds : (proposal.targetGoalId ? [proposal.targetGoalId] : []);
464
+ }
465
+ function steeringTargets(plan, proposal) {
466
+ return proposalTargetIds(proposal).map((id) => {
467
+ const goal = plan.goals.find((candidate) => candidate.id === id);
468
+ if (!goal)
469
+ throw new UltragoalError(`Unknown ultragoal id: ${id}`);
470
+ return goal;
471
+ });
472
+ }
473
+ function mentionsWeakenedCompletion(...values) {
474
+ const normalized = values.filter(Boolean).join(' ').toLowerCase();
475
+ return /\b(skip|bypass|weaken|remove|omit|auto[-\s]?complete|mark complete|complete faster)\b/.test(normalized)
476
+ && /\b(test|tests|verification|review|quality gate|complete|completion)\b/.test(normalized);
477
+ }
478
+ function hasProtectedSteeringPayload(value) {
479
+ if (!value || typeof value !== 'object')
480
+ return false;
481
+ const protectedKeys = new Set([
482
+ 'aggregateCompletion',
483
+ 'brief',
484
+ 'briefPath',
485
+ 'codexObjective',
486
+ 'constraints',
487
+ 'completedAt',
488
+ 'qualityGate',
489
+ 'status',
490
+ ]);
491
+ const stack = [value];
492
+ while (stack.length > 0) {
493
+ const current = stack.pop();
494
+ if (!current || typeof current !== 'object')
495
+ continue;
496
+ for (const [key, child] of Object.entries(current)) {
497
+ if (protectedKeys.has(key))
498
+ return true;
499
+ if (key.toLowerCase().includes('complete'))
500
+ return true;
501
+ if (child && typeof child === 'object')
502
+ stack.push(child);
503
+ }
504
+ }
505
+ return false;
506
+ }
507
+ function protectedIntentText(proposal) {
508
+ const after = proposal.after;
509
+ const childTexts = rawChildGoalsFromProposal(proposal).flatMap((child) => {
510
+ if (!child || typeof child !== 'object' || Array.isArray(child))
511
+ return [];
512
+ const candidate = child;
513
+ return [candidate.title, candidate.objective];
514
+ });
515
+ return [
516
+ proposal.title,
517
+ proposal.objective,
518
+ proposal.revisedTitle,
519
+ proposal.revisedObjective,
520
+ after?.title,
521
+ after?.objective,
522
+ proposal.rationale,
523
+ proposal.directiveText,
524
+ ...childTexts,
525
+ ]
526
+ .filter((value) => typeof value === 'string')
527
+ .join('\n')
528
+ .toLowerCase();
529
+ }
530
+ function rawChildGoalsFromProposal(proposal) {
531
+ if (Array.isArray(proposal.childGoals) && proposal.childGoals.length > 0)
532
+ return proposal.childGoals;
533
+ const after = proposal.after;
534
+ return Array.isArray(after?.children) ? after.children : [];
535
+ }
536
+ function isValidSteeringChildGoal(value) {
537
+ if (!value || typeof value !== 'object' || Array.isArray(value))
538
+ return false;
539
+ const candidate = value;
540
+ return typeof candidate.title === 'string'
541
+ && candidate.title.trim().length > 0
542
+ && typeof candidate.objective === 'string'
543
+ && candidate.objective.trim().length > 0;
544
+ }
545
+ function childGoalsFromProposal(proposal) {
546
+ return rawChildGoalsFromProposal(proposal).filter(isValidSteeringChildGoal);
547
+ }
548
+ function pendingOrderFromProposal(proposal) {
549
+ if (proposal.pendingOrder?.length)
550
+ return proposal.pendingOrder;
551
+ const after = proposal.after;
552
+ return Array.isArray(after?.pendingGoalIds) ? after.pendingGoalIds : [];
553
+ }
554
+ function revisedTitleFromProposal(proposal) {
555
+ if (proposal.revisedTitle?.trim())
556
+ return proposal.revisedTitle;
557
+ const after = proposal.after;
558
+ return after?.title ?? proposal.title;
559
+ }
560
+ function revisedObjectiveFromProposal(proposal) {
561
+ if (proposal.revisedObjective?.trim())
562
+ return proposal.revisedObjective;
563
+ const after = proposal.after;
564
+ return after?.objective ?? proposal.objective;
565
+ }
566
+ export function validateUltragoalSteeringProposal(plan, proposal) {
567
+ const rejectedReasons = [];
568
+ const evidenceBackedNecessity = Boolean(proposal.evidence?.trim()) && Boolean(proposal.rationale?.trim());
569
+ if (!ULTRAGOAL_STEERING_MUTATION_KINDS.includes(proposal.kind))
570
+ rejectedReasons.push(`Invalid steering mutation kind: ${String(proposal.kind)}.`);
571
+ if (!ULTRAGOAL_STEERING_SOURCES.includes(proposal.source))
572
+ rejectedReasons.push(`Invalid steering source: ${String(proposal.source)}.`);
573
+ if (!evidenceBackedNecessity)
574
+ rejectedReasons.push('Steering requires non-empty evidence and rationale.');
575
+ if (hasProtectedSteeringPayload(proposal.after))
576
+ rejectedReasons.push('Steering payload must not edit protected objective, constraint, quality gate, or completion fields.');
577
+ if (/\b(?:skip|bypass|weaken|remove)\b.*\b(?:test|tests|review|verification|quality gate|complete|completion)\b|\bauto[- ]?complete\b/.test(protectedIntentText(proposal))) {
578
+ rejectedReasons.push('Steering must not weaken completion, quality gates, tests, reviews, or auto-complete work.');
579
+ }
580
+ if (plan.aggregateCompletion?.status === 'complete')
581
+ rejectedReasons.push('Cannot steer an already completed aggregate ultragoal plan.');
582
+ let targets = [];
583
+ try {
584
+ targets = steeringTargets(plan, proposal);
585
+ }
586
+ catch (error) {
587
+ const message = error instanceof Error ? error.message : String(error);
588
+ rejectedReasons.push(message.replace(/^Unknown ultragoal id:/, 'unknown ultragoal id:'));
589
+ }
590
+ const target = targets[0];
591
+ if ((proposal.kind === 'split_subgoal' || proposal.kind === 'revise_pending_wording' || proposal.kind === 'mark_blocked_superseded') && !target) {
592
+ rejectedReasons.push(`${proposal.kind} requires a target goal id.`);
593
+ }
594
+ if ((proposal.kind === 'split_subgoal' || proposal.kind === 'revise_pending_wording') && target?.status !== 'pending') {
595
+ rejectedReasons.push(`${proposal.kind} can only target a pending goal.`);
596
+ }
597
+ if (proposal.kind === 'add_subgoal') {
598
+ if (!proposal.title?.trim() || !proposal.objective?.trim())
599
+ rejectedReasons.push('add_subgoal requires title and objective.');
600
+ }
601
+ if (proposal.kind === 'split_subgoal') {
602
+ const rawChildren = rawChildGoalsFromProposal(proposal);
603
+ if (rawChildren.length === 0)
604
+ rejectedReasons.push('split_subgoal requires replacement child goals.');
605
+ if (rawChildren.some((child) => !isValidSteeringChildGoal(child)))
606
+ rejectedReasons.push('split_subgoal children require title and objective.');
607
+ }
608
+ if (proposal.kind === 'mark_blocked_superseded') {
609
+ const rawChildren = rawChildGoalsFromProposal(proposal);
610
+ if (rawChildren.some((child) => !isValidSteeringChildGoal(child)))
611
+ rejectedReasons.push('mark_blocked_superseded replacement children require title and objective.');
612
+ }
613
+ if (proposal.kind === 'reorder_pending') {
614
+ const requested = pendingOrderFromProposal(proposal);
615
+ const pending = plan.goals.filter((goal) => goal.status === 'pending' && isScheduleEligible(goal)).map((goal) => goal.id);
616
+ if (requested.length === 0)
617
+ rejectedReasons.push('reorder_pending requires at least one pending goal id.');
618
+ if (new Set(requested).size !== requested.length)
619
+ rejectedReasons.push('duplicate goal id in pendingOrder.');
620
+ if (requested.some((id) => !pending.includes(id)))
621
+ rejectedReasons.push('pendingOrder contains non-pending or unknown goal.');
622
+ }
623
+ if (proposal.kind === 'revise_pending_wording') {
624
+ if (!revisedTitleFromProposal(proposal)?.trim() && !revisedObjectiveFromProposal(proposal)?.trim())
625
+ rejectedReasons.push('revise_pending_wording requires title or objective.');
626
+ }
627
+ if (proposal.kind === 'annotate_ledger' && !proposal.evidence?.trim())
628
+ rejectedReasons.push('annotate_ledger requires evidence.');
629
+ const accepted = rejectedReasons.length === 0;
630
+ const noEasierCompletion = !mentionsWeakenedCompletion(protectedIntentText(proposal));
631
+ return {
632
+ structuralInvariantAccepted: accepted,
633
+ evidenceBackedNecessity,
634
+ noEasierCompletion,
635
+ accepted,
636
+ rejectedReasons,
637
+ reasons: rejectedReasons,
638
+ };
639
+ }
640
+ export const validateSteeringProposal = validateUltragoalSteeringProposal;
641
+ async function readSteeringLedgerEntries(cwd) {
642
+ try {
643
+ const raw = await readFile(ultragoalLedgerPath(cwd), 'utf-8');
644
+ return raw.split(/\r?\n/).filter(Boolean).map((line) => JSON.parse(line));
645
+ }
646
+ catch {
647
+ return [];
648
+ }
649
+ }
650
+ function cloneForAudit(value) {
651
+ return JSON.parse(JSON.stringify(value));
652
+ }
653
+ function moveGoalsAfterTarget(plan, targetId, movedIds) {
654
+ const moved = movedIds.map((id) => plan.goals.find((goal) => goal.id === id)).filter((goal) => Boolean(goal));
655
+ if (moved.length === 0)
656
+ return;
657
+ plan.goals = plan.goals.filter((goal) => !movedIds.includes(goal.id));
658
+ const targetIndex = plan.goals.findIndex((goal) => goal.id === targetId);
659
+ plan.goals.splice(targetIndex >= 0 ? targetIndex + 1 : plan.goals.length, 0, ...moved);
660
+ }
661
+ function applySteeringMutation(plan, proposal, now) {
662
+ const targets = steeringTargets(plan, proposal);
663
+ const target = targets[0];
664
+ if (proposal.kind === 'add_subgoal') {
665
+ const goal = appendGoalToPlan(plan, { title: proposal.title ?? '', objective: proposal.objective ?? '', evidence: proposal.evidence, now: new Date(now) });
666
+ return { before: undefined, after: cloneForAudit(goal) };
667
+ }
668
+ if (proposal.kind === 'split_subgoal') {
669
+ const before = cloneForAudit(target);
670
+ const children = childGoalsFromProposal(proposal).map((child) => appendGoalToPlan(plan, { ...child, evidence: proposal.evidence, now: new Date(now) }));
671
+ target.steeringStatus = 'superseded';
672
+ target.supersededBy = children.map((child) => child.id);
673
+ moveGoalsAfterTarget(plan, target.id, children.map((child) => child.id));
674
+ target.steeringEvidence = proposal.evidence;
675
+ target.steeringRationale = proposal.rationale;
676
+ target.updatedAt = now;
677
+ for (const child of children)
678
+ child.supersedes = [target.id];
679
+ if (plan.activeGoalId === target.id)
680
+ plan.activeGoalId = undefined;
681
+ plan.updatedAt = now;
682
+ return { before, after: { target: cloneForAudit(target), children: cloneForAudit(children) } };
683
+ }
684
+ if (proposal.kind === 'reorder_pending') {
685
+ const before = plan.goals.map((goal) => goal.id);
686
+ const requested = pendingOrderFromProposal(proposal);
687
+ const requestedSet = new Set(requested);
688
+ const requestedGoals = requested.map((id) => plan.goals.find((goal) => goal.id === id)).filter((goal) => Boolean(goal));
689
+ const remaining = plan.goals.filter((goal) => !requestedSet.has(goal.id));
690
+ plan.goals = [...requestedGoals, ...remaining];
691
+ plan.updatedAt = now;
692
+ return { before, after: plan.goals.map((goal) => goal.id) };
693
+ }
694
+ if (proposal.kind === 'revise_pending_wording') {
695
+ const before = cloneForAudit(target);
696
+ const revisedTitle = revisedTitleFromProposal(proposal);
697
+ const revisedObjective = revisedObjectiveFromProposal(proposal);
698
+ if (revisedTitle?.trim())
699
+ target.title = revisedTitle.trim();
700
+ if (revisedObjective?.trim())
701
+ target.objective = revisedObjective.trim();
702
+ target.steeringEvidence = proposal.evidence;
703
+ target.steeringRationale = proposal.rationale;
704
+ target.updatedAt = now;
705
+ plan.updatedAt = now;
706
+ return { before, after: cloneForAudit(target) };
707
+ }
708
+ if (proposal.kind === 'annotate_ledger') {
709
+ return { before: undefined, after: { evidence: proposal.evidence, rationale: proposal.rationale } };
710
+ }
711
+ if (proposal.kind === 'mark_blocked_superseded') {
712
+ const before = cloneForAudit(target);
713
+ const children = childGoalsFromProposal(proposal);
714
+ if (children.length > 0) {
715
+ const replacements = children.map((child) => appendGoalToPlan(plan, { ...child, evidence: proposal.evidence, now: new Date(now) }));
716
+ target.steeringStatus = 'superseded';
717
+ target.supersededBy = replacements.map((child) => child.id);
718
+ moveGoalsAfterTarget(plan, target.id, replacements.map((child) => child.id));
719
+ target.steeringEvidence = proposal.evidence;
720
+ target.steeringRationale = proposal.rationale;
721
+ target.updatedAt = now;
722
+ for (const replacement of replacements)
723
+ replacement.supersedes = [target.id];
724
+ if (plan.activeGoalId === target.id)
725
+ plan.activeGoalId = undefined;
726
+ plan.updatedAt = now;
727
+ return { before, after: { target: cloneForAudit(target), children: cloneForAudit(replacements) } };
728
+ }
729
+ if (plan.activeGoalId === target.id)
730
+ delete plan.activeGoalId;
731
+ target.steeringStatus = 'blocked';
732
+ target.blockedReason = proposal.blockedReason ?? proposal.rationale;
733
+ target.steeringEvidence = proposal.evidence;
734
+ target.steeringRationale = proposal.rationale;
735
+ target.updatedAt = now;
736
+ if (plan.activeGoalId === target.id)
737
+ plan.activeGoalId = undefined;
738
+ plan.updatedAt = now;
739
+ return { before, after: cloneForAudit(target) };
740
+ }
741
+ return {};
742
+ }
743
+ export async function steerUltragoal(cwd, proposal, options = {}) {
744
+ return withUltragoalMutationLock(cwd, async () => {
745
+ const plan = await readUltragoalPlan(cwd);
746
+ const existing = proposal.idempotencyKey
747
+ ? (await readSteeringLedgerEntries(cwd)).find((entry) => entry.event === 'steering_accepted' && (entry.idempotencyKey === proposal.idempotencyKey || entry.steering?.idempotencyKey === proposal.idempotencyKey) && entry.steering)
748
+ : undefined;
749
+ if (existing?.steering) {
750
+ return { plan, accepted: true, audit: { ...existing.steering, deduped: true }, rejectedReasons: [], deduped: true };
751
+ }
752
+ let invariant = validateUltragoalSteeringProposal(plan, proposal);
753
+ const now = iso(options.now ?? proposal.now);
754
+ const beforePlan = cloneForAudit(plan);
755
+ let mutation = {};
756
+ if (invariant.accepted) {
757
+ try {
758
+ mutation = applySteeringMutation(plan, proposal, now);
759
+ }
760
+ catch (error) {
761
+ const message = error instanceof Error ? error.message : String(error);
762
+ const rejectedReasons = [...invariant.rejectedReasons, `Steering mutation failed: ${message}`];
763
+ invariant = {
764
+ ...invariant,
765
+ accepted: false,
766
+ structuralInvariantAccepted: false,
767
+ rejectedReasons,
768
+ reasons: rejectedReasons,
769
+ };
770
+ }
771
+ }
772
+ const audit = {
773
+ kind: proposal.kind,
774
+ source: proposal.source,
775
+ targetGoalIds: proposalTargetIds(proposal),
776
+ before: mutation.before ?? beforePlan,
777
+ after: mutation.after,
778
+ evidence: proposal.evidence,
779
+ rationale: proposal.rationale,
780
+ invariant,
781
+ directiveText: options.directiveText ?? proposal.directiveText,
782
+ promptSignature: proposal.promptSignature,
783
+ idempotencyKey: proposal.idempotencyKey,
784
+ };
785
+ if (invariant.accepted)
786
+ await writePlan(cwd, plan);
787
+ await appendLedger(cwd, {
788
+ ts: now,
789
+ event: invariant.accepted ? 'steering_accepted' : 'steering_rejected',
790
+ goalId: proposalTargetIds(proposal)[0],
791
+ evidence: proposal.evidence,
792
+ message: proposal.rationale,
793
+ steering: audit,
794
+ mutationKind: proposal.kind,
795
+ before: audit.before,
796
+ after: audit.after,
797
+ });
798
+ return { plan, accepted: invariant.accepted, audit, rejectedReasons: invariant.rejectedReasons, deduped: false };
267
799
  });
268
- return { plan, goal };
269
800
  }
270
801
  function validateQualityGate(value) {
271
802
  if (!value || typeof value !== 'object') {
@@ -301,230 +832,278 @@ function validateQualityGate(value) {
301
832
  return gate;
302
833
  }
303
834
  export async function startNextUltragoal(cwd, options = {}) {
304
- const plan = await readUltragoalPlan(cwd);
305
- const now = iso(options.now);
306
- if (plan.aggregateCompletion?.status === 'complete')
307
- return { plan, goal: null, resumed: false, done: true };
308
- const existing = plan.goals.find((goal) => goal.status === 'in_progress');
309
- if (existing) {
310
- await appendLedger(cwd, { ts: now, event: 'goal_resumed', goalId: existing.id, status: existing.status, message: 'Resuming active ultragoal' });
311
- return { plan, goal: existing, resumed: true, done: false };
312
- }
313
- let next = plan.goals.find((goal) => goal.status === 'pending');
314
- if (!next && options.retryFailed) {
315
- next = plan.goals.find((goal) => goal.status === 'failed');
316
- if (next)
317
- await appendLedger(cwd, { ts: now, event: 'goal_retried', goalId: next.id, status: 'pending', message: next.failureReason });
318
- }
319
- if (!next)
320
- return { plan, goal: null, resumed: false, done: isUltragoalDone(plan) };
321
- next.status = 'in_progress';
322
- next.attempt += 1;
323
- next.startedAt = now;
324
- next.failedAt = undefined;
325
- next.failureReason = undefined;
326
- next.updatedAt = now;
327
- plan.activeGoalId = next.id;
328
- plan.updatedAt = now;
329
- await writePlan(cwd, plan);
330
- await appendLedger(cwd, { ts: now, event: 'goal_started', goalId: next.id, status: next.status, message: `Attempt ${next.attempt}` });
331
- return { plan, goal: next, resumed: false, done: false };
835
+ return withUltragoalMutationLock(cwd, async () => {
836
+ const plan = await readUltragoalPlan(cwd);
837
+ const now = iso(options.now);
838
+ if (plan.aggregateCompletion?.status === 'complete')
839
+ return { plan, goal: null, resumed: false, done: true };
840
+ const existing = plan.goals.find((goal) => goal.status === 'in_progress' && isScheduleEligibleGoal(goal));
841
+ if (existing) {
842
+ await appendLedger(cwd, { ts: now, event: 'goal_resumed', goalId: existing.id, status: existing.status, message: 'Resuming active ultragoal' });
843
+ return { plan, goal: existing, resumed: true, done: false };
844
+ }
845
+ let next = plan.goals.find((goal) => goal.status === 'pending' && isScheduleEligible(goal));
846
+ if (!next && options.retryFailed) {
847
+ next = plan.goals.find((goal) => goal.status === 'failed' && !goal.nonRetriable && isScheduleEligible(goal));
848
+ if (next)
849
+ await appendLedger(cwd, { ts: now, event: 'goal_retried', goalId: next.id, status: 'pending', message: next.failureReason });
850
+ }
851
+ if (!next)
852
+ return { plan, goal: null, resumed: false, done: isUltragoalDone(plan) };
853
+ next.status = 'in_progress';
854
+ next.attempt += 1;
855
+ next.startedAt = now;
856
+ next.failedAt = undefined;
857
+ next.failureReason = undefined;
858
+ clearGoalBlockerFields(next);
859
+ next.updatedAt = now;
860
+ plan.activeGoalId = next.id;
861
+ plan.updatedAt = now;
862
+ await writePlan(cwd, plan);
863
+ await appendLedger(cwd, { ts: now, event: 'goal_started', goalId: next.id, status: next.status, message: `Attempt ${next.attempt}` });
864
+ return { plan, goal: next, resumed: false, done: false };
865
+ });
332
866
  }
333
867
  export async function checkpointUltragoal(cwd, options) {
334
- const plan = await readUltragoalPlan(cwd);
335
- const goal = plan.goals.find((candidate) => candidate.id === options.goalId);
336
- if (!goal)
337
- throw new UltragoalError(`Unknown ultragoal id: ${options.goalId}`);
338
- const now = iso(options.now);
339
- if (options.status === 'blocked') {
340
- if (goal.status !== 'in_progress') {
341
- throw new UltragoalError(`Cannot record a blocked checkpoint for ${goal.id} while it is ${goal.status}; start or resume the ultragoal before recording a non-terminal blocker.`);
868
+ return withUltragoalMutationLock(cwd, async () => {
869
+ const plan = await readUltragoalPlan(cwd);
870
+ const goal = plan.goals.find((candidate) => candidate.id === options.goalId);
871
+ if (!goal)
872
+ throw new UltragoalError(`Unknown ultragoal id: ${options.goalId}`);
873
+ const now = iso(options.now);
874
+ if (options.status === 'blocked') {
875
+ if (goal.status !== 'in_progress') {
876
+ throw new UltragoalError(`Cannot record a blocked checkpoint for ${goal.id} while it is ${goal.status}; start or resume the ultragoal before recording a non-terminal blocker.`);
877
+ }
878
+ const snapshot = options.codexGoal === undefined ? null : parseCodexGoalSnapshot(options.codexGoal);
879
+ if (snapshot?.unavailableReason === 'db_schema_context_error') {
880
+ goal.updatedAt = now;
881
+ goal.failureReason = assertNonEmpty(options.evidence, '--evidence');
882
+ plan.activeGoalId = goal.id;
883
+ plan.updatedAt = now;
884
+ await writePlan(cwd, plan);
885
+ await appendLedger(cwd, {
886
+ ts: now,
887
+ event: 'goal_blocked',
888
+ goalId: goal.id,
889
+ status: goal.status,
890
+ evidence: options.evidence,
891
+ codexGoal: options.codexGoal,
892
+ message: 'Codex get_goal was unavailable due to a DB/schema/context error; strict completion reconciliation is deferred until get_goal works.',
893
+ });
894
+ return plan;
895
+ }
896
+ if (!snapshot?.available) {
897
+ throw new UltragoalError('Blocked ultragoal checkpoints require either a get_goal snapshot for the completed legacy Codex goal that blocked create_goal, or an unavailable get_goal error JSON for a Codex DB/schema/context failure; pass --codex-goal-json.');
898
+ }
899
+ if (snapshot.status !== 'complete') {
900
+ throw new UltragoalError(`Cannot record a blocked ultragoal checkpoint while the existing Codex goal is ${snapshot.status ?? 'unknown'}; strict objective mismatch protection remains required for active or incomplete goals.`);
901
+ }
902
+ if (!snapshot.objective) {
903
+ throw new UltragoalError('Blocked ultragoal checkpoint Codex snapshot is missing objective text.');
904
+ }
905
+ if (normalizeObjective(snapshot.objective) === normalizeObjective(expectedCodexObjective(plan, goal))) {
906
+ throw new UltragoalError('Blocked ultragoal checkpoint is only for a different completed legacy Codex goal; complete this ultragoal with --status complete after its audit passes.');
907
+ }
908
+ goal.updatedAt = now;
909
+ plan.activeGoalId = goal.id;
910
+ plan.updatedAt = now;
911
+ await writePlan(cwd, plan);
912
+ await appendLedger(cwd, {
913
+ ts: now,
914
+ event: 'goal_blocked',
915
+ goalId: goal.id,
916
+ status: goal.status,
917
+ evidence: options.evidence,
918
+ codexGoal: options.codexGoal,
919
+ });
920
+ return plan;
342
921
  }
343
- const snapshot = options.codexGoal === undefined ? null : parseCodexGoalSnapshot(options.codexGoal);
344
- if (!snapshot?.available) {
345
- throw new UltragoalError('Blocked ultragoal checkpoints require a get_goal snapshot for the completed legacy Codex goal that blocked create_goal; pass --codex-goal-json.');
922
+ let aggregateCompletion;
923
+ if (options.status === 'complete') {
924
+ const expectedObjective = expectedCodexObjective(plan, goal);
925
+ const aggregateMode = codexGoalMode(plan) === 'aggregate';
926
+ const finalRunCheckpoint = isFinalRunCompletionCandidate(plan, goal);
927
+ const snapshot = options.codexGoal === undefined ? null : parseCodexGoalSnapshot(options.codexGoal);
928
+ const reconciliation = reconcileCodexGoalSnapshot(snapshot, {
929
+ expectedObjective,
930
+ acceptedObjectives: aggregateMode ? compatibleCodexObjectives(plan) : undefined,
931
+ allowedStatuses: aggregateMode
932
+ ? (finalRunCheckpoint && !options.allowActiveFinalCodexGoal ? ['complete'] : ['active'])
933
+ : ['complete'],
934
+ requireSnapshot: true,
935
+ requireComplete: !aggregateMode || (finalRunCheckpoint && !options.allowActiveFinalCodexGoal),
936
+ });
937
+ if (!reconciliation.ok) {
938
+ const completedTaskScopedAggregateSnapshot = snapshot?.available
939
+ && snapshot.status === 'complete'
940
+ && Boolean(snapshot.objective)
941
+ && normalizeObjective(snapshot.objective ?? '') !== normalizeObjective(expectedObjective)
942
+ && await canReconcileCompletedTaskScopedAggregateSnapshot(cwd, plan, goal, snapshot.objective ?? '', options.evidence);
943
+ if (completedTaskScopedAggregateSnapshot) {
944
+ aggregateCompletion = {
945
+ status: 'complete',
946
+ completedAt: now,
947
+ evidence: assertNonEmpty(options.evidence, '--evidence'),
948
+ codexGoal: options.codexGoal,
949
+ };
950
+ }
951
+ else {
952
+ const taskScopedRequirement = aggregateMode && snapshot?.status === 'complete' && Boolean(snapshot.objective)
953
+ ? ' Completed task-scoped aggregate reconciliation requires the checkpoint goal to be the active in-progress OMX goal, evidence that names that active OMX goal id, names .omx/ultragoal/goals.json or ledger.jsonl, includes completed implementation plus validation/review evidence, and a get_goal objective that maps to the ultragoal brief/artifact.'
954
+ : '';
955
+ const remediation = reconciliation.snapshot.available
956
+ && reconciliation.snapshot.status === 'complete'
957
+ && Boolean(reconciliation.snapshot.objective)
958
+ && normalizeObjective(reconciliation.snapshot.objective ?? '') !== normalizeObjective(expectedObjective)
959
+ ? ` ${buildCompletedLegacyGoalRemediation(goal)}`
960
+ : reconciliation.snapshot.unavailableReason === 'db_schema_context_error'
961
+ ? ` ${buildUnavailableCodexGoalRemediation(goal)}`
962
+ : '';
963
+ throw new UltragoalError(`${formatCodexGoalReconciliation(reconciliation)}${taskScopedRequirement}${remediation}`);
964
+ }
965
+ }
966
+ if (finalRunCheckpoint && !options.allowActiveFinalCodexGoal)
967
+ goal.evidence = options.evidence;
346
968
  }
347
- if (snapshot.status !== 'complete') {
348
- throw new UltragoalError(`Cannot record a blocked ultragoal checkpoint while the existing Codex goal is ${snapshot.status ?? 'unknown'}; strict objective mismatch protection remains required for active or incomplete goals.`);
969
+ const qualityGate = options.status === 'complete' && (aggregateCompletion !== undefined || (isFinalRunCompletionCandidate(plan, goal) && !options.allowActiveFinalCodexGoal))
970
+ ? validateQualityGate(options.qualityGate)
971
+ : undefined;
972
+ if (aggregateCompletion) {
973
+ plan.aggregateCompletion = aggregateCompletion;
974
+ if (plan.activeGoalId === goal.id)
975
+ delete plan.activeGoalId;
976
+ plan.updatedAt = now;
977
+ await writePlan(cwd, plan);
978
+ await appendLedger(cwd, {
979
+ ts: now,
980
+ event: 'aggregate_completed',
981
+ goalId: goal.id,
982
+ status: goal.status,
983
+ evidence: options.evidence,
984
+ codexGoal: options.codexGoal,
985
+ qualityGate,
986
+ message: 'Aggregate ultragoal plan completed via task-scoped Codex goal snapshot; microgoal ledger progress remains independent.',
987
+ });
988
+ return plan;
349
989
  }
350
- if (!snapshot.objective) {
351
- throw new UltragoalError('Blocked ultragoal checkpoint Codex snapshot is missing objective text.');
990
+ goal.status = options.status;
991
+ goal.updatedAt = now;
992
+ if (options.status === 'complete') {
993
+ goal.completedAt = now;
994
+ goal.evidence = options.evidence;
995
+ goal.failureReason = undefined;
996
+ goal.failedAt = undefined;
997
+ clearGoalBlockerFields(goal);
998
+ if (plan.activeGoalId === goal.id)
999
+ delete plan.activeGoalId;
352
1000
  }
353
- if (normalizeObjective(snapshot.objective) === normalizeObjective(expectedCodexObjective(plan, goal))) {
354
- throw new UltragoalError('Blocked ultragoal checkpoint is only for a different completed legacy Codex goal; complete this ultragoal with --status complete after its audit passes.');
1001
+ else {
1002
+ const blocker = classifyExternalAuthorizationBlocker(options.evidence);
1003
+ const previousEntries = blocker ? await readSteeringLedgerEntries(cwd) : [];
1004
+ const occurrenceCount = blocker ? sameBlockerOccurrences(previousEntries, goal.id, blocker.signature) + 1 : 0;
1005
+ const shouldCircuitBreak = blocker !== null && occurrenceCount >= 3;
1006
+ goal.failedAt = now;
1007
+ goal.failureReason = options.evidence;
1008
+ goal.blockerSignature = blocker?.signature;
1009
+ goal.blockerOccurrenceCount = blocker ? occurrenceCount : undefined;
1010
+ goal.requiredExternalDecision = blocker?.requiredDecision;
1011
+ goal.nonRetriable = shouldCircuitBreak || undefined;
1012
+ if (shouldCircuitBreak) {
1013
+ goal.status = 'needs_user_decision';
1014
+ goal.blockedReason = options.evidence;
1015
+ }
1016
+ if (plan.activeGoalId === goal.id)
1017
+ delete plan.activeGoalId;
355
1018
  }
356
- goal.updatedAt = now;
357
- plan.activeGoalId = goal.id;
358
1019
  plan.updatedAt = now;
359
1020
  await writePlan(cwd, plan);
1021
+ const blockerEvent = goal.status === 'needs_user_decision';
360
1022
  await appendLedger(cwd, {
361
1023
  ts: now,
362
- event: 'goal_blocked',
1024
+ event: options.status === 'complete' ? 'goal_completed' : blockerEvent ? 'goal_needs_user_decision' : 'goal_failed',
363
1025
  goalId: goal.id,
364
1026
  status: goal.status,
365
1027
  evidence: options.evidence,
366
1028
  codexGoal: options.codexGoal,
1029
+ qualityGate,
1030
+ blockerSignature: goal.blockerSignature,
1031
+ blockerOccurrenceCount: goal.blockerOccurrenceCount,
1032
+ requiredExternalDecision: goal.requiredExternalDecision,
1033
+ message: blockerEvent
1034
+ ? `Blocked on repeated external authorization. Required decision: ${goal.requiredExternalDecision}.`
1035
+ : undefined,
367
1036
  });
368
1037
  return plan;
369
- }
370
- let aggregateCompletion;
371
- if (options.status === 'complete') {
1038
+ });
1039
+ }
1040
+ export async function recordFinalReviewBlockers(cwd, options) {
1041
+ return withUltragoalMutationLock(cwd, async () => {
1042
+ const plan = await readUltragoalPlan(cwd);
1043
+ const goal = plan.goals.find((candidate) => candidate.id === options.goalId);
1044
+ if (!goal)
1045
+ throw new UltragoalError(`Unknown ultragoal id: ${options.goalId}`);
1046
+ assertNonEmpty(options.evidence, '--evidence');
1047
+ if (goal.status !== 'in_progress') {
1048
+ throw new UltragoalError(`Cannot record final review blockers for ${goal.id} while it is ${goal.status}; start or resume the ultragoal first.`);
1049
+ }
1050
+ if (!isFinalRunCompletionCandidate(plan, goal)) {
1051
+ throw new UltragoalError(`Cannot record final review blockers for ${goal.id}; it is not the only unresolved ultragoal story.`);
1052
+ }
1053
+ const now = iso(options.now);
372
1054
  const expectedObjective = expectedCodexObjective(plan, goal);
373
1055
  const aggregateMode = codexGoalMode(plan) === 'aggregate';
374
- const finalRunCheckpoint = isFinalRunCompletionCandidate(plan, goal);
375
- const snapshot = options.codexGoal === undefined ? null : parseCodexGoalSnapshot(options.codexGoal);
376
- const reconciliation = reconcileCodexGoalSnapshot(snapshot, {
1056
+ const reconciliation = reconcileCodexGoalSnapshot(options.codexGoal === undefined ? null : parseCodexGoalSnapshot(options.codexGoal), {
377
1057
  expectedObjective,
378
- allowedStatuses: aggregateMode
379
- ? (finalRunCheckpoint && !options.allowActiveFinalCodexGoal ? ['complete'] : ['active'])
380
- : ['complete'],
1058
+ acceptedObjectives: aggregateMode ? compatibleCodexObjectives(plan) : undefined,
1059
+ allowedStatuses: ['active'],
381
1060
  requireSnapshot: true,
382
- requireComplete: !aggregateMode || (finalRunCheckpoint && !options.allowActiveFinalCodexGoal),
1061
+ requireComplete: false,
383
1062
  });
384
1063
  if (!reconciliation.ok) {
385
- const completedTaskScopedAggregateSnapshot = snapshot?.available
386
- && snapshot.status === 'complete'
387
- && Boolean(snapshot.objective)
388
- && normalizeObjective(snapshot.objective ?? '') !== normalizeObjective(expectedObjective)
389
- && await canReconcileCompletedTaskScopedAggregateSnapshot(cwd, plan, goal, snapshot.objective ?? '', options.evidence);
390
- if (completedTaskScopedAggregateSnapshot) {
391
- aggregateCompletion = {
392
- status: 'complete',
393
- completedAt: now,
394
- evidence: assertNonEmpty(options.evidence, '--evidence'),
395
- codexGoal: options.codexGoal,
396
- };
397
- }
398
- else {
399
- const taskScopedRequirement = aggregateMode && snapshot?.status === 'complete' && Boolean(snapshot.objective)
400
- ? ' Completed task-scoped aggregate reconciliation requires the checkpoint goal to be the active in-progress OMX goal, evidence that names that active OMX goal id, names .omx/ultragoal/goals.json or ledger.jsonl, includes completed implementation plus validation/review evidence, and a get_goal objective that maps to the ultragoal brief/artifact.'
401
- : '';
402
- const remediation = reconciliation.snapshot.available
403
- && reconciliation.snapshot.status === 'complete'
404
- && Boolean(reconciliation.snapshot.objective)
405
- && normalizeObjective(reconciliation.snapshot.objective ?? '') !== normalizeObjective(expectedObjective)
406
- ? ` ${buildCompletedLegacyGoalRemediation(goal)}`
407
- : '';
408
- throw new UltragoalError(`${formatCodexGoalReconciliation(reconciliation)}${taskScopedRequirement}${remediation}`);
409
- }
1064
+ throw new UltragoalError(formatCodexGoalReconciliation(reconciliation));
410
1065
  }
411
- if (finalRunCheckpoint && !options.allowActiveFinalCodexGoal)
412
- goal.evidence = options.evidence;
413
- }
414
- const qualityGate = options.status === 'complete' && (aggregateCompletion !== undefined || (isFinalRunCompletionCandidate(plan, goal) && !options.allowActiveFinalCodexGoal))
415
- ? validateQualityGate(options.qualityGate)
416
- : undefined;
417
- if (aggregateCompletion) {
418
- plan.aggregateCompletion = aggregateCompletion;
1066
+ const addedGoal = appendGoalToPlan(plan, { ...options, now: options.now });
1067
+ goal.status = 'review_blocked';
1068
+ goal.reviewBlockedAt = now;
1069
+ goal.updatedAt = now;
1070
+ goal.completedAt = undefined;
1071
+ goal.failedAt = undefined;
1072
+ goal.failureReason = undefined;
1073
+ goal.evidence = options.evidence;
419
1074
  if (plan.activeGoalId === goal.id)
420
1075
  delete plan.activeGoalId;
421
1076
  plan.updatedAt = now;
422
1077
  await writePlan(cwd, plan);
423
1078
  await appendLedger(cwd, {
424
1079
  ts: now,
425
- event: 'aggregate_completed',
1080
+ event: 'final_review_failed',
426
1081
  goalId: goal.id,
427
1082
  status: goal.status,
428
1083
  evidence: options.evidence,
429
1084
  codexGoal: options.codexGoal,
430
- qualityGate,
431
- message: 'Aggregate ultragoal plan completed via task-scoped Codex goal snapshot; microgoal ledger progress remains independent.',
1085
+ message: aggregateMode
1086
+ ? 'Final aggregate code-review was not clean; blocker story was appended while Codex goal remains active.'
1087
+ : 'Final per-story code-review was not clean; blocker story was appended and may require an available Codex goal context.',
432
1088
  });
433
- return plan;
434
- }
435
- goal.status = options.status;
436
- goal.updatedAt = now;
437
- if (options.status === 'complete') {
438
- goal.completedAt = now;
439
- goal.evidence = options.evidence;
440
- goal.failureReason = undefined;
441
- goal.failedAt = undefined;
442
- if (plan.activeGoalId === goal.id)
443
- delete plan.activeGoalId;
444
- }
445
- else {
446
- goal.failedAt = now;
447
- goal.failureReason = options.evidence;
448
- if (plan.activeGoalId === goal.id)
449
- delete plan.activeGoalId;
450
- }
451
- plan.updatedAt = now;
452
- await writePlan(cwd, plan);
453
- await appendLedger(cwd, {
454
- ts: now,
455
- event: options.status === 'complete' ? 'goal_completed' : 'goal_failed',
456
- goalId: goal.id,
457
- status: goal.status,
458
- evidence: options.evidence,
459
- codexGoal: options.codexGoal,
460
- qualityGate,
461
- });
462
- return plan;
463
- }
464
- export async function recordFinalReviewBlockers(cwd, options) {
465
- const plan = await readUltragoalPlan(cwd);
466
- const goal = plan.goals.find((candidate) => candidate.id === options.goalId);
467
- if (!goal)
468
- throw new UltragoalError(`Unknown ultragoal id: ${options.goalId}`);
469
- assertNonEmpty(options.evidence, '--evidence');
470
- if (goal.status !== 'in_progress') {
471
- throw new UltragoalError(`Cannot record final review blockers for ${goal.id} while it is ${goal.status}; start or resume the ultragoal first.`);
472
- }
473
- if (!isFinalRunCompletionCandidate(plan, goal)) {
474
- throw new UltragoalError(`Cannot record final review blockers for ${goal.id}; it is not the only unresolved ultragoal story.`);
475
- }
476
- const now = iso(options.now);
477
- const expectedObjective = expectedCodexObjective(plan, goal);
478
- const aggregateMode = codexGoalMode(plan) === 'aggregate';
479
- const reconciliation = reconcileCodexGoalSnapshot(options.codexGoal === undefined ? null : parseCodexGoalSnapshot(options.codexGoal), {
480
- expectedObjective,
481
- allowedStatuses: ['active'],
482
- requireSnapshot: true,
483
- requireComplete: false,
484
- });
485
- if (!reconciliation.ok) {
486
- throw new UltragoalError(formatCodexGoalReconciliation(reconciliation));
487
- }
488
- const addedGoal = appendGoalToPlan(plan, options, now);
489
- goal.status = 'review_blocked';
490
- goal.reviewBlockedAt = now;
491
- goal.updatedAt = now;
492
- goal.completedAt = undefined;
493
- goal.failedAt = undefined;
494
- goal.failureReason = undefined;
495
- goal.evidence = options.evidence;
496
- if (plan.activeGoalId === goal.id)
497
- delete plan.activeGoalId;
498
- plan.updatedAt = now;
499
- await writePlan(cwd, plan);
500
- await appendLedger(cwd, {
501
- ts: now,
502
- event: 'final_review_failed',
503
- goalId: goal.id,
504
- status: goal.status,
505
- evidence: options.evidence,
506
- codexGoal: options.codexGoal,
507
- message: aggregateMode
508
- ? 'Final aggregate code-review was not clean; blocker story was appended while Codex goal remains active.'
509
- : 'Final per-story code-review was not clean; blocker story was appended and may require a fresh/available Codex goal context.',
510
- });
511
- await appendLedger(cwd, {
512
- ts: now,
513
- event: 'goal_added',
514
- goalId: addedGoal.id,
515
- status: addedGoal.status,
516
- evidence: options.evidence,
517
- message: addedGoal.title,
518
- });
519
- await appendLedger(cwd, {
520
- ts: now,
521
- event: 'goal_review_blocked',
522
- goalId: goal.id,
523
- status: goal.status,
524
- evidence: options.evidence,
525
- codexGoal: options.codexGoal,
1089
+ await appendLedger(cwd, {
1090
+ ts: now,
1091
+ event: 'goal_added',
1092
+ goalId: addedGoal.id,
1093
+ status: addedGoal.status,
1094
+ evidence: options.evidence,
1095
+ message: addedGoal.title,
1096
+ });
1097
+ await appendLedger(cwd, {
1098
+ ts: now,
1099
+ event: 'goal_review_blocked',
1100
+ goalId: goal.id,
1101
+ status: goal.status,
1102
+ evidence: options.evidence,
1103
+ codexGoal: options.codexGoal,
1104
+ });
1105
+ return { plan, blockedGoal: goal, addedGoal };
526
1106
  });
527
- return { plan, blockedGoal: goal, addedGoal };
528
1107
  }
529
1108
  export function buildCodexGoalInstruction(goal, plan) {
530
1109
  if (codexGoalMode(plan) === 'aggregate')
@@ -546,7 +1125,8 @@ function buildPerStoryCodexGoalInstruction(goal, plan) {
546
1125
  'Codex goal integration constraints:',
547
1126
  '- First call get_goal. If no active goal exists, call create_goal with the payload below.',
548
1127
  '- If a different active Codex goal exists, finish/checkpoint that goal before starting this ultragoal.',
549
- '- If get_goal returns a different completed legacy/thread goal and create_goal rejects because this thread already has a completed goal, continue this ultragoal in a fresh Codex thread (same repo/worktree) and create the payload there.',
1128
+ '- Ultragoal cannot call /goal clear from the model/shell tool surface. For another per-story goal in the same session/thread after a completed Codex goal, manually run /goal clear in the Codex UI before creating the next goal.',
1129
+ '- If get_goal returns a different completed legacy/thread goal and create_goal rejects because this thread already has a completed goal, continue only from a Codex goal context with no active/completed conflicting goal in the same repo/worktree and create the payload there.',
550
1130
  `- To preserve the durable ledger before switching threads, record the non-terminal blocker without failing this goal: omx ultragoal checkpoint --goal-id ${goal.id} --status blocked --evidence "<completed legacy Codex goal blocks create_goal in this thread>" --codex-goal-json "<get_goal JSON or path>"`,
551
1131
  '- Work only this goal until its completion audit passes.',
552
1132
  finalStory
@@ -559,7 +1139,7 @@ function buildPerStoryCodexGoalInstruction(goal, plan) {
559
1139
  ? ` omx ultragoal record-review-blockers --goal-id ${goal.id} --title "Resolve final code-review blockers" --objective "<blocker-resolution objective>" --evidence "<review findings>" --codex-goal-json "<active get_goal JSON or path>"`
560
1140
  : ` omx ultragoal checkpoint --goal-id ${goal.id} --status complete --evidence "<tests/files/PR evidence>" --codex-goal-json "<fresh get_goal JSON or path>"`,
561
1141
  finalStory
562
- ? '- In legacy per-story mode, the blocker story may require a fresh/available Codex goal context because this story remains an active incomplete Codex goal; do not claim it is complete.'
1142
+ ? '- In legacy per-story mode, the blocker story may require an available Codex goal context because this story remains an active incomplete Codex goal; do not claim it is complete.'
563
1143
  : null,
564
1144
  finalStory
565
1145
  ? '- If final $code-review is clean (APPROVE + CLEAR), call update_goal({status: "complete"}), call get_goal again, then checkpoint with --quality-gate-json:'
@@ -592,6 +1172,7 @@ function buildAggregateCodexGoalInstruction(goal, plan) {
592
1172
  '- First call get_goal. If no active goal exists, call create_goal with the aggregate payload below.',
593
1173
  '- If get_goal reports the same aggregate objective as active, continue this OMX story without creating a new Codex goal.',
594
1174
  '- If a different active or incomplete Codex goal exists, finish/checkpoint that goal before starting this ultragoal; do not replace hidden Codex state from the shell.',
1175
+ '- Ultragoal does not call /goal clear. After a completed aggregate run, manually run /goal clear in the Codex UI before starting another ultragoal run in the same session/thread.',
595
1176
  finalStory
596
1177
  ? '- This is the final pending story: run the mandatory final ai-slop-cleaner pass, rerun verification, and run $code-review before any update_goal call.'
597
1178
  : '- This is not the final story: do not call update_goal yet; the aggregate Codex goal must remain active while later OMX stories remain.',