oh-my-opencode 4.16.3 → 4.17.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (220) hide show
  1. package/.agents/command/omomomo.md +1 -1
  2. package/.agents/command/publish.md +31 -58
  3. package/.agents/skills/hyperplan/SKILL.md +3 -3
  4. package/.agents/skills/omomomo/SKILL.md +1 -1
  5. package/.agents/skills/pre-publish-review/SKILL.md +1 -1
  6. package/.agents/skills/publish/SKILL.md +32 -59
  7. package/.opencode/command/omomomo.md +1 -1
  8. package/.opencode/command/publish.md +31 -58
  9. package/.opencode/skills/hyperplan/SKILL.md +3 -3
  10. package/.opencode/skills/pre-publish-review/SKILL.md +4 -1
  11. package/README.ja.md +4 -4
  12. package/README.ko.md +4 -4
  13. package/README.md +2 -2
  14. package/README.ru.md +4 -4
  15. package/README.zh-cn.md +4 -4
  16. package/dist/cli/index.js +195 -76
  17. package/dist/cli-node/index.js +195 -76
  18. package/dist/features/team-mode/test-support/async-test-helpers.d.ts +2 -2
  19. package/dist/index.js +310 -299
  20. package/dist/plugin/chat-message/types.d.ts +4 -0
  21. package/dist/plugin/command-execute-before.d.ts +1 -0
  22. package/dist/plugin/stop-continuation.d.ts +17 -0
  23. package/dist/skills/remove-ai-slops/SKILL.md +2 -2
  24. package/dist/skills/review-work/SKILL.md +10 -2
  25. package/dist/skills/start-work/SKILL.md +6 -3
  26. package/dist/skills/ultimate-browsing/ATTRIBUTION.md +2 -2
  27. package/dist/skills/ultimate-browsing/engine/templates/package.json +1 -1
  28. package/dist/skills/ultimate-browsing/references/chrome-stealth.md +11 -11
  29. package/dist/skills/ulw-plan/SKILL.md +2 -1
  30. package/dist/skills/ulw-plan/references/full-workflow.md +1 -1
  31. package/dist/skills/ulw-plan/references/intent-unclear.md +4 -4
  32. package/dist/skills/ulw-plan/scripts/scaffold-plan.mjs +2 -2
  33. package/dist/skills/visual-qa/SKILL.md +9 -5
  34. package/dist/tui.js +79 -32
  35. package/package.json +15 -14
  36. package/packages/git-bash-mcp/package.json +27 -0
  37. package/packages/lsp-daemon/dist/cli.js +7 -13
  38. package/packages/lsp-daemon/dist/daemon-client.js +3 -5
  39. package/packages/lsp-daemon/dist/index.js +12 -18
  40. package/packages/lsp-daemon/dist/request-routing.js +6 -8
  41. package/packages/omo-codex/plugin/.codex-plugin/plugin.json +3 -1
  42. package/packages/omo-codex/plugin/components/bootstrap/dist/cli.js +1080 -1017
  43. package/packages/omo-codex/plugin/components/bootstrap/hooks/hooks.json +1 -1
  44. package/packages/omo-codex/plugin/components/bootstrap/package.json +1 -1
  45. package/packages/omo-codex/plugin/components/codegraph/package.json +1 -1
  46. package/packages/omo-codex/plugin/components/comment-checker/hooks/hooks.json +1 -1
  47. package/packages/omo-codex/plugin/components/comment-checker/package.json +1 -1
  48. package/packages/omo-codex/plugin/components/git-bash/hooks/hooks.json +2 -2
  49. package/packages/omo-codex/plugin/components/git-bash/package.json +1 -1
  50. package/packages/omo-codex/plugin/components/lazycodex-executor-verify/AGENTS.md +2 -2
  51. package/packages/omo-codex/plugin/components/lazycodex-executor-verify/dist/cli.js +6 -2
  52. package/packages/omo-codex/plugin/components/lazycodex-executor-verify/dist/codex-hook.js +6 -2
  53. package/packages/omo-codex/plugin/components/lazycodex-executor-verify/hooks/hooks.json +2 -2
  54. package/packages/omo-codex/plugin/components/lazycodex-executor-verify/package.json +1 -1
  55. package/packages/omo-codex/plugin/components/lazycodex-executor-verify/src/codex-hook.ts +6 -2
  56. package/packages/omo-codex/plugin/components/lazycodex-executor-verify/test/cli.test.ts +1 -1
  57. package/packages/omo-codex/plugin/components/lazycodex-executor-verify/test/codex-hook.test.ts +67 -2
  58. package/packages/omo-codex/plugin/components/lsp/dist/cli.js +14 -14
  59. package/packages/omo-codex/plugin/components/lsp/hooks/hooks.json +2 -2
  60. package/packages/omo-codex/plugin/components/lsp/package.json +1 -1
  61. package/packages/omo-codex/plugin/components/lsp/test/package-smoke.test.ts +0 -13
  62. package/packages/omo-codex/plugin/components/rules/bundled-rules/hephaestus/gpt-5.5.md +2 -2
  63. package/packages/omo-codex/plugin/components/rules/bundled-rules/hephaestus/gpt-5.6.md +3 -3
  64. package/packages/omo-codex/plugin/components/rules/hooks/hooks.json +4 -4
  65. package/packages/omo-codex/plugin/components/rules/package.json +1 -1
  66. package/packages/omo-codex/plugin/components/start-work-continuation/hooks/hooks.json +2 -2
  67. package/packages/omo-codex/plugin/components/start-work-continuation/package.json +1 -1
  68. package/packages/omo-codex/plugin/components/start-work-continuation/test/codex-hook.test.ts +1 -79
  69. package/packages/omo-codex/plugin/components/teammode/AGENTS.md +2 -2
  70. package/packages/omo-codex/plugin/components/teammode/hooks/hooks.json +1 -1
  71. package/packages/omo-codex/plugin/components/teammode/package.json +1 -1
  72. package/packages/omo-codex/plugin/components/teammode/skills/teammode/SKILL.md +33 -16
  73. package/packages/omo-codex/plugin/components/teammode/skills/teammode/scripts/team.mjs +2 -1
  74. package/packages/omo-codex/plugin/components/teammode/test/v2-spawn-schema.test.ts +69 -0
  75. package/packages/omo-codex/plugin/components/telemetry/hooks/hooks.json +1 -1
  76. package/packages/omo-codex/plugin/components/telemetry/package.json +1 -1
  77. package/packages/omo-codex/plugin/components/ultrawork/agents/explorer.toml +2 -2
  78. package/packages/omo-codex/plugin/components/ultrawork/agents/lazycodex-code-reviewer.toml +2 -2
  79. package/packages/omo-codex/plugin/components/ultrawork/agents/lazycodex-gate-reviewer.toml +6 -6
  80. package/packages/omo-codex/plugin/components/ultrawork/agents/lazycodex-qa-executor.toml +5 -5
  81. package/packages/omo-codex/plugin/components/ultrawork/agents/lazycodex-worker-high.toml +26 -0
  82. package/packages/omo-codex/plugin/components/ultrawork/agents/{lazycodex-executor.toml → lazycodex-worker-low.toml} +6 -4
  83. package/packages/omo-codex/plugin/components/ultrawork/agents/lazycodex-worker-medium.toml +26 -0
  84. package/packages/omo-codex/plugin/components/ultrawork/agents/librarian.toml +2 -2
  85. package/packages/omo-codex/plugin/components/ultrawork/agents/plan.toml +7 -7
  86. package/packages/omo-codex/plugin/components/ultrawork/directive.md +76 -37
  87. package/packages/omo-codex/plugin/components/ultrawork/hooks/hooks.json +1 -1
  88. package/packages/omo-codex/plugin/components/ultrawork/package.json +1 -1
  89. package/packages/omo-codex/plugin/components/ultrawork/skills/ultrawork/SKILL.md +76 -37
  90. package/packages/omo-codex/plugin/components/ultrawork/skills/ulw-plan/SKILL.md +2 -1
  91. package/packages/omo-codex/plugin/components/ultrawork/skills/ulw-plan/references/full-workflow.md +1 -1
  92. package/packages/omo-codex/plugin/components/ultrawork/skills/ulw-plan/references/intent-unclear.md +4 -4
  93. package/packages/omo-codex/plugin/components/ultrawork/skills/ulw-plan/scripts/scaffold-plan.mjs +2 -2
  94. package/packages/omo-codex/plugin/components/ultrawork/test/codex-hook.test.ts +25 -0
  95. package/packages/omo-codex/plugin/components/ultrawork/test/package-smoke.test.ts +0 -68
  96. package/packages/omo-codex/plugin/components/ulw-loop/AGENTS.md +1 -1
  97. package/packages/omo-codex/plugin/components/ulw-loop/CHANGELOG.md +2 -0
  98. package/packages/omo-codex/plugin/components/ulw-loop/README.md +3 -1
  99. package/packages/omo-codex/plugin/components/ulw-loop/directive.md +76 -37
  100. package/packages/omo-codex/plugin/components/ulw-loop/dist/checkpoint.js +6 -1
  101. package/packages/omo-codex/plugin/components/ulw-loop/dist/cli-subcommands.d.ts +1 -1
  102. package/packages/omo-codex/plugin/components/ulw-loop/dist/cli-subcommands.js +13 -2
  103. package/packages/omo-codex/plugin/components/ulw-loop/dist/cli.js +405 -25
  104. package/packages/omo-codex/plugin/components/ulw-loop/dist/codex-goal-instruction.js +12 -1
  105. package/packages/omo-codex/plugin/components/ulw-loop/dist/domain-types.d.ts +4 -2
  106. package/packages/omo-codex/plugin/components/ulw-loop/dist/paths.d.ts +7 -0
  107. package/packages/omo-codex/plugin/components/ulw-loop/dist/paths.js +16 -1
  108. package/packages/omo-codex/plugin/components/ulw-loop/dist/plan-crud.js +1 -0
  109. package/packages/omo-codex/plugin/components/ulw-loop/dist/quality-gate-verdicts.d.ts +6 -0
  110. package/packages/omo-codex/plugin/components/ulw-loop/dist/quality-gate-verdicts.js +20 -0
  111. package/packages/omo-codex/plugin/components/ulw-loop/dist/quality-gate.d.ts +1 -0
  112. package/packages/omo-codex/plugin/components/ulw-loop/dist/quality-gate.js +12 -9
  113. package/packages/omo-codex/plugin/components/ulw-loop/dist/spawn-guard.d.ts +3 -0
  114. package/packages/omo-codex/plugin/components/ulw-loop/dist/spawn-guard.js +148 -0
  115. package/packages/omo-codex/plugin/components/ulw-loop/dist/stop-resume-hook.d.ts +2 -0
  116. package/packages/omo-codex/plugin/components/ulw-loop/dist/stop-resume-hook.js +209 -0
  117. package/packages/omo-codex/plugin/components/ulw-loop/hooks/hooks.json +25 -2
  118. package/packages/omo-codex/plugin/components/ulw-loop/package.json +1 -1
  119. package/packages/omo-codex/plugin/components/ulw-loop/skills/ulw-loop/SKILL.md +14 -15
  120. package/packages/omo-codex/plugin/components/ulw-loop/skills/ulw-loop/references/full-workflow.md +30 -28
  121. package/packages/omo-codex/plugin/components/ulw-loop/src/checkpoint.ts +6 -1
  122. package/packages/omo-codex/plugin/components/ulw-loop/src/cli-subcommands.ts +14 -3
  123. package/packages/omo-codex/plugin/components/ulw-loop/src/cli.ts +10 -0
  124. package/packages/omo-codex/plugin/components/ulw-loop/src/codex-goal-instruction.ts +12 -1
  125. package/packages/omo-codex/plugin/components/ulw-loop/src/domain-types.ts +4 -2
  126. package/packages/omo-codex/plugin/components/ulw-loop/src/paths.ts +27 -1
  127. package/packages/omo-codex/plugin/components/ulw-loop/src/plan-crud.ts +1 -0
  128. package/packages/omo-codex/plugin/components/ulw-loop/src/quality-gate-verdicts.ts +23 -0
  129. package/packages/omo-codex/plugin/components/ulw-loop/src/quality-gate.ts +16 -9
  130. package/packages/omo-codex/plugin/components/ulw-loop/src/spawn-guard.ts +138 -0
  131. package/packages/omo-codex/plugin/components/ulw-loop/src/stop-resume-hook.ts +208 -0
  132. package/packages/omo-codex/plugin/components/ulw-loop/test/cli-create-goals.test.ts +12 -0
  133. package/packages/omo-codex/plugin/components/ulw-loop/test/cli-entrypoint.test.ts +4 -1
  134. package/packages/omo-codex/plugin/components/ulw-loop/test/codex-goal-instruction.test.ts +8 -35
  135. package/packages/omo-codex/plugin/components/ulw-loop/test/fixtures/quality-gate-builder.ts +24 -13
  136. package/packages/omo-codex/plugin/components/ulw-loop/test/package-smoke.test.ts +5 -2
  137. package/packages/omo-codex/plugin/components/ulw-loop/test/paths.test.ts +43 -8
  138. package/packages/omo-codex/plugin/components/ulw-loop/test/quality-gate.test.ts +55 -2
  139. package/packages/omo-codex/plugin/components/ulw-loop/test/spawn-guard.test.ts +228 -0
  140. package/packages/omo-codex/plugin/components/ulw-loop/test/stop-resume-hook.test.ts +193 -0
  141. package/packages/omo-codex/plugin/hooks/post-compact-resetting-git-bash-mcp-reminder.json +1 -1
  142. package/packages/omo-codex/plugin/hooks/post-compact-resetting-lsp-diagnostics-cache.json +1 -1
  143. package/packages/omo-codex/plugin/hooks/post-compact-resetting-project-rule-cache.json +1 -1
  144. package/packages/omo-codex/plugin/hooks/post-tool-use-checking-codegraph-init-guidance.json +1 -1
  145. package/packages/omo-codex/plugin/hooks/post-tool-use-checking-comments.json +1 -1
  146. package/packages/omo-codex/plugin/hooks/post-tool-use-checking-lsp-diagnostics.json +1 -1
  147. package/packages/omo-codex/plugin/hooks/post-tool-use-checking-thread-title-hygiene.json +1 -1
  148. package/packages/omo-codex/plugin/hooks/post-tool-use-matching-project-rules.json +1 -1
  149. package/packages/omo-codex/plugin/hooks/pre-tool-use-enforcing-unlimited-goal-budget.json +1 -1
  150. package/packages/omo-codex/plugin/hooks/pre-tool-use-guarding-ulw-loop-spawns.json +18 -0
  151. package/packages/omo-codex/plugin/hooks/pre-tool-use-recommending-git-bash-mcp.json +1 -1
  152. package/packages/omo-codex/plugin/hooks/session-start-checking-auto-update.json +1 -1
  153. package/packages/omo-codex/plugin/hooks/session-start-checking-bootstrap-provisioning.json +1 -1
  154. package/packages/omo-codex/plugin/hooks/session-start-checking-codegraph-bootstrap.json +1 -1
  155. package/packages/omo-codex/plugin/hooks/session-start-loading-project-rules.json +1 -1
  156. package/packages/omo-codex/plugin/hooks/session-start-recording-session-telemetry.json +1 -1
  157. package/packages/omo-codex/plugin/hooks/stop-checking-start-work-continuation.json +1 -1
  158. package/packages/omo-codex/plugin/hooks/stop-checking-ulw-loop-resume.json +17 -0
  159. package/packages/omo-codex/plugin/hooks/subagent-stop-checking-start-work-continuation.json +1 -1
  160. package/packages/omo-codex/plugin/hooks/subagent-stop-verifying-lazycodex-executor-evidence.json +2 -2
  161. package/packages/omo-codex/plugin/hooks/user-prompt-submit-checking-ultrawork-trigger.json +1 -1
  162. package/packages/omo-codex/plugin/hooks/user-prompt-submit-checking-ulw-loop-steering.json +1 -1
  163. package/packages/omo-codex/plugin/hooks/user-prompt-submit-loading-project-rules.json +1 -1
  164. package/packages/omo-codex/plugin/model-catalog.json +16 -7
  165. package/packages/omo-codex/plugin/package-lock.json +13 -13
  166. package/packages/omo-codex/plugin/package.json +1 -1
  167. package/packages/omo-codex/plugin/scripts/migrate-codex-config/catalog.mjs +16 -7
  168. package/packages/omo-codex/plugin/scripts/sync-skills.mjs +8 -2
  169. package/packages/omo-codex/plugin/skills/init-deep/SKILL.md +2 -2
  170. package/packages/omo-codex/plugin/skills/refactor/SKILL.md +2 -2
  171. package/packages/omo-codex/plugin/skills/remove-ai-slops/SKILL.md +4 -4
  172. package/packages/omo-codex/plugin/skills/review-work/SKILL.md +18 -4
  173. package/packages/omo-codex/plugin/skills/start-work/SKILL.md +6 -3
  174. package/packages/omo-codex/plugin/skills/teammode/SKILL.md +33 -16
  175. package/packages/omo-codex/plugin/skills/teammode/scripts/team.mjs +2 -1
  176. package/packages/omo-codex/plugin/skills/ultimate-browsing/ATTRIBUTION.md +2 -2
  177. package/packages/omo-codex/plugin/skills/ultimate-browsing/engine/templates/package.json +1 -1
  178. package/packages/omo-codex/plugin/skills/ultimate-browsing/references/chrome-stealth.md +11 -11
  179. package/packages/omo-codex/plugin/skills/ultrawork/SKILL.md +76 -37
  180. package/packages/omo-codex/plugin/skills/ulw-loop/SKILL.md +14 -15
  181. package/packages/omo-codex/plugin/skills/ulw-loop/references/full-workflow.md +30 -28
  182. package/packages/omo-codex/plugin/skills/ulw-plan/SKILL.md +2 -1
  183. package/packages/omo-codex/plugin/skills/ulw-plan/references/full-workflow.md +1 -1
  184. package/packages/omo-codex/plugin/skills/ulw-plan/references/intent-unclear.md +4 -4
  185. package/packages/omo-codex/plugin/skills/ulw-plan/scripts/scaffold-plan.mjs +2 -2
  186. package/packages/omo-codex/plugin/skills/ulw-research/SKILL.md +2 -2
  187. package/packages/omo-codex/plugin/skills/visual-qa/SKILL.md +11 -7
  188. package/packages/omo-codex/plugin/test/aggregate-agents.test.mjs +76 -16
  189. package/packages/omo-codex/plugin/test/aggregate-hooks.test.mjs +23 -2
  190. package/packages/omo-codex/plugin/test/aggregate-manifest.test.mjs +1 -1
  191. package/packages/omo-codex/plugin/test/aggregate-model-catalog.test.mjs +4 -4
  192. package/packages/omo-codex/plugin/test/auto-update.test.mjs +4 -4
  193. package/packages/omo-codex/plugin/test/component-hook-contract-cases.mjs +2 -2
  194. package/packages/omo-codex/plugin/test/lcx-bug-skills.test.mjs +4 -101
  195. package/packages/omo-codex/plugin/test/migrate-codex-config.test.mjs +14 -14
  196. package/packages/omo-codex/plugin/test/sync-skills-orchestration.test.mjs +11 -0
  197. package/packages/omo-codex/plugin/test/sync-skills-test-support.mjs +1 -1
  198. package/packages/omo-codex/plugin/test/sync-skills.test.mjs +5 -3
  199. package/packages/omo-codex/plugin/test/teammode-transport.test.mjs +25 -0
  200. package/packages/omo-codex/plugin/test/ulw-plan-scope-contract.test.mjs +24 -0
  201. package/packages/omo-codex/plugin/test/ulw-plan-skill-contract.test.mjs +9 -40
  202. package/packages/omo-codex/plugin/test/ulw-research-skill-contract.test.mjs +4 -277
  203. package/packages/omo-codex/scripts/install-config-reasoning.test.mjs +5 -5
  204. package/packages/omo-codex/scripts/install-config.test.mjs +22 -3
  205. package/packages/omo-codex/scripts/install-dist/install-local.mjs +98 -30
  206. package/packages/shared-skills/skills/remove-ai-slops/SKILL.md +2 -2
  207. package/packages/shared-skills/skills/review-work/SKILL.md +10 -2
  208. package/packages/shared-skills/skills/start-work/SKILL.md +6 -3
  209. package/packages/shared-skills/skills/ultimate-browsing/ATTRIBUTION.md +2 -2
  210. package/packages/shared-skills/skills/ultimate-browsing/engine/templates/package.json +1 -1
  211. package/packages/shared-skills/skills/ultimate-browsing/references/chrome-stealth.md +11 -11
  212. package/packages/shared-skills/skills/ulw-plan/SKILL.md +2 -1
  213. package/packages/shared-skills/skills/ulw-plan/references/full-workflow.md +1 -1
  214. package/packages/shared-skills/skills/ulw-plan/references/intent-unclear.md +4 -4
  215. package/packages/shared-skills/skills/ulw-plan/scripts/scaffold-plan.mjs +2 -2
  216. package/packages/shared-skills/skills/visual-qa/SKILL.md +9 -5
  217. package/dist/skills/visual-qa/scripts/skill-prompt-contract.test.ts +0 -296
  218. package/packages/omo-codex/plugin/components/ulw-loop/test/skill-contract.test.ts +0 -70
  219. package/packages/omo-codex/plugin/test/ulw-research-epistemic-contract.test.mjs +0 -98
  220. package/packages/shared-skills/skills/visual-qa/scripts/skill-prompt-contract.test.ts +0 -296
@@ -209,17 +209,8 @@ function normalizeCodexGoalMode(value) {
209
209
  // components/ulw-loop/src/cli-subcommands.ts
210
210
  import { readFile as readFile6 } from "node:fs/promises";
211
211
 
212
- // components/ulw-loop/src/checkpoint.ts
213
- import { existsSync as existsSync3, statSync } from "node:fs";
214
- import { readFile as readFile5 } from "node:fs/promises";
215
- import { resolve as resolve3 } from "node:path";
216
-
217
- // components/ulw-loop/src/checkpoint-reconciliation.ts
218
- import { existsSync } from "node:fs";
219
- import { readFile as readFile2 } from "node:fs/promises";
220
-
221
212
  // components/ulw-loop/src/paths.ts
222
- import { join } from "node:path";
213
+ import { isAbsolute, join, relative, sep } from "node:path";
223
214
  var SESSION_ENV_KEYS = ["OMO_ULW_LOOP_SESSION_ID", "CODEX_SESSION_ID", "CODEX_THREAD_ID"];
224
215
  function normalizeUlwLoopSessionId(sessionId) {
225
216
  const trimmed = sessionId?.trim();
@@ -271,6 +262,28 @@ function repoRelative(absolutePath, repoRoot) {
271
262
  return absolutePath.slice(backslashPrefix.length).split("\\").join("/");
272
263
  return absolutePath.split("\\").join("/");
273
264
  }
265
+ function ulwLoopAttemptEvidenceDir(goalId, attempt, scope) {
266
+ const sessionId = normalizeUlwLoopSessionId(scope?.sessionId) ?? resolveUlwLoopSessionIdFromEnv() ?? "session";
267
+ return `.omo/evidence/ulw/${sessionId}/${goalId}/a${attempt}`;
268
+ }
269
+ var PLATFORM_PATH_API = { relative, isAbsolute, sep };
270
+ function isWithinAttemptDir(absolutePath, attemptRoot, pathApi = PLATFORM_PATH_API) {
271
+ const relativePath = pathApi.relative(attemptRoot, absolutePath);
272
+ if (relativePath === "")
273
+ return true;
274
+ if (relativePath === ".." || relativePath.startsWith(`..${pathApi.sep}`))
275
+ return false;
276
+ return !pathApi.isAbsolute(relativePath);
277
+ }
278
+
279
+ // components/ulw-loop/src/checkpoint.ts
280
+ import { existsSync as existsSync3, statSync } from "node:fs";
281
+ import { readFile as readFile5 } from "node:fs/promises";
282
+ import { resolve as resolve3 } from "node:path";
283
+
284
+ // components/ulw-loop/src/checkpoint-reconciliation.ts
285
+ import { existsSync } from "node:fs";
286
+ import { readFile as readFile2 } from "node:fs/promises";
274
287
 
275
288
  // components/ulw-loop/src/goal-status.ts
276
289
  var ULW_LOOP_AGGREGATE_CODEX_OBJECTIVE = aggregateCodexObjectiveForScope();
@@ -779,6 +792,27 @@ function literal(value, expected, field) {
779
792
  invalid(`${field} must be ${String(expected)}.`, field);
780
793
  }
781
794
 
795
+ // components/ulw-loop/src/quality-gate-verdicts.ts
796
+ function passedVerdict(value, field) {
797
+ if (value === "not_applicable")
798
+ invalid(`${field} must not be not_applicable.`, field);
799
+ return literal(value, "passed", field);
800
+ }
801
+ function codeQualityStatusField(value, field) {
802
+ if (value === "CLEAR" || value === "WATCH")
803
+ return value;
804
+ invalid(`${field} must be CLEAR or WATCH.`, field);
805
+ }
806
+ function adversarialVerdict(row, field) {
807
+ const value = row["verdict"];
808
+ if (value === "passed")
809
+ return { verdict: "passed" };
810
+ if (value === "not_applicable") {
811
+ return { verdict: "not_applicable", reason: textField(row["reason"], `${field}.reason`) };
812
+ }
813
+ invalid(`${field} must be passed or not_applicable with a reason.`, field);
814
+ }
815
+
782
816
  // components/ulw-loop/src/quality-gate-blockers.ts
783
817
  var BLOCKER_FIELD_KEYS = "blocker blockerSignature blockerEvidence blockerOccurrences blockedAt".split(" ");
784
818
  var URL_PATTERN = /https?:\/\/\S+/g;
@@ -843,11 +877,6 @@ function kindField(value, field) {
843
877
  return value;
844
878
  invalid(`${field} must be a supported artifact kind.`, field);
845
879
  }
846
- function passedVerdict(value, field) {
847
- if (value === "not_applicable")
848
- invalid(`${field} must not be not_applicable.`, field);
849
- return literal(value, "passed", field);
850
- }
851
880
  function artifactCompatible(surface, kind) {
852
881
  switch (surface) {
853
882
  case "cli":
@@ -870,9 +899,13 @@ function checkFile(path, field, opts) {
870
899
  const absolute = resolve2(opts.repoRoot, path);
871
900
  if (!opts.fs.existsSync(absolute))
872
901
  invalid(`${field} must point to an existing artifact.`, field);
873
- const stat = opts.fs.statSync(absolute);
874
- if (stat.size <= 0)
902
+ if (opts.fs.statSync(absolute).size <= 0)
875
903
  invalid(`${field} must point to a non-empty artifact.`, field);
904
+ if (opts.currentAttemptDir !== undefined) {
905
+ const attemptRoot = resolve2(opts.repoRoot, opts.currentAttemptDir);
906
+ if (!isWithinAttemptDir(absolute, attemptRoot))
907
+ invalid(`${field} (${path}) must point to an artifact from the current attempt (${opts.currentAttemptDir}).`, field);
908
+ }
876
909
  }
877
910
  function artifactMap(refs) {
878
911
  const byId = new Map;
@@ -929,7 +962,7 @@ function validateQualityGate(input, opts) {
929
962
  codeReview: {
930
963
  by: reviewerRoleField(codeReview["by"], REVIEWER_ROLES.codeReview, "codeReview.by"),
931
964
  recommendation: literal(codeReview["recommendation"], "APPROVE", "codeReview.recommendation"),
932
- codeQualityStatus: literal(codeReview["codeQualityStatus"], "CLEAR", "codeReview.codeQualityStatus"),
965
+ codeQualityStatus: codeQualityStatusField(codeReview["codeQualityStatus"], "codeReview.codeQualityStatus"),
933
966
  reportPath: codeReportPath,
934
967
  evidence: textField(codeReview["evidence"], "codeReview.evidence"),
935
968
  blockers: emptyBlockers(codeReview["blockers"], "codeReview.blockers")
@@ -993,12 +1026,14 @@ function parseAdversarialCases(value, byId) {
993
1026
  return value.map((item, index) => {
994
1027
  const row = section(item, `manualQa.adversarialCases[${index}]`);
995
1028
  const artifacts = referencedArtifacts(row["artifactRefs"], `manualQa.adversarialCases[${index}].artifactRefs`, byId);
1029
+ const verdictInfo = adversarialVerdict(row, `manualQa.adversarialCases[${index}]`);
996
1030
  return {
997
1031
  id: textField(row["id"], `manualQa.adversarialCases[${index}].id`),
998
1032
  criterionRef: textField(row["criterionRef"], `manualQa.adversarialCases[${index}].criterionRef`),
999
1033
  scenario: textField(row["scenario"], `manualQa.adversarialCases[${index}].scenario`),
1000
1034
  expectedBehavior: textField(row["expectedBehavior"], `manualQa.adversarialCases[${index}].expectedBehavior`),
1001
- verdict: passedVerdict(row["verdict"], `manualQa.adversarialCases[${index}].verdict`),
1035
+ verdict: verdictInfo.verdict,
1036
+ ...verdictInfo.reason === undefined ? {} : { reason: verdictInfo.reason },
1002
1037
  artifactRefs: artifacts.map((artifact) => artifact.id)
1003
1038
  };
1004
1039
  });
@@ -1074,12 +1109,13 @@ function ledgerKind2(status, goal, aggregateCompletion) {
1074
1109
  return status === "blocked" ? "goal_blocked" : "goal_failed";
1075
1110
  }
1076
1111
  function buildLedger(now, args, goal, qualityGate, codexGoal, aggregateCompletion) {
1112
+ const watch = qualityGate?.codeReview.codeQualityStatus === "WATCH";
1077
1113
  const entry = {
1078
1114
  at: now,
1079
1115
  kind: ledgerKind2(args.status, goal, aggregateCompletion),
1080
1116
  goalId: goal.id,
1081
1117
  status: goal.status,
1082
- evidence: args.evidence
1118
+ evidence: watch ? `${args.evidence} | codeQuality=WATCH: ${qualityGate.codeReview.evidence}` : args.evidence
1083
1119
  };
1084
1120
  if (codexGoal !== undefined)
1085
1121
  entry.codexGoal = codexGoal;
@@ -1135,7 +1171,8 @@ async function checkpointUlwLoop(repoRoot, args, scope) {
1135
1171
  if (final || aggregateCompletion !== undefined)
1136
1172
  qualityGate = validateQualityGate(await readJsonInput2(args.qualityGateJson, repoRoot), {
1137
1173
  repoRoot,
1138
- fs: QUALITY_GATE_FS
1174
+ fs: QUALITY_GATE_FS,
1175
+ ...plan.evidenceLayoutVersion === 2 ? { currentAttemptDir: ulwLoopAttemptEvidenceDir(goal.id, goal.attempt, scope) } : {}
1139
1176
  });
1140
1177
  goal.status = "complete";
1141
1178
  goal.completedAt = now;
@@ -1391,6 +1428,7 @@ function buildText(mode, plan, goal, createGoal, isFinal) {
1391
1428
  "- Use the create_goal payload exactly as rendered: objective only.",
1392
1429
  "- Goals are unlimited. Do not add numeric limits.",
1393
1430
  ...modeConstraintLines(mode, isFinal),
1431
+ ...evidenceLayoutLines(plan),
1394
1432
  finalSection(plan, goal, isFinal, mode === "aggregate"),
1395
1433
  ...checkpointLines(plan, mode),
1396
1434
  "",
@@ -1436,6 +1474,13 @@ function formatCriterionLine(criterion) {
1436
1474
  const marker = isEssentialCriterion(criterion) ? "essential" : "non-essential";
1437
1475
  return `-${remainingWork} [${criterion.id}] [${marker}] (${criterion.userModel}) ${criterion.scenario} — expect: ${criterion.expectedEvidence} — status: ${criterion.status}`;
1438
1476
  }
1477
+ function evidenceLayoutLines(plan) {
1478
+ if (plan.evidenceLayoutVersion !== 2)
1479
+ return [];
1480
+ return [
1481
+ "- Evidence layout v2: write every artifact for the active goal (QA matrix, review reports, receipts) under the current attempt directory — read currentAttemptDir from `omo ulw-loop status --json` (.omo/evidence/ulw/<session>/<goalId>/a<attempt>). The final checkpoint rejects quality-gate artifacts outside that directory."
1482
+ ];
1483
+ }
1439
1484
  function finalSection(plan, goal, isFinal, aggregate) {
1440
1485
  if (!isFinal)
1441
1486
  return "- This is not the final ulw-loop story; do not run the final reviewer/manual-QA/gate-review quality gate yet.";
@@ -1446,8 +1491,11 @@ function finalSection(plan, goal, isFinal, aggregate) {
1446
1491
  "Final story — run mandatory quality gate before update_goal:",
1447
1492
  "- Run targeted verification for changed behavior.",
1448
1493
  "- Confirm every manualQa artifact path exists and has non-zero size.",
1449
- "- Spawn final reviewers with fork_context=false: lazycodex-code-reviewer, lazycodex-qa-executor, and lazycodex-gate-reviewer. Include the original brief, goal objectives, desired user-visible outcome, diff, and evidence.",
1494
+ '- First spawn lazycodex-code-reviewer and lazycodex-qa-executor in parallel (fork_context: false on the v1 surface; fork_turns: "none" on v2). Include the original brief, goal objectives, desired user-visible outcome, diff, and evidence; wait for BOTH to return and confirm their report artifacts exist on disk (code-review report + manualQa matrix).',
1495
+ "- Only then spawn lazycodex-gate-reviewer (same fork settings), passing those artifact paths.",
1450
1496
  "- Require clean codeReview, manualQa, gateReview, iteration, and criteriaCoverage. criteriaCoverage must summarize originalIntent, desiredOutcome, and userOutcomeReview; counts alone are not approval.",
1497
+ "- On a reviewer REJECT, fix only the cited blockers, rerun the affected verification/Manual-QA, and re-review the delta at most TWICE; if blockers remain, record them and surface to the user.",
1498
+ "- If codeQualityStatus is WATCH, include the WATCH notes verbatim in your final user-facing message.",
1451
1499
  "- If any reviewer is blocked/inconclusive or the quality gate is not clean, do not call update_goal. Record blocker work first:",
1452
1500
  ` ${blockerCommand}`,
1453
1501
  aggregate ? '- If the quality gate is clean, call update_goal({status: "complete"}), call get_goal again, then checkpoint the aggregate story:' : '- If the quality gate is clean, call update_goal({status: "complete"}), call get_goal again, then checkpoint:',
@@ -1588,6 +1636,7 @@ async function createUlwLoopPlan(repoRoot, args, scope) {
1588
1636
  const goals = deriveGoalCandidates(args.brief).map((goal, index) => makeGoal(goal.title, goal.objective, index, now));
1589
1637
  const plan = {
1590
1638
  version: 1,
1639
+ evidenceLayoutVersion: 2,
1591
1640
  createdAt: now,
1592
1641
  updatedAt: now,
1593
1642
  briefPath: ulwLoopBriefRelativePath(scope),
@@ -2087,9 +2136,16 @@ ledger: ${plan.ledgerPath}
2087
2136
  }
2088
2137
  async function status(repoRoot, json, scope) {
2089
2138
  const plan = await readUlwLoopPlan(repoRoot, scope);
2090
- if (json)
2091
- printJson({ ok: true, plan, summary: summarizeUlwLoopPlan(plan) });
2092
- else
2139
+ if (json) {
2140
+ const active = plan.goals.find((goal2) => goal2.id === plan.activeGoalId);
2141
+ const currentAttemptDir = plan.evidenceLayoutVersion === 2 && active ? ulwLoopAttemptEvidenceDir(active.id, active.attempt, scope) : undefined;
2142
+ printJson({
2143
+ ok: true,
2144
+ plan,
2145
+ summary: summarizeUlwLoopPlan(plan),
2146
+ ...currentAttemptDir === undefined ? {} : { currentAttemptDir }
2147
+ });
2148
+ } else
2093
2149
  printStatus(plan);
2094
2150
  return 0;
2095
2151
  }
@@ -2622,6 +2678,322 @@ function readAll(stdin) {
2622
2678
  });
2623
2679
  }
2624
2680
 
2681
+ // components/ulw-loop/src/spawn-guard.ts
2682
+ import { existsSync as existsSync6, readdirSync, readFileSync as readFileSync3, statSync as statSync2, writeFileSync } from "node:fs";
2683
+ import { join as join2 } from "node:path";
2684
+ var SPAWN_TOOL_TOKENS = new Set(["spawn_agent", "collaborationspawn_agent", "collaboration.spawn_agent"]);
2685
+ var DEFAULT_FANOUT_LIMIT = 60;
2686
+ var GATE_MESSAGE_PATTERN = /lazycodex-gate-reviewer|final gate review/i;
2687
+ function applySpawnGuards(payload) {
2688
+ if (payload.hook_event_name !== "PreToolUse" || !SPAWN_TOOL_TOKENS.has(payload.tool_name))
2689
+ return "";
2690
+ const stateDir = ulwLoopDir(payload.cwd, { sessionId: payload.session_id });
2691
+ const plan = readPlan(join2(stateDir, "goals.json"));
2692
+ if (plan === null)
2693
+ return "";
2694
+ const fanOutDenial = consumeFanOutBudget(stateDir);
2695
+ if (fanOutDenial !== null)
2696
+ return deny(fanOutDenial);
2697
+ const missingArtifact = missingGateArtifact(payload, plan);
2698
+ if (missingArtifact !== null)
2699
+ return deny(`spawn code-review + QA first; gate audits their artifacts: missing ${missingArtifact}`);
2700
+ return "";
2701
+ }
2702
+ async function runSpawnGuardCli(stdin, stdout) {
2703
+ try {
2704
+ const chunks = [];
2705
+ for await (const chunk of stdin)
2706
+ chunks.push(Buffer.from(chunk));
2707
+ const payload = parsePreToolUsePayload(Buffer.concat(chunks).toString("utf8"));
2708
+ if (payload === null)
2709
+ return;
2710
+ const output = applySpawnGuards(payload);
2711
+ if (output.length > 0)
2712
+ stdout.write(output);
2713
+ } catch (error) {
2714
+ if (error instanceof Error)
2715
+ return;
2716
+ }
2717
+ }
2718
+ function consumeFanOutBudget(stateDir) {
2719
+ const counterPath = join2(stateDir, "spawn-count.json");
2720
+ const count = readCount(counterPath) + 1;
2721
+ writeFileSync(counterPath, JSON.stringify({ count }));
2722
+ const limit = fanOutLimit();
2723
+ if (count <= limit)
2724
+ return null;
2725
+ return `ulw-loop spawn fan-out cap reached (${count}/${limit}). Consolidate work into the agents already running, or raise OMO_SPAWN_FANOUT_LIMIT if this volume is intentional.`;
2726
+ }
2727
+ function missingGateArtifact(payload, plan) {
2728
+ if (!isGateReviewerSpawn(payload.tool_input))
2729
+ return null;
2730
+ const goal2 = plan.goals.find((candidate) => isFinalRunCompletionCandidate(plan, candidate));
2731
+ if (goal2 === undefined || goal2.status === "complete")
2732
+ return null;
2733
+ if (!goal2.successCriteria.every((criterion) => criterion.status === "pass"))
2734
+ return null;
2735
+ const scope = { sessionId: payload.session_id };
2736
+ if (plan.evidenceLayoutVersion === 2) {
2737
+ const attemptDir = ulwLoopAttemptEvidenceDir(goal2.id, goal2.attempt, scope);
2738
+ for (const name of [`${goal2.id}-code-review.md`, `${goal2.id}-manual-qa.md`]) {
2739
+ const relative2 = `${attemptDir}/${name}`;
2740
+ if (!isNonEmptyFile(join2(payload.cwd, relative2)))
2741
+ return relative2;
2742
+ }
2743
+ return null;
2744
+ }
2745
+ const flatReport = `.omo/evidence/${goal2.id}-code-review.md`;
2746
+ if (!isNonEmptyFile(join2(payload.cwd, flatReport)))
2747
+ return flatReport;
2748
+ if (!hasOtherEvidenceFile(join2(payload.cwd, ".omo", "evidence"), `${goal2.id}-code-review.md`))
2749
+ return `.omo/evidence/<any manual-QA artifact besides ${goal2.id}-code-review.md>`;
2750
+ return null;
2751
+ }
2752
+ function isGateReviewerSpawn(toolInput) {
2753
+ if (typeof toolInput !== "object" || toolInput === null)
2754
+ return false;
2755
+ const record = toolInput;
2756
+ const agentType = record["agent_type"];
2757
+ if (typeof agentType === "string")
2758
+ return agentType === "lazycodex-gate-reviewer";
2759
+ const message = record["message"];
2760
+ return typeof message === "string" && GATE_MESSAGE_PATTERN.test(message);
2761
+ }
2762
+ function deny(reason) {
2763
+ return `${JSON.stringify({
2764
+ hookSpecificOutput: {
2765
+ hookEventName: "PreToolUse",
2766
+ permissionDecision: "deny",
2767
+ permissionDecisionReason: reason,
2768
+ additionalContext: reason
2769
+ }
2770
+ })}
2771
+ `;
2772
+ }
2773
+ function fanOutLimit() {
2774
+ const raw = process.env["OMO_SPAWN_FANOUT_LIMIT"];
2775
+ if (raw === undefined)
2776
+ return DEFAULT_FANOUT_LIMIT;
2777
+ const parsed = Number.parseInt(raw, 10);
2778
+ return Number.isFinite(parsed) && parsed > 0 ? parsed : DEFAULT_FANOUT_LIMIT;
2779
+ }
2780
+ function isNonEmptyFile(path) {
2781
+ try {
2782
+ return existsSync6(path) && statSync2(path).size > 0;
2783
+ } catch (error) {
2784
+ if (error instanceof Error)
2785
+ return false;
2786
+ throw error;
2787
+ }
2788
+ }
2789
+ function hasOtherEvidenceFile(evidenceDir, excludedName) {
2790
+ try {
2791
+ return readdirSync(evidenceDir).some((name) => name !== excludedName && isNonEmptyFile(join2(evidenceDir, name)));
2792
+ } catch (error) {
2793
+ if (error instanceof Error)
2794
+ return false;
2795
+ throw error;
2796
+ }
2797
+ }
2798
+ function readCount(counterPath) {
2799
+ try {
2800
+ const parsed = JSON.parse(readFileSync3(counterPath, "utf8"));
2801
+ return typeof parsed["count"] === "number" && parsed["count"] >= 0 ? parsed["count"] : 0;
2802
+ } catch (error) {
2803
+ if (error instanceof Error)
2804
+ return 0;
2805
+ throw error;
2806
+ }
2807
+ }
2808
+ function readPlan(goalsPath) {
2809
+ try {
2810
+ return JSON.parse(readFileSync3(goalsPath, "utf8"));
2811
+ } catch (error) {
2812
+ if (error instanceof Error)
2813
+ return null;
2814
+ throw error;
2815
+ }
2816
+ }
2817
+
2818
+ // components/ulw-loop/src/stop-resume-hook.ts
2819
+ import { existsSync as existsSync7, readFileSync as readFileSync4, writeFileSync as writeFileSync2 } from "node:fs";
2820
+ import { isAbsolute as isAbsolute2, join as join3 } from "node:path";
2821
+ var RESUME_CAP = 2;
2822
+ var CONTEXT_PRESSURE_MARKERS2 = [
2823
+ "context compacted",
2824
+ "context_length_exceeded",
2825
+ "skill descriptions were shortened",
2826
+ "context_too_large",
2827
+ "codex ran out of room in the model's context window",
2828
+ "your input exceeds the context window",
2829
+ "long threads and multiple compactions"
2830
+ ];
2831
+ function runStopResumeHook(input) {
2832
+ const payload = parseStopPayload(input);
2833
+ if (payload === null || payload.stop_hook_active)
2834
+ return "";
2835
+ if (transcriptShowsContextPressure(payload.transcript_path))
2836
+ return "";
2837
+ if (boulderContinuationWillFire(payload.cwd, payload.session_id))
2838
+ return "";
2839
+ const stateDir = ulwLoopDir(payload.cwd, { sessionId: payload.session_id });
2840
+ const plan = readPlan2(join3(stateDir, "goals.json"));
2841
+ if (plan === null || plan.aggregateCompletion?.status === "complete")
2842
+ return "";
2843
+ const goal2 = resumableGoal(plan);
2844
+ if (goal2 === undefined)
2845
+ return "";
2846
+ if (!consumeResumeBudget(stateDir, goal2.id))
2847
+ return "";
2848
+ const output = {
2849
+ decision: "block",
2850
+ reason: renderResumeDirective(plan, goal2, payload.session_id)
2851
+ };
2852
+ return JSON.stringify(output);
2853
+ }
2854
+ async function runStopResumeHookCli(stdin, stdout) {
2855
+ try {
2856
+ const chunks = [];
2857
+ for await (const chunk of stdin)
2858
+ chunks.push(Buffer.from(chunk));
2859
+ const output = runStopResumeHook(JSON.parse(Buffer.concat(chunks).toString("utf8")));
2860
+ if (output.length > 0)
2861
+ stdout.write(output);
2862
+ } catch (error) {
2863
+ if (error instanceof Error)
2864
+ return;
2865
+ }
2866
+ }
2867
+ function resumableGoal(plan) {
2868
+ const active = plan.goals.find((goal2) => goal2.id === plan.activeGoalId);
2869
+ if (active !== undefined && isResumableStatus(active.status))
2870
+ return active;
2871
+ return plan.goals.find((goal2) => isResumableStatus(goal2.status));
2872
+ }
2873
+ function isResumableStatus(status2) {
2874
+ return status2 === "pending" || status2 === "in_progress";
2875
+ }
2876
+ function consumeResumeBudget(stateDir, goalId) {
2877
+ const ledgerLineCount = countLedgerLines(join3(stateDir, "ledger.jsonl"));
2878
+ const counterPath = join3(stateDir, `auto-resume-${goalId}.json`);
2879
+ const previous = readCounter(counterPath);
2880
+ const count = previous !== null && previous.ledgerLineCount === ledgerLineCount ? previous.count : 0;
2881
+ if (count >= RESUME_CAP) {
2882
+ writeFileSync2(join3(stateDir, `auto-resume-${goalId}.stuck`), `no ledger progress after ${count} resumes
2883
+ `);
2884
+ return false;
2885
+ }
2886
+ writeFileSync2(counterPath, JSON.stringify({ count: count + 1, ledgerLineCount }));
2887
+ return true;
2888
+ }
2889
+ function renderResumeDirective(plan, goal2, sessionId) {
2890
+ const normalized = normalizeUlwLoopSessionId(sessionId);
2891
+ const option = normalized !== null && plan.goalsPath.includes(`/${normalized}/`) ? ` --session-id ${normalized}` : "";
2892
+ return [
2893
+ `The ulw-loop run in this session still has unfinished goals (next: ${goal2.id} — ${goal2.title}).`,
2894
+ "The turn ended before the loop completed. Resume it now:",
2895
+ `1. Run \`omo ulw-loop status${option} --json\` to reload the plan, the active goal, and currentAttemptDir.`,
2896
+ "2. Continue the active goal's remaining success criteria, recording evidence with record-evidence.",
2897
+ `3. Checkpoint through \`omo ulw-loop checkpoint${option}\` when the goal's criteria are proven.`,
2898
+ "If the loop is genuinely blocked on the user, checkpoint the goal as blocked with the reason instead."
2899
+ ].join(`
2900
+ `);
2901
+ }
2902
+ function readPlan2(goalsPath) {
2903
+ try {
2904
+ return JSON.parse(readFileSync4(goalsPath, "utf8"));
2905
+ } catch (error) {
2906
+ if (error instanceof Error)
2907
+ return null;
2908
+ throw error;
2909
+ }
2910
+ }
2911
+ function countLedgerLines(ledgerPath) {
2912
+ try {
2913
+ return readFileSync4(ledgerPath, "utf8").split(`
2914
+ `).filter(Boolean).length;
2915
+ } catch (error) {
2916
+ if (error instanceof Error)
2917
+ return 0;
2918
+ throw error;
2919
+ }
2920
+ }
2921
+ function readCounter(counterPath) {
2922
+ try {
2923
+ if (!existsSync7(counterPath))
2924
+ return null;
2925
+ const parsed = JSON.parse(readFileSync4(counterPath, "utf8"));
2926
+ if (typeof parsed["count"] !== "number" || typeof parsed["ledgerLineCount"] !== "number")
2927
+ return null;
2928
+ return { count: parsed["count"], ledgerLineCount: parsed["ledgerLineCount"] };
2929
+ } catch (error) {
2930
+ if (error instanceof Error)
2931
+ return null;
2932
+ throw error;
2933
+ }
2934
+ }
2935
+ function boulderContinuationWillFire(cwd, sessionId) {
2936
+ try {
2937
+ const raw = JSON.parse(readFileSync4(join3(cwd, ".omo", "boulder.json"), "utf8"));
2938
+ const works = raw["works"];
2939
+ const entries = typeof works === "object" && works !== null ? Object.values(works) : [raw];
2940
+ return entries.some((work) => {
2941
+ if (typeof work !== "object" || work === null)
2942
+ return false;
2943
+ const entry = work;
2944
+ const sessionIds = Array.isArray(entry["session_ids"]) ? entry["session_ids"] : [];
2945
+ const continuable = entry["status"] === "active" || entry["status"] === "paused";
2946
+ return continuable && sessionIds.includes(`codex:${sessionId}`) && boulderPlanHasRemainingTask(cwd, entry);
2947
+ });
2948
+ } catch (error) {
2949
+ if (error instanceof Error)
2950
+ return false;
2951
+ throw error;
2952
+ }
2953
+ }
2954
+ function transcriptShowsContextPressure(transcriptPath) {
2955
+ try {
2956
+ const transcript = readFileSync4(transcriptPath, "utf8").toLowerCase();
2957
+ return CONTEXT_PRESSURE_MARKERS2.some((marker) => transcript.includes(marker));
2958
+ } catch (error) {
2959
+ if (error instanceof Error)
2960
+ return false;
2961
+ throw error;
2962
+ }
2963
+ }
2964
+ function boulderPlanHasRemainingTask(cwd, entry) {
2965
+ const activePlan = entry["active_plan"];
2966
+ if (typeof activePlan !== "string" || activePlan.trim().length === 0)
2967
+ return false;
2968
+ const planPath = isAbsolute2(activePlan) ? activePlan : join3(cwd, activePlan);
2969
+ const worktree = entry["worktree_path"];
2970
+ const candidates = typeof worktree === "string" && worktree.trim().length > 0 && !isAbsolute2(activePlan) ? [join3(isAbsolute2(worktree) ? worktree : join3(cwd, worktree), activePlan), planPath] : [planPath];
2971
+ for (const candidate of candidates) {
2972
+ try {
2973
+ return readFileSync4(candidate, "utf8").split(/\r?\n/).some((line) => line.startsWith("- [ ] "));
2974
+ } catch (error) {
2975
+ if (!(error instanceof Error))
2976
+ throw error;
2977
+ }
2978
+ }
2979
+ return false;
2980
+ }
2981
+ function parseStopPayload(value) {
2982
+ if (typeof value !== "object" || value === null || Array.isArray(value))
2983
+ return null;
2984
+ const record = value;
2985
+ const optionalMessage = record["last_assistant_message"];
2986
+ const valid = record["hook_event_name"] === "Stop" && typeof record["session_id"] === "string" && typeof record["turn_id"] === "string" && typeof record["transcript_path"] === "string" && typeof record["cwd"] === "string" && typeof record["model"] === "string" && typeof record["permission_mode"] === "string" && typeof record["stop_hook_active"] === "boolean" && (optionalMessage === undefined || typeof optionalMessage === "string");
2987
+ if (!valid)
2988
+ return null;
2989
+ return {
2990
+ session_id: record["session_id"],
2991
+ cwd: record["cwd"],
2992
+ transcript_path: record["transcript_path"],
2993
+ stop_hook_active: record["stop_hook_active"]
2994
+ };
2995
+ }
2996
+
2625
2997
  // components/ulw-loop/src/cli.ts
2626
2998
  var TOP_LEVEL_HELP = `Usage:
2627
2999
  omo ulw-loop <subcommand> [args]
@@ -2651,6 +3023,14 @@ async function main() {
2651
3023
  await runPreToolUseGoalBudgetGuardCli(process.stdin, process.stdout);
2652
3024
  return 0;
2653
3025
  }
3026
+ if (sub === "stop") {
3027
+ await runStopResumeHookCli(process.stdin, process.stdout);
3028
+ return 0;
3029
+ }
3030
+ if (sub === "pre-tool-use-spawn") {
3031
+ await runSpawnGuardCli(process.stdin, process.stdout);
3032
+ return 0;
3033
+ }
2654
3034
  process.stderr.write(`[omo] unknown hook subcommand: ${sub ?? "(none)"}
2655
3035
  `);
2656
3036
  return 1;
@@ -24,6 +24,7 @@ function buildText(mode, plan, goal, createGoal, isFinal) {
24
24
  "- Use the create_goal payload exactly as rendered: objective only.",
25
25
  "- Goals are unlimited. Do not add numeric limits.",
26
26
  ...modeConstraintLines(mode, isFinal),
27
+ ...evidenceLayoutLines(plan),
27
28
  finalSection(plan, goal, isFinal, mode === "aggregate"),
28
29
  ...checkpointLines(plan, mode),
29
30
  "",
@@ -71,6 +72,13 @@ function formatCriterionLine(criterion) {
71
72
  const marker = isEssentialCriterion(criterion) ? "essential" : "non-essential";
72
73
  return `-${remainingWork} [${criterion.id}] [${marker}] (${criterion.userModel}) ${criterion.scenario} — expect: ${criterion.expectedEvidence} — status: ${criterion.status}`;
73
74
  }
75
+ function evidenceLayoutLines(plan) {
76
+ if (plan.evidenceLayoutVersion !== 2)
77
+ return [];
78
+ return [
79
+ "- Evidence layout v2: write every artifact for the active goal (QA matrix, review reports, receipts) under the current attempt directory — read currentAttemptDir from `omo ulw-loop status --json` (.omo/evidence/ulw/<session>/<goalId>/a<attempt>). The final checkpoint rejects quality-gate artifacts outside that directory.",
80
+ ];
81
+ }
74
82
  function finalSection(plan, goal, isFinal, aggregate) {
75
83
  if (!isFinal)
76
84
  return "- This is not the final ulw-loop story; do not run the final reviewer/manual-QA/gate-review quality gate yet.";
@@ -81,8 +89,11 @@ function finalSection(plan, goal, isFinal, aggregate) {
81
89
  "Final story — run mandatory quality gate before update_goal:",
82
90
  "- Run targeted verification for changed behavior.",
83
91
  "- Confirm every manualQa artifact path exists and has non-zero size.",
84
- "- Spawn final reviewers with fork_context=false: lazycodex-code-reviewer, lazycodex-qa-executor, and lazycodex-gate-reviewer. Include the original brief, goal objectives, desired user-visible outcome, diff, and evidence.",
92
+ "- First spawn lazycodex-code-reviewer and lazycodex-qa-executor in parallel (fork_context: false on the v1 surface; fork_turns: \"none\" on v2). Include the original brief, goal objectives, desired user-visible outcome, diff, and evidence; wait for BOTH to return and confirm their report artifacts exist on disk (code-review report + manualQa matrix).",
93
+ "- Only then spawn lazycodex-gate-reviewer (same fork settings), passing those artifact paths.",
85
94
  "- Require clean codeReview, manualQa, gateReview, iteration, and criteriaCoverage. criteriaCoverage must summarize originalIntent, desiredOutcome, and userOutcomeReview; counts alone are not approval.",
95
+ "- On a reviewer REJECT, fix only the cited blockers, rerun the affected verification/Manual-QA, and re-review the delta at most TWICE; if blockers remain, record them and surface to the user.",
96
+ "- If codeQualityStatus is WATCH, include the WATCH notes verbatim in your final user-facing message.",
86
97
  "- If any reviewer is blocked/inconclusive or the quality gate is not clean, do not call update_goal. Record blocker work first:",
87
98
  ` ${blockerCommand}`,
88
99
  aggregate
@@ -45,6 +45,7 @@ export interface UlwLoopAggregateCompletion {
45
45
  }
46
46
  export interface UlwLoopPlan {
47
47
  version: 1;
48
+ evidenceLayoutVersion?: 2;
48
49
  createdAt: string;
49
50
  updatedAt: string;
50
51
  briefPath: string;
@@ -78,14 +79,15 @@ export interface UlwLoopManualQaAdversarialCase {
78
79
  readonly criterionRef: string;
79
80
  readonly scenario: string;
80
81
  readonly expectedBehavior: string;
81
- readonly verdict: "passed";
82
+ readonly verdict: "passed" | "not_applicable";
83
+ readonly reason?: string;
82
84
  readonly artifactRefs: readonly string[];
83
85
  }
84
86
  export interface UlwLoopQualityGate {
85
87
  readonly codeReview: {
86
88
  readonly by: string;
87
89
  readonly recommendation: "APPROVE";
88
- readonly codeQualityStatus: "CLEAR";
90
+ readonly codeQualityStatus: "CLEAR" | "WATCH";
89
91
  readonly reportPath: string;
90
92
  readonly evidence: string;
91
93
  readonly blockers: readonly [];
@@ -13,4 +13,11 @@ export declare function ulwLoopBriefPath(repoRoot: string, scope?: UlwLoopScope)
13
13
  export declare function ulwLoopGoalsPath(repoRoot: string, scope?: UlwLoopScope): string;
14
14
  export declare function ulwLoopLedgerPath(repoRoot: string, scope?: UlwLoopScope): string;
15
15
  export declare function repoRelative(absolutePath: string, repoRoot: string): string;
16
+ export declare function ulwLoopAttemptEvidenceDir(goalId: string, attempt: number, scope?: UlwLoopScope): string;
17
+ interface AttemptPathApi {
18
+ relative(from: string, to: string): string;
19
+ isAbsolute(path: string): boolean;
20
+ readonly sep: string;
21
+ }
22
+ export declare function isWithinAttemptDir(absolutePath: string, attemptRoot: string, pathApi?: AttemptPathApi): boolean;
16
23
  export {};
@@ -1,4 +1,4 @@
1
- import { join } from "node:path";
1
+ import { isAbsolute, join, relative, sep } from "node:path";
2
2
  import { ULW_LOOP_BRIEF, ULW_LOOP_DIR, ULW_LOOP_GOALS, ULW_LOOP_LEDGER } from "./types.js";
3
3
  const SESSION_ENV_KEYS = ["OMO_ULW_LOOP_SESSION_ID", "CODEX_SESSION_ID", "CODEX_THREAD_ID"];
4
4
  export function normalizeUlwLoopSessionId(sessionId) {
@@ -57,3 +57,18 @@ export function repoRelative(absolutePath, repoRoot) {
57
57
  return absolutePath.slice(backslashPrefix.length).split("\\").join("/");
58
58
  return absolutePath.split("\\").join("/");
59
59
  }
60
+ // Both the status --json emitter and the checkpoint enforcement resolve the attempt dir through
61
+ // this function; a second resolution path would let the gate reject its own advertised directory.
62
+ export function ulwLoopAttemptEvidenceDir(goalId, attempt, scope) {
63
+ const sessionId = normalizeUlwLoopSessionId(scope?.sessionId) ?? resolveUlwLoopSessionIdFromEnv() ?? "session";
64
+ return `.omo/evidence/ulw/${sessionId}/${goalId}/a${attempt}`;
65
+ }
66
+ const PLATFORM_PATH_API = { relative, isAbsolute, sep };
67
+ export function isWithinAttemptDir(absolutePath, attemptRoot, pathApi = PLATFORM_PATH_API) {
68
+ const relativePath = pathApi.relative(attemptRoot, absolutePath);
69
+ if (relativePath === "")
70
+ return true;
71
+ if (relativePath === ".." || relativePath.startsWith(`..${pathApi.sep}`))
72
+ return false;
73
+ return !pathApi.isAbsolute(relativePath);
74
+ }
@@ -33,6 +33,7 @@ export async function createUlwLoopPlan(repoRoot, args, scope) {
33
33
  const goals = deriveGoalCandidates(args.brief).map((goal, index) => makeGoal(goal.title, goal.objective, index, now));
34
34
  const plan = {
35
35
  version: 1,
36
+ evidenceLayoutVersion: 2,
36
37
  createdAt: now,
37
38
  updatedAt: now,
38
39
  briefPath: ulwLoopBriefRelativePath(scope),
@@ -0,0 +1,6 @@
1
+ export declare function passedVerdict(value: unknown, field: string): "passed";
2
+ export declare function codeQualityStatusField(value: unknown, field: string): "CLEAR" | "WATCH";
3
+ export declare function adversarialVerdict(row: Record<string, unknown>, field: string): {
4
+ verdict: "passed" | "not_applicable";
5
+ reason?: string;
6
+ };