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
@@ -0,0 +1,183 @@
1
+ import { mkdir, 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 { ulwLoopDir } from "../src/paths.js";
8
+ import { writePlan } from "../src/plan-io.js";
9
+ import type { UlwLoopItem, UlwLoopPlan, UlwLoopSuccessCriterion } from "../src/types.js";
10
+
11
+ const NOW = "2026-05-23T00:00:00.000Z";
12
+ const SESSION_ENV_KEYS = ["OMO_ULW_LOOP_SESSION_ID", "CODEX_SESSION_ID", "CODEX_THREAD_ID", "PI_SESSION_ID"] as const;
13
+
14
+ let testDir: string;
15
+ let out: string[];
16
+ let err: string[];
17
+ let savedEnv: Record<string, string | undefined>;
18
+
19
+ beforeEach(async () => {
20
+ testDir = await mkdtemp(join(tmpdir(), "ug-status-next-"));
21
+ out = [];
22
+ err = [];
23
+ savedEnv = {};
24
+ for (const key of SESSION_ENV_KEYS) {
25
+ savedEnv[key] = process.env[key];
26
+ delete process.env[key];
27
+ }
28
+ vi.spyOn(process, "cwd").mockReturnValue(testDir);
29
+ vi.spyOn(process.stdout, "write").mockImplementation((chunk: string | Uint8Array): boolean => {
30
+ out.push(chunk.toString());
31
+ return true;
32
+ });
33
+ vi.spyOn(process.stderr, "write").mockImplementation((chunk: string | Uint8Array): boolean => {
34
+ err.push(chunk.toString());
35
+ return true;
36
+ });
37
+ });
38
+
39
+ afterEach(async () => {
40
+ vi.restoreAllMocks();
41
+ for (const key of SESSION_ENV_KEYS) {
42
+ const saved = savedEnv[key];
43
+ if (saved === undefined) delete process.env[key];
44
+ else process.env[key] = saved;
45
+ }
46
+ await rm(testDir, { recursive: true, force: true });
47
+ });
48
+
49
+ function stdoutJson(): Record<string, unknown> {
50
+ return JSON.parse(out.join(""));
51
+ }
52
+
53
+ function nextActions(): readonly string[] {
54
+ const value = stdoutJson()["nextActions"];
55
+ if (!Array.isArray(value)) throw new Error(`expected nextActions array, got ${JSON.stringify(value)}`);
56
+ return value.map((entry) => String(entry));
57
+ }
58
+
59
+ function criterion(id: string, status: UlwLoopSuccessCriterion["status"]): UlwLoopSuccessCriterion {
60
+ return {
61
+ id,
62
+ scenario: `${id} scenario`,
63
+ userModel: "happy",
64
+ expectedEvidence: `${id} proof`,
65
+ capturedEvidence: status === "pass" ? `${id} passed` : null,
66
+ status,
67
+ };
68
+ }
69
+
70
+ function goal(overrides: Partial<UlwLoopItem> = {}): UlwLoopItem {
71
+ return {
72
+ id: "G001",
73
+ title: "Build auth",
74
+ objective: "Implement JWT auth endpoint",
75
+ status: "in_progress",
76
+ successCriteria: [criterion("C001", "pass")],
77
+ attempt: 1,
78
+ createdAt: NOW,
79
+ updatedAt: NOW,
80
+ ...overrides,
81
+ };
82
+ }
83
+
84
+ function plan(goals: UlwLoopItem[], overrides: Partial<UlwLoopPlan> = {}): UlwLoopPlan {
85
+ const result: UlwLoopPlan = {
86
+ version: 1,
87
+ evidenceLayoutVersion: 2,
88
+ createdAt: NOW,
89
+ updatedAt: NOW,
90
+ briefPath: ".omo/ulw-loop/brief.md",
91
+ goalsPath: ".omo/ulw-loop/goals.json",
92
+ ledgerPath: ".omo/ulw-loop/ledger.jsonl",
93
+ goals,
94
+ };
95
+ Object.assign(result, overrides);
96
+ return result;
97
+ }
98
+
99
+ function legacyLayoutPlan(goals: UlwLoopItem[], overrides: Partial<UlwLoopPlan> = {}): UlwLoopPlan {
100
+ const result = plan(goals, overrides);
101
+ delete result.evidenceLayoutVersion;
102
+ return result;
103
+ }
104
+
105
+ async function seed(seedPlan: UlwLoopPlan): Promise<void> {
106
+ await mkdir(ulwLoopDir(testDir), { recursive: true });
107
+ await writePlan(testDir, seedPlan);
108
+ }
109
+
110
+ describe("#given a plan with no goals", () => {
111
+ it("#when status --json #then nextActions points at create-goals", async () => {
112
+ await seed(plan([]));
113
+
114
+ expect(await ulwLoopCommand(["status", "--json"])).toBe(0);
115
+ expect(nextActions().join("\n")).toContain('omo-agent-toolkit ulw-loop create-goals --brief "<brief>" --json');
116
+ });
117
+ });
118
+
119
+ describe("#given an active goal with unresolved criteria", () => {
120
+ it("#when status --json #then nextActions names record-evidence with the remaining criterion ids", async () => {
121
+ await seed(
122
+ plan(
123
+ [
124
+ goal({
125
+ successCriteria: [criterion("C001", "pass"), criterion("C002", "pending"), criterion("C003", "fail")],
126
+ }),
127
+ ],
128
+ {
129
+ activeGoalId: "G001",
130
+ },
131
+ ),
132
+ );
133
+
134
+ expect(await ulwLoopCommand(["status", "--json"])).toBe(0);
135
+ const actions = nextActions().join("\n");
136
+ expect(actions).toContain("record-evidence");
137
+ expect(actions).toContain("C002");
138
+ expect(actions).toContain("C003");
139
+ expect(actions).not.toContain("C001");
140
+ expect(stdoutJson()).toHaveProperty("currentAttemptDir");
141
+ });
142
+ });
143
+
144
+ describe("#given the final story with every criterion passing", () => {
145
+ it("#when status --json #then nextActions tells the agent to complete the goal then print the template", async () => {
146
+ await seed(plan([goal({ successCriteria: [criterion("C001", "pass")] })], { activeGoalId: "G001" }));
147
+
148
+ expect(await ulwLoopCommand(["status", "--json"])).toBe(0);
149
+ const actions = nextActions().join("\n");
150
+ expect(actions).toContain("update_goal complete, then checkpoint --print-template");
151
+ });
152
+ });
153
+
154
+ describe("#given a review_blocked goal", () => {
155
+ it("#when status --json #then nextActions points at record-review-blockers", async () => {
156
+ await seed(plan([goal({ status: "review_blocked", successCriteria: [criterion("C001", "pass")] })]));
157
+
158
+ expect(await ulwLoopCommand(["status", "--json"])).toBe(0);
159
+ expect(nextActions().join("\n")).toContain("record-review-blockers");
160
+ });
161
+ });
162
+
163
+ describe("#given an evidence-layout v1 plan", () => {
164
+ it("#when status --json #then currentAttemptDir stays absent and nextActions explains the v1 layout", async () => {
165
+ await seed(legacyLayoutPlan([goal()], { activeGoalId: "G001" }));
166
+
167
+ expect(await ulwLoopCommand(["status", "--json"])).toBe(0);
168
+ expect(stdoutJson()).not.toHaveProperty("currentAttemptDir");
169
+ expect(nextActions().join("\n")).toContain("plan is evidence-layout v1; artifacts go under .omo/evidence/");
170
+ });
171
+ });
172
+
173
+ describe("#given a valueless --session-id in a subprocess context", () => {
174
+ it("#when any subcommand runs #then the error explains the missing session env and the flag usage", async () => {
175
+ expect(await ulwLoopCommand(["status", "--session-id", "--json"])).toBe(1);
176
+
177
+ const payload = stdoutJson();
178
+ expect(payload).toMatchObject({ ok: false, error: { code: "ULW_LOOP_SESSION_ID_REQUIRED" } });
179
+ const message = String((payload["error"] as Record<string, unknown>)["message"]);
180
+ expect(message).toContain("--session-id <id>");
181
+ expect(message).toContain("subprocess");
182
+ });
183
+ });
@@ -95,7 +95,7 @@ describe("buildCodexGoalInstruction aggregate mode", () => {
95
95
  expect(text).toContain("checkpoint");
96
96
  });
97
97
 
98
- it("#given a final story on the omo-senpi surface #when rendering instructions #then names the omo-senpi reviewers only", () => {
98
+ it("#given a final story on the omo-senpi surface #when rendering instructions #then teaches the single-gate category chain", () => {
99
99
  const { text } = buildCodexGoalInstruction({
100
100
  plan: makePlan({ codexGoalMode: "aggregate" }),
101
101
  goal: makeGoal(),
@@ -103,8 +103,18 @@ describe("buildCodexGoalInstruction aggregate mode", () => {
103
103
  surface: "omo-senpi",
104
104
  });
105
105
 
106
- expectTextToContainAll(text, SENPI_REVIEW_ROLES);
107
- for (const role of FINAL_REVIEW_ROLES) expect(text).not.toContain(role);
106
+ expectTextToContainAll(text, [
107
+ "main-session",
108
+ 'category: "deep"',
109
+ "unspecified-high",
110
+ "unspecified-low",
111
+ "gateReview.by",
112
+ "category:<name>",
113
+ "--print-template",
114
+ ]);
115
+ for (const role of [...FINAL_REVIEW_ROLES, ...SENPI_REVIEW_ROLES]) expect(text).not.toContain(role);
116
+ expect(text).toMatch(/model_unavailable/);
117
+ expect(text).not.toContain("attempted_chain");
108
118
  });
109
119
 
110
120
  it("#given a final story with the explicit lazycodex surface #when rendering instructions #then keeps the lazycodex reviewers", () => {
@@ -0,0 +1,105 @@
1
+ import { mkdir, mkdtemp } from "node:fs/promises";
2
+ import { tmpdir } from "node:os";
3
+ import { join } from "node:path";
4
+ import { describe, expect, it } from "vitest";
5
+
6
+ import { checkpointUlwLoop } from "../src/checkpoint.js";
7
+ import { ULW_LOOP_AGGREGATE_CODEX_OBJECTIVE } from "../src/goal-status.js";
8
+ import { ulwLoopDir } from "../src/paths.js";
9
+ import { readUlwLoopPlan } from "../src/plan-io.js";
10
+ import { recordFinalReviewBlockers } from "../src/review-blockers.js";
11
+ import { UlwLoopError } from "../src/types.js";
12
+ import { goal, passGoal, plan, repoWith, snapshot } from "./fixtures/checkpoint-builders.js";
13
+
14
+ async function captureError(action: () => Promise<unknown>): Promise<UlwLoopError> {
15
+ try {
16
+ await action();
17
+ } catch (error) {
18
+ expect(error).toBeInstanceOf(UlwLoopError);
19
+ if (error instanceof UlwLoopError) return error;
20
+ throw error;
21
+ }
22
+ throw new Error("Expected UlwLoopError");
23
+ }
24
+
25
+ describe("#given a codex goal snapshot whose objective differs from the plan", () => {
26
+ it("#when checkpoint reconciles it #then the mismatch carries the verbatim expected and received objectives", async () => {
27
+ const repo = await repoWith(plan([passGoal("G001"), goal({ id: "G002", status: "pending" })]));
28
+
29
+ const error = await captureError(() =>
30
+ checkpointUlwLoop(repo, {
31
+ goalId: "G001",
32
+ status: "complete",
33
+ evidence: "work complete and validation passed",
34
+ codexGoalJson: snapshot("active", "wrong objective"),
35
+ }),
36
+ );
37
+
38
+ expect(error.code).toBe("ulw_loop_codex_snapshot_mismatch");
39
+ expect(error.details).toMatchObject({
40
+ expectedObjective: ULW_LOOP_AGGREGATE_CODEX_OBJECTIVE,
41
+ receivedObjective: "wrong objective",
42
+ });
43
+ expect(error.message).toContain(
44
+ "objective must equal the plan's codexObjective exactly — copy the expected value below",
45
+ );
46
+ expect(error.message).toContain(ULW_LOOP_AGGREGATE_CODEX_OBJECTIVE);
47
+ });
48
+
49
+ it("#when record-review-blockers reconciles it #then the same guided payload is emitted", async () => {
50
+ const repo = await repoWith(
51
+ plan([passGoal("G001", { status: "complete" }), goal({ id: "G002", status: "in_progress" })]),
52
+ );
53
+
54
+ const error = await captureError(() =>
55
+ recordFinalReviewBlockers(repo, {
56
+ goalId: "G002",
57
+ title: "Resolve final review blockers",
58
+ objective: "Address the BLOCK findings",
59
+ evidence: "review verdict: REQUEST_CHANGES",
60
+ codexGoalJson: JSON.stringify({ goal: { objective: "stale objective", status: "active" } }),
61
+ }),
62
+ );
63
+
64
+ expect(error.code).toBe("ulw_loop_codex_snapshot_mismatch");
65
+ expect(error.details).toMatchObject({
66
+ expectedObjective: ULW_LOOP_AGGREGATE_CODEX_OBJECTIVE,
67
+ receivedObjective: "stale objective",
68
+ });
69
+ expect(error.message).toContain(
70
+ "objective must equal the plan's codexObjective exactly — copy the expected value below",
71
+ );
72
+ });
73
+ });
74
+
75
+ describe("#given no ulw-loop plan on disk", () => {
76
+ it("#when the plan is read #then the error names the exact create-goals bootstrap command", async () => {
77
+ const repo = await mkdtemp(join(tmpdir(), "ug-guided-plan-"));
78
+
79
+ const error = await captureError(() => readUlwLoopPlan(repo));
80
+
81
+ expect(error.code).toBe("ULW_LOOP_PLAN_MISSING");
82
+ expect(error.message).toContain('omo-agent-toolkit ulw-loop create-goals --brief "<brief>" --json');
83
+ });
84
+
85
+ it("#when sibling session dirs exist #then the error lists the existing session ids", async () => {
86
+ const repo = await mkdtemp(join(tmpdir(), "ug-guided-plan-"));
87
+ await mkdir(join(ulwLoopDir(repo), "session-alpha"), { recursive: true });
88
+ await mkdir(join(ulwLoopDir(repo), "session-beta"), { recursive: true });
89
+
90
+ const error = await captureError(() => readUlwLoopPlan(repo, { sessionId: "session-gamma" }));
91
+
92
+ expect(error.code).toBe("ULW_LOOP_PLAN_MISSING");
93
+ expect(error.message).toContain("session-alpha");
94
+ expect(error.message).toContain("session-beta");
95
+ expect(error.details).toMatchObject({ existingSessionIds: ["session-alpha", "session-beta"] });
96
+ });
97
+
98
+ it("#when no session dirs exist #then the error lists no session ids", async () => {
99
+ const repo = await mkdtemp(join(tmpdir(), "ug-guided-plan-"));
100
+
101
+ const error = await captureError(() => readUlwLoopPlan(repo, { sessionId: "session-gamma" }));
102
+
103
+ expect(error.details?.["existingSessionIds"]).toBeUndefined();
104
+ });
105
+ });
@@ -0,0 +1,101 @@
1
+ import { existsSync, statSync } from "node:fs";
2
+ import { describe, expect, it } from "vitest";
3
+ import { validateQualityGate } from "../src/quality-gate.js";
4
+ import { UlwLoopError } from "../src/types.js";
5
+
6
+ const opts = { repoRoot: process.cwd(), fs: { existsSync, statSync }, reviewerSurface: "omo-senpi" as const };
7
+
8
+ describe("aggregated quality gate validation", () => {
9
+ it("#given four independent defects #when validating #then reports all defects in one error", () => {
10
+ const gate = {
11
+ manualQa: {
12
+ by: "main-session",
13
+ status: "passed",
14
+ evidence: "placeholder",
15
+ surfaceEvidence: [],
16
+ adversarialCases: [],
17
+ artifactRefs: [
18
+ { id: "missing", kind: "cli-transcript", description: "artifact", path: "does-not-exist.txt" },
19
+ ],
20
+ },
21
+ gateReview: {
22
+ by: "category:deep",
23
+ recommendation: "REJECT",
24
+ reportPath: "test/fixtures/artifacts/gate-review.md",
25
+ blockers: [],
26
+ },
27
+ iteration: { fullRerun: true, status: "passed", rerunCommands: ["test"], evidence: "rerun" },
28
+ criteriaCoverage: {
29
+ totalCriteria: 1,
30
+ passCount: 1,
31
+ originalIntent: "intent",
32
+ desiredOutcome: "outcome",
33
+ userOutcomeReview: "review",
34
+ adversarialClassesCovered: ["none"],
35
+ },
36
+ };
37
+ try {
38
+ validateQualityGate(gate, opts);
39
+ throw new Error("expected validation failure");
40
+ } catch (error) {
41
+ expect(error).toBeInstanceOf(UlwLoopError);
42
+ if (!(error instanceof UlwLoopError)) throw error;
43
+ expect(error.code).toBe("ULW_LOOP_QUALITY_GATE_INVALID");
44
+ expect(error.details?.["field"]).toBe("manualQa.evidence");
45
+ expect(error.details?.["fields"]).toEqual(
46
+ expect.arrayContaining([
47
+ { field: "manualQa.evidence", message: expect.stringContaining("placeholder") },
48
+ { field: "gateReview.evidence", message: expect.stringContaining("non-empty") },
49
+ { field: "gateReview.recommendation", message: expect.stringContaining("APPROVE") },
50
+ { field: "manualQa.artifactRefs[0].path", message: expect.stringContaining("existing") },
51
+ ]),
52
+ );
53
+ }
54
+ });
55
+
56
+ it("#given more than twenty-five defects #when validating #then caps the list and flags truncation", () => {
57
+ const refs = Array.from({ length: 30 }, (_, index) => ({
58
+ id: `ref-${index}`,
59
+ kind: "cli-transcript",
60
+ description: "artifact",
61
+ path: `does-not-exist-${index}.txt`,
62
+ }));
63
+ const gate = {
64
+ manualQa: {
65
+ by: "main-session",
66
+ status: "passed",
67
+ evidence: "real evidence",
68
+ surfaceEvidence: [],
69
+ adversarialCases: [],
70
+ artifactRefs: refs,
71
+ },
72
+ gateReview: {
73
+ by: "category:deep",
74
+ recommendation: "APPROVE",
75
+ reportPath: "test/fixtures/artifacts/gate-review.md",
76
+ evidence: "gate evidence",
77
+ blockers: [],
78
+ },
79
+ iteration: { fullRerun: true, status: "passed", rerunCommands: ["test"], evidence: "rerun" },
80
+ criteriaCoverage: {
81
+ totalCriteria: 1,
82
+ passCount: 1,
83
+ originalIntent: "intent",
84
+ desiredOutcome: "outcome",
85
+ userOutcomeReview: "review",
86
+ adversarialClassesCovered: ["none"],
87
+ },
88
+ };
89
+ try {
90
+ validateQualityGate(gate, opts);
91
+ throw new Error("expected validation failure");
92
+ } catch (error) {
93
+ expect(error).toBeInstanceOf(UlwLoopError);
94
+ if (!(error instanceof UlwLoopError)) throw error;
95
+ const fields = error.details?.["fields"];
96
+ expect(Array.isArray(fields) && fields.length).toBe(25);
97
+ expect(error.details?.["truncated"]).toBe(true);
98
+ expect(error.message).toContain("25+");
99
+ }
100
+ });
101
+ });
@@ -85,18 +85,20 @@ function qualityGateError(input: unknown, surface?: "lazycodex" | "omo-senpi"):
85
85
  throw new Error("Expected UlwLoopError");
86
86
  }
87
87
 
88
- function senpiGate(): Record<string, unknown> {
89
- return gateWith({
90
- codeReview: { ...BASE_GATE.codeReview, by: "omo-senpi-code-reviewer" },
91
- manualQa: { ...BASE_GATE.manualQa, by: "omo-senpi-qa-executor" },
92
- gateReview: { ...BASE_GATE.gateReview, by: "omo-senpi-gate-reviewer" },
88
+ function senpiGate(overrides: Record<string, unknown> = {}): Record<string, unknown> {
89
+ const { codeReview: _codeReview, ...withoutCodeReview } = BASE_GATE;
90
+ return {
91
+ ...withoutCodeReview,
92
+ manualQa: { ...BASE_GATE.manualQa, by: "main-session" },
93
+ gateReview: { ...BASE_GATE.gateReview, by: "category:deep" },
93
94
  criteriaCoverage: {
94
95
  ...BASE_GATE.criteriaCoverage,
95
96
  originalIntent: "User wanted the senpi surface to accept honest reviewer attribution.",
96
97
  desiredOutcome: "A gate naming the omo-senpi reviewers validates on the omo-senpi surface.",
97
98
  userOutcomeReview: "The senpi-surface gate passed with truthful reviewer identities.",
98
99
  },
99
- });
100
+ ...overrides,
101
+ };
100
102
  }
101
103
 
102
104
  describe("validateQualityGate reviewer roles", () => {
@@ -124,29 +126,28 @@ describe("validateQualityGate reviewer roles", () => {
124
126
  expect(() => validateQualityGate(senpiGate(), { reviewerSurface: "omo-senpi" })).not.toThrow();
125
127
  });
126
128
 
127
- it("#given lazycodex identities #when validated on the omo-senpi surface #then each role names the omo-senpi identity", () => {
129
+ it("#given a codeReview lane #when validated on the omo-senpi surface #then it is rejected", () => {
128
130
  const error = qualityGateError(gateWith({}), "omo-senpi");
129
131
  expect(error.code).toBe("ULW_LOOP_QUALITY_GATE_INVALID");
130
- expect(error.message).toContain("omo-senpi-code-reviewer");
132
+ expect(error.message).toContain("no codeReview lane");
131
133
  });
132
134
 
133
- it("#given omo-senpi identities #when validated on the default lazycodex surface #then the gate is rejected", () => {
135
+ it("#given a four-section senpi gate #when validated on the default lazycodex surface #then codeReview is required", () => {
134
136
  const error = qualityGateError(senpiGate());
135
137
  expect(error.code).toBe("ULW_LOOP_QUALITY_GATE_INVALID");
136
- expect(error.message).toContain("lazycodex-code-reviewer");
138
+ expect(error.message).toContain("codeReview");
137
139
  });
138
140
 
139
141
  it("#given swapped omo-senpi reviewer roles #when validated on the omo-senpi surface #then section-specific roles are enforced", () => {
140
- const swapped = gateWith({
141
- codeReview: { ...BASE_GATE.codeReview, by: "omo-senpi-qa-executor" },
142
- manualQa: { ...BASE_GATE.manualQa, by: "omo-senpi-qa-executor" },
143
- gateReview: { ...BASE_GATE.gateReview, by: "omo-senpi-gate-reviewer" },
142
+ const swapped = senpiGate({
143
+ manualQa: { ...BASE_GATE.manualQa, by: "category:deep" },
144
+ gateReview: { ...BASE_GATE.gateReview, by: "main-session" },
144
145
  });
145
146
 
146
147
  const error = qualityGateError(swapped, "omo-senpi");
147
148
 
148
149
  expect(error.code).toBe("ULW_LOOP_QUALITY_GATE_INVALID");
149
- expect(error.message).toContain("codeReview.by");
150
+ expect(error.message).toContain("manualQa.by");
150
151
  });
151
152
 
152
153
  it("#given swapped LazyCodex reviewer roles #when validated #then section-specific roles are enforced", () => {
@@ -0,0 +1,131 @@
1
+ import { readFile } from "node:fs/promises";
2
+
3
+ import { describe, expect, it } from "vitest";
4
+
5
+ import { validateQualityGate } from "../src/quality-gate.js";
6
+ import { UlwLoopError } from "../src/types.js";
7
+
8
+ const COMMON = {
9
+ manualQa: {
10
+ status: "passed",
11
+ evidence: "Ran senpi manual QA with artifact-backed evidence.",
12
+ surfaceEvidence: [
13
+ {
14
+ id: "surface-cli-pass",
15
+ criterionRef: "C1",
16
+ surface: "cli",
17
+ invocation: "omo-agent-toolkit ulw-loop checkpoint --status complete",
18
+ verdict: "passed",
19
+ artifactRefs: ["artifact-cli-pass"],
20
+ },
21
+ ],
22
+ adversarialCases: [
23
+ {
24
+ id: "adv-role",
25
+ criterionRef: "C2",
26
+ scenario: "a reviewer role is invalid",
27
+ expectedBehavior: "the gate rejects the invalid role",
28
+ verdict: "passed",
29
+ artifactRefs: ["artifact-cli-pass"],
30
+ },
31
+ ],
32
+ artifactRefs: [
33
+ {
34
+ id: "artifact-cli-pass",
35
+ kind: "cli-transcript",
36
+ description: "CLI transcript for valid checkpoint.",
37
+ path: "cli-pass.txt",
38
+ },
39
+ ],
40
+ },
41
+ iteration: {
42
+ fullRerun: true,
43
+ status: "passed",
44
+ rerunCommands: ["bunx vitest run"],
45
+ evidence: "Focused tests passed.",
46
+ },
47
+ criteriaCoverage: {
48
+ totalCriteria: 2,
49
+ passCount: 2,
50
+ originalIntent: "User wanted a valid senpi quality gate.",
51
+ desiredOutcome: "The senpi gate accepts its four required lanes.",
52
+ userOutcomeReview: "The requested senpi outcome is covered.",
53
+ adversarialClassesCovered: ["role_mismatch"],
54
+ },
55
+ } as const;
56
+
57
+ function senpiGate(overrides: Record<string, unknown> = {}): Record<string, unknown> {
58
+ return {
59
+ ...COMMON,
60
+ manualQa: { ...COMMON.manualQa, by: "main-session" },
61
+ gateReview: {
62
+ by: "category:deep",
63
+ recommendation: "APPROVE",
64
+ reportPath: "gate-review.md",
65
+ evidence: "Verified the senpi gate.",
66
+ blockers: [],
67
+ },
68
+ ...overrides,
69
+ };
70
+ }
71
+
72
+ function section(input: Record<string, unknown>, key: string): Record<string, unknown> {
73
+ const value = input[key];
74
+ if (typeof value !== "object" || value === null || Array.isArray(value))
75
+ throw new Error(`missing object section: ${key}`);
76
+ return value as Record<string, unknown>;
77
+ }
78
+
79
+ function errorFor(input: unknown): UlwLoopError {
80
+ try {
81
+ validateQualityGate(input, { reviewerSurface: "omo-senpi" });
82
+ } catch (error) {
83
+ if (error instanceof UlwLoopError) return error;
84
+ throw error;
85
+ }
86
+ throw new Error("Expected quality gate validation to fail");
87
+ }
88
+
89
+ describe("validateQualityGate omo-senpi surface", () => {
90
+ it("#given a four-section senpi gate #when validated #then it accepts main-session manual QA and category:deep review", () => {
91
+ expect(() => validateQualityGate(senpiGate(), { reviewerSurface: "omo-senpi" })).not.toThrow();
92
+ });
93
+
94
+ it("#given a senpi gate with codeReview #when validated #then it rejects the forbidden codeReview lane", () => {
95
+ const error = errorFor(senpiGate({ codeReview: { by: "anything" } }));
96
+ expect(error.details?.["field"]).toContain("codeReview");
97
+ expect(error.message).toContain("omo-senpi gate has no codeReview lane");
98
+ });
99
+
100
+ it.each(["category:deep", "category:unspecified-high", "category:unspecified-low"])(
101
+ "#given gateReview.by %s #when validated on senpi #then it accepts the category role",
102
+ (by) => {
103
+ expect(() =>
104
+ validateQualityGate(senpiGate({ gateReview: { ...section(senpiGate(), "gateReview"), by } }), {
105
+ reviewerSurface: "omo-senpi",
106
+ }),
107
+ ).not.toThrow();
108
+ },
109
+ );
110
+
111
+ it.each(["omo-senpi-gate-reviewer", "lazycodex-gate-reviewer", "arbitrary"])(
112
+ "#given gateReview.by %s #when validated on senpi #then it rejects the non-category role",
113
+ (by) => {
114
+ const error = errorFor(senpiGate({ gateReview: { ...section(senpiGate(), "gateReview"), by } }));
115
+ expect(error.message).toContain("gateReview.by");
116
+ },
117
+ );
118
+
119
+ it.each(["omo-senpi-qa-executor", "lazycodex-qa-executor", "arbitrary"])(
120
+ "#given manualQa.by %s #when validated on senpi #then it rejects the non-main-session role",
121
+ (by) => {
122
+ const error = errorFor(senpiGate({ manualQa: { ...COMMON.manualQa, by } }));
123
+ expect(error.message).toContain("manualQa.by");
124
+ },
125
+ );
126
+
127
+ it("#given the existing lazycodex fixture #when validated on the default surface #then it remains valid", async () => {
128
+ const raw = await readFile(new URL("./fixtures/sample-quality-gate.json", import.meta.url), "utf8");
129
+ expect(() => validateQualityGate(JSON.parse(raw))).not.toThrow();
130
+ });
131
+ });
@@ -111,7 +111,10 @@ describe("validateQualityGate", () => {
111
111
  "gateReview",
112
112
  "iteration",
113
113
  "manualQa",
114
+ "surface",
114
115
  ]);
116
+ expect("codeReview" in gate).toBe(true);
117
+ if (!("codeReview" in gate)) throw new Error("expected lazycodex codeReview section");
115
118
  expect(gate.codeReview.codeQualityStatus).toBe("CLEAR");
116
119
  expect(gate).toMatchObject({
117
120
  criteriaCoverage: { totalCriteria: 9, passCount: 9, userOutcomeReview: expect.stringContaining("user") },
@@ -127,6 +130,8 @@ describe("validateQualityGate", () => {
127
130
  const gate = validateQualityGate(parsed, FS_OPTS);
128
131
 
129
132
  // then
133
+ expect("codeReview" in gate).toBe(true);
134
+ if (!("codeReview" in gate)) throw new Error("expected lazycodex codeReview section");
130
135
  expect(gate.codeReview.recommendation).toBe("APPROVE");
131
136
  expect(gate.manualQa.artifactRefs).toHaveLength(5);
132
137
  });