oh-my-codex 0.18.0 → 0.18.1

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