oh-my-opencode 5.0.0-beta.34 → 5.0.0-beta.36

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 (220) hide show
  1. package/.agents/skills/codex-qa/scripts/lsp-e2e.sh +1 -3
  2. package/.agents/skills/opencode-qa/scripts/lsp-e2e.sh +1 -1
  3. package/.agents/skills/tech-debt-audit/SKILL.md +6 -76
  4. package/README.ja.md +1 -1
  5. package/README.ko.md +1 -1
  6. package/README.md +1 -1
  7. package/README.ru.md +1 -1
  8. package/README.zh-cn.md +1 -1
  9. package/THIRD-PARTY-NOTICES.md +27 -99
  10. package/dist/cli/index.js +48341 -48541
  11. package/dist/cli-node/index.js +49016 -49216
  12. package/dist/config/index.d.ts +1 -1
  13. package/dist/config/schema/hooks.d.ts +0 -1
  14. package/dist/config/schema/oh-my-opencode-config.d.ts +0 -10
  15. package/dist/config/schema.d.ts +0 -1
  16. package/dist/create-hooks.d.ts +0 -1
  17. package/dist/hooks/index.d.ts +0 -1
  18. package/dist/index.js +11675 -13718
  19. package/dist/mcp/index.d.ts +0 -4
  20. package/dist/mcp/types.d.ts +0 -1
  21. package/dist/oh-my-opencode.schema.json +1 -45
  22. package/dist/plugin/hooks/create-core-hooks.d.ts +0 -1
  23. package/dist/plugin/hooks/create-session-hooks.d.ts +1 -2
  24. package/dist/shared/omo-process-sweep.d.ts +2 -4
  25. package/dist/skills/init-deep/SKILL.md +13 -13
  26. package/dist/skills/ulw-execute/SKILL.md +9 -9
  27. package/dist/skills/ulw-plan/SKILL.md +0 -1
  28. package/dist/tui.js +44809 -44883
  29. package/package.json +16 -21
  30. package/packages/omo-codex/THIRD-PARTY-NOTICES.md +0 -17
  31. package/packages/omo-codex/plugin/.codex-plugin/plugin.json +1 -3
  32. package/packages/omo-codex/plugin/.mcp.json +0 -6
  33. package/packages/omo-codex/plugin/AGENTS.md +5 -5
  34. package/packages/omo-codex/plugin/README.md +1 -1
  35. package/packages/omo-codex/plugin/components/bootstrap/dist/cli.js +1 -15
  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/comment-checker/hooks/hooks.json +1 -1
  39. package/packages/omo-codex/plugin/components/comment-checker/package.json +1 -1
  40. package/packages/omo-codex/plugin/components/git-bash/hooks/hooks.json +2 -2
  41. package/packages/omo-codex/plugin/components/git-bash/package.json +1 -1
  42. package/packages/omo-codex/plugin/components/lazycodex-executor-verify/hooks/hooks.json +1 -1
  43. package/packages/omo-codex/plugin/components/lazycodex-executor-verify/package.json +1 -1
  44. package/packages/omo-codex/plugin/components/lcx/skills/lcx-doctor/SKILL.md +1 -1
  45. package/packages/omo-codex/plugin/components/lsp/dist/.omo-runtime-manifest.json +2 -2
  46. package/packages/omo-codex/plugin/components/lsp/hooks/hooks.json +2 -2
  47. package/packages/omo-codex/plugin/components/lsp/package.json +1 -1
  48. package/packages/omo-codex/plugin/components/rules/hooks/hooks.json +4 -4
  49. package/packages/omo-codex/plugin/components/rules/package.json +1 -1
  50. package/packages/omo-codex/plugin/components/teammode/hooks/hooks.json +1 -1
  51. package/packages/omo-codex/plugin/components/teammode/package.json +1 -1
  52. package/packages/omo-codex/plugin/components/teammode/test/thread-title-hook.test.ts +1 -1
  53. package/packages/omo-codex/plugin/components/telemetry/hooks/hooks.json +1 -1
  54. package/packages/omo-codex/plugin/components/telemetry/package.json +1 -1
  55. package/packages/omo-codex/plugin/components/ultrawork/dist/cli.js +2 -2
  56. package/packages/omo-codex/plugin/components/ultrawork/hooks/hooks.json +1 -1
  57. package/packages/omo-codex/plugin/components/ultrawork/package.json +1 -1
  58. package/packages/omo-codex/plugin/components/ultrawork/skills/ulw-plan/SKILL.md +0 -1
  59. package/packages/omo-codex/plugin/components/ultrawork/src/directive-content.ts +1 -1
  60. package/packages/omo-codex/plugin/components/ulw-execute-continuation/hooks/hooks.json +2 -2
  61. package/packages/omo-codex/plugin/components/ulw-execute-continuation/package.json +1 -1
  62. package/packages/omo-codex/plugin/components/ulw-loop/README.md +11 -1
  63. package/packages/omo-codex/plugin/components/ulw-loop/directive.md +2 -2
  64. package/packages/omo-codex/plugin/components/ulw-loop/dist/checkpoint-codex-validation.d.ts +12 -0
  65. package/packages/omo-codex/plugin/components/ulw-loop/dist/checkpoint-codex-validation.js +45 -0
  66. package/packages/omo-codex/plugin/components/ulw-loop/dist/checkpoint-reconciliation.d.ts +1 -0
  67. package/packages/omo-codex/plugin/components/ulw-loop/dist/checkpoint-reconciliation.js +5 -0
  68. package/packages/omo-codex/plugin/components/ulw-loop/dist/checkpoint-template.js +10 -3
  69. package/packages/omo-codex/plugin/components/ulw-loop/dist/checkpoint.js +35 -38
  70. package/packages/omo-codex/plugin/components/ulw-loop/dist/cli.js +278 -193
  71. package/packages/omo-codex/plugin/components/ulw-loop/dist/quality-gate-aggregate.d.ts +5 -0
  72. package/packages/omo-codex/plugin/components/ulw-loop/dist/quality-gate-aggregate.js +7 -51
  73. package/packages/omo-codex/plugin/components/ulw-loop/dist/quality-gate-artifacts.js +39 -22
  74. package/packages/omo-codex/plugin/components/ulw-loop/dist/quality-gate-fields.d.ts +13 -1
  75. package/packages/omo-codex/plugin/components/ulw-loop/dist/quality-gate-fields.js +64 -8
  76. package/packages/omo-codex/plugin/components/ulw-loop/dist/quality-gate-verdicts.js +5 -1
  77. package/packages/omo-codex/plugin/components/ulw-loop/dist/quality-gate.js +37 -18
  78. package/packages/omo-codex/plugin/components/ulw-loop/hooks/hooks.json +4 -4
  79. package/packages/omo-codex/plugin/components/ulw-loop/package.json +1 -1
  80. package/packages/omo-codex/plugin/components/ulw-loop/skills/ulw-loop/references/full-workflow.md +2 -1
  81. package/packages/omo-codex/plugin/components/ulw-loop/src/checkpoint-codex-validation.ts +81 -0
  82. package/packages/omo-codex/plugin/components/ulw-loop/src/checkpoint-reconciliation.ts +6 -0
  83. package/packages/omo-codex/plugin/components/ulw-loop/src/checkpoint-template.ts +10 -3
  84. package/packages/omo-codex/plugin/components/ulw-loop/src/checkpoint.ts +32 -63
  85. package/packages/omo-codex/plugin/components/ulw-loop/src/quality-gate-aggregate.ts +7 -53
  86. package/packages/omo-codex/plugin/components/ulw-loop/src/quality-gate-artifacts.ts +50 -31
  87. package/packages/omo-codex/plugin/components/ulw-loop/src/quality-gate-fields.ts +67 -14
  88. package/packages/omo-codex/plugin/components/ulw-loop/src/quality-gate-verdicts.ts +6 -1
  89. package/packages/omo-codex/plugin/components/ulw-loop/src/quality-gate.ts +36 -17
  90. package/packages/omo-codex/plugin/components/ulw-loop/test/checkpoint-final.test.ts +26 -0
  91. package/packages/omo-codex/plugin/components/ulw-loop/test/checkpoint-template.test.ts +47 -2
  92. package/packages/omo-codex/plugin/components/ulw-loop/test/quality-gate-aggregate-basics.test.ts +258 -0
  93. package/packages/omo-codex/plugin/components/ulw-loop/test/quality-gate-cap-and-dedupe.test.ts +22 -0
  94. package/packages/omo-codex/plugin/components/ulw-loop/test/quality-gate-poisoning-cascades.test.ts +97 -0
  95. package/packages/omo-codex/plugin/components/ulw-loop/test/quality-gate-single-report.test.ts +241 -0
  96. package/packages/omo-codex/plugin/hooks/post-compact-resetting-git-bash-mcp-reminder.json +1 -1
  97. package/packages/omo-codex/plugin/hooks/post-compact-resetting-lsp-diagnostics-cache.json +1 -1
  98. package/packages/omo-codex/plugin/hooks/post-compact-resetting-project-rule-cache.json +1 -1
  99. package/packages/omo-codex/plugin/hooks/post-tool-use-checking-comments.json +1 -1
  100. package/packages/omo-codex/plugin/hooks/post-tool-use-checking-lsp-diagnostics.json +1 -1
  101. package/packages/omo-codex/plugin/hooks/post-tool-use-checking-thread-title-hygiene.json +1 -1
  102. package/packages/omo-codex/plugin/hooks/post-tool-use-matching-project-rules.json +1 -1
  103. package/packages/omo-codex/plugin/hooks/pre-tool-use-enforcing-unlimited-goal-budget.json +1 -1
  104. package/packages/omo-codex/plugin/hooks/pre-tool-use-guarding-ulw-loop-spawns.json +1 -1
  105. package/packages/omo-codex/plugin/hooks/pre-tool-use-recommending-git-bash-mcp.json +1 -1
  106. package/packages/omo-codex/plugin/hooks/session-start-checking-auto-update.json +1 -1
  107. package/packages/omo-codex/plugin/hooks/session-start-checking-bootstrap-provisioning.json +1 -1
  108. package/packages/omo-codex/plugin/hooks/session-start-loading-project-rules.json +1 -1
  109. package/packages/omo-codex/plugin/hooks/session-start-recording-session-telemetry.json +1 -1
  110. package/packages/omo-codex/plugin/hooks/stop-checking-ulw-execute-continuation.json +1 -1
  111. package/packages/omo-codex/plugin/hooks/stop-checking-ulw-loop-resume.json +1 -1
  112. package/packages/omo-codex/plugin/hooks/subagent-stop-checking-ulw-execute-continuation.json +1 -1
  113. package/packages/omo-codex/plugin/hooks/subagent-stop-verifying-lazycodex-executor-evidence.json +1 -1
  114. package/packages/omo-codex/plugin/hooks/user-prompt-submit-checking-ultrawork-trigger.json +1 -1
  115. package/packages/omo-codex/plugin/hooks/user-prompt-submit-checking-ulw-loop-steering.json +1 -1
  116. package/packages/omo-codex/plugin/hooks/user-prompt-submit-loading-project-rules.json +1 -1
  117. package/packages/omo-codex/plugin/package-lock.json +12 -146
  118. package/packages/omo-codex/plugin/package.json +1 -2
  119. package/packages/omo-codex/plugin/scripts/AGENTS.md +0 -1
  120. package/packages/omo-codex/plugin/scripts/auto-update-plan.mjs +0 -1
  121. package/packages/omo-codex/plugin/scripts/auto-update.mjs +0 -9
  122. package/packages/omo-codex/plugin/scripts/hook-status-message.mjs +0 -1
  123. package/packages/omo-codex/plugin/shared/src/config-loader.ts +2 -102
  124. package/packages/omo-codex/plugin/shared/src/config-migration.ts +0 -1
  125. package/packages/omo-codex/plugin/shared/test/config-loader.test.ts +41 -130
  126. package/packages/omo-codex/plugin/skills/init-deep/SKILL.md +13 -13
  127. package/packages/omo-codex/plugin/skills/lcx-doctor/SKILL.md +1 -1
  128. package/packages/omo-codex/plugin/skills/ultrawork/SKILL.md +2 -2
  129. package/packages/omo-codex/plugin/skills/ulw-execute/SKILL.md +11 -19
  130. package/packages/omo-codex/plugin/skills/ulw-loop/references/full-workflow.md +2 -1
  131. package/packages/omo-codex/plugin/skills/ulw-plan/SKILL.md +0 -1
  132. package/packages/omo-codex/plugin/test/AGENTS.md +2 -2
  133. package/packages/omo-codex/plugin/test/aggregate-hooks.test.mjs +2 -21
  134. package/packages/omo-codex/plugin/test/aggregate-manifest.test.mjs +1 -2
  135. package/packages/omo-codex/plugin/test/aggregate-mcp.test.mjs +2 -7
  136. package/packages/omo-codex/plugin/test/auto-update.test.mjs +0 -25
  137. package/packages/omo-codex/plugin/test/bootstrap-setup.test.mjs +0 -31
  138. package/packages/omo-codex/plugin/test/component-bundled-cli.test.mjs +0 -50
  139. package/packages/omo-codex/plugin/test/hook-status-message.test.mjs +0 -1
  140. package/packages/omo-codex/plugin/test/mcp-research-servers.test.mjs +1 -3
  141. package/packages/omo-codex/scripts/install-config-git-bash.test.mjs +0 -23
  142. package/packages/omo-codex/scripts/install-dist/install-local.mjs +227 -435
  143. package/packages/omo-codex/scripts/install-git-bash-mcp-env.test.mjs +1 -25
  144. package/packages/omo-codex/scripts/install-mcp-runtime.test.mjs +0 -36
  145. package/packages/shared-skills/skills/init-deep/SKILL.md +13 -13
  146. package/packages/shared-skills/skills/ulw-execute/SKILL.md +9 -9
  147. package/packages/shared-skills/skills/ulw-plan/SKILL.md +0 -1
  148. package/dist/config/schema/codegraph.d.ts +0 -12
  149. package/dist/hooks/codegraph-bootstrap/command-runner.d.ts +0 -17
  150. package/dist/hooks/codegraph-bootstrap/hook.d.ts +0 -35
  151. package/dist/hooks/codegraph-bootstrap/index.d.ts +0 -1
  152. package/dist/hooks/codegraph-bootstrap/project-root.d.ts +0 -1
  153. package/dist/hooks/codegraph-bootstrap/status.d.ts +0 -11
  154. package/dist/mcp/codegraph.d.ts +0 -16
  155. package/packages/omo-codex/plugin/components/codegraph/AGENTS.md +0 -58
  156. package/packages/omo-codex/plugin/components/codegraph/LICENSE +0 -21
  157. package/packages/omo-codex/plugin/components/codegraph/NODE-RUNTIME-LICENSES.md +0 -2951
  158. package/packages/omo-codex/plugin/components/codegraph/NOTICE +0 -16
  159. package/packages/omo-codex/plugin/components/codegraph/dist/cli.js +0 -12326
  160. package/packages/omo-codex/plugin/components/codegraph/dist/serve.js +0 -9908
  161. package/packages/omo-codex/plugin/components/codegraph/package.json +0 -33
  162. package/packages/omo-codex/plugin/components/codegraph/src/cache-gc.ts +0 -29
  163. package/packages/omo-codex/plugin/components/codegraph/src/cli.ts +0 -79
  164. package/packages/omo-codex/plugin/components/codegraph/src/hook-input.ts +0 -33
  165. package/packages/omo-codex/plugin/components/codegraph/src/hook-sweep.ts +0 -25
  166. package/packages/omo-codex/plugin/components/codegraph/src/hook-types.ts +0 -131
  167. package/packages/omo-codex/plugin/components/codegraph/src/hook.ts +0 -258
  168. package/packages/omo-codex/plugin/components/codegraph/src/mcp-bridge.ts +0 -310
  169. package/packages/omo-codex/plugin/components/codegraph/src/mcp-unavailable.ts +0 -92
  170. package/packages/omo-codex/plugin/components/codegraph/src/post-tool-use-hook.ts +0 -34
  171. package/packages/omo-codex/plugin/components/codegraph/src/serve-invocation.ts +0 -29
  172. package/packages/omo-codex/plugin/components/codegraph/src/serve.ts +0 -263
  173. package/packages/omo-codex/plugin/components/codegraph/src/session-start-command.ts +0 -106
  174. package/packages/omo-codex/plugin/components/codegraph/src/session-start-cooldown.ts +0 -145
  175. package/packages/omo-codex/plugin/components/codegraph/src/session-start-hook-runtime.ts +0 -22
  176. package/packages/omo-codex/plugin/components/codegraph/src/session-start-lock.ts +0 -139
  177. package/packages/omo-codex/plugin/components/codegraph/src/session-start-outcome.ts +0 -15
  178. package/packages/omo-codex/plugin/components/codegraph/src/session-start-paths.ts +0 -32
  179. package/packages/omo-codex/plugin/components/codegraph/src/session-start-project.ts +0 -109
  180. package/packages/omo-codex/plugin/components/codegraph/src/session-start-worker-result.ts +0 -148
  181. package/packages/omo-codex/plugin/components/codegraph/src/session-start-worker.ts +0 -175
  182. package/packages/omo-codex/plugin/components/codegraph/src/sweep-cli.ts +0 -81
  183. package/packages/omo-codex/plugin/components/codegraph/test/cache-gc.test.ts +0 -34
  184. package/packages/omo-codex/plugin/components/codegraph/test/hook-exclusion.test.ts +0 -112
  185. package/packages/omo-codex/plugin/components/codegraph/test/hook-registration.test.ts +0 -26
  186. package/packages/omo-codex/plugin/components/codegraph/test/hook-session-start-guard.test.ts +0 -160
  187. package/packages/omo-codex/plugin/components/codegraph/test/hook-store-upgrade.test.ts +0 -50
  188. package/packages/omo-codex/plugin/components/codegraph/test/hook-sweep.test.ts +0 -29
  189. package/packages/omo-codex/plugin/components/codegraph/test/hook.test.ts +0 -320
  190. package/packages/omo-codex/plugin/components/codegraph/test/mcp-bridge-fixtures.ts +0 -172
  191. package/packages/omo-codex/plugin/components/codegraph/test/package-runtime.test.ts +0 -91
  192. package/packages/omo-codex/plugin/components/codegraph/test/provisioned-node-guard.test.ts +0 -100
  193. package/packages/omo-codex/plugin/components/codegraph/test/serve-built-wrapper.test.ts +0 -76
  194. package/packages/omo-codex/plugin/components/codegraph/test/serve-mcp-bridge-lifecycle.test.ts +0 -133
  195. package/packages/omo-codex/plugin/components/codegraph/test/serve-mcp-bridge.test.ts +0 -245
  196. package/packages/omo-codex/plugin/components/codegraph/test/serve-mcp-facade.test.ts +0 -69
  197. package/packages/omo-codex/plugin/components/codegraph/test/serve-node-support.test.ts +0 -43
  198. package/packages/omo-codex/plugin/components/codegraph/test/serve-provision.test.ts +0 -147
  199. package/packages/omo-codex/plugin/components/codegraph/test/serve-unavailable-idle.test.ts +0 -47
  200. package/packages/omo-codex/plugin/components/codegraph/test/serve-unavailable.test.ts +0 -175
  201. package/packages/omo-codex/plugin/components/codegraph/test/serve.test.ts +0 -385
  202. package/packages/omo-codex/plugin/components/codegraph/test/session-start-node-support.test.ts +0 -206
  203. package/packages/omo-codex/plugin/components/codegraph/test/session-start-project.test.ts +0 -58
  204. package/packages/omo-codex/plugin/components/codegraph/test/session-start-state.test.ts +0 -104
  205. package/packages/omo-codex/plugin/components/codegraph/test/session-start-trust-boundary.test.ts +0 -65
  206. package/packages/omo-codex/plugin/components/codegraph/test/session-start-worker-availability.test.ts +0 -106
  207. package/packages/omo-codex/plugin/components/codegraph/test/session-start-worker-cooldown.test.ts +0 -116
  208. package/packages/omo-codex/plugin/components/codegraph/test/session-start-worker-flow.test.ts +0 -258
  209. package/packages/omo-codex/plugin/components/codegraph/test/sweep-cli.test.ts +0 -56
  210. package/packages/omo-codex/plugin/components/codegraph/tsconfig.build.json +0 -13
  211. package/packages/omo-codex/plugin/components/codegraph/tsconfig.json +0 -25
  212. package/packages/omo-codex/plugin/components/ulw-loop/test/quality-gate-aggregate.test.ts +0 -101
  213. package/packages/omo-codex/plugin/hooks/post-tool-use-checking-codegraph-init-guidance.json +0 -18
  214. package/packages/omo-codex/plugin/hooks/session-start-checking-codegraph-bootstrap.json +0 -17
  215. package/packages/omo-codex/plugin/scripts/migrate-omo-sot/editor.mjs +0 -125
  216. package/packages/omo-codex/plugin/scripts/migrate-omo-sot/jsonc.mjs +0 -43
  217. package/packages/omo-codex/plugin/scripts/migrate-omo-sot/scaffold.mjs +0 -29
  218. package/packages/omo-codex/plugin/scripts/migrate-omo-sot.mjs +0 -121
  219. package/packages/omo-codex/plugin/test/component-codegraph-mcp-smoke.test.mjs +0 -76
  220. package/packages/omo-codex/plugin/test/migrate-omo-sot.test.mjs +0 -179
@@ -114,6 +114,32 @@ describe("checkpointUlwLoop final story surface resolution", () => {
114
114
  });
115
115
 
116
116
  describe("checkpointUlwLoop final story", () => {
117
+ it("#given a mismatched codex objective and multiple gate defects #when completing the final checkpoint #then reports both validation domains", async () => {
118
+ const repo = await repoWith(
119
+ plan([passGoal("G001", { status: "complete" }), passGoal("G002")], { activeGoalId: "G002" }),
120
+ );
121
+ const gate = JSON.parse(await qualityGateJson(repo)) as Record<string, Record<string, unknown>>;
122
+ const manualQa = requiredSection(gate, "manualQa");
123
+ manualQa["surfaceEvidence"] = [];
124
+ requiredSection(gate, "gateReview")["recommendation"] = "REJECT";
125
+
126
+ try {
127
+ await checkpointUlwLoop(repo, {
128
+ goalId: "G002",
129
+ status: "complete",
130
+ evidence: "final work complete and validation passed",
131
+ codexGoalJson: snapshot("complete", "wrong objective"),
132
+ qualityGateJson: JSON.stringify(gate),
133
+ });
134
+ throw new Error("expected validation failure");
135
+ } catch (error) {
136
+ expect(error).toBeInstanceOf(Error);
137
+ if (!(error instanceof Error)) throw error;
138
+ expect(error.message).toContain("Codex goal objective mismatch");
139
+ expect(error.message).toContain("manualQa.surfaceEvidence");
140
+ expect(error.message).toContain("gateReview.recommendation");
141
+ }
142
+ });
117
143
  it("requires quality-gate-json for the final goal complete", async () => {
118
144
  const repo = await repoWith(
119
145
  plan([passGoal("G001", { status: "complete" }), passGoal("G002")], { activeGoalId: "G002" }),
@@ -1,10 +1,12 @@
1
- import { mkdtemp, rm } from "node:fs/promises";
1
+ import { existsSync, statSync } from "node:fs";
2
+ import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises";
2
3
  import { tmpdir } from "node:os";
3
- import { join } from "node:path";
4
+ import { join, resolve } from "node:path";
4
5
  import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
5
6
 
6
7
  import { ulwLoopCommand } from "../src/cli-commands.js";
7
8
  import { writePlan } from "../src/plan-io.js";
9
+ import { validateQualityGate } from "../src/quality-gate.js";
8
10
  import { goal, plan } from "./fixtures/checkpoint-builders.js";
9
11
 
10
12
  let repo: string;
@@ -32,7 +34,50 @@ async function run(args: string[]): Promise<Record<string, unknown>> {
32
34
  return JSON.parse(output.join(""));
33
35
  }
34
36
 
37
+ function fillTemplate(value: unknown): unknown {
38
+ if (typeof value === "string") {
39
+ return value.replace(/<replace:[^>]+>/g, "plausible evidence");
40
+ }
41
+ if (Array.isArray(value)) return value.map(fillTemplate);
42
+ if (value !== null && typeof value === "object") {
43
+ return Object.fromEntries(Object.entries(value).map(([key, item]) => [key, fillTemplate(item)]));
44
+ }
45
+ return value;
46
+ }
47
+
35
48
  describe("checkpoint --print-template", () => {
49
+ it("#given a printed template #when every placeholder is filled #then the quality gate validates", async () => {
50
+ await writePlan(
51
+ repo,
52
+ plan([goal({ id: "G001", attempt: 2 })], {
53
+ evidenceLayoutVersion: 2,
54
+ activeGoalId: "G001",
55
+ codexObjective: "Exact objective from goals.json",
56
+ }),
57
+ );
58
+ process.env["OMO_AGENT_TOOLKIT_SURFACE"] = "omo-senpi";
59
+ const result = await run(["--print-template", "--goal-id", "G001", "--json"]);
60
+ const template = fillTemplate(result["qualityGateTemplate"]) as Record<string, unknown>;
61
+ const paths = [
62
+ ...((template["manualQa"] as Record<string, unknown>)["artifactRefs"] as Array<Record<string, unknown>>),
63
+ ];
64
+ await Promise.all(
65
+ [
66
+ ...paths.map((artifact) => String(artifact["path"])),
67
+ String((template["gateReview"] as Record<string, unknown>)["reportPath"]),
68
+ ].map(async (path) => {
69
+ const absolute = resolve(repo, path);
70
+ await mkdir(resolve(absolute, ".."), { recursive: true });
71
+ await writeFile(absolute, "evidence");
72
+ }),
73
+ );
74
+ const validated = validateQualityGate(template, {
75
+ repoRoot: repo,
76
+ fs: { existsSync, statSync },
77
+ reviewerSurface: "omo-senpi",
78
+ });
79
+ expect(validated.gateReview.recommendation).toBe("APPROVE");
80
+ });
36
81
  it("#given an active senpi v2 plan #when printed with only goal-id #then emits the senpi gate skeleton", async () => {
37
82
  await writePlan(
38
83
  repo,
@@ -0,0 +1,258 @@
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
+ describe("aggregated quality gate basics", () => {
8
+ it("#given rejected recommendation, empty surface evidence, and invalid artifact kind #when validating #then reports all three defects", () => {
9
+ const gate = {
10
+ manualQa: {
11
+ by: "main-session",
12
+ status: "passed",
13
+ evidence: "qa evidence",
14
+ surfaceEvidence: [],
15
+ adversarialCases: [
16
+ {
17
+ id: "adv",
18
+ criterionRef: "C001",
19
+ scenario: "scenario",
20
+ expectedBehavior: "reject",
21
+ verdict: "passed",
22
+ artifactRefs: [],
23
+ },
24
+ ],
25
+ artifactRefs: [
26
+ {
27
+ id: "artifact",
28
+ kind: "invalid-kind",
29
+ description: "artifact",
30
+ path: "test/fixtures/artifacts/cli-pass.txt",
31
+ },
32
+ ],
33
+ },
34
+ gateReview: {
35
+ by: "category:deep",
36
+ recommendation: "REJECT",
37
+ evidence: "gate evidence",
38
+ reportPath: "test/fixtures/artifacts/gate-review.md",
39
+ blockers: [],
40
+ },
41
+ iteration: { fullRerun: true, status: "passed", rerunCommands: ["test"], evidence: "rerun" },
42
+ criteriaCoverage: {
43
+ totalCriteria: 1,
44
+ passCount: 1,
45
+ originalIntent: "intent",
46
+ desiredOutcome: "outcome",
47
+ userOutcomeReview: "review",
48
+ adversarialClassesCovered: ["none"],
49
+ },
50
+ };
51
+
52
+ try {
53
+ validateQualityGate(gate, { reviewerSurface: "omo-senpi" });
54
+ throw new Error("expected validation failure");
55
+ } catch (error) {
56
+ expect(error).toBeInstanceOf(UlwLoopError);
57
+ if (!(error instanceof UlwLoopError)) throw error;
58
+ const fields = error.details?.["fields"];
59
+ expect(Array.isArray(fields) ? fields.map((item) => item["field"]) : []).toEqual(
60
+ expect.arrayContaining([
61
+ "gateReview.recommendation",
62
+ "manualQa.surfaceEvidence",
63
+ "manualQa.artifactRefs[0].kind",
64
+ ]),
65
+ );
66
+ }
67
+ });
68
+
69
+ it("#given an empty existing artifact and another defect #when validating #then reports both defects", () => {
70
+ const gate = {
71
+ manualQa: {
72
+ by: "main-session",
73
+ status: "passed",
74
+ evidence: "qa evidence",
75
+ surfaceEvidence: [
76
+ {
77
+ id: "surface",
78
+ criterionRef: "C001",
79
+ surface: "cli",
80
+ invocation: "run",
81
+ verdict: "passed",
82
+ artifactRefs: ["empty"],
83
+ },
84
+ ],
85
+ adversarialCases: [
86
+ {
87
+ id: "adv",
88
+ criterionRef: "C001",
89
+ scenario: "scenario",
90
+ expectedBehavior: "reject",
91
+ verdict: "passed",
92
+ artifactRefs: [],
93
+ },
94
+ ],
95
+ artifactRefs: [{ id: "empty", kind: "cli-transcript", description: "empty", path: "empty.txt" }],
96
+ },
97
+ gateReview: {
98
+ by: "category:deep",
99
+ recommendation: "REJECT",
100
+ evidence: "gate evidence",
101
+ reportPath: "test/fixtures/artifacts/gate-review.md",
102
+ blockers: [],
103
+ },
104
+ iteration: { fullRerun: true, status: "passed", rerunCommands: ["test"], evidence: "rerun" },
105
+ criteriaCoverage: {
106
+ totalCriteria: 1,
107
+ passCount: 1,
108
+ originalIntent: "intent",
109
+ desiredOutcome: "outcome",
110
+ userOutcomeReview: "review",
111
+ adversarialClassesCovered: ["none"],
112
+ },
113
+ };
114
+
115
+ try {
116
+ validateQualityGate(gate, {
117
+ repoRoot: process.cwd(),
118
+ reviewerSurface: "omo-senpi",
119
+ fs: {
120
+ existsSync: (path) => path.endsWith("empty.txt") || path.endsWith("gate-review.md"),
121
+ statSync: () => ({ size: 0 }),
122
+ },
123
+ });
124
+ throw new Error("expected validation failure");
125
+ } catch (error) {
126
+ expect(error).toBeInstanceOf(UlwLoopError);
127
+ if (!(error instanceof UlwLoopError)) throw error;
128
+ const fields = error.details?.["fields"];
129
+ expect(Array.isArray(fields) ? fields.map((item) => item["field"]) : []).toEqual(
130
+ expect.arrayContaining(["manualQa.artifactRefs[0].path", "gateReview.recommendation"]),
131
+ );
132
+ }
133
+ });
134
+ it("#given four independent defects #when validating #then reports all defects in one error", () => {
135
+ const gate = {
136
+ manualQa: {
137
+ by: "main-session",
138
+ status: "passed",
139
+ evidence: "placeholder",
140
+ surfaceEvidence: [],
141
+ adversarialCases: [],
142
+ artifactRefs: [
143
+ { id: "missing", kind: "cli-transcript", description: "artifact", path: "does-not-exist.txt" },
144
+ ],
145
+ },
146
+ gateReview: {
147
+ by: "category:deep",
148
+ recommendation: "REJECT",
149
+ reportPath: "test/fixtures/artifacts/gate-review.md",
150
+ blockers: [],
151
+ },
152
+ iteration: { fullRerun: true, status: "passed", rerunCommands: ["test"], evidence: "rerun" },
153
+ criteriaCoverage: {
154
+ totalCriteria: 1,
155
+ passCount: 1,
156
+ originalIntent: "intent",
157
+ desiredOutcome: "outcome",
158
+ userOutcomeReview: "review",
159
+ adversarialClassesCovered: ["none"],
160
+ },
161
+ };
162
+ try {
163
+ validateQualityGate(gate, opts);
164
+ throw new Error("expected validation failure");
165
+ } catch (error) {
166
+ expect(error).toBeInstanceOf(UlwLoopError);
167
+ if (!(error instanceof UlwLoopError)) throw error;
168
+ expect(error.code).toBe("ULW_LOOP_QUALITY_GATE_INVALID");
169
+ expect(error.details?.["field"]).toBe("manualQa.evidence");
170
+ expect(error.details?.["fields"]).toEqual(
171
+ expect.arrayContaining([
172
+ { field: "manualQa.evidence", message: expect.stringContaining("placeholder") },
173
+ { field: "gateReview.evidence", message: expect.stringContaining("non-empty") },
174
+ { field: "gateReview.recommendation", message: expect.stringContaining("APPROVE") },
175
+ { field: "manualQa.artifactRefs[0].path", message: expect.stringContaining("existing") },
176
+ ]),
177
+ );
178
+ }
179
+ });
180
+
181
+ it("#given missing required fields #when validating #then reports each field in one error", () => {
182
+ const gate = {
183
+ manualQa: { evidence: "qa", artifactRefs: [] },
184
+ gateReview: { by: "invalid", recommendation: "REJECT", evidence: "", reportPath: "", blockers: ["blocker"] },
185
+ iteration: { fullRerun: false, status: "failed", rerunCommands: [], evidence: "" },
186
+ criteriaCoverage: {
187
+ totalCriteria: "bad",
188
+ passCount: 0,
189
+ originalIntent: "",
190
+ desiredOutcome: "",
191
+ userOutcomeReview: "",
192
+ adversarialClassesCovered: [],
193
+ },
194
+ };
195
+ expect(() => validateQualityGate(gate, { reviewerSurface: "omo-senpi" })).toThrow(/gateReview\.by/);
196
+ try {
197
+ validateQualityGate(gate, { reviewerSurface: "omo-senpi" });
198
+ } catch (error) {
199
+ expect(error).toBeInstanceOf(UlwLoopError);
200
+ if (!(error instanceof UlwLoopError)) throw error;
201
+ const fields = error.details?.["fields"];
202
+ expect(Array.isArray(fields) ? fields.map((field) => field["field"]) : []).toEqual(
203
+ expect.arrayContaining([
204
+ "gateReview.by",
205
+ "iteration.status",
206
+ "criteriaCoverage.totalCriteria",
207
+ "manualQa.artifactRefs",
208
+ ]),
209
+ );
210
+ }
211
+ });
212
+
213
+ it("#given more than twenty-five defects #when validating #then caps the list and flags truncation", () => {
214
+ const refs = Array.from({ length: 30 }, (_, index) => ({
215
+ id: `ref-${index}`,
216
+ kind: "cli-transcript",
217
+ description: "artifact",
218
+ path: `does-not-exist-${index}.txt`,
219
+ }));
220
+ const gate = {
221
+ manualQa: {
222
+ by: "main-session",
223
+ status: "passed",
224
+ evidence: "real evidence",
225
+ surfaceEvidence: [],
226
+ adversarialCases: [],
227
+ artifactRefs: refs,
228
+ },
229
+ gateReview: {
230
+ by: "category:deep",
231
+ recommendation: "APPROVE",
232
+ reportPath: "test/fixtures/artifacts/gate-review.md",
233
+ evidence: "gate evidence",
234
+ blockers: [],
235
+ },
236
+ iteration: { fullRerun: true, status: "passed", rerunCommands: ["test"], evidence: "rerun" },
237
+ criteriaCoverage: {
238
+ totalCriteria: 1,
239
+ passCount: 1,
240
+ originalIntent: "intent",
241
+ desiredOutcome: "outcome",
242
+ userOutcomeReview: "review",
243
+ adversarialClassesCovered: ["none"],
244
+ },
245
+ };
246
+ try {
247
+ validateQualityGate(gate, opts);
248
+ throw new Error("expected validation failure");
249
+ } catch (error) {
250
+ expect(error).toBeInstanceOf(UlwLoopError);
251
+ if (!(error instanceof UlwLoopError)) throw error;
252
+ const fields = error.details?.["fields"];
253
+ expect(Array.isArray(fields) && fields.length).toBe(25);
254
+ expect(error.details?.["truncated"]).toBe(true);
255
+ expect(error.message).toContain("25+");
256
+ }
257
+ });
258
+ });
@@ -0,0 +1,22 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { throwQualityGateDefects } from "../src/quality-gate-fields.js";
3
+ import { UlwLoopError } from "../src/types.js";
4
+
5
+ describe("quality gate cap and dedupe", () => {
6
+ it("#given duplicate defects beyond the cap #when formatting #then deduplicates before capping", () => {
7
+ const defects = Array.from({ length: 30 }, (_, index) => ({
8
+ field: index < 10 ? "duplicate" : `field-${index - 10}`,
9
+ message: index < 10 ? "same defect" : `defect-${index - 10}`,
10
+ }));
11
+ try {
12
+ throwQualityGateDefects(defects);
13
+ throw new Error("expected validation failure");
14
+ } catch (error) {
15
+ expect(error).toBeInstanceOf(UlwLoopError);
16
+ if (!(error instanceof UlwLoopError)) throw error;
17
+ const fields = error.details?.["fields"];
18
+ expect(Array.isArray(fields) ? fields.length : 0).toBe(21);
19
+ expect(error.details?.["truncated"]).toBeUndefined();
20
+ }
21
+ });
22
+ });
@@ -0,0 +1,97 @@
1
+ import { existsSync } 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 shared = {
7
+ gateReview: {
8
+ by: "category:deep",
9
+ recommendation: "APPROVE",
10
+ evidence: "review",
11
+ reportPath: "report.txt",
12
+ blockers: [],
13
+ },
14
+ iteration: { fullRerun: true, status: "passed", rerunCommands: ["test"], evidence: "rerun" },
15
+ criteriaCoverage: {
16
+ totalCriteria: 1,
17
+ passCount: 1,
18
+ originalIntent: "intent",
19
+ desiredOutcome: "outcome",
20
+ userOutcomeReview: "review",
21
+ adversarialClassesCovered: ["none"],
22
+ },
23
+ };
24
+ function fields(gate: Record<string, unknown>, withFs = false): readonly string[] {
25
+ try {
26
+ validateQualityGate(gate, {
27
+ reviewerSurface: "omo-senpi",
28
+ ...(withFs ? { repoRoot: process.cwd(), fs: { existsSync, statSync: () => ({ size: 1 }) } } : {}),
29
+ });
30
+ } catch (error) {
31
+ if (error instanceof UlwLoopError) {
32
+ const list = error.details?.["fields"];
33
+ return Array.isArray(list) ? list.map((item) => `${String(item["field"])}: ${String(item["message"])}`) : [];
34
+ }
35
+ throw error;
36
+ }
37
+ throw new Error("expected validation failure");
38
+ }
39
+ function manual(overrides: Record<string, unknown> = {}): Record<string, unknown> {
40
+ return {
41
+ by: "main-session",
42
+ status: "passed",
43
+ evidence: "qa",
44
+ surfaceEvidence: [],
45
+ adversarialCases: [],
46
+ artifactRefs: [{ id: "a", kind: "log", description: "x", path: "x.txt" }],
47
+ ...overrides,
48
+ };
49
+ }
50
+ describe("quality gate poisoning contract", () => {
51
+ it.each([
52
+ [
53
+ "invalid artifact kind",
54
+ [{ id: "a", kind: "invalid-kind", description: "x", path: "x.txt" }],
55
+ (list: readonly string[]) => {
56
+ expect(list.some((x) => x.startsWith("manualQa.artifactRefs[0].kind:"))).toBe(true);
57
+ expect(list.some((x) => x.includes("references unknown artifact a"))).toBe(false);
58
+ },
59
+ ],
60
+ [
61
+ "duplicate id with invalid kind",
62
+ [
63
+ { id: "a", kind: "cli-transcript", description: "x", path: "x.txt" },
64
+ { id: "a", kind: "invalid-kind", description: "x", path: "x.txt" },
65
+ ],
66
+ (list: readonly string[]) => {
67
+ expect(list.some((x) => x.startsWith("manualQa.artifactRefs[1].kind:"))).toBe(true);
68
+ expect(list.some((x) => x.includes("duplicate a"))).toBe(true);
69
+ },
70
+ ],
71
+ ] as const)("#given %s #when validating #then preserves independent defects", (_name, artifactRefs, assertResult) =>
72
+ assertResult(fields({ ...shared, manualQa: manual({ artifactRefs }) })),
73
+ );
74
+ it("#given a poisoned numeric input #when validating #then skips its cross-field comparison", () => {
75
+ const list = fields({
76
+ ...shared,
77
+ criteriaCoverage: { ...shared.criteriaCoverage, passCount: "bad" },
78
+ manualQa: manual(),
79
+ });
80
+ expect(list.some((x) => x.includes("passCount") && x.includes("numeric"))).toBe(true);
81
+ expect(list.some((x) => x.includes("cover totalCriteria"))).toBe(false);
82
+ });
83
+ it("#given poisoned parent rows #when validating #then suppresses all child defects", () => {
84
+ const list = fields({ ...shared, manualQa: manual({ surfaceEvidence: [null], adversarialCases: [null] }) });
85
+ expect(list.some((x) => x.startsWith("manualQa.surfaceEvidence[0]:"))).toBe(true);
86
+ expect(list.some((x) => x.startsWith("manualQa.surfaceEvidence[0]."))).toBe(false);
87
+ expect(list.some((x) => x.startsWith("manualQa.adversarialCases[0]:"))).toBe(true);
88
+ });
89
+ it("#given a poisoned report path #when validating with fs options #then skips file checks", () => {
90
+ const list = fields(
91
+ { ...shared, gateReview: { ...shared.gateReview, reportPath: "tbd" }, manualQa: manual() },
92
+ true,
93
+ );
94
+ expect(list.some((x) => x.includes("reportPath") && x.includes("placeholder"))).toBe(true);
95
+ expect(list.some((x) => x.includes("gateReview.reportPath") && x.includes("existing artifact"))).toBe(false);
96
+ });
97
+ });