lazycodex-ai 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 (189) hide show
  1. package/README.ja.md +1 -1
  2. package/README.ko.md +1 -1
  3. package/README.md +1 -1
  4. package/README.ru.md +1 -1
  5. package/README.zh-cn.md +1 -1
  6. package/dist/cli/index.js +48341 -48541
  7. package/dist/cli-node/index.js +49016 -49216
  8. package/package.json +1 -6
  9. package/packages/omo-codex/plugin/.codex-plugin/plugin.json +1 -3
  10. package/packages/omo-codex/plugin/.mcp.json +0 -6
  11. package/packages/omo-codex/plugin/AGENTS.md +5 -5
  12. package/packages/omo-codex/plugin/README.md +1 -1
  13. package/packages/omo-codex/plugin/components/bootstrap/dist/cli.js +1 -15
  14. package/packages/omo-codex/plugin/components/bootstrap/hooks/hooks.json +1 -1
  15. package/packages/omo-codex/plugin/components/bootstrap/package.json +1 -1
  16. package/packages/omo-codex/plugin/components/comment-checker/hooks/hooks.json +1 -1
  17. package/packages/omo-codex/plugin/components/comment-checker/package.json +1 -1
  18. package/packages/omo-codex/plugin/components/git-bash/hooks/hooks.json +2 -2
  19. package/packages/omo-codex/plugin/components/git-bash/package.json +1 -1
  20. package/packages/omo-codex/plugin/components/lazycodex-executor-verify/hooks/hooks.json +1 -1
  21. package/packages/omo-codex/plugin/components/lazycodex-executor-verify/package.json +1 -1
  22. package/packages/omo-codex/plugin/components/lcx/skills/lcx-doctor/SKILL.md +1 -1
  23. package/packages/omo-codex/plugin/components/lsp/dist/.omo-runtime-manifest.json +2 -2
  24. package/packages/omo-codex/plugin/components/lsp/hooks/hooks.json +2 -2
  25. package/packages/omo-codex/plugin/components/lsp/package.json +1 -1
  26. package/packages/omo-codex/plugin/components/rules/hooks/hooks.json +4 -4
  27. package/packages/omo-codex/plugin/components/rules/package.json +1 -1
  28. package/packages/omo-codex/plugin/components/teammode/hooks/hooks.json +1 -1
  29. package/packages/omo-codex/plugin/components/teammode/package.json +1 -1
  30. package/packages/omo-codex/plugin/components/teammode/test/thread-title-hook.test.ts +1 -1
  31. package/packages/omo-codex/plugin/components/telemetry/hooks/hooks.json +1 -1
  32. package/packages/omo-codex/plugin/components/telemetry/package.json +1 -1
  33. package/packages/omo-codex/plugin/components/ultrawork/dist/cli.js +2 -2
  34. package/packages/omo-codex/plugin/components/ultrawork/hooks/hooks.json +1 -1
  35. package/packages/omo-codex/plugin/components/ultrawork/package.json +1 -1
  36. package/packages/omo-codex/plugin/components/ultrawork/skills/ulw-plan/SKILL.md +0 -1
  37. package/packages/omo-codex/plugin/components/ultrawork/src/directive-content.ts +1 -1
  38. package/packages/omo-codex/plugin/components/ulw-execute-continuation/hooks/hooks.json +2 -2
  39. package/packages/omo-codex/plugin/components/ulw-execute-continuation/package.json +1 -1
  40. package/packages/omo-codex/plugin/components/ulw-loop/README.md +11 -1
  41. package/packages/omo-codex/plugin/components/ulw-loop/directive.md +2 -2
  42. package/packages/omo-codex/plugin/components/ulw-loop/dist/checkpoint-codex-validation.d.ts +12 -0
  43. package/packages/omo-codex/plugin/components/ulw-loop/dist/checkpoint-codex-validation.js +45 -0
  44. package/packages/omo-codex/plugin/components/ulw-loop/dist/checkpoint-reconciliation.d.ts +1 -0
  45. package/packages/omo-codex/plugin/components/ulw-loop/dist/checkpoint-reconciliation.js +5 -0
  46. package/packages/omo-codex/plugin/components/ulw-loop/dist/checkpoint-template.js +10 -3
  47. package/packages/omo-codex/plugin/components/ulw-loop/dist/checkpoint.js +35 -38
  48. package/packages/omo-codex/plugin/components/ulw-loop/dist/cli.js +278 -193
  49. package/packages/omo-codex/plugin/components/ulw-loop/dist/quality-gate-aggregate.d.ts +5 -0
  50. package/packages/omo-codex/plugin/components/ulw-loop/dist/quality-gate-aggregate.js +7 -51
  51. package/packages/omo-codex/plugin/components/ulw-loop/dist/quality-gate-artifacts.js +39 -22
  52. package/packages/omo-codex/plugin/components/ulw-loop/dist/quality-gate-fields.d.ts +13 -1
  53. package/packages/omo-codex/plugin/components/ulw-loop/dist/quality-gate-fields.js +64 -8
  54. package/packages/omo-codex/plugin/components/ulw-loop/dist/quality-gate-verdicts.js +5 -1
  55. package/packages/omo-codex/plugin/components/ulw-loop/dist/quality-gate.js +37 -18
  56. package/packages/omo-codex/plugin/components/ulw-loop/hooks/hooks.json +4 -4
  57. package/packages/omo-codex/plugin/components/ulw-loop/package.json +1 -1
  58. package/packages/omo-codex/plugin/components/ulw-loop/skills/ulw-loop/references/full-workflow.md +2 -1
  59. package/packages/omo-codex/plugin/components/ulw-loop/src/checkpoint-codex-validation.ts +81 -0
  60. package/packages/omo-codex/plugin/components/ulw-loop/src/checkpoint-reconciliation.ts +6 -0
  61. package/packages/omo-codex/plugin/components/ulw-loop/src/checkpoint-template.ts +10 -3
  62. package/packages/omo-codex/plugin/components/ulw-loop/src/checkpoint.ts +32 -63
  63. package/packages/omo-codex/plugin/components/ulw-loop/src/quality-gate-aggregate.ts +7 -53
  64. package/packages/omo-codex/plugin/components/ulw-loop/src/quality-gate-artifacts.ts +50 -31
  65. package/packages/omo-codex/plugin/components/ulw-loop/src/quality-gate-fields.ts +67 -14
  66. package/packages/omo-codex/plugin/components/ulw-loop/src/quality-gate-verdicts.ts +6 -1
  67. package/packages/omo-codex/plugin/components/ulw-loop/src/quality-gate.ts +36 -17
  68. package/packages/omo-codex/plugin/components/ulw-loop/test/checkpoint-final.test.ts +26 -0
  69. package/packages/omo-codex/plugin/components/ulw-loop/test/checkpoint-template.test.ts +47 -2
  70. package/packages/omo-codex/plugin/components/ulw-loop/test/quality-gate-aggregate-basics.test.ts +258 -0
  71. package/packages/omo-codex/plugin/components/ulw-loop/test/quality-gate-cap-and-dedupe.test.ts +22 -0
  72. package/packages/omo-codex/plugin/components/ulw-loop/test/quality-gate-poisoning-cascades.test.ts +97 -0
  73. package/packages/omo-codex/plugin/components/ulw-loop/test/quality-gate-single-report.test.ts +241 -0
  74. package/packages/omo-codex/plugin/hooks/post-compact-resetting-git-bash-mcp-reminder.json +1 -1
  75. package/packages/omo-codex/plugin/hooks/post-compact-resetting-lsp-diagnostics-cache.json +1 -1
  76. package/packages/omo-codex/plugin/hooks/post-compact-resetting-project-rule-cache.json +1 -1
  77. package/packages/omo-codex/plugin/hooks/post-tool-use-checking-comments.json +1 -1
  78. package/packages/omo-codex/plugin/hooks/post-tool-use-checking-lsp-diagnostics.json +1 -1
  79. package/packages/omo-codex/plugin/hooks/post-tool-use-checking-thread-title-hygiene.json +1 -1
  80. package/packages/omo-codex/plugin/hooks/post-tool-use-matching-project-rules.json +1 -1
  81. package/packages/omo-codex/plugin/hooks/pre-tool-use-enforcing-unlimited-goal-budget.json +1 -1
  82. package/packages/omo-codex/plugin/hooks/pre-tool-use-guarding-ulw-loop-spawns.json +1 -1
  83. package/packages/omo-codex/plugin/hooks/pre-tool-use-recommending-git-bash-mcp.json +1 -1
  84. package/packages/omo-codex/plugin/hooks/session-start-checking-auto-update.json +1 -1
  85. package/packages/omo-codex/plugin/hooks/session-start-checking-bootstrap-provisioning.json +1 -1
  86. package/packages/omo-codex/plugin/hooks/session-start-loading-project-rules.json +1 -1
  87. package/packages/omo-codex/plugin/hooks/session-start-recording-session-telemetry.json +1 -1
  88. package/packages/omo-codex/plugin/hooks/stop-checking-ulw-execute-continuation.json +1 -1
  89. package/packages/omo-codex/plugin/hooks/stop-checking-ulw-loop-resume.json +1 -1
  90. package/packages/omo-codex/plugin/hooks/subagent-stop-checking-ulw-execute-continuation.json +1 -1
  91. package/packages/omo-codex/plugin/hooks/subagent-stop-verifying-lazycodex-executor-evidence.json +1 -1
  92. package/packages/omo-codex/plugin/hooks/user-prompt-submit-checking-ultrawork-trigger.json +1 -1
  93. package/packages/omo-codex/plugin/hooks/user-prompt-submit-checking-ulw-loop-steering.json +1 -1
  94. package/packages/omo-codex/plugin/hooks/user-prompt-submit-loading-project-rules.json +1 -1
  95. package/packages/omo-codex/plugin/package-lock.json +12 -146
  96. package/packages/omo-codex/plugin/package.json +1 -2
  97. package/packages/omo-codex/plugin/scripts/AGENTS.md +0 -1
  98. package/packages/omo-codex/plugin/scripts/auto-update-plan.mjs +0 -1
  99. package/packages/omo-codex/plugin/scripts/auto-update.mjs +0 -9
  100. package/packages/omo-codex/plugin/scripts/hook-status-message.mjs +0 -1
  101. package/packages/omo-codex/plugin/shared/src/config-loader.ts +2 -102
  102. package/packages/omo-codex/plugin/shared/src/config-migration.ts +0 -1
  103. package/packages/omo-codex/plugin/shared/test/config-loader.test.ts +41 -130
  104. package/packages/omo-codex/plugin/skills/init-deep/SKILL.md +13 -13
  105. package/packages/omo-codex/plugin/skills/lcx-doctor/SKILL.md +1 -1
  106. package/packages/omo-codex/plugin/skills/ultrawork/SKILL.md +2 -2
  107. package/packages/omo-codex/plugin/skills/ulw-execute/SKILL.md +11 -19
  108. package/packages/omo-codex/plugin/skills/ulw-loop/references/full-workflow.md +2 -1
  109. package/packages/omo-codex/plugin/skills/ulw-plan/SKILL.md +0 -1
  110. package/packages/omo-codex/plugin/test/AGENTS.md +2 -2
  111. package/packages/omo-codex/plugin/test/aggregate-hooks.test.mjs +2 -21
  112. package/packages/omo-codex/plugin/test/aggregate-manifest.test.mjs +1 -2
  113. package/packages/omo-codex/plugin/test/aggregate-mcp.test.mjs +2 -7
  114. package/packages/omo-codex/plugin/test/auto-update.test.mjs +0 -25
  115. package/packages/omo-codex/plugin/test/bootstrap-setup.test.mjs +0 -31
  116. package/packages/omo-codex/plugin/test/component-bundled-cli.test.mjs +0 -50
  117. package/packages/omo-codex/plugin/test/hook-status-message.test.mjs +0 -1
  118. package/packages/omo-codex/plugin/test/mcp-research-servers.test.mjs +1 -3
  119. package/packages/omo-codex/scripts/install-dist/install-local.mjs +227 -435
  120. package/packages/prompts-core/prompts/ultrawork/codex.md +2 -2
  121. package/packages/shared-skills/skills/init-deep/SKILL.md +13 -13
  122. package/packages/shared-skills/skills/ulw-execute/SKILL.md +9 -9
  123. package/packages/shared-skills/skills/ulw-plan/SKILL.md +0 -1
  124. package/packages/omo-codex/plugin/components/codegraph/AGENTS.md +0 -58
  125. package/packages/omo-codex/plugin/components/codegraph/LICENSE +0 -21
  126. package/packages/omo-codex/plugin/components/codegraph/NODE-RUNTIME-LICENSES.md +0 -2951
  127. package/packages/omo-codex/plugin/components/codegraph/NOTICE +0 -16
  128. package/packages/omo-codex/plugin/components/codegraph/dist/cli.js +0 -12326
  129. package/packages/omo-codex/plugin/components/codegraph/dist/serve.js +0 -9908
  130. package/packages/omo-codex/plugin/components/codegraph/package.json +0 -33
  131. package/packages/omo-codex/plugin/components/codegraph/src/cache-gc.ts +0 -29
  132. package/packages/omo-codex/plugin/components/codegraph/src/cli.ts +0 -79
  133. package/packages/omo-codex/plugin/components/codegraph/src/hook-input.ts +0 -33
  134. package/packages/omo-codex/plugin/components/codegraph/src/hook-sweep.ts +0 -25
  135. package/packages/omo-codex/plugin/components/codegraph/src/hook-types.ts +0 -131
  136. package/packages/omo-codex/plugin/components/codegraph/src/hook.ts +0 -258
  137. package/packages/omo-codex/plugin/components/codegraph/src/mcp-bridge.ts +0 -310
  138. package/packages/omo-codex/plugin/components/codegraph/src/mcp-unavailable.ts +0 -92
  139. package/packages/omo-codex/plugin/components/codegraph/src/post-tool-use-hook.ts +0 -34
  140. package/packages/omo-codex/plugin/components/codegraph/src/serve-invocation.ts +0 -29
  141. package/packages/omo-codex/plugin/components/codegraph/src/serve.ts +0 -263
  142. package/packages/omo-codex/plugin/components/codegraph/src/session-start-command.ts +0 -106
  143. package/packages/omo-codex/plugin/components/codegraph/src/session-start-cooldown.ts +0 -145
  144. package/packages/omo-codex/plugin/components/codegraph/src/session-start-hook-runtime.ts +0 -22
  145. package/packages/omo-codex/plugin/components/codegraph/src/session-start-lock.ts +0 -139
  146. package/packages/omo-codex/plugin/components/codegraph/src/session-start-outcome.ts +0 -15
  147. package/packages/omo-codex/plugin/components/codegraph/src/session-start-paths.ts +0 -32
  148. package/packages/omo-codex/plugin/components/codegraph/src/session-start-project.ts +0 -109
  149. package/packages/omo-codex/plugin/components/codegraph/src/session-start-worker-result.ts +0 -148
  150. package/packages/omo-codex/plugin/components/codegraph/src/session-start-worker.ts +0 -175
  151. package/packages/omo-codex/plugin/components/codegraph/src/sweep-cli.ts +0 -81
  152. package/packages/omo-codex/plugin/components/codegraph/test/cache-gc.test.ts +0 -34
  153. package/packages/omo-codex/plugin/components/codegraph/test/hook-exclusion.test.ts +0 -112
  154. package/packages/omo-codex/plugin/components/codegraph/test/hook-registration.test.ts +0 -26
  155. package/packages/omo-codex/plugin/components/codegraph/test/hook-session-start-guard.test.ts +0 -160
  156. package/packages/omo-codex/plugin/components/codegraph/test/hook-store-upgrade.test.ts +0 -50
  157. package/packages/omo-codex/plugin/components/codegraph/test/hook-sweep.test.ts +0 -29
  158. package/packages/omo-codex/plugin/components/codegraph/test/hook.test.ts +0 -320
  159. package/packages/omo-codex/plugin/components/codegraph/test/mcp-bridge-fixtures.ts +0 -172
  160. package/packages/omo-codex/plugin/components/codegraph/test/package-runtime.test.ts +0 -91
  161. package/packages/omo-codex/plugin/components/codegraph/test/provisioned-node-guard.test.ts +0 -100
  162. package/packages/omo-codex/plugin/components/codegraph/test/serve-built-wrapper.test.ts +0 -76
  163. package/packages/omo-codex/plugin/components/codegraph/test/serve-mcp-bridge-lifecycle.test.ts +0 -133
  164. package/packages/omo-codex/plugin/components/codegraph/test/serve-mcp-bridge.test.ts +0 -245
  165. package/packages/omo-codex/plugin/components/codegraph/test/serve-mcp-facade.test.ts +0 -69
  166. package/packages/omo-codex/plugin/components/codegraph/test/serve-node-support.test.ts +0 -43
  167. package/packages/omo-codex/plugin/components/codegraph/test/serve-provision.test.ts +0 -147
  168. package/packages/omo-codex/plugin/components/codegraph/test/serve-unavailable-idle.test.ts +0 -47
  169. package/packages/omo-codex/plugin/components/codegraph/test/serve-unavailable.test.ts +0 -175
  170. package/packages/omo-codex/plugin/components/codegraph/test/serve.test.ts +0 -385
  171. package/packages/omo-codex/plugin/components/codegraph/test/session-start-node-support.test.ts +0 -206
  172. package/packages/omo-codex/plugin/components/codegraph/test/session-start-project.test.ts +0 -58
  173. package/packages/omo-codex/plugin/components/codegraph/test/session-start-state.test.ts +0 -104
  174. package/packages/omo-codex/plugin/components/codegraph/test/session-start-trust-boundary.test.ts +0 -65
  175. package/packages/omo-codex/plugin/components/codegraph/test/session-start-worker-availability.test.ts +0 -106
  176. package/packages/omo-codex/plugin/components/codegraph/test/session-start-worker-cooldown.test.ts +0 -116
  177. package/packages/omo-codex/plugin/components/codegraph/test/session-start-worker-flow.test.ts +0 -258
  178. package/packages/omo-codex/plugin/components/codegraph/test/sweep-cli.test.ts +0 -56
  179. package/packages/omo-codex/plugin/components/codegraph/tsconfig.build.json +0 -13
  180. package/packages/omo-codex/plugin/components/codegraph/tsconfig.json +0 -25
  181. package/packages/omo-codex/plugin/components/ulw-loop/test/quality-gate-aggregate.test.ts +0 -101
  182. package/packages/omo-codex/plugin/hooks/post-tool-use-checking-codegraph-init-guidance.json +0 -18
  183. package/packages/omo-codex/plugin/hooks/session-start-checking-codegraph-bootstrap.json +0 -17
  184. package/packages/omo-codex/plugin/scripts/migrate-omo-sot/editor.mjs +0 -125
  185. package/packages/omo-codex/plugin/scripts/migrate-omo-sot/jsonc.mjs +0 -43
  186. package/packages/omo-codex/plugin/scripts/migrate-omo-sot/scaffold.mjs +0 -29
  187. package/packages/omo-codex/plugin/scripts/migrate-omo-sot.mjs +0 -121
  188. package/packages/omo-codex/plugin/test/component-codegraph-mcp-smoke.test.mjs +0 -76
  189. 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
+ });