oh-my-opencode 4.19.0 → 4.19.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 (310) hide show
  1. package/.agents/command/omomomo.md +1 -1
  2. package/.agents/skills/omomomo/SKILL.md +1 -1
  3. package/.opencode/command/omomomo.md +1 -1
  4. package/README.ja.md +6 -6
  5. package/README.ko.md +6 -6
  6. package/README.md +6 -6
  7. package/README.ru.md +6 -6
  8. package/README.zh-cn.md +6 -6
  9. package/dist/agents/atlas/agent.d.ts +5 -3
  10. package/dist/agents/explore.d.ts +4 -2
  11. package/dist/agents/gpt-prompt-identity.d.ts +3 -0
  12. package/dist/agents/hephaestus/agent.d.ts +4 -2
  13. package/dist/agents/librarian.d.ts +4 -2
  14. package/dist/agents/metis.d.ts +4 -2
  15. package/dist/agents/momus.d.ts +3 -2
  16. package/dist/agents/multimodal-looker.d.ts +4 -2
  17. package/dist/agents/oracle.d.ts +4 -2
  18. package/dist/agents/sisyphus/claude-opus-5.d.ts +33 -0
  19. package/dist/agents/sisyphus/gpt-5-5.d.ts +1 -1
  20. package/dist/agents/sisyphus/gpt-task-system-guide.d.ts +1 -0
  21. package/dist/agents/sisyphus/index.d.ts +2 -0
  22. package/dist/agents/sisyphus-agent-factory.d.ts +5 -2
  23. package/dist/agents/sisyphus-junior/agent.d.ts +4 -1
  24. package/dist/agents/sisyphus-junior/gpt-5-5.d.ts +2 -2
  25. package/dist/agents/types.d.ts +2 -2
  26. package/dist/cli/index.js +23577 -19269
  27. package/dist/cli-node/index.js +23577 -19269
  28. package/dist/config/schema/agent-names.d.ts +26 -26
  29. package/dist/config/schema/agent-overrides.d.ts +400 -400
  30. package/dist/config/schema/browser-automation.d.ts +2 -2
  31. package/dist/config/schema/categories.d.ts +36 -36
  32. package/dist/config/schema/codegraph.d.ts +2 -0
  33. package/dist/config/schema/commands.d.ts +2 -2
  34. package/dist/config/schema/dynamic-context-pruning.d.ts +1 -1
  35. package/dist/config/schema/experimental.d.ts +1 -1
  36. package/dist/config/schema/fallback-models.d.ts +25 -25
  37. package/dist/config/schema/hooks.d.ts +43 -43
  38. package/dist/config/schema/internal/permission.d.ts +9 -9
  39. package/dist/config/schema/keyword-detector.d.ts +6 -6
  40. package/dist/config/schema/oh-my-opencode-config.d.ts +404 -402
  41. package/dist/config/schema/tmux.d.ts +6 -6
  42. package/dist/create-hooks.d.ts +35 -35
  43. package/dist/features/background-agent/spawner/tmux-callback-invoker.d.ts +11 -0
  44. package/dist/features/builtin-commands/templates/start-work.d.ts +1 -1
  45. package/dist/features/claude-tasks/types.d.ts +4 -4
  46. package/dist/features/team-mode/team-runtime/shutdown-helpers.d.ts +1 -1
  47. package/dist/features/team-mode/team-runtime/shutdown-test-fixtures.d.ts +4 -4
  48. package/dist/features/team-mode/tools/lifecycle-test-fixture.d.ts +42 -42
  49. package/dist/features/tmux-subagent/environment-eligibility.d.ts +2 -0
  50. package/dist/features/tmux-subagent/index.d.ts +1 -0
  51. package/dist/features/tmux-subagent/tracked-session-state.d.ts +1 -0
  52. package/dist/features/tui-sidebar/snapshot-schema.d.ts +6 -6
  53. package/dist/help/schema/acp.d.ts +3 -3
  54. package/dist/help/schema/doctor.d.ts +4 -4
  55. package/dist/help/schema/status.d.ts +3 -3
  56. package/dist/hooks/codegraph-bootstrap/hook.d.ts +3 -2
  57. package/dist/hooks/goal/types.d.ts +5 -5
  58. package/dist/hooks/non-interactive-env/constants.d.ts +4 -1
  59. package/dist/hooks/sisyphus-junior-notepad/constants.d.ts +1 -1
  60. package/dist/hooks/start-work/notepad-scaffold.d.ts +10 -0
  61. package/dist/hooks/start-work/parse-user-request.d.ts +2 -0
  62. package/dist/hooks/start-work/worktree-block.d.ts +5 -0
  63. package/dist/index.js +27370 -21301
  64. package/dist/mcp/types.d.ts +3 -3
  65. package/dist/oh-my-opencode.schema.json +11 -0
  66. package/dist/plugin/chat-message/loop-commands.d.ts +1 -0
  67. package/dist/plugin/command-execute-before.d.ts +3 -3
  68. package/dist/plugin/hooks/create-core-hooks.d.ts +25 -25
  69. package/dist/shared/omo-process-sweep.d.ts +18 -0
  70. package/dist/shared/tmux/tmux-utils/environment.d.ts +1 -1
  71. package/dist/shared/tmux/tmux-utils.d.ts +1 -1
  72. package/dist/shared/typescript-native-source-parser.d.ts +7 -0
  73. package/dist/skills/coding-agent-sessions/SKILL.md +5 -2
  74. package/dist/skills/coding-agent-sessions/references/all-platforms.md +3 -1
  75. package/dist/skills/coding-agent-sessions/references/senpi.md +15 -5
  76. package/dist/skills/coding-agent-sessions/scripts/agent_sessions/pi_family.py +46 -0
  77. package/dist/skills/coding-agent-sessions/scripts/agent_sessions/scanners.py +19 -10
  78. package/dist/skills/debugging/SKILL.md +2 -1
  79. package/dist/skills/debugging/references/methodology/03-flaky-triage.md +47 -0
  80. package/dist/skills/programming/SKILL.md +11 -1
  81. package/dist/skills/programming/references/logging.md +6 -1
  82. package/dist/skills/programming/scripts/typescript/check-no-excuse-rules.test.ts +87 -0
  83. package/dist/skills/programming/scripts/typescript/check-no-excuse-rules.ts +75 -11
  84. package/dist/skills/start-work/SKILL.md +12 -2
  85. package/dist/skills/ulw-plan/SKILL.md +21 -2
  86. package/dist/skills/ulw-plan/references/full-workflow.md +12 -1
  87. package/dist/skills/ulw-plan/references/intent-clear.md +1 -1
  88. package/dist/testing/create-plugin-module.d.ts +1 -0
  89. package/dist/tools/task/types.d.ts +10 -10
  90. package/dist/tui.js +23442 -19182
  91. package/package.json +14 -14
  92. package/packages/git-bash-mcp/dist/cli.js +54 -8
  93. package/packages/lsp-core/src/lsp/client-diagnostics-freshness.integration.test.ts +62 -2
  94. package/packages/lsp-core/src/lsp/client.ts +14 -1
  95. package/packages/lsp-core/src/lsp/fixtures/diagnostics-freshness-contract-probe.ts +2 -2
  96. package/packages/lsp-core/src/mcp.ts +9 -0
  97. package/packages/lsp-daemon/dist/cli.js +319 -51
  98. package/packages/lsp-daemon/dist/client.js +136 -48
  99. package/packages/lsp-daemon/dist/daemon-client.d.ts +0 -10
  100. package/packages/lsp-daemon/dist/daemon-client.js +4 -34
  101. package/packages/lsp-daemon/dist/daemon-failure-result.d.ts +3 -0
  102. package/packages/lsp-daemon/dist/daemon-failure-result.js +38 -0
  103. package/packages/lsp-daemon/dist/daemon-request-error.d.ts +14 -0
  104. package/packages/lsp-daemon/dist/daemon-request-error.js +26 -0
  105. package/packages/lsp-daemon/dist/daemon-server.js +5 -1
  106. package/packages/lsp-daemon/dist/index.js +137 -48
  107. package/packages/lsp-daemon/dist/proxy.d.ts +2 -0
  108. package/packages/lsp-daemon/dist/proxy.js +1 -0
  109. package/packages/lsp-daemon/dist/version-reap.d.ts +26 -0
  110. package/packages/lsp-daemon/dist/version-reap.js +182 -0
  111. package/packages/lsp-tools-mcp/dist/cli.js +61 -10
  112. package/packages/lsp-tools-mcp/dist/lsp/manager.js +6 -1
  113. package/packages/lsp-tools-mcp/dist/mcp.js +61 -10
  114. package/packages/lsp-tools-mcp/dist/tools.js +6 -1
  115. package/packages/omo-codex/plugin/.codex-plugin/plugin.json +1 -1
  116. package/packages/omo-codex/plugin/components/bootstrap/dist/cli.js +72 -12
  117. package/packages/omo-codex/plugin/components/bootstrap/hooks/hooks.json +1 -1
  118. package/packages/omo-codex/plugin/components/bootstrap/package.json +1 -1
  119. package/packages/omo-codex/plugin/components/bootstrap/src/setup.ts +22 -2
  120. package/packages/omo-codex/plugin/components/codegraph/dist/cli.js +779 -385
  121. package/packages/omo-codex/plugin/components/codegraph/dist/serve.js +302 -173
  122. package/packages/omo-codex/plugin/components/codegraph/package.json +2 -2
  123. package/packages/omo-codex/plugin/components/codegraph/src/hook-types.ts +3 -1
  124. package/packages/omo-codex/plugin/components/codegraph/src/hook.ts +23 -42
  125. package/packages/omo-codex/plugin/components/codegraph/src/mcp-bridge.ts +98 -32
  126. package/packages/omo-codex/plugin/components/codegraph/src/mcp-unavailable.ts +5 -0
  127. package/packages/omo-codex/plugin/components/codegraph/src/serve.ts +17 -9
  128. package/packages/omo-codex/plugin/components/codegraph/src/session-start-worker.ts +18 -25
  129. package/packages/omo-codex/plugin/components/codegraph/src/sweep-cli.ts +1 -0
  130. package/packages/omo-codex/plugin/components/codegraph/test/hook-exclusion.test.ts +5 -0
  131. package/packages/omo-codex/plugin/components/codegraph/test/hook-store-upgrade.test.ts +176 -0
  132. package/packages/omo-codex/plugin/components/codegraph/test/hook.test.ts +6 -1
  133. package/packages/omo-codex/plugin/components/codegraph/test/mcp-bridge-fixtures.ts +31 -2
  134. package/packages/omo-codex/plugin/components/codegraph/test/package-runtime.test.ts +1 -1
  135. package/packages/omo-codex/plugin/components/codegraph/test/provisioned-node-guard.test.ts +6 -0
  136. package/packages/omo-codex/plugin/components/codegraph/test/serve-mcp-bridge-lifecycle.test.ts +65 -1
  137. package/packages/omo-codex/plugin/components/codegraph/test/serve-mcp-bridge.test.ts +3 -7
  138. package/packages/omo-codex/plugin/components/codegraph/test/serve-mcp-facade.test.ts +2 -1
  139. package/packages/omo-codex/plugin/components/codegraph/test/serve-provision.test.ts +57 -3
  140. package/packages/omo-codex/plugin/components/codegraph/test/serve-unavailable.test.ts +42 -0
  141. package/packages/omo-codex/plugin/components/codegraph/test/serve.test.ts +122 -10
  142. package/packages/omo-codex/plugin/components/codegraph/test/session-start-node-support.test.ts +2 -2
  143. package/packages/omo-codex/plugin/components/codegraph/test/session-start-worker-flow.test.ts +101 -64
  144. package/packages/omo-codex/plugin/components/comment-checker/hooks/hooks.json +1 -1
  145. package/packages/omo-codex/plugin/components/comment-checker/package.json +1 -1
  146. package/packages/omo-codex/plugin/components/git-bash/hooks/hooks.json +2 -2
  147. package/packages/omo-codex/plugin/components/git-bash/package.json +1 -1
  148. package/packages/omo-codex/plugin/components/lazycodex-executor-verify/hooks/hooks.json +1 -1
  149. package/packages/omo-codex/plugin/components/lazycodex-executor-verify/package.json +1 -1
  150. package/packages/omo-codex/plugin/components/lsp/dist/.omo-runtime-manifest.json +3 -3
  151. package/packages/omo-codex/plugin/components/lsp/dist/cli.js +80 -40
  152. package/packages/omo-codex/plugin/components/lsp/hooks/hooks.json +2 -2
  153. package/packages/omo-codex/plugin/components/lsp/package.json +1 -1
  154. package/packages/omo-codex/plugin/components/rules/dist/cli.js +11 -0
  155. package/packages/omo-codex/plugin/components/rules/hooks/hooks.json +4 -4
  156. package/packages/omo-codex/plugin/components/rules/package.json +1 -1
  157. package/packages/omo-codex/plugin/components/rules/src/post-compact-budget.ts +11 -0
  158. package/packages/omo-codex/plugin/components/rules/test/post-compact-budget.test.ts +60 -0
  159. package/packages/omo-codex/plugin/components/start-work-continuation/directive.md +1 -1
  160. package/packages/omo-codex/plugin/components/start-work-continuation/hooks/hooks.json +2 -2
  161. package/packages/omo-codex/plugin/components/start-work-continuation/package.json +1 -1
  162. package/packages/omo-codex/plugin/components/teammode/hooks/hooks.json +1 -1
  163. package/packages/omo-codex/plugin/components/teammode/package.json +1 -1
  164. package/packages/omo-codex/plugin/components/telemetry/hooks/hooks.json +1 -1
  165. package/packages/omo-codex/plugin/components/telemetry/package.json +1 -1
  166. package/packages/omo-codex/plugin/components/ultrawork/agents/lazycodex-clone-fidelity-reviewer.toml +2 -2
  167. package/packages/omo-codex/plugin/components/ultrawork/agents/lazycodex-code-reviewer.toml +2 -2
  168. package/packages/omo-codex/plugin/components/ultrawork/agents/lazycodex-gate-reviewer.toml +1 -1
  169. package/packages/omo-codex/plugin/components/ultrawork/agents/lazycodex-worker-high.toml +1 -1
  170. package/packages/omo-codex/plugin/components/ultrawork/agents/lazycodex-worker-medium.toml +2 -2
  171. package/packages/omo-codex/plugin/components/ultrawork/agents/plan.toml +1 -1
  172. package/packages/omo-codex/plugin/components/ultrawork/directive.md +12 -7
  173. package/packages/omo-codex/plugin/components/ultrawork/dist/cli.js +1 -1
  174. package/packages/omo-codex/plugin/components/ultrawork/hooks/hooks.json +1 -1
  175. package/packages/omo-codex/plugin/components/ultrawork/package.json +1 -1
  176. package/packages/omo-codex/plugin/components/ultrawork/skills/ultrawork/SKILL.md +12 -7
  177. package/packages/omo-codex/plugin/components/ultrawork/skills/ulw-plan/SKILL.md +21 -2
  178. package/packages/omo-codex/plugin/components/ultrawork/skills/ulw-plan/references/full-workflow.md +12 -1
  179. package/packages/omo-codex/plugin/components/ultrawork/skills/ulw-plan/references/intent-clear.md +1 -1
  180. package/packages/omo-codex/plugin/components/ultrawork/src/codex-hook.ts +1 -1
  181. package/packages/omo-codex/plugin/components/ultrawork/test/codex-hook-trigger-policy.test.ts +42 -0
  182. package/packages/omo-codex/plugin/components/ulw-loop/README.md +6 -4
  183. package/packages/omo-codex/plugin/components/ulw-loop/directive.md +12 -7
  184. package/packages/omo-codex/plugin/components/ulw-loop/dist/checkpoint-continuation.d.ts +21 -0
  185. package/packages/omo-codex/plugin/components/ulw-loop/dist/checkpoint-continuation.js +64 -0
  186. package/packages/omo-codex/plugin/components/ulw-loop/dist/checkpoint.js +15 -2
  187. package/packages/omo-codex/plugin/components/ulw-loop/dist/cli-arg-parser.js +1 -1
  188. package/packages/omo-codex/plugin/components/ulw-loop/dist/cli-commands.js +2 -1
  189. package/packages/omo-codex/plugin/components/ulw-loop/dist/cli-output.d.ts +1 -1
  190. package/packages/omo-codex/plugin/components/ulw-loop/dist/cli-output.js +3 -3
  191. package/packages/omo-codex/plugin/components/ulw-loop/dist/cli-steering.d.ts +3 -0
  192. package/packages/omo-codex/plugin/components/ulw-loop/dist/cli-steering.js +85 -0
  193. package/packages/omo-codex/plugin/components/ulw-loop/dist/cli-subcommands.d.ts +0 -1
  194. package/packages/omo-codex/plugin/components/ulw-loop/dist/cli-subcommands.js +13 -33
  195. package/packages/omo-codex/plugin/components/ulw-loop/dist/cli.js +998 -593
  196. package/packages/omo-codex/plugin/components/ulw-loop/dist/codex-goal-instruction.js +1 -0
  197. package/packages/omo-codex/plugin/components/ulw-loop/dist/constants.d.ts +1 -1
  198. package/packages/omo-codex/plugin/components/ulw-loop/dist/constants.js +2 -0
  199. package/packages/omo-codex/plugin/components/ulw-loop/dist/domain-types.d.ts +6 -0
  200. package/packages/omo-codex/plugin/components/ulw-loop/dist/goal-status.d.ts +1 -0
  201. package/packages/omo-codex/plugin/components/ulw-loop/dist/goal-status.js +3 -0
  202. package/packages/omo-codex/plugin/components/ulw-loop/dist/plan-crud.d.ts +1 -0
  203. package/packages/omo-codex/plugin/components/ulw-loop/dist/plan-crud.js +4 -0
  204. package/packages/omo-codex/plugin/components/ulw-loop/dist/plan-io.d.ts +1 -0
  205. package/packages/omo-codex/plugin/components/ulw-loop/dist/plan-io.js +10 -2
  206. package/packages/omo-codex/plugin/components/ulw-loop/dist/quality-gate-verdicts.js +1 -1
  207. package/packages/omo-codex/plugin/components/ulw-loop/dist/steering-batch.d.ts +15 -0
  208. package/packages/omo-codex/plugin/components/ulw-loop/dist/steering-batch.js +84 -0
  209. package/packages/omo-codex/plugin/components/ulw-loop/dist/steering-mutations.d.ts +5 -0
  210. package/packages/omo-codex/plugin/components/ulw-loop/dist/steering-mutations.js +81 -0
  211. package/packages/omo-codex/plugin/components/ulw-loop/dist/steering.js +7 -59
  212. package/packages/omo-codex/plugin/components/ulw-loop/dist/stop-resume-hook.js +12 -4
  213. package/packages/omo-codex/plugin/components/ulw-loop/dist/validation-batch.d.ts +9 -0
  214. package/packages/omo-codex/plugin/components/ulw-loop/dist/validation-batch.js +122 -0
  215. package/packages/omo-codex/plugin/components/ulw-loop/hooks/hooks.json +4 -4
  216. package/packages/omo-codex/plugin/components/ulw-loop/package.json +1 -1
  217. package/packages/omo-codex/plugin/components/ulw-loop/skills/ulw-loop/SKILL.md +2 -1
  218. package/packages/omo-codex/plugin/components/ulw-loop/skills/ulw-loop/references/full-workflow.md +8 -6
  219. package/packages/omo-codex/plugin/components/ulw-loop/src/checkpoint-continuation.ts +84 -0
  220. package/packages/omo-codex/plugin/components/ulw-loop/src/checkpoint.ts +13 -2
  221. package/packages/omo-codex/plugin/components/ulw-loop/src/cli-arg-parser.ts +1 -1
  222. package/packages/omo-codex/plugin/components/ulw-loop/src/cli-commands.ts +1 -1
  223. package/packages/omo-codex/plugin/components/ulw-loop/src/cli-output.ts +3 -3
  224. package/packages/omo-codex/plugin/components/ulw-loop/src/cli-steering.ts +49 -0
  225. package/packages/omo-codex/plugin/components/ulw-loop/src/cli-subcommands.ts +13 -45
  226. package/packages/omo-codex/plugin/components/ulw-loop/src/codex-goal-instruction.ts +1 -0
  227. package/packages/omo-codex/plugin/components/ulw-loop/src/constants.ts +2 -0
  228. package/packages/omo-codex/plugin/components/ulw-loop/src/domain-types.ts +7 -0
  229. package/packages/omo-codex/plugin/components/ulw-loop/src/goal-status.ts +4 -0
  230. package/packages/omo-codex/plugin/components/ulw-loop/src/plan-crud.ts +4 -1
  231. package/packages/omo-codex/plugin/components/ulw-loop/src/plan-io.ts +20 -2
  232. package/packages/omo-codex/plugin/components/ulw-loop/src/quality-gate-verdicts.ts +1 -1
  233. package/packages/omo-codex/plugin/components/ulw-loop/src/steering-batch.ts +125 -0
  234. package/packages/omo-codex/plugin/components/ulw-loop/src/steering-mutations.ts +59 -0
  235. package/packages/omo-codex/plugin/components/ulw-loop/src/steering.ts +6 -58
  236. package/packages/omo-codex/plugin/components/ulw-loop/src/stop-resume-hook.ts +12 -4
  237. package/packages/omo-codex/plugin/components/ulw-loop/src/validation-batch.ts +122 -0
  238. package/packages/omo-codex/plugin/components/ulw-loop/test/checkpoint-continuation.test.ts +76 -0
  239. package/packages/omo-codex/plugin/components/ulw-loop/test/cli-checkpoint-continuation.test.ts +111 -0
  240. package/packages/omo-codex/plugin/components/ulw-loop/test/cli-steering-batch.test.ts +67 -0
  241. package/packages/omo-codex/plugin/components/ulw-loop/test/cli-validation-batch.test.ts +57 -0
  242. package/packages/omo-codex/plugin/components/ulw-loop/test/paths.test.ts +1 -0
  243. package/packages/omo-codex/plugin/components/ulw-loop/test/steering-batch.test.ts +152 -0
  244. package/packages/omo-codex/plugin/components/ulw-loop/test/stop-resume-hook.test.ts +29 -0
  245. package/packages/omo-codex/plugin/components/ulw-loop/test/validation-batch-checkpoint.test.ts +147 -0
  246. package/packages/omo-codex/plugin/components/ulw-loop/test/validation-batch.test.ts +73 -0
  247. package/packages/omo-codex/plugin/hooks/post-compact-resetting-git-bash-mcp-reminder.json +1 -1
  248. package/packages/omo-codex/plugin/hooks/post-compact-resetting-lsp-diagnostics-cache.json +1 -1
  249. package/packages/omo-codex/plugin/hooks/post-compact-resetting-project-rule-cache.json +1 -1
  250. package/packages/omo-codex/plugin/hooks/post-tool-use-checking-codegraph-init-guidance.json +1 -1
  251. package/packages/omo-codex/plugin/hooks/post-tool-use-checking-comments.json +1 -1
  252. package/packages/omo-codex/plugin/hooks/post-tool-use-checking-lsp-diagnostics.json +1 -1
  253. package/packages/omo-codex/plugin/hooks/post-tool-use-checking-thread-title-hygiene.json +1 -1
  254. package/packages/omo-codex/plugin/hooks/post-tool-use-matching-project-rules.json +1 -1
  255. package/packages/omo-codex/plugin/hooks/pre-tool-use-enforcing-unlimited-goal-budget.json +1 -1
  256. package/packages/omo-codex/plugin/hooks/pre-tool-use-guarding-ulw-loop-spawns.json +1 -1
  257. package/packages/omo-codex/plugin/hooks/pre-tool-use-recommending-git-bash-mcp.json +1 -1
  258. package/packages/omo-codex/plugin/hooks/session-start-checking-auto-update.json +2 -2
  259. package/packages/omo-codex/plugin/hooks/session-start-checking-bootstrap-provisioning.json +1 -1
  260. package/packages/omo-codex/plugin/hooks/session-start-checking-codegraph-bootstrap.json +2 -2
  261. package/packages/omo-codex/plugin/hooks/session-start-loading-project-rules.json +1 -1
  262. package/packages/omo-codex/plugin/hooks/session-start-recording-session-telemetry.json +2 -2
  263. package/packages/omo-codex/plugin/hooks/stop-checking-start-work-continuation.json +1 -1
  264. package/packages/omo-codex/plugin/hooks/stop-checking-ulw-loop-resume.json +1 -1
  265. package/packages/omo-codex/plugin/hooks/subagent-stop-checking-start-work-continuation.json +1 -1
  266. package/packages/omo-codex/plugin/hooks/subagent-stop-verifying-lazycodex-executor-evidence.json +1 -1
  267. package/packages/omo-codex/plugin/hooks/user-prompt-submit-checking-ultrawork-trigger.json +1 -1
  268. package/packages/omo-codex/plugin/hooks/user-prompt-submit-checking-ulw-loop-steering.json +1 -1
  269. package/packages/omo-codex/plugin/hooks/user-prompt-submit-loading-project-rules.json +1 -1
  270. package/packages/omo-codex/plugin/package-lock.json +41 -41
  271. package/packages/omo-codex/plugin/package.json +1 -1
  272. package/packages/omo-codex/plugin/shared/test/config-loader.test.ts +44 -0
  273. package/packages/omo-codex/plugin/skills/coding-agent-sessions/SKILL.md +5 -2
  274. package/packages/omo-codex/plugin/skills/coding-agent-sessions/references/all-platforms.md +3 -1
  275. package/packages/omo-codex/plugin/skills/coding-agent-sessions/references/senpi.md +15 -5
  276. package/packages/omo-codex/plugin/skills/coding-agent-sessions/scripts/agent_sessions/pi_family.py +46 -0
  277. package/packages/omo-codex/plugin/skills/coding-agent-sessions/scripts/agent_sessions/scanners.py +19 -10
  278. package/packages/omo-codex/plugin/skills/debugging/SKILL.md +2 -1
  279. package/packages/omo-codex/plugin/skills/debugging/references/methodology/03-flaky-triage.md +47 -0
  280. package/packages/omo-codex/plugin/skills/programming/SKILL.md +11 -1
  281. package/packages/omo-codex/plugin/skills/programming/references/logging.md +6 -1
  282. package/packages/omo-codex/plugin/skills/programming/scripts/typescript/check-no-excuse-rules.ts +75 -11
  283. package/packages/omo-codex/plugin/skills/start-work/SKILL.md +12 -2
  284. package/packages/omo-codex/plugin/skills/ultrawork/SKILL.md +12 -7
  285. package/packages/omo-codex/plugin/skills/ulw-loop/SKILL.md +2 -1
  286. package/packages/omo-codex/plugin/skills/ulw-loop/references/full-workflow.md +8 -6
  287. package/packages/omo-codex/plugin/skills/ulw-plan/SKILL.md +21 -2
  288. package/packages/omo-codex/plugin/skills/ulw-plan/references/full-workflow.md +12 -1
  289. package/packages/omo-codex/plugin/skills/ulw-plan/references/intent-clear.md +1 -1
  290. package/packages/omo-codex/plugin/test/aggregate-agents.test.mjs +9 -9
  291. package/packages/omo-codex/plugin/test/aggregate-hooks.test.mjs +26 -0
  292. package/packages/omo-codex/plugin/test/bootstrap-setup.test.mjs +41 -0
  293. package/packages/omo-codex/plugin/test/sync-skills-orchestration.test.mjs +22 -0
  294. package/packages/omo-codex/scripts/install-dist/install-local.mjs +49 -5
  295. package/packages/omo-codex/scripts/install-marketplace-cache.test.mjs +4 -1
  296. package/packages/shared-skills/skills/coding-agent-sessions/SKILL.md +5 -2
  297. package/packages/shared-skills/skills/coding-agent-sessions/references/all-platforms.md +3 -1
  298. package/packages/shared-skills/skills/coding-agent-sessions/references/senpi.md +15 -5
  299. package/packages/shared-skills/skills/coding-agent-sessions/scripts/agent_sessions/pi_family.py +46 -0
  300. package/packages/shared-skills/skills/coding-agent-sessions/scripts/agent_sessions/scanners.py +19 -10
  301. package/packages/shared-skills/skills/debugging/SKILL.md +2 -1
  302. package/packages/shared-skills/skills/debugging/references/methodology/03-flaky-triage.md +47 -0
  303. package/packages/shared-skills/skills/programming/SKILL.md +11 -1
  304. package/packages/shared-skills/skills/programming/references/logging.md +6 -1
  305. package/packages/shared-skills/skills/programming/scripts/typescript/check-no-excuse-rules.test.ts +87 -0
  306. package/packages/shared-skills/skills/programming/scripts/typescript/check-no-excuse-rules.ts +75 -11
  307. package/packages/shared-skills/skills/start-work/SKILL.md +12 -2
  308. package/packages/shared-skills/skills/ulw-plan/SKILL.md +21 -2
  309. package/packages/shared-skills/skills/ulw-plan/references/full-workflow.md +12 -1
  310. package/packages/shared-skills/skills/ulw-plan/references/intent-clear.md +1 -1
@@ -1,7 +1,16 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- // components/ulw-loop/src/cli-arg-parser.ts
3
+ // components/ulw-loop/src/checkpoint.ts
4
+ import { existsSync as existsSync3, statSync } from "node:fs";
5
+ import { readFile as readFile5 } from "node:fs/promises";
6
+ import { resolve as resolve3 } from "node:path";
7
+
8
+ // components/ulw-loop/src/checkpoint-reconciliation.ts
9
+ import { existsSync } from "node:fs";
4
10
  import { readFile } from "node:fs/promises";
11
+
12
+ // components/ulw-loop/src/paths.ts
13
+ import { isAbsolute, join, relative, sep } from "node:path";
5
14
  // components/ulw-loop/src/constants.ts
6
15
  var ULW_LOOP_DIR = ".omo/ulw-loop";
7
16
  var ULW_LOOP_BRIEF = "brief.md";
@@ -38,179 +47,7 @@ class UlwLoopError extends Error {
38
47
  function iso() {
39
48
  return new Date().toISOString();
40
49
  }
41
- // components/ulw-loop/src/cli-arg-parser.ts
42
- var VALUE_FLAGS = new Set("--brief --brief-file --session-id --codex-goal-mode --goal --goal-id --criterion-id --status --evidence --notes --codex-goal-json --quality-gate-json --kind --rationale --title --objective --target-goal-id --source --after-json --directive-json --directive-file --idempotency-key".split(" "));
43
- var SUBCOMMANDS = new Set("create-goals status complete-goals criteria record-evidence checkpoint steer add-goal record-review-blockers".split(" "));
44
- function hasFlag(argv, flag) {
45
- return argv.includes(flag);
46
- }
47
- function readValue(argv, flag) {
48
- const index = argv.indexOf(flag);
49
- if (index >= 0) {
50
- const next = argv[index + 1];
51
- return next === undefined || next.startsWith("--") ? undefined : next;
52
- }
53
- const prefix = `${flag}=`;
54
- return argv.find((arg) => arg.startsWith(prefix))?.slice(prefix.length);
55
- }
56
- function parseGoalArg(argv) {
57
- return readValue(argv, "--goal-id") ?? readValue(argv, "--goal");
58
- }
59
- async function readStdin() {
60
- const chunks = [];
61
- for await (const chunk of process.stdin)
62
- chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
63
- return Buffer.concat(chunks).toString("utf8");
64
- }
65
- function positionalText(argv) {
66
- const words = [];
67
- for (let index = SUBCOMMANDS.has(argv[0] ?? "") ? 1 : 0;index < argv.length; index += 1) {
68
- const arg = argv[index];
69
- if (arg === undefined)
70
- continue;
71
- if (VALUE_FLAGS.has(arg)) {
72
- index += 1;
73
- continue;
74
- }
75
- if (arg.startsWith("--"))
76
- continue;
77
- words.push(arg);
78
- }
79
- return words.join(" ").trim();
80
- }
81
- function looksLikeJson(value) {
82
- const trimmed = value.trim();
83
- return trimmed.startsWith("{") || trimmed.startsWith("[");
84
- }
85
- async function readJsonInput(value) {
86
- if (value === undefined)
87
- return;
88
- try {
89
- return JSON.parse(looksLikeJson(value) ? value : await readFile(value, "utf8"));
90
- } catch (error) {
91
- const message = error instanceof Error ? error.message : "unknown error";
92
- throw new UlwLoopError(`Invalid JSON input: ${message}`, "ULW_LOOP_JSON_INPUT_INVALID", { cause: error });
93
- }
94
- }
95
- async function parseCodexGoalJson(value) {
96
- if (value === undefined)
97
- return;
98
- const raw = looksLikeJson(value) ? value : await readFile(value, "utf8");
99
- try {
100
- JSON.parse(raw);
101
- return raw;
102
- } catch (error) {
103
- const message = error instanceof Error ? error.message : "unknown error";
104
- throw new UlwLoopError(`Invalid --codex-goal-json: ${message}`, "ULW_LOOP_CODEX_GOAL_JSON_INVALID", { cause: error });
105
- }
106
- }
107
- function required(argv, flag, code) {
108
- const value = readValue(argv, flag)?.trim();
109
- if (value)
110
- return value;
111
- throw new UlwLoopError(`Missing ${flag}.`, code, { details: { flag } });
112
- }
113
- function evidenceStatus(value) {
114
- switch (value) {
115
- case "pass":
116
- return "pass";
117
- case "fail":
118
- return "fail";
119
- case "blocked":
120
- return "blocked";
121
- default:
122
- throw new UlwLoopError("Invalid --status; expected pass, fail, or blocked.", "ULW_LOOP_EVIDENCE_STATUS_INVALID", { details: { status: value } });
123
- }
124
- }
125
- function parseRecordEvidenceArgs(argv) {
126
- const result = { goalId: required(argv, "--goal-id", "ULW_LOOP_GOAL_ID_REQUIRED"), criterionId: required(argv, "--criterion-id", "ULW_LOOP_CRITERION_ID_REQUIRED"), status: evidenceStatus(required(argv, "--status", "ULW_LOOP_EVIDENCE_STATUS_REQUIRED")), evidence: required(argv, "--evidence", "ULW_LOOP_EVIDENCE_REQUIRED") };
127
- const notes = readValue(argv, "--notes")?.trim();
128
- return notes ? { ...result, notes } : result;
129
- }
130
-
131
- // components/ulw-loop/src/cli-output.ts
132
- var ULW_LOOP_HELP = `Usage:
133
- omo ulw-loop create-goals --brief "..." [--brief-file <path>] [--from-stdin] [--codex-goal-mode aggregate|per_story] [--force] [--json]
134
- omo ulw-loop status [--json]
135
- omo ulw-loop complete-goals [--retry-failed] [--json]
136
- omo ulw-loop criteria --goal-id <id> [--json]
137
- omo ulw-loop record-evidence --goal-id <id> --criterion-id <id> --status pass|fail|blocked --evidence "..." [--notes "..."] [--json]
138
- omo ulw-loop checkpoint --goal-id <id> --status complete|failed|blocked --evidence "..." --codex-goal-json <...> [--quality-gate-json <...>] [--json]
139
- omo ulw-loop steer --kind <kind> ... --evidence "..." --rationale "..." [--json]
140
- omo ulw-loop add-goal --title "..." --objective "..." [--json]
141
- omo ulw-loop record-review-blockers --goal-id <id> --title "..." --objective "..." --evidence "..." --codex-goal-json <...> [--json]
142
-
143
- All subcommands accept [--session-id <id>] to isolate state under .omo/ulw-loop/<id>/; without it, Codex session env is used when present.`;
144
- function printJson(value) {
145
- process.stdout.write(`${JSON.stringify(value, null, 2)}
146
- `);
147
- }
148
- function printJsonError(error) {
149
- if (error instanceof UlwLoopError) {
150
- printJson({
151
- ok: false,
152
- error: {
153
- code: error.code,
154
- message: error.message,
155
- ...error.details === undefined ? {} : { details: error.details }
156
- }
157
- });
158
- return;
159
- }
160
- if (error instanceof Error) {
161
- printJson({ ok: false, error: { code: "ULW_LOOP_UNEXPECTED", message: error.message } });
162
- return;
163
- }
164
- printJson({ ok: false, error: { code: "ULW_LOOP_UNKNOWN", message: "unknown error" } });
165
- }
166
- function criteriaCounts(goal) {
167
- let pass = 0;
168
- for (const criterion of goal.successCriteria)
169
- if (criterion.status === "pass")
170
- pass += 1;
171
- return { pass, total: goal.successCriteria.length };
172
- }
173
- function printStatus(plan) {
174
- let totalCriteria = 0;
175
- let passCriteria = 0;
176
- const lines = ["ulw-loop status", "", "goals:"];
177
- for (const goal of plan.goals) {
178
- const counts = criteriaCounts(goal);
179
- totalCriteria += counts.total;
180
- passCriteria += counts.pass;
181
- const marker = goal.id === plan.activeGoalId ? "*" : "-";
182
- lines.push(`${marker} ${goal.id} [${goal.status}] ${goal.title} (criteria: ${counts.pass}/${counts.total})`);
183
- }
184
- lines.push("", "summary:", `total goals: ${plan.goals.length}`, `criteria: ${passCriteria}/${totalCriteria} pass`);
185
- process.stdout.write(`${lines.join(`
186
- `)}
187
- `);
188
- }
189
- function blockedDecisionHandoff(plan) {
190
- const blocked = plan.goals.find((goal) => goal.status === "needs_user_decision" && goal.nonRetriable);
191
- if (blocked === undefined)
192
- return "";
193
- return [
194
- "ulw-loop: blocked on repeated external authorization; no retryable failed goals remain.",
195
- `Goal: ${blocked.id} - ${blocked.title}`,
196
- `Required external decision: ${blocked.requiredExternalDecision ?? "provide the missing authorization or choose a different unblock path"}.`,
197
- "Do not run complete-goals --retry-failed again until external state changes or the user authorizes an unblock path."
198
- ].join(`
199
- `);
200
- }
201
- function normalizeCodexGoalMode(value) {
202
- if (value === undefined)
203
- return "aggregate";
204
- if (value === "aggregate" || value === "per_story")
205
- return value;
206
- throw new UlwLoopError("Invalid --codex-goal-mode; expected aggregate or per_story.", "ULW_LOOP_CODEX_GOAL_MODE_INVALID", { details: { value } });
207
- }
208
-
209
- // components/ulw-loop/src/cli-subcommands.ts
210
- import { readFile as readFile6 } from "node:fs/promises";
211
-
212
50
  // components/ulw-loop/src/paths.ts
213
- import { isAbsolute, join, relative, sep } from "node:path";
214
51
  var SESSION_ENV_KEYS = ["OMO_ULW_LOOP_SESSION_ID", "CODEX_SESSION_ID", "CODEX_THREAD_ID"];
215
52
  function normalizeUlwLoopSessionId(sessionId) {
216
53
  const trimmed = sessionId?.trim();
@@ -276,15 +113,6 @@ function isWithinAttemptDir(absolutePath, attemptRoot, pathApi = PLATFORM_PATH_A
276
113
  return !pathApi.isAbsolute(relativePath);
277
114
  }
278
115
 
279
- // components/ulw-loop/src/checkpoint.ts
280
- import { existsSync as existsSync3, statSync } from "node:fs";
281
- import { readFile as readFile5 } from "node:fs/promises";
282
- import { resolve as resolve3 } from "node:path";
283
-
284
- // components/ulw-loop/src/checkpoint-reconciliation.ts
285
- import { existsSync } from "node:fs";
286
- import { readFile as readFile2 } from "node:fs/promises";
287
-
288
116
  // components/ulw-loop/src/goal-status.ts
289
117
  var ULW_LOOP_AGGREGATE_CODEX_OBJECTIVE = aggregateCodexObjectiveForScope();
290
118
  function aggregateCodexObjectiveForScope(scope) {
@@ -296,6 +124,9 @@ function codexGoalMode(plan) {
296
124
  function isResolvedStatus(status) {
297
125
  return status === "complete";
298
126
  }
127
+ function isMemberResolved(goal, plan) {
128
+ return isResolvedStatus(goal.status) || isSupersededResolved(goal, plan);
129
+ }
299
130
  function isSupersededResolved(goal, plan) {
300
131
  if (goal.steeringStatus !== "superseded")
301
132
  return false;
@@ -389,7 +220,7 @@ async function snapshotObjectiveMapsToUlwLoopPlan(repoRoot, snapshotObjective, s
389
220
  if (actual.length < 24 || !existsSync(ulwLoopBriefPath(repoRoot, scope)))
390
221
  return false;
391
222
  try {
392
- const brief = normalizeObjective(await readFile2(ulwLoopBriefPath(repoRoot, scope), "utf8")).toLowerCase();
223
+ const brief = normalizeObjective(await readFile(ulwLoopBriefPath(repoRoot, scope), "utf8")).toLowerCase();
393
224
  return brief.length >= 24 && (brief.includes(actual) || actual.includes(brief));
394
225
  } catch (error) {
395
226
  if (error instanceof Error)
@@ -438,7 +269,7 @@ function buildTaskScopedAggregateReconciliationHint(goal, final) {
438
269
 
439
270
  // components/ulw-loop/src/codex-goal-snapshot.ts
440
271
  import { existsSync as existsSync2 } from "node:fs";
441
- import { readFile as readFile3 } from "node:fs/promises";
272
+ import { readFile as readFile2 } from "node:fs/promises";
442
273
  import { resolve } from "node:path";
443
274
 
444
275
  class CodexGoalSnapshotError extends Error {
@@ -492,7 +323,7 @@ async function readCodexGoalSnapshotInput(raw, cwd = process.cwd()) {
492
323
  throw new CodexGoalSnapshotError(`Codex goal snapshot is neither valid JSON nor a readable path: ${trimmed}`);
493
324
  }
494
325
  try {
495
- return parseCodexGoalSnapshot(JSON.parse(await readFile3(path, "utf-8")));
326
+ return parseCodexGoalSnapshot(JSON.parse(await readFile2(path, "utf-8")));
496
327
  } catch (error) {
497
328
  throw new CodexGoalSnapshotError(`Codex goal snapshot path does not contain valid JSON: ${trimmed}${error instanceof Error ? ` (${error.message})` : ""}`);
498
329
  }
@@ -535,7 +366,7 @@ function formatCodexGoalReconciliation(reconciliation) {
535
366
 
536
367
  // components/ulw-loop/src/plan-io.ts
537
368
  import { createReadStream } from "node:fs";
538
- import { appendFile, mkdir, readFile as readFile4, rename, writeFile } from "node:fs/promises";
369
+ import { appendFile, mkdir, readFile as readFile3, rename, writeFile } from "node:fs/promises";
539
370
  import { createInterface } from "node:readline";
540
371
  var LEGACY_OBJECTIVE_PREFIX = `Complete all ulw-loop stories in ${ULW_LOOP_DIR}/${ULW_LOOP_GOALS}: `;
541
372
  var LEGACY_OBJECTIVE = `Complete all ulw-loop stories listed in ${ULW_LOOP_DIR}/${ULW_LOOP_GOALS}. Use ${ULW_LOOP_DIR}/${ULW_LOOP_LEDGER} as the durable audit trail.`;
@@ -547,7 +378,7 @@ function isLegacyEnumeratedAggregateObjective(objective) {
547
378
  return objective === LEGACY_OBJECTIVE || Boolean(objective?.startsWith(LEGACY_OBJECTIVE_PREFIX));
548
379
  }
549
380
  function isSteeringKind(value) {
550
- return value === "steering_accepted" || value === "steering_rejected" || value === "criteria_revised";
381
+ return value === "steering_accepted" || value === "steering_rejected" || value === "criteria_revised" || value === "batch_updated";
551
382
  }
552
383
  async function withUlwLoopMutationLock(repoRoot, scopeOrFn, maybeFn) {
553
384
  const scope = typeof scopeOrFn === "function" ? undefined : scopeOrFn;
@@ -573,7 +404,7 @@ async function readUlwLoopPlan(repoRoot, scope) {
573
404
  const path = ulwLoopGoalsPath(repoRoot, scope);
574
405
  let raw;
575
406
  try {
576
- raw = await readFile4(path, "utf8");
407
+ raw = await readFile3(path, "utf8");
577
408
  } catch (error) {
578
409
  if (!hasCode(error, "ENOENT"))
579
410
  throw error;
@@ -609,8 +440,14 @@ async function writePlan(repoRoot, plan, scope) {
609
440
  await rename(tmpPath, path);
610
441
  }
611
442
  async function appendLedger(repoRoot, entry, scope) {
443
+ await appendLedgerEntries(repoRoot, [entry], scope);
444
+ }
445
+ async function appendLedgerEntries(repoRoot, entries, scope) {
446
+ if (entries.length === 0)
447
+ return;
612
448
  await mkdir(ulwLoopDir(repoRoot, scope), { recursive: true });
613
- await appendFile(ulwLoopLedgerPath(repoRoot, scope), `${JSON.stringify(entry)}
449
+ await appendFile(ulwLoopLedgerPath(repoRoot, scope), `${entries.map((entry) => JSON.stringify(entry)).join(`
450
+ `)}
614
451
  `, "utf8");
615
452
  }
616
453
  async function* ledgerLines(repoRoot, scope) {
@@ -1039,36 +876,245 @@ function parseAdversarialCases(value, byId) {
1039
876
  });
1040
877
  }
1041
878
 
1042
- // components/ulw-loop/src/checkpoint.ts
1043
- var QUALITY_GATE_FS = { existsSync: existsSync3, statSync };
1044
- function ulwLoopFail2(message, code) {
1045
- throw new UlwLoopError(message, code);
879
+ // components/ulw-loop/src/cli-arg-parser.ts
880
+ import { readFile as readFile4 } from "node:fs/promises";
881
+ var VALUE_FLAGS = new Set("--brief --brief-file --session-id --codex-goal-mode --validation-batch-json --goal --goal-id --criterion-id --status --evidence --notes --codex-goal-json --quality-gate-json --kind --rationale --title --objective --target-goal-id --source --after-json --directive-json --directive-file --idempotency-key --proposals-json".split(" "));
882
+ var SUBCOMMANDS = new Set("create-goals status complete-goals criteria record-evidence checkpoint steer add-goal record-review-blockers".split(" "));
883
+ function hasFlag(argv, flag) {
884
+ return argv.includes(flag);
1046
885
  }
1047
- function normalizeObjective3(value) {
1048
- return value.replace(/\s+/g, " ").trim();
886
+ function readValue(argv, flag) {
887
+ const index = argv.indexOf(flag);
888
+ if (index >= 0) {
889
+ const next = argv[index + 1];
890
+ return next === undefined || next.startsWith("--") ? undefined : next;
891
+ }
892
+ const prefix = `${flag}=`;
893
+ return argv.find((arg) => arg.startsWith(prefix))?.slice(prefix.length);
1049
894
  }
1050
- function nonEmptyEvidence2(value) {
1051
- const trimmed = value.trim();
1052
- return trimmed || ulwLoopFail2("Evidence must be a non-empty string.", "ulw_loop_evidence_required");
895
+ function parseGoalArg(argv) {
896
+ return readValue(argv, "--goal-id") ?? readValue(argv, "--goal");
1053
897
  }
1054
- function findGoal2(plan, goalId) {
1055
- const goal = plan.goals.find((candidate) => candidate.id === goalId);
1056
- return goal ?? ulwLoopFail2(`Unknown ulw-loop id: ${goalId}.`, "ulw_loop_goal_not_found");
898
+ async function readStdin() {
899
+ const chunks = [];
900
+ for await (const chunk of process.stdin)
901
+ chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
902
+ return Buffer.concat(chunks).toString("utf8");
1057
903
  }
1058
- async function readJsonInput2(raw, repoRoot) {
1059
- if (raw === undefined || raw.trim() === "")
904
+ function positionalText(argv) {
905
+ const words = [];
906
+ for (let index = SUBCOMMANDS.has(argv[0] ?? "") ? 1 : 0;index < argv.length; index += 1) {
907
+ const arg = argv[index];
908
+ if (arg === undefined)
909
+ continue;
910
+ if (VALUE_FLAGS.has(arg)) {
911
+ index += 1;
912
+ continue;
913
+ }
914
+ if (arg.startsWith("--"))
915
+ continue;
916
+ words.push(arg);
917
+ }
918
+ return words.join(" ").trim();
919
+ }
920
+ function looksLikeJson(value) {
921
+ const trimmed = value.trim();
922
+ return trimmed.startsWith("{") || trimmed.startsWith("[");
923
+ }
924
+ async function readJsonInput(value) {
925
+ if (value === undefined)
1060
926
  return;
1061
- const trimmed = raw.trim();
1062
927
  try {
1063
- return JSON.parse(trimmed);
928
+ return JSON.parse(looksLikeJson(value) ? value : await readFile4(value, "utf8"));
1064
929
  } catch (error) {
1065
- if (!(error instanceof SyntaxError))
1066
- throw error;
930
+ const message = error instanceof Error ? error.message : "unknown error";
931
+ throw new UlwLoopError(`Invalid JSON input: ${message}`, "ULW_LOOP_JSON_INPUT_INVALID", { cause: error });
1067
932
  }
1068
- const path = resolve3(repoRoot, trimmed);
1069
- if (!existsSync3(path))
1070
- return ulwLoopFail2("Quality gate JSON is neither valid JSON nor a readable path.", "ulw_loop_json_input_invalid");
1071
- try {
933
+ }
934
+ async function parseCodexGoalJson(value) {
935
+ if (value === undefined)
936
+ return;
937
+ const raw = looksLikeJson(value) ? value : await readFile4(value, "utf8");
938
+ try {
939
+ JSON.parse(raw);
940
+ return raw;
941
+ } catch (error) {
942
+ const message = error instanceof Error ? error.message : "unknown error";
943
+ throw new UlwLoopError(`Invalid --codex-goal-json: ${message}`, "ULW_LOOP_CODEX_GOAL_JSON_INVALID", { cause: error });
944
+ }
945
+ }
946
+ function required(argv, flag, code) {
947
+ const value = readValue(argv, flag)?.trim();
948
+ if (value)
949
+ return value;
950
+ throw new UlwLoopError(`Missing ${flag}.`, code, { details: { flag } });
951
+ }
952
+ function evidenceStatus(value) {
953
+ switch (value) {
954
+ case "pass":
955
+ return "pass";
956
+ case "fail":
957
+ return "fail";
958
+ case "blocked":
959
+ return "blocked";
960
+ default:
961
+ throw new UlwLoopError("Invalid --status; expected pass, fail, or blocked.", "ULW_LOOP_EVIDENCE_STATUS_INVALID", { details: { status: value } });
962
+ }
963
+ }
964
+ function parseRecordEvidenceArgs(argv) {
965
+ const result = { goalId: required(argv, "--goal-id", "ULW_LOOP_GOAL_ID_REQUIRED"), criterionId: required(argv, "--criterion-id", "ULW_LOOP_CRITERION_ID_REQUIRED"), status: evidenceStatus(required(argv, "--status", "ULW_LOOP_EVIDENCE_STATUS_REQUIRED")), evidence: required(argv, "--evidence", "ULW_LOOP_EVIDENCE_REQUIRED") };
966
+ const notes = readValue(argv, "--notes")?.trim();
967
+ return notes ? { ...result, notes } : result;
968
+ }
969
+
970
+ // components/ulw-loop/src/validation-batch.ts
971
+ function isObject(value) {
972
+ return typeof value === "object" && value !== null && !Array.isArray(value);
973
+ }
974
+ function read(value, key) {
975
+ return Object.entries(value).find(([name]) => name === key)?.[1];
976
+ }
977
+ function text(value, key) {
978
+ const candidate = read(value, key);
979
+ return typeof candidate === "string" && candidate.trim().length > 0 ? candidate.trim() : undefined;
980
+ }
981
+ function strings(value, key) {
982
+ const candidate = read(value, key);
983
+ return Array.isArray(candidate) && candidate.every((item) => typeof item === "string" && item.trim().length > 0) ? candidate.map((item) => item.trim()) : undefined;
984
+ }
985
+ async function parseValidationBatches(input, goals) {
986
+ const raw = await readJsonInput(input);
987
+ if (raw === undefined)
988
+ return;
989
+ if (!Array.isArray(raw))
990
+ fail("--validation-batch-json must be a JSON array.");
991
+ const batches = raw.map(batchFromObject);
992
+ validateBatches(batches, goals);
993
+ return batches;
994
+ }
995
+ function batchFromObject(value) {
996
+ if (!isObject(value))
997
+ fail("validation batch entries must be objects.");
998
+ const batchId = text(value, "batchId");
999
+ const memberIds = strings(value, "memberIds");
1000
+ const finalGoalId = text(value, "finalGoalId");
1001
+ if (batchId === undefined)
1002
+ fail("validation batch requires batchId.");
1003
+ if (memberIds === undefined || memberIds.length < 2)
1004
+ fail("validation batch requires at least two memberIds.");
1005
+ if (finalGoalId === undefined)
1006
+ fail("validation batch requires finalGoalId.");
1007
+ return { batchId, memberIds, finalGoalId };
1008
+ }
1009
+ function validateBatches(batches, goals) {
1010
+ const goalIds = new Set(goals.map((goal) => goal.id));
1011
+ const batchIds = new Set;
1012
+ const members = new Set;
1013
+ for (const batch of batches) {
1014
+ if (batchIds.has(batch.batchId))
1015
+ fail(`duplicate validation batch id: ${batch.batchId}.`);
1016
+ batchIds.add(batch.batchId);
1017
+ if (new Set(batch.memberIds).size !== batch.memberIds.length)
1018
+ fail(`validation batch ${batch.batchId} has duplicate memberIds.`);
1019
+ if (!batch.memberIds.includes(batch.finalGoalId))
1020
+ fail(`validation batch ${batch.batchId} finalGoalId must be a member.`, "ULW_LOOP_VALIDATION_BATCH_FINAL_NOT_MEMBER");
1021
+ for (const memberId of batch.memberIds) {
1022
+ if (!goalIds.has(memberId))
1023
+ fail(`validation batch ${batch.batchId} references unknown goal: ${memberId}.`, "ULW_LOOP_VALIDATION_BATCH_MEMBER_UNKNOWN");
1024
+ if (members.has(memberId))
1025
+ fail(`goal appears in multiple validation batches: ${memberId}.`, "ULW_LOOP_VALIDATION_BATCH_OVERLAP");
1026
+ members.add(memberId);
1027
+ }
1028
+ }
1029
+ }
1030
+ function updateBatchesAfterSupersede(plan, targetId, replacementIds) {
1031
+ if (replacementIds.length === 0 || plan.validationBatches === undefined)
1032
+ return;
1033
+ plan.validationBatches = plan.validationBatches.map((batch) => {
1034
+ if (!batch.memberIds.includes(targetId))
1035
+ return batch;
1036
+ const memberIds = batch.memberIds.flatMap((id) => id === targetId ? [...replacementIds] : [id]);
1037
+ const replacementFinalGoalId = replacementIds[replacementIds.length - 1] ?? batch.finalGoalId;
1038
+ const finalGoalId = batch.finalGoalId === targetId ? replacementFinalGoalId : batch.finalGoalId;
1039
+ return { batchId: batch.batchId, memberIds, finalGoalId };
1040
+ });
1041
+ }
1042
+ function batchUpdateLedgerEntry(before, after, at) {
1043
+ if (JSON.stringify(before.validationBatches ?? []) === JSON.stringify(after.validationBatches ?? []))
1044
+ return null;
1045
+ return { at, kind: "batch_updated", before: before.validationBatches ?? [], after: after.validationBatches ?? [], message: "Validation batch membership updated after steering." };
1046
+ }
1047
+ function batchOf(plan, goalId) {
1048
+ return plan.validationBatches?.find((batch) => batch.memberIds.includes(goalId));
1049
+ }
1050
+ function batchClosedBy(plan, goalId) {
1051
+ const batch = batchOf(plan, goalId);
1052
+ return batch?.finalGoalId === goalId ? batch : undefined;
1053
+ }
1054
+ function requireBatchFinalReady(plan, goal) {
1055
+ const batch = batchClosedBy(plan, goal.id);
1056
+ if (batch === undefined)
1057
+ return;
1058
+ const open = batch.memberIds.filter((id) => id !== goal.id && !memberResolved(plan, id));
1059
+ if (open.length > 0)
1060
+ throw new UlwLoopError("Validation batch has unresolved members.", "ULW_LOOP_VALIDATION_BATCH_OPEN", { details: { batchId: batch.batchId, open } });
1061
+ }
1062
+ function requireAllValidationBatchesClosed(plan, closingGoalId) {
1063
+ const open = (plan.validationBatches ?? []).filter((batch) => batch.memberIds.some((id) => id !== closingGoalId && !memberResolved(plan, id)));
1064
+ if (open.length > 0)
1065
+ throw new UlwLoopError("Validation batches remain open.", "ULW_LOOP_VALIDATION_BATCH_OPEN", { details: { batchIds: open.map((batch) => batch.batchId) } });
1066
+ }
1067
+ function requireBatchGate(plan, goal, gate) {
1068
+ const batch = batchClosedBy(plan, goal.id);
1069
+ if (batch === undefined)
1070
+ return;
1071
+ const members = batch.memberIds.map((id) => plan.goals.find((item) => item.id === id)).filter((item) => item !== undefined);
1072
+ const pending = members.flatMap((member) => member.successCriteria.filter((criterion) => criterion.status !== "pass").map((criterion) => `${member.id}:${criterion.id}`));
1073
+ if (pending.length > 0)
1074
+ throw new UlwLoopError("Validation batch criteria remain pending.", "ULW_LOOP_VALIDATION_BATCH_CRITERIA_PENDING", { details: { batchId: batch.batchId, pending } });
1075
+ const totalCriteria = members.reduce((sum, member) => sum + member.successCriteria.length, 0);
1076
+ const passCount = members.reduce((sum, member) => sum + member.successCriteria.filter((criterion) => criterion.status === "pass").length, 0);
1077
+ if (gate.criteriaCoverage.totalCriteria !== totalCriteria || gate.criteriaCoverage.passCount !== passCount)
1078
+ throw new UlwLoopError("Validation batch gate coverage does not match member criteria.", "ULW_LOOP_VALIDATION_BATCH_GATE_MISMATCH", { details: { batchId: batch.batchId, expected: { totalCriteria, passCount }, actual: gate.criteriaCoverage } });
1079
+ }
1080
+ function memberResolved(plan, goalId) {
1081
+ const goal = plan.goals.find((candidate) => candidate.id === goalId);
1082
+ return goal !== undefined && isMemberResolved(goal, plan);
1083
+ }
1084
+ function fail(message, code = "ULW_LOOP_VALIDATION_BATCH_INVALID") {
1085
+ throw new UlwLoopError(message, code);
1086
+ }
1087
+
1088
+ // components/ulw-loop/src/checkpoint.ts
1089
+ var QUALITY_GATE_FS = { existsSync: existsSync3, statSync };
1090
+ function ulwLoopFail2(message, code) {
1091
+ throw new UlwLoopError(message, code);
1092
+ }
1093
+ function normalizeObjective3(value) {
1094
+ return value.replace(/\s+/g, " ").trim();
1095
+ }
1096
+ function nonEmptyEvidence2(value) {
1097
+ const trimmed = value.trim();
1098
+ return trimmed || ulwLoopFail2("Evidence must be a non-empty string.", "ulw_loop_evidence_required");
1099
+ }
1100
+ function findGoal2(plan, goalId) {
1101
+ const goal = plan.goals.find((candidate) => candidate.id === goalId);
1102
+ return goal ?? ulwLoopFail2(`Unknown ulw-loop id: ${goalId}.`, "ulw_loop_goal_not_found");
1103
+ }
1104
+ async function readJsonInput2(raw, repoRoot) {
1105
+ if (raw === undefined || raw.trim() === "")
1106
+ return;
1107
+ const trimmed = raw.trim();
1108
+ try {
1109
+ return JSON.parse(trimmed);
1110
+ } catch (error) {
1111
+ if (!(error instanceof SyntaxError))
1112
+ throw error;
1113
+ }
1114
+ const path = resolve3(repoRoot, trimmed);
1115
+ if (!existsSync3(path))
1116
+ return ulwLoopFail2("Quality gate JSON is neither valid JSON nor a readable path.", "ulw_loop_json_input_invalid");
1117
+ try {
1072
1118
  return JSON.parse(await readFile5(path, "utf8"));
1073
1119
  } catch (error) {
1074
1120
  return ulwLoopFail2(`Quality gate path does not contain valid JSON${error instanceof Error ? `: ${error.message}` : "."}`, "ulw_loop_json_input_invalid");
@@ -1141,9 +1187,11 @@ async function checkpointUlwLoop(repoRoot, args, scope) {
1141
1187
  if (args.status === "complete") {
1142
1188
  const aggregate = codexGoalMode(plan) === "aggregate";
1143
1189
  const final = isFinalRunCompletionCandidate(plan, goal);
1190
+ const closesBatch = batchClosedBy(plan, goal.id) !== undefined;
1144
1191
  if (final) {
1145
1192
  requireAllCriteriaPass(goal);
1146
1193
  requireAllPlanCriteriaPass(plan);
1194
+ requireAllValidationBatchesClosed(plan, goal.id);
1147
1195
  } else if (aggregate)
1148
1196
  requireEssentialCriteriaPass(goal);
1149
1197
  else
@@ -1166,14 +1214,20 @@ async function checkpointUlwLoop(repoRoot, args, scope) {
1166
1214
  if (!taskScoped)
1167
1215
  throw new UlwLoopError(`${formatCodexGoalReconciliation(reconciliation)}${aggregate && snapshot?.status === "complete" && objective !== undefined ? buildTaskScopedAggregateReconciliationHint(goal, final) : ""}`, "ulw_loop_codex_snapshot_mismatch");
1168
1216
  }
1217
+ if (closesBatch)
1218
+ requireBatchFinalReady(plan, goal);
1219
+ if (closesBatch && args.qualityGateJson === undefined)
1220
+ throw new UlwLoopError("Validation batch final checkpoint requires --quality-gate-json.", "ULW_LOOP_VALIDATION_BATCH_GATE_REQUIRED");
1169
1221
  if (final)
1170
1222
  aggregateCompletion = makeAggregateCompletion(now, evidence, codexGoal);
1171
- if (final || aggregateCompletion !== undefined)
1223
+ if (final || aggregateCompletion !== undefined || closesBatch) {
1172
1224
  qualityGate = validateQualityGate(await readJsonInput2(args.qualityGateJson, repoRoot), {
1173
1225
  repoRoot,
1174
1226
  fs: QUALITY_GATE_FS,
1175
1227
  ...plan.evidenceLayoutVersion === 2 ? { currentAttemptDir: ulwLoopAttemptEvidenceDir(goal.id, goal.attempt, scope) } : {}
1176
1228
  });
1229
+ requireBatchGate(plan, goal, qualityGate);
1230
+ }
1177
1231
  goal.status = "complete";
1178
1232
  goal.completedAt = now;
1179
1233
  goal.evidence = evidence;
@@ -1191,215 +1245,89 @@ async function checkpointUlwLoop(repoRoot, args, scope) {
1191
1245
  await writePlan(repoRoot, plan, scope);
1192
1246
  const ledgerEntry = buildLedger(now, args, goal, qualityGate, codexGoal, aggregateCompletion);
1193
1247
  await appendLedger(repoRoot, ledgerEntry, scope);
1248
+ const closedBatch = args.status === "complete" ? batchClosedBy(plan, goal.id) : undefined;
1249
+ if (closedBatch !== undefined)
1250
+ await appendLedger(repoRoot, { at: now, kind: "batch_closed", goalId: goal.id, message: closedBatch.batchId }, scope);
1194
1251
  return aggregateCompletion === undefined ? { plan, goal, ledgerEntry } : { plan, goal, ledgerEntry, aggregateCompletion };
1195
1252
  });
1196
1253
  }
1197
1254
 
1198
- // components/ulw-loop/src/cli-steering.ts
1199
- var SOURCES = ["user_prompt_submit", "finding", "cli"];
1200
- var STEERING_KIND_HELP = [
1201
- `Allowed --kind values: ${ULW_LOOP_STEERING_MUTATION_KINDS.join(", ")}`,
1202
- "Kind-specific required flags:",
1203
- " add_subgoal: --title, --objective, --evidence, --rationale",
1204
- " split_subgoal: --goal-id, --children, --evidence, --rationale",
1205
- " reorder_pending: --order, --evidence, --rationale",
1206
- " revise_pending_wording: --goal-id, --title or --objective, --evidence, --rationale",
1207
- " revise_criterion: --goal-id, --criterion-id, one of --scenario/--expected-evidence/--user-model, --evidence, --rationale",
1208
- " annotate_ledger: --evidence, --rationale",
1209
- " mark_blocked_superseded: --goal-id, optional --replacements, --evidence, --rationale",
1210
- 'Example: omo ulw-loop steer --kind annotate_ledger --evidence "observed behavior" --rationale "why this changes the plan" --json'
1211
- ].join(`
1212
- `);
1213
- function isKind(value) {
1214
- return value !== undefined && ULW_LOOP_STEERING_MUTATION_KINDS.some((kind) => kind === value);
1215
- }
1216
- function isSource(value) {
1217
- return value !== undefined && SOURCES.some((source) => source === value);
1218
- }
1219
- function isModel(value) {
1220
- return ULW_LOOP_SUCCESS_CRITERION_USER_MODELS.some((model) => model === value);
1221
- }
1222
- function fail(message, code, details) {
1223
- throw new UlwLoopError(message, code, { details });
1224
- }
1225
- function kindMessage(prefix) {
1226
- return `${prefix}
1255
+ // components/ulw-loop/src/cli-output.ts
1256
+ var ULW_LOOP_HELP = `Usage:
1257
+ omo ulw-loop create-goals --brief "..." [--brief-file <path>] [--from-stdin] [--codex-goal-mode aggregate|per_story] [--validation-batch-json <json-or-path>] [--force] [--json]
1258
+ omo ulw-loop status [--json]
1259
+ omo ulw-loop complete-goals [--retry-failed] [--json]
1260
+ omo ulw-loop criteria --goal-id <id> [--json]
1261
+ omo ulw-loop record-evidence --goal-id <id> --criterion-id <id> --status pass|fail|blocked --evidence "..." [--notes "..."] [--json]
1262
+ omo ulw-loop checkpoint --goal-id <id> --status complete|failed|blocked --evidence "..." --codex-goal-json <...> [--quality-gate-json <...>] [--no-advance] [--json]
1263
+ omo ulw-loop steer --kind <kind> ... --evidence "..." --rationale "..." [--proposals-json <json-or-path>] [--json]
1264
+ omo ulw-loop add-goal --title "..." --objective "..." [--json]
1265
+ omo ulw-loop record-review-blockers --goal-id <id> --title "..." --objective "..." --evidence "..." --codex-goal-json <...> [--json]
1227
1266
 
1228
- ${STEERING_KIND_HELP}`;
1267
+ All subcommands accept [--session-id <id>] to isolate state under .omo/ulw-loop/<id>/; without it, Codex session env is used when present.`;
1268
+ function printJson(value) {
1269
+ process.stdout.write(`${JSON.stringify(value, null, 2)}
1270
+ `);
1229
1271
  }
1230
- function text(value, field) {
1231
- if (value === undefined)
1272
+ function printJsonError(error) {
1273
+ if (error instanceof UlwLoopError) {
1274
+ printJson({
1275
+ ok: false,
1276
+ error: {
1277
+ code: error.code,
1278
+ message: error.message,
1279
+ ...error.details === undefined ? {} : { details: error.details }
1280
+ }
1281
+ });
1232
1282
  return;
1233
- const trimmed = value.trim();
1234
- if (trimmed.length > 0)
1235
- return trimmed;
1236
- return fail(`Empty ${field}.`, "ULW_LOOP_STEERING_FIELD_EMPTY", { field });
1237
- }
1238
- function required2(argv, flag) {
1239
- const value = text(readValue(argv, flag), flag);
1240
- return value ?? fail(`Missing ${flag}.`, "ULW_LOOP_STEERING_FIELD_REQUIRED", { flag });
1241
- }
1242
- function requiredGoal(argv) {
1243
- const value = text(parseGoalArg(argv), "--goal-id");
1244
- return value ?? fail("Missing --goal-id.", "ULW_LOOP_GOAL_ID_REQUIRED", { flag: "--goal-id" });
1245
- }
1246
- function readObject(value, key) {
1247
- return Object.entries(value).find(([name]) => name === key)?.[1];
1248
- }
1249
- function isPlain(value) {
1250
- return typeof value === "object" && value !== null && !Array.isArray(value);
1251
- }
1252
- function objectText(value, key) {
1253
- const candidate = readObject(value, key);
1254
- return typeof candidate === "string" ? candidate : undefined;
1255
- }
1256
- function parseSteeringKind(argv) {
1257
- const value = readValue(argv, "--kind");
1258
- if (isKind(value))
1259
- return value;
1260
- return value === undefined ? fail(kindMessage("Missing --kind."), "ULW_LOOP_STEERING_KIND_REQUIRED", { flag: "--kind", expected: ULW_LOOP_STEERING_MUTATION_KINDS, usage: STEERING_KIND_HELP }) : fail(kindMessage(`Invalid --kind: ${value}.`), "ULW_LOOP_STEERING_KIND_INVALID", { value, expected: ULW_LOOP_STEERING_MUTATION_KINDS, usage: STEERING_KIND_HELP });
1261
- }
1262
- function parseSteeringSource(argv) {
1263
- const value = readValue(argv, "--source");
1264
- if (value === undefined)
1265
- return "cli";
1266
- return isSource(value) ? value : fail(`Invalid --source: ${value}.`, "ULW_LOOP_STEERING_SOURCE_INVALID", { value, expected: SOURCES });
1267
- }
1268
- function child(value) {
1269
- if (!isPlain(value))
1270
- return null;
1271
- const title = text(objectText(value, "title"), "title");
1272
- const objective = text(objectText(value, "objective"), "objective");
1273
- if (title === undefined || objective === undefined)
1274
- return null;
1275
- return { title, objective };
1276
- }
1277
- async function children(argv, flag, needed) {
1278
- const input = needed ? required2(argv, flag) : text(readValue(argv, flag), flag);
1279
- if (input === undefined)
1280
- return [];
1281
- const raw = await readJsonInput(input);
1282
- if (!Array.isArray(raw))
1283
- return fail(`${flag} must be a JSON array.`, "ULW_LOOP_STEERING_JSON_ARRAY_REQUIRED", { flag });
1284
- const parsed = [];
1285
- for (const item of raw) {
1286
- const next = child(item);
1287
- if (next === null)
1288
- return fail(`${flag} entries require title/objective.`, "ULW_LOOP_STEERING_CHILD_INVALID", { flag });
1289
- parsed.push(next);
1290
1283
  }
1291
- return parsed;
1292
- }
1293
- async function stringArray2(argv, flag) {
1294
- const raw = await readJsonInput(required2(argv, flag));
1295
- if (!Array.isArray(raw))
1296
- return fail(`${flag} must be a JSON array.`, "ULW_LOOP_STEERING_JSON_ARRAY_REQUIRED", { flag });
1297
- const values = [];
1298
- for (const item of raw) {
1299
- if (typeof item !== "string")
1300
- return fail(`${flag} entries must be strings.`, "ULW_LOOP_STEERING_STRING_ARRAY_REQUIRED", { flag });
1301
- values.push(text(item, flag) ?? "");
1302
- }
1303
- return values;
1304
- }
1305
- function model(value) {
1306
- const trimmed = text(value, "--user-model");
1307
- if (trimmed === undefined)
1284
+ if (error instanceof Error) {
1285
+ printJson({ ok: false, error: { code: "ULW_LOOP_UNEXPECTED", message: error.message } });
1308
1286
  return;
1309
- return isModel(trimmed) ? trimmed : fail(`Invalid --user-model: ${trimmed}.`, "ULW_LOOP_STEERING_USER_MODEL_INVALID", { value: trimmed, expected: ULW_LOOP_SUCCESS_CRITERION_USER_MODELS });
1310
- }
1311
- function neverKind(kind) {
1312
- return fail(`Unsupported steering kind: ${String(kind)}.`, "ULW_LOOP_STEERING_KIND_UNSUPPORTED", { kind });
1313
- }
1314
- async function parseSteeringProposal(argv) {
1315
- const kind = parseSteeringKind(argv);
1316
- const source = parseSteeringSource(argv);
1317
- const idempotencyKey = text(readValue(argv, "--idempotency-key"), "--idempotency-key");
1318
- const base = { kind, source, evidence: required2(argv, "--evidence"), rationale: required2(argv, "--rationale"), ...idempotencyKey === undefined ? {} : { idempotencyKey } };
1319
- switch (kind) {
1320
- case "add_subgoal":
1321
- return normalizeSteeringProposal({ ...base, title: required2(argv, "--title"), objective: required2(argv, "--objective") });
1322
- case "split_subgoal": {
1323
- const goalId = requiredGoal(argv);
1324
- return normalizeSteeringProposal({ ...base, goalId, targetGoalId: goalId, childGoals: await children(argv, "--children", true) });
1325
- }
1326
- case "reorder_pending":
1327
- return normalizeSteeringProposal({ ...base, pendingOrder: await stringArray2(argv, "--order") });
1328
- case "revise_pending_wording": {
1329
- const goalId = requiredGoal(argv);
1330
- const revisedTitle = readValue(argv, "--title");
1331
- const revisedObjective = readValue(argv, "--objective");
1332
- if (revisedTitle === undefined && revisedObjective === undefined)
1333
- return fail("revise_pending_wording requires --title or --objective.", "ULW_LOOP_STEERING_UPDATE_REQUIRED", { kind });
1334
- return normalizeSteeringProposal({ ...base, goalId, targetGoalId: goalId, ...revisedTitle === undefined ? {} : { revisedTitle }, ...revisedObjective === undefined ? {} : { revisedObjective } });
1335
- }
1336
- case "revise_criterion": {
1337
- const goalId = requiredGoal(argv);
1338
- const criterionId = required2(argv, "--criterion-id");
1339
- const scenario = readValue(argv, "--scenario");
1340
- const expectedEvidence = readValue(argv, "--expected-evidence");
1341
- const userModel = model(readValue(argv, "--user-model"));
1342
- if (scenario === undefined && expectedEvidence === undefined && userModel === undefined)
1343
- return fail("revise_criterion requires scenario, expected-evidence, or user-model.", "ULW_LOOP_STEERING_UPDATE_REQUIRED", { kind });
1344
- return normalizeSteeringProposal({ ...base, goalId, targetGoalId: goalId, criterionId, ...scenario === undefined ? {} : { scenario }, ...expectedEvidence === undefined ? {} : { expectedEvidence }, ...userModel === undefined ? {} : { userModel } });
1345
- }
1346
- case "annotate_ledger":
1347
- return normalizeSteeringProposal(base);
1348
- case "mark_blocked_superseded": {
1349
- const goalId = requiredGoal(argv);
1350
- const childGoals = await children(argv, "--replacements", false);
1351
- return normalizeSteeringProposal({ ...base, goalId, targetGoalId: goalId, ...childGoals.length === 0 ? {} : { childGoals } });
1352
- }
1353
- default:
1354
- return neverKind(kind);
1355
1287
  }
1288
+ printJson({ ok: false, error: { code: "ULW_LOOP_UNKNOWN", message: "unknown error" } });
1356
1289
  }
1357
- function normalizedChildren(values) {
1358
- if (values === undefined)
1359
- return;
1360
- return values.map((item) => ({ title: text(item.title, "child.title") ?? "", objective: text(item.objective, "child.objective") ?? "" }));
1361
- }
1362
- function normalizedStrings(values, field) {
1363
- if (values === undefined)
1364
- return;
1365
- return values.map((value) => text(value, field) ?? "");
1366
- }
1367
- function normalizeSteeringProposal(proposal) {
1368
- const evidence = text(proposal.evidence, "evidence") ?? "";
1369
- const rationale = text(proposal.rationale, "rationale") ?? "";
1370
- const goalId = text(proposal.goalId, "goalId");
1371
- const targetGoalId = text(proposal.targetGoalId, "targetGoalId");
1372
- const targetGoalIds = normalizedStrings(proposal.targetGoalIds, "targetGoalIds");
1373
- const criterionId = text(proposal.criterionId, "criterionId");
1374
- const title = text(proposal.title, "title");
1375
- const objective = text(proposal.objective, "objective");
1376
- const revisedTitle = text(proposal.revisedTitle, "revisedTitle");
1377
- const revisedObjective = text(proposal.revisedObjective, "revisedObjective");
1378
- const blockedReason = text(proposal.blockedReason, "blockedReason");
1379
- const directiveText = text(proposal.directiveText, "directiveText");
1380
- const promptSignature = text(proposal.promptSignature, "promptSignature");
1381
- const idempotencyKey = text(proposal.idempotencyKey, "idempotencyKey");
1382
- const scenario = text(proposal.scenario, "scenario");
1383
- const expectedEvidence = text(proposal.expectedEvidence, "expectedEvidence");
1384
- const childGoals = normalizedChildren(proposal.childGoals);
1385
- const pendingOrder = normalizedStrings(proposal.pendingOrder, "pendingOrder");
1386
- return { kind: proposal.kind, source: proposal.source, evidence, rationale, ...goalId === undefined ? {} : { goalId }, ...targetGoalId === undefined ? {} : { targetGoalId }, ...targetGoalIds === undefined ? {} : { targetGoalIds }, ...criterionId === undefined ? {} : { criterionId }, ...title === undefined ? {} : { title }, ...objective === undefined ? {} : { objective }, ...childGoals === undefined ? {} : { childGoals }, ...revisedTitle === undefined ? {} : { revisedTitle }, ...revisedObjective === undefined ? {} : { revisedObjective }, ...pendingOrder === undefined ? {} : { pendingOrder }, ...blockedReason === undefined ? {} : { blockedReason }, ...proposal.after === undefined ? {} : { after: proposal.after }, ...directiveText === undefined ? {} : { directiveText }, ...promptSignature === undefined ? {} : { promptSignature }, ...idempotencyKey === undefined ? {} : { idempotencyKey }, ...proposal.now === undefined ? {} : { now: proposal.now }, ...scenario === undefined ? {} : { scenario }, ...expectedEvidence === undefined ? {} : { expectedEvidence }, ...proposal.userModel === undefined ? {} : { userModel: proposal.userModel } };
1290
+ function criteriaCounts(goal) {
1291
+ let pass = 0;
1292
+ for (const criterion of goal.successCriteria)
1293
+ if (criterion.status === "pass")
1294
+ pass += 1;
1295
+ return { pass, total: goal.successCriteria.length };
1387
1296
  }
1388
- function printSteerResult(result, json) {
1389
- if (json) {
1390
- printJson({ ok: result.accepted, accepted: result.accepted, rejectedReasons: result.rejectedReasons, deduped: result.deduped, audit: result.audit, plan: result.plan });
1391
- return;
1297
+ function printStatus(plan) {
1298
+ let totalCriteria = 0;
1299
+ let passCriteria = 0;
1300
+ const lines = ["ulw-loop status", "", "goals:"];
1301
+ for (const goal of plan.goals) {
1302
+ const counts = criteriaCounts(goal);
1303
+ totalCriteria += counts.total;
1304
+ passCriteria += counts.pass;
1305
+ const marker = goal.id === plan.activeGoalId ? "*" : "-";
1306
+ lines.push(`${marker} ${goal.id} [${goal.status}] ${goal.title} (criteria: ${counts.pass}/${counts.total})`);
1392
1307
  }
1393
- const outcome = result.deduped ? "deduped" : result.accepted ? "accepted" : "rejected";
1394
- process.stdout.write(`ulw-loop steer: ${outcome} ${result.audit.kind}
1395
- `);
1396
- if (result.rejectedReasons.length > 0)
1397
- process.stdout.write(`rejected: ${result.rejectedReasons.join("; ")}
1308
+ lines.push("", "summary:", `total goals: ${plan.goals.length}`, `criteria: ${passCriteria}/${totalCriteria} pass`);
1309
+ process.stdout.write(`${lines.join(`
1310
+ `)}
1398
1311
  `);
1399
- if (result.audit.idempotencyKey !== undefined)
1400
- process.stdout.write(`idempotency-key: ${result.audit.idempotencyKey}
1312
+ }
1313
+ function blockedDecisionHandoff(plan) {
1314
+ const blocked = plan.goals.find((goal) => goal.status === "needs_user_decision" && goal.nonRetriable);
1315
+ if (blocked === undefined)
1316
+ return "";
1317
+ return [
1318
+ "ulw-loop: blocked on repeated external authorization; no retryable failed goals remain.",
1319
+ `Goal: ${blocked.id} - ${blocked.title}`,
1320
+ `Required external decision: ${blocked.requiredExternalDecision ?? "provide the missing authorization or choose a different unblock path"}.`,
1321
+ "Do not run complete-goals --retry-failed again until external state changes or the user authorizes an unblock path."
1322
+ ].join(`
1401
1323
  `);
1402
- printStatus(result.plan);
1324
+ }
1325
+ function normalizeCodexGoalMode(value) {
1326
+ if (value === undefined)
1327
+ return "aggregate";
1328
+ if (value === "aggregate" || value === "per_story")
1329
+ return value;
1330
+ throw new UlwLoopError("Invalid --codex-goal-mode; expected aggregate or per_story.", "ULW_LOOP_CODEX_GOAL_MODE_INVALID", { details: { value } });
1403
1331
  }
1404
1332
 
1405
1333
  // components/ulw-loop/src/codex-goal-instruction.ts
@@ -1645,6 +1573,9 @@ async function createUlwLoopPlan(repoRoot, args, scope) {
1645
1573
  codexGoalMode: args.codexGoalMode ?? "aggregate",
1646
1574
  goals
1647
1575
  };
1576
+ const validationBatches = await parseValidationBatches(args.validationBatchesJson, goals);
1577
+ if (validationBatches !== undefined)
1578
+ plan.validationBatches = validationBatches;
1648
1579
  if (plan.codexGoalMode === "aggregate")
1649
1580
  plan.codexObjective = aggregateCodexObjectiveForScope(scope);
1650
1581
  await mkdir2(ulwLoopDir(repoRoot, scope), { recursive: true });
@@ -1729,7 +1660,380 @@ function summarizeUlwLoopPlan(plan) {
1729
1660
  fail: countCriteria("fail"),
1730
1661
  blocked: countCriteria("blocked")
1731
1662
  }
1732
- };
1663
+ };
1664
+ }
1665
+
1666
+ // components/ulw-loop/src/checkpoint-continuation.ts
1667
+ async function checkpointAndContinue(repoRoot, args, scope) {
1668
+ const result = await checkpointUlwLoop(repoRoot, args, scope);
1669
+ if (args.status !== "complete" || result.aggregateCompletion !== undefined || !args.advance)
1670
+ return result;
1671
+ const next = await startNextUlwLoop(repoRoot, {}, scope);
1672
+ if ("done" in next)
1673
+ return { ...result, plan: next.plan, next: doneNext(next.plan) };
1674
+ const instruction = buildCodexGoalInstruction({ plan: next.plan, goal: next.goal });
1675
+ return { ...result, plan: next.plan, next: { resumed: next.resumed, goal: next.goal, instruction } };
1676
+ }
1677
+ async function checkpoint(repoRoot, argv, json, scope) {
1678
+ const goalId = required2(argv, "--goal-id");
1679
+ const statusValue = checkpointStatus(required2(argv, "--status"));
1680
+ const evidence = required2(argv, "--evidence");
1681
+ const codexGoalJson = await parseCodexGoalJson(statusValue === "complete" ? required2(argv, "--codex-goal-json") : readValue(argv, "--codex-goal-json"));
1682
+ if (statusValue === "complete" && codexGoalJson === undefined) {
1683
+ throw new UlwLoopError("Missing --codex-goal-json.", "ULW_LOOP_CODEX_GOAL_JSON_REQUIRED");
1684
+ }
1685
+ const qualityGateJson = readValue(argv, "--quality-gate-json");
1686
+ const args = {
1687
+ goalId,
1688
+ status: statusValue,
1689
+ evidence,
1690
+ advance: !hasFlag(argv, "--no-advance"),
1691
+ ...codexGoalJson === undefined ? {} : { codexGoalJson },
1692
+ ...qualityGateJson === undefined ? {} : { qualityGateJson }
1693
+ };
1694
+ const result = await checkpointAndContinue(repoRoot, args, scope);
1695
+ if (json)
1696
+ printJson({ ok: true, ...result, summary: summarizeUlwLoopPlan(result.plan) });
1697
+ else
1698
+ printCheckpointText(result);
1699
+ return 0;
1700
+ }
1701
+ function printCheckpointText(result) {
1702
+ process.stdout.write(`ulw-loop checkpoint: ${result.goal.id} -> ${result.goal.status}
1703
+ `);
1704
+ if (result.next === undefined)
1705
+ return;
1706
+ if ("instruction" in result.next)
1707
+ process.stdout.write(`${result.next.instruction.text}
1708
+ `);
1709
+ else
1710
+ process.stdout.write(`${result.next.handoff || "ulw-loop: all goals complete"}
1711
+ `);
1712
+ }
1713
+ function doneNext(plan) {
1714
+ const handoff = blockedDecisionHandoff(plan);
1715
+ return { done: true, blocked: handoff.length > 0, handoff };
1716
+ }
1717
+ function required2(argv, flag) {
1718
+ const value = readValue(argv, flag)?.trim();
1719
+ if (value)
1720
+ return value;
1721
+ throw new UlwLoopError(`Missing ${flag}.`, "ULW_LOOP_ARGUMENT_MISSING", { details: { flag } });
1722
+ }
1723
+ function checkpointStatus(value) {
1724
+ if (value === "complete" || value === "failed" || value === "blocked")
1725
+ return value;
1726
+ throw new UlwLoopError("Missing or invalid --status; expected complete, failed, or blocked.", "ULW_LOOP_STATUS_INVALID", { details: { status: value } });
1727
+ }
1728
+
1729
+ // components/ulw-loop/src/cli-subcommands.ts
1730
+ import { readFile as readFile6 } from "node:fs/promises";
1731
+
1732
+ // components/ulw-loop/src/cli-steering.ts
1733
+ var SOURCES = ["user_prompt_submit", "finding", "cli"];
1734
+ var STEERING_KIND_HELP = [
1735
+ `Allowed --kind values: ${ULW_LOOP_STEERING_MUTATION_KINDS.join(", ")}`,
1736
+ "Kind-specific required flags:",
1737
+ " add_subgoal: --title, --objective, --evidence, --rationale",
1738
+ " split_subgoal: --goal-id, --children, --evidence, --rationale",
1739
+ " reorder_pending: --order, --evidence, --rationale",
1740
+ " revise_pending_wording: --goal-id, --title or --objective, --evidence, --rationale",
1741
+ " revise_criterion: --goal-id, --criterion-id, one of --scenario/--expected-evidence/--user-model, --evidence, --rationale",
1742
+ " annotate_ledger: --evidence, --rationale",
1743
+ " mark_blocked_superseded: --goal-id, optional --replacements, --evidence, --rationale",
1744
+ 'Example: omo ulw-loop steer --kind annotate_ledger --evidence "observed behavior" --rationale "why this changes the plan" --json'
1745
+ ].join(`
1746
+ `);
1747
+ function isKind(value) {
1748
+ return value !== undefined && ULW_LOOP_STEERING_MUTATION_KINDS.some((kind) => kind === value);
1749
+ }
1750
+ function isSource(value) {
1751
+ return value !== undefined && SOURCES.some((source) => source === value);
1752
+ }
1753
+ function isModel(value) {
1754
+ return ULW_LOOP_SUCCESS_CRITERION_USER_MODELS.some((model) => model === value);
1755
+ }
1756
+ function fail2(message, code, details) {
1757
+ throw new UlwLoopError(message, code, { details });
1758
+ }
1759
+ function kindMessage(prefix) {
1760
+ return `${prefix}
1761
+
1762
+ ${STEERING_KIND_HELP}`;
1763
+ }
1764
+ function text2(value, field) {
1765
+ if (value === undefined)
1766
+ return;
1767
+ const trimmed = value.trim();
1768
+ if (trimmed.length > 0)
1769
+ return trimmed;
1770
+ return fail2(`Empty ${field}.`, "ULW_LOOP_STEERING_FIELD_EMPTY", { field });
1771
+ }
1772
+ function required3(argv, flag) {
1773
+ const value = text2(readValue(argv, flag), flag);
1774
+ return value ?? fail2(`Missing ${flag}.`, "ULW_LOOP_STEERING_FIELD_REQUIRED", { flag });
1775
+ }
1776
+ function requiredGoal(argv) {
1777
+ const value = text2(parseGoalArg(argv), "--goal-id");
1778
+ return value ?? fail2("Missing --goal-id.", "ULW_LOOP_GOAL_ID_REQUIRED", { flag: "--goal-id" });
1779
+ }
1780
+ function readObject(value, key) {
1781
+ return Object.entries(value).find(([name]) => name === key)?.[1];
1782
+ }
1783
+ function isPlain(value) {
1784
+ return typeof value === "object" && value !== null && !Array.isArray(value);
1785
+ }
1786
+ function objectText(value, key) {
1787
+ const candidate = readObject(value, key);
1788
+ return typeof candidate === "string" ? candidate : undefined;
1789
+ }
1790
+ function objectStrings(value, key) {
1791
+ const candidate = readObject(value, key);
1792
+ return Array.isArray(candidate) && candidate.every((item) => typeof item === "string") ? candidate : undefined;
1793
+ }
1794
+ function objectChildren(value, key) {
1795
+ const candidate = readObject(value, key);
1796
+ if (!Array.isArray(candidate))
1797
+ return;
1798
+ const parsed = [];
1799
+ for (const item of candidate) {
1800
+ const next = child(item);
1801
+ if (next === null)
1802
+ return fail2(`${key} entries require title/objective.`, "ULW_LOOP_STEERING_CHILD_INVALID", { key });
1803
+ parsed.push(next);
1804
+ }
1805
+ return parsed;
1806
+ }
1807
+ function isProposalKind(value) {
1808
+ return typeof value === "string" && ULW_LOOP_STEERING_MUTATION_KINDS.some((kind) => kind === value);
1809
+ }
1810
+ function isProposalSource(value) {
1811
+ return typeof value === "string" && SOURCES.some((source) => source === value);
1812
+ }
1813
+ function parseSteeringKind(argv) {
1814
+ const value = readValue(argv, "--kind");
1815
+ if (isKind(value))
1816
+ return value;
1817
+ return value === undefined ? fail2(kindMessage("Missing --kind."), "ULW_LOOP_STEERING_KIND_REQUIRED", { flag: "--kind", expected: ULW_LOOP_STEERING_MUTATION_KINDS, usage: STEERING_KIND_HELP }) : fail2(kindMessage(`Invalid --kind: ${value}.`), "ULW_LOOP_STEERING_KIND_INVALID", { value, expected: ULW_LOOP_STEERING_MUTATION_KINDS, usage: STEERING_KIND_HELP });
1818
+ }
1819
+ function parseSteeringSource(argv) {
1820
+ const value = readValue(argv, "--source");
1821
+ if (value === undefined)
1822
+ return "cli";
1823
+ return isSource(value) ? value : fail2(`Invalid --source: ${value}.`, "ULW_LOOP_STEERING_SOURCE_INVALID", { value, expected: SOURCES });
1824
+ }
1825
+ function child(value) {
1826
+ if (!isPlain(value))
1827
+ return null;
1828
+ const title = text2(objectText(value, "title"), "title");
1829
+ const objective = text2(objectText(value, "objective"), "objective");
1830
+ if (title === undefined || objective === undefined)
1831
+ return null;
1832
+ return { title, objective };
1833
+ }
1834
+ async function children(argv, flag, needed) {
1835
+ const input = needed ? required3(argv, flag) : text2(readValue(argv, flag), flag);
1836
+ if (input === undefined)
1837
+ return [];
1838
+ const raw = await readJsonInput(input);
1839
+ if (!Array.isArray(raw))
1840
+ return fail2(`${flag} must be a JSON array.`, "ULW_LOOP_STEERING_JSON_ARRAY_REQUIRED", { flag });
1841
+ const parsed = [];
1842
+ for (const item of raw) {
1843
+ const next = child(item);
1844
+ if (next === null)
1845
+ return fail2(`${flag} entries require title/objective.`, "ULW_LOOP_STEERING_CHILD_INVALID", { flag });
1846
+ parsed.push(next);
1847
+ }
1848
+ return parsed;
1849
+ }
1850
+ async function stringArray2(argv, flag) {
1851
+ const raw = await readJsonInput(required3(argv, flag));
1852
+ if (!Array.isArray(raw))
1853
+ return fail2(`${flag} must be a JSON array.`, "ULW_LOOP_STEERING_JSON_ARRAY_REQUIRED", { flag });
1854
+ const values = [];
1855
+ for (const item of raw) {
1856
+ if (typeof item !== "string")
1857
+ return fail2(`${flag} entries must be strings.`, "ULW_LOOP_STEERING_STRING_ARRAY_REQUIRED", { flag });
1858
+ values.push(text2(item, flag) ?? "");
1859
+ }
1860
+ return values;
1861
+ }
1862
+ function model(value) {
1863
+ const trimmed = text2(value, "--user-model");
1864
+ if (trimmed === undefined)
1865
+ return;
1866
+ return isModel(trimmed) ? trimmed : fail2(`Invalid --user-model: ${trimmed}.`, "ULW_LOOP_STEERING_USER_MODEL_INVALID", { value: trimmed, expected: ULW_LOOP_SUCCESS_CRITERION_USER_MODELS });
1867
+ }
1868
+ function neverKind(kind) {
1869
+ return fail2(`Unsupported steering kind: ${String(kind)}.`, "ULW_LOOP_STEERING_KIND_UNSUPPORTED", { kind });
1870
+ }
1871
+ async function parseSteeringProposal(argv) {
1872
+ const kind = parseSteeringKind(argv);
1873
+ const source = parseSteeringSource(argv);
1874
+ const idempotencyKey = text2(readValue(argv, "--idempotency-key"), "--idempotency-key");
1875
+ const base = { kind, source, evidence: required3(argv, "--evidence"), rationale: required3(argv, "--rationale"), ...idempotencyKey === undefined ? {} : { idempotencyKey } };
1876
+ switch (kind) {
1877
+ case "add_subgoal":
1878
+ return normalizeSteeringProposal({ ...base, title: required3(argv, "--title"), objective: required3(argv, "--objective") });
1879
+ case "split_subgoal": {
1880
+ const goalId = requiredGoal(argv);
1881
+ return normalizeSteeringProposal({ ...base, goalId, targetGoalId: goalId, childGoals: await children(argv, "--children", true) });
1882
+ }
1883
+ case "reorder_pending":
1884
+ return normalizeSteeringProposal({ ...base, pendingOrder: await stringArray2(argv, "--order") });
1885
+ case "revise_pending_wording": {
1886
+ const goalId = requiredGoal(argv);
1887
+ const revisedTitle = readValue(argv, "--title");
1888
+ const revisedObjective = readValue(argv, "--objective");
1889
+ if (revisedTitle === undefined && revisedObjective === undefined)
1890
+ return fail2("revise_pending_wording requires --title or --objective.", "ULW_LOOP_STEERING_UPDATE_REQUIRED", { kind });
1891
+ return normalizeSteeringProposal({ ...base, goalId, targetGoalId: goalId, ...revisedTitle === undefined ? {} : { revisedTitle }, ...revisedObjective === undefined ? {} : { revisedObjective } });
1892
+ }
1893
+ case "revise_criterion": {
1894
+ const goalId = requiredGoal(argv);
1895
+ const criterionId = required3(argv, "--criterion-id");
1896
+ const scenario = readValue(argv, "--scenario");
1897
+ const expectedEvidence = readValue(argv, "--expected-evidence");
1898
+ const userModel = model(readValue(argv, "--user-model"));
1899
+ if (scenario === undefined && expectedEvidence === undefined && userModel === undefined)
1900
+ return fail2("revise_criterion requires scenario, expected-evidence, or user-model.", "ULW_LOOP_STEERING_UPDATE_REQUIRED", { kind });
1901
+ return normalizeSteeringProposal({ ...base, goalId, targetGoalId: goalId, criterionId, ...scenario === undefined ? {} : { scenario }, ...expectedEvidence === undefined ? {} : { expectedEvidence }, ...userModel === undefined ? {} : { userModel } });
1902
+ }
1903
+ case "annotate_ledger":
1904
+ return normalizeSteeringProposal(base);
1905
+ case "mark_blocked_superseded": {
1906
+ const goalId = requiredGoal(argv);
1907
+ const childGoals = await children(argv, "--replacements", false);
1908
+ return normalizeSteeringProposal({ ...base, goalId, targetGoalId: goalId, ...childGoals.length === 0 ? {} : { childGoals } });
1909
+ }
1910
+ default:
1911
+ return neverKind(kind);
1912
+ }
1913
+ }
1914
+ function normalizedChildren(values) {
1915
+ if (values === undefined)
1916
+ return;
1917
+ return values.map((item) => ({ title: text2(item.title, "child.title") ?? "", objective: text2(item.objective, "child.objective") ?? "" }));
1918
+ }
1919
+ function normalizedStrings(values, field) {
1920
+ if (values === undefined)
1921
+ return;
1922
+ return values.map((value) => text2(value, field) ?? "");
1923
+ }
1924
+ function normalizeSteeringProposal(proposal) {
1925
+ const evidence = text2(proposal.evidence, "evidence") ?? "";
1926
+ const rationale = text2(proposal.rationale, "rationale") ?? "";
1927
+ const goalId = text2(proposal.goalId, "goalId");
1928
+ const targetGoalId = text2(proposal.targetGoalId, "targetGoalId");
1929
+ const targetGoalIds = normalizedStrings(proposal.targetGoalIds, "targetGoalIds");
1930
+ const criterionId = text2(proposal.criterionId, "criterionId");
1931
+ const title = text2(proposal.title, "title");
1932
+ const objective = text2(proposal.objective, "objective");
1933
+ const revisedTitle = text2(proposal.revisedTitle, "revisedTitle");
1934
+ const revisedObjective = text2(proposal.revisedObjective, "revisedObjective");
1935
+ const blockedReason = text2(proposal.blockedReason, "blockedReason");
1936
+ const directiveText = text2(proposal.directiveText, "directiveText");
1937
+ const promptSignature = text2(proposal.promptSignature, "promptSignature");
1938
+ const idempotencyKey = text2(proposal.idempotencyKey, "idempotencyKey");
1939
+ const scenario = text2(proposal.scenario, "scenario");
1940
+ const expectedEvidence = text2(proposal.expectedEvidence, "expectedEvidence");
1941
+ const childGoals = normalizedChildren(proposal.childGoals);
1942
+ const pendingOrder = normalizedStrings(proposal.pendingOrder, "pendingOrder");
1943
+ return { kind: proposal.kind, source: proposal.source, evidence, rationale, ...goalId === undefined ? {} : { goalId }, ...targetGoalId === undefined ? {} : { targetGoalId }, ...targetGoalIds === undefined ? {} : { targetGoalIds }, ...criterionId === undefined ? {} : { criterionId }, ...title === undefined ? {} : { title }, ...objective === undefined ? {} : { objective }, ...childGoals === undefined ? {} : { childGoals }, ...revisedTitle === undefined ? {} : { revisedTitle }, ...revisedObjective === undefined ? {} : { revisedObjective }, ...pendingOrder === undefined ? {} : { pendingOrder }, ...blockedReason === undefined ? {} : { blockedReason }, ...proposal.after === undefined ? {} : { after: proposal.after }, ...directiveText === undefined ? {} : { directiveText }, ...promptSignature === undefined ? {} : { promptSignature }, ...idempotencyKey === undefined ? {} : { idempotencyKey }, ...proposal.now === undefined ? {} : { now: proposal.now }, ...scenario === undefined ? {} : { scenario }, ...expectedEvidence === undefined ? {} : { expectedEvidence }, ...proposal.userModel === undefined ? {} : { userModel: proposal.userModel } };
1944
+ }
1945
+ async function parseSteeringProposals(argv) {
1946
+ const input = text2(readValue(argv, "--proposals-json"), "--proposals-json");
1947
+ if (input === undefined)
1948
+ return [await parseSteeringProposal(argv)];
1949
+ if (readValue(argv, "--kind") !== undefined)
1950
+ return fail2("--kind and --proposals-json are mutually exclusive.", "ULW_LOOP_STEERING_BATCH_CONFLICT", { flags: ["--kind", "--proposals-json"] });
1951
+ const raw = await readJsonInput(input);
1952
+ if (!Array.isArray(raw) || raw.length === 0)
1953
+ return fail2("--proposals-json must be a non-empty JSON array.", "ULW_LOOP_STEERING_BATCH_ARRAY_REQUIRED", { flag: "--proposals-json" });
1954
+ const proposals = [];
1955
+ for (const item of raw)
1956
+ proposals.push(normalizeSteeringProposal(proposalFromObject(item)));
1957
+ return proposals;
1958
+ }
1959
+ function proposalFromObject(value) {
1960
+ if (!isPlain(value))
1961
+ return fail2("--proposals-json entries must be objects.", "ULW_LOOP_STEERING_BATCH_ITEM_INVALID", { flag: "--proposals-json" });
1962
+ const kind = readObject(value, "kind");
1963
+ const source = readObject(value, "source") ?? "cli";
1964
+ if (!isProposalKind(kind))
1965
+ return fail2(`Invalid batch steering kind: ${String(kind)}.`, "ULW_LOOP_STEERING_KIND_INVALID", { value: kind });
1966
+ if (!isProposalSource(source))
1967
+ return fail2(`Invalid batch steering source: ${String(source)}.`, "ULW_LOOP_STEERING_SOURCE_INVALID", { value: source });
1968
+ let proposal = { kind, source, evidence: objectText(value, "evidence") ?? "", rationale: objectText(value, "rationale") ?? "" };
1969
+ const goalId = objectText(value, "goalId");
1970
+ const targetGoalId = objectText(value, "targetGoalId");
1971
+ const criterionId = objectText(value, "criterionId");
1972
+ const title = objectText(value, "title");
1973
+ const objective = objectText(value, "objective");
1974
+ const revisedTitle = objectText(value, "revisedTitle");
1975
+ const revisedObjective = objectText(value, "revisedObjective");
1976
+ const scenario = objectText(value, "scenario");
1977
+ const expectedEvidence = objectText(value, "expectedEvidence");
1978
+ const idempotencyKey = objectText(value, "idempotencyKey");
1979
+ const targetGoalIds = objectStrings(value, "targetGoalIds");
1980
+ const pendingOrder = objectStrings(value, "pendingOrder");
1981
+ const childGoals = objectChildren(value, "childGoals");
1982
+ if (goalId !== undefined)
1983
+ proposal = { ...proposal, goalId };
1984
+ if (targetGoalId !== undefined)
1985
+ proposal = { ...proposal, targetGoalId };
1986
+ if (criterionId !== undefined)
1987
+ proposal = { ...proposal, criterionId };
1988
+ if (title !== undefined)
1989
+ proposal = { ...proposal, title };
1990
+ if (objective !== undefined)
1991
+ proposal = { ...proposal, objective };
1992
+ if (revisedTitle !== undefined)
1993
+ proposal = { ...proposal, revisedTitle };
1994
+ if (revisedObjective !== undefined)
1995
+ proposal = { ...proposal, revisedObjective };
1996
+ if (scenario !== undefined)
1997
+ proposal = { ...proposal, scenario };
1998
+ if (expectedEvidence !== undefined)
1999
+ proposal = { ...proposal, expectedEvidence };
2000
+ if (idempotencyKey !== undefined)
2001
+ proposal = { ...proposal, idempotencyKey };
2002
+ if (targetGoalIds !== undefined)
2003
+ proposal = { ...proposal, targetGoalIds };
2004
+ if (pendingOrder !== undefined)
2005
+ proposal = { ...proposal, pendingOrder };
2006
+ if (childGoals !== undefined)
2007
+ proposal = { ...proposal, childGoals };
2008
+ return proposal;
2009
+ }
2010
+ function printSteerResult(result, json) {
2011
+ if (json) {
2012
+ printJson({ ok: result.accepted, accepted: result.accepted, rejectedReasons: result.rejectedReasons, deduped: result.deduped, audit: result.audit, plan: result.plan });
2013
+ return;
2014
+ }
2015
+ const outcome = result.deduped ? "deduped" : result.accepted ? "accepted" : "rejected";
2016
+ process.stdout.write(`ulw-loop steer: ${outcome} ${result.audit.kind}
2017
+ `);
2018
+ if (result.rejectedReasons.length > 0)
2019
+ process.stdout.write(`rejected: ${result.rejectedReasons.join("; ")}
2020
+ `);
2021
+ if (result.audit.idempotencyKey !== undefined)
2022
+ process.stdout.write(`idempotency-key: ${result.audit.idempotencyKey}
2023
+ `);
2024
+ printStatus(result.plan);
2025
+ }
2026
+ function printSteerBatchResult(result, json) {
2027
+ if (json) {
2028
+ printJson({ ok: result.accepted, accepted: result.accepted, rejectedReasons: result.rejectedReasons, results: result.results, plan: result.plan });
2029
+ return;
2030
+ }
2031
+ process.stdout.write(`ulw-loop steer batch: ${result.accepted ? "accepted" : "rejected"} ${result.results.length} proposal(s)
2032
+ `);
2033
+ if (result.rejectedReasons.length > 0)
2034
+ process.stdout.write(`rejected: ${result.rejectedReasons.join("; ")}
2035
+ `);
2036
+ printStatus(result.plan);
1733
2037
  }
1734
2038
 
1735
2039
  // components/ulw-loop/src/review-blockers.ts
@@ -1798,24 +2102,111 @@ async function recordFinalReviewBlockers(repoRoot, args, scope) {
1798
2102
  });
1799
2103
  }
1800
2104
 
2105
+ // components/ulw-loop/src/steering-mutations.ts
2106
+ var read2 = (value, key) => Object.entries(value).find(([name]) => name === key)?.[1];
2107
+ var isText = (value) => typeof value === "string" && value.trim().length > 0;
2108
+ var text3 = (value, key) => {
2109
+ const candidate = read2(value, key);
2110
+ return isText(candidate) ? candidate.trim() : undefined;
2111
+ };
2112
+ var isModel2 = (value) => typeof value === "string" && ULW_LOOP_SUCCESS_CRITERION_USER_MODELS.some((model2) => model2 === value);
2113
+ var after = (proposal) => {
2114
+ const candidate = read2(proposal, "after");
2115
+ return typeof candidate === "object" && candidate !== null && !Array.isArray(candidate) ? candidate : undefined;
2116
+ };
2117
+ var revised = (proposal, direct, nested) => text3(proposal, direct) ?? text3(after(proposal) ?? proposal, nested);
2118
+ var targets = (proposal) => proposal.targetGoalIds ?? [proposal.targetGoalId ?? text3(proposal, "goalId") ?? ""].filter(Boolean);
2119
+ var child2 = (value) => {
2120
+ if (typeof value !== "object" || value === null || Array.isArray(value))
2121
+ return null;
2122
+ const title = text3(value, "title");
2123
+ const objective = text3(value, "objective");
2124
+ return title === undefined || objective === undefined ? null : { title, objective };
2125
+ };
2126
+ var children2 = (proposal) => {
2127
+ const direct = proposal.childGoals;
2128
+ if (direct !== undefined && direct.length > 0)
2129
+ return direct;
2130
+ const nested = after(proposal);
2131
+ const fromAfter = nested === undefined ? undefined : read2(nested, "children");
2132
+ return Array.isArray(fromAfter) ? fromAfter.map(child2).filter((item) => item !== null) : [];
2133
+ };
2134
+ var goal = (plan, id) => id === undefined ? undefined : plan.goals.find((item) => item.id === id);
2135
+ function nextId(plan, offset) {
2136
+ const max = plan.goals.reduce((current, item) => {
2137
+ const digits = /^G(\d+)(?:-|$)/u.exec(item.id)?.[1];
2138
+ return digits === undefined ? current : Math.max(current, Number(digits));
2139
+ }, 0);
2140
+ return `G${String(max + offset).padStart(3, "0")}`;
2141
+ }
2142
+ function makeGoal2(plan, childGoal, evidence, now, offset) {
2143
+ const id = nextId(plan, offset);
2144
+ const digits = /^G(\d+)/u.exec(id)?.[1];
2145
+ const goalIndex = digits === undefined ? plan.goals.length + offset - 1 : Number(digits) - 1;
2146
+ return { id, title: childGoal.title, objective: childGoal.objective, status: "pending", successCriteria: seedDefaultSuccessCriteria(goalIndex, childGoal.objective), attempt: 0, createdAt: now, updatedAt: now, evidence };
2147
+ }
2148
+ function reviseWording(plan, proposal, now) {
2149
+ const target = goal(plan, targets(proposal)[0]);
2150
+ if (target === undefined)
2151
+ return;
2152
+ target.title = revised(proposal, "revisedTitle", "title") ?? target.title;
2153
+ target.objective = revised(proposal, "revisedObjective", "objective") ?? target.objective;
2154
+ target.steeringEvidence = proposal.evidence;
2155
+ target.steeringRationale = proposal.rationale;
2156
+ target.updatedAt = now;
2157
+ }
2158
+ function splitOrBlock(plan, proposal, now) {
2159
+ const target = goal(plan, targets(proposal)[0]);
2160
+ if (target === undefined)
2161
+ return;
2162
+ const replacements = children2(proposal).map((item, index) => makeGoal2(plan, item, proposal.evidence, now, index + 1));
2163
+ target.steeringEvidence = proposal.evidence;
2164
+ target.steeringRationale = proposal.rationale;
2165
+ target.updatedAt = now;
2166
+ if (replacements.length === 0) {
2167
+ target.status = "blocked";
2168
+ target.steeringStatus = "blocked";
2169
+ target.blockedReason = proposal.blockedReason ?? proposal.rationale;
2170
+ } else {
2171
+ target.steeringStatus = "superseded";
2172
+ target.supersededBy = replacements.map((item) => item.id);
2173
+ for (const item of replacements)
2174
+ item.supersedes = [target.id];
2175
+ plan.goals.splice(plan.goals.indexOf(target) + 1, 0, ...replacements);
2176
+ updateBatchesAfterSupersede(plan, target.id, replacements.map((item) => item.id));
2177
+ }
2178
+ if (plan.activeGoalId === target.id)
2179
+ delete plan.activeGoalId;
2180
+ }
2181
+ function reviseCriterion(plan, proposal, now) {
2182
+ const target = goal(plan, targets(proposal)[0]);
2183
+ const index = target?.successCriteria.findIndex((item) => item.id === proposal.criterionId) ?? -1;
2184
+ const current = target?.successCriteria[index];
2185
+ if (target === undefined || current === undefined)
2186
+ return;
2187
+ const model2 = read2(proposal, "userModel");
2188
+ target.successCriteria[index] = { ...current, scenario: text3(proposal, "scenario") ?? current.scenario, expectedEvidence: text3(proposal, "expectedEvidence") ?? current.expectedEvidence, userModel: isModel2(model2) ? model2 : current.userModel };
2189
+ target.updatedAt = now;
2190
+ }
2191
+
1801
2192
  // components/ulw-loop/src/steering-snapshot.ts
1802
2193
  function buildSteeringPlanSnapshot(plan, changedGoalIds) {
1803
2194
  const snapshot = {
1804
2195
  updatedAt: plan.updatedAt,
1805
2196
  goalCount: plan.goals.length,
1806
- goalIds: plan.goals.map((goal) => goal.id),
1807
- goals: plan.goals.filter((goal) => changedGoalIds.has(goal.id))
2197
+ goalIds: plan.goals.map((goal2) => goal2.id),
2198
+ goals: plan.goals.filter((goal2) => changedGoalIds.has(goal2.id))
1808
2199
  };
1809
2200
  return plan.activeGoalId === undefined ? snapshot : { ...snapshot, activeGoalId: plan.activeGoalId };
1810
2201
  }
1811
- function changedGoalIdsBetween(before, after) {
1812
- const beforeById = new Map(before.goals.map((goal) => [goal.id, goal]));
2202
+ function changedGoalIdsBetween(before, after2) {
2203
+ const beforeById = new Map(before.goals.map((goal2) => [goal2.id, goal2]));
1813
2204
  const changed = new Set;
1814
- for (const goal of after.goals) {
1815
- const prior = beforeById.get(goal.id);
1816
- if (prior === undefined || JSON.stringify(prior) !== JSON.stringify(goal))
1817
- changed.add(goal.id);
1818
- beforeById.delete(goal.id);
2205
+ for (const goal2 of after2.goals) {
2206
+ const prior = beforeById.get(goal2.id);
2207
+ if (prior === undefined || JSON.stringify(prior) !== JSON.stringify(goal2))
2208
+ changed.add(goal2.id);
2209
+ beforeById.delete(goal2.id);
1819
2210
  }
1820
2211
  for (const id of beforeById.keys())
1821
2212
  changed.add(id);
@@ -1825,55 +2216,54 @@ function changedGoalIdsBetween(before, after) {
1825
2216
  // components/ulw-loop/src/steering.ts
1826
2217
  var SOURCES2 = ["user_prompt_submit", "finding", "cli"];
1827
2218
  var PROTECTED = new Set(["aggregateCompletion", "codexObjective", "codexObjectiveAliases", "originalConstraints", "qualityGate", "status", "completedAt", "completionStatus"]);
1828
- var isObject = (value) => typeof value === "object" && value !== null;
1829
- var isPlain2 = (value) => isObject(value) && !Array.isArray(value);
1830
- var read = (value, key) => Object.entries(value).find(([name]) => name === key)?.[1];
1831
- var isText = (value) => typeof value === "string" && value.trim().length > 0;
1832
- var text2 = (value, key) => {
1833
- const candidate = read(value, key);
1834
- return isText(candidate) ? candidate.trim() : undefined;
2219
+ var isObject2 = (value) => typeof value === "object" && value !== null;
2220
+ var isPlain2 = (value) => isObject2(value) && !Array.isArray(value);
2221
+ var read3 = (value, key) => Object.entries(value).find(([name]) => name === key)?.[1];
2222
+ var isText2 = (value) => typeof value === "string" && value.trim().length > 0;
2223
+ var text4 = (value, key) => {
2224
+ const candidate = read3(value, key);
2225
+ return isText2(candidate) ? candidate.trim() : undefined;
1835
2226
  };
1836
2227
  var isKind2 = (value) => typeof value === "string" && ULW_LOOP_STEERING_MUTATION_KINDS.some((kind) => kind === value);
1837
2228
  var isSource2 = (value) => typeof value === "string" && SOURCES2.some((source) => source === value);
1838
- var isModel2 = (value) => typeof value === "string" && ULW_LOOP_SUCCESS_CRITERION_USER_MODELS.some((model2) => model2 === value);
2229
+ var isModel3 = (value) => typeof value === "string" && ULW_LOOP_SUCCESS_CRITERION_USER_MODELS.some((model2) => model2 === value);
1839
2230
  var texts = (value, key) => {
1840
- const candidate = read(value, key);
2231
+ const candidate = read3(value, key);
1841
2232
  return Array.isArray(candidate) && candidate.every((item) => typeof item === "string") ? candidate : [];
1842
2233
  };
1843
- function targets(proposal) {
2234
+ function targets2(proposal) {
1844
2235
  const many = texts(proposal, "targetGoalIds");
1845
- const one = text2(proposal, "targetGoalId") ?? text2(proposal, "goalId");
2236
+ const one = text4(proposal, "targetGoalId") ?? text4(proposal, "goalId");
1846
2237
  return many.length > 0 ? many : one === undefined ? [] : [one];
1847
2238
  }
1848
- var after = (proposal) => {
1849
- const candidate = read(proposal, "after");
2239
+ var after2 = (proposal) => {
2240
+ const candidate = read3(proposal, "after");
1850
2241
  return isPlain2(candidate) ? candidate : undefined;
1851
2242
  };
1852
- var revised = (proposal, direct, nested) => text2(proposal, direct) ?? text2(after(proposal) ?? proposal, nested);
1853
- function child2(value) {
2243
+ var revised2 = (proposal, direct, nested) => text4(proposal, direct) ?? text4(after2(proposal) ?? proposal, nested);
2244
+ function child3(value) {
1854
2245
  if (!isPlain2(value))
1855
2246
  return null;
1856
- const title = text2(value, "title");
1857
- const objective = text2(value, "objective");
2247
+ const title = text4(value, "title");
2248
+ const objective = text4(value, "objective");
1858
2249
  if (title === undefined || objective === undefined)
1859
2250
  return null;
1860
2251
  return { title, objective };
1861
2252
  }
1862
2253
  function childValues(proposal) {
1863
- const direct = read(proposal, "childGoals");
2254
+ const direct = read3(proposal, "childGoals");
1864
2255
  if (Array.isArray(direct) && direct.length > 0)
1865
2256
  return direct;
1866
- const nested = after(proposal);
1867
- const fromAfter = nested === undefined ? undefined : read(nested, "children");
2257
+ const nested = after2(proposal);
2258
+ const fromAfter = nested === undefined ? undefined : read3(nested, "children");
1868
2259
  return Array.isArray(fromAfter) ? fromAfter : [];
1869
2260
  }
1870
- var children2 = (proposal) => childValues(proposal).map(child2).filter((item) => item !== null);
1871
2261
  var pendingOrder = (proposal) => {
1872
2262
  const direct = texts(proposal, "pendingOrder");
1873
- return direct.length > 0 ? direct : texts(after(proposal) ?? proposal, "pendingGoalIds");
2263
+ return direct.length > 0 ? direct : texts(after2(proposal) ?? proposal, "pendingGoalIds");
1874
2264
  };
1875
2265
  function hasProtected(value) {
1876
- if (!isObject(value))
2266
+ if (!isObject2(value))
1877
2267
  return false;
1878
2268
  for (const [key, childValue] of Object.entries(value))
1879
2269
  if (PROTECTED.has(key) || key.toLowerCase().includes("complete") || hasProtected(childValue))
@@ -1883,7 +2273,7 @@ function hasProtected(value) {
1883
2273
  function allText(value) {
1884
2274
  if (typeof value === "string")
1885
2275
  return value;
1886
- return isObject(value) ? Object.values(value).map(allText).filter(Boolean).join(`
2276
+ return isObject2(value) ? Object.values(value).map(allText).filter(Boolean).join(`
1887
2277
  `) : "";
1888
2278
  }
1889
2279
  function weakens(value) {
@@ -1892,17 +2282,17 @@ function weakens(value) {
1892
2282
  }
1893
2283
  function auditFor(proposal, reasons) {
1894
2284
  const object = isPlain2(proposal) ? proposal : undefined;
1895
- const kindRaw = object === undefined ? undefined : read(object, "kind");
1896
- const sourceRaw = object === undefined ? undefined : read(object, "source");
1897
- const evidence = object === undefined ? "" : text2(object, "evidence") ?? "";
1898
- const rationale = object === undefined ? "" : text2(object, "rationale") ?? "";
1899
- const audit = { kind: isKind2(kindRaw) ? kindRaw : "annotate_ledger", source: isSource2(sourceRaw) ? sourceRaw : "cli", targetGoalIds: object === undefined ? [] : targets(object), evidence, rationale, invariant: { accepted: reasons.length === 0, structuralInvariantAccepted: reasons.length === 0, evidenceBackedNecessity: evidence.length > 0 && rationale.length > 0, noEasierCompletion: !weakens(proposal), rejectedReasons: reasons, reasons } };
2285
+ const kindRaw = object === undefined ? undefined : read3(object, "kind");
2286
+ const sourceRaw = object === undefined ? undefined : read3(object, "source");
2287
+ const evidence = object === undefined ? "" : text4(object, "evidence") ?? "";
2288
+ const rationale = object === undefined ? "" : text4(object, "rationale") ?? "";
2289
+ const audit = { kind: isKind2(kindRaw) ? kindRaw : "annotate_ledger", source: isSource2(sourceRaw) ? sourceRaw : "cli", targetGoalIds: object === undefined ? [] : targets2(object), evidence, rationale, invariant: { accepted: reasons.length === 0, structuralInvariantAccepted: reasons.length === 0, evidenceBackedNecessity: evidence.length > 0 && rationale.length > 0, noEasierCompletion: !weakens(proposal), rejectedReasons: reasons, reasons } };
1900
2290
  if (object === undefined)
1901
2291
  return audit;
1902
- const criterionId = text2(object, "criterionId");
1903
- const directiveText = text2(object, "directiveText");
1904
- const promptSignature = text2(object, "promptSignature");
1905
- const idempotencyKey = text2(object, "idempotencyKey");
2292
+ const criterionId = text4(object, "criterionId");
2293
+ const directiveText = text4(object, "directiveText");
2294
+ const promptSignature = text4(object, "promptSignature");
2295
+ const idempotencyKey = text4(object, "idempotencyKey");
1906
2296
  if (criterionId !== undefined)
1907
2297
  audit.criterionId = criterionId;
1908
2298
  if (directiveText !== undefined)
@@ -1918,14 +2308,14 @@ function validateUlwLoopSteeringProposal(plan, proposal) {
1918
2308
  if (!isPlain2(proposal))
1919
2309
  reasons.push("proposal must be an object");
1920
2310
  const object = isPlain2(proposal) ? proposal : {};
1921
- const kind = read(object, "kind");
2311
+ const kind = read3(object, "kind");
1922
2312
  if (!isKind2(kind))
1923
2313
  reasons.push(`invalid kind: ${String(kind)}`);
1924
- if (!isSource2(read(object, "source")))
1925
- reasons.push(`invalid source: ${String(read(object, "source"))}`);
1926
- if (text2(object, "evidence") === undefined)
2314
+ if (!isSource2(read3(object, "source")))
2315
+ reasons.push(`invalid source: ${String(read3(object, "source"))}`);
2316
+ if (text4(object, "evidence") === undefined)
1927
2317
  reasons.push("missing evidence");
1928
- if (text2(object, "rationale") === undefined)
2318
+ if (text4(object, "rationale") === undefined)
1929
2319
  reasons.push("missing rationale");
1930
2320
  if (hasProtected(proposal))
1931
2321
  reasons.push("protected payload");
@@ -1937,12 +2327,12 @@ function validateUlwLoopSteeringProposal(plan, proposal) {
1937
2327
  validateKind(plan, object, kind, reasons);
1938
2328
  return auditFor(proposal, reasons);
1939
2329
  }
1940
- function goal(plan, id) {
2330
+ function goal2(plan, id) {
1941
2331
  return id === undefined ? undefined : plan.goals.find((item) => item.id === id);
1942
2332
  }
1943
2333
  function validateKind(plan, proposal, kind, reasons) {
1944
- const target = goal(plan, targets(proposal)[0]);
1945
- if (kind === "add_subgoal" && (text2(proposal, "title") === undefined || text2(proposal, "objective") === undefined))
2334
+ const target = goal2(plan, targets2(proposal)[0]);
2335
+ if (kind === "add_subgoal" && (text4(proposal, "title") === undefined || text4(proposal, "objective") === undefined))
1946
2336
  reasons.push("add_subgoal requires title/objective");
1947
2337
  if ((kind === "split_subgoal" || kind === "revise_pending_wording" || kind === "mark_blocked_superseded") && target === undefined)
1948
2338
  reasons.push(`${kind} requires target`);
@@ -1951,11 +2341,11 @@ function validateKind(plan, proposal, kind, reasons) {
1951
2341
  const rawChildren = childValues(proposal);
1952
2342
  if (kind === "split_subgoal" && rawChildren.length === 0)
1953
2343
  reasons.push("split_subgoal requires children");
1954
- if ((kind === "split_subgoal" || kind === "mark_blocked_superseded") && rawChildren.some((item) => child2(item) === null))
2344
+ if ((kind === "split_subgoal" || kind === "mark_blocked_superseded") && rawChildren.some((item) => child3(item) === null))
1955
2345
  reasons.push(`${kind} children require title/objective`);
1956
2346
  if (kind === "reorder_pending")
1957
2347
  validateOrder(plan, proposal, reasons);
1958
- if (kind === "revise_pending_wording" && revised(proposal, "revisedTitle", "title") === undefined && revised(proposal, "revisedObjective", "objective") === undefined)
2348
+ if (kind === "revise_pending_wording" && revised2(proposal, "revisedTitle", "title") === undefined && revised2(proposal, "revisedObjective", "objective") === undefined)
1959
2349
  reasons.push("revise_pending_wording requires update");
1960
2350
  if (kind === "revise_criterion")
1961
2351
  validateCriterion(plan, proposal, reasons);
@@ -1971,31 +2361,18 @@ function validateOrder(plan, proposal, reasons) {
1971
2361
  reasons.push("unknown pending id");
1972
2362
  }
1973
2363
  function validateCriterion(plan, proposal, reasons) {
1974
- const target = goal(plan, targets(proposal)[0]);
1975
- const criterionId = text2(proposal, "criterionId");
2364
+ const target = goal2(plan, targets2(proposal)[0]);
2365
+ const criterionId = text4(proposal, "criterionId");
1976
2366
  if (target === undefined)
1977
2367
  reasons.push("revise_criterion requires goalId");
1978
2368
  else if (criterionId === undefined || target.successCriteria.every((item) => item.id !== criterionId))
1979
2369
  reasons.push("revise_criterion requires criterionId");
1980
- const model2 = read(proposal, "userModel");
1981
- if (read(proposal, "scenario") === undefined && read(proposal, "expectedEvidence") === undefined && model2 === undefined)
2370
+ const model2 = read3(proposal, "userModel");
2371
+ if (read3(proposal, "scenario") === undefined && read3(proposal, "expectedEvidence") === undefined && model2 === undefined)
1982
2372
  reasons.push("revise_criterion requires update");
1983
- if (model2 !== undefined && !isModel2(model2))
2373
+ if (model2 !== undefined && !isModel3(model2))
1984
2374
  reasons.push("invalid userModel");
1985
2375
  }
1986
- function nextId(plan, offset) {
1987
- const max = plan.goals.reduce((current, item) => {
1988
- const digits = /^G(\d+)(?:-|$)/u.exec(item.id)?.[1];
1989
- return digits === undefined ? current : Math.max(current, Number(digits));
1990
- }, 0);
1991
- return `G${String(max + offset).padStart(3, "0")}`;
1992
- }
1993
- function makeGoal2(plan, childGoal, evidence, now, offset) {
1994
- const id = nextId(plan, offset);
1995
- const digits = /^G(\d+)/u.exec(id)?.[1];
1996
- const goalIndex = digits === undefined ? plan.goals.length + offset - 1 : Number(digits) - 1;
1997
- return { id, title: childGoal.title, objective: childGoal.objective, status: "pending", successCriteria: seedDefaultSuccessCriteria(goalIndex, childGoal.objective), attempt: 0, createdAt: now, updatedAt: now, evidence };
1998
- }
1999
2376
  function applySteeringMutation(plan, proposal, audit) {
2000
2377
  const next = structuredClone(plan);
2001
2378
  if (!audit.invariant.accepted)
@@ -2005,7 +2382,7 @@ function applySteeringMutation(plan, proposal, audit) {
2005
2382
  next.goals.push(makeGoal2(next, { title: proposal.title ?? "", objective: proposal.objective ?? "" }, proposal.evidence, now, 1));
2006
2383
  if (proposal.kind === "reorder_pending") {
2007
2384
  const order = pendingOrder(proposal);
2008
- next.goals = [...order.map((id) => goal(next, id)).filter((item) => item !== undefined), ...next.goals.filter((item) => !order.includes(item.id))];
2385
+ next.goals = [...order.map((id) => goal2(next, id)).filter((item) => item !== undefined), ...next.goals.filter((item) => !order.includes(item.id))];
2009
2386
  }
2010
2387
  if (proposal.kind === "revise_pending_wording")
2011
2388
  reviseWording(next, proposal, now);
@@ -2017,53 +2394,11 @@ function applySteeringMutation(plan, proposal, audit) {
2017
2394
  next.updatedAt = now;
2018
2395
  return next;
2019
2396
  }
2020
- function reviseWording(plan, proposal, now) {
2021
- const target = goal(plan, targets(proposal)[0]);
2022
- if (target === undefined)
2023
- return;
2024
- target.title = revised(proposal, "revisedTitle", "title") ?? target.title;
2025
- target.objective = revised(proposal, "revisedObjective", "objective") ?? target.objective;
2026
- target.steeringEvidence = proposal.evidence;
2027
- target.steeringRationale = proposal.rationale;
2028
- target.updatedAt = now;
2029
- }
2030
- function splitOrBlock(plan, proposal, now) {
2031
- const target = goal(plan, targets(proposal)[0]);
2032
- if (target === undefined)
2033
- return;
2034
- const replacements = children2(proposal).map((item, index) => makeGoal2(plan, item, proposal.evidence, now, index + 1));
2035
- target.steeringEvidence = proposal.evidence;
2036
- target.steeringRationale = proposal.rationale;
2037
- target.updatedAt = now;
2038
- if (replacements.length === 0) {
2039
- target.status = "blocked";
2040
- target.steeringStatus = "blocked";
2041
- target.blockedReason = proposal.blockedReason ?? proposal.rationale;
2042
- } else {
2043
- target.steeringStatus = "superseded";
2044
- target.supersededBy = replacements.map((item) => item.id);
2045
- for (const item of replacements)
2046
- item.supersedes = [target.id];
2047
- plan.goals.splice(plan.goals.indexOf(target) + 1, 0, ...replacements);
2048
- }
2049
- if (plan.activeGoalId === target.id)
2050
- delete plan.activeGoalId;
2051
- }
2052
- function reviseCriterion(plan, proposal, now) {
2053
- const target = goal(plan, targets(proposal)[0]);
2054
- const index = target?.successCriteria.findIndex((item) => item.id === proposal.criterionId) ?? -1;
2055
- const current = target?.successCriteria[index];
2056
- if (target === undefined || current === undefined)
2057
- return;
2058
- const model2 = read(proposal, "userModel");
2059
- target.successCriteria[index] = { ...current, scenario: text2(proposal, "scenario") ?? current.scenario, expectedEvidence: text2(proposal, "expectedEvidence") ?? current.expectedEvidence, userModel: isModel2(model2) ? model2 : current.userModel };
2060
- target.updatedAt = now;
2061
- }
2062
2397
  function isProposal(value) {
2063
- return isPlain2(value) && isKind2(read(value, "kind")) && isSource2(read(value, "source")) && isText(read(value, "evidence")) && isText(read(value, "rationale"));
2398
+ return isPlain2(value) && isKind2(read3(value, "kind")) && isSource2(read3(value, "source")) && isText2(read3(value, "evidence")) && isText2(read3(value, "rationale"));
2064
2399
  }
2065
- function parseUlwLoopSteeringDirective(text3) {
2066
- const match = /(?:^|\s)(?:OMO_ULW_LOOP_STEER|omo\.ulw-loop\.steer|omo ulw-loop steer):\s*([\s\S]+)$/u.exec(text3);
2400
+ function parseUlwLoopSteeringDirective(text5) {
2401
+ const match = /(?:^|\s)(?:OMO_ULW_LOOP_STEER|omo\.ulw-loop\.steer|omo ulw-loop steer):\s*([\s\S]+)$/u.exec(text5);
2067
2402
  if (match?.[1] === undefined)
2068
2403
  return null;
2069
2404
  try {
@@ -2093,9 +2428,13 @@ async function steerUlwLoop(repoRoot, proposal, scope) {
2093
2428
  finalAudit.before = buildSteeringPlanSnapshot(plan, changed);
2094
2429
  finalAudit.after = buildSteeringPlanSnapshot(next, changed);
2095
2430
  }
2431
+ const at = proposal.now?.toISOString() ?? iso();
2432
+ const batchEntry = accepted ? batchUpdateLedgerEntry(plan, next, at) : null;
2096
2433
  if (accepted)
2097
2434
  await writePlan(repoRoot, next, scope);
2098
- await appendLedger(repoRoot, ledgerEntry(proposal, finalAudit, proposal.now?.toISOString() ?? iso()), scope);
2435
+ await appendLedger(repoRoot, ledgerEntry(proposal, finalAudit, at), scope);
2436
+ if (batchEntry !== null)
2437
+ await appendLedger(repoRoot, batchEntry, scope);
2099
2438
  return { plan: next, accepted, audit: finalAudit, rejectedReasons: audit.invariant.rejectedReasons, deduped: false };
2100
2439
  });
2101
2440
  }
@@ -2111,6 +2450,87 @@ function ledgerEntry(proposal, audit, at) {
2111
2450
  return entry;
2112
2451
  }
2113
2452
 
2453
+ // components/ulw-loop/src/steering-batch.ts
2454
+ async function steerUlwLoopBatch(repoRoot, proposals, scope) {
2455
+ return withUlwLoopMutationLock(repoRoot, scope, async () => {
2456
+ const plan = await readUlwLoopPlan(repoRoot, scope);
2457
+ const prepared = await prepareBatch(repoRoot, plan, proposals, scope);
2458
+ const failed = prepared.results.find((item) => !item.accepted);
2459
+ if (failed !== undefined) {
2460
+ const entry = rejectedLedgerEntry(prepared.results);
2461
+ await appendLedger(repoRoot, entry, scope);
2462
+ return rejected(plan, prepared.results, failed.rejectedReasons);
2463
+ }
2464
+ let next = plan;
2465
+ for (const item of prepared.items)
2466
+ if (item.kind === "fresh")
2467
+ next = item.prepared.next;
2468
+ const fresh = prepared.items.filter((item) => item.kind === "fresh");
2469
+ if (fresh.length > 0) {
2470
+ await writePlan(repoRoot, next, scope);
2471
+ const entries = fresh.map((item) => ledgerEntry2(item.prepared.proposal, item.prepared.audit, item.prepared.proposal.now?.toISOString() ?? iso()));
2472
+ const batchEntry = batchUpdateLedgerEntry(plan, next, iso());
2473
+ await appendLedgerEntries(repoRoot, batchEntry === null ? entries : [...entries, batchEntry], scope);
2474
+ }
2475
+ return { plan: next, accepted: true, results: prepared.results, rejectedReasons: [] };
2476
+ });
2477
+ }
2478
+ async function prepareBatch(repoRoot, plan, proposals, scope) {
2479
+ const items = [];
2480
+ const results = [];
2481
+ let current = plan;
2482
+ for (const proposal of proposals) {
2483
+ const key = proposal.idempotencyKey ?? proposal.promptSignature;
2484
+ const prior = key === undefined ? undefined : await findAcceptedSteeringLedgerEntry(repoRoot, key, scope);
2485
+ if (prior?.steering !== undefined) {
2486
+ const result2 = { accepted: true, deduped: true, audit: { ...prior.steering, deduped: true }, rejectedReasons: [] };
2487
+ items.push({ kind: "deduped", result: result2 });
2488
+ results.push(result2);
2489
+ continue;
2490
+ }
2491
+ const audit = validateUlwLoopSteeringProposal(current, proposal);
2492
+ if (!audit.invariant.accepted) {
2493
+ const result2 = { accepted: false, deduped: false, audit, rejectedReasons: audit.invariant.rejectedReasons };
2494
+ items.push({ kind: "deduped", result: result2 });
2495
+ results.push(result2);
2496
+ continue;
2497
+ }
2498
+ const next = applySteeringMutation(current, proposal, audit);
2499
+ const changed = changedGoalIdsBetween(current, next);
2500
+ const finalAudit = { ...audit, before: buildSteeringPlanSnapshot(current, changed), after: buildSteeringPlanSnapshot(next, changed) };
2501
+ const result = { accepted: true, deduped: false, audit: finalAudit, rejectedReasons: [] };
2502
+ items.push({ kind: "fresh", prepared: { proposal, audit: finalAudit, before: current, next } });
2503
+ results.push(result);
2504
+ current = next;
2505
+ }
2506
+ return { items, results };
2507
+ }
2508
+ function rejected(plan, results, rejectedReasons) {
2509
+ return { plan, accepted: false, results, rejectedReasons };
2510
+ }
2511
+ function rejectedLedgerEntry(results) {
2512
+ const rejectedItems = results.map((result, index) => ({ result, index })).filter((item) => !item.result.accepted);
2513
+ return { at: iso(), kind: "steering_rejected", message: rejectedItems.map((item) => `index ${item.index}: ${item.result.rejectedReasons.join(", ")}`).join("; ") };
2514
+ }
2515
+ function ledgerEntry2(proposal, audit, at) {
2516
+ const entry = {
2517
+ at,
2518
+ kind: proposal.kind === "revise_criterion" ? "criteria_revised" : "steering_accepted",
2519
+ evidence: proposal.evidence,
2520
+ message: proposal.rationale,
2521
+ steering: audit,
2522
+ mutationKind: proposal.kind
2523
+ };
2524
+ const goalId = audit.targetGoalIds[0];
2525
+ if (goalId !== undefined)
2526
+ entry.goalId = goalId;
2527
+ if (proposal.criterionId !== undefined)
2528
+ entry.criterionId = proposal.criterionId;
2529
+ if (proposal.idempotencyKey !== undefined)
2530
+ entry.idempotencyKey = proposal.idempotencyKey;
2531
+ return entry;
2532
+ }
2533
+
2114
2534
  // components/ulw-loop/src/cli-subcommands.ts
2115
2535
  async function createGoals(repoRoot, argv, json, scope) {
2116
2536
  const briefFile = readValue(argv, "--brief-file");
@@ -2118,10 +2538,12 @@ async function createGoals(repoRoot, argv, json, scope) {
2118
2538
  if (!brief.trim()) {
2119
2539
  throw new UlwLoopError("Missing brief text. Pass --brief, --brief-file, --from-stdin, or positional text.", "ULW_LOOP_BRIEF_REQUIRED");
2120
2540
  }
2541
+ const validationBatchesJson = readValue(argv, "--validation-batch-json");
2121
2542
  const plan = await createUlwLoopPlan(repoRoot, {
2122
2543
  brief,
2123
2544
  codexGoalMode: normalizeCodexGoalMode(readValue(argv, "--codex-goal-mode")),
2124
- force: hasFlag(argv, "--force")
2545
+ force: hasFlag(argv, "--force"),
2546
+ ...validationBatchesJson === undefined ? {} : { validationBatchesJson }
2125
2547
  }, scope);
2126
2548
  if (json)
2127
2549
  printJson({ ok: true, plan, summary: summarizeUlwLoopPlan(plan) });
@@ -2137,7 +2559,7 @@ ledger: ${plan.ledgerPath}
2137
2559
  async function status(repoRoot, json, scope) {
2138
2560
  const plan = await readUlwLoopPlan(repoRoot, scope);
2139
2561
  if (json) {
2140
- const active = plan.goals.find((goal2) => goal2.id === plan.activeGoalId);
2562
+ const active = plan.goals.find((goal3) => goal3.id === plan.activeGoalId);
2141
2563
  const currentAttemptDir = plan.evidenceLayoutVersion === 2 && active ? ulwLoopAttemptEvidenceDir(active.id, active.attempt, scope) : undefined;
2142
2564
  printJson({
2143
2565
  ok: true,
@@ -2175,38 +2597,20 @@ async function completeGoals(repoRoot, argv, json, scope) {
2175
2597
  `);
2176
2598
  return 0;
2177
2599
  }
2178
- async function checkpoint(repoRoot, argv, json, scope) {
2179
- const goalId = required3(argv, "--goal-id");
2180
- const statusValue = checkpointStatus(required3(argv, "--status"));
2181
- const evidence = required3(argv, "--evidence");
2182
- const codexGoalJson = await parseCodexGoalJson(statusValue === "complete" ? required3(argv, "--codex-goal-json") : readValue(argv, "--codex-goal-json"));
2183
- if (statusValue === "complete" && codexGoalJson === undefined) {
2184
- throw new UlwLoopError("Missing --codex-goal-json.", "ULW_LOOP_CODEX_GOAL_JSON_REQUIRED");
2185
- }
2186
- const qualityGateJson = readValue(argv, "--quality-gate-json");
2187
- const args = {
2188
- goalId,
2189
- status: statusValue,
2190
- evidence,
2191
- ...codexGoalJson === undefined ? {} : { codexGoalJson },
2192
- ...qualityGateJson === undefined ? {} : { qualityGateJson }
2193
- };
2194
- const result = await checkpointUlwLoop(repoRoot, args, scope);
2195
- if (json)
2196
- printJson({ ok: true, ...result, summary: summarizeUlwLoopPlan(result.plan) });
2197
- else
2198
- process.stdout.write(`ulw-loop checkpoint: ${result.goal.id} -> ${result.goal.status}
2199
- `);
2200
- return 0;
2201
- }
2202
2600
  async function steer(repoRoot, argv, json, scope) {
2203
- const proposal = await parseSteeringProposal(argv);
2204
- const result = await steerUlwLoop(repoRoot, proposal, scope);
2205
- printSteerResult(result, json);
2601
+ const proposals = await parseSteeringProposals(argv);
2602
+ const single = proposals[0];
2603
+ if (single !== undefined && proposals.length === 1 && readValue(argv, "--proposals-json") === undefined) {
2604
+ const result2 = await steerUlwLoop(repoRoot, single, scope);
2605
+ printSteerResult(result2, json);
2606
+ return result2.accepted ? 0 : 1;
2607
+ }
2608
+ const result = await steerUlwLoopBatch(repoRoot, proposals, scope);
2609
+ printSteerBatchResult(result, json);
2206
2610
  return result.accepted ? 0 : 1;
2207
2611
  }
2208
2612
  async function addGoal(repoRoot, argv, json, scope) {
2209
- const result = await addUlwLoopGoal(repoRoot, { title: required3(argv, "--title"), objective: required3(argv, "--objective") }, scope);
2613
+ const result = await addUlwLoopGoal(repoRoot, { title: required4(argv, "--title"), objective: required4(argv, "--objective") }, scope);
2210
2614
  if (json)
2211
2615
  printJson({ ok: true, plan: result.plan, goal: result.goal, summary: summarizeUlwLoopPlan(result.plan) });
2212
2616
  else {
@@ -2217,13 +2621,13 @@ async function addGoal(repoRoot, argv, json, scope) {
2217
2621
  return 0;
2218
2622
  }
2219
2623
  async function criteria(repoRoot, argv, json, scope) {
2220
- const goalId = required3(argv, "--goal-id");
2221
- const goal2 = findGoal3(await readUlwLoopPlan(repoRoot, scope), goalId);
2624
+ const goalId = required4(argv, "--goal-id");
2625
+ const goal3 = findGoal3(await readUlwLoopPlan(repoRoot, scope), goalId);
2222
2626
  if (json)
2223
- printJson({ ok: true, goalId: goal2.id, criteria: goal2.successCriteria });
2627
+ printJson({ ok: true, goalId: goal3.id, criteria: goal3.successCriteria });
2224
2628
  else {
2225
- process.stdout.write(`criteria for ${goal2.id}:
2226
- ${goal2.successCriteria.map(formatCriterionForCli).join(`
2629
+ process.stdout.write(`criteria for ${goal3.id}:
2630
+ ${goal3.successCriteria.map(formatCriterionForCli).join(`
2227
2631
  `)}
2228
2632
  `);
2229
2633
  }
@@ -2240,15 +2644,15 @@ async function captureEvidence(repoRoot, argv, json, scope) {
2240
2644
  return 0;
2241
2645
  }
2242
2646
  async function reviewBlockers(repoRoot, argv, json, scope) {
2243
- const codexGoalJson = await parseCodexGoalJson(required3(argv, "--codex-goal-json"));
2647
+ const codexGoalJson = await parseCodexGoalJson(required4(argv, "--codex-goal-json"));
2244
2648
  if (codexGoalJson === undefined) {
2245
2649
  throw new UlwLoopError("Missing --codex-goal-json.", "ULW_LOOP_CODEX_GOAL_JSON_REQUIRED");
2246
2650
  }
2247
2651
  const result = await recordFinalReviewBlockers(repoRoot, {
2248
- goalId: required3(argv, "--goal-id"),
2249
- title: required3(argv, "--title"),
2250
- objective: required3(argv, "--objective"),
2251
- evidence: required3(argv, "--evidence"),
2652
+ goalId: required4(argv, "--goal-id"),
2653
+ title: required4(argv, "--title"),
2654
+ objective: required4(argv, "--objective"),
2655
+ evidence: required4(argv, "--evidence"),
2252
2656
  codexGoalJson
2253
2657
  }, scope);
2254
2658
  if (json) {
@@ -2270,21 +2674,16 @@ function formatCriterionForCli(criterion) {
2270
2674
  const marker = isEssentialCriterion(criterion) ? "essential" : "non-essential";
2271
2675
  return `- ${criterion.id} [${criterion.status}] [${marker}] (${criterion.userModel}) ${criterion.scenario} evidence: ${criterion.capturedEvidence ?? "pending"}`;
2272
2676
  }
2273
- function required3(argv, flag) {
2677
+ function required4(argv, flag) {
2274
2678
  const value = readValue(argv, flag)?.trim();
2275
2679
  if (value)
2276
2680
  return value;
2277
2681
  throw new UlwLoopError(`Missing ${flag}.`, "ULW_LOOP_ARGUMENT_MISSING", { details: { flag } });
2278
2682
  }
2279
- function checkpointStatus(value) {
2280
- if (value === "complete" || value === "failed" || value === "blocked")
2281
- return value;
2282
- throw new UlwLoopError("Missing or invalid --status; expected complete, failed, or blocked.", "ULW_LOOP_STATUS_INVALID", { details: { status: value } });
2283
- }
2284
2683
  function findGoal3(plan, goalId) {
2285
- const goal2 = plan.goals.find((candidate) => candidate.id === goalId);
2286
- if (goal2 !== undefined)
2287
- return goal2;
2684
+ const goal3 = plan.goals.find((candidate) => candidate.id === goalId);
2685
+ if (goal3 !== undefined)
2686
+ return goal3;
2288
2687
  throw new UlwLoopError(`Unknown ulw-loop id: ${goalId}.`, "ULW_LOOP_GOAL_NOT_FOUND", { details: { goalId } });
2289
2688
  }
2290
2689
 
@@ -2727,26 +3126,26 @@ function consumeFanOutBudget(stateDir) {
2727
3126
  function missingGateArtifact(payload, plan) {
2728
3127
  if (!isGateReviewerSpawn(payload.tool_input))
2729
3128
  return null;
2730
- const goal2 = plan.goals.find((candidate) => isFinalRunCompletionCandidate(plan, candidate));
2731
- if (goal2 === undefined || goal2.status === "complete")
3129
+ const goal3 = plan.goals.find((candidate) => isFinalRunCompletionCandidate(plan, candidate));
3130
+ if (goal3 === undefined || goal3.status === "complete")
2732
3131
  return null;
2733
- if (!goal2.successCriteria.every((criterion) => criterion.status === "pass"))
3132
+ if (!goal3.successCriteria.every((criterion) => criterion.status === "pass"))
2734
3133
  return null;
2735
3134
  const scope = { sessionId: payload.session_id };
2736
3135
  if (plan.evidenceLayoutVersion === 2) {
2737
- const attemptDir = ulwLoopAttemptEvidenceDir(goal2.id, goal2.attempt, scope);
2738
- for (const name of [`${goal2.id}-code-review.md`, `${goal2.id}-manual-qa.md`]) {
3136
+ const attemptDir = ulwLoopAttemptEvidenceDir(goal3.id, goal3.attempt, scope);
3137
+ for (const name of [`${goal3.id}-code-review.md`, `${goal3.id}-manual-qa.md`]) {
2739
3138
  const relative2 = `${attemptDir}/${name}`;
2740
3139
  if (!isNonEmptyFile(join2(payload.cwd, relative2)))
2741
3140
  return relative2;
2742
3141
  }
2743
3142
  return null;
2744
3143
  }
2745
- const flatReport = `.omo/evidence/${goal2.id}-code-review.md`;
3144
+ const flatReport = `.omo/evidence/${goal3.id}-code-review.md`;
2746
3145
  if (!isNonEmptyFile(join2(payload.cwd, flatReport)))
2747
3146
  return flatReport;
2748
- if (!hasOtherEvidenceFile(join2(payload.cwd, ".omo", "evidence"), `${goal2.id}-code-review.md`))
2749
- return `.omo/evidence/<any manual-QA artifact besides ${goal2.id}-code-review.md>`;
3147
+ if (!hasOtherEvidenceFile(join2(payload.cwd, ".omo", "evidence"), `${goal3.id}-code-review.md`))
3148
+ return `.omo/evidence/<any manual-QA artifact besides ${goal3.id}-code-review.md>`;
2750
3149
  return null;
2751
3150
  }
2752
3151
  function isGateReviewerSpawn(toolInput) {
@@ -2817,7 +3216,7 @@ function readPlan(goalsPath) {
2817
3216
 
2818
3217
  // components/ulw-loop/src/stop-resume-hook.ts
2819
3218
  import { existsSync as existsSync7, readFileSync as readFileSync4, writeFileSync as writeFileSync2 } from "node:fs";
2820
- import { isAbsolute as isAbsolute2, join as join3 } from "node:path";
3219
+ import { isAbsolute as isAbsolute2, join as join3, resolve as resolve4, sep as sep2 } from "node:path";
2821
3220
  var RESUME_CAP = 2;
2822
3221
  var CONTEXT_PRESSURE_MARKERS2 = [
2823
3222
  "context compacted",
@@ -2840,14 +3239,14 @@ function runStopResumeHook(input) {
2840
3239
  const plan = readPlan2(join3(stateDir, "goals.json"));
2841
3240
  if (plan === null || plan.aggregateCompletion?.status === "complete")
2842
3241
  return "";
2843
- const goal2 = resumableGoal(plan);
2844
- if (goal2 === undefined)
3242
+ const goal3 = resumableGoal(plan);
3243
+ if (goal3 === undefined)
2845
3244
  return "";
2846
- if (!consumeResumeBudget(stateDir, goal2.id))
3245
+ if (!consumeResumeBudget(stateDir, goal3.id))
2847
3246
  return "";
2848
3247
  const output = {
2849
3248
  decision: "block",
2850
- reason: renderResumeDirective(plan, goal2, payload.session_id)
3249
+ reason: renderResumeDirective(plan, goal3, payload.session_id)
2851
3250
  };
2852
3251
  return JSON.stringify(output);
2853
3252
  }
@@ -2865,36 +3264,42 @@ async function runStopResumeHookCli(stdin, stdout) {
2865
3264
  }
2866
3265
  }
2867
3266
  function resumableGoal(plan) {
2868
- const active = plan.goals.find((goal2) => goal2.id === plan.activeGoalId);
3267
+ const active = plan.goals.find((goal3) => goal3.id === plan.activeGoalId);
2869
3268
  if (active !== undefined && isResumableStatus(active.status))
2870
3269
  return active;
2871
- return plan.goals.find((goal2) => isResumableStatus(goal2.status));
3270
+ return plan.goals.find((goal3) => isResumableStatus(goal3.status));
2872
3271
  }
2873
3272
  function isResumableStatus(status2) {
2874
3273
  return status2 === "pending" || status2 === "in_progress";
2875
3274
  }
2876
3275
  function consumeResumeBudget(stateDir, goalId) {
2877
3276
  const ledgerLineCount = countLedgerLines(join3(stateDir, "ledger.jsonl"));
2878
- const counterPath = join3(stateDir, `auto-resume-${goalId}.json`);
3277
+ const counterPath = resolve4(stateDir, `auto-resume-${goalId}.json`);
3278
+ const stuckPath = resolve4(stateDir, `auto-resume-${goalId}.stuck`);
3279
+ if (!isInsideDir(stateDir, counterPath) || !isInsideDir(stateDir, stuckPath))
3280
+ return false;
2879
3281
  const previous = readCounter(counterPath);
2880
3282
  const count = previous !== null && previous.ledgerLineCount === ledgerLineCount ? previous.count : 0;
2881
3283
  if (count >= RESUME_CAP) {
2882
- writeFileSync2(join3(stateDir, `auto-resume-${goalId}.stuck`), `no ledger progress after ${count} resumes
3284
+ writeFileSync2(stuckPath, `no ledger progress after ${count} resumes
2883
3285
  `);
2884
3286
  return false;
2885
3287
  }
2886
3288
  writeFileSync2(counterPath, JSON.stringify({ count: count + 1, ledgerLineCount }));
2887
3289
  return true;
2888
3290
  }
2889
- function renderResumeDirective(plan, goal2, sessionId) {
3291
+ function isInsideDir(dir, candidate) {
3292
+ return candidate.startsWith(resolve4(dir) + sep2);
3293
+ }
3294
+ function renderResumeDirective(plan, goal3, sessionId) {
2890
3295
  const normalized = normalizeUlwLoopSessionId(sessionId);
2891
3296
  const option = normalized !== null && plan.goalsPath.includes(`/${normalized}/`) ? ` --session-id ${normalized}` : "";
2892
3297
  return [
2893
- `The ulw-loop run in this session still has unfinished goals (next: ${goal2.id} — ${goal2.title}).`,
3298
+ `The ulw-loop run in this session still has unfinished goals (next: ${goal3.id} — ${goal3.title}).`,
2894
3299
  "The turn ended before the loop completed. Resume it now:",
2895
3300
  `1. Run \`omo ulw-loop status${option} --json\` to reload the plan, the active goal, and currentAttemptDir.`,
2896
3301
  "2. Continue the active goal's remaining success criteria, recording evidence with record-evidence.",
2897
- `3. Checkpoint through \`omo ulw-loop checkpoint${option}\` when the goal's criteria are proven.`,
3302
+ `3. Checkpoint through \`omo ulw-loop checkpoint${option}\` when the goal's criteria are proven; a complete checkpoint prints the next goal instruction.`,
2898
3303
  "If the loop is genuinely blocked on the user, checkpoint the goal as blocked with the reason instead."
2899
3304
  ].join(`
2900
3305
  `);