oh-my-opencode 5.0.0-beta.31 → 5.0.0-beta.32

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 (227) hide show
  1. package/dist/cli/index.js +136 -44
  2. package/dist/cli-node/index.js +136 -44
  3. package/dist/features/background-agent/parent-wake-notifier-types.d.ts +1 -1
  4. package/dist/features/background-agent/parent-wake-prompt-dispatch.d.ts +1 -1
  5. package/dist/features/background-agent/parent-wake-session-inspector.d.ts +1 -1
  6. package/dist/features/monitor/manager-internals.d.ts +1 -1
  7. package/dist/features/monitor/output-injector-types.d.ts +1 -1
  8. package/dist/features/team-mode/tools/messaging-live-delivery-recipient.d.ts +1 -0
  9. package/dist/features/team-mode/tools/messaging-runtime.d.ts +1 -0
  10. package/dist/hooks/anthropic-context-window-limit-recovery/tool-result-storage.d.ts +11 -0
  11. package/dist/index.js +286 -99
  12. package/dist/skills/ultimate-browsing/references/insane-search/README.md +9 -2
  13. package/dist/tools/session-manager/directory-filter.d.ts +2 -0
  14. package/dist/tui.js +109 -23
  15. package/package.json +16 -15
  16. package/packages/lsp-core/src/lsp/client-diagnostics-freshness.integration.test.ts +47 -19
  17. package/packages/lsp-core/src/lsp/client-diagnostics-pull-timeout.test.ts +64 -0
  18. package/packages/lsp-core/src/lsp/client.ts +6 -3
  19. package/packages/lsp-core/src/lsp/controlled-clock-test-support.ts +61 -0
  20. package/packages/lsp-core/src/lsp/fixtures/workspace-edit-server.mjs +10 -3
  21. package/packages/lsp-core/src/lsp/timer-provider.ts +13 -0
  22. package/packages/lsp-core/src/lsp/transport-request-timeout.test.ts +73 -0
  23. package/packages/lsp-core/src/lsp/transport.ts +7 -2
  24. package/packages/lsp-core/src/lsp/workspace-document-state.test.ts +84 -1
  25. package/packages/lsp-core/src/lsp/workspace-document-state.ts +30 -11
  26. package/packages/lsp-core/src/tools/parameters.test.ts +51 -0
  27. package/packages/lsp-daemon/dist/cli.js +41 -16
  28. package/packages/lsp-daemon/dist/client.js +41 -16
  29. package/packages/lsp-daemon/dist/index.js +41 -16
  30. package/packages/lsp-tools-mcp/dist/cli.js +41 -16
  31. package/packages/lsp-tools-mcp/dist/lsp/manager.js +41 -16
  32. package/packages/lsp-tools-mcp/dist/mcp.js +41 -16
  33. package/packages/lsp-tools-mcp/dist/tools.js +41 -16
  34. package/packages/lsp-tools-mcp/package.json +1 -1
  35. package/packages/omo-codex/plugin/.codex-plugin/plugin.json +1 -1
  36. package/packages/omo-codex/plugin/components/bootstrap/hooks/hooks.json +1 -1
  37. package/packages/omo-codex/plugin/components/bootstrap/package.json +1 -1
  38. package/packages/omo-codex/plugin/components/codegraph/dist/cli.js +3 -17
  39. package/packages/omo-codex/plugin/components/codegraph/dist/serve.js +3 -17
  40. package/packages/omo-codex/plugin/components/codegraph/package.json +1 -1
  41. package/packages/omo-codex/plugin/components/comment-checker/hooks/hooks.json +1 -1
  42. package/packages/omo-codex/plugin/components/comment-checker/package.json +1 -1
  43. package/packages/omo-codex/plugin/components/git-bash/hooks/hooks.json +2 -2
  44. package/packages/omo-codex/plugin/components/git-bash/package.json +1 -1
  45. package/packages/omo-codex/plugin/components/lazycodex-executor-verify/hooks/hooks.json +1 -1
  46. package/packages/omo-codex/plugin/components/lazycodex-executor-verify/package.json +1 -1
  47. package/packages/omo-codex/plugin/components/lsp/dist/.omo-runtime-manifest.json +3 -3
  48. package/packages/omo-codex/plugin/components/lsp/dist/cli.js +39 -16
  49. package/packages/omo-codex/plugin/components/lsp/hooks/hooks.json +2 -2
  50. package/packages/omo-codex/plugin/components/lsp/package.json +1 -1
  51. package/packages/omo-codex/plugin/components/rules/hooks/hooks.json +4 -4
  52. package/packages/omo-codex/plugin/components/rules/package.json +1 -1
  53. package/packages/omo-codex/plugin/components/teammode/hooks/hooks.json +1 -1
  54. package/packages/omo-codex/plugin/components/teammode/package.json +1 -1
  55. package/packages/omo-codex/plugin/components/telemetry/hooks/hooks.json +1 -1
  56. package/packages/omo-codex/plugin/components/telemetry/package.json +1 -1
  57. package/packages/omo-codex/plugin/components/ultrawork/AGENTS.md +5 -4
  58. package/packages/omo-codex/plugin/components/ultrawork/dist/cli.js +489 -4
  59. package/packages/omo-codex/plugin/components/ultrawork/hooks/hooks.json +1 -1
  60. package/packages/omo-codex/plugin/components/ultrawork/package.json +1 -2
  61. package/packages/omo-codex/plugin/components/ultrawork/scripts/sync-directive.mjs +30 -14
  62. package/packages/omo-codex/plugin/components/ultrawork/src/directive-content.ts +4 -0
  63. package/packages/omo-codex/plugin/components/ultrawork/src/directive.ts +5 -2
  64. package/packages/omo-codex/plugin/components/ultrawork/test/directive-source.test.ts +14 -8
  65. package/packages/omo-codex/plugin/components/ultrawork/test/package-smoke.test.ts +12 -1
  66. package/packages/omo-codex/plugin/components/ulw-execute-continuation/hooks/hooks.json +2 -2
  67. package/packages/omo-codex/plugin/components/ulw-execute-continuation/package.json +1 -1
  68. package/packages/omo-codex/plugin/components/ulw-loop/AGENTS.md +4 -2
  69. package/packages/omo-codex/plugin/components/ulw-loop/README.md +1 -0
  70. package/packages/omo-codex/plugin/components/ulw-loop/dist/checkpoint-continuation.js +9 -0
  71. package/packages/omo-codex/plugin/components/ulw-loop/dist/checkpoint-reconciliation.d.ts +14 -0
  72. package/packages/omo-codex/plugin/components/ulw-loop/dist/checkpoint-reconciliation.js +10 -1
  73. package/packages/omo-codex/plugin/components/ulw-loop/dist/checkpoint-template.d.ts +8 -0
  74. package/packages/omo-codex/plugin/components/ulw-loop/dist/checkpoint-template.js +113 -0
  75. package/packages/omo-codex/plugin/components/ulw-loop/dist/checkpoint.js +7 -5
  76. package/packages/omo-codex/plugin/components/ulw-loop/dist/cli-commands.js +2 -1
  77. package/packages/omo-codex/plugin/components/ulw-loop/dist/cli-output.d.ts +1 -1
  78. package/packages/omo-codex/plugin/components/ulw-loop/dist/cli-output.js +1 -0
  79. package/packages/omo-codex/plugin/components/ulw-loop/dist/cli-subcommands.js +2 -0
  80. package/packages/omo-codex/plugin/components/ulw-loop/dist/cli.js +775 -312
  81. package/packages/omo-codex/plugin/components/ulw-loop/dist/codex-goal-instruction.js +18 -0
  82. package/packages/omo-codex/plugin/components/ulw-loop/dist/codex-goal-snapshot.d.ts +13 -0
  83. package/packages/omo-codex/plugin/components/ulw-loop/dist/codex-goal-snapshot.js +14 -0
  84. package/packages/omo-codex/plugin/components/ulw-loop/dist/domain-types.d.ts +17 -9
  85. package/packages/omo-codex/plugin/components/ulw-loop/dist/plan-io.js +17 -2
  86. package/packages/omo-codex/plugin/components/ulw-loop/dist/plan-missing-recovery.d.ts +13 -0
  87. package/packages/omo-codex/plugin/components/ulw-loop/dist/plan-missing-recovery.js +19 -0
  88. package/packages/omo-codex/plugin/components/ulw-loop/dist/quality-gate-aggregate.d.ts +2 -0
  89. package/packages/omo-codex/plugin/components/ulw-loop/dist/quality-gate-aggregate.js +53 -0
  90. package/packages/omo-codex/plugin/components/ulw-loop/dist/quality-gate-artifacts.d.ts +9 -0
  91. package/packages/omo-codex/plugin/components/ulw-loop/dist/quality-gate-artifacts.js +85 -0
  92. package/packages/omo-codex/plugin/components/ulw-loop/dist/quality-gate-fields.js +1 -1
  93. package/packages/omo-codex/plugin/components/ulw-loop/dist/quality-gate.js +41 -99
  94. package/packages/omo-codex/plugin/components/ulw-loop/dist/review-blockers.js +4 -2
  95. package/packages/omo-codex/plugin/components/ulw-loop/dist/spawn-guard.js +184 -14
  96. package/packages/omo-codex/plugin/components/ulw-loop/dist/status-next-actions.d.ts +6 -0
  97. package/packages/omo-codex/plugin/components/ulw-loop/dist/status-next-actions.js +33 -0
  98. package/packages/omo-codex/plugin/components/ulw-loop/dist/surface.d.ts +3 -0
  99. package/packages/omo-codex/plugin/components/ulw-loop/dist/surface.js +15 -0
  100. package/packages/omo-codex/plugin/components/ulw-loop/hooks/hooks.json +4 -4
  101. package/packages/omo-codex/plugin/components/ulw-loop/package.json +1 -1
  102. package/packages/omo-codex/plugin/components/ulw-loop/skills/ulw-loop/references/full-workflow.md +17 -0
  103. package/packages/omo-codex/plugin/components/ulw-loop/src/checkpoint-continuation.ts +7 -0
  104. package/packages/omo-codex/plugin/components/ulw-loop/src/checkpoint-reconciliation.ts +25 -1
  105. package/packages/omo-codex/plugin/components/ulw-loop/src/checkpoint-template.ts +127 -0
  106. package/packages/omo-codex/plugin/components/ulw-loop/src/checkpoint.ts +8 -12
  107. package/packages/omo-codex/plugin/components/ulw-loop/src/cli-commands.ts +2 -1
  108. package/packages/omo-codex/plugin/components/ulw-loop/src/cli-output.ts +1 -0
  109. package/packages/omo-codex/plugin/components/ulw-loop/src/cli-subcommands.ts +2 -0
  110. package/packages/omo-codex/plugin/components/ulw-loop/src/codex-goal-instruction.ts +18 -0
  111. package/packages/omo-codex/plugin/components/ulw-loop/src/codex-goal-snapshot.ts +23 -0
  112. package/packages/omo-codex/plugin/components/ulw-loop/src/domain-types.ts +19 -9
  113. package/packages/omo-codex/plugin/components/ulw-loop/src/plan-io.ts +18 -3
  114. package/packages/omo-codex/plugin/components/ulw-loop/src/plan-missing-recovery.ts +27 -0
  115. package/packages/omo-codex/plugin/components/ulw-loop/src/quality-gate-aggregate.ts +57 -0
  116. package/packages/omo-codex/plugin/components/ulw-loop/src/quality-gate-artifacts.ts +103 -0
  117. package/packages/omo-codex/plugin/components/ulw-loop/src/quality-gate-fields.ts +1 -1
  118. package/packages/omo-codex/plugin/components/ulw-loop/src/quality-gate.ts +50 -116
  119. package/packages/omo-codex/plugin/components/ulw-loop/src/review-blockers.ts +4 -2
  120. package/packages/omo-codex/plugin/components/ulw-loop/src/spawn-guard.ts +186 -12
  121. package/packages/omo-codex/plugin/components/ulw-loop/src/status-next-actions.ts +36 -0
  122. package/packages/omo-codex/plugin/components/ulw-loop/src/surface.ts +22 -0
  123. package/packages/omo-codex/plugin/components/ulw-loop/test/checkpoint-final.test.ts +66 -3
  124. package/packages/omo-codex/plugin/components/ulw-loop/test/checkpoint-template.test.ts +92 -0
  125. package/packages/omo-codex/plugin/components/ulw-loop/test/cli-status-next-actions.test.ts +183 -0
  126. package/packages/omo-codex/plugin/components/ulw-loop/test/codex-goal-instruction.test.ts +13 -3
  127. package/packages/omo-codex/plugin/components/ulw-loop/test/guided-recovery.test.ts +105 -0
  128. package/packages/omo-codex/plugin/components/ulw-loop/test/quality-gate-aggregate.test.ts +101 -0
  129. package/packages/omo-codex/plugin/components/ulw-loop/test/quality-gate-roles.test.ts +16 -15
  130. package/packages/omo-codex/plugin/components/ulw-loop/test/quality-gate-senpi-surface.test.ts +131 -0
  131. package/packages/omo-codex/plugin/components/ulw-loop/test/quality-gate.test.ts +5 -0
  132. package/packages/omo-codex/plugin/components/ulw-loop/test/spawn-guard.test.ts +300 -3
  133. package/packages/omo-codex/plugin/components/ulw-loop/test/ultrawork-directive.test.ts +9 -3
  134. package/packages/omo-codex/plugin/hooks/post-compact-resetting-git-bash-mcp-reminder.json +1 -1
  135. package/packages/omo-codex/plugin/hooks/post-compact-resetting-lsp-diagnostics-cache.json +1 -1
  136. package/packages/omo-codex/plugin/hooks/post-compact-resetting-project-rule-cache.json +1 -1
  137. package/packages/omo-codex/plugin/hooks/post-tool-use-checking-codegraph-init-guidance.json +1 -1
  138. package/packages/omo-codex/plugin/hooks/post-tool-use-checking-comments.json +1 -1
  139. package/packages/omo-codex/plugin/hooks/post-tool-use-checking-lsp-diagnostics.json +1 -1
  140. package/packages/omo-codex/plugin/hooks/post-tool-use-checking-thread-title-hygiene.json +1 -1
  141. package/packages/omo-codex/plugin/hooks/post-tool-use-matching-project-rules.json +1 -1
  142. package/packages/omo-codex/plugin/hooks/pre-tool-use-enforcing-unlimited-goal-budget.json +1 -1
  143. package/packages/omo-codex/plugin/hooks/pre-tool-use-guarding-ulw-loop-spawns.json +1 -1
  144. package/packages/omo-codex/plugin/hooks/pre-tool-use-recommending-git-bash-mcp.json +1 -1
  145. package/packages/omo-codex/plugin/hooks/session-start-checking-auto-update.json +1 -1
  146. package/packages/omo-codex/plugin/hooks/session-start-checking-bootstrap-provisioning.json +1 -1
  147. package/packages/omo-codex/plugin/hooks/session-start-checking-codegraph-bootstrap.json +1 -1
  148. package/packages/omo-codex/plugin/hooks/session-start-loading-project-rules.json +1 -1
  149. package/packages/omo-codex/plugin/hooks/session-start-recording-session-telemetry.json +1 -1
  150. package/packages/omo-codex/plugin/hooks/stop-checking-ulw-execute-continuation.json +1 -1
  151. package/packages/omo-codex/plugin/hooks/stop-checking-ulw-loop-resume.json +1 -1
  152. package/packages/omo-codex/plugin/hooks/subagent-stop-checking-ulw-execute-continuation.json +1 -1
  153. package/packages/omo-codex/plugin/hooks/subagent-stop-verifying-lazycodex-executor-evidence.json +1 -1
  154. package/packages/omo-codex/plugin/hooks/user-prompt-submit-checking-ultrawork-trigger.json +1 -1
  155. package/packages/omo-codex/plugin/hooks/user-prompt-submit-checking-ulw-loop-steering.json +1 -1
  156. package/packages/omo-codex/plugin/hooks/user-prompt-submit-loading-project-rules.json +1 -1
  157. package/packages/omo-codex/plugin/package-lock.json +13 -13
  158. package/packages/omo-codex/plugin/package.json +1 -1
  159. package/packages/omo-codex/plugin/scripts/sync-skills.mjs +31 -2
  160. package/packages/omo-codex/plugin/skills/ultimate-browsing/references/insane-search/README.md +9 -2
  161. package/packages/omo-codex/plugin/skills/ulw-loop/references/full-workflow.md +17 -0
  162. package/packages/omo-codex/plugin/test/sync-skills-test-support.mjs +11 -1
  163. package/packages/omo-codex/plugin/test/sync-skills.test.mjs +13 -0
  164. package/packages/omo-codex/plugin/test/ultrawork-skill-pointer.test.mjs +5 -2
  165. package/packages/omo-codex/scripts/install-dist/install-local.mjs +2 -2
  166. package/packages/shared-skills/skills/ultimate-browsing/references/insane-search/README.md +9 -2
  167. package/dist/features/builtin-skills/index.d.ts +0 -1
  168. package/dist/features/builtin-skills/skill-file-loader.d.ts +0 -1
  169. package/dist/features/builtin-skills/skills/agent-browser-skill.d.ts +0 -1
  170. package/dist/features/builtin-skills/skills/agent-browser-template.d.ts +0 -1
  171. package/dist/features/builtin-skills/skills/debugging.d.ts +0 -1
  172. package/dist/features/builtin-skills/skills/dev-browser.d.ts +0 -1
  173. package/dist/features/builtin-skills/skills/frontend.d.ts +0 -1
  174. package/dist/features/builtin-skills/skills/git-master-sections/commit-atomic-planning.d.ts +0 -1
  175. package/dist/features/builtin-skills/skills/git-master-sections/commit-context-analysis.d.ts +0 -1
  176. package/dist/features/builtin-skills/skills/git-master-sections/commit-execution-verification.d.ts +0 -1
  177. package/dist/features/builtin-skills/skills/git-master-sections/commit-workflow.d.ts +0 -1
  178. package/dist/features/builtin-skills/skills/git-master-sections/history-search-workflow.d.ts +0 -1
  179. package/dist/features/builtin-skills/skills/git-master-sections/overview.d.ts +0 -1
  180. package/dist/features/builtin-skills/skills/git-master-sections/quick-reference.d.ts +0 -1
  181. package/dist/features/builtin-skills/skills/git-master-sections/rebase-workflow.d.ts +0 -1
  182. package/dist/features/builtin-skills/skills/git-master-skill-metadata.d.ts +0 -1
  183. package/dist/features/builtin-skills/skills/git-master.d.ts +0 -1
  184. package/dist/features/builtin-skills/skills/index.d.ts +0 -1
  185. package/dist/features/builtin-skills/skills/init-deep.d.ts +0 -1
  186. package/dist/features/builtin-skills/skills/playwright-cli.d.ts +0 -1
  187. package/dist/features/builtin-skills/skills/playwright-mcp-skill.d.ts +0 -1
  188. package/dist/features/builtin-skills/skills/playwright.d.ts +0 -1
  189. package/dist/features/builtin-skills/skills/remove-ai-slops.d.ts +0 -1
  190. package/dist/features/builtin-skills/skills/review-work.d.ts +0 -1
  191. package/dist/features/builtin-skills/skills/security-research.d.ts +0 -1
  192. package/dist/features/builtin-skills/skills/security-review.d.ts +0 -1
  193. package/dist/features/builtin-skills/skills/team-mode.d.ts +0 -1
  194. package/dist/features/builtin-skills/skills/visual-qa.d.ts +0 -1
  195. package/dist/features/builtin-skills/skills.d.ts +0 -1
  196. package/dist/features/builtin-skills/types.d.ts +0 -1
  197. package/dist/features/claude-code-plugin-loader/agent-loader.d.ts +0 -1
  198. package/dist/features/claude-code-plugin-loader/command-loader.d.ts +0 -1
  199. package/dist/features/claude-code-plugin-loader/discovery-core.d.ts +0 -1
  200. package/dist/features/claude-code-plugin-loader/discovery-paths.d.ts +0 -1
  201. package/dist/features/claude-code-plugin-loader/discovery.d.ts +0 -1
  202. package/dist/features/claude-code-plugin-loader/hook-loader.d.ts +0 -1
  203. package/dist/features/claude-code-plugin-loader/index.d.ts +0 -1
  204. package/dist/features/claude-code-plugin-loader/install-path-resolver.d.ts +0 -1
  205. package/dist/features/claude-code-plugin-loader/installed-plugin-database.d.ts +0 -1
  206. package/dist/features/claude-code-plugin-loader/loaded-plugin.d.ts +0 -1
  207. package/dist/features/claude-code-plugin-loader/loader.d.ts +0 -1
  208. package/dist/features/claude-code-plugin-loader/mcp-server-loader.d.ts +0 -1
  209. package/dist/features/claude-code-plugin-loader/plugin-key.d.ts +0 -1
  210. package/dist/features/claude-code-plugin-loader/plugin-manifest.d.ts +0 -1
  211. package/dist/features/claude-code-plugin-loader/plugin-path-resolver.d.ts +0 -1
  212. package/dist/features/claude-code-plugin-loader/plugin-settings.d.ts +0 -1
  213. package/dist/features/claude-code-plugin-loader/scope-filter.d.ts +0 -1
  214. package/dist/features/claude-code-plugin-loader/skill-loader.d.ts +0 -1
  215. package/dist/features/claude-code-plugin-loader/types.d.ts +0 -1
  216. package/dist/shared/prompt-async-gate/message-inspection-error.d.ts +0 -1
  217. package/dist/shared/prompt-async-gate/pending-tool-turn.d.ts +0 -1
  218. package/dist/shared/prompt-async-gate/prompt-message-state.d.ts +0 -1
  219. package/dist/shared/prompt-async-gate/queue.d.ts +0 -1
  220. package/dist/shared/prompt-async-gate/recent-dispatches.d.ts +0 -1
  221. package/dist/shared/prompt-async-gate/reservations.d.ts +0 -1
  222. package/dist/shared/prompt-async-gate/semantic-dedupe.d.ts +0 -1
  223. package/dist/shared/prompt-async-gate/session-idle-dispatch.d.ts +0 -1
  224. package/dist/shared/prompt-async-gate/timing.d.ts +0 -1
  225. package/dist/shared/prompt-async-gate/types.d.ts +0 -1
  226. package/packages/omo-codex/plugin/components/ultrawork/directive.md +0 -483
  227. package/packages/omo-codex/plugin/components/ultrawork/skills/ultrawork/SKILL.md +0 -490
@@ -1,17 +1,124 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- // packages/omo-codex/plugin/components/ulw-loop/src/checkpoint.ts
3
+ // components/ulw-loop/src/checkpoint.ts
4
4
  import { existsSync as existsSync4, statSync } from "node:fs";
5
5
  import { readFile as readFile5 } from "node:fs/promises";
6
- import { resolve as resolve3 } from "node:path";
6
+ import { resolve as resolve4 } from "node:path";
7
+
8
+ // components/ulw-loop/src/checkpoint-reconciliation.ts
9
+ import { existsSync as existsSync2 } from "node:fs";
10
+ import { readFile as readFile2 } from "node:fs/promises";
7
11
 
8
- // packages/omo-codex/plugin/components/ulw-loop/src/checkpoint-reconciliation.ts
12
+ // components/ulw-loop/src/codex-goal-snapshot.ts
9
13
  import { existsSync } from "node:fs";
10
14
  import { readFile } from "node:fs/promises";
15
+ import { resolve } from "node:path";
16
+
17
+ class CodexGoalSnapshotError extends Error {
18
+ }
19
+ function safeObject(value) {
20
+ return value && typeof value === "object" && !Array.isArray(value) ? value : {};
21
+ }
22
+ function safeString(value) {
23
+ return typeof value === "string" ? value.trim() : "";
24
+ }
25
+ function normalizeStatus(value) {
26
+ const status = safeString(value).toLowerCase();
27
+ if (status === "complete" || status === "completed" || status === "done")
28
+ return "complete";
29
+ if (status === "cancelled" || status === "canceled")
30
+ return "cancelled";
31
+ if (status === "failed" || status === "failure")
32
+ return "failed";
33
+ if (status === "active" || status === "in_progress" || status === "pending" || status === "running")
34
+ return "active";
35
+ return "unknown";
36
+ }
37
+ function normalizeObjective(value) {
38
+ return value.replace(/\s+/g, " ").trim();
39
+ }
40
+ function parseCodexGoalSnapshot(value) {
41
+ const root = safeObject(value);
42
+ const goalValue = Object.hasOwn(root, "goal") ? root["goal"] : value;
43
+ if (goalValue === null || goalValue === undefined || goalValue === false) {
44
+ return { available: false, raw: value };
45
+ }
46
+ const goal = safeObject(goalValue);
47
+ const objective = safeString(goal["objective"] ?? goal["goal"] ?? goal["description"] ?? goal["title"] ?? root["objective"] ?? root["title"]);
48
+ const status = normalizeStatus(goal["status"] ?? root["status"]);
49
+ return {
50
+ available: Boolean(objective || status !== "unknown"),
51
+ ...objective ? { objective } : {},
52
+ status,
53
+ raw: value
54
+ };
55
+ }
56
+ async function readCodexGoalSnapshotInput(raw, cwd = process.cwd()) {
57
+ if (!raw?.trim())
58
+ return null;
59
+ const trimmed = raw.trim();
60
+ try {
61
+ return parseCodexGoalSnapshot(JSON.parse(trimmed));
62
+ } catch {
63
+ const path = resolve(cwd, trimmed);
64
+ if (!existsSync(path)) {
65
+ throw new CodexGoalSnapshotError(`Codex goal snapshot is neither valid JSON nor a readable path: ${trimmed}`);
66
+ }
67
+ try {
68
+ return parseCodexGoalSnapshot(JSON.parse(await readFile(path, "utf-8")));
69
+ } catch (error) {
70
+ throw new CodexGoalSnapshotError(`Codex goal snapshot path does not contain valid JSON: ${trimmed}${error instanceof Error ? ` (${error.message})` : ""}`);
71
+ }
72
+ }
73
+ }
74
+ function reconcileCodexGoalSnapshot(snapshot, options) {
75
+ const effectiveSnapshot = snapshot ?? { available: false, raw: null };
76
+ const errors = [];
77
+ const warnings = [];
78
+ if (!effectiveSnapshot.available) {
79
+ const message = "Codex goal snapshot is absent or reports no active goal; call get_goal and pass its JSON with --codex-goal-json.";
80
+ if (options.requireSnapshot)
81
+ errors.push(message);
82
+ else
83
+ warnings.push(message);
84
+ return { ok: errors.length === 0, snapshot: effectiveSnapshot, warnings, errors };
85
+ }
86
+ const expected = normalizeObjective(options.expectedObjective);
87
+ const accepted = new Set([expected, ...(options.acceptedObjectives ?? []).map((objective) => normalizeObjective(objective))].filter(Boolean));
88
+ const actual = normalizeObjective(effectiveSnapshot.objective ?? "");
89
+ if (!actual) {
90
+ errors.push("Codex goal snapshot is missing objective text.");
91
+ } else if (!accepted.has(actual)) {
92
+ errors.push(`Codex goal objective mismatch: expected "${expected}", got "${actual}".`);
93
+ }
94
+ const allowed = options.allowedStatuses ?? (options.requireComplete ? ["complete"] : ["active", "complete"]);
95
+ const actualStatus = effectiveSnapshot.status ?? "unknown";
96
+ if (!allowed.includes(actualStatus)) {
97
+ errors.push(`Codex goal status mismatch: expected ${allowed.join(" or ")}, got ${actualStatus}.`);
98
+ }
99
+ if (options.requireComplete && actualStatus !== "complete") {
100
+ errors.push('Codex goal is not complete; call update_goal({status: "complete"}) only after the objective is actually complete, then pass the fresh get_goal JSON.');
101
+ }
102
+ return { ok: errors.length === 0, snapshot: effectiveSnapshot, warnings, errors };
103
+ }
104
+ function formatCodexGoalReconciliation(reconciliation) {
105
+ const parts = [...reconciliation.errors, ...reconciliation.warnings];
106
+ return parts.join(" ");
107
+ }
108
+ function codexGoalMismatchRecovery(expectedObjective, snapshot) {
109
+ const receivedObjective = snapshot?.objective ?? "";
110
+ const message = [
111
+ "Recovery: the Codex goal objective must equal the plan's codexObjective exactly — copy the expected value below into update_goal and re-run get_goal.",
112
+ `expected codexObjective: ${expectedObjective}`,
113
+ `received objective: ${receivedObjective || "(none)"}`
114
+ ].join(`
115
+ `);
116
+ return { message, details: { expectedObjective, receivedObjective } };
117
+ }
11
118
 
12
- // packages/omo-codex/plugin/components/ulw-loop/src/paths.ts
119
+ // components/ulw-loop/src/paths.ts
13
120
  import { isAbsolute, join, relative, sep } from "node:path";
14
- // packages/omo-codex/plugin/components/ulw-loop/src/constants.ts
121
+ // components/ulw-loop/src/constants.ts
15
122
  var ULW_LOOP_DIR = ".omo/ulw-loop";
16
123
  var ULW_LOOP_BRIEF = "brief.md";
17
124
  var ULW_LOOP_GOALS = "goals.json";
@@ -31,7 +138,7 @@ var ULW_LOOP_SUCCESS_CRITERION_USER_MODELS = [
31
138
  "regression",
32
139
  "adversarial"
33
140
  ];
34
- // packages/omo-codex/plugin/components/ulw-loop/src/runtime.ts
141
+ // components/ulw-loop/src/runtime.ts
35
142
  class UlwLoopError extends Error {
36
143
  constructor(message, code, opts) {
37
144
  super(message, opts?.cause === undefined ? undefined : { cause: opts.cause });
@@ -45,7 +152,7 @@ class UlwLoopError extends Error {
45
152
  function iso() {
46
153
  return new Date().toISOString();
47
154
  }
48
- // packages/omo-codex/plugin/components/ulw-loop/src/paths.ts
155
+ // components/ulw-loop/src/paths.ts
49
156
  var SESSION_ENV_KEYS = ["OMO_ULW_LOOP_SESSION_ID", "CODEX_SESSION_ID", "CODEX_THREAD_ID", "PI_SESSION_ID"];
50
157
  function normalizeUlwLoopSessionId(sessionId) {
51
158
  const trimmed = sessionId?.trim();
@@ -111,7 +218,7 @@ function isWithinAttemptDir(absolutePath, attemptRoot, pathApi = PLATFORM_PATH_A
111
218
  return !pathApi.isAbsolute(relativePath);
112
219
  }
113
220
 
114
- // packages/omo-codex/plugin/components/ulw-loop/src/goal-status.ts
221
+ // components/ulw-loop/src/goal-status.ts
115
222
  var ULW_LOOP_AGGREGATE_CODEX_OBJECTIVE = aggregateCodexObjectiveForScope();
116
223
  function aggregateCodexObjectiveForScope(scope) {
117
224
  return `Complete the durable ulw-loop plan in ${ulwLoopGoalsRelativePath(scope)}, including later accepted/appended stories, under the original brief constraints; use ${ulwLoopLedgerRelativePath(scope)} as the audit trail.`;
@@ -194,8 +301,15 @@ function hasEssentialCriteriaPass(goal) {
194
301
  return criteria.length > 0 && criteria.every((criterion) => criterion.status === "pass");
195
302
  }
196
303
 
197
- // packages/omo-codex/plugin/components/ulw-loop/src/checkpoint-reconciliation.ts
198
- function normalizeObjective(value) {
304
+ // components/ulw-loop/src/checkpoint-reconciliation.ts
305
+ function codexSnapshotMismatchError(input) {
306
+ const { reconciliation, snapshot, taskScopedHint } = input;
307
+ const hint = taskScopedHint?.aggregate === true && snapshot?.status === "complete" && snapshot.objective !== undefined ? buildTaskScopedAggregateReconciliationHint(taskScopedHint.goal, taskScopedHint.final) : "";
308
+ const recovery = codexGoalMismatchRecovery(input.expectedObjective, snapshot);
309
+ return new UlwLoopError(`${formatCodexGoalReconciliation(reconciliation)}${hint}
310
+ ${recovery.message}`, "ulw_loop_codex_snapshot_mismatch", { details: recovery.details });
311
+ }
312
+ function normalizeObjective2(value) {
199
313
  return value.replace(/\s+/g, " ").trim();
200
314
  }
201
315
  function textMentionsUlwLoopPlanArtifact(value) {
@@ -212,13 +326,13 @@ function textHasCompletionValidationEvidence(value) {
212
326
  return done && verified;
213
327
  }
214
328
  async function snapshotObjectiveMapsToUlwLoopPlan(repoRoot, snapshotObjective, scope) {
215
- const actual = normalizeObjective(snapshotObjective).toLowerCase();
329
+ const actual = normalizeObjective2(snapshotObjective).toLowerCase();
216
330
  if (textMentionsUlwLoopPlanArtifact(actual))
217
331
  return true;
218
- if (actual.length < 24 || !existsSync(ulwLoopBriefPath(repoRoot, scope)))
332
+ if (actual.length < 24 || !existsSync2(ulwLoopBriefPath(repoRoot, scope)))
219
333
  return false;
220
334
  try {
221
- const brief = normalizeObjective(await readFile(ulwLoopBriefPath(repoRoot, scope), "utf8")).toLowerCase();
335
+ const brief = normalizeObjective2(await readFile2(ulwLoopBriefPath(repoRoot, scope), "utf8")).toLowerCase();
222
336
  return brief.length >= 24 && (brief.includes(actual) || actual.includes(brief));
223
337
  } catch (error) {
224
338
  if (error instanceof Error)
@@ -265,107 +379,32 @@ function buildTaskScopedAggregateReconciliationHint(goal, final) {
265
379
  return ` Completed task-scoped aggregate reconciliation requires the checkpoint goal to be the active in-progress OMO goal, evidence that names that active OMO goal id, names .omo/ulw-loop/goals.json or ledger.jsonl, includes completed implementation plus validation/review evidence, and a get_goal objective that maps to the ulw-loop brief/artifact. ${buildCompletedLegacyGoalRemediation(goal)}`;
266
380
  }
267
381
 
268
- // packages/omo-codex/plugin/components/ulw-loop/src/codex-goal-snapshot.ts
269
- import { existsSync as existsSync2 } from "node:fs";
270
- import { readFile as readFile2 } from "node:fs/promises";
271
- import { resolve } from "node:path";
382
+ // components/ulw-loop/src/plan-io.ts
383
+ import { createReadStream, readdirSync } from "node:fs";
384
+ import { appendFile, mkdir, readFile as readFile3, rename, writeFile } from "node:fs/promises";
385
+ import { createInterface } from "node:readline";
272
386
 
273
- class CodexGoalSnapshotError extends Error {
274
- }
275
- function safeObject(value) {
276
- return value && typeof value === "object" && !Array.isArray(value) ? value : {};
277
- }
278
- function safeString(value) {
279
- return typeof value === "string" ? value.trim() : "";
280
- }
281
- function normalizeStatus(value) {
282
- const status = safeString(value).toLowerCase();
283
- if (status === "complete" || status === "completed" || status === "done")
284
- return "complete";
285
- if (status === "cancelled" || status === "canceled")
286
- return "cancelled";
287
- if (status === "failed" || status === "failure")
288
- return "failed";
289
- if (status === "active" || status === "in_progress" || status === "pending" || status === "running")
290
- return "active";
291
- return "unknown";
292
- }
293
- function normalizeObjective2(value) {
294
- return value.replace(/\s+/g, " ").trim();
295
- }
296
- function parseCodexGoalSnapshot(value) {
297
- const root = safeObject(value);
298
- const goalValue = Object.hasOwn(root, "goal") ? root["goal"] : value;
299
- if (goalValue === null || goalValue === undefined || goalValue === false) {
300
- return { available: false, raw: value };
301
- }
302
- const goal = safeObject(goalValue);
303
- const objective = safeString(goal["objective"] ?? goal["goal"] ?? goal["description"] ?? goal["title"] ?? root["objective"] ?? root["title"]);
304
- const status = normalizeStatus(goal["status"] ?? root["status"]);
305
- return {
306
- available: Boolean(objective || status !== "unknown"),
307
- ...objective ? { objective } : {},
308
- status,
309
- raw: value
310
- };
311
- }
312
- async function readCodexGoalSnapshotInput(raw, cwd = process.cwd()) {
313
- if (!raw?.trim())
314
- return null;
315
- const trimmed = raw.trim();
316
- try {
317
- return parseCodexGoalSnapshot(JSON.parse(trimmed));
318
- } catch {
319
- const path = resolve(cwd, trimmed);
320
- if (!existsSync2(path)) {
321
- throw new CodexGoalSnapshotError(`Codex goal snapshot is neither valid JSON nor a readable path: ${trimmed}`);
322
- }
323
- try {
324
- return parseCodexGoalSnapshot(JSON.parse(await readFile2(path, "utf-8")));
325
- } catch (error) {
326
- throw new CodexGoalSnapshotError(`Codex goal snapshot path does not contain valid JSON: ${trimmed}${error instanceof Error ? ` (${error.message})` : ""}`);
327
- }
328
- }
329
- }
330
- function reconcileCodexGoalSnapshot(snapshot, options) {
331
- const effectiveSnapshot = snapshot ?? { available: false, raw: null };
332
- const errors = [];
333
- const warnings = [];
334
- if (!effectiveSnapshot.available) {
335
- const message = "Codex goal snapshot is absent or reports no active goal; call get_goal and pass its JSON with --codex-goal-json.";
336
- if (options.requireSnapshot)
337
- errors.push(message);
338
- else
339
- warnings.push(message);
340
- return { ok: errors.length === 0, snapshot: effectiveSnapshot, warnings, errors };
341
- }
342
- const expected = normalizeObjective2(options.expectedObjective);
343
- const accepted = new Set([expected, ...(options.acceptedObjectives ?? []).map((objective) => normalizeObjective2(objective))].filter(Boolean));
344
- const actual = normalizeObjective2(effectiveSnapshot.objective ?? "");
345
- if (!actual) {
346
- errors.push("Codex goal snapshot is missing objective text.");
347
- } else if (!accepted.has(actual)) {
348
- errors.push(`Codex goal objective mismatch: expected "${expected}", got "${actual}".`);
349
- }
350
- const allowed = options.allowedStatuses ?? (options.requireComplete ? ["complete"] : ["active", "complete"]);
351
- const actualStatus = effectiveSnapshot.status ?? "unknown";
352
- if (!allowed.includes(actualStatus)) {
353
- errors.push(`Codex goal status mismatch: expected ${allowed.join(" or ")}, got ${actualStatus}.`);
354
- }
355
- if (options.requireComplete && actualStatus !== "complete") {
356
- errors.push('Codex goal is not complete; call update_goal({status: "complete"}) only after the objective is actually complete, then pass the fresh get_goal JSON.');
357
- }
358
- return { ok: errors.length === 0, snapshot: effectiveSnapshot, warnings, errors };
359
- }
360
- function formatCodexGoalReconciliation(reconciliation) {
361
- const parts = [...reconciliation.errors, ...reconciliation.warnings];
362
- return parts.join(" ");
387
+ // components/ulw-loop/src/plan-missing-recovery.ts
388
+ var ULW_LOOP_CREATE_GOALS_COMMAND = 'omo-agent-toolkit ulw-loop create-goals --brief "<brief>" --json';
389
+ function planMissingRecovery(existingSessionIds) {
390
+ const lines = [`Recovery: bootstrap the plan with \`${ULW_LOOP_CREATE_GOALS_COMMAND}\`.`];
391
+ if (existingSessionIds.length === 0)
392
+ return { message: lines.join(`
393
+ `) };
394
+ lines.push(`Existing ulw-loop session ids under .omo/ulw-loop/: ${existingSessionIds.join(", ")}. Re-run with \`--session-id <id>\` to target one of them.`);
395
+ return { message: lines.join(`
396
+ `), details: { existingSessionIds } };
397
+ }
398
+ function sessionIdRequiredMessage(flag) {
399
+ return [
400
+ `${flag} requires a non-empty value.`,
401
+ "Recovery: subprocess, eval, and hook contexts do not inherit the ulw-loop session env (OMO_ULW_LOOP_SESSION_ID / CODEX_SESSION_ID / CODEX_THREAD_ID / PI_SESSION_ID),",
402
+ `so pass the scope explicitly: \`${flag} <id>\` (for example \`${flag} 01a05b8a-6763-7780-834f-319423b071ce\`).`
403
+ ].join(`
404
+ `);
363
405
  }
364
406
 
365
- // packages/omo-codex/plugin/components/ulw-loop/src/plan-io.ts
366
- import { createReadStream } from "node:fs";
367
- import { appendFile, mkdir, readFile as readFile3, rename, writeFile } from "node:fs/promises";
368
- import { createInterface } from "node:readline";
407
+ // components/ulw-loop/src/plan-io.ts
369
408
  var LEGACY_OBJECTIVE_PREFIX = `Complete all ulw-loop stories in ${ULW_LOOP_DIR}/${ULW_LOOP_GOALS}: `;
370
409
  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.`;
371
410
  var locks = new Map;
@@ -406,7 +445,9 @@ async function readUlwLoopPlan(repoRoot, scope) {
406
445
  } catch (error) {
407
446
  if (!hasCode(error, "ENOENT"))
408
447
  throw error;
409
- throw new UlwLoopError(`No ulw-loop plan found at ${repoRelative(path, repoRoot)}. Run \`omo-agent-toolkit ulw-loop create-goals ...\` first.`, "ULW_LOOP_PLAN_MISSING", { cause: error });
448
+ const recovery = planMissingRecovery(readSessionDirs(repoRoot));
449
+ throw new UlwLoopError(`No ulw-loop plan found at ${repoRelative(path, repoRoot)}.
450
+ ${recovery.message}`, "ULW_LOOP_PLAN_MISSING", { cause: error, ...recovery.details === undefined ? {} : { details: recovery.details } });
410
451
  }
411
452
  const parsed = JSON.parse(raw);
412
453
  if (parsed.version !== 1 || !Array.isArray(parsed.goals)) {
@@ -429,6 +470,13 @@ async function readUlwLoopPlan(repoRoot, scope) {
429
470
  }
430
471
  return parsed;
431
472
  }
473
+ function readSessionDirs(repoRoot) {
474
+ try {
475
+ return readdirSync(ulwLoopDir(repoRoot), { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => entry.name).sort();
476
+ } catch {
477
+ return [];
478
+ }
479
+ }
432
480
  async function writePlan(repoRoot, plan, scope) {
433
481
  await mkdir(ulwLoopDir(repoRoot, scope), { recursive: true });
434
482
  const path = ulwLoopGoalsPath(repoRoot, scope);
@@ -480,7 +528,7 @@ async function findAcceptedSteeringLedgerEntry(repoRoot, key, scope) {
480
528
  return;
481
529
  }
482
530
 
483
- // packages/omo-codex/plugin/components/ulw-loop/src/evidence.ts
531
+ // components/ulw-loop/src/evidence.ts
484
532
  function ulwLoopFail(message, code, details) {
485
533
  throw new UlwLoopError(message, code, { details });
486
534
  }
@@ -586,11 +634,11 @@ function requireEssentialCriteriaPass(goal) {
586
634
  });
587
635
  }
588
636
 
589
- // packages/omo-codex/plugin/components/ulw-loop/src/quality-gate.ts
637
+ // components/ulw-loop/src/quality-gate-aggregate.ts
590
638
  import { resolve as resolve2 } from "node:path";
591
639
 
592
- // packages/omo-codex/plugin/components/ulw-loop/src/quality-gate-fields.ts
593
- var PLACEHOLDER_PATTERN = /^(?:placeholder|todo|tbd|n\/a|stub)$/i;
640
+ // components/ulw-loop/src/quality-gate-fields.ts
641
+ var PLACEHOLDER_PATTERN = /^(?:<replace:[^>]+>|placeholder|todo|tbd|n\/a|stub)$/i;
594
642
  function invalid(message, field) {
595
643
  throw new UlwLoopError(message, "ULW_LOOP_QUALITY_GATE_INVALID", { details: { field } });
596
644
  }
@@ -627,7 +675,135 @@ function literal(value, expected, field) {
627
675
  invalid(`${field} must be ${String(expected)}.`, field);
628
676
  }
629
677
 
630
- // packages/omo-codex/plugin/components/ulw-loop/src/quality-gate-verdicts.ts
678
+ // components/ulw-loop/src/quality-gate-aggregate.ts
679
+ var PLACEHOLDER = /^(?:<replace:[^>]+>|placeholder|todo|tbd|n\/a|stub)$/i;
680
+ function add(defects, field, message) {
681
+ defects.push({ field, message });
682
+ }
683
+ function text(value, field, defects) {
684
+ if (typeof value !== "string" || value.trim() === "")
685
+ add(defects, field, `Final quality gate requires non-empty ${field}.`);
686
+ else if (PLACEHOLDER.test(value.trim()))
687
+ add(defects, field, `Final quality gate rejects placeholder ${field}.`);
688
+ }
689
+ function section2(value, field, defects) {
690
+ if (!isRecord(value)) {
691
+ add(defects, field, `Final quality gate is missing ${field} evidence.`);
692
+ return {};
693
+ }
694
+ return value;
695
+ }
696
+ function aggregateQualityGateDefects(input, opts) {
697
+ if (!isRecord(input))
698
+ return;
699
+ const defects = [];
700
+ const gate = input;
701
+ const manual = section2(gate["manualQa"], "manualQa", defects);
702
+ const review = section2(gate["gateReview"], "gateReview", defects);
703
+ text(manual["evidence"], "manualQa.evidence", defects);
704
+ text(review["evidence"], "gateReview.evidence", defects);
705
+ if (review["recommendation"] !== "APPROVE")
706
+ add(defects, "gateReview.recommendation", "gateReview.recommendation must be APPROVE.");
707
+ const artifacts = Array.isArray(manual["artifactRefs"]) ? manual["artifactRefs"] : [];
708
+ for (const [index, item] of artifacts.entries()) {
709
+ if (!isRecord(item))
710
+ continue;
711
+ const path = item["path"];
712
+ if (typeof path !== "string" || path.trim() === "")
713
+ continue;
714
+ if (opts?.repoRoot !== undefined && opts.fs !== undefined && !opts.fs.existsSync(resolve2(opts.repoRoot, path)))
715
+ add(defects, `manualQa.artifactRefs[${index}].path`, `manualQa.artifactRefs[${index}].path must point to an existing artifact.`);
716
+ }
717
+ if (defects.length === 0)
718
+ return;
719
+ const truncated = defects.length > 25;
720
+ const fields = defects.slice(0, 25).map(({ field, message: message2 }) => ({ field, message: message2 }));
721
+ const message = [
722
+ `Final quality gate has ${fields.length}${truncated ? "+" : ""} validation defects:`,
723
+ ...fields.map((item) => `- ${item.field}: ${item.message}`)
724
+ ].join(`
725
+ `);
726
+ throw new UlwLoopError(message, "ULW_LOOP_QUALITY_GATE_INVALID", {
727
+ details: { field: fields[0]?.field, fields, ...truncated ? { truncated: true } : {} }
728
+ });
729
+ }
730
+
731
+ // components/ulw-loop/src/quality-gate-artifacts.ts
732
+ import { resolve as resolve3 } from "node:path";
733
+ function surfaceField(value, field) {
734
+ if (value === "cli" || value === "http" || value === "tmux" || value === "browser" || value === "gui" || value === "data")
735
+ return value;
736
+ invalid(`${field} must be a supported manual QA surface.`, field);
737
+ }
738
+ function kindField(value, field) {
739
+ if (value === "cli-transcript" || value === "log" || value === "screenshot" || value === "image" || value === "http-dump" || value === "data-diff")
740
+ return value;
741
+ invalid(`${field} must be a supported artifact kind.`, field);
742
+ }
743
+ function artifactCompatible(surface, kind) {
744
+ switch (surface) {
745
+ case "cli":
746
+ case "tmux":
747
+ return kind === "cli-transcript" || kind === "log";
748
+ case "http":
749
+ return kind === "http-dump";
750
+ case "browser":
751
+ case "gui":
752
+ return kind === "screenshot" || kind === "image";
753
+ case "data":
754
+ return kind === "data-diff";
755
+ default:
756
+ invalid("manualQa.surfaceEvidence has an unsupported surface.", "manualQa.surfaceEvidence.surface");
757
+ }
758
+ }
759
+ function checkFile(path, field, opts) {
760
+ if (opts?.repoRoot === undefined || opts.fs === undefined)
761
+ return;
762
+ const absolute = resolve3(opts.repoRoot, path);
763
+ if (!opts.fs.existsSync(absolute))
764
+ invalid(`${field} must point to an existing artifact.`, field);
765
+ if (opts.fs.statSync(absolute).size <= 0)
766
+ invalid(`${field} must point to a non-empty artifact.`, field);
767
+ if (opts.currentAttemptDir !== undefined && opts.repoRoot !== undefined) {
768
+ const attemptRoot = resolve3(opts.repoRoot, opts.currentAttemptDir);
769
+ if (!isWithinAttemptDir(absolute, attemptRoot))
770
+ invalid(`${field} (${path}) must point to an artifact from the current attempt (${opts.currentAttemptDir}).`, field);
771
+ }
772
+ }
773
+ function artifactMap(refs) {
774
+ const byId = new Map;
775
+ for (const ref of refs) {
776
+ if (byId.has(ref.id))
777
+ invalid(`manualQa.artifactRefs contains duplicate ${ref.id}.`, "manualQa.artifactRefs");
778
+ byId.set(ref.id, ref);
779
+ }
780
+ return byId;
781
+ }
782
+ function parseArtifactRefs(value, opts) {
783
+ if (!Array.isArray(value) || value.length === 0)
784
+ invalid("manualQa.artifactRefs must not be empty.", "manualQa.artifactRefs");
785
+ return value.map((item, index) => {
786
+ const ref = section(item, `manualQa.artifactRefs[${index}]`);
787
+ const path = textField(ref["path"], `manualQa.artifactRefs[${index}].path`);
788
+ checkFile(path, `manualQa.artifactRefs[${index}].path`, opts);
789
+ return {
790
+ id: textField(ref["id"], `manualQa.artifactRefs[${index}].id`),
791
+ kind: kindField(ref["kind"], `manualQa.artifactRefs[${index}].kind`),
792
+ description: textField(ref["description"], `manualQa.artifactRefs[${index}].description`),
793
+ path
794
+ };
795
+ });
796
+ }
797
+ function referencedArtifacts(value, field, byId) {
798
+ return stringArray(value, field).map((id) => {
799
+ const artifact = byId.get(id);
800
+ if (artifact === undefined)
801
+ invalid(`${field} references unknown artifact ${id}.`, field);
802
+ return artifact;
803
+ });
804
+ }
805
+
806
+ // components/ulw-loop/src/quality-gate-verdicts.ts
631
807
  function passedVerdict(value, field) {
632
808
  if (value === "not_applicable")
633
809
  invalid(`${field} must not be not_applicable.`, field);
@@ -648,7 +824,7 @@ function adversarialVerdict(row, field) {
648
824
  invalid(`${field} must be passed or not_applicable with a reason.`, field);
649
825
  }
650
826
 
651
- // packages/omo-codex/plugin/components/ulw-loop/src/surface.ts
827
+ // components/ulw-loop/src/surface.ts
652
828
  import { existsSync as existsSync3, readFileSync } from "node:fs";
653
829
  import { dirname, join as join2 } from "node:path";
654
830
  import { fileURLToPath } from "node:url";
@@ -665,6 +841,21 @@ var REVIEWER_ROLES_BY_SURFACE = {
665
841
  }
666
842
  };
667
843
  var GATE_REVIEWER_AGENT_NAMES = new Set(Object.values(REVIEWER_ROLES_BY_SURFACE).map((roles) => roles.gateReview));
844
+ var REQUIRED_GATE_SECTIONS_BY_SURFACE = {
845
+ lazycodex: ["codeReview", "manualQa", "gateReview", "iteration", "criteriaCoverage"],
846
+ "omo-senpi": ["manualQa", "gateReview", "iteration", "criteriaCoverage"]
847
+ };
848
+ var GATE_SECTION_BY_ACCEPTOR = {
849
+ lazycodex: {
850
+ codeReview: [REVIEWER_ROLES_BY_SURFACE.lazycodex.codeReview],
851
+ manualQa: [REVIEWER_ROLES_BY_SURFACE.lazycodex.manualQa],
852
+ gateReview: [REVIEWER_ROLES_BY_SURFACE.lazycodex.gateReview]
853
+ },
854
+ "omo-senpi": {
855
+ manualQa: ["main-session"],
856
+ gateReview: ["category:deep", "category:unspecified-high", "category:unspecified-low"]
857
+ }
858
+ };
668
859
  function reviewerRolesFor(surface) {
669
860
  return REVIEWER_ROLES_BY_SURFACE[surface];
670
861
  }
@@ -695,7 +886,7 @@ function readMarkerSurface(markerPath) {
695
886
  }
696
887
  }
697
888
 
698
- // packages/omo-codex/plugin/components/ulw-loop/src/quality-gate-blockers.ts
889
+ // components/ulw-loop/src/quality-gate-blockers.ts
699
890
  var BLOCKER_FIELD_KEYS = "blocker blockerSignature blockerEvidence blockerOccurrences blockedAt".split(" ");
700
891
  var URL_PATTERN = /https?:\/\/\S+/g;
701
892
  var PUNCTUATION_PATTERN = /[`"'()[\]{}:,;]/g;
@@ -737,93 +928,38 @@ function clearGoalBlockerFields(goal) {
737
928
  Reflect.deleteProperty(goal, key);
738
929
  }
739
930
 
740
- // packages/omo-codex/plugin/components/ulw-loop/src/quality-gate.ts
931
+ // components/ulw-loop/src/quality-gate.ts
741
932
  function reviewerRoleField(value, expected, field) {
742
933
  const actual = textField(value, field);
743
934
  if (actual !== expected)
744
935
  invalid(`${field} must be ${expected}.`, field);
745
936
  return expected;
746
937
  }
747
- function surfaceField(value, field) {
748
- if (value === "cli" || value === "http" || value === "tmux" || value === "browser" || value === "gui" || value === "data")
749
- return value;
750
- invalid(`${field} must be a supported manual QA surface.`, field);
751
- }
752
- function kindField(value, field) {
753
- if (value === "cli-transcript" || value === "log" || value === "screenshot" || value === "image" || value === "http-dump" || value === "data-diff")
754
- return value;
755
- invalid(`${field} must be a supported artifact kind.`, field);
756
- }
757
- function artifactCompatible(surface, kind) {
758
- switch (surface) {
759
- case "cli":
760
- case "tmux":
761
- return kind === "cli-transcript" || kind === "log";
762
- case "http":
763
- return kind === "http-dump";
764
- case "browser":
765
- case "gui":
766
- return kind === "screenshot" || kind === "image";
767
- case "data":
768
- return kind === "data-diff";
769
- default:
770
- invalid("manualQa.surfaceEvidence has an unsupported surface.", "manualQa.surfaceEvidence.surface");
771
- }
772
- }
773
- function checkFile(path, field, opts) {
774
- if (opts?.repoRoot === undefined || opts.fs === undefined)
775
- return;
776
- const absolute = resolve2(opts.repoRoot, path);
777
- if (!opts.fs.existsSync(absolute))
778
- invalid(`${field} must point to an existing artifact.`, field);
779
- if (opts.fs.statSync(absolute).size <= 0)
780
- invalid(`${field} must point to a non-empty artifact.`, field);
781
- if (opts.currentAttemptDir !== undefined && opts.repoRoot !== undefined) {
782
- const attemptRoot = resolve2(opts.repoRoot, opts.currentAttemptDir);
783
- if (!isWithinAttemptDir(absolute, attemptRoot))
784
- invalid(`${field} (${path}) must point to an artifact from the current attempt (${opts.currentAttemptDir}).`, field);
785
- }
786
- }
787
- function artifactMap(refs) {
788
- const byId = new Map;
789
- for (const ref of refs) {
790
- if (byId.has(ref.id))
791
- invalid(`manualQa.artifactRefs contains duplicate ${ref.id}.`, "manualQa.artifactRefs");
792
- byId.set(ref.id, ref);
793
- }
794
- return byId;
795
- }
796
- function parseArtifactRefs(value, opts) {
797
- if (!Array.isArray(value) || value.length === 0)
798
- invalid("manualQa.artifactRefs must not be empty.", "manualQa.artifactRefs");
799
- return value.map((item, index) => {
800
- const ref = section(item, `manualQa.artifactRefs[${index}]`);
801
- const path = textField(ref["path"], `manualQa.artifactRefs[${index}].path`);
802
- checkFile(path, `manualQa.artifactRefs[${index}].path`, opts);
803
- return {
804
- id: textField(ref["id"], `manualQa.artifactRefs[${index}].id`),
805
- kind: kindField(ref["kind"], `manualQa.artifactRefs[${index}].kind`),
806
- description: textField(ref["description"], `manualQa.artifactRefs[${index}].description`),
807
- path
808
- };
809
- });
810
- }
811
- function referencedArtifacts(value, field, byId) {
812
- return stringArray(value, field).map((id) => {
813
- const artifact = byId.get(id);
814
- if (artifact === undefined)
815
- invalid(`${field} references unknown artifact ${id}.`, field);
816
- return artifact;
817
- });
938
+ function reviewerAcceptorField(value, surface, sectionName) {
939
+ const field = `${sectionName}.by`;
940
+ const actual = textField(value, field);
941
+ const accepted = GATE_SECTION_BY_ACCEPTOR[surface][sectionName];
942
+ if (accepted === undefined || !accepted.includes(actual))
943
+ invalid(`${field} must be one of ${accepted?.join(", ") ?? "the configured reviewers"}.`, field);
944
+ return actual;
818
945
  }
819
946
  function validateQualityGate(input, opts) {
820
- const reviewerRoles = reviewerRolesFor(opts?.reviewerSurface ?? "lazycodex");
947
+ const surface = opts?.reviewerSurface ?? "lazycodex";
948
+ aggregateQualityGateDefects(input, opts);
821
949
  const gate = section(input, "qualityGate");
822
- const codeReview = section(gate["codeReview"], "codeReview");
950
+ const requiredSections = REQUIRED_GATE_SECTIONS_BY_SURFACE[surface];
951
+ for (const sectionName of requiredSections)
952
+ section(gate[sectionName], sectionName);
953
+ if (surface === "omo-senpi" && gate["codeReview"] !== undefined)
954
+ invalid("omo-senpi gate has no codeReview lane.", "codeReview");
823
955
  const manualQa = section(gate["manualQa"], "manualQa");
824
956
  const gateReview = section(gate["gateReview"], "gateReview");
825
957
  const iteration = section(gate["iteration"], "iteration");
826
958
  const coverage = section(gate["criteriaCoverage"], "criteriaCoverage");
959
+ const manualQaBy = reviewerAcceptorField(manualQa["by"], surface, "manualQa");
960
+ const gateReviewBy = reviewerAcceptorField(gateReview["by"], surface, "gateReview");
961
+ if (surface === "lazycodex")
962
+ reviewerRoleField(section(gate["codeReview"], "codeReview")["by"], "lazycodex-code-reviewer", "codeReview.by");
827
963
  const totalCriteria = numberField(coverage["totalCriteria"], "criteriaCoverage.totalCriteria");
828
964
  const passCount = numberField(coverage["passCount"], "criteriaCoverage.passCount");
829
965
  if (passCount < totalCriteria)
@@ -832,21 +968,11 @@ function validateQualityGate(input, opts) {
832
968
  const byId = artifactMap(artifactRefs);
833
969
  const surfaceEvidence = parseSurfaceEvidence(manualQa["surfaceEvidence"], byId);
834
970
  const adversarialCases = parseAdversarialCases(manualQa["adversarialCases"], byId);
835
- const codeReportPath = textField(codeReview["reportPath"], "codeReview.reportPath");
836
971
  const gateReportPath = textField(gateReview["reportPath"], "gateReview.reportPath");
837
- checkFile(codeReportPath, "codeReview.reportPath", opts);
838
972
  checkFile(gateReportPath, "gateReview.reportPath", opts);
839
- return {
840
- codeReview: {
841
- by: reviewerRoleField(codeReview["by"], reviewerRoles.codeReview, "codeReview.by"),
842
- recommendation: literal(codeReview["recommendation"], "APPROVE", "codeReview.recommendation"),
843
- codeQualityStatus: codeQualityStatusField(codeReview["codeQualityStatus"], "codeReview.codeQualityStatus"),
844
- reportPath: codeReportPath,
845
- evidence: textField(codeReview["evidence"], "codeReview.evidence"),
846
- blockers: emptyBlockers(codeReview["blockers"], "codeReview.blockers")
847
- },
973
+ const common = {
848
974
  manualQa: {
849
- by: reviewerRoleField(manualQa["by"], reviewerRoles.manualQa, "manualQa.by"),
975
+ by: manualQaBy,
850
976
  status: literal(manualQa["status"], "passed", "manualQa.status"),
851
977
  evidence: textField(manualQa["evidence"], "manualQa.evidence"),
852
978
  surfaceEvidence,
@@ -854,7 +980,7 @@ function validateQualityGate(input, opts) {
854
980
  artifactRefs
855
981
  },
856
982
  gateReview: {
857
- by: reviewerRoleField(gateReview["by"], reviewerRoles.gateReview, "gateReview.by"),
983
+ by: gateReviewBy,
858
984
  recommendation: literal(gateReview["recommendation"], "APPROVE", "gateReview.recommendation"),
859
985
  reportPath: gateReportPath,
860
986
  evidence: textField(gateReview["evidence"], "gateReview.evidence"),
@@ -875,6 +1001,23 @@ function validateQualityGate(input, opts) {
875
1001
  adversarialClassesCovered: stringArray(coverage["adversarialClassesCovered"], "criteriaCoverage.adversarialClassesCovered")
876
1002
  }
877
1003
  };
1004
+ if (surface === "omo-senpi")
1005
+ return { surface, ...common };
1006
+ const codeReview = section(gate["codeReview"], "codeReview");
1007
+ const codeReportPath = textField(codeReview["reportPath"], "codeReview.reportPath");
1008
+ checkFile(codeReportPath, "codeReview.reportPath", opts);
1009
+ return {
1010
+ surface,
1011
+ ...common,
1012
+ codeReview: {
1013
+ by: "lazycodex-code-reviewer",
1014
+ recommendation: literal(codeReview["recommendation"], "APPROVE", "codeReview.recommendation"),
1015
+ codeQualityStatus: codeQualityStatusField(codeReview["codeQualityStatus"], "codeReview.codeQualityStatus"),
1016
+ reportPath: codeReportPath,
1017
+ evidence: textField(codeReview["evidence"], "codeReview.evidence"),
1018
+ blockers: emptyBlockers(codeReview["blockers"], "codeReview.blockers")
1019
+ }
1020
+ };
878
1021
  }
879
1022
  function parseSurfaceEvidence(value, byId) {
880
1023
  if (!Array.isArray(value) || value.length === 0)
@@ -917,7 +1060,7 @@ function parseAdversarialCases(value, byId) {
917
1060
  });
918
1061
  }
919
1062
 
920
- // packages/omo-codex/plugin/components/ulw-loop/src/cli-arg-parser.ts
1063
+ // components/ulw-loop/src/cli-arg-parser.ts
921
1064
  import { readFile as readFile4 } from "node:fs/promises";
922
1065
  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(" "));
923
1066
  var SUBCOMMANDS = new Set("create-goals status complete-goals criteria record-evidence checkpoint steer add-goal record-review-blockers".split(" "));
@@ -1008,14 +1151,14 @@ function parseRecordEvidenceArgs(argv) {
1008
1151
  return notes ? { ...result, notes } : result;
1009
1152
  }
1010
1153
 
1011
- // packages/omo-codex/plugin/components/ulw-loop/src/validation-batch.ts
1154
+ // components/ulw-loop/src/validation-batch.ts
1012
1155
  function isObject(value) {
1013
1156
  return typeof value === "object" && value !== null && !Array.isArray(value);
1014
1157
  }
1015
1158
  function read(value, key) {
1016
1159
  return Object.entries(value).find(([name]) => name === key)?.[1];
1017
1160
  }
1018
- function text(value, key) {
1161
+ function text2(value, key) {
1019
1162
  const candidate = read(value, key);
1020
1163
  return typeof candidate === "string" && candidate.trim().length > 0 ? candidate.trim() : undefined;
1021
1164
  }
@@ -1036,9 +1179,9 @@ async function parseValidationBatches(input, goals) {
1036
1179
  function batchFromObject(value) {
1037
1180
  if (!isObject(value))
1038
1181
  fail("validation batch entries must be objects.");
1039
- const batchId = text(value, "batchId");
1182
+ const batchId = text2(value, "batchId");
1040
1183
  const memberIds = strings(value, "memberIds");
1041
- const finalGoalId = text(value, "finalGoalId");
1184
+ const finalGoalId = text2(value, "finalGoalId");
1042
1185
  if (batchId === undefined)
1043
1186
  fail("validation batch requires batchId.");
1044
1187
  if (memberIds === undefined || memberIds.length < 2)
@@ -1126,7 +1269,7 @@ function fail(message, code = "ULW_LOOP_VALIDATION_BATCH_INVALID") {
1126
1269
  throw new UlwLoopError(message, code);
1127
1270
  }
1128
1271
 
1129
- // packages/omo-codex/plugin/components/ulw-loop/src/checkpoint.ts
1272
+ // components/ulw-loop/src/checkpoint.ts
1130
1273
  var QUALITY_GATE_FS = { existsSync: existsSync4, statSync };
1131
1274
  function ulwLoopFail2(message, code) {
1132
1275
  throw new UlwLoopError(message, code);
@@ -1152,7 +1295,7 @@ async function readJsonInput2(raw, repoRoot) {
1152
1295
  if (!(error instanceof SyntaxError))
1153
1296
  throw error;
1154
1297
  }
1155
- const path = resolve3(repoRoot, trimmed);
1298
+ const path = resolve4(repoRoot, trimmed);
1156
1299
  if (!existsSync4(path))
1157
1300
  return ulwLoopFail2("Quality gate JSON is neither valid JSON nor a readable path.", "ulw_loop_json_input_invalid");
1158
1301
  try {
@@ -1196,13 +1339,13 @@ function ledgerKind2(status, goal, aggregateCompletion) {
1196
1339
  return status === "blocked" ? "goal_blocked" : "goal_failed";
1197
1340
  }
1198
1341
  function buildLedger(now, args, goal, qualityGate, codexGoal, aggregateCompletion) {
1199
- const watch = qualityGate?.codeReview.codeQualityStatus === "WATCH";
1342
+ const watch = qualityGate?.surface === "lazycodex" && qualityGate.codeReview.codeQualityStatus === "WATCH";
1200
1343
  const entry = {
1201
1344
  at: now,
1202
1345
  kind: ledgerKind2(args.status, goal, aggregateCompletion),
1203
1346
  goalId: goal.id,
1204
1347
  status: goal.status,
1205
- evidence: watch ? `${args.evidence} | codeQuality=WATCH: ${qualityGate.codeReview.evidence}` : args.evidence
1348
+ evidence: watch && qualityGate.surface === "lazycodex" ? `${args.evidence} | codeQuality=WATCH: ${qualityGate.codeReview.evidence}` : args.evidence
1206
1349
  };
1207
1350
  if (codexGoal !== undefined)
1208
1351
  entry.codexGoal = codexGoal;
@@ -1253,7 +1396,7 @@ async function checkpointUlwLoop(repoRoot, args, scope) {
1253
1396
  const activeFinalTaskScoped = mismatchedTaskObjective && snapshot.status === "active" && await canReconcileActiveFinalTaskScopedAggregateSnapshot(repoRoot, plan, goal, objective, evidence, scope);
1254
1397
  const taskScoped = completedTaskScoped || activeFinalTaskScoped;
1255
1398
  if (!taskScoped)
1256
- throw new UlwLoopError(`${formatCodexGoalReconciliation(reconciliation)}${aggregate && snapshot?.status === "complete" && objective !== undefined ? buildTaskScopedAggregateReconciliationHint(goal, final) : ""}`, "ulw_loop_codex_snapshot_mismatch");
1399
+ throw codexSnapshotMismatchError({ reconciliation, snapshot, expectedObjective: expectedCodexObjective(plan, goal), taskScopedHint: { goal, aggregate, final } });
1257
1400
  }
1258
1401
  if (closesBatch)
1259
1402
  requireBatchFinalReady(plan, goal);
@@ -1294,7 +1437,116 @@ async function checkpointUlwLoop(repoRoot, args, scope) {
1294
1437
  });
1295
1438
  }
1296
1439
 
1297
- // packages/omo-codex/plugin/components/ulw-loop/src/cli-output.ts
1440
+ // components/ulw-loop/src/checkpoint-template.ts
1441
+ function artifactPath(base, name) {
1442
+ return `${base}/${name}`;
1443
+ }
1444
+ function gateTemplate(surface, base) {
1445
+ const artifacts = [
1446
+ {
1447
+ id: "artifact-cli",
1448
+ kind: "cli-transcript",
1449
+ description: "<replace:artifact description>",
1450
+ path: artifactPath(base, "cli-transcript.txt")
1451
+ },
1452
+ {
1453
+ id: "artifact-data",
1454
+ kind: "data-diff",
1455
+ description: "<replace:artifact description>",
1456
+ path: artifactPath(base, "data-diff.txt")
1457
+ }
1458
+ ];
1459
+ const manualQa = {
1460
+ by: surface === "omo-senpi" ? "main-session" : "lazycodex-qa-executor",
1461
+ status: "passed",
1462
+ evidence: "<replace:manual QA evidence>",
1463
+ surfaceEvidence: [
1464
+ {
1465
+ id: "surface-cli",
1466
+ criterionRef: "<replace:criterion id>",
1467
+ surface: "cli",
1468
+ invocation: "<replace:command>",
1469
+ verdict: "passed",
1470
+ artifactRefs: ["artifact-cli"]
1471
+ },
1472
+ {
1473
+ id: "surface-data",
1474
+ criterionRef: "<replace:criterion id>",
1475
+ surface: "data",
1476
+ invocation: "<replace:command>",
1477
+ verdict: "passed",
1478
+ artifactRefs: ["artifact-data"]
1479
+ }
1480
+ ],
1481
+ adversarialCases: [
1482
+ {
1483
+ id: "<replace:adversarial case id>",
1484
+ criterionRef: "<replace:criterion id>",
1485
+ scenario: "<replace:scenario>",
1486
+ expectedBehavior: "<replace:expected behavior>",
1487
+ verdict: "not_applicable",
1488
+ reason: "<replace:reason>",
1489
+ artifactRefs: ["artifact-cli"]
1490
+ }
1491
+ ],
1492
+ artifactRefs: artifacts
1493
+ };
1494
+ const common = {
1495
+ manualQa,
1496
+ gateReview: {
1497
+ by: surface === "omo-senpi" ? "category:deep" : "lazycodex-gate-reviewer",
1498
+ recommendation: "APPROVE",
1499
+ reportPath: artifactPath(base, "gate-review.md"),
1500
+ evidence: "<replace:gate review evidence>",
1501
+ blockers: []
1502
+ },
1503
+ iteration: {
1504
+ fullRerun: true,
1505
+ status: "passed",
1506
+ rerunCommands: ["<replace:verification command>"],
1507
+ evidence: "<replace:iteration evidence>"
1508
+ },
1509
+ criteriaCoverage: {
1510
+ totalCriteria: 0,
1511
+ passCount: 0,
1512
+ originalIntent: "<replace:original intent>",
1513
+ desiredOutcome: "<replace:desired outcome>",
1514
+ userOutcomeReview: "<replace:user outcome review>",
1515
+ adversarialClassesCovered: ["<replace:adversarial class>"]
1516
+ }
1517
+ };
1518
+ if (surface === "omo-senpi")
1519
+ return common;
1520
+ return {
1521
+ codeReview: {
1522
+ by: "lazycodex-code-reviewer",
1523
+ recommendation: "APPROVE",
1524
+ codeQualityStatus: "CLEAR",
1525
+ reportPath: artifactPath(base, "code-review.md"),
1526
+ evidence: "<replace:code review evidence>",
1527
+ blockers: []
1528
+ },
1529
+ ...common
1530
+ };
1531
+ }
1532
+ async function checkpointTemplate(repoRoot, scope, goalId) {
1533
+ const plan = await readUlwLoopPlan(repoRoot, scope);
1534
+ const targetId = goalId ?? plan.activeGoalId;
1535
+ const active = plan.goals.find((goal) => goal.id === targetId);
1536
+ if (goalId !== undefined && active === undefined)
1537
+ throw new UlwLoopError(`Unknown ulw-loop id: ${goalId}.`, "ULW_LOOP_GOAL_NOT_FOUND", { details: { goalId } });
1538
+ const hasAttempt = plan.evidenceLayoutVersion === 2 && active !== undefined;
1539
+ const attemptDir = hasAttempt ? ulwLoopAttemptEvidenceDir(active.id, active.attempt, scope) : ".omo/evidence";
1540
+ return {
1541
+ qualityGateTemplate: gateTemplate(resolveToolkitSurface(), attemptDir),
1542
+ codexGoalTemplate: {
1543
+ goal: { objective: plan.codexObjective ?? "<replace:codex objective>", status: "complete" }
1544
+ },
1545
+ ...hasAttempt ? { attemptDir } : { guidance: "This plan is evidence-layout v1; artifacts go under .omo/evidence/." }
1546
+ };
1547
+ }
1548
+
1549
+ // components/ulw-loop/src/cli-output.ts
1298
1550
  var ULW_LOOP_HELP = `Usage:
1299
1551
  omo-agent-toolkit hook user-prompt-submit [--with-ultrawork] (Codex UserPromptSubmit hook)
1300
1552
  omo-agent-toolkit help | --help | -h (this message)
@@ -1304,6 +1556,7 @@ var ULW_LOOP_HELP = `Usage:
1304
1556
  omo-agent-toolkit ulw-loop complete-goals [--retry-failed] [--json]
1305
1557
  omo-agent-toolkit ulw-loop criteria --goal-id <id> [--json]
1306
1558
  omo-agent-toolkit ulw-loop record-evidence --goal-id <id> --criterion-id <id> --status pass|fail|blocked --evidence "..." [--notes "..."] [--json]
1559
+ omo-agent-toolkit ulw-loop checkpoint --print-template [--goal-id <id>] [--json]
1307
1560
  omo-agent-toolkit ulw-loop checkpoint --goal-id <id> --status complete|failed|blocked --evidence "..." --codex-goal-json <...> [--quality-gate-json <...>] [--no-advance] [--json]
1308
1561
  omo-agent-toolkit ulw-loop steer --kind <kind> ... --evidence "..." --rationale "..." [--proposals-json <json-or-path>] [--json]
1309
1562
  omo-agent-toolkit ulw-loop add-goal --title "..." --objective "..." [--json]
@@ -1384,7 +1637,7 @@ function normalizeCodexGoalMode(value) {
1384
1637
  throw new UlwLoopError("Invalid --codex-goal-mode; expected aggregate or per_story.", "ULW_LOOP_CODEX_GOAL_MODE_INVALID", { details: { value } });
1385
1638
  }
1386
1639
 
1387
- // packages/omo-codex/plugin/components/ulw-loop/src/codex-goal-instruction.ts
1640
+ // components/ulw-loop/src/codex-goal-instruction.ts
1388
1641
  function buildCodexGoalInstruction(args) {
1389
1642
  const mode = codexGoalMode(args.plan);
1390
1643
  const createGoal = buildCreateGoalPayload(args.plan, args.goal);
@@ -1469,6 +1722,8 @@ function finalSection(plan, goal, isFinal, aggregate, surface) {
1469
1722
  if (!isFinal)
1470
1723
  return "- This is not the final ulw-loop story; do not run the final reviewer/manual-QA/gate-review quality gate yet.";
1471
1724
  const option = sessionOption(plan);
1725
+ if (surface === "omo-senpi")
1726
+ return senpiFinalSection(plan, goal, aggregate);
1472
1727
  const blockerCommand = `omo-agent-toolkit ulw-loop record-review-blockers${option} --goal-id ${goal.id} --title "Resolve final code-review blockers" --objective "<blocker-resolution objective>" --evidence "<review findings>" --codex-goal-json "<active get_goal JSON or path>"`;
1473
1728
  const checkpointCommand = `omo-agent-toolkit ulw-loop checkpoint${option} --goal-id ${goal.id} --status complete --evidence "<targeted verification/manualQa/gateReview evidence>" --codex-goal-json "<fresh complete get_goal JSON or path>" --quality-gate-json "<quality gate JSON or path>"`;
1474
1729
  return joinLines([
@@ -1486,6 +1741,20 @@ function finalSection(plan, goal, isFinal, aggregate, surface) {
1486
1741
  ` ${checkpointCommand}`
1487
1742
  ]);
1488
1743
  }
1744
+ function senpiFinalSection(plan, goal, aggregate) {
1745
+ const option = sessionOption(plan);
1746
+ const checkpointCommand = `omo-agent-toolkit ulw-loop checkpoint${option} --goal-id ${goal.id} --status complete --evidence "<manualQa/gateReview evidence>" --codex-goal-json "<fresh complete get_goal JSON or path>" --quality-gate-json "$(omo-agent-toolkit ulw-loop checkpoint${option} --print-template)"`;
1747
+ return joinLines([
1748
+ "Final story — run the single-reviewer quality gate before update_goal:",
1749
+ '- Run manual QA yourself and write the non-empty artifact under currentAttemptDir; set manualQa.by to "main-session".',
1750
+ '- Spawn exactly one gate reviewer with task(category: "deep").',
1751
+ "- If that task fails with any model_unavailable failure, retry with category:unspecified-high, then category:unspecified-low.",
1752
+ "- Set gateReview.by to the exact category:<name> literal used for the successful task.",
1753
+ "- Build the gate JSON with omo-agent-toolkit ulw-loop checkpoint --print-template, then fill manualQa, gateReview, iteration, and criteriaCoverage.",
1754
+ '- Require passed manualQa, approved gateReview, passed iteration, and complete criteriaCoverage before update_goal({status: "complete"}).',
1755
+ aggregate ? `- If the gate is clean, call update_goal({status: "complete"}), call get_goal again, then checkpoint the aggregate story: ${checkpointCommand}` : `- If the gate is clean, call update_goal({status: "complete"}), call get_goal again, then checkpoint: ${checkpointCommand}`
1756
+ ]);
1757
+ }
1489
1758
  function sessionOption(plan) {
1490
1759
  const prefix = ".omo/ulw-loop/";
1491
1760
  const suffix = "/goals.json";
@@ -1499,11 +1768,11 @@ function joinLines(lines) {
1499
1768
  `);
1500
1769
  }
1501
1770
 
1502
- // packages/omo-codex/plugin/components/ulw-loop/src/plan-crud.ts
1771
+ // components/ulw-loop/src/plan-crud.ts
1503
1772
  import { existsSync as existsSync5 } from "node:fs";
1504
1773
  import { mkdir as mkdir2, writeFile as writeFile2 } from "node:fs/promises";
1505
1774
 
1506
- // packages/omo-codex/plugin/components/ulw-loop/src/plan-goal-factory.ts
1775
+ // components/ulw-loop/src/plan-goal-factory.ts
1507
1776
  function cleanLine(line) {
1508
1777
  return line.replace(/^\s*(?:[-*+]\s+|\d+[.)]\s+)/, "").trim();
1509
1778
  }
@@ -1592,7 +1861,7 @@ function appendGoalToPlan(plan, title, objective, now) {
1592
1861
  return goal;
1593
1862
  }
1594
1863
 
1595
- // packages/omo-codex/plugin/components/ulw-loop/src/plan-crud.ts
1864
+ // components/ulw-loop/src/plan-crud.ts
1596
1865
  function isScheduleEligible(goal) {
1597
1866
  return goal.steeringStatus !== "superseded" && goal.steeringStatus !== "blocked";
1598
1867
  }
@@ -1719,7 +1988,7 @@ function summarizeUlwLoopPlan(plan) {
1719
1988
  };
1720
1989
  }
1721
1990
 
1722
- // packages/omo-codex/plugin/components/ulw-loop/src/checkpoint-continuation.ts
1991
+ // components/ulw-loop/src/checkpoint-continuation.ts
1723
1992
  async function checkpointAndContinue(repoRoot, args, scope) {
1724
1993
  const result = await checkpointUlwLoop(repoRoot, args, scope);
1725
1994
  if (args.status !== "complete" || result.aggregateCompletion !== undefined || !args.advance)
@@ -1731,6 +2000,14 @@ async function checkpointAndContinue(repoRoot, args, scope) {
1731
2000
  return { ...result, plan: next.plan, next: { resumed: next.resumed, goal: next.goal, instruction } };
1732
2001
  }
1733
2002
  async function checkpoint(repoRoot, argv, json, scope) {
2003
+ if (hasFlag(argv, "--print-template")) {
2004
+ const template = await checkpointTemplate(repoRoot, scope, readValue(argv, "--goal-id"));
2005
+ if (json)
2006
+ printJson({ ok: true, ...template });
2007
+ else
2008
+ printJson(template);
2009
+ return 0;
2010
+ }
1734
2011
  const goalId = required2(argv, "--goal-id");
1735
2012
  const statusValue = checkpointStatus(required2(argv, "--status"));
1736
2013
  const evidence = required2(argv, "--evidence");
@@ -1782,10 +2059,10 @@ function checkpointStatus(value) {
1782
2059
  throw new UlwLoopError("Missing or invalid --status; expected complete, failed, or blocked.", "ULW_LOOP_STATUS_INVALID", { details: { status: value } });
1783
2060
  }
1784
2061
 
1785
- // packages/omo-codex/plugin/components/ulw-loop/src/cli-subcommands.ts
2062
+ // components/ulw-loop/src/cli-subcommands.ts
1786
2063
  import { readFile as readFile6 } from "node:fs/promises";
1787
2064
 
1788
- // packages/omo-codex/plugin/components/ulw-loop/src/cli-steering.ts
2065
+ // components/ulw-loop/src/cli-steering.ts
1789
2066
  var SOURCES = ["user_prompt_submit", "finding", "cli"];
1790
2067
  var STEERING_KIND_HELP = [
1791
2068
  `Allowed --kind values: ${ULW_LOOP_STEERING_MUTATION_KINDS.join(", ")}`,
@@ -1817,7 +2094,7 @@ function kindMessage(prefix) {
1817
2094
 
1818
2095
  ${STEERING_KIND_HELP}`;
1819
2096
  }
1820
- function text2(value, field) {
2097
+ function text3(value, field) {
1821
2098
  if (value === undefined)
1822
2099
  return;
1823
2100
  const trimmed = value.trim();
@@ -1826,11 +2103,11 @@ function text2(value, field) {
1826
2103
  return fail2(`Empty ${field}.`, "ULW_LOOP_STEERING_FIELD_EMPTY", { field });
1827
2104
  }
1828
2105
  function required3(argv, flag) {
1829
- const value = text2(readValue(argv, flag), flag);
2106
+ const value = text3(readValue(argv, flag), flag);
1830
2107
  return value ?? fail2(`Missing ${flag}.`, "ULW_LOOP_STEERING_FIELD_REQUIRED", { flag });
1831
2108
  }
1832
2109
  function requiredGoal(argv) {
1833
- const value = text2(parseGoalArg(argv), "--goal-id");
2110
+ const value = text3(parseGoalArg(argv), "--goal-id");
1834
2111
  return value ?? fail2("Missing --goal-id.", "ULW_LOOP_GOAL_ID_REQUIRED", { flag: "--goal-id" });
1835
2112
  }
1836
2113
  function readObject(value, key) {
@@ -1881,14 +2158,14 @@ function parseSteeringSource(argv) {
1881
2158
  function child(value) {
1882
2159
  if (!isPlain(value))
1883
2160
  return null;
1884
- const title = text2(objectText(value, "title"), "title");
1885
- const objective = text2(objectText(value, "objective"), "objective");
2161
+ const title = text3(objectText(value, "title"), "title");
2162
+ const objective = text3(objectText(value, "objective"), "objective");
1886
2163
  if (title === undefined || objective === undefined)
1887
2164
  return null;
1888
2165
  return { title, objective };
1889
2166
  }
1890
2167
  async function children(argv, flag, needed) {
1891
- const input = needed ? required3(argv, flag) : text2(readValue(argv, flag), flag);
2168
+ const input = needed ? required3(argv, flag) : text3(readValue(argv, flag), flag);
1892
2169
  if (input === undefined)
1893
2170
  return [];
1894
2171
  const raw = await readJsonInput(input);
@@ -1911,12 +2188,12 @@ async function stringArray2(argv, flag) {
1911
2188
  for (const item of raw) {
1912
2189
  if (typeof item !== "string")
1913
2190
  return fail2(`${flag} entries must be strings.`, "ULW_LOOP_STEERING_STRING_ARRAY_REQUIRED", { flag });
1914
- values.push(text2(item, flag) ?? "");
2191
+ values.push(text3(item, flag) ?? "");
1915
2192
  }
1916
2193
  return values;
1917
2194
  }
1918
2195
  function model(value) {
1919
- const trimmed = text2(value, "--user-model");
2196
+ const trimmed = text3(value, "--user-model");
1920
2197
  if (trimmed === undefined)
1921
2198
  return;
1922
2199
  return isModel(trimmed) ? trimmed : fail2(`Invalid --user-model: ${trimmed}.`, "ULW_LOOP_STEERING_USER_MODEL_INVALID", { value: trimmed, expected: ULW_LOOP_SUCCESS_CRITERION_USER_MODELS });
@@ -1927,7 +2204,7 @@ function neverKind(kind) {
1927
2204
  async function parseSteeringProposal(argv) {
1928
2205
  const kind = parseSteeringKind(argv);
1929
2206
  const source = parseSteeringSource(argv);
1930
- const idempotencyKey = text2(readValue(argv, "--idempotency-key"), "--idempotency-key");
2207
+ const idempotencyKey = text3(readValue(argv, "--idempotency-key"), "--idempotency-key");
1931
2208
  const base = { kind, source, evidence: required3(argv, "--evidence"), rationale: required3(argv, "--rationale"), ...idempotencyKey === undefined ? {} : { idempotencyKey } };
1932
2209
  switch (kind) {
1933
2210
  case "add_subgoal":
@@ -1970,36 +2247,36 @@ async function parseSteeringProposal(argv) {
1970
2247
  function normalizedChildren(values) {
1971
2248
  if (values === undefined)
1972
2249
  return;
1973
- return values.map((item) => ({ title: text2(item.title, "child.title") ?? "", objective: text2(item.objective, "child.objective") ?? "" }));
2250
+ return values.map((item) => ({ title: text3(item.title, "child.title") ?? "", objective: text3(item.objective, "child.objective") ?? "" }));
1974
2251
  }
1975
2252
  function normalizedStrings(values, field) {
1976
2253
  if (values === undefined)
1977
2254
  return;
1978
- return values.map((value) => text2(value, field) ?? "");
2255
+ return values.map((value) => text3(value, field) ?? "");
1979
2256
  }
1980
2257
  function normalizeSteeringProposal(proposal) {
1981
- const evidence = text2(proposal.evidence, "evidence") ?? "";
1982
- const rationale = text2(proposal.rationale, "rationale") ?? "";
1983
- const goalId = text2(proposal.goalId, "goalId");
1984
- const targetGoalId = text2(proposal.targetGoalId, "targetGoalId");
2258
+ const evidence = text3(proposal.evidence, "evidence") ?? "";
2259
+ const rationale = text3(proposal.rationale, "rationale") ?? "";
2260
+ const goalId = text3(proposal.goalId, "goalId");
2261
+ const targetGoalId = text3(proposal.targetGoalId, "targetGoalId");
1985
2262
  const targetGoalIds = normalizedStrings(proposal.targetGoalIds, "targetGoalIds");
1986
- const criterionId = text2(proposal.criterionId, "criterionId");
1987
- const title = text2(proposal.title, "title");
1988
- const objective = text2(proposal.objective, "objective");
1989
- const revisedTitle = text2(proposal.revisedTitle, "revisedTitle");
1990
- const revisedObjective = text2(proposal.revisedObjective, "revisedObjective");
1991
- const blockedReason = text2(proposal.blockedReason, "blockedReason");
1992
- const directiveText = text2(proposal.directiveText, "directiveText");
1993
- const promptSignature = text2(proposal.promptSignature, "promptSignature");
1994
- const idempotencyKey = text2(proposal.idempotencyKey, "idempotencyKey");
1995
- const scenario = text2(proposal.scenario, "scenario");
1996
- const expectedEvidence = text2(proposal.expectedEvidence, "expectedEvidence");
2263
+ const criterionId = text3(proposal.criterionId, "criterionId");
2264
+ const title = text3(proposal.title, "title");
2265
+ const objective = text3(proposal.objective, "objective");
2266
+ const revisedTitle = text3(proposal.revisedTitle, "revisedTitle");
2267
+ const revisedObjective = text3(proposal.revisedObjective, "revisedObjective");
2268
+ const blockedReason = text3(proposal.blockedReason, "blockedReason");
2269
+ const directiveText = text3(proposal.directiveText, "directiveText");
2270
+ const promptSignature = text3(proposal.promptSignature, "promptSignature");
2271
+ const idempotencyKey = text3(proposal.idempotencyKey, "idempotencyKey");
2272
+ const scenario = text3(proposal.scenario, "scenario");
2273
+ const expectedEvidence = text3(proposal.expectedEvidence, "expectedEvidence");
1997
2274
  const childGoals = normalizedChildren(proposal.childGoals);
1998
2275
  const pendingOrder = normalizedStrings(proposal.pendingOrder, "pendingOrder");
1999
2276
  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 } };
2000
2277
  }
2001
2278
  async function parseSteeringProposals(argv) {
2002
- const input = text2(readValue(argv, "--proposals-json"), "--proposals-json");
2279
+ const input = text3(readValue(argv, "--proposals-json"), "--proposals-json");
2003
2280
  if (input === undefined)
2004
2281
  return [await parseSteeringProposal(argv)];
2005
2282
  if (readValue(argv, "--kind") !== undefined)
@@ -2092,7 +2369,7 @@ function printSteerBatchResult(result, json) {
2092
2369
  printStatus(result.plan);
2093
2370
  }
2094
2371
 
2095
- // packages/omo-codex/plugin/components/ulw-loop/src/review-blockers.ts
2372
+ // components/ulw-loop/src/review-blockers.ts
2096
2373
  var BLOCKER_FIELDS = "blockedReason blockerSignature blockerOccurrenceCount requiredExternalDecision nonRetriable failedAt failureReason completedAt blocker blockerEvidence blockerOccurrences blockedAt".split(" ");
2097
2374
  function ulwLoopError(message, code) {
2098
2375
  throw new UlwLoopError(message, code);
@@ -2131,9 +2408,10 @@ async function recordFinalReviewBlockers(repoRoot, args, scope) {
2131
2408
  ulwLoopError(`${goal.id} is not final.`, "ulw_loop_not_final_story");
2132
2409
  const snapshot = await readCodexGoalSnapshotInput(args.codexGoalJson, repoRoot);
2133
2410
  const aggregate = codexGoalMode(plan) === "aggregate";
2134
- const reconciliation = reconcileCodexGoalSnapshot(snapshot, { expectedObjective: expectedCodexObjective(plan, goal), ...aggregate ? { acceptedObjectives: compatibleCodexObjectives(plan) } : {}, allowedStatuses: ["active"], requireSnapshot: true, requireComplete: false });
2411
+ const expected = expectedCodexObjective(plan, goal);
2412
+ const reconciliation = reconcileCodexGoalSnapshot(snapshot, { expectedObjective: expected, ...aggregate ? { acceptedObjectives: compatibleCodexObjectives(plan) } : {}, allowedStatuses: ["active"], requireSnapshot: true, requireComplete: false });
2135
2413
  if (!reconciliation.ok)
2136
- ulwLoopError(reconciliation.errors.join(" "), "ulw_loop_codex_snapshot_mismatch");
2414
+ throw codexSnapshotMismatchError({ reconciliation, snapshot, expectedObjective: expected });
2137
2415
  const now = iso();
2138
2416
  for (const field of BLOCKER_FIELDS)
2139
2417
  Reflect.deleteProperty(goal, field);
@@ -2158,10 +2436,39 @@ async function recordFinalReviewBlockers(repoRoot, args, scope) {
2158
2436
  });
2159
2437
  }
2160
2438
 
2161
- // packages/omo-codex/plugin/components/ulw-loop/src/steering-mutations.ts
2439
+ // components/ulw-loop/src/status-next-actions.ts
2440
+ function statusNextActions(plan) {
2441
+ const actions = [];
2442
+ const active = plan.goals.find((goal) => goal.id === plan.activeGoalId);
2443
+ if (plan.goals.length === 0)
2444
+ actions.push(`No goals yet: bootstrap with \`${ULW_LOOP_CREATE_GOALS_COMMAND}\`.`);
2445
+ else if (active !== undefined)
2446
+ actions.push(...activeGoalActions(plan, active));
2447
+ for (const goal of plan.goals.filter((candidate) => candidate.status === "review_blocked"))
2448
+ actions.push(`${goal.id} is review_blocked: capture the reviewer verdict with \`omo-agent-toolkit ulw-loop record-review-blockers --goal-id ${goal.id} --title "<title>" --objective "<objective>" --evidence "<verdict>" --codex-goal-json '<get_goal json>'\`.`);
2449
+ if (plan.evidenceLayoutVersion !== 2 || active === undefined)
2450
+ actions.push("plan is evidence-layout v1; artifacts go under .omo/evidence/");
2451
+ return actions;
2452
+ }
2453
+ function activeGoalActions(plan, active) {
2454
+ const unresolved = active.successCriteria.filter((criterion) => criterion.status !== "pass");
2455
+ if (unresolved.length > 0)
2456
+ return [
2457
+ `${active.id} has ${unresolved.length} unresolved criterion(s) (${unresolved.map((criterion) => criterion.id).join(", ")}): record proof with \`omo-agent-toolkit ulw-loop record-evidence --goal-id ${active.id} --criterion-id <id> --status pass --evidence "<observable proof>"\`.`
2458
+ ];
2459
+ if (hasAllCriteriaPass(active) && isFinalRunCompletionCandidate(plan, active))
2460
+ return [
2461
+ `${active.id} passes every criterion and is the final story: update_goal complete, then checkpoint --print-template to build the final quality gate.`
2462
+ ];
2463
+ return [
2464
+ `${active.id} passes every criterion: close it with \`omo-agent-toolkit ulw-loop checkpoint --goal-id ${active.id} --status complete --evidence "<proof>" --codex-goal-json '<get_goal json>'\`.`
2465
+ ];
2466
+ }
2467
+
2468
+ // components/ulw-loop/src/steering-mutations.ts
2162
2469
  var read2 = (value, key) => Object.entries(value).find(([name]) => name === key)?.[1];
2163
2470
  var isText = (value) => typeof value === "string" && value.trim().length > 0;
2164
- var text3 = (value, key) => {
2471
+ var text4 = (value, key) => {
2165
2472
  const candidate = read2(value, key);
2166
2473
  return isText(candidate) ? candidate.trim() : undefined;
2167
2474
  };
@@ -2170,13 +2477,13 @@ var after = (proposal) => {
2170
2477
  const candidate = read2(proposal, "after");
2171
2478
  return typeof candidate === "object" && candidate !== null && !Array.isArray(candidate) ? candidate : undefined;
2172
2479
  };
2173
- var revised = (proposal, direct, nested) => text3(proposal, direct) ?? text3(after(proposal) ?? proposal, nested);
2174
- var targets = (proposal) => proposal.targetGoalIds ?? [proposal.targetGoalId ?? text3(proposal, "goalId") ?? ""].filter(Boolean);
2480
+ var revised = (proposal, direct, nested) => text4(proposal, direct) ?? text4(after(proposal) ?? proposal, nested);
2481
+ var targets = (proposal) => proposal.targetGoalIds ?? [proposal.targetGoalId ?? text4(proposal, "goalId") ?? ""].filter(Boolean);
2175
2482
  var child2 = (value) => {
2176
2483
  if (typeof value !== "object" || value === null || Array.isArray(value))
2177
2484
  return null;
2178
- const title = text3(value, "title");
2179
- const objective = text3(value, "objective");
2485
+ const title = text4(value, "title");
2486
+ const objective = text4(value, "objective");
2180
2487
  return title === undefined || objective === undefined ? null : { title, objective };
2181
2488
  };
2182
2489
  var children2 = (proposal) => {
@@ -2241,11 +2548,11 @@ function reviseCriterion(plan, proposal, now) {
2241
2548
  if (target === undefined || current === undefined)
2242
2549
  return;
2243
2550
  const model2 = read2(proposal, "userModel");
2244
- target.successCriteria[index] = { ...current, scenario: text3(proposal, "scenario") ?? current.scenario, expectedEvidence: text3(proposal, "expectedEvidence") ?? current.expectedEvidence, userModel: isModel2(model2) ? model2 : current.userModel };
2551
+ target.successCriteria[index] = { ...current, scenario: text4(proposal, "scenario") ?? current.scenario, expectedEvidence: text4(proposal, "expectedEvidence") ?? current.expectedEvidence, userModel: isModel2(model2) ? model2 : current.userModel };
2245
2552
  target.updatedAt = now;
2246
2553
  }
2247
2554
 
2248
- // packages/omo-codex/plugin/components/ulw-loop/src/steering-snapshot.ts
2555
+ // components/ulw-loop/src/steering-snapshot.ts
2249
2556
  function buildSteeringPlanSnapshot(plan, changedGoalIds) {
2250
2557
  const snapshot = {
2251
2558
  updatedAt: plan.updatedAt,
@@ -2269,14 +2576,14 @@ function changedGoalIdsBetween(before, after2) {
2269
2576
  return changed;
2270
2577
  }
2271
2578
 
2272
- // packages/omo-codex/plugin/components/ulw-loop/src/steering.ts
2579
+ // components/ulw-loop/src/steering.ts
2273
2580
  var SOURCES2 = ["user_prompt_submit", "finding", "cli"];
2274
2581
  var PROTECTED = new Set(["aggregateCompletion", "codexObjective", "codexObjectiveAliases", "originalConstraints", "qualityGate", "status", "completedAt", "completionStatus"]);
2275
2582
  var isObject2 = (value) => typeof value === "object" && value !== null;
2276
2583
  var isPlain2 = (value) => isObject2(value) && !Array.isArray(value);
2277
2584
  var read3 = (value, key) => Object.entries(value).find(([name]) => name === key)?.[1];
2278
2585
  var isText2 = (value) => typeof value === "string" && value.trim().length > 0;
2279
- var text4 = (value, key) => {
2586
+ var text5 = (value, key) => {
2280
2587
  const candidate = read3(value, key);
2281
2588
  return isText2(candidate) ? candidate.trim() : undefined;
2282
2589
  };
@@ -2289,19 +2596,19 @@ var texts = (value, key) => {
2289
2596
  };
2290
2597
  function targets2(proposal) {
2291
2598
  const many = texts(proposal, "targetGoalIds");
2292
- const one = text4(proposal, "targetGoalId") ?? text4(proposal, "goalId");
2599
+ const one = text5(proposal, "targetGoalId") ?? text5(proposal, "goalId");
2293
2600
  return many.length > 0 ? many : one === undefined ? [] : [one];
2294
2601
  }
2295
2602
  var after2 = (proposal) => {
2296
2603
  const candidate = read3(proposal, "after");
2297
2604
  return isPlain2(candidate) ? candidate : undefined;
2298
2605
  };
2299
- var revised2 = (proposal, direct, nested) => text4(proposal, direct) ?? text4(after2(proposal) ?? proposal, nested);
2606
+ var revised2 = (proposal, direct, nested) => text5(proposal, direct) ?? text5(after2(proposal) ?? proposal, nested);
2300
2607
  function child3(value) {
2301
2608
  if (!isPlain2(value))
2302
2609
  return null;
2303
- const title = text4(value, "title");
2304
- const objective = text4(value, "objective");
2610
+ const title = text5(value, "title");
2611
+ const objective = text5(value, "objective");
2305
2612
  if (title === undefined || objective === undefined)
2306
2613
  return null;
2307
2614
  return { title, objective };
@@ -2340,15 +2647,15 @@ function auditFor(proposal, reasons) {
2340
2647
  const object = isPlain2(proposal) ? proposal : undefined;
2341
2648
  const kindRaw = object === undefined ? undefined : read3(object, "kind");
2342
2649
  const sourceRaw = object === undefined ? undefined : read3(object, "source");
2343
- const evidence = object === undefined ? "" : text4(object, "evidence") ?? "";
2344
- const rationale = object === undefined ? "" : text4(object, "rationale") ?? "";
2650
+ const evidence = object === undefined ? "" : text5(object, "evidence") ?? "";
2651
+ const rationale = object === undefined ? "" : text5(object, "rationale") ?? "";
2345
2652
  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 } };
2346
2653
  if (object === undefined)
2347
2654
  return audit;
2348
- const criterionId = text4(object, "criterionId");
2349
- const directiveText = text4(object, "directiveText");
2350
- const promptSignature = text4(object, "promptSignature");
2351
- const idempotencyKey = text4(object, "idempotencyKey");
2655
+ const criterionId = text5(object, "criterionId");
2656
+ const directiveText = text5(object, "directiveText");
2657
+ const promptSignature = text5(object, "promptSignature");
2658
+ const idempotencyKey = text5(object, "idempotencyKey");
2352
2659
  if (criterionId !== undefined)
2353
2660
  audit.criterionId = criterionId;
2354
2661
  if (directiveText !== undefined)
@@ -2369,9 +2676,9 @@ function validateUlwLoopSteeringProposal(plan, proposal) {
2369
2676
  reasons.push(`invalid kind: ${String(kind)}`);
2370
2677
  if (!isSource2(read3(object, "source")))
2371
2678
  reasons.push(`invalid source: ${String(read3(object, "source"))}`);
2372
- if (text4(object, "evidence") === undefined)
2679
+ if (text5(object, "evidence") === undefined)
2373
2680
  reasons.push("missing evidence");
2374
- if (text4(object, "rationale") === undefined)
2681
+ if (text5(object, "rationale") === undefined)
2375
2682
  reasons.push("missing rationale");
2376
2683
  if (hasProtected(proposal))
2377
2684
  reasons.push("protected payload");
@@ -2388,7 +2695,7 @@ function goal2(plan, id) {
2388
2695
  }
2389
2696
  function validateKind(plan, proposal, kind, reasons) {
2390
2697
  const target = goal2(plan, targets2(proposal)[0]);
2391
- if (kind === "add_subgoal" && (text4(proposal, "title") === undefined || text4(proposal, "objective") === undefined))
2698
+ if (kind === "add_subgoal" && (text5(proposal, "title") === undefined || text5(proposal, "objective") === undefined))
2392
2699
  reasons.push("add_subgoal requires title/objective");
2393
2700
  if ((kind === "split_subgoal" || kind === "revise_pending_wording" || kind === "mark_blocked_superseded") && target === undefined)
2394
2701
  reasons.push(`${kind} requires target`);
@@ -2418,7 +2725,7 @@ function validateOrder(plan, proposal, reasons) {
2418
2725
  }
2419
2726
  function validateCriterion(plan, proposal, reasons) {
2420
2727
  const target = goal2(plan, targets2(proposal)[0]);
2421
- const criterionId = text4(proposal, "criterionId");
2728
+ const criterionId = text5(proposal, "criterionId");
2422
2729
  if (target === undefined)
2423
2730
  reasons.push("revise_criterion requires goalId");
2424
2731
  else if (criterionId === undefined || target.successCriteria.every((item) => item.id !== criterionId))
@@ -2453,8 +2760,8 @@ function applySteeringMutation(plan, proposal, audit) {
2453
2760
  function isProposal(value) {
2454
2761
  return isPlain2(value) && isKind2(read3(value, "kind")) && isSource2(read3(value, "source")) && isText2(read3(value, "evidence")) && isText2(read3(value, "rationale"));
2455
2762
  }
2456
- function parseUlwLoopSteeringDirective(text5) {
2457
- const match = /(?:^|\s)(?:OMO_ULW_LOOP_STEER|omo\.ulw-loop\.steer|omo ulw-loop steer|omo-agent-toolkit ulw-loop steer):\s*([\s\S]+)$/u.exec(text5);
2763
+ function parseUlwLoopSteeringDirective(text6) {
2764
+ const match = /(?:^|\s)(?:OMO_ULW_LOOP_STEER|omo\.ulw-loop\.steer|omo ulw-loop steer|omo-agent-toolkit ulw-loop steer):\s*([\s\S]+)$/u.exec(text6);
2458
2765
  if (match?.[1] === undefined)
2459
2766
  return null;
2460
2767
  try {
@@ -2506,7 +2813,7 @@ function ledgerEntry(proposal, audit, at) {
2506
2813
  return entry;
2507
2814
  }
2508
2815
 
2509
- // packages/omo-codex/plugin/components/ulw-loop/src/steering-batch.ts
2816
+ // components/ulw-loop/src/steering-batch.ts
2510
2817
  async function steerUlwLoopBatch(repoRoot, proposals, scope) {
2511
2818
  return withUlwLoopMutationLock(repoRoot, scope, async () => {
2512
2819
  const plan = await readUlwLoopPlan(repoRoot, scope);
@@ -2587,7 +2894,7 @@ function ledgerEntry2(proposal, audit, at) {
2587
2894
  return entry;
2588
2895
  }
2589
2896
 
2590
- // packages/omo-codex/plugin/components/ulw-loop/src/cli-subcommands.ts
2897
+ // components/ulw-loop/src/cli-subcommands.ts
2591
2898
  async function createGoals(repoRoot, argv, json, scope) {
2592
2899
  const briefFile = readValue(argv, "--brief-file");
2593
2900
  const brief = readValue(argv, "--brief") ?? (briefFile === undefined ? undefined : await readFile6(briefFile, "utf8")) ?? (hasFlag(argv, "--from-stdin") ? await readStdin() : undefined) ?? positionalText(argv);
@@ -2621,7 +2928,8 @@ async function status(repoRoot, json, scope) {
2621
2928
  ok: true,
2622
2929
  plan,
2623
2930
  summary: summarizeUlwLoopPlan(plan),
2624
- ...currentAttemptDir === undefined ? {} : { currentAttemptDir }
2931
+ ...currentAttemptDir === undefined ? {} : { currentAttemptDir },
2932
+ nextActions: statusNextActions(plan)
2625
2933
  });
2626
2934
  } else
2627
2935
  printStatus(plan);
@@ -2743,7 +3051,7 @@ function findGoal3(plan, goalId) {
2743
3051
  throw new UlwLoopError(`Unknown ulw-loop id: ${goalId}.`, "ULW_LOOP_GOAL_NOT_FOUND", { details: { goalId } });
2744
3052
  }
2745
3053
 
2746
- // packages/omo-codex/plugin/components/ulw-loop/src/cli-commands.ts
3054
+ // components/ulw-loop/src/cli-commands.ts
2747
3055
  var ULW_LOOP_SUBCOMMANDS = [
2748
3056
  "help",
2749
3057
  "create-goals",
@@ -2837,7 +3145,7 @@ function commandScope(argv) {
2837
3145
  if (sessionIdFlagPresent(argv)) {
2838
3146
  const sessionId2 = readValue(argv, SESSION_ID_FLAG)?.trim();
2839
3147
  if (!sessionId2) {
2840
- throw new UlwLoopError(`${SESSION_ID_FLAG} requires a non-empty value.`, "ULW_LOOP_SESSION_ID_REQUIRED", {
3148
+ throw new UlwLoopError(sessionIdRequiredMessage(SESSION_ID_FLAG), "ULW_LOOP_SESSION_ID_REQUIRED", {
2841
3149
  details: { flag: SESSION_ID_FLAG }
2842
3150
  });
2843
3151
  }
@@ -2847,10 +3155,10 @@ function commandScope(argv) {
2847
3155
  return sessionId === null ? undefined : { sessionId };
2848
3156
  }
2849
3157
 
2850
- // packages/omo-codex/plugin/components/ulw-loop/src/ultrawork-directive.ts
3158
+ // components/ulw-loop/src/ultrawork-directive.ts
2851
3159
  import { readFileSync as readFileSync3 } from "node:fs";
2852
3160
 
2853
- // packages/omo-codex/plugin/components/ulw-loop/src/ultrawork-skill-pointer.ts
3161
+ // components/ulw-loop/src/ultrawork-skill-pointer.ts
2854
3162
  import { existsSync as existsSync6, readFileSync as readFileSync2 } from "node:fs";
2855
3163
  import { fileURLToPath as fileURLToPath2 } from "node:url";
2856
3164
  var ULTRAWORK_SKILL_POINTER_TEMPLATE = `<ultrawork-mode>
@@ -2898,7 +3206,7 @@ function buildUltraworkAdditionalContext(options = {}) {
2898
3206
  return ULTRAWORK_DIRECTIVE;
2899
3207
  }
2900
3208
 
2901
- // packages/omo-codex/plugin/components/ulw-loop/src/ultrawork-directive.ts
3209
+ // components/ulw-loop/src/ultrawork-directive.ts
2902
3210
  var ULTRAWORK_CURRENT_PROMPT_PATTERN = /(?:ultrawork|ulw)/i;
2903
3211
  var ULTRAWORK_DIRECTIVE_MARKER = "<ultrawork-mode>";
2904
3212
  var TRANSCRIPT_SEARCH_BYTES = 512000;
@@ -3001,7 +3309,7 @@ function isRecord3(value) {
3001
3309
  return typeof value === "object" && value !== null && !Array.isArray(value);
3002
3310
  }
3003
3311
 
3004
- // packages/omo-codex/plugin/components/ulw-loop/src/codex-hook.ts
3312
+ // components/ulw-loop/src/codex-hook.ts
3005
3313
  var CREATE_GOAL_TOOL_NAME = "create_goal";
3006
3314
  var CREATE_GOAL_PAYLOAD_WARNING = "Use create_goal with objective only. Omit token_budget so the goal stays unlimited, and put lifecycle status changes on update_goal.";
3007
3315
  function parseUserPromptSubmitPayload(raw) {
@@ -3127,23 +3435,41 @@ function optionalString(value) {
3127
3435
  return value === undefined || typeof value === "string";
3128
3436
  }
3129
3437
  function readAll(stdin) {
3130
- return new Promise((resolve4, reject) => {
3438
+ return new Promise((resolve5, reject) => {
3131
3439
  let data = "";
3132
3440
  stdin.setEncoding("utf8");
3133
3441
  stdin.on("data", (chunk) => {
3134
3442
  data += chunk instanceof Buffer ? chunk.toString() : String(chunk);
3135
3443
  });
3136
3444
  stdin.once("error", reject);
3137
- stdin.once("end", () => resolve4(data));
3445
+ stdin.once("end", () => resolve5(data));
3138
3446
  });
3139
3447
  }
3140
3448
 
3141
- // packages/omo-codex/plugin/components/ulw-loop/src/spawn-guard.ts
3142
- import { existsSync as existsSync7, readdirSync, readFileSync as readFileSync4, statSync as statSync2, writeFileSync } from "node:fs";
3143
- import { join as join3 } from "node:path";
3449
+ // components/ulw-loop/src/spawn-guard.ts
3450
+ import { randomBytes } from "node:crypto";
3451
+ import {
3452
+ existsSync as existsSync7,
3453
+ mkdirSync,
3454
+ openSync,
3455
+ readdirSync as readdirSync2,
3456
+ readFileSync as readFileSync4,
3457
+ renameSync,
3458
+ statSync as statSync2,
3459
+ unlinkSync,
3460
+ writeFileSync
3461
+ } from "node:fs";
3462
+ import { dirname as dirname2, join as join3 } from "node:path";
3144
3463
  var SPAWN_TOOL_TOKENS = new Set(["spawn_agent", "collaborationspawn_agent", "collaboration.spawn_agent"]);
3145
3464
  var DEFAULT_FANOUT_LIMIT = 60;
3465
+ var DEFAULT_REVIEW_SPAWN_LIMIT = 3;
3146
3466
  var GATE_MESSAGE_PATTERN = /lazycodex-gate-reviewer|omo-senpi-gate-reviewer|final gate review/i;
3467
+ var REVIEW_AGENT_TYPES = [
3468
+ ...Object.values(REVIEWER_ROLES_BY_SURFACE).map((roles) => roles.gateReview),
3469
+ ...Object.values(REVIEWER_ROLES_BY_SURFACE).map((roles) => roles.codeReview),
3470
+ ...Object.values(REVIEWER_ROLES_BY_SURFACE).map((roles) => roles.manualQa)
3471
+ ];
3472
+ var REVIEW_AGENT_TYPE_SET = new Set(REVIEW_AGENT_TYPES);
3147
3473
  function applySpawnGuards(payload) {
3148
3474
  if (payload.hook_event_name !== "PreToolUse" || !SPAWN_TOOL_TOKENS.has(payload.tool_name))
3149
3475
  return "";
@@ -3151,12 +3477,18 @@ function applySpawnGuards(payload) {
3151
3477
  const plan = readPlan(join3(stateDir, "goals.json"));
3152
3478
  if (plan === null)
3153
3479
  return "";
3154
- const fanOutDenial = consumeFanOutBudget(stateDir);
3155
- if (fanOutDenial !== null)
3156
- return deny(fanOutDenial);
3480
+ const fanOutPeek = peekFanOutBudget(stateDir);
3481
+ if (fanOutPeek !== null)
3482
+ return deny(fanOutPeek);
3157
3483
  const missingArtifact = missingGateArtifact(payload, plan);
3158
3484
  if (missingArtifact !== null)
3159
3485
  return deny(`spawn code-review + QA first; gate audits their artifacts: missing ${missingArtifact}`);
3486
+ const reviewDenial = consumeReviewSpawnBudget(payload, plan, stateDir);
3487
+ if (reviewDenial !== null)
3488
+ return deny(reviewDenial);
3489
+ const fanOutDenial = consumeFanOutBudget(stateDir);
3490
+ if (fanOutDenial !== null)
3491
+ return deny(fanOutDenial);
3160
3492
  return "";
3161
3493
  }
3162
3494
  async function runSpawnGuardCli(stdin, stdout) {
@@ -3175,15 +3507,44 @@ async function runSpawnGuardCli(stdin, stdout) {
3175
3507
  return;
3176
3508
  }
3177
3509
  }
3510
+ function peekFanOutBudget(stateDir) {
3511
+ const counterPath = join3(stateDir, "spawn-count.json");
3512
+ const count = readCount(counterPath) + 1;
3513
+ const limit = fanOutLimit();
3514
+ if (count <= limit)
3515
+ return null;
3516
+ return `ulw-loop spawn fan-out cap reached (${count}/${limit}). Consolidate work into the agents already running, or raise OMO_SPAWN_FANOUT_LIMIT if this volume is intentional.`;
3517
+ }
3178
3518
  function consumeFanOutBudget(stateDir) {
3179
3519
  const counterPath = join3(stateDir, "spawn-count.json");
3180
3520
  const count = readCount(counterPath) + 1;
3181
- writeFileSync(counterPath, JSON.stringify({ count }));
3521
+ atomicWriteJson(counterPath, { count });
3182
3522
  const limit = fanOutLimit();
3183
3523
  if (count <= limit)
3184
3524
  return null;
3185
3525
  return `ulw-loop spawn fan-out cap reached (${count}/${limit}). Consolidate work into the agents already running, or raise OMO_SPAWN_FANOUT_LIMIT if this volume is intentional.`;
3186
3526
  }
3527
+ function consumeReviewSpawnBudget(payload, plan, stateDir) {
3528
+ const agentType = reviewAgentType(payload.tool_input);
3529
+ if (agentType === null)
3530
+ return null;
3531
+ const goal3 = plan.goals.find((candidate) => candidate.id === plan.activeGoalId) ?? plan.goals.find((candidate) => isFinalRunCompletionCandidate(plan, candidate));
3532
+ if (goal3 === undefined)
3533
+ return null;
3534
+ const counterPath = join3(stateDir, "review-spawn-counts.json");
3535
+ const lockPath = `${counterPath}.lock`;
3536
+ const limit = reviewSpawnLimit();
3537
+ return withExclusiveLock(lockPath, () => {
3538
+ const counts = readCounts(counterPath);
3539
+ const key = `${agentType}:${goal3.id}:a${goal3.attempt}`;
3540
+ const count = (counts[key] ?? 0) + 1;
3541
+ if (count > limit)
3542
+ return `ulw-loop reviewer no-progress cap reached (${agentType} ${count}/${limit}) for ${goal3.id} attempt ${goal3.attempt}. Consolidate existing review findings, or checkpoint and start a new attempt after concrete progress.`;
3543
+ counts[key] = count;
3544
+ atomicWriteJson(counterPath, counts);
3545
+ return null;
3546
+ });
3547
+ }
3187
3548
  function missingGateArtifact(payload, plan) {
3188
3549
  if (!isGateReviewerSpawn(payload.tool_input))
3189
3550
  return null;
@@ -3193,9 +3554,11 @@ function missingGateArtifact(payload, plan) {
3193
3554
  if (!goal3.successCriteria.every((criterion) => criterion.status === "pass"))
3194
3555
  return null;
3195
3556
  const scope = { sessionId: payload.session_id };
3557
+ const surface = resolveToolkitSurface();
3558
+ const requiredArtifacts = surface === "omo-senpi" ? [`${goal3.id}-manual-qa.md`] : [`${goal3.id}-code-review.md`, `${goal3.id}-manual-qa.md`];
3196
3559
  if (plan.evidenceLayoutVersion === 2) {
3197
3560
  const attemptDir = ulwLoopAttemptEvidenceDir(goal3.id, goal3.attempt, scope);
3198
- for (const name of [`${goal3.id}-code-review.md`, `${goal3.id}-manual-qa.md`]) {
3561
+ for (const name of requiredArtifacts) {
3199
3562
  const relative2 = `${attemptDir}/${name}`;
3200
3563
  if (!isNonEmptyFile(join3(payload.cwd, relative2)))
3201
3564
  return relative2;
@@ -3203,21 +3566,97 @@ function missingGateArtifact(payload, plan) {
3203
3566
  return null;
3204
3567
  }
3205
3568
  const flatReport = `.omo/evidence/${goal3.id}-code-review.md`;
3206
- if (!isNonEmptyFile(join3(payload.cwd, flatReport)))
3569
+ if (surface !== "omo-senpi" && !isNonEmptyFile(join3(payload.cwd, flatReport)))
3207
3570
  return flatReport;
3571
+ if (surface === "omo-senpi") {
3572
+ const manualQa = `.omo/evidence/${goal3.id}-manual-qa.md`;
3573
+ return isNonEmptyFile(join3(payload.cwd, manualQa)) ? null : manualQa;
3574
+ }
3208
3575
  if (!hasOtherEvidenceFile(join3(payload.cwd, ".omo", "evidence"), `${goal3.id}-code-review.md`))
3209
- return `.omo/evidence/<any manual-QA artifact besides ${goal3.id}-code-review.md>`;
3576
+ return `.omo/evidence/${goal3.id}-manual-qa.md`;
3210
3577
  return null;
3211
3578
  }
3212
3579
  function isGateReviewerSpawn(toolInput) {
3580
+ const agentType = reviewAgentType(toolInput);
3581
+ return agentType !== null && GATE_REVIEWER_AGENT_NAMES.has(agentType);
3582
+ }
3583
+ function reviewAgentType(toolInput) {
3213
3584
  if (typeof toolInput !== "object" || toolInput === null)
3214
- return false;
3585
+ return null;
3215
3586
  const record = toolInput;
3216
3587
  const agentType = record["agent_type"];
3217
- if (typeof agentType === "string")
3218
- return GATE_REVIEWER_AGENT_NAMES.has(agentType);
3588
+ if (typeof agentType === "string") {
3589
+ if (!REVIEW_AGENT_TYPE_SET.has(agentType))
3590
+ return null;
3591
+ return activeSurfaceReviewerAlias(agentType);
3592
+ }
3219
3593
  const message = record["message"];
3220
- return typeof message === "string" && GATE_MESSAGE_PATTERN.test(message);
3594
+ if (typeof message !== "string")
3595
+ return null;
3596
+ const normalizedMessage = message.toLowerCase();
3597
+ const allRoleNames = [...REVIEW_AGENT_TYPES];
3598
+ const explicitAssignment = allRoleNames.map((name) => ({
3599
+ name,
3600
+ index: normalizedMessage.search(new RegExp(`\\bact as (?:an? )?${name}\\b`))
3601
+ })).filter(({ index }) => index >= 0).sort((left, right) => left.index - right.index)[0];
3602
+ if (explicitAssignment !== undefined)
3603
+ return activeSurfaceReviewerAlias(explicitAssignment.name);
3604
+ const nonReviewerActAs = /\bact as (?:an? )?\S+/.test(normalizedMessage);
3605
+ if (nonReviewerActAs)
3606
+ return null;
3607
+ const namedReviewer = REVIEW_AGENT_TYPES.find((name) => normalizedMessage.includes(name));
3608
+ if (namedReviewer !== undefined)
3609
+ return activeSurfaceReviewerAlias(namedReviewer);
3610
+ return GATE_MESSAGE_PATTERN.test(message) ? reviewerRolesFor(resolveToolkitSurface()).gateReview : null;
3611
+ }
3612
+ function activeSurfaceReviewerAlias(reviewer) {
3613
+ const activeRoles = reviewerRolesFor(resolveToolkitSurface());
3614
+ for (const roles of Object.values(REVIEWER_ROLES_BY_SURFACE)) {
3615
+ if (reviewer === roles.codeReview)
3616
+ return activeRoles.codeReview;
3617
+ if (reviewer === roles.manualQa)
3618
+ return activeRoles.manualQa;
3619
+ if (reviewer === roles.gateReview)
3620
+ return activeRoles.gateReview;
3621
+ }
3622
+ return reviewer;
3623
+ }
3624
+ function withExclusiveLock(lockPath, fn) {
3625
+ mkdirSync(dirname2(lockPath), { recursive: true });
3626
+ const maxAttempts = 10;
3627
+ const baseDelayMs = 10;
3628
+ for (let attempt = 0;attempt < maxAttempts; attempt++) {
3629
+ let fd = null;
3630
+ try {
3631
+ fd = openSync(lockPath, "wx");
3632
+ writeFileSync(fd, process.pid.toString());
3633
+ try {
3634
+ return fn();
3635
+ } finally {
3636
+ try {
3637
+ unlinkSync(lockPath);
3638
+ } catch {}
3639
+ }
3640
+ } catch (error) {
3641
+ if (fd !== null) {
3642
+ try {
3643
+ unlinkSync(lockPath);
3644
+ } catch {}
3645
+ }
3646
+ if (error.code !== "EEXIST") {
3647
+ return fn();
3648
+ }
3649
+ const delayMs = baseDelayMs * 2 ** attempt + Math.random() * baseDelayMs;
3650
+ const deadline = Date.now() + delayMs;
3651
+ while (Date.now() < deadline) {}
3652
+ }
3653
+ }
3654
+ return fn();
3655
+ }
3656
+ function atomicWriteJson(targetPath, data) {
3657
+ const tmp = join3(dirname2(targetPath), `.tmp-${randomBytes(6).toString("hex")}`);
3658
+ writeFileSync(tmp, JSON.stringify(data));
3659
+ renameSync(tmp, targetPath);
3221
3660
  }
3222
3661
  function deny(reason) {
3223
3662
  return `${JSON.stringify({
@@ -3237,6 +3676,13 @@ function fanOutLimit() {
3237
3676
  const parsed = Number.parseInt(raw, 10);
3238
3677
  return Number.isFinite(parsed) && parsed > 0 ? parsed : DEFAULT_FANOUT_LIMIT;
3239
3678
  }
3679
+ function reviewSpawnLimit() {
3680
+ const raw = process.env["OMO_ULW_LOOP_REVIEW_SPAWN_LIMIT"];
3681
+ if (raw === undefined)
3682
+ return DEFAULT_REVIEW_SPAWN_LIMIT;
3683
+ const parsed = Number.parseInt(raw, 10);
3684
+ return Number.isFinite(parsed) && parsed > 0 ? parsed : DEFAULT_REVIEW_SPAWN_LIMIT;
3685
+ }
3240
3686
  function isNonEmptyFile(path) {
3241
3687
  try {
3242
3688
  return existsSync7(path) && statSync2(path).size > 0;
@@ -3248,7 +3694,7 @@ function isNonEmptyFile(path) {
3248
3694
  }
3249
3695
  function hasOtherEvidenceFile(evidenceDir, excludedName) {
3250
3696
  try {
3251
- return readdirSync(evidenceDir).some((name) => name !== excludedName && isNonEmptyFile(join3(evidenceDir, name)));
3697
+ return readdirSync2(evidenceDir).some((name) => name !== excludedName && isNonEmptyFile(join3(evidenceDir, name)));
3252
3698
  } catch (error) {
3253
3699
  if (error instanceof Error)
3254
3700
  return false;
@@ -3265,6 +3711,23 @@ function readCount(counterPath) {
3265
3711
  throw error;
3266
3712
  }
3267
3713
  }
3714
+ function readCounts(counterPath) {
3715
+ try {
3716
+ const parsed = JSON.parse(readFileSync4(counterPath, "utf8"));
3717
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed))
3718
+ return {};
3719
+ const counts = {};
3720
+ for (const [key, value] of Object.entries(parsed)) {
3721
+ if (typeof value === "number" && value >= 0)
3722
+ counts[key] = value;
3723
+ }
3724
+ return counts;
3725
+ } catch (error) {
3726
+ if (error instanceof Error)
3727
+ return {};
3728
+ throw error;
3729
+ }
3730
+ }
3268
3731
  function readPlan(goalsPath) {
3269
3732
  try {
3270
3733
  return JSON.parse(readFileSync4(goalsPath, "utf8"));
@@ -3275,9 +3738,9 @@ function readPlan(goalsPath) {
3275
3738
  }
3276
3739
  }
3277
3740
 
3278
- // packages/omo-codex/plugin/components/ulw-loop/src/stop-resume-hook.ts
3741
+ // components/ulw-loop/src/stop-resume-hook.ts
3279
3742
  import { existsSync as existsSync8, readFileSync as readFileSync5, writeFileSync as writeFileSync2 } from "node:fs";
3280
- import { isAbsolute as isAbsolute2, join as join4, resolve as resolve4, sep as sep2 } from "node:path";
3743
+ import { isAbsolute as isAbsolute2, join as join4, resolve as resolve5, sep as sep2 } from "node:path";
3281
3744
  var RESUME_CAP = 2;
3282
3745
  var CONTEXT_PRESSURE_MARKERS2 = [
3283
3746
  "context compacted",
@@ -3335,8 +3798,8 @@ function isResumableStatus(status2) {
3335
3798
  }
3336
3799
  function consumeResumeBudget(stateDir, goalId) {
3337
3800
  const ledgerLineCount = countLedgerLines(join4(stateDir, "ledger.jsonl"));
3338
- const counterPath = resolve4(stateDir, `auto-resume-${goalId}.json`);
3339
- const stuckPath = resolve4(stateDir, `auto-resume-${goalId}.stuck`);
3801
+ const counterPath = resolve5(stateDir, `auto-resume-${goalId}.json`);
3802
+ const stuckPath = resolve5(stateDir, `auto-resume-${goalId}.stuck`);
3340
3803
  if (!isInsideDir(stateDir, counterPath) || !isInsideDir(stateDir, stuckPath))
3341
3804
  return false;
3342
3805
  const previous = readCounter(counterPath);
@@ -3350,7 +3813,7 @@ function consumeResumeBudget(stateDir, goalId) {
3350
3813
  return true;
3351
3814
  }
3352
3815
  function isInsideDir(dir, candidate) {
3353
- return candidate.startsWith(resolve4(dir) + sep2);
3816
+ return candidate.startsWith(resolve5(dir) + sep2);
3354
3817
  }
3355
3818
  function renderResumeDirective(plan, goal3, sessionId) {
3356
3819
  const normalized = normalizeUlwLoopSessionId(sessionId);
@@ -3460,7 +3923,7 @@ function parseStopPayload(value) {
3460
3923
  };
3461
3924
  }
3462
3925
 
3463
- // packages/omo-codex/plugin/components/ulw-loop/src/cli.ts
3926
+ // components/ulw-loop/src/cli.ts
3464
3927
  async function main() {
3465
3928
  const argv = process.argv.slice(2);
3466
3929
  const command = argv[0];