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,11 +1,27 @@
1
- import { existsSync, readdirSync, readFileSync, statSync, writeFileSync } from "node:fs";
2
- import { join } from "node:path";
1
+ import { randomBytes } from "node:crypto";
2
+ import {
3
+ existsSync,
4
+ mkdirSync,
5
+ openSync,
6
+ readdirSync,
7
+ readFileSync,
8
+ renameSync,
9
+ statSync,
10
+ unlinkSync,
11
+ writeFileSync,
12
+ } from "node:fs";
13
+ import { dirname, join } from "node:path";
3
14
 
4
15
  import type { PreToolUsePayload } from "./codex-hook.js";
5
16
  import { parsePreToolUsePayload } from "./codex-hook.js";
6
17
  import { isFinalRunCompletionCandidate } from "./goal-status.js";
7
18
  import { ulwLoopAttemptEvidenceDir, ulwLoopDir } from "./paths.js";
8
- import { GATE_REVIEWER_AGENT_NAMES } from "./surface.js";
19
+ import {
20
+ GATE_REVIEWER_AGENT_NAMES,
21
+ REVIEWER_ROLES_BY_SURFACE,
22
+ resolveToolkitSurface,
23
+ reviewerRolesFor,
24
+ } from "./surface.js";
9
25
  import type { UlwLoopPlan } from "./types.js";
10
26
 
11
27
  // spawn_agent = v1; collaborationspawn_agent = the delimiter-free flattened v2
@@ -13,18 +29,29 @@ import type { UlwLoopPlan } from "./types.js";
13
29
  // dotted token observed live in the task-1 probe (hook-tool-tokens.txt).
14
30
  const SPAWN_TOOL_TOKENS = new Set(["spawn_agent", "collaborationspawn_agent", "collaboration.spawn_agent"]);
15
31
  const DEFAULT_FANOUT_LIMIT = 60;
32
+ const DEFAULT_REVIEW_SPAWN_LIMIT = 3;
16
33
  const GATE_MESSAGE_PATTERN = /lazycodex-gate-reviewer|omo-senpi-gate-reviewer|final gate review/i;
34
+ const REVIEW_AGENT_TYPES = [
35
+ ...Object.values(REVIEWER_ROLES_BY_SURFACE).map((roles) => roles.gateReview),
36
+ ...Object.values(REVIEWER_ROLES_BY_SURFACE).map((roles) => roles.codeReview),
37
+ ...Object.values(REVIEWER_ROLES_BY_SURFACE).map((roles) => roles.manualQa),
38
+ ] as const;
39
+ const REVIEW_AGENT_TYPE_SET = new Set<string>(REVIEW_AGENT_TYPES);
17
40
 
18
41
  export function applySpawnGuards(payload: PreToolUsePayload): string {
19
42
  if (payload.hook_event_name !== "PreToolUse" || !SPAWN_TOOL_TOKENS.has(payload.tool_name)) return "";
20
43
  const stateDir = ulwLoopDir(payload.cwd, { sessionId: payload.session_id });
21
44
  const plan = readPlan(join(stateDir, "goals.json"));
22
45
  if (plan === null) return "";
23
- const fanOutDenial = consumeFanOutBudget(stateDir);
24
- if (fanOutDenial !== null) return deny(fanOutDenial);
46
+ const fanOutPeek = peekFanOutBudget(stateDir);
47
+ if (fanOutPeek !== null) return deny(fanOutPeek);
25
48
  const missingArtifact = missingGateArtifact(payload, plan);
26
49
  if (missingArtifact !== null)
27
50
  return deny(`spawn code-review + QA first; gate audits their artifacts: missing ${missingArtifact}`);
51
+ const reviewDenial = consumeReviewSpawnBudget(payload, plan, stateDir);
52
+ if (reviewDenial !== null) return deny(reviewDenial);
53
+ const fanOutDenial = consumeFanOutBudget(stateDir);
54
+ if (fanOutDenial !== null) return deny(fanOutDenial);
28
55
  return "";
29
56
  }
30
57
 
@@ -41,46 +68,171 @@ export async function runSpawnGuardCli(stdin: NodeJS.ReadableStream, stdout: Nod
41
68
  }
42
69
  }
43
70
 
71
+ // Read-only fan-out eligibility check. Returns a denial reason when the next
72
+ // spawn would exceed the limit, without incrementing the counter. Call this
73
+ // before charging any per-reviewer quota so a saturated global cap cannot
74
+ // silently consume reviewer allowances for spawns that will never run.
75
+ function peekFanOutBudget(stateDir: string): string | null {
76
+ const counterPath = join(stateDir, "spawn-count.json");
77
+ const count = readCount(counterPath) + 1;
78
+ const limit = fanOutLimit();
79
+ if (count <= limit) return null;
80
+ 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.`;
81
+ }
82
+
44
83
  // Per-session spawn counter; depth/lineage tracking is descoped — this is a
45
84
  // total-volume backstop against fan-out explosions, not a recursion tracker.
46
85
  function consumeFanOutBudget(stateDir: string): string | null {
47
86
  const counterPath = join(stateDir, "spawn-count.json");
48
87
  const count = readCount(counterPath) + 1;
49
- writeFileSync(counterPath, JSON.stringify({ count }));
88
+ atomicWriteJson(counterPath, { count });
50
89
  const limit = fanOutLimit();
51
90
  if (count <= limit) return null;
52
91
  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.`;
53
92
  }
54
93
 
94
+ function consumeReviewSpawnBudget(payload: PreToolUsePayload, plan: UlwLoopPlan, stateDir: string): string | null {
95
+ const agentType = reviewAgentType(payload.tool_input);
96
+ if (agentType === null) return null;
97
+ const goal =
98
+ plan.goals.find((candidate) => candidate.id === plan.activeGoalId) ??
99
+ plan.goals.find((candidate) => isFinalRunCompletionCandidate(plan, candidate));
100
+ if (goal === undefined) return null;
101
+ const counterPath = join(stateDir, "review-spawn-counts.json");
102
+ const lockPath = `${counterPath}.lock`;
103
+ const limit = reviewSpawnLimit();
104
+ return withExclusiveLock(lockPath, () => {
105
+ const counts = readCounts(counterPath);
106
+ const key = `${agentType}:${goal.id}:a${goal.attempt}`;
107
+ const count = (counts[key] ?? 0) + 1;
108
+ if (count > limit)
109
+ return `ulw-loop reviewer no-progress cap reached (${agentType} ${count}/${limit}) for ${goal.id} attempt ${goal.attempt}. Consolidate existing review findings, or checkpoint and start a new attempt after concrete progress.`;
110
+ counts[key] = count;
111
+ atomicWriteJson(counterPath, counts);
112
+ return null;
113
+ });
114
+ }
115
+
55
116
  function missingGateArtifact(payload: PreToolUsePayload, plan: UlwLoopPlan): string | null {
56
117
  if (!isGateReviewerSpawn(payload.tool_input)) return null;
57
118
  const goal = plan.goals.find((candidate) => isFinalRunCompletionCandidate(plan, candidate));
58
119
  if (goal === undefined || goal.status === "complete") return null;
59
120
  if (!goal.successCriteria.every((criterion) => criterion.status === "pass")) return null;
60
121
  const scope = { sessionId: payload.session_id } as const;
122
+ const surface = resolveToolkitSurface();
123
+ const requiredArtifacts =
124
+ surface === "omo-senpi" ? [`${goal.id}-manual-qa.md`] : [`${goal.id}-code-review.md`, `${goal.id}-manual-qa.md`];
61
125
  if (plan.evidenceLayoutVersion === 2) {
62
126
  const attemptDir = ulwLoopAttemptEvidenceDir(goal.id, goal.attempt, scope);
63
- for (const name of [`${goal.id}-code-review.md`, `${goal.id}-manual-qa.md`]) {
127
+ for (const name of requiredArtifacts) {
64
128
  const relative = `${attemptDir}/${name}`;
65
129
  if (!isNonEmptyFile(join(payload.cwd, relative))) return relative;
66
130
  }
67
131
  return null;
68
132
  }
69
133
  const flatReport = `.omo/evidence/${goal.id}-code-review.md`;
70
- if (!isNonEmptyFile(join(payload.cwd, flatReport))) return flatReport;
134
+ if (surface !== "omo-senpi" && !isNonEmptyFile(join(payload.cwd, flatReport))) return flatReport;
135
+ if (surface === "omo-senpi") {
136
+ const manualQa = `.omo/evidence/${goal.id}-manual-qa.md`;
137
+ return isNonEmptyFile(join(payload.cwd, manualQa)) ? null : manualQa;
138
+ }
71
139
  // v1 manual-QA approximation: any other non-empty evidence file counts.
72
140
  if (!hasOtherEvidenceFile(join(payload.cwd, ".omo", "evidence"), `${goal.id}-code-review.md`))
73
- return `.omo/evidence/<any manual-QA artifact besides ${goal.id}-code-review.md>`;
141
+ return `.omo/evidence/${goal.id}-manual-qa.md`;
74
142
  return null;
75
143
  }
76
144
 
77
145
  function isGateReviewerSpawn(toolInput: unknown): boolean {
78
- if (typeof toolInput !== "object" || toolInput === null) return false;
146
+ const agentType = reviewAgentType(toolInput);
147
+ return agentType !== null && GATE_REVIEWER_AGENT_NAMES.has(agentType);
148
+ }
149
+
150
+ function reviewAgentType(toolInput: unknown): string | null {
151
+ if (typeof toolInput !== "object" || toolInput === null) return null;
79
152
  const record = toolInput as Record<string, unknown>;
80
153
  const agentType = record["agent_type"];
81
- if (typeof agentType === "string") return GATE_REVIEWER_AGENT_NAMES.has(agentType);
154
+ if (typeof agentType === "string") {
155
+ // V1: agent_type is present — only reviewer types proceed; any other type is not a review spawn.
156
+ if (!REVIEW_AGENT_TYPE_SET.has(agentType)) return null;
157
+ return activeSurfaceReviewerAlias(agentType);
158
+ }
82
159
  const message = record["message"];
83
- return typeof message === "string" && GATE_MESSAGE_PATTERN.test(message);
160
+ if (typeof message !== "string") return null;
161
+ const normalizedMessage = message.toLowerCase();
162
+ // Explicit "act as <role>" assignment takes priority. If the assigned role is not a reviewer,
163
+ // treat the spawn as non-review so a message that merely mentions a reviewer name does not
164
+ // accidentally charge that reviewer's quota.
165
+ const allRoleNames = [...REVIEW_AGENT_TYPES];
166
+ const explicitAssignment = allRoleNames
167
+ .map((name) => ({
168
+ name,
169
+ index: normalizedMessage.search(new RegExp(`\\bact as (?:an? )?${name}\\b`)),
170
+ }))
171
+ .filter(({ index }) => index >= 0)
172
+ .sort((left, right) => left.index - right.index)[0];
173
+ if (explicitAssignment !== undefined) return activeSurfaceReviewerAlias(explicitAssignment.name);
174
+ // Check for an explicit "act as <non-reviewer-role>" assignment. If found, the spawn is not a
175
+ // review spawn even if the message body mentions a reviewer name.
176
+ const nonReviewerActAs = /\bact as (?:an? )?\S+/.test(normalizedMessage);
177
+ if (nonReviewerActAs) return null;
178
+ const namedReviewer = REVIEW_AGENT_TYPES.find((name) => normalizedMessage.includes(name));
179
+ if (namedReviewer !== undefined) return activeSurfaceReviewerAlias(namedReviewer);
180
+ return GATE_MESSAGE_PATTERN.test(message) ? reviewerRolesFor(resolveToolkitSurface()).gateReview : null;
181
+ }
182
+
183
+ function activeSurfaceReviewerAlias(reviewer: string): string {
184
+ const activeRoles = reviewerRolesFor(resolveToolkitSurface());
185
+ for (const roles of Object.values(REVIEWER_ROLES_BY_SURFACE)) {
186
+ if (reviewer === roles.codeReview) return activeRoles.codeReview;
187
+ if (reviewer === roles.manualQa) return activeRoles.manualQa;
188
+ if (reviewer === roles.gateReview) return activeRoles.gateReview;
189
+ }
190
+ return reviewer;
191
+ }
192
+
193
+ function withExclusiveLock<T>(lockPath: string, fn: () => T): T {
194
+ mkdirSync(dirname(lockPath), { recursive: true });
195
+ const maxAttempts = 10;
196
+ const baseDelayMs = 10;
197
+ for (let attempt = 0; attempt < maxAttempts; attempt++) {
198
+ let fd: number | null = null;
199
+ try {
200
+ fd = openSync(lockPath, "wx");
201
+ writeFileSync(fd, process.pid.toString());
202
+ try {
203
+ return fn();
204
+ } finally {
205
+ try {
206
+ unlinkSync(lockPath);
207
+ } catch {
208
+ /* empty */
209
+ }
210
+ }
211
+ } catch (error) {
212
+ if (fd !== null) {
213
+ try {
214
+ unlinkSync(lockPath);
215
+ } catch {
216
+ /* empty */
217
+ }
218
+ }
219
+ if ((error as NodeJS.ErrnoException).code !== "EEXIST") {
220
+ return fn();
221
+ }
222
+ const delayMs = baseDelayMs * 2 ** attempt + Math.random() * baseDelayMs;
223
+ const deadline = Date.now() + delayMs;
224
+ while (Date.now() < deadline) {
225
+ /* spin */
226
+ }
227
+ }
228
+ }
229
+ return fn();
230
+ }
231
+
232
+ function atomicWriteJson(targetPath: string, data: unknown): void {
233
+ const tmp = join(dirname(targetPath), `.tmp-${randomBytes(6).toString("hex")}`);
234
+ writeFileSync(tmp, JSON.stringify(data));
235
+ renameSync(tmp, targetPath);
84
236
  }
85
237
 
86
238
  function deny(reason: string): string {
@@ -101,6 +253,13 @@ function fanOutLimit(): number {
101
253
  return Number.isFinite(parsed) && parsed > 0 ? parsed : DEFAULT_FANOUT_LIMIT;
102
254
  }
103
255
 
256
+ function reviewSpawnLimit(): number {
257
+ const raw = process.env["OMO_ULW_LOOP_REVIEW_SPAWN_LIMIT"];
258
+ if (raw === undefined) return DEFAULT_REVIEW_SPAWN_LIMIT;
259
+ const parsed = Number.parseInt(raw, 10);
260
+ return Number.isFinite(parsed) && parsed > 0 ? parsed : DEFAULT_REVIEW_SPAWN_LIMIT;
261
+ }
262
+
104
263
  function isNonEmptyFile(path: string): boolean {
105
264
  try {
106
265
  return existsSync(path) && statSync(path).size > 0;
@@ -129,6 +288,21 @@ function readCount(counterPath: string): number {
129
288
  }
130
289
  }
131
290
 
291
+ function readCounts(counterPath: string): Record<string, number> {
292
+ try {
293
+ const parsed: unknown = JSON.parse(readFileSync(counterPath, "utf8"));
294
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) return {};
295
+ const counts: Record<string, number> = {};
296
+ for (const [key, value] of Object.entries(parsed)) {
297
+ if (typeof value === "number" && value >= 0) counts[key] = value;
298
+ }
299
+ return counts;
300
+ } catch (error) {
301
+ if (error instanceof Error) return {};
302
+ throw error;
303
+ }
304
+ }
305
+
132
306
  function readPlan(goalsPath: string): UlwLoopPlan | null {
133
307
  try {
134
308
  return JSON.parse(readFileSync(goalsPath, "utf8")) as UlwLoopPlan;
@@ -0,0 +1,36 @@
1
+ import { hasAllCriteriaPass, isFinalRunCompletionCandidate } from "./goal-status.js";
2
+ import { ULW_LOOP_CREATE_GOALS_COMMAND } from "./plan-missing-recovery.js";
3
+ import type { UlwLoopItem, UlwLoopPlan } from "./types.js";
4
+
5
+ /**
6
+ * `status --json` is the one call every agent already makes between steps, so it is
7
+ * also the cheapest place to answer "what now?" without a second round trip.
8
+ */
9
+ export function statusNextActions(plan: UlwLoopPlan): readonly string[] {
10
+ const actions: string[] = [];
11
+ const active = plan.goals.find((goal) => goal.id === plan.activeGoalId);
12
+ if (plan.goals.length === 0) actions.push(`No goals yet: bootstrap with \`${ULW_LOOP_CREATE_GOALS_COMMAND}\`.`);
13
+ else if (active !== undefined) actions.push(...activeGoalActions(plan, active));
14
+ for (const goal of plan.goals.filter((candidate) => candidate.status === "review_blocked"))
15
+ actions.push(
16
+ `${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>'\`.`,
17
+ );
18
+ if (plan.evidenceLayoutVersion !== 2 || active === undefined)
19
+ actions.push("plan is evidence-layout v1; artifacts go under .omo/evidence/");
20
+ return actions;
21
+ }
22
+
23
+ function activeGoalActions(plan: UlwLoopPlan, active: UlwLoopItem): readonly string[] {
24
+ const unresolved = active.successCriteria.filter((criterion) => criterion.status !== "pass");
25
+ if (unresolved.length > 0)
26
+ return [
27
+ `${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>"\`.`,
28
+ ];
29
+ if (hasAllCriteriaPass(active) && isFinalRunCompletionCandidate(plan, active))
30
+ return [
31
+ `${active.id} passes every criterion and is the final story: update_goal complete, then checkpoint --print-template to build the final quality gate.`,
32
+ ];
33
+ return [
34
+ `${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>'\`.`,
35
+ ];
36
+ }
@@ -32,6 +32,28 @@ export const GATE_REVIEWER_AGENT_NAMES: ReadonlySet<string> = new Set(
32
32
  Object.values(REVIEWER_ROLES_BY_SURFACE).map((roles) => roles.gateReview),
33
33
  );
34
34
 
35
+ export type UlwLoopGateSection = "codeReview" | "manualQa" | "gateReview" | "iteration" | "criteriaCoverage";
36
+
37
+ export const REQUIRED_GATE_SECTIONS_BY_SURFACE: Readonly<Record<UlwLoopToolkitSurface, readonly UlwLoopGateSection[]>> =
38
+ {
39
+ lazycodex: ["codeReview", "manualQa", "gateReview", "iteration", "criteriaCoverage"],
40
+ "omo-senpi": ["manualQa", "gateReview", "iteration", "criteriaCoverage"],
41
+ };
42
+
43
+ export const GATE_SECTION_BY_ACCEPTOR: Readonly<
44
+ Record<UlwLoopToolkitSurface, Readonly<Partial<Record<UlwLoopGateSection, readonly string[]>>>>
45
+ > = {
46
+ lazycodex: {
47
+ codeReview: [REVIEWER_ROLES_BY_SURFACE.lazycodex.codeReview],
48
+ manualQa: [REVIEWER_ROLES_BY_SURFACE.lazycodex.manualQa],
49
+ gateReview: [REVIEWER_ROLES_BY_SURFACE.lazycodex.gateReview],
50
+ },
51
+ "omo-senpi": {
52
+ manualQa: ["main-session"],
53
+ gateReview: ["category:deep", "category:unspecified-high", "category:unspecified-low"],
54
+ },
55
+ };
56
+
35
57
  export function reviewerRolesFor(surface: UlwLoopToolkitSurface): UlwLoopReviewerRoles {
36
58
  return REVIEWER_ROLES_BY_SURFACE[surface];
37
59
  }
@@ -1,5 +1,5 @@
1
1
  import { writeFile } from "node:fs/promises";
2
- import { afterEach, describe, expect, it } from "vitest";
2
+ import { afterEach, beforeEach, describe, expect, it } from "vitest";
3
3
 
4
4
  import { checkpointUlwLoop } from "../src/checkpoint.js";
5
5
  import { ulwLoopBriefPath } from "../src/paths.js";
@@ -7,26 +7,89 @@ import { startNextUlwLoop } from "../src/plan-crud.js";
7
7
  import { criterion, expectCode, goal, passGoal, plan, repoWith, snapshot } from "./fixtures/checkpoint-builders.js";
8
8
  import { MISSING_ARTIFACT_PATH, qualityGateJson } from "./fixtures/quality-gate-builder.js";
9
9
 
10
+ function requiredSection(gate: Record<string, Record<string, unknown>>, key: string): Record<string, unknown> {
11
+ const section = gate[key];
12
+ if (section === undefined) throw new Error(`missing gate section: ${key}`);
13
+ return section;
14
+ }
15
+
10
16
  describe("checkpointUlwLoop final story surface resolution", () => {
17
+ beforeEach(() => {
18
+ delete process.env["OMO_AGENT_TOOLKIT_SURFACE"];
19
+ });
20
+
11
21
  afterEach(() => {
12
22
  delete process.env["OMO_AGENT_TOOLKIT_SURFACE"];
13
23
  });
14
24
 
15
- it("#given the omo-senpi surface #when the gate names omo-senpi reviewers #then the final story completes", async () => {
25
+ it("#given the omo-senpi surface #when the gate omits codeReview #then the final story completes", async () => {
16
26
  process.env["OMO_AGENT_TOOLKIT_SURFACE"] = "omo-senpi";
17
27
  const repo = await repoWith(
18
28
  plan([passGoal("G001", { status: "complete" }), passGoal("G002")], { activeGoalId: "G002" }),
19
29
  );
30
+ const gate = JSON.parse(await qualityGateJson(repo, undefined, "G001-finished", "omo-senpi")) as Record<
31
+ string,
32
+ Record<string, unknown>
33
+ >;
34
+ delete gate["codeReview"];
35
+ requiredSection(gate, "manualQa")["by"] = "main-session";
36
+ requiredSection(gate, "gateReview")["by"] = "category:deep";
20
37
 
21
38
  const result = await checkpointUlwLoop(repo, {
22
39
  goalId: "G002",
23
40
  status: "complete",
24
41
  evidence: "final work complete and validation passed",
25
42
  codexGoalJson: snapshot("complete"),
26
- qualityGateJson: await qualityGateJson(repo, undefined, "G001-finished", "omo-senpi"),
43
+ qualityGateJson: JSON.stringify(gate),
27
44
  });
28
45
 
29
46
  expect(result.aggregateCompletion?.status).toBe("complete");
47
+ expect(result.plan.aggregateCompletion?.status).toBe("complete");
48
+ });
49
+
50
+ it("#given the same codeReview-free gate #when the surface is lazycodex #then the final story is rejected", async () => {
51
+ const repo = await repoWith(
52
+ plan([passGoal("G001", { status: "complete" }), passGoal("G002")], { activeGoalId: "G002" }),
53
+ );
54
+ const gate = JSON.parse(await qualityGateJson(repo, undefined, "G001-finished", "omo-senpi")) as Record<
55
+ string,
56
+ Record<string, unknown>
57
+ >;
58
+ delete gate["codeReview"];
59
+ requiredSection(gate, "manualQa")["by"] = "main-session";
60
+ requiredSection(gate, "gateReview")["by"] = "category:deep";
61
+
62
+ await expectCode(
63
+ () =>
64
+ checkpointUlwLoop(repo, {
65
+ goalId: "G002",
66
+ status: "complete",
67
+ evidence: "final work complete and validation passed",
68
+ codexGoalJson: snapshot("complete"),
69
+ qualityGateJson: JSON.stringify(gate),
70
+ }),
71
+ "ULW_LOOP_QUALITY_GATE_INVALID",
72
+ );
73
+ });
74
+
75
+ it("#given the omo-senpi surface #when the gate includes codeReview #then the final story is rejected", async () => {
76
+ process.env["OMO_AGENT_TOOLKIT_SURFACE"] = "omo-senpi";
77
+ const repo = await repoWith(
78
+ plan([passGoal("G001", { status: "complete" }), passGoal("G002")], { activeGoalId: "G002" }),
79
+ );
80
+ const gateJson = await qualityGateJson(repo, undefined, "G001-finished", "omo-senpi");
81
+
82
+ await expectCode(
83
+ () =>
84
+ checkpointUlwLoop(repo, {
85
+ goalId: "G002",
86
+ status: "complete",
87
+ evidence: "final work complete and validation passed",
88
+ codexGoalJson: snapshot("complete"),
89
+ qualityGateJson: gateJson,
90
+ }),
91
+ "ULW_LOOP_QUALITY_GATE_INVALID",
92
+ );
30
93
  });
31
94
 
32
95
  it("#given the omo-senpi surface #when the gate names lazycodex reviewers #then the final story is rejected", async () => {
@@ -0,0 +1,92 @@
1
+ import { mkdtemp, rm } from "node:fs/promises";
2
+ import { tmpdir } from "node:os";
3
+ import { join } from "node:path";
4
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
5
+
6
+ import { ulwLoopCommand } from "../src/cli-commands.js";
7
+ import { writePlan } from "../src/plan-io.js";
8
+ import { goal, plan } from "./fixtures/checkpoint-builders.js";
9
+
10
+ let repo: string;
11
+ let output: string[];
12
+
13
+ beforeEach(async () => {
14
+ repo = await mkdtemp(join(tmpdir(), "ulw-template-"));
15
+ output = [];
16
+ vi.spyOn(process, "cwd").mockReturnValue(repo);
17
+ vi.spyOn(process.stdout, "write").mockImplementation((chunk: string | Uint8Array): boolean => {
18
+ output.push(chunk.toString());
19
+ return true;
20
+ });
21
+ delete process.env["OMO_AGENT_TOOLKIT_SURFACE"];
22
+ });
23
+
24
+ afterEach(async () => {
25
+ vi.restoreAllMocks();
26
+ await rm(repo, { recursive: true, force: true });
27
+ });
28
+
29
+ async function run(args: string[]): Promise<Record<string, unknown>> {
30
+ const code = await ulwLoopCommand(["checkpoint", ...args]);
31
+ expect(code).toBe(0);
32
+ return JSON.parse(output.join(""));
33
+ }
34
+
35
+ describe("checkpoint --print-template", () => {
36
+ it("#given an active senpi v2 plan #when printed with only goal-id #then emits the senpi gate skeleton", async () => {
37
+ await writePlan(
38
+ repo,
39
+ plan([goal({ id: "G001", attempt: 2 })], {
40
+ evidenceLayoutVersion: 2,
41
+ activeGoalId: "G001",
42
+ codexObjective: "Exact objective from goals.json",
43
+ }),
44
+ );
45
+ process.env["OMO_AGENT_TOOLKIT_SURFACE"] = "omo-senpi";
46
+ const result = await run(["--print-template", "--goal-id", "G001", "--json"]);
47
+ const gate = result["qualityGateTemplate"] as Record<string, unknown>;
48
+ expect(Object.keys(gate)).toEqual(["manualQa", "gateReview", "iteration", "criteriaCoverage"]);
49
+ expect((gate["manualQa"] as Record<string, unknown>)["by"]).toBe("main-session");
50
+ expect((gate["gateReview"] as Record<string, unknown>)["by"]).toBe("category:deep");
51
+ expect(result["codexGoalTemplate"]).toEqual({
52
+ goal: { objective: "Exact objective from goals.json", status: "complete" },
53
+ });
54
+ expect(result["attemptDir"]).toBe(".omo/evidence/ulw/session/G001/a2");
55
+ });
56
+
57
+ it("#given an active v2 plan #when printed without any goal id #then targets the active goal", async () => {
58
+ await writePlan(
59
+ repo,
60
+ plan([goal({ id: "G001", attempt: 1 })], {
61
+ evidenceLayoutVersion: 2,
62
+ activeGoalId: "G001",
63
+ codexObjective: "Exact objective from goals.json",
64
+ }),
65
+ );
66
+ process.env["OMO_AGENT_TOOLKIT_SURFACE"] = "omo-senpi";
67
+ const result = await run(["--print-template", "--json"]);
68
+ const gate = result["qualityGateTemplate"] as Record<string, unknown>;
69
+ expect(Object.keys(gate)).toEqual(["manualQa", "gateReview", "iteration", "criteriaCoverage"]);
70
+ expect(result["attemptDir"]).toBe(".omo/evidence/ulw/session/G001/a1");
71
+ });
72
+
73
+ it("#given two goals #when printed with an explicit non-active goal id #then targets that goal's attempt dir", async () => {
74
+ await writePlan(
75
+ repo,
76
+ plan([goal({ id: "G001", attempt: 1 }), goal({ id: "G002", attempt: 3 })], {
77
+ evidenceLayoutVersion: 2,
78
+ activeGoalId: "G001",
79
+ codexObjective: "Exact objective from goals.json",
80
+ }),
81
+ );
82
+ const result = await run(["--print-template", "--goal-id", "G002", "--json"]);
83
+ expect(result["attemptDir"]).toBe(".omo/evidence/ulw/session/G002/a3");
84
+ });
85
+
86
+ it("#given a v1 plan #when printed without status #then exits successfully and gives v1 guidance", async () => {
87
+ await writePlan(repo, plan([goal({ id: "G001" })]));
88
+ const result = await run(["--print-template", "--goal-id", "G001", "--json"]);
89
+ expect(result["guidance"]).toContain("evidence-layout v1");
90
+ expect(result).not.toHaveProperty("attemptDir");
91
+ });
92
+ });