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
@@ -1,2 +1,7 @@
1
1
  import type { ValidateQualityGateOptions } from "./quality-gate.js";
2
+ /**
3
+ * Compatibility entry point for callers that used the former pre-pass API.
4
+ * Validation is performed by the real parser, which collects defects across
5
+ * every nested field before throwing one aggregate error.
6
+ */
2
7
  export declare function aggregateQualityGateDefects(input: unknown, opts: ValidateQualityGateOptions | undefined): void;
@@ -1,53 +1,9 @@
1
- import { resolve } from "node:path";
2
- import { isRecord } from "./quality-gate-fields.js";
3
- import { UlwLoopError } from "./types.js";
4
- const PLACEHOLDER = /^(?:<replace:[^>]+>|placeholder|todo|tbd|n\/a|stub)$/i;
5
- function add(defects, field, message) {
6
- defects.push({ field, message });
7
- }
8
- function text(value, field, defects) {
9
- if (typeof value !== "string" || value.trim() === "")
10
- add(defects, field, `Final quality gate requires non-empty ${field}.`);
11
- else if (PLACEHOLDER.test(value.trim()))
12
- add(defects, field, `Final quality gate rejects placeholder ${field}.`);
13
- }
14
- function section(value, field, defects) {
15
- if (!isRecord(value)) {
16
- add(defects, field, `Final quality gate is missing ${field} evidence.`);
17
- return {};
18
- }
19
- return value;
20
- }
1
+ import { validateQualityGate } from "./quality-gate.js";
2
+ /**
3
+ * Compatibility entry point for callers that used the former pre-pass API.
4
+ * Validation is performed by the real parser, which collects defects across
5
+ * every nested field before throwing one aggregate error.
6
+ */
21
7
  export function aggregateQualityGateDefects(input, opts) {
22
- if (!isRecord(input))
23
- return;
24
- const defects = [];
25
- const gate = input;
26
- const manual = section(gate["manualQa"], "manualQa", defects);
27
- const review = section(gate["gateReview"], "gateReview", defects);
28
- text(manual["evidence"], "manualQa.evidence", defects);
29
- text(review["evidence"], "gateReview.evidence", defects);
30
- if (review["recommendation"] !== "APPROVE")
31
- add(defects, "gateReview.recommendation", "gateReview.recommendation must be APPROVE.");
32
- const artifacts = Array.isArray(manual["artifactRefs"]) ? manual["artifactRefs"] : [];
33
- for (const [index, item] of artifacts.entries()) {
34
- if (!isRecord(item))
35
- continue;
36
- const path = item["path"];
37
- if (typeof path !== "string" || path.trim() === "")
38
- continue;
39
- if (opts?.repoRoot !== undefined && opts.fs !== undefined && !opts.fs.existsSync(resolve(opts.repoRoot, path)))
40
- add(defects, `manualQa.artifactRefs[${index}].path`, `manualQa.artifactRefs[${index}].path must point to an existing artifact.`);
41
- }
42
- if (defects.length === 0)
43
- return;
44
- const truncated = defects.length > 25;
45
- const fields = defects.slice(0, 25).map(({ field, message }) => ({ field, message }));
46
- const message = [
47
- `Final quality gate has ${fields.length}${truncated ? "+" : ""} validation defects:`,
48
- ...fields.map((item) => `- ${item.field}: ${item.message}`),
49
- ].join("\n");
50
- throw new UlwLoopError(message, "ULW_LOOP_QUALITY_GATE_INVALID", {
51
- details: { field: fields[0]?.field, fields, ...(truncated ? { truncated: true } : {}) },
52
- });
8
+ validateQualityGate(input, opts);
53
9
  }
@@ -1,6 +1,6 @@
1
1
  import { resolve } from "node:path";
2
2
  import { isWithinAttemptDir } from "./paths.js";
3
- import { invalid, section, stringArray, textField } from "./quality-gate-fields.js";
3
+ import { invalid, isPoisoned, markPoisonedArtifactKind, section, stringArray, textField, } from "./quality-gate-fields.js";
4
4
  export function surfaceField(value, field) {
5
5
  if (value === "cli" ||
6
6
  value === "http" ||
@@ -10,6 +10,7 @@ export function surfaceField(value, field) {
10
10
  value === "data")
11
11
  return value;
12
12
  invalid(`${field} must be a supported manual QA surface.`, field);
13
+ return "cli";
13
14
  }
14
15
  export function kindField(value, field) {
15
16
  if (value === "cli-transcript" ||
@@ -20,6 +21,7 @@ export function kindField(value, field) {
20
21
  value === "data-diff")
21
22
  return value;
22
23
  invalid(`${field} must be a supported artifact kind.`, field);
24
+ return "log";
23
25
  }
24
26
  export function artifactCompatible(surface, kind) {
25
27
  switch (surface) {
@@ -35,21 +37,22 @@ export function artifactCompatible(surface, kind) {
35
37
  return kind === "data-diff";
36
38
  default:
37
39
  invalid("manualQa.surfaceEvidence has an unsupported surface.", "manualQa.surfaceEvidence.surface");
40
+ return false;
38
41
  }
39
42
  }
40
43
  export function checkFile(path, field, opts) {
41
- if (opts?.repoRoot === undefined || opts.fs === undefined)
44
+ if (opts?.repoRoot === undefined || opts.fs === undefined || isPoisoned(field))
42
45
  return;
43
46
  const absolute = resolve(opts.repoRoot, path);
44
- if (!opts.fs.existsSync(absolute))
47
+ if (!opts.fs.existsSync(absolute)) {
45
48
  invalid(`${field} must point to an existing artifact.`, field);
49
+ return;
50
+ }
46
51
  if (opts.fs.statSync(absolute).size <= 0)
47
52
  invalid(`${field} must point to a non-empty artifact.`, field);
48
- if (opts.currentAttemptDir !== undefined && opts.repoRoot !== undefined) {
49
- const attemptRoot = resolve(opts.repoRoot, opts.currentAttemptDir);
50
- if (!isWithinAttemptDir(absolute, attemptRoot))
51
- invalid(`${field} (${path}) must point to an artifact from the current attempt (${opts.currentAttemptDir}).`, field);
52
- }
53
+ if (opts.currentAttemptDir !== undefined &&
54
+ !isWithinAttemptDir(absolute, resolve(opts.repoRoot, opts.currentAttemptDir)))
55
+ invalid(`${field} (${path}) must point to an artifact from the current attempt (${opts.currentAttemptDir}).`, field);
53
56
  }
54
57
  export function artifactMap(refs) {
55
58
  const byId = new Map();
@@ -61,25 +64,39 @@ export function artifactMap(refs) {
61
64
  return byId;
62
65
  }
63
66
  export function parseArtifactRefs(value, opts) {
64
- if (!Array.isArray(value) || value.length === 0)
67
+ if (!Array.isArray(value) || value.length === 0) {
65
68
  invalid("manualQa.artifactRefs must not be empty.", "manualQa.artifactRefs");
66
- return value.map((item, index) => {
67
- const ref = section(item, `manualQa.artifactRefs[${index}]`);
68
- const path = textField(ref["path"], `manualQa.artifactRefs[${index}].path`);
69
- checkFile(path, `manualQa.artifactRefs[${index}].path`, opts);
70
- return {
71
- id: textField(ref["id"], `manualQa.artifactRefs[${index}].id`),
72
- kind: kindField(ref["kind"], `manualQa.artifactRefs[${index}].kind`),
73
- description: textField(ref["description"], `manualQa.artifactRefs[${index}].description`),
74
- path,
75
- };
69
+ return [];
70
+ }
71
+ return value.flatMap((item, index) => {
72
+ const prefix = `manualQa.artifactRefs[${index}]`;
73
+ const ref = section(item, prefix);
74
+ if (isPoisoned(prefix))
75
+ return [];
76
+ const pathField = `${prefix}.path`;
77
+ const idField = `${prefix}.id`;
78
+ const kindFieldName = `${prefix}.kind`;
79
+ const descriptionField = `${prefix}.description`;
80
+ const path = textField(ref["path"], pathField);
81
+ const id = textField(ref["id"], idField);
82
+ const kind = kindField(ref["kind"], kindFieldName);
83
+ if (isPoisoned(kindFieldName))
84
+ markPoisonedArtifactKind(id);
85
+ const description = textField(ref["description"], descriptionField);
86
+ checkFile(path, pathField, opts);
87
+ return [{ id, kind, description, path }];
76
88
  });
77
89
  }
78
90
  export function referencedArtifacts(value, field, byId) {
79
- return stringArray(value, field).map((id) => {
91
+ const ids = stringArray(value, field);
92
+ if (isPoisoned(field))
93
+ return [];
94
+ return ids.flatMap((id) => {
80
95
  const artifact = byId.get(id);
81
- if (artifact === undefined)
96
+ if (artifact === undefined) {
82
97
  invalid(`${field} references unknown artifact ${id}.`, field);
83
- return artifact;
98
+ return [];
99
+ }
100
+ return [artifact];
84
101
  });
85
102
  }
@@ -1,4 +1,15 @@
1
- export declare function invalid(message: string, field: string): never;
1
+ type QualityGateDefect = {
2
+ readonly field: string;
3
+ readonly message: string;
4
+ };
5
+ /** A recorded defect poisons only its field; consumers skip dependent checks while preserving valid siblings and entry identity. */
6
+ export declare function withQualityGateCollector<T>(operation: () => T): T;
7
+ export declare function throwQualityGateDefects(defects: readonly QualityGateDefect[]): never;
8
+ export declare function invalid(message: string, field: string): undefined;
9
+ export declare function isPoisoned(field: string): boolean;
10
+ export declare function poisonField(field: string): void;
11
+ export declare function markPoisonedArtifactKind(id: string): void;
12
+ export declare function isPoisonedArtifactKind(id: string): boolean;
2
13
  export declare function isRecord(value: unknown): value is Record<string, unknown>;
3
14
  export declare function section(value: unknown, field: string): Record<string, unknown>;
4
15
  export declare function textField(value: unknown, field: string): string;
@@ -6,3 +17,4 @@ export declare function numberField(value: unknown, field: string): number;
6
17
  export declare function stringArray(value: unknown, field: string): readonly string[];
7
18
  export declare function emptyBlockers(value: unknown, field: string): readonly [];
8
19
  export declare function literal<T extends string | boolean>(value: unknown, expected: T, field: string): T;
20
+ export {};
@@ -1,39 +1,95 @@
1
1
  import { UlwLoopError } from "./types.js";
2
2
  const PLACEHOLDER_PATTERN = /^(?:<replace:[^>]+>|placeholder|todo|tbd|n\/a|stub)$/i;
3
+ let activeCollector;
4
+ /** A recorded defect poisons only its field; consumers skip dependent checks while preserving valid siblings and entry identity. */
5
+ export function withQualityGateCollector(operation) {
6
+ const previous = activeCollector;
7
+ const collector = { defects: [], poisonedFields: new Set(), poisonedArtifactKinds: new Set() };
8
+ activeCollector = collector;
9
+ try {
10
+ const result = operation();
11
+ if (collector.defects.length === 0)
12
+ return result;
13
+ throwQualityGateDefects(collector.defects);
14
+ }
15
+ finally {
16
+ activeCollector = previous;
17
+ }
18
+ }
19
+ export function throwQualityGateDefects(defects) {
20
+ const uniqueDefects = [
21
+ ...new Map(defects.map((defect) => [`${defect.field}\u0000${defect.message}`, defect])).values(),
22
+ ];
23
+ const fields = uniqueDefects.slice(0, 25);
24
+ const truncated = uniqueDefects.length > fields.length;
25
+ const message = [
26
+ `Final quality gate has ${fields.length}${truncated ? "+" : ""} validation defects:`,
27
+ ...fields.map((item) => `- ${item.field}: ${item.message}`),
28
+ ].join("\n");
29
+ throw new UlwLoopError(message, "ULW_LOOP_QUALITY_GATE_INVALID", {
30
+ details: { field: fields[0]?.field, fields, ...(truncated ? { truncated: true } : {}) },
31
+ });
32
+ }
3
33
  export function invalid(message, field) {
4
- throw new UlwLoopError(message, "ULW_LOOP_QUALITY_GATE_INVALID", { details: { field } });
34
+ if (activeCollector === undefined)
35
+ throw new UlwLoopError(message, "ULW_LOOP_QUALITY_GATE_INVALID", { details: { field } });
36
+ activeCollector.defects.push({ field, message });
37
+ activeCollector.poisonedFields.add(field);
38
+ return undefined;
39
+ }
40
+ export function isPoisoned(field) {
41
+ return activeCollector?.poisonedFields.has(field) ?? false;
42
+ }
43
+ export function poisonField(field) {
44
+ activeCollector?.poisonedFields.add(field);
45
+ }
46
+ export function markPoisonedArtifactKind(id) {
47
+ activeCollector?.poisonedArtifactKinds.add(id);
48
+ }
49
+ export function isPoisonedArtifactKind(id) {
50
+ return activeCollector?.poisonedArtifactKinds.has(id) ?? false;
5
51
  }
6
52
  export function isRecord(value) {
7
53
  return typeof value === "object" && value !== null && !Array.isArray(value);
8
54
  }
9
55
  export function section(value, field) {
10
- return isRecord(value) ? value : invalid(`Final quality gate is missing ${field} evidence.`, field);
56
+ if (isRecord(value))
57
+ return value;
58
+ invalid(`Final quality gate is missing ${field} evidence.`, field);
59
+ return {};
11
60
  }
12
61
  export function textField(value, field) {
13
- if (typeof value !== "string" || value.trim() === "")
62
+ if (typeof value !== "string" || value.trim() === "") {
14
63
  invalid(`Final quality gate requires non-empty ${field}.`, field);
64
+ return "";
65
+ }
15
66
  const trimmed = value.trim();
16
67
  if (PLACEHOLDER_PATTERN.test(trimmed))
17
68
  invalid(`Final quality gate rejects placeholder ${field}.`, field);
18
69
  return trimmed;
19
70
  }
20
71
  export function numberField(value, field) {
21
- return typeof value === "number" && Number.isFinite(value)
22
- ? value
23
- : invalid(`Final quality gate requires numeric ${field}.`, field);
72
+ if (typeof value === "number" && Number.isFinite(value))
73
+ return value;
74
+ invalid(`Final quality gate requires numeric ${field}.`, field);
75
+ return 0;
24
76
  }
25
77
  export function stringArray(value, field) {
26
- if (!Array.isArray(value) || value.length === 0)
27
- return invalid(`Final quality gate requires ${field}.`, field);
78
+ if (!Array.isArray(value) || value.length === 0) {
79
+ invalid(`Final quality gate requires ${field}.`, field);
80
+ return [];
81
+ }
28
82
  return value.map((item) => textField(item, field));
29
83
  }
30
84
  export function emptyBlockers(value, field) {
31
85
  if (Array.isArray(value) && value.length === 0)
32
86
  return [];
33
87
  invalid(`${field} must be empty.`, field);
88
+ return [];
34
89
  }
35
90
  export function literal(value, expected, field) {
36
91
  if (value === expected)
37
92
  return expected;
38
93
  invalid(`${field} must be ${String(expected)}.`, field);
94
+ return expected;
39
95
  }
@@ -1,13 +1,16 @@
1
1
  import { invalid, literal, textField } from "./quality-gate-fields.js";
2
2
  export function passedVerdict(value, field) {
3
- if (value === "not_applicable")
3
+ if (value === "not_applicable") {
4
4
  invalid(`${field} must not be not_applicable.`, field);
5
+ return "passed";
6
+ }
5
7
  return literal(value, "passed", field);
6
8
  }
7
9
  export function codeQualityStatusField(value, field) {
8
10
  if (value === "CLEAR" || value === "WATCH")
9
11
  return value;
10
12
  invalid(`${field} must be CLEAR or WATCH.`, field);
13
+ return "CLEAR";
11
14
  }
12
15
  export function adversarialVerdict(row, field) {
13
16
  const value = row["verdict"];
@@ -17,4 +20,5 @@ export function adversarialVerdict(row, field) {
17
20
  return { verdict: "not_applicable", reason: textField(row["reason"], `${field}.reason`) };
18
21
  }
19
22
  invalid(`${field} must be passed or not_applicable with a reason.`, field);
23
+ return { verdict: "passed" };
20
24
  }
@@ -1,10 +1,13 @@
1
- import { aggregateQualityGateDefects } from "./quality-gate-aggregate.js";
2
1
  import { artifactCompatible, artifactMap, checkFile, parseArtifactRefs, referencedArtifacts, surfaceField, } from "./quality-gate-artifacts.js";
3
- import { emptyBlockers, invalid, literal, numberField, section, stringArray, textField, } from "./quality-gate-fields.js";
2
+ import { emptyBlockers, invalid, isPoisoned, isPoisonedArtifactKind, literal, numberField, section, stringArray, textField, withQualityGateCollector, } from "./quality-gate-fields.js";
4
3
  import { adversarialVerdict, codeQualityStatusField, passedVerdict } from "./quality-gate-verdicts.js";
5
- import { GATE_SECTION_BY_ACCEPTOR, REQUIRED_GATE_SECTIONS_BY_SURFACE } from "./surface.js";
4
+ import { GATE_SECTION_BY_ACCEPTOR } from "./surface.js";
6
5
  export { classifyExternalAuthorizationBlocker, clearGoalBlockerFields, normalizeBlockerEvidence, sameBlockerOccurrences, } from "./quality-gate-blockers.js";
7
6
  function reviewerRoleField(value, expected, field) {
7
+ if (typeof value !== "string" || value.trim() === "") {
8
+ textField(value, field);
9
+ return expected;
10
+ }
8
11
  const actual = textField(value, field);
9
12
  if (actual !== expected)
10
13
  invalid(`${field} must be ${expected}.`, field);
@@ -12,44 +15,51 @@ function reviewerRoleField(value, expected, field) {
12
15
  }
13
16
  function reviewerAcceptorField(value, surface, sectionName) {
14
17
  const field = `${sectionName}.by`;
15
- const actual = textField(value, field);
16
18
  const accepted = GATE_SECTION_BY_ACCEPTOR[surface][sectionName];
19
+ if (typeof value !== "string" || value.trim() === "") {
20
+ textField(value, field);
21
+ return accepted?.[0] ?? "";
22
+ }
23
+ const actual = textField(value, field);
17
24
  if (accepted === undefined || !accepted.includes(actual))
18
25
  invalid(`${field} must be one of ${accepted?.join(", ") ?? "the configured reviewers"}.`, field);
19
26
  return actual;
20
27
  }
21
28
  export function validateQualityGate(input, opts) {
29
+ return withQualityGateCollector(() => validateQualityGateUncollected(input, opts));
30
+ }
31
+ function validateQualityGateUncollected(input, opts) {
22
32
  const surface = opts?.reviewerSurface ?? "lazycodex";
23
- aggregateQualityGateDefects(input, opts);
24
33
  const gate = section(input, "qualityGate");
25
- const requiredSections = REQUIRED_GATE_SECTIONS_BY_SURFACE[surface];
26
- for (const sectionName of requiredSections)
27
- section(gate[sectionName], sectionName);
28
34
  if (surface === "omo-senpi" && gate["codeReview"] !== undefined)
29
35
  invalid("omo-senpi gate has no codeReview lane.", "codeReview");
30
36
  const manualQa = section(gate["manualQa"], "manualQa");
31
37
  const gateReview = section(gate["gateReview"], "gateReview");
32
38
  const iteration = section(gate["iteration"], "iteration");
33
39
  const coverage = section(gate["criteriaCoverage"], "criteriaCoverage");
40
+ const codeReview = surface === "lazycodex" ? section(gate["codeReview"], "codeReview") : {};
34
41
  const manualQaBy = reviewerAcceptorField(manualQa["by"], surface, "manualQa");
35
42
  const gateReviewBy = reviewerAcceptorField(gateReview["by"], surface, "gateReview");
43
+ const manualQaEvidence = textField(manualQa["evidence"], "manualQa.evidence");
44
+ const gateReviewEvidence = textField(gateReview["evidence"], "gateReview.evidence");
36
45
  if (surface === "lazycodex")
37
- reviewerRoleField(section(gate["codeReview"], "codeReview")["by"], "lazycodex-code-reviewer", "codeReview.by");
46
+ reviewerRoleField(codeReview?.["by"], "lazycodex-code-reviewer", "codeReview.by");
38
47
  const totalCriteria = numberField(coverage["totalCriteria"], "criteriaCoverage.totalCriteria");
39
48
  const passCount = numberField(coverage["passCount"], "criteriaCoverage.passCount");
40
- if (passCount < totalCriteria)
49
+ if (!isPoisoned("criteriaCoverage.passCount") && passCount < totalCriteria)
41
50
  invalid("criteriaCoverage.passCount must cover totalCriteria.", "criteriaCoverage.passCount");
42
51
  const artifactRefs = parseArtifactRefs(manualQa["artifactRefs"], opts);
43
52
  const byId = artifactMap(artifactRefs);
44
53
  const surfaceEvidence = parseSurfaceEvidence(manualQa["surfaceEvidence"], byId);
45
54
  const adversarialCases = parseAdversarialCases(manualQa["adversarialCases"], byId);
46
55
  const gateReportPath = textField(gateReview["reportPath"], "gateReview.reportPath");
47
- checkFile(gateReportPath, "gateReview.reportPath", opts);
56
+ if (!isPoisoned("gateReview.reportPath"))
57
+ checkFile(gateReportPath, "gateReview.reportPath", opts);
48
58
  const common = {
49
59
  manualQa: {
50
60
  by: manualQaBy,
51
61
  status: literal(manualQa["status"], "passed", "manualQa.status"),
52
- evidence: textField(manualQa["evidence"], "manualQa.evidence"),
62
+ evidence: manualQaEvidence,
53
63
  surfaceEvidence,
54
64
  adversarialCases,
55
65
  artifactRefs,
@@ -58,7 +68,7 @@ export function validateQualityGate(input, opts) {
58
68
  by: gateReviewBy,
59
69
  recommendation: literal(gateReview["recommendation"], "APPROVE", "gateReview.recommendation"),
60
70
  reportPath: gateReportPath,
61
- evidence: textField(gateReview["evidence"], "gateReview.evidence"),
71
+ evidence: gateReviewEvidence,
62
72
  blockers: emptyBlockers(gateReview["blockers"], "gateReview.blockers"),
63
73
  },
64
74
  iteration: {
@@ -78,7 +88,6 @@ export function validateQualityGate(input, opts) {
78
88
  };
79
89
  if (surface === "omo-senpi")
80
90
  return { surface, ...common };
81
- const codeReview = section(gate["codeReview"], "codeReview");
82
91
  const codeReportPath = textField(codeReview["reportPath"], "codeReview.reportPath");
83
92
  checkFile(codeReportPath, "codeReview.reportPath", opts);
84
93
  return {
@@ -95,13 +104,19 @@ export function validateQualityGate(input, opts) {
95
104
  };
96
105
  }
97
106
  function parseSurfaceEvidence(value, byId) {
98
- if (!Array.isArray(value) || value.length === 0)
107
+ if (!Array.isArray(value) || value.length === 0) {
99
108
  invalid("manualQa.surfaceEvidence must not be empty.", "manualQa.surfaceEvidence");
100
- return value.map((item, index) => {
109
+ return [];
110
+ }
111
+ return value.flatMap((item, index) => {
101
112
  const row = section(item, `manualQa.surfaceEvidence[${index}]`);
113
+ if (isPoisoned(`manualQa.surfaceEvidence[${index}]`))
114
+ return [];
102
115
  const surface = surfaceField(row["surface"], `manualQa.surfaceEvidence[${index}].surface`);
103
116
  const artifacts = referencedArtifacts(row["artifactRefs"], `manualQa.surfaceEvidence[${index}].artifactRefs`, byId);
104
117
  for (const artifact of artifacts) {
118
+ if (isPoisoned(`manualQa.surfaceEvidence[${index}].surface`) || isPoisonedArtifactKind(artifact.id))
119
+ continue;
105
120
  if (!artifactCompatible(surface, artifact.kind)) {
106
121
  invalid(`manualQa.surfaceEvidence ${surface} artifact ${artifact.kind} is incompatible.`, "manualQa.surfaceEvidence");
107
122
  }
@@ -117,10 +132,14 @@ function parseSurfaceEvidence(value, byId) {
117
132
  });
118
133
  }
119
134
  function parseAdversarialCases(value, byId) {
120
- if (!Array.isArray(value) || value.length === 0)
135
+ if (!Array.isArray(value) || value.length === 0) {
121
136
  invalid("manualQa.adversarialCases must not be empty.", "manualQa.adversarialCases");
122
- return value.map((item, index) => {
137
+ return [];
138
+ }
139
+ return value.flatMap((item, index) => {
123
140
  const row = section(item, `manualQa.adversarialCases[${index}]`);
141
+ if (isPoisoned(`manualQa.adversarialCases[${index}]`))
142
+ return [];
124
143
  const artifacts = referencedArtifacts(row["artifactRefs"], `manualQa.adversarialCases[${index}].artifactRefs`, byId);
125
144
  const verdictInfo = adversarialVerdict(row, `manualQa.adversarialCases[${index}]`);
126
145
  return {
@@ -7,7 +7,7 @@
7
7
  "type": "command",
8
8
  "command": "node \"${PLUGIN_ROOT}/dist/cli.js\" hook user-prompt-submit --with-ultrawork",
9
9
  "timeout": 10,
10
- "statusMessage": "(OmO 5.0.0-beta.33) Checking Ulw-Loop Steering"
10
+ "statusMessage": "(OmO 5.0.0-beta.35) Checking Ulw-Loop Steering"
11
11
  }
12
12
  ]
13
13
  }
@@ -20,7 +20,7 @@
20
20
  "type": "command",
21
21
  "command": "node \"${PLUGIN_ROOT}/dist/cli.js\" hook pre-tool-use",
22
22
  "timeout": 5,
23
- "statusMessage": "(OmO 5.0.0-beta.33) Enforcing Unlimited Ulw-Loop Budget"
23
+ "statusMessage": "(OmO 5.0.0-beta.35) Enforcing Unlimited Ulw-Loop Budget"
24
24
  }
25
25
  ]
26
26
  },
@@ -31,7 +31,7 @@
31
31
  "type": "command",
32
32
  "command": "node \"${PLUGIN_ROOT}/dist/cli.js\" hook pre-tool-use-spawn",
33
33
  "timeout": 5,
34
- "statusMessage": "(OmO 5.0.0-beta.33) Guarding Ulw-Loop Spawns"
34
+ "statusMessage": "(OmO 5.0.0-beta.35) Guarding Ulw-Loop Spawns"
35
35
  }
36
36
  ]
37
37
  }
@@ -43,7 +43,7 @@
43
43
  "type": "command",
44
44
  "command": "node \"${PLUGIN_ROOT}/dist/cli.js\" hook stop",
45
45
  "timeout": 10,
46
- "statusMessage": "(OmO 5.0.0-beta.33) Checking Ulw-Loop Resume"
46
+ "statusMessage": "(OmO 5.0.0-beta.35) Checking Ulw-Loop Resume"
47
47
  }
48
48
  ]
49
49
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@code-yeongyu/codex-ulw-loop",
3
- "version": "5.0.0-beta.33",
3
+ "version": "5.0.0-beta.35",
4
4
  "description": "Codex plugin: durable repo-native multi-goal orchestration with embedded success criteria and observable evidence audit.",
5
5
  "type": "module",
6
6
  "packageManager": "npm@11.12.1",
@@ -190,6 +190,7 @@ omo-agent-toolkit ulw-loop record-evidence --goal-id <g> --criterion-id <c> --st
190
190
  # Then use the harness update_goal tool with status complete.
191
191
  omo-agent-toolkit ulw-loop checkpoint --goal-id <g> --print-template --json
192
192
  # Fill the printed template: replace every placeholder and use real artifact paths under currentAttemptDir.
193
+ # codex-goal-json.goal.objective must equal the plan's codexObjective verbatim.
193
194
  omo-agent-toolkit ulw-loop checkpoint --goal-id <g> --status complete --evidence "..." --codex-goal-json <path> --quality-gate-json <path>
194
195
  omo-agent-toolkit ulw-loop complete-goals
195
196
  ```
@@ -214,7 +215,7 @@ omo-agent-toolkit ulw-loop checkpoint --goal-id <id> --status complete --evidenc
214
215
  {
215
216
  "codeReview":{"by":"lazycodex-code-reviewer","recommendation":"APPROVE","codeQualityStatus":"CLEAR","reportPath":"test/fixtures/artifacts/code-review.md","evidence":"Diff review passed.","blockers":[]},
216
217
  "manualQa":{"by":"lazycodex-qa-executor","status":"passed","evidence":"CLI and data surfaces passed.","surfaceEvidence":[{"id":"surface-cli-pass","criterionRef":"C1","surface":"cli","invocation":"omo-agent-toolkit ulw-loop checkpoint --quality-gate-json sample-quality-gate.json --json","verdict":"passed","artifactRefs":["artifact-cli-pass"]},{"id":"surface-data-pass","criterionRef":"C2","surface":"data","invocation":"diff -u before-ledger.json after-ledger.json","verdict":"passed","artifactRefs":["artifact-data-diff"]}],"adversarialCases":[{"id":"adv-malformed-input","criterionRef":"C3","scenario":"malformed gate input omits manual QA evidence","expectedBehavior":"validator rejects ULW_LOOP_QUALITY_GATE_INVALID","verdict":"passed","artifactRefs":["artifact-cli-reject"]}],"artifactRefs":[{"id":"artifact-cli-pass","kind":"cli-transcript","description":"CLI pass artifact.","path":"test/fixtures/artifacts/cli-pass.txt"},{"id":"artifact-cli-reject","kind":"log","description":"Reject log artifact.","path":"test/fixtures/artifacts/rejection.txt"},{"id":"artifact-data-diff","kind":"data-diff","description":"Data diff artifact.","path":"test/fixtures/artifacts/data-diff.txt"}]},
217
- "gateReview":{"by":"lazycodex-gate-reviewer","recommendation":"APPROVE","reportPath":"test/fixtures/artifacts/gate-review.md","evidence":"Gate review passed.","blockers":[]},
218
+ "gateReview":{"by":"lazycodex-gate-reviewer","recommendation":"APPROVE","reportPath":"test/fixtures/artifacts/gate-review.md","evidence":"Gate review passed.","blockers":[],"notes":[]},
218
219
  "iteration":{"fullRerun":true,"status":"passed","rerunCommands":["bunx vitest run packages/omo-codex/plugin/components/ulw-loop/test/quality-gate-doc.test.ts"],"evidence":"Focused rerun passed."},
219
220
  "criteriaCoverage":{"totalCriteria":3,"passCount":3,"originalIntent":"User wanted artifact-backed completion.","desiredOutcome":"Behavior ships with review and QA evidence.","userOutcomeReview":"Result matches brief and goals.","adversarialClassesCovered":["malformed_input","stale_state"]}
220
221
  }
@@ -0,0 +1,81 @@
1
+ import {
2
+ canReconcileActiveFinalTaskScopedAggregateSnapshot,
3
+ canReconcileCompletedTaskScopedAggregateSnapshot,
4
+ codexSnapshotMismatchError,
5
+ } from "./checkpoint-reconciliation.js";
6
+ import { readCodexGoalSnapshotInput, reconcileCodexGoalSnapshot } from "./codex-goal-snapshot.js";
7
+ import {
8
+ codexGoalMode,
9
+ compatibleCodexObjectives,
10
+ expectedCodexObjective,
11
+ isFinalRunCompletionCandidate,
12
+ } from "./goal-status.js";
13
+ import type { UlwLoopScope } from "./paths.js";
14
+ import type { UlwLoopItem, UlwLoopPlan } from "./types.js";
15
+ import { UlwLoopError } from "./types.js";
16
+
17
+ function normalizeObjective(value: string): string {
18
+ return value.replace(/\s+/g, " ").trim();
19
+ }
20
+
21
+ export async function validateCheckpointCodexGoal(input: {
22
+ readonly repoRoot: string;
23
+ readonly plan: UlwLoopPlan;
24
+ readonly goal: UlwLoopItem;
25
+ readonly raw: string | undefined;
26
+ readonly evidence: string;
27
+ readonly scope?: UlwLoopScope;
28
+ }): Promise<unknown> {
29
+ const aggregate = codexGoalMode(input.plan) === "aggregate";
30
+ const final = isFinalRunCompletionCandidate(input.plan, input.goal);
31
+ const snapshot = await readCodexGoalSnapshotInput(input.raw, input.repoRoot);
32
+ const expectedObjective = expectedCodexObjective(input.plan, input.goal);
33
+ const reconciliation = reconcileCodexGoalSnapshot(snapshot, {
34
+ expectedObjective,
35
+ ...(aggregate ? { acceptedObjectives: compatibleCodexObjectives(input.plan) } : {}),
36
+ allowedStatuses: aggregate ? (final ? ["complete"] : ["active"]) : ["complete"],
37
+ requireSnapshot: true,
38
+ requireComplete: !aggregate || final,
39
+ });
40
+ if (reconciliation.ok) return reconciliation.snapshot.raw;
41
+ const objective = snapshot?.objective;
42
+ const mismatchedTaskObjective =
43
+ snapshot?.available === true &&
44
+ objective !== undefined &&
45
+ normalizeObjective(objective) !== normalizeObjective(expectedObjective);
46
+ const completedTaskScoped =
47
+ mismatchedTaskObjective &&
48
+ snapshot.status === "complete" &&
49
+ (await canReconcileCompletedTaskScopedAggregateSnapshot(
50
+ input.repoRoot,
51
+ input.plan,
52
+ input.goal,
53
+ objective,
54
+ input.evidence,
55
+ input.scope,
56
+ ));
57
+ const activeFinalTaskScoped =
58
+ mismatchedTaskObjective &&
59
+ snapshot.status === "active" &&
60
+ (await canReconcileActiveFinalTaskScopedAggregateSnapshot(
61
+ input.repoRoot,
62
+ input.plan,
63
+ input.goal,
64
+ objective,
65
+ input.evidence,
66
+ input.scope,
67
+ ));
68
+ if (completedTaskScoped || activeFinalTaskScoped) return reconciliation.snapshot.raw;
69
+ throw codexSnapshotMismatchError({
70
+ reconciliation,
71
+ snapshot,
72
+ expectedObjective,
73
+ taskScopedHint: { goal: input.goal, aggregate, final },
74
+ });
75
+ }
76
+
77
+ export function combineCheckpointValidationErrors(codexError: UlwLoopError, gateError: UlwLoopError): UlwLoopError {
78
+ return new UlwLoopError(`${codexError.message}\n${gateError.message}`, "ULW_LOOP_QUALITY_GATE_INVALID", {
79
+ details: { ...(codexError.details ?? {}), ...(gateError.details ?? {}) },
80
+ });
81
+ }
@@ -16,6 +16,12 @@ export interface CodexSnapshotMismatchInput {
16
16
  readonly taskScopedHint?: { readonly goal: UlwLoopItem; readonly aggregate: boolean; readonly final: boolean };
17
17
  }
18
18
 
19
+ export function combineCheckpointValidationErrors(codexError: UlwLoopError, gateError: UlwLoopError): UlwLoopError {
20
+ return new UlwLoopError(`${codexError.message}\n${gateError.message}`, "ULW_LOOP_QUALITY_GATE_INVALID", {
21
+ details: { ...(codexError.details ?? {}), ...(gateError.details ?? {}) },
22
+ });
23
+ }
24
+
19
25
  export function codexSnapshotMismatchError(input: CodexSnapshotMismatchInput): UlwLoopError {
20
26
  const { reconciliation, snapshot, taskScopedHint } = input;
21
27
  const hint =