oh-my-opencode 5.0.0-beta.33 → 5.0.0-beta.35

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 (221) 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 +48350 -48559
  11. package/dist/cli-node/index.js +48480 -48689
  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 -13742
  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-plan/SKILL.md +0 -1
  27. package/dist/tui.js +44809 -44910
  28. package/package.json +16 -18
  29. package/packages/omo-codex/THIRD-PARTY-NOTICES.md +0 -17
  30. package/packages/omo-codex/plugin/.codex-plugin/plugin.json +1 -3
  31. package/packages/omo-codex/plugin/.mcp.json +0 -6
  32. package/packages/omo-codex/plugin/AGENTS.md +5 -5
  33. package/packages/omo-codex/plugin/README.md +1 -1
  34. package/packages/omo-codex/plugin/components/bootstrap/dist/cli.js +1 -15
  35. package/packages/omo-codex/plugin/components/bootstrap/hooks/hooks.json +1 -1
  36. package/packages/omo-codex/plugin/components/bootstrap/package.json +1 -1
  37. package/packages/omo-codex/plugin/components/comment-checker/hooks/hooks.json +1 -1
  38. package/packages/omo-codex/plugin/components/comment-checker/package.json +1 -1
  39. package/packages/omo-codex/plugin/components/git-bash/hooks/hooks.json +2 -2
  40. package/packages/omo-codex/plugin/components/git-bash/package.json +1 -1
  41. package/packages/omo-codex/plugin/components/lazycodex-executor-verify/hooks/hooks.json +1 -1
  42. package/packages/omo-codex/plugin/components/lazycodex-executor-verify/package.json +1 -1
  43. package/packages/omo-codex/plugin/components/lcx/skills/lcx-doctor/SKILL.md +1 -1
  44. package/packages/omo-codex/plugin/components/lsp/dist/.omo-runtime-manifest.json +2 -2
  45. package/packages/omo-codex/plugin/components/lsp/hooks/hooks.json +2 -2
  46. package/packages/omo-codex/plugin/components/lsp/package.json +1 -1
  47. package/packages/omo-codex/plugin/components/rules/hooks/hooks.json +4 -4
  48. package/packages/omo-codex/plugin/components/rules/package.json +1 -1
  49. package/packages/omo-codex/plugin/components/teammode/hooks/hooks.json +1 -1
  50. package/packages/omo-codex/plugin/components/teammode/package.json +1 -1
  51. package/packages/omo-codex/plugin/components/teammode/test/thread-title-hook.test.ts +1 -1
  52. package/packages/omo-codex/plugin/components/telemetry/hooks/hooks.json +1 -1
  53. package/packages/omo-codex/plugin/components/telemetry/package.json +1 -1
  54. package/packages/omo-codex/plugin/components/ultrawork/dist/cli.js +2 -2
  55. package/packages/omo-codex/plugin/components/ultrawork/hooks/hooks.json +1 -1
  56. package/packages/omo-codex/plugin/components/ultrawork/package.json +1 -1
  57. package/packages/omo-codex/plugin/components/ultrawork/skills/ulw-plan/SKILL.md +0 -1
  58. package/packages/omo-codex/plugin/components/ultrawork/src/directive-content.ts +1 -1
  59. package/packages/omo-codex/plugin/components/ulw-execute-continuation/hooks/hooks.json +2 -2
  60. package/packages/omo-codex/plugin/components/ulw-execute-continuation/package.json +1 -1
  61. package/packages/omo-codex/plugin/components/ulw-loop/README.md +11 -1
  62. package/packages/omo-codex/plugin/components/ulw-loop/directive.md +2 -2
  63. package/packages/omo-codex/plugin/components/ulw-loop/dist/checkpoint-codex-validation.d.ts +12 -0
  64. package/packages/omo-codex/plugin/components/ulw-loop/dist/checkpoint-codex-validation.js +45 -0
  65. package/packages/omo-codex/plugin/components/ulw-loop/dist/checkpoint-reconciliation.d.ts +1 -0
  66. package/packages/omo-codex/plugin/components/ulw-loop/dist/checkpoint-reconciliation.js +5 -0
  67. package/packages/omo-codex/plugin/components/ulw-loop/dist/checkpoint-template.js +10 -3
  68. package/packages/omo-codex/plugin/components/ulw-loop/dist/checkpoint.js +35 -38
  69. package/packages/omo-codex/plugin/components/ulw-loop/dist/cli.js +278 -193
  70. package/packages/omo-codex/plugin/components/ulw-loop/dist/quality-gate-aggregate.d.ts +5 -0
  71. package/packages/omo-codex/plugin/components/ulw-loop/dist/quality-gate-aggregate.js +7 -51
  72. package/packages/omo-codex/plugin/components/ulw-loop/dist/quality-gate-artifacts.js +39 -22
  73. package/packages/omo-codex/plugin/components/ulw-loop/dist/quality-gate-fields.d.ts +13 -1
  74. package/packages/omo-codex/plugin/components/ulw-loop/dist/quality-gate-fields.js +64 -8
  75. package/packages/omo-codex/plugin/components/ulw-loop/dist/quality-gate-verdicts.js +5 -1
  76. package/packages/omo-codex/plugin/components/ulw-loop/dist/quality-gate.js +37 -18
  77. package/packages/omo-codex/plugin/components/ulw-loop/hooks/hooks.json +4 -4
  78. package/packages/omo-codex/plugin/components/ulw-loop/package.json +1 -1
  79. package/packages/omo-codex/plugin/components/ulw-loop/skills/ulw-loop/references/full-workflow.md +2 -1
  80. package/packages/omo-codex/plugin/components/ulw-loop/src/checkpoint-codex-validation.ts +81 -0
  81. package/packages/omo-codex/plugin/components/ulw-loop/src/checkpoint-reconciliation.ts +6 -0
  82. package/packages/omo-codex/plugin/components/ulw-loop/src/checkpoint-template.ts +10 -3
  83. package/packages/omo-codex/plugin/components/ulw-loop/src/checkpoint.ts +32 -63
  84. package/packages/omo-codex/plugin/components/ulw-loop/src/quality-gate-aggregate.ts +7 -53
  85. package/packages/omo-codex/plugin/components/ulw-loop/src/quality-gate-artifacts.ts +50 -31
  86. package/packages/omo-codex/plugin/components/ulw-loop/src/quality-gate-fields.ts +67 -14
  87. package/packages/omo-codex/plugin/components/ulw-loop/src/quality-gate-verdicts.ts +6 -1
  88. package/packages/omo-codex/plugin/components/ulw-loop/src/quality-gate.ts +36 -17
  89. package/packages/omo-codex/plugin/components/ulw-loop/test/checkpoint-final.test.ts +26 -0
  90. package/packages/omo-codex/plugin/components/ulw-loop/test/checkpoint-template.test.ts +47 -2
  91. package/packages/omo-codex/plugin/components/ulw-loop/test/quality-gate-aggregate-basics.test.ts +258 -0
  92. package/packages/omo-codex/plugin/components/ulw-loop/test/quality-gate-cap-and-dedupe.test.ts +22 -0
  93. package/packages/omo-codex/plugin/components/ulw-loop/test/quality-gate-poisoning-cascades.test.ts +97 -0
  94. package/packages/omo-codex/plugin/components/ulw-loop/test/quality-gate-single-report.test.ts +241 -0
  95. package/packages/omo-codex/plugin/hooks/post-compact-resetting-git-bash-mcp-reminder.json +1 -1
  96. package/packages/omo-codex/plugin/hooks/post-compact-resetting-lsp-diagnostics-cache.json +1 -1
  97. package/packages/omo-codex/plugin/hooks/post-compact-resetting-project-rule-cache.json +1 -1
  98. package/packages/omo-codex/plugin/hooks/post-tool-use-checking-comments.json +1 -1
  99. package/packages/omo-codex/plugin/hooks/post-tool-use-checking-lsp-diagnostics.json +1 -1
  100. package/packages/omo-codex/plugin/hooks/post-tool-use-checking-thread-title-hygiene.json +1 -1
  101. package/packages/omo-codex/plugin/hooks/post-tool-use-matching-project-rules.json +1 -1
  102. package/packages/omo-codex/plugin/hooks/pre-tool-use-enforcing-unlimited-goal-budget.json +1 -1
  103. package/packages/omo-codex/plugin/hooks/pre-tool-use-guarding-ulw-loop-spawns.json +1 -1
  104. package/packages/omo-codex/plugin/hooks/pre-tool-use-recommending-git-bash-mcp.json +1 -1
  105. package/packages/omo-codex/plugin/hooks/session-start-checking-auto-update.json +1 -1
  106. package/packages/omo-codex/plugin/hooks/session-start-checking-bootstrap-provisioning.json +1 -1
  107. package/packages/omo-codex/plugin/hooks/session-start-loading-project-rules.json +1 -1
  108. package/packages/omo-codex/plugin/hooks/session-start-recording-session-telemetry.json +1 -1
  109. package/packages/omo-codex/plugin/hooks/stop-checking-ulw-execute-continuation.json +1 -1
  110. package/packages/omo-codex/plugin/hooks/stop-checking-ulw-loop-resume.json +1 -1
  111. package/packages/omo-codex/plugin/hooks/subagent-stop-checking-ulw-execute-continuation.json +1 -1
  112. package/packages/omo-codex/plugin/hooks/subagent-stop-verifying-lazycodex-executor-evidence.json +1 -1
  113. package/packages/omo-codex/plugin/hooks/user-prompt-submit-checking-ultrawork-trigger.json +1 -1
  114. package/packages/omo-codex/plugin/hooks/user-prompt-submit-checking-ulw-loop-steering.json +1 -1
  115. package/packages/omo-codex/plugin/hooks/user-prompt-submit-loading-project-rules.json +1 -1
  116. package/packages/omo-codex/plugin/package-lock.json +12 -146
  117. package/packages/omo-codex/plugin/package.json +1 -2
  118. package/packages/omo-codex/plugin/scripts/AGENTS.md +0 -1
  119. package/packages/omo-codex/plugin/scripts/auto-update-plan.mjs +0 -1
  120. package/packages/omo-codex/plugin/scripts/auto-update.mjs +0 -9
  121. package/packages/omo-codex/plugin/scripts/canonical-ultrawork-directive.mjs +17 -0
  122. package/packages/omo-codex/plugin/scripts/hook-status-message.mjs +0 -1
  123. package/packages/omo-codex/plugin/scripts/sync-skills.mjs +2 -8
  124. package/packages/omo-codex/plugin/shared/src/config-loader.ts +2 -102
  125. package/packages/omo-codex/plugin/shared/src/config-migration.ts +0 -1
  126. package/packages/omo-codex/plugin/shared/test/config-loader.test.ts +41 -130
  127. package/packages/omo-codex/plugin/skills/init-deep/SKILL.md +13 -13
  128. package/packages/omo-codex/plugin/skills/lcx-doctor/SKILL.md +1 -1
  129. package/packages/omo-codex/plugin/skills/ultrawork/SKILL.md +2 -2
  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/canonical-ultrawork-directive.test.mjs +53 -0
  139. package/packages/omo-codex/plugin/test/component-bundled-cli.test.mjs +0 -50
  140. package/packages/omo-codex/plugin/test/hook-status-message.test.mjs +0 -1
  141. package/packages/omo-codex/plugin/test/mcp-research-servers.test.mjs +1 -3
  142. package/packages/omo-codex/scripts/install-cache-copy.test.mjs +27 -1
  143. package/packages/omo-codex/scripts/install-config-git-bash.test.mjs +0 -23
  144. package/packages/omo-codex/scripts/install-dist/install-local.mjs +238 -431
  145. package/packages/omo-codex/scripts/install-git-bash-mcp-env.test.mjs +1 -25
  146. package/packages/omo-codex/scripts/install-mcp-runtime.test.mjs +0 -36
  147. package/packages/shared-skills/skills/init-deep/SKILL.md +13 -13
  148. package/packages/shared-skills/skills/ulw-plan/SKILL.md +0 -1
  149. package/dist/config/schema/codegraph.d.ts +0 -12
  150. package/dist/hooks/codegraph-bootstrap/command-runner.d.ts +0 -17
  151. package/dist/hooks/codegraph-bootstrap/hook.d.ts +0 -35
  152. package/dist/hooks/codegraph-bootstrap/index.d.ts +0 -1
  153. package/dist/hooks/codegraph-bootstrap/project-root.d.ts +0 -1
  154. package/dist/hooks/codegraph-bootstrap/status.d.ts +0 -11
  155. package/dist/mcp/codegraph.d.ts +0 -16
  156. package/packages/omo-codex/plugin/components/codegraph/AGENTS.md +0 -58
  157. package/packages/omo-codex/plugin/components/codegraph/LICENSE +0 -21
  158. package/packages/omo-codex/plugin/components/codegraph/NODE-RUNTIME-LICENSES.md +0 -2951
  159. package/packages/omo-codex/plugin/components/codegraph/NOTICE +0 -16
  160. package/packages/omo-codex/plugin/components/codegraph/dist/cli.js +0 -12326
  161. package/packages/omo-codex/plugin/components/codegraph/dist/serve.js +0 -9908
  162. package/packages/omo-codex/plugin/components/codegraph/package.json +0 -33
  163. package/packages/omo-codex/plugin/components/codegraph/src/cache-gc.ts +0 -29
  164. package/packages/omo-codex/plugin/components/codegraph/src/cli.ts +0 -79
  165. package/packages/omo-codex/plugin/components/codegraph/src/hook-input.ts +0 -33
  166. package/packages/omo-codex/plugin/components/codegraph/src/hook-sweep.ts +0 -25
  167. package/packages/omo-codex/plugin/components/codegraph/src/hook-types.ts +0 -131
  168. package/packages/omo-codex/plugin/components/codegraph/src/hook.ts +0 -258
  169. package/packages/omo-codex/plugin/components/codegraph/src/mcp-bridge.ts +0 -310
  170. package/packages/omo-codex/plugin/components/codegraph/src/mcp-unavailable.ts +0 -92
  171. package/packages/omo-codex/plugin/components/codegraph/src/post-tool-use-hook.ts +0 -34
  172. package/packages/omo-codex/plugin/components/codegraph/src/serve-invocation.ts +0 -29
  173. package/packages/omo-codex/plugin/components/codegraph/src/serve.ts +0 -263
  174. package/packages/omo-codex/plugin/components/codegraph/src/session-start-command.ts +0 -106
  175. package/packages/omo-codex/plugin/components/codegraph/src/session-start-cooldown.ts +0 -145
  176. package/packages/omo-codex/plugin/components/codegraph/src/session-start-hook-runtime.ts +0 -22
  177. package/packages/omo-codex/plugin/components/codegraph/src/session-start-lock.ts +0 -139
  178. package/packages/omo-codex/plugin/components/codegraph/src/session-start-outcome.ts +0 -15
  179. package/packages/omo-codex/plugin/components/codegraph/src/session-start-paths.ts +0 -32
  180. package/packages/omo-codex/plugin/components/codegraph/src/session-start-project.ts +0 -109
  181. package/packages/omo-codex/plugin/components/codegraph/src/session-start-worker-result.ts +0 -148
  182. package/packages/omo-codex/plugin/components/codegraph/src/session-start-worker.ts +0 -175
  183. package/packages/omo-codex/plugin/components/codegraph/src/sweep-cli.ts +0 -81
  184. package/packages/omo-codex/plugin/components/codegraph/test/cache-gc.test.ts +0 -34
  185. package/packages/omo-codex/plugin/components/codegraph/test/hook-exclusion.test.ts +0 -112
  186. package/packages/omo-codex/plugin/components/codegraph/test/hook-registration.test.ts +0 -26
  187. package/packages/omo-codex/plugin/components/codegraph/test/hook-session-start-guard.test.ts +0 -160
  188. package/packages/omo-codex/plugin/components/codegraph/test/hook-store-upgrade.test.ts +0 -50
  189. package/packages/omo-codex/plugin/components/codegraph/test/hook-sweep.test.ts +0 -29
  190. package/packages/omo-codex/plugin/components/codegraph/test/hook.test.ts +0 -320
  191. package/packages/omo-codex/plugin/components/codegraph/test/mcp-bridge-fixtures.ts +0 -172
  192. package/packages/omo-codex/plugin/components/codegraph/test/package-runtime.test.ts +0 -91
  193. package/packages/omo-codex/plugin/components/codegraph/test/provisioned-node-guard.test.ts +0 -100
  194. package/packages/omo-codex/plugin/components/codegraph/test/serve-built-wrapper.test.ts +0 -76
  195. package/packages/omo-codex/plugin/components/codegraph/test/serve-mcp-bridge-lifecycle.test.ts +0 -133
  196. package/packages/omo-codex/plugin/components/codegraph/test/serve-mcp-bridge.test.ts +0 -245
  197. package/packages/omo-codex/plugin/components/codegraph/test/serve-mcp-facade.test.ts +0 -69
  198. package/packages/omo-codex/plugin/components/codegraph/test/serve-node-support.test.ts +0 -43
  199. package/packages/omo-codex/plugin/components/codegraph/test/serve-provision.test.ts +0 -147
  200. package/packages/omo-codex/plugin/components/codegraph/test/serve-unavailable-idle.test.ts +0 -47
  201. package/packages/omo-codex/plugin/components/codegraph/test/serve-unavailable.test.ts +0 -175
  202. package/packages/omo-codex/plugin/components/codegraph/test/serve.test.ts +0 -385
  203. package/packages/omo-codex/plugin/components/codegraph/test/session-start-node-support.test.ts +0 -206
  204. package/packages/omo-codex/plugin/components/codegraph/test/session-start-project.test.ts +0 -58
  205. package/packages/omo-codex/plugin/components/codegraph/test/session-start-state.test.ts +0 -104
  206. package/packages/omo-codex/plugin/components/codegraph/test/session-start-trust-boundary.test.ts +0 -65
  207. package/packages/omo-codex/plugin/components/codegraph/test/session-start-worker-availability.test.ts +0 -106
  208. package/packages/omo-codex/plugin/components/codegraph/test/session-start-worker-cooldown.test.ts +0 -116
  209. package/packages/omo-codex/plugin/components/codegraph/test/session-start-worker-flow.test.ts +0 -258
  210. package/packages/omo-codex/plugin/components/codegraph/test/sweep-cli.test.ts +0 -56
  211. package/packages/omo-codex/plugin/components/codegraph/tsconfig.build.json +0 -13
  212. package/packages/omo-codex/plugin/components/codegraph/tsconfig.json +0 -25
  213. package/packages/omo-codex/plugin/components/ulw-loop/test/quality-gate-aggregate.test.ts +0 -101
  214. package/packages/omo-codex/plugin/hooks/post-tool-use-checking-codegraph-init-guidance.json +0 -18
  215. package/packages/omo-codex/plugin/hooks/session-start-checking-codegraph-bootstrap.json +0 -17
  216. package/packages/omo-codex/plugin/scripts/migrate-omo-sot/editor.mjs +0 -125
  217. package/packages/omo-codex/plugin/scripts/migrate-omo-sot/jsonc.mjs +0 -43
  218. package/packages/omo-codex/plugin/scripts/migrate-omo-sot/scaffold.mjs +0 -29
  219. package/packages/omo-codex/plugin/scripts/migrate-omo-sot.mjs +0 -121
  220. package/packages/omo-codex/plugin/test/component-codegraph-mcp-smoke.test.mjs +0 -76
  221. package/packages/omo-codex/plugin/test/migrate-omo-sot.test.mjs +0 -179
@@ -73,6 +73,7 @@ function gateTemplate(surface: UlwLoopToolkitSurface, base: string): Record<stri
73
73
  reportPath: artifactPath(base, "gate-review.md"),
74
74
  evidence: "<replace:gate review evidence>",
75
75
  blockers: [],
76
+ notes: [],
76
77
  },
77
78
  iteration: {
78
79
  fullRerun: true,
@@ -115,13 +116,19 @@ export async function checkpointTemplate(
115
116
  throw new UlwLoopError(`Unknown ulw-loop id: ${goalId}.`, "ULW_LOOP_GOAL_NOT_FOUND", { details: { goalId } });
116
117
  const hasAttempt = plan.evidenceLayoutVersion === 2 && active !== undefined;
117
118
  const attemptDir = hasAttempt ? ulwLoopAttemptEvidenceDir(active.id, active.attempt, scope) : ".omo/evidence";
119
+ const guidance = [
120
+ "codex-goal-json requires goal.objective to equal the plan's codexObjective verbatim; do not paraphrase it.",
121
+ "Fill every <replace:...> value with plausible non-empty evidence and use real, non-empty artifact files.",
122
+ 'Passing codex-goal-json example: {"goal":{"objective":"<plan codexObjective verbatim>","status":"complete"}}.',
123
+ 'Passing quality-gate-json example requires gateReview {"by":"category:deep","recommendation":"APPROVE","evidence":"review passed","reportPath":"<attemptDir>/gate-review.md","blockers":[],"notes":[]}, manualQa.artifactRefs objects, iteration, and criteriaCoverage.',
124
+ ...(hasAttempt ? [] : ["This plan is evidence-layout v1; artifacts go under .omo/evidence/."]),
125
+ ].join(" ");
118
126
  return {
119
127
  qualityGateTemplate: gateTemplate(resolveToolkitSurface(), attemptDir),
120
128
  codexGoalTemplate: {
121
129
  goal: { objective: plan.codexObjective ?? "<replace:codex objective>", status: "complete" },
122
130
  },
123
- ...(hasAttempt
124
- ? { attemptDir }
125
- : { guidance: "This plan is evidence-layout v1; artifacts go under .omo/evidence/." }),
131
+ guidance,
132
+ ...(hasAttempt ? { attemptDir } : {}),
126
133
  };
127
134
  }
@@ -3,18 +3,11 @@ import { existsSync, statSync } from "node:fs";
3
3
  import { readFile } from "node:fs/promises";
4
4
  import { resolve } from "node:path";
5
5
  import {
6
- canReconcileActiveFinalTaskScopedAggregateSnapshot,
7
- canReconcileCompletedTaskScopedAggregateSnapshot,
8
- codexSnapshotMismatchError,
9
- } from "./checkpoint-reconciliation.js";
10
- import { readCodexGoalSnapshotInput, reconcileCodexGoalSnapshot } from "./codex-goal-snapshot.js";
6
+ combineCheckpointValidationErrors,
7
+ validateCheckpointCodexGoal,
8
+ } from "./checkpoint-codex-validation.js";
11
9
  import { requireAllCriteriaPass, requireAllPlanCriteriaPass, requireEssentialCriteriaPass } from "./evidence.js";
12
- import {
13
- codexGoalMode,
14
- compatibleCodexObjectives,
15
- expectedCodexObjective,
16
- isFinalRunCompletionCandidate,
17
- } from "./goal-status.js";
10
+ import { codexGoalMode, isFinalRunCompletionCandidate } from "./goal-status.js";
18
11
  import type { UlwLoopScope } from "./paths.js";
19
12
  import { ulwLoopAttemptEvidenceDir } from "./paths.js";
20
13
  import { appendLedger, readUlwLoopPlan, withUlwLoopMutationLock, writePlan } from "./plan-io.js";
@@ -54,9 +47,6 @@ const QUALITY_GATE_FS = { existsSync, statSync } as const;
54
47
  function ulwLoopFail(message: string, code: string): never {
55
48
  throw new UlwLoopError(message, code);
56
49
  }
57
- function normalizeObjective(value: string): string {
58
- return value.replace(/\s+/g, " ").trim();
59
- }
60
50
  function nonEmptyEvidence(value: string): string {
61
51
  const trimmed = value.trim();
62
52
  return trimmed || ulwLoopFail("Evidence must be a non-empty string.", "ulw_loop_evidence_required");
@@ -178,62 +168,41 @@ export async function checkpointUlwLoop(
178
168
  requireAllValidationBatchesClosed(plan, goal.id);
179
169
  } else if (aggregate) requireEssentialCriteriaPass(goal);
180
170
  else requireAllCriteriaPass(goal);
181
- const snapshot = await readCodexGoalSnapshotInput(args.codexGoalJson, repoRoot);
182
- const reconciliation = reconcileCodexGoalSnapshot(snapshot, {
183
- expectedObjective: expectedCodexObjective(plan, goal),
184
- ...(aggregate ? { acceptedObjectives: compatibleCodexObjectives(plan) } : {}),
185
- allowedStatuses: aggregate ? (final ? ["complete"] : ["active"]) : ["complete"],
186
- requireSnapshot: true,
187
- requireComplete: !aggregate || final,
188
- });
189
- codexGoal = reconciliation.snapshot.raw;
190
- if (!reconciliation.ok) {
191
- const objective = snapshot?.objective;
192
- const mismatchedTaskObjective =
193
- snapshot?.available === true &&
194
- objective !== undefined &&
195
- normalizeObjective(objective) !== normalizeObjective(expectedCodexObjective(plan, goal));
196
- const completedTaskScoped =
197
- mismatchedTaskObjective &&
198
- snapshot.status === "complete" &&
199
- (await canReconcileCompletedTaskScopedAggregateSnapshot(
200
- repoRoot,
201
- plan,
202
- goal,
203
- objective,
204
- evidence,
205
- scope,
206
- ));
207
- const activeFinalTaskScoped =
208
- mismatchedTaskObjective &&
209
- snapshot.status === "active" &&
210
- (await canReconcileActiveFinalTaskScopedAggregateSnapshot(
211
- repoRoot,
212
- plan,
213
- goal,
214
- objective,
215
- evidence,
216
- scope,
217
- ));
218
- const taskScoped = completedTaskScoped || activeFinalTaskScoped;
219
- if (!taskScoped)
220
- throw codexSnapshotMismatchError({ reconciliation, snapshot, expectedObjective: expectedCodexObjective(plan, goal), taskScopedHint: { goal, aggregate, final } });
171
+ let codexValidationError: UlwLoopError | undefined;
172
+ try {
173
+ codexGoal = await validateCheckpointCodexGoal({
174
+ repoRoot,
175
+ plan,
176
+ goal,
177
+ raw: args.codexGoalJson,
178
+ evidence,
179
+ ...(scope === undefined ? {} : { scope }),
180
+ });
181
+ } catch (error) {
182
+ if (!(error instanceof UlwLoopError)) throw error;
183
+ codexValidationError = error;
221
184
  }
222
185
  if (closesBatch) requireBatchFinalReady(plan, goal);
223
186
  if (closesBatch && args.qualityGateJson === undefined)
224
187
  throw new UlwLoopError("Validation batch final checkpoint requires --quality-gate-json.", "ULW_LOOP_VALIDATION_BATCH_GATE_REQUIRED");
225
188
  if (final) aggregateCompletion = makeAggregateCompletion(now, evidence, codexGoal);
226
189
  if (final || aggregateCompletion !== undefined || closesBatch) {
227
- qualityGate = validateQualityGate(await readJsonInput(args.qualityGateJson, repoRoot), {
228
- repoRoot,
229
- fs: QUALITY_GATE_FS,
230
- reviewerSurface: resolveToolkitSurface(),
231
- ...(plan.evidenceLayoutVersion === 2
232
- ? { currentAttemptDir: ulwLoopAttemptEvidenceDir(goal.id, goal.attempt, scope) }
233
- : {}),
234
- });
235
- requireBatchGate(plan, goal, qualityGate);
190
+ try {
191
+ qualityGate = validateQualityGate(await readJsonInput(args.qualityGateJson, repoRoot), {
192
+ repoRoot,
193
+ fs: QUALITY_GATE_FS,
194
+ reviewerSurface: resolveToolkitSurface(),
195
+ ...(plan.evidenceLayoutVersion === 2
196
+ ? { currentAttemptDir: ulwLoopAttemptEvidenceDir(goal.id, goal.attempt, scope) }
197
+ : {}),
198
+ });
199
+ requireBatchGate(plan, goal, qualityGate);
200
+ } catch (error) {
201
+ if (!(error instanceof UlwLoopError) || codexValidationError === undefined) throw error;
202
+ throw combineCheckpointValidationErrors(codexValidationError, error);
203
+ }
236
204
  }
205
+ if (codexValidationError !== undefined) throw codexValidationError;
237
206
  goal.status = "complete";
238
207
  goal.completedAt = now;
239
208
  goal.evidence = evidence;
@@ -1,57 +1,11 @@
1
- import { resolve } from "node:path";
2
1
  import type { ValidateQualityGateOptions } from "./quality-gate.js";
3
- import { isRecord } from "./quality-gate-fields.js";
4
- import { UlwLoopError } from "./types.js";
5
-
6
- type Defect = { readonly field: string; readonly message: string };
7
- const PLACEHOLDER = /^(?:<replace:[^>]+>|placeholder|todo|tbd|n\/a|stub)$/i;
8
-
9
- function add(defects: Defect[], field: string, message: string): void {
10
- defects.push({ field, message });
11
- }
12
- function text(value: unknown, field: string, defects: Defect[]): void {
13
- if (typeof value !== "string" || value.trim() === "")
14
- add(defects, field, `Final quality gate requires non-empty ${field}.`);
15
- else if (PLACEHOLDER.test(value.trim())) add(defects, field, `Final quality gate rejects placeholder ${field}.`);
16
- }
17
- function section(value: unknown, field: string, defects: Defect[]): Record<string, unknown> {
18
- if (!isRecord(value)) {
19
- add(defects, field, `Final quality gate is missing ${field} evidence.`);
20
- return {};
21
- }
22
- return value;
23
- }
2
+ import { validateQualityGate } from "./quality-gate.js";
24
3
 
4
+ /**
5
+ * Compatibility entry point for callers that used the former pre-pass API.
6
+ * Validation is performed by the real parser, which collects defects across
7
+ * every nested field before throwing one aggregate error.
8
+ */
25
9
  export function aggregateQualityGateDefects(input: unknown, opts: ValidateQualityGateOptions | undefined): void {
26
- if (!isRecord(input)) return;
27
- const defects: Defect[] = [];
28
- const gate = input;
29
- const manual = section(gate["manualQa"], "manualQa", defects);
30
- const review = section(gate["gateReview"], "gateReview", defects);
31
- text(manual["evidence"], "manualQa.evidence", defects);
32
- text(review["evidence"], "gateReview.evidence", defects);
33
- if (review["recommendation"] !== "APPROVE")
34
- add(defects, "gateReview.recommendation", "gateReview.recommendation must be APPROVE.");
35
- const artifacts = Array.isArray(manual["artifactRefs"]) ? manual["artifactRefs"] : [];
36
- for (const [index, item] of artifacts.entries()) {
37
- if (!isRecord(item)) continue;
38
- const path = item["path"];
39
- if (typeof path !== "string" || path.trim() === "") continue;
40
- if (opts?.repoRoot !== undefined && opts.fs !== undefined && !opts.fs.existsSync(resolve(opts.repoRoot, path)))
41
- add(
42
- defects,
43
- `manualQa.artifactRefs[${index}].path`,
44
- `manualQa.artifactRefs[${index}].path must point to an existing artifact.`,
45
- );
46
- }
47
- if (defects.length === 0) return;
48
- const truncated = defects.length > 25;
49
- const fields = defects.slice(0, 25).map(({ field, message }) => ({ field, message }));
50
- const message = [
51
- `Final quality gate has ${fields.length}${truncated ? "+" : ""} validation defects:`,
52
- ...fields.map((item) => `- ${item.field}: ${item.message}`),
53
- ].join("\n");
54
- throw new UlwLoopError(message, "ULW_LOOP_QUALITY_GATE_INVALID", {
55
- details: { field: fields[0]?.field, fields, ...(truncated ? { truncated: true } : {}) },
56
- });
10
+ validateQualityGate(input, opts);
57
11
  }
@@ -1,7 +1,14 @@
1
1
  import { resolve } from "node:path";
2
2
  import { isWithinAttemptDir } from "./paths.js";
3
3
  import type { ValidateQualityGateOptions } from "./quality-gate.js";
4
- import { invalid, section, stringArray, textField } from "./quality-gate-fields.js";
4
+ import {
5
+ invalid,
6
+ isPoisoned,
7
+ markPoisonedArtifactKind,
8
+ section,
9
+ stringArray,
10
+ textField,
11
+ } from "./quality-gate-fields.js";
5
12
  import type { UlwLoopManualQaArtifactKind, UlwLoopManualQaArtifactRef, UlwLoopManualQaSurface } from "./types.js";
6
13
 
7
14
  export function surfaceField(value: unknown, field: string): UlwLoopManualQaSurface {
@@ -15,8 +22,8 @@ export function surfaceField(value: unknown, field: string): UlwLoopManualQaSurf
15
22
  )
16
23
  return value;
17
24
  invalid(`${field} must be a supported manual QA surface.`, field);
25
+ return "cli";
18
26
  }
19
-
20
27
  export function kindField(value: unknown, field: string): UlwLoopManualQaArtifactKind {
21
28
  if (
22
29
  value === "cli-transcript" ||
@@ -28,8 +35,8 @@ export function kindField(value: unknown, field: string): UlwLoopManualQaArtifac
28
35
  )
29
36
  return value;
30
37
  invalid(`${field} must be a supported artifact kind.`, field);
38
+ return "log";
31
39
  }
32
-
33
40
  export function artifactCompatible(surface: UlwLoopManualQaSurface, kind: UlwLoopManualQaArtifactKind): boolean {
34
41
  switch (surface) {
35
42
  case "cli":
@@ -44,24 +51,26 @@ export function artifactCompatible(surface: UlwLoopManualQaSurface, kind: UlwLoo
44
51
  return kind === "data-diff";
45
52
  default:
46
53
  invalid("manualQa.surfaceEvidence has an unsupported surface.", "manualQa.surfaceEvidence.surface");
54
+ return false;
47
55
  }
48
56
  }
49
-
50
57
  export function checkFile(path: string, field: string, opts?: ValidateQualityGateOptions): void {
51
- if (opts?.repoRoot === undefined || opts.fs === undefined) return;
58
+ if (opts?.repoRoot === undefined || opts.fs === undefined || isPoisoned(field)) return;
52
59
  const absolute = resolve(opts.repoRoot, path);
53
- if (!opts.fs.existsSync(absolute)) invalid(`${field} must point to an existing artifact.`, field);
54
- if (opts.fs.statSync(absolute).size <= 0) invalid(`${field} must point to a non-empty artifact.`, field);
55
- if (opts.currentAttemptDir !== undefined && opts.repoRoot !== undefined) {
56
- const attemptRoot = resolve(opts.repoRoot, opts.currentAttemptDir);
57
- if (!isWithinAttemptDir(absolute, attemptRoot))
58
- invalid(
59
- `${field} (${path}) must point to an artifact from the current attempt (${opts.currentAttemptDir}).`,
60
- field,
61
- );
60
+ if (!opts.fs.existsSync(absolute)) {
61
+ invalid(`${field} must point to an existing artifact.`, field);
62
+ return;
62
63
  }
64
+ if (opts.fs.statSync(absolute).size <= 0) invalid(`${field} must point to a non-empty artifact.`, field);
65
+ if (
66
+ opts.currentAttemptDir !== undefined &&
67
+ !isWithinAttemptDir(absolute, resolve(opts.repoRoot, opts.currentAttemptDir))
68
+ )
69
+ invalid(
70
+ `${field} (${path}) must point to an artifact from the current attempt (${opts.currentAttemptDir}).`,
71
+ field,
72
+ );
63
73
  }
64
-
65
74
  export function artifactMap(refs: readonly UlwLoopManualQaArtifactRef[]): Map<string, UlwLoopManualQaArtifactRef> {
66
75
  const byId = new Map<string, UlwLoopManualQaArtifactRef>();
67
76
  for (const ref of refs) {
@@ -70,34 +79,44 @@ export function artifactMap(refs: readonly UlwLoopManualQaArtifactRef[]): Map<st
70
79
  }
71
80
  return byId;
72
81
  }
73
-
74
82
  export function parseArtifactRefs(
75
83
  value: unknown,
76
84
  opts?: ValidateQualityGateOptions,
77
85
  ): readonly UlwLoopManualQaArtifactRef[] {
78
- if (!Array.isArray(value) || value.length === 0)
86
+ if (!Array.isArray(value) || value.length === 0) {
79
87
  invalid("manualQa.artifactRefs must not be empty.", "manualQa.artifactRefs");
80
- return value.map((item, index) => {
81
- const ref = section(item, `manualQa.artifactRefs[${index}]`);
82
- const path = textField(ref["path"], `manualQa.artifactRefs[${index}].path`);
83
- checkFile(path, `manualQa.artifactRefs[${index}].path`, opts);
84
- return {
85
- id: textField(ref["id"], `manualQa.artifactRefs[${index}].id`),
86
- kind: kindField(ref["kind"], `manualQa.artifactRefs[${index}].kind`),
87
- description: textField(ref["description"], `manualQa.artifactRefs[${index}].description`),
88
- path,
89
- };
88
+ return [];
89
+ }
90
+ return value.flatMap((item, index) => {
91
+ const prefix = `manualQa.artifactRefs[${index}]`;
92
+ const ref = section(item, prefix);
93
+ if (isPoisoned(prefix)) return [];
94
+ const pathField = `${prefix}.path`;
95
+ const idField = `${prefix}.id`;
96
+ const kindFieldName = `${prefix}.kind`;
97
+ const descriptionField = `${prefix}.description`;
98
+ const path = textField(ref["path"], pathField);
99
+ const id = textField(ref["id"], idField);
100
+ const kind = kindField(ref["kind"], kindFieldName);
101
+ if (isPoisoned(kindFieldName)) markPoisonedArtifactKind(id);
102
+ const description = textField(ref["description"], descriptionField);
103
+ checkFile(path, pathField, opts);
104
+ return [{ id, kind, description, path }];
90
105
  });
91
106
  }
92
-
93
107
  export function referencedArtifacts(
94
108
  value: unknown,
95
109
  field: string,
96
110
  byId: ReadonlyMap<string, UlwLoopManualQaArtifactRef>,
97
111
  ): readonly UlwLoopManualQaArtifactRef[] {
98
- return stringArray(value, field).map((id) => {
112
+ const ids = stringArray(value, field);
113
+ if (isPoisoned(field)) return [];
114
+ return ids.flatMap((id) => {
99
115
  const artifact = byId.get(id);
100
- if (artifact === undefined) invalid(`${field} references unknown artifact ${id}.`, field);
101
- return artifact;
116
+ if (artifact === undefined) {
117
+ invalid(`${field} references unknown artifact ${id}.`, field);
118
+ return [];
119
+ }
120
+ return [artifact];
102
121
  });
103
122
  }
@@ -1,44 +1,97 @@
1
1
  import { UlwLoopError } from "./types.js";
2
2
 
3
3
  const PLACEHOLDER_PATTERN = /^(?:<replace:[^>]+>|placeholder|todo|tbd|n\/a|stub)$/i;
4
+ type QualityGateDefect = { readonly field: string; readonly message: string };
5
+ type QualityGateCollector = {
6
+ readonly defects: QualityGateDefect[];
7
+ readonly poisonedFields: Set<string>;
8
+ readonly poisonedArtifactKinds: Set<string>;
9
+ };
10
+ let activeCollector: QualityGateCollector | undefined;
4
11
 
5
- export function invalid(message: string, field: string): never {
6
- throw new UlwLoopError(message, "ULW_LOOP_QUALITY_GATE_INVALID", { details: { field } });
12
+ /** A recorded defect poisons only its field; consumers skip dependent checks while preserving valid siblings and entry identity. */
13
+ export function withQualityGateCollector<T>(operation: () => T): T {
14
+ const previous = activeCollector;
15
+ const collector: QualityGateCollector = { defects: [], poisonedFields: new Set(), poisonedArtifactKinds: new Set() };
16
+ activeCollector = collector;
17
+ try {
18
+ const result = operation();
19
+ if (collector.defects.length === 0) return result;
20
+ throwQualityGateDefects(collector.defects);
21
+ } finally {
22
+ activeCollector = previous;
23
+ }
7
24
  }
8
25
 
26
+ export function throwQualityGateDefects(defects: readonly QualityGateDefect[]): never {
27
+ const uniqueDefects = [
28
+ ...new Map(defects.map((defect) => [`${defect.field}\u0000${defect.message}`, defect])).values(),
29
+ ];
30
+ const fields = uniqueDefects.slice(0, 25);
31
+ const truncated = uniqueDefects.length > fields.length;
32
+ const message = [
33
+ `Final quality gate has ${fields.length}${truncated ? "+" : ""} validation defects:`,
34
+ ...fields.map((item) => `- ${item.field}: ${item.message}`),
35
+ ].join("\n");
36
+ throw new UlwLoopError(message, "ULW_LOOP_QUALITY_GATE_INVALID", {
37
+ details: { field: fields[0]?.field, fields, ...(truncated ? { truncated: true } : {}) },
38
+ });
39
+ }
40
+ export function invalid(message: string, field: string): undefined {
41
+ if (activeCollector === undefined)
42
+ throw new UlwLoopError(message, "ULW_LOOP_QUALITY_GATE_INVALID", { details: { field } });
43
+ activeCollector.defects.push({ field, message });
44
+ activeCollector.poisonedFields.add(field);
45
+ return undefined;
46
+ }
47
+ export function isPoisoned(field: string): boolean {
48
+ return activeCollector?.poisonedFields.has(field) ?? false;
49
+ }
50
+ export function poisonField(field: string): void {
51
+ activeCollector?.poisonedFields.add(field);
52
+ }
53
+ export function markPoisonedArtifactKind(id: string): void {
54
+ activeCollector?.poisonedArtifactKinds.add(id);
55
+ }
56
+ export function isPoisonedArtifactKind(id: string): boolean {
57
+ return activeCollector?.poisonedArtifactKinds.has(id) ?? false;
58
+ }
9
59
  export function isRecord(value: unknown): value is Record<string, unknown> {
10
60
  return typeof value === "object" && value !== null && !Array.isArray(value);
11
61
  }
12
-
13
62
  export function section(value: unknown, field: string): Record<string, unknown> {
14
- return isRecord(value) ? value : invalid(`Final quality gate is missing ${field} evidence.`, field);
63
+ if (isRecord(value)) return value;
64
+ invalid(`Final quality gate is missing ${field} evidence.`, field);
65
+ return {};
15
66
  }
16
-
17
67
  export function textField(value: unknown, field: string): string {
18
- if (typeof value !== "string" || value.trim() === "")
68
+ if (typeof value !== "string" || value.trim() === "") {
19
69
  invalid(`Final quality gate requires non-empty ${field}.`, field);
70
+ return "";
71
+ }
20
72
  const trimmed = value.trim();
21
73
  if (PLACEHOLDER_PATTERN.test(trimmed)) invalid(`Final quality gate rejects placeholder ${field}.`, field);
22
74
  return trimmed;
23
75
  }
24
-
25
76
  export function numberField(value: unknown, field: string): number {
26
- return typeof value === "number" && Number.isFinite(value)
27
- ? value
28
- : invalid(`Final quality gate requires numeric ${field}.`, field);
77
+ if (typeof value === "number" && Number.isFinite(value)) return value;
78
+ invalid(`Final quality gate requires numeric ${field}.`, field);
79
+ return 0;
29
80
  }
30
-
31
81
  export function stringArray(value: unknown, field: string): readonly string[] {
32
- if (!Array.isArray(value) || value.length === 0) return invalid(`Final quality gate requires ${field}.`, field);
82
+ if (!Array.isArray(value) || value.length === 0) {
83
+ invalid(`Final quality gate requires ${field}.`, field);
84
+ return [];
85
+ }
33
86
  return value.map((item) => textField(item, field));
34
87
  }
35
-
36
88
  export function emptyBlockers(value: unknown, field: string): readonly [] {
37
89
  if (Array.isArray(value) && value.length === 0) return [];
38
90
  invalid(`${field} must be empty.`, field);
91
+ return [];
39
92
  }
40
-
41
93
  export function literal<T extends string | boolean>(value: unknown, expected: T, field: string): T {
42
94
  if (value === expected) return expected;
43
95
  invalid(`${field} must be ${String(expected)}.`, field);
96
+ return expected;
44
97
  }
@@ -1,13 +1,17 @@
1
1
  import { invalid, literal, textField } from "./quality-gate-fields.js";
2
2
 
3
3
  export function passedVerdict(value: unknown, field: string): "passed" {
4
- if (value === "not_applicable") invalid(`${field} must not be not_applicable.`, field);
4
+ if (value === "not_applicable") {
5
+ invalid(`${field} must not be not_applicable.`, field);
6
+ return "passed";
7
+ }
5
8
  return literal(value, "passed", field);
6
9
  }
7
10
 
8
11
  export function codeQualityStatusField(value: unknown, field: string): "CLEAR" | "WATCH" {
9
12
  if (value === "CLEAR" || value === "WATCH") return value;
10
13
  invalid(`${field} must be CLEAR or WATCH.`, field);
14
+ return "CLEAR";
11
15
  }
12
16
 
13
17
  export function adversarialVerdict(
@@ -20,4 +24,5 @@ export function adversarialVerdict(
20
24
  return { verdict: "not_applicable", reason: textField(row["reason"], `${field}.reason`) };
21
25
  }
22
26
  invalid(`${field} must be passed or not_applicable with a reason.`, field);
27
+ return { verdict: "passed" };
23
28
  }
@@ -1,4 +1,3 @@
1
- import { aggregateQualityGateDefects } from "./quality-gate-aggregate.js";
2
1
  import {
3
2
  artifactCompatible,
4
3
  artifactMap,
@@ -10,14 +9,17 @@ import {
10
9
  import {
11
10
  emptyBlockers,
12
11
  invalid,
12
+ isPoisoned,
13
+ isPoisonedArtifactKind,
13
14
  literal,
14
15
  numberField,
15
16
  section,
16
17
  stringArray,
17
18
  textField,
19
+ withQualityGateCollector,
18
20
  } from "./quality-gate-fields.js";
19
21
  import { adversarialVerdict, codeQualityStatusField, passedVerdict } from "./quality-gate-verdicts.js";
20
- import { GATE_SECTION_BY_ACCEPTOR, REQUIRED_GATE_SECTIONS_BY_SURFACE, type UlwLoopToolkitSurface } from "./surface.js";
22
+ import { GATE_SECTION_BY_ACCEPTOR, type UlwLoopToolkitSurface } from "./surface.js";
21
23
  import type { UlwLoopManualQaArtifactRef, UlwLoopQualityGate } from "./types.js";
22
24
 
23
25
  export {
@@ -40,6 +42,10 @@ export interface ValidateQualityGateOptions {
40
42
  }
41
43
 
42
44
  function reviewerRoleField(value: unknown, expected: string, field: string): string {
45
+ if (typeof value !== "string" || value.trim() === "") {
46
+ textField(value, field);
47
+ return expected;
48
+ }
43
49
  const actual = textField(value, field);
44
50
  if (actual !== expected) invalid(`${field} must be ${expected}.`, field);
45
51
  return expected;
@@ -51,44 +57,51 @@ function reviewerAcceptorField(
51
57
  sectionName: "manualQa" | "gateReview",
52
58
  ): string {
53
59
  const field = `${sectionName}.by`;
54
- const actual = textField(value, field);
55
60
  const accepted = GATE_SECTION_BY_ACCEPTOR[surface][sectionName];
61
+ if (typeof value !== "string" || value.trim() === "") {
62
+ textField(value, field);
63
+ return accepted?.[0] ?? "";
64
+ }
65
+ const actual = textField(value, field);
56
66
  if (accepted === undefined || !accepted.includes(actual))
57
67
  invalid(`${field} must be one of ${accepted?.join(", ") ?? "the configured reviewers"}.`, field);
58
68
  return actual;
59
69
  }
60
70
 
61
71
  export function validateQualityGate(input: unknown, opts?: ValidateQualityGateOptions): UlwLoopQualityGate {
72
+ return withQualityGateCollector(() => validateQualityGateUncollected(input, opts));
73
+ }
74
+
75
+ function validateQualityGateUncollected(input: unknown, opts?: ValidateQualityGateOptions): UlwLoopQualityGate {
62
76
  const surface = opts?.reviewerSurface ?? "lazycodex";
63
- aggregateQualityGateDefects(input, opts);
64
77
  const gate = section(input, "qualityGate");
65
- const requiredSections = REQUIRED_GATE_SECTIONS_BY_SURFACE[surface];
66
- for (const sectionName of requiredSections) section(gate[sectionName], sectionName);
67
78
  if (surface === "omo-senpi" && gate["codeReview"] !== undefined)
68
79
  invalid("omo-senpi gate has no codeReview lane.", "codeReview");
69
80
  const manualQa = section(gate["manualQa"], "manualQa");
70
81
  const gateReview = section(gate["gateReview"], "gateReview");
71
82
  const iteration = section(gate["iteration"], "iteration");
72
83
  const coverage = section(gate["criteriaCoverage"], "criteriaCoverage");
84
+ const codeReview = surface === "lazycodex" ? section(gate["codeReview"], "codeReview") : {};
73
85
  const manualQaBy = reviewerAcceptorField(manualQa["by"], surface, "manualQa");
74
86
  const gateReviewBy = reviewerAcceptorField(gateReview["by"], surface, "gateReview");
75
- if (surface === "lazycodex")
76
- reviewerRoleField(section(gate["codeReview"], "codeReview")["by"], "lazycodex-code-reviewer", "codeReview.by");
87
+ const manualQaEvidence = textField(manualQa["evidence"], "manualQa.evidence");
88
+ const gateReviewEvidence = textField(gateReview["evidence"], "gateReview.evidence");
89
+ if (surface === "lazycodex") reviewerRoleField(codeReview?.["by"], "lazycodex-code-reviewer", "codeReview.by");
77
90
  const totalCriteria = numberField(coverage["totalCriteria"], "criteriaCoverage.totalCriteria");
78
91
  const passCount = numberField(coverage["passCount"], "criteriaCoverage.passCount");
79
- if (passCount < totalCriteria)
92
+ if (!isPoisoned("criteriaCoverage.passCount") && passCount < totalCriteria)
80
93
  invalid("criteriaCoverage.passCount must cover totalCriteria.", "criteriaCoverage.passCount");
81
94
  const artifactRefs = parseArtifactRefs(manualQa["artifactRefs"], opts);
82
95
  const byId = artifactMap(artifactRefs);
83
96
  const surfaceEvidence = parseSurfaceEvidence(manualQa["surfaceEvidence"], byId);
84
97
  const adversarialCases = parseAdversarialCases(manualQa["adversarialCases"], byId);
85
98
  const gateReportPath = textField(gateReview["reportPath"], "gateReview.reportPath");
86
- checkFile(gateReportPath, "gateReview.reportPath", opts);
99
+ if (!isPoisoned("gateReview.reportPath")) checkFile(gateReportPath, "gateReview.reportPath", opts);
87
100
  const common = {
88
101
  manualQa: {
89
102
  by: manualQaBy,
90
103
  status: literal(manualQa["status"], "passed", "manualQa.status"),
91
- evidence: textField(manualQa["evidence"], "manualQa.evidence"),
104
+ evidence: manualQaEvidence,
92
105
  surfaceEvidence,
93
106
  adversarialCases,
94
107
  artifactRefs,
@@ -97,7 +110,7 @@ export function validateQualityGate(input: unknown, opts?: ValidateQualityGateOp
97
110
  by: gateReviewBy,
98
111
  recommendation: literal(gateReview["recommendation"], "APPROVE", "gateReview.recommendation"),
99
112
  reportPath: gateReportPath,
100
- evidence: textField(gateReview["evidence"], "gateReview.evidence"),
113
+ evidence: gateReviewEvidence,
101
114
  blockers: emptyBlockers(gateReview["blockers"], "gateReview.blockers"),
102
115
  },
103
116
  iteration: {
@@ -119,7 +132,6 @@ export function validateQualityGate(input: unknown, opts?: ValidateQualityGateOp
119
132
  },
120
133
  };
121
134
  if (surface === "omo-senpi") return { surface, ...common };
122
- const codeReview = section(gate["codeReview"], "codeReview");
123
135
  const codeReportPath = textField(codeReview["reportPath"], "codeReview.reportPath");
124
136
  checkFile(codeReportPath, "codeReview.reportPath", opts);
125
137
  return {
@@ -140,10 +152,13 @@ function parseSurfaceEvidence(
140
152
  value: unknown,
141
153
  byId: ReadonlyMap<string, UlwLoopManualQaArtifactRef>,
142
154
  ): UlwLoopQualityGate["manualQa"]["surfaceEvidence"] {
143
- if (!Array.isArray(value) || value.length === 0)
155
+ if (!Array.isArray(value) || value.length === 0) {
144
156
  invalid("manualQa.surfaceEvidence must not be empty.", "manualQa.surfaceEvidence");
145
- return value.map((item, index) => {
157
+ return [];
158
+ }
159
+ return value.flatMap((item, index) => {
146
160
  const row = section(item, `manualQa.surfaceEvidence[${index}]`);
161
+ if (isPoisoned(`manualQa.surfaceEvidence[${index}]`)) return [];
147
162
  const surface = surfaceField(row["surface"], `manualQa.surfaceEvidence[${index}].surface`);
148
163
  const artifacts = referencedArtifacts(
149
164
  row["artifactRefs"],
@@ -151,6 +166,7 @@ function parseSurfaceEvidence(
151
166
  byId,
152
167
  );
153
168
  for (const artifact of artifacts) {
169
+ if (isPoisoned(`manualQa.surfaceEvidence[${index}].surface`) || isPoisonedArtifactKind(artifact.id)) continue;
154
170
  if (!artifactCompatible(surface, artifact.kind)) {
155
171
  invalid(
156
172
  `manualQa.surfaceEvidence ${surface} artifact ${artifact.kind} is incompatible.`,
@@ -173,10 +189,13 @@ function parseAdversarialCases(
173
189
  value: unknown,
174
190
  byId: ReadonlyMap<string, UlwLoopManualQaArtifactRef>,
175
191
  ): UlwLoopQualityGate["manualQa"]["adversarialCases"] {
176
- if (!Array.isArray(value) || value.length === 0)
192
+ if (!Array.isArray(value) || value.length === 0) {
177
193
  invalid("manualQa.adversarialCases must not be empty.", "manualQa.adversarialCases");
178
- return value.map((item, index) => {
194
+ return [];
195
+ }
196
+ return value.flatMap((item, index) => {
179
197
  const row = section(item, `manualQa.adversarialCases[${index}]`);
198
+ if (isPoisoned(`manualQa.adversarialCases[${index}]`)) return [];
180
199
  const artifacts = referencedArtifacts(
181
200
  row["artifactRefs"],
182
201
  `manualQa.adversarialCases[${index}].artifactRefs`,