oh-my-opencode 4.16.2 → 4.17.0

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 (234) hide show
  1. package/.agents/command/omomomo.md +1 -1
  2. package/.agents/skills/hyperplan/SKILL.md +3 -3
  3. package/.agents/skills/omomomo/SKILL.md +1 -1
  4. package/.agents/skills/pre-publish-review/SKILL.md +3 -0
  5. package/.opencode/command/omomomo.md +1 -1
  6. package/.opencode/skills/hyperplan/SKILL.md +3 -3
  7. package/.opencode/skills/pre-publish-review/SKILL.md +3 -0
  8. package/README.ja.md +4 -4
  9. package/README.ko.md +4 -4
  10. package/README.md +4 -4
  11. package/README.ru.md +4 -4
  12. package/README.zh-cn.md +4 -4
  13. package/dist/agents/hephaestus/agent.d.ts +1 -1
  14. package/dist/agents/hephaestus/gpt-5-6.d.ts +2 -0
  15. package/dist/agents/momus-gpt-5-6.d.ts +1 -0
  16. package/dist/agents/types.d.ts +2 -0
  17. package/dist/cli/index.js +947 -567
  18. package/dist/cli-node/index.js +947 -567
  19. package/dist/features/background-agent/error-classifier.d.ts +5 -0
  20. package/dist/features/builtin-commands/templates/refactor-sections/intro-and-analysis.d.ts +1 -1
  21. package/dist/features/builtin-commands/templates/remove-ai-slops.d.ts +1 -1
  22. package/dist/features/team-mode/test-support/async-test-helpers.d.ts +2 -2
  23. package/dist/index.js +523 -81
  24. package/dist/plugin/chat-message/types.d.ts +4 -0
  25. package/dist/plugin/command-execute-before.d.ts +1 -0
  26. package/dist/plugin/stop-continuation.d.ts +17 -0
  27. package/dist/shared/live-server-route.d.ts +1 -1
  28. package/dist/skills/remove-ai-slops/SKILL.md +2 -2
  29. package/dist/skills/review-work/SKILL.md +1 -1
  30. package/dist/skills/start-work/SKILL.md +5 -2
  31. package/dist/skills/ulw-plan/references/full-workflow.md +1 -1
  32. package/dist/skills/ulw-plan/scripts/scaffold-plan.mjs +2 -2
  33. package/dist/skills/ulw-research/SKILL.md +2 -2
  34. package/dist/skills/visual-qa/SKILL.md +1 -1
  35. package/dist/tui.js +195 -41
  36. package/package.json +15 -14
  37. package/packages/git-bash-mcp/package.json +27 -0
  38. package/packages/omo-codex/plugin/.codex-plugin/plugin.json +3 -1
  39. package/packages/omo-codex/plugin/components/bootstrap/dist/cli.js +465 -248
  40. package/packages/omo-codex/plugin/components/bootstrap/hooks/hooks.json +1 -1
  41. package/packages/omo-codex/plugin/components/bootstrap/package.json +1 -1
  42. package/packages/omo-codex/plugin/components/codegraph/package.json +1 -1
  43. package/packages/omo-codex/plugin/components/comment-checker/hooks/hooks.json +1 -1
  44. package/packages/omo-codex/plugin/components/comment-checker/package.json +1 -1
  45. package/packages/omo-codex/plugin/components/git-bash/hooks/hooks.json +2 -2
  46. package/packages/omo-codex/plugin/components/git-bash/package.json +1 -1
  47. package/packages/omo-codex/plugin/components/lazycodex-executor-verify/AGENTS.md +2 -2
  48. package/packages/omo-codex/plugin/components/lazycodex-executor-verify/dist/cli.js +6 -2
  49. package/packages/omo-codex/plugin/components/lazycodex-executor-verify/dist/codex-hook.js +6 -2
  50. package/packages/omo-codex/plugin/components/lazycodex-executor-verify/hooks/hooks.json +2 -2
  51. package/packages/omo-codex/plugin/components/lazycodex-executor-verify/package.json +1 -1
  52. package/packages/omo-codex/plugin/components/lazycodex-executor-verify/src/codex-hook.ts +6 -2
  53. package/packages/omo-codex/plugin/components/lazycodex-executor-verify/test/cli.test.ts +1 -1
  54. package/packages/omo-codex/plugin/components/lazycodex-executor-verify/test/codex-hook.test.ts +67 -2
  55. package/packages/omo-codex/plugin/components/lsp/hooks/hooks.json +2 -2
  56. package/packages/omo-codex/plugin/components/lsp/package.json +1 -1
  57. package/packages/omo-codex/plugin/components/lsp/test/package-smoke.test.ts +0 -13
  58. package/packages/omo-codex/plugin/components/rules/bundled-rules/hephaestus/gpt-5.6.md +93 -0
  59. package/packages/omo-codex/plugin/components/rules/dist/cli.js +43 -24
  60. package/packages/omo-codex/plugin/components/rules/hooks/hooks.json +4 -4
  61. package/packages/omo-codex/plugin/components/rules/package.json +1 -1
  62. package/packages/omo-codex/plugin/components/rules/src/codex-hook.ts +2 -1
  63. package/packages/omo-codex/plugin/components/rules/src/dynamic-target-fingerprints.ts +15 -6
  64. package/packages/omo-codex/plugin/components/rules/src/rules-engine-factory.ts +7 -2
  65. package/packages/omo-codex/plugin/components/rules/src/static-injection.ts +2 -2
  66. package/packages/omo-codex/plugin/components/rules/test/codex-hook.test.ts +32 -0
  67. package/packages/omo-codex/plugin/components/rules/test/hephaestus-model-variant.test.ts +106 -0
  68. package/packages/omo-codex/plugin/components/start-work-continuation/hooks/hooks.json +2 -2
  69. package/packages/omo-codex/plugin/components/start-work-continuation/package.json +1 -1
  70. package/packages/omo-codex/plugin/components/start-work-continuation/test/codex-hook.test.ts +1 -79
  71. package/packages/omo-codex/plugin/components/teammode/AGENTS.md +12 -9
  72. package/packages/omo-codex/plugin/components/teammode/hooks/hooks.json +1 -1
  73. package/packages/omo-codex/plugin/components/teammode/package.json +1 -1
  74. package/packages/omo-codex/plugin/components/teammode/skills/teammode/SKILL.md +136 -76
  75. package/packages/omo-codex/plugin/components/teammode/skills/teammode/scripts/team-guide.mjs +94 -23
  76. package/packages/omo-codex/plugin/components/teammode/skills/teammode/scripts/team-state.mjs +94 -14
  77. package/packages/omo-codex/plugin/components/teammode/skills/teammode/scripts/team-transport.mjs +55 -0
  78. package/packages/omo-codex/plugin/components/teammode/skills/teammode/scripts/team.mjs +72 -18
  79. package/packages/omo-codex/plugin/components/teammode/test/thread-title-hook.test.ts +23 -0
  80. package/packages/omo-codex/plugin/components/telemetry/hooks/hooks.json +1 -1
  81. package/packages/omo-codex/plugin/components/telemetry/package.json +1 -1
  82. package/packages/omo-codex/plugin/components/ultrawork/agents/explorer.toml +23 -40
  83. package/packages/omo-codex/plugin/components/ultrawork/agents/lazycodex-clone-fidelity-reviewer.toml +1 -1
  84. package/packages/omo-codex/plugin/components/ultrawork/agents/lazycodex-code-reviewer.toml +3 -3
  85. package/packages/omo-codex/plugin/components/ultrawork/agents/lazycodex-gate-reviewer.toml +7 -7
  86. package/packages/omo-codex/plugin/components/ultrawork/agents/lazycodex-qa-executor.toml +5 -5
  87. package/packages/omo-codex/plugin/components/ultrawork/agents/lazycodex-worker-high.toml +26 -0
  88. package/packages/omo-codex/plugin/components/ultrawork/agents/{lazycodex-executor.toml → lazycodex-worker-low.toml} +7 -5
  89. package/packages/omo-codex/plugin/components/ultrawork/agents/lazycodex-worker-medium.toml +26 -0
  90. package/packages/omo-codex/plugin/components/ultrawork/agents/librarian.toml +20 -48
  91. package/packages/omo-codex/plugin/components/ultrawork/agents/metis.toml +17 -29
  92. package/packages/omo-codex/plugin/components/ultrawork/agents/momus.toml +20 -50
  93. package/packages/omo-codex/plugin/components/ultrawork/agents/plan.toml +22 -43
  94. package/packages/omo-codex/plugin/components/ultrawork/directive.md +55 -28
  95. package/packages/omo-codex/plugin/components/ultrawork/hooks/hooks.json +1 -1
  96. package/packages/omo-codex/plugin/components/ultrawork/package.json +1 -1
  97. package/packages/omo-codex/plugin/components/ultrawork/skills/ultrawork/SKILL.md +55 -28
  98. package/packages/omo-codex/plugin/components/ultrawork/skills/ulw-plan/references/full-workflow.md +1 -1
  99. package/packages/omo-codex/plugin/components/ultrawork/skills/ulw-plan/scripts/scaffold-plan.mjs +2 -2
  100. package/packages/omo-codex/plugin/components/ultrawork/test/codex-hook.test.ts +22 -0
  101. package/packages/omo-codex/plugin/components/ultrawork/test/package-smoke.test.ts +0 -68
  102. package/packages/omo-codex/plugin/components/ulw-loop/AGENTS.md +1 -1
  103. package/packages/omo-codex/plugin/components/ulw-loop/CHANGELOG.md +2 -0
  104. package/packages/omo-codex/plugin/components/ulw-loop/README.md +3 -1
  105. package/packages/omo-codex/plugin/components/ulw-loop/directive.md +55 -28
  106. package/packages/omo-codex/plugin/components/ulw-loop/dist/checkpoint.js +6 -1
  107. package/packages/omo-codex/plugin/components/ulw-loop/dist/cli-subcommands.d.ts +1 -1
  108. package/packages/omo-codex/plugin/components/ulw-loop/dist/cli-subcommands.js +13 -2
  109. package/packages/omo-codex/plugin/components/ulw-loop/dist/cli.js +405 -25
  110. package/packages/omo-codex/plugin/components/ulw-loop/dist/codex-goal-instruction.js +12 -1
  111. package/packages/omo-codex/plugin/components/ulw-loop/dist/domain-types.d.ts +4 -2
  112. package/packages/omo-codex/plugin/components/ulw-loop/dist/paths.d.ts +7 -0
  113. package/packages/omo-codex/plugin/components/ulw-loop/dist/paths.js +16 -1
  114. package/packages/omo-codex/plugin/components/ulw-loop/dist/plan-crud.js +1 -0
  115. package/packages/omo-codex/plugin/components/ulw-loop/dist/quality-gate-verdicts.d.ts +6 -0
  116. package/packages/omo-codex/plugin/components/ulw-loop/dist/quality-gate-verdicts.js +20 -0
  117. package/packages/omo-codex/plugin/components/ulw-loop/dist/quality-gate.d.ts +1 -0
  118. package/packages/omo-codex/plugin/components/ulw-loop/dist/quality-gate.js +12 -9
  119. package/packages/omo-codex/plugin/components/ulw-loop/dist/spawn-guard.d.ts +3 -0
  120. package/packages/omo-codex/plugin/components/ulw-loop/dist/spawn-guard.js +148 -0
  121. package/packages/omo-codex/plugin/components/ulw-loop/dist/stop-resume-hook.d.ts +2 -0
  122. package/packages/omo-codex/plugin/components/ulw-loop/dist/stop-resume-hook.js +209 -0
  123. package/packages/omo-codex/plugin/components/ulw-loop/hooks/hooks.json +25 -2
  124. package/packages/omo-codex/plugin/components/ulw-loop/package.json +1 -1
  125. package/packages/omo-codex/plugin/components/ulw-loop/skills/ulw-loop/SKILL.md +14 -15
  126. package/packages/omo-codex/plugin/components/ulw-loop/skills/ulw-loop/references/full-workflow.md +29 -27
  127. package/packages/omo-codex/plugin/components/ulw-loop/src/checkpoint.ts +6 -1
  128. package/packages/omo-codex/plugin/components/ulw-loop/src/cli-subcommands.ts +14 -3
  129. package/packages/omo-codex/plugin/components/ulw-loop/src/cli.ts +10 -0
  130. package/packages/omo-codex/plugin/components/ulw-loop/src/codex-goal-instruction.ts +12 -1
  131. package/packages/omo-codex/plugin/components/ulw-loop/src/domain-types.ts +4 -2
  132. package/packages/omo-codex/plugin/components/ulw-loop/src/paths.ts +27 -1
  133. package/packages/omo-codex/plugin/components/ulw-loop/src/plan-crud.ts +1 -0
  134. package/packages/omo-codex/plugin/components/ulw-loop/src/quality-gate-verdicts.ts +23 -0
  135. package/packages/omo-codex/plugin/components/ulw-loop/src/quality-gate.ts +16 -9
  136. package/packages/omo-codex/plugin/components/ulw-loop/src/spawn-guard.ts +138 -0
  137. package/packages/omo-codex/plugin/components/ulw-loop/src/stop-resume-hook.ts +208 -0
  138. package/packages/omo-codex/plugin/components/ulw-loop/test/cli-create-goals.test.ts +12 -0
  139. package/packages/omo-codex/plugin/components/ulw-loop/test/cli-entrypoint.test.ts +4 -1
  140. package/packages/omo-codex/plugin/components/ulw-loop/test/codex-goal-instruction.test.ts +8 -35
  141. package/packages/omo-codex/plugin/components/ulw-loop/test/fixtures/quality-gate-builder.ts +24 -13
  142. package/packages/omo-codex/plugin/components/ulw-loop/test/package-smoke.test.ts +5 -2
  143. package/packages/omo-codex/plugin/components/ulw-loop/test/paths.test.ts +43 -8
  144. package/packages/omo-codex/plugin/components/ulw-loop/test/quality-gate.test.ts +55 -2
  145. package/packages/omo-codex/plugin/components/ulw-loop/test/spawn-guard.test.ts +228 -0
  146. package/packages/omo-codex/plugin/components/ulw-loop/test/stop-resume-hook.test.ts +193 -0
  147. package/packages/omo-codex/plugin/hooks/post-compact-resetting-git-bash-mcp-reminder.json +1 -1
  148. package/packages/omo-codex/plugin/hooks/post-compact-resetting-lsp-diagnostics-cache.json +1 -1
  149. package/packages/omo-codex/plugin/hooks/post-compact-resetting-project-rule-cache.json +1 -1
  150. package/packages/omo-codex/plugin/hooks/post-tool-use-checking-codegraph-init-guidance.json +1 -1
  151. package/packages/omo-codex/plugin/hooks/post-tool-use-checking-comments.json +1 -1
  152. package/packages/omo-codex/plugin/hooks/post-tool-use-checking-lsp-diagnostics.json +1 -1
  153. package/packages/omo-codex/plugin/hooks/post-tool-use-checking-thread-title-hygiene.json +1 -1
  154. package/packages/omo-codex/plugin/hooks/post-tool-use-matching-project-rules.json +1 -1
  155. package/packages/omo-codex/plugin/hooks/pre-tool-use-enforcing-unlimited-goal-budget.json +1 -1
  156. package/packages/omo-codex/plugin/hooks/pre-tool-use-guarding-ulw-loop-spawns.json +18 -0
  157. package/packages/omo-codex/plugin/hooks/pre-tool-use-recommending-git-bash-mcp.json +1 -1
  158. package/packages/omo-codex/plugin/hooks/session-start-checking-auto-update.json +1 -1
  159. package/packages/omo-codex/plugin/hooks/session-start-checking-bootstrap-provisioning.json +1 -1
  160. package/packages/omo-codex/plugin/hooks/session-start-checking-codegraph-bootstrap.json +1 -1
  161. package/packages/omo-codex/plugin/hooks/session-start-loading-project-rules.json +1 -1
  162. package/packages/omo-codex/plugin/hooks/session-start-recording-session-telemetry.json +1 -1
  163. package/packages/omo-codex/plugin/hooks/stop-checking-start-work-continuation.json +1 -1
  164. package/packages/omo-codex/plugin/hooks/stop-checking-ulw-loop-resume.json +17 -0
  165. package/packages/omo-codex/plugin/hooks/subagent-stop-checking-start-work-continuation.json +1 -1
  166. package/packages/omo-codex/plugin/hooks/subagent-stop-verifying-lazycodex-executor-evidence.json +2 -2
  167. package/packages/omo-codex/plugin/hooks/user-prompt-submit-checking-ultrawork-trigger.json +1 -1
  168. package/packages/omo-codex/plugin/hooks/user-prompt-submit-checking-ulw-loop-steering.json +1 -1
  169. package/packages/omo-codex/plugin/hooks/user-prompt-submit-loading-project-rules.json +1 -1
  170. package/packages/omo-codex/plugin/model-catalog.json +16 -7
  171. package/packages/omo-codex/plugin/package-lock.json +13 -13
  172. package/packages/omo-codex/plugin/package.json +1 -1
  173. package/packages/omo-codex/plugin/scripts/auto-update.mjs +9 -2
  174. package/packages/omo-codex/plugin/scripts/entry-guard.mjs +26 -0
  175. package/packages/omo-codex/plugin/scripts/materialize-shared-upstreams.mjs +3 -1
  176. package/packages/omo-codex/plugin/scripts/migrate-codex-config/catalog.mjs +16 -7
  177. package/packages/omo-codex/plugin/scripts/migrate-codex-config/multi-agent-v2-guard.mjs +39 -4
  178. package/packages/omo-codex/plugin/scripts/migrate-codex-config/subagent-limit-guard.mjs +24 -4
  179. package/packages/omo-codex/plugin/scripts/migrate-codex-config.mjs +3 -3
  180. package/packages/omo-codex/plugin/scripts/migrate-omo-sot.mjs +2 -2
  181. package/packages/omo-codex/plugin/scripts/sync-hook-status-messages.mjs +2 -2
  182. package/packages/omo-codex/plugin/scripts/sync-skills.mjs +14 -6
  183. package/packages/omo-codex/plugin/scripts/sync-version.mjs +4 -2
  184. package/packages/omo-codex/plugin/skills/init-deep/SKILL.md +3 -3
  185. package/packages/omo-codex/plugin/skills/refactor/SKILL.md +3 -3
  186. package/packages/omo-codex/plugin/skills/remove-ai-slops/SKILL.md +5 -5
  187. package/packages/omo-codex/plugin/skills/review-work/SKILL.md +9 -3
  188. package/packages/omo-codex/plugin/skills/start-work/SKILL.md +6 -3
  189. package/packages/omo-codex/plugin/skills/teammode/SKILL.md +136 -76
  190. package/packages/omo-codex/plugin/skills/teammode/scripts/team-guide.mjs +94 -23
  191. package/packages/omo-codex/plugin/skills/teammode/scripts/team-state.mjs +94 -14
  192. package/packages/omo-codex/plugin/skills/teammode/scripts/team-transport.mjs +55 -0
  193. package/packages/omo-codex/plugin/skills/teammode/scripts/team.mjs +72 -18
  194. package/packages/omo-codex/plugin/skills/ultrawork/SKILL.md +55 -28
  195. package/packages/omo-codex/plugin/skills/ulw-loop/SKILL.md +14 -15
  196. package/packages/omo-codex/plugin/skills/ulw-loop/references/full-workflow.md +29 -27
  197. package/packages/omo-codex/plugin/skills/ulw-plan/references/full-workflow.md +1 -1
  198. package/packages/omo-codex/plugin/skills/ulw-plan/scripts/scaffold-plan.mjs +2 -2
  199. package/packages/omo-codex/plugin/skills/ulw-research/SKILL.md +4 -4
  200. package/packages/omo-codex/plugin/skills/visual-qa/SKILL.md +4 -4
  201. package/packages/omo-codex/plugin/test/aggregate-agents.test.mjs +116 -10
  202. package/packages/omo-codex/plugin/test/aggregate-hooks.test.mjs +23 -2
  203. package/packages/omo-codex/plugin/test/aggregate-manifest.test.mjs +1 -1
  204. package/packages/omo-codex/plugin/test/aggregate-model-catalog.test.mjs +4 -4
  205. package/packages/omo-codex/plugin/test/aggregate-skills.test.mjs +4 -2
  206. package/packages/omo-codex/plugin/test/auto-update-restart-notice.test.mjs +33 -0
  207. package/packages/omo-codex/plugin/test/auto-update.test.mjs +4 -4
  208. package/packages/omo-codex/plugin/test/component-hook-contract-cases.mjs +2 -2
  209. package/packages/omo-codex/plugin/test/lcx-bug-skills.test.mjs +4 -101
  210. package/packages/omo-codex/plugin/test/migrate-codex-config.test.mjs +160 -23
  211. package/packages/omo-codex/plugin/test/multi-agent-v2-regression.test.mjs +71 -0
  212. package/packages/omo-codex/plugin/test/subagent-limit-migration.test.mjs +32 -0
  213. package/packages/omo-codex/plugin/test/sync-skills-orchestration.test.mjs +20 -2
  214. package/packages/omo-codex/plugin/test/sync-skills-test-support.mjs +1 -1
  215. package/packages/omo-codex/plugin/test/sync-skills.test.mjs +4 -2
  216. package/packages/omo-codex/plugin/test/teammode-transport.test.mjs +345 -0
  217. package/packages/omo-codex/plugin/test/ulw-plan-skill-contract.test.mjs +9 -40
  218. package/packages/omo-codex/plugin/test/ulw-research-skill-contract.test.mjs +4 -277
  219. package/packages/omo-codex/scripts/install-config-reasoning.test.mjs +5 -5
  220. package/packages/omo-codex/scripts/install-config.test.mjs +22 -3
  221. package/packages/omo-codex/scripts/install-dist/install-local.mjs +508 -286
  222. package/packages/omo-codex/scripts/install-generated-bundle.test.mjs +111 -1
  223. package/packages/shared-skills/skills/remove-ai-slops/SKILL.md +2 -2
  224. package/packages/shared-skills/skills/review-work/SKILL.md +1 -1
  225. package/packages/shared-skills/skills/start-work/SKILL.md +5 -2
  226. package/packages/shared-skills/skills/ulw-plan/references/full-workflow.md +1 -1
  227. package/packages/shared-skills/skills/ulw-plan/scripts/scaffold-plan.mjs +2 -2
  228. package/packages/shared-skills/skills/ulw-research/SKILL.md +2 -2
  229. package/packages/shared-skills/skills/visual-qa/SKILL.md +1 -1
  230. package/dist/skills/visual-qa/scripts/skill-prompt-contract.test.ts +0 -296
  231. package/packages/omo-codex/plugin/components/ulw-loop/test/skill-contract.test.ts +0 -70
  232. package/packages/omo-codex/plugin/test/ulw-research-epistemic-contract.test.mjs +0 -98
  233. package/packages/shared-skills/skills/visual-qa/scripts/skill-prompt-contract.test.ts +0 -296
  234. /package/packages/omo-codex/plugin/components/rules/bundled-rules/{hephaestus.md → hephaestus/gpt-5.5.md} +0 -0
@@ -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
+ };
@@ -0,0 +1,20 @@
1
+ import { literal, invalid, textField } from "./quality-gate-fields.js";
2
+ export function passedVerdict(value, field) {
3
+ if (value === "not_applicable")
4
+ invalid(`${field} must not be not_applicable.`, field);
5
+ return literal(value, "passed", field);
6
+ }
7
+ export function codeQualityStatusField(value, field) {
8
+ if (value === "CLEAR" || value === "WATCH")
9
+ return value;
10
+ invalid(`${field} must be CLEAR or WATCH.`, field);
11
+ }
12
+ export function adversarialVerdict(row, field) {
13
+ const value = row["verdict"];
14
+ if (value === "passed")
15
+ return { verdict: "passed" };
16
+ if (value === "not_applicable") {
17
+ return { verdict: "not_applicable", reason: textField(row["reason"], `${field}.reason`) };
18
+ }
19
+ invalid(`${field} must be passed or not_applicable with a reason.`, field);
20
+ }
@@ -9,5 +9,6 @@ export interface QualityGateFs {
9
9
  export interface ValidateQualityGateOptions {
10
10
  readonly repoRoot: string;
11
11
  readonly fs: QualityGateFs;
12
+ readonly currentAttemptDir?: string;
12
13
  }
13
14
  export declare function validateQualityGate(input: unknown, opts?: ValidateQualityGateOptions): UlwLoopQualityGate;
@@ -1,5 +1,7 @@
1
1
  import { resolve } from "node:path";
2
+ import { isWithinAttemptDir } from "./paths.js";
2
3
  import { emptyBlockers, invalid, literal, numberField, section, stringArray, textField, } from "./quality-gate-fields.js";
4
+ import { adversarialVerdict, codeQualityStatusField, passedVerdict } from "./quality-gate-verdicts.js";
3
5
  const REVIEWER_ROLES = {
4
6
  codeReview: "lazycodex-code-reviewer",
5
7
  manualQa: "lazycodex-qa-executor",
@@ -32,11 +34,6 @@ function kindField(value, field) {
32
34
  return value;
33
35
  invalid(`${field} must be a supported artifact kind.`, field);
34
36
  }
35
- function passedVerdict(value, field) {
36
- if (value === "not_applicable")
37
- invalid(`${field} must not be not_applicable.`, field);
38
- return literal(value, "passed", field);
39
- }
40
37
  function artifactCompatible(surface, kind) {
41
38
  switch (surface) {
42
39
  case "cli":
@@ -59,9 +56,13 @@ function checkFile(path, field, opts) {
59
56
  const absolute = resolve(opts.repoRoot, path);
60
57
  if (!opts.fs.existsSync(absolute))
61
58
  invalid(`${field} must point to an existing artifact.`, field);
62
- const stat = opts.fs.statSync(absolute);
63
- if (stat.size <= 0)
59
+ if (opts.fs.statSync(absolute).size <= 0)
64
60
  invalid(`${field} must point to a non-empty artifact.`, field);
61
+ if (opts.currentAttemptDir !== undefined) {
62
+ const attemptRoot = resolve(opts.repoRoot, opts.currentAttemptDir);
63
+ if (!isWithinAttemptDir(absolute, attemptRoot))
64
+ invalid(`${field} (${path}) must point to an artifact from the current attempt (${opts.currentAttemptDir}).`, field);
65
+ }
65
66
  }
66
67
  function artifactMap(refs) {
67
68
  const byId = new Map();
@@ -118,7 +119,7 @@ export function validateQualityGate(input, opts) {
118
119
  codeReview: {
119
120
  by: reviewerRoleField(codeReview["by"], REVIEWER_ROLES.codeReview, "codeReview.by"),
120
121
  recommendation: literal(codeReview["recommendation"], "APPROVE", "codeReview.recommendation"),
121
- codeQualityStatus: literal(codeReview["codeQualityStatus"], "CLEAR", "codeReview.codeQualityStatus"),
122
+ codeQualityStatus: codeQualityStatusField(codeReview["codeQualityStatus"], "codeReview.codeQualityStatus"),
122
123
  reportPath: codeReportPath,
123
124
  evidence: textField(codeReview["evidence"], "codeReview.evidence"),
124
125
  blockers: emptyBlockers(codeReview["blockers"], "codeReview.blockers"),
@@ -182,12 +183,14 @@ function parseAdversarialCases(value, byId) {
182
183
  return value.map((item, index) => {
183
184
  const row = section(item, `manualQa.adversarialCases[${index}]`);
184
185
  const artifacts = referencedArtifacts(row["artifactRefs"], `manualQa.adversarialCases[${index}].artifactRefs`, byId);
186
+ const verdictInfo = adversarialVerdict(row, `manualQa.adversarialCases[${index}]`);
185
187
  return {
186
188
  id: textField(row["id"], `manualQa.adversarialCases[${index}].id`),
187
189
  criterionRef: textField(row["criterionRef"], `manualQa.adversarialCases[${index}].criterionRef`),
188
190
  scenario: textField(row["scenario"], `manualQa.adversarialCases[${index}].scenario`),
189
191
  expectedBehavior: textField(row["expectedBehavior"], `manualQa.adversarialCases[${index}].expectedBehavior`),
190
- verdict: passedVerdict(row["verdict"], `manualQa.adversarialCases[${index}].verdict`),
192
+ verdict: verdictInfo.verdict,
193
+ ...(verdictInfo.reason === undefined ? {} : { reason: verdictInfo.reason }),
191
194
  artifactRefs: artifacts.map((artifact) => artifact.id),
192
195
  };
193
196
  });
@@ -0,0 +1,3 @@
1
+ import type { PreToolUsePayload } from "./codex-hook.js";
2
+ export declare function applySpawnGuards(payload: PreToolUsePayload): string;
3
+ export declare function runSpawnGuardCli(stdin: NodeJS.ReadableStream, stdout: NodeJS.WritableStream): Promise<void>;
@@ -0,0 +1,148 @@
1
+ import { existsSync, readdirSync, readFileSync, statSync, writeFileSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { parsePreToolUsePayload } from "./codex-hook.js";
4
+ import { isFinalRunCompletionCandidate } from "./goal-status.js";
5
+ import { ulwLoopAttemptEvidenceDir, ulwLoopDir } from "./paths.js";
6
+ // spawn_agent = v1; collaborationspawn_agent = the delimiter-free flattened v2
7
+ // hook token from codex-rs hook_names.rs; collaboration.spawn_agent = the
8
+ // dotted token observed live in the task-1 probe (hook-tool-tokens.txt).
9
+ const SPAWN_TOOL_TOKENS = new Set(["spawn_agent", "collaborationspawn_agent", "collaboration.spawn_agent"]);
10
+ const DEFAULT_FANOUT_LIMIT = 60;
11
+ const GATE_MESSAGE_PATTERN = /lazycodex-gate-reviewer|final gate review/i;
12
+ export function applySpawnGuards(payload) {
13
+ if (payload.hook_event_name !== "PreToolUse" || !SPAWN_TOOL_TOKENS.has(payload.tool_name))
14
+ return "";
15
+ const stateDir = ulwLoopDir(payload.cwd, { sessionId: payload.session_id });
16
+ const plan = readPlan(join(stateDir, "goals.json"));
17
+ if (plan === null)
18
+ return "";
19
+ const fanOutDenial = consumeFanOutBudget(stateDir);
20
+ if (fanOutDenial !== null)
21
+ return deny(fanOutDenial);
22
+ const missingArtifact = missingGateArtifact(payload, plan);
23
+ if (missingArtifact !== null)
24
+ return deny(`spawn code-review + QA first; gate audits their artifacts: missing ${missingArtifact}`);
25
+ return "";
26
+ }
27
+ export async function runSpawnGuardCli(stdin, stdout) {
28
+ try {
29
+ const chunks = [];
30
+ for await (const chunk of stdin)
31
+ chunks.push(Buffer.from(chunk));
32
+ const payload = parsePreToolUsePayload(Buffer.concat(chunks).toString("utf8"));
33
+ if (payload === null)
34
+ return;
35
+ const output = applySpawnGuards(payload);
36
+ if (output.length > 0)
37
+ stdout.write(output);
38
+ }
39
+ catch (error) {
40
+ if (error instanceof Error)
41
+ return;
42
+ }
43
+ }
44
+ // Per-session spawn counter; depth/lineage tracking is descoped — this is a
45
+ // total-volume backstop against fan-out explosions, not a recursion tracker.
46
+ function consumeFanOutBudget(stateDir) {
47
+ const counterPath = join(stateDir, "spawn-count.json");
48
+ const count = readCount(counterPath) + 1;
49
+ writeFileSync(counterPath, JSON.stringify({ count }));
50
+ const limit = fanOutLimit();
51
+ if (count <= limit)
52
+ return null;
53
+ 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.`;
54
+ }
55
+ function missingGateArtifact(payload, plan) {
56
+ if (!isGateReviewerSpawn(payload.tool_input))
57
+ return null;
58
+ const goal = plan.goals.find((candidate) => isFinalRunCompletionCandidate(plan, candidate));
59
+ if (goal === undefined || goal.status === "complete")
60
+ return null;
61
+ if (!goal.successCriteria.every((criterion) => criterion.status === "pass"))
62
+ return null;
63
+ const scope = { sessionId: payload.session_id };
64
+ if (plan.evidenceLayoutVersion === 2) {
65
+ const attemptDir = ulwLoopAttemptEvidenceDir(goal.id, goal.attempt, scope);
66
+ for (const name of [`${goal.id}-code-review.md`, `${goal.id}-manual-qa.md`]) {
67
+ const relative = `${attemptDir}/${name}`;
68
+ if (!isNonEmptyFile(join(payload.cwd, relative)))
69
+ return relative;
70
+ }
71
+ return null;
72
+ }
73
+ const flatReport = `.omo/evidence/${goal.id}-code-review.md`;
74
+ if (!isNonEmptyFile(join(payload.cwd, flatReport)))
75
+ return flatReport;
76
+ // v1 manual-QA approximation: any other non-empty evidence file counts.
77
+ if (!hasOtherEvidenceFile(join(payload.cwd, ".omo", "evidence"), `${goal.id}-code-review.md`))
78
+ return `.omo/evidence/<any manual-QA artifact besides ${goal.id}-code-review.md>`;
79
+ return null;
80
+ }
81
+ function isGateReviewerSpawn(toolInput) {
82
+ if (typeof toolInput !== "object" || toolInput === null)
83
+ return false;
84
+ const record = toolInput;
85
+ const agentType = record["agent_type"];
86
+ if (typeof agentType === "string")
87
+ return agentType === "lazycodex-gate-reviewer";
88
+ const message = record["message"];
89
+ return typeof message === "string" && GATE_MESSAGE_PATTERN.test(message);
90
+ }
91
+ function deny(reason) {
92
+ return `${JSON.stringify({
93
+ hookSpecificOutput: {
94
+ hookEventName: "PreToolUse",
95
+ permissionDecision: "deny",
96
+ permissionDecisionReason: reason,
97
+ additionalContext: reason,
98
+ },
99
+ })}\n`;
100
+ }
101
+ function fanOutLimit() {
102
+ const raw = process.env["OMO_SPAWN_FANOUT_LIMIT"];
103
+ if (raw === undefined)
104
+ return DEFAULT_FANOUT_LIMIT;
105
+ const parsed = Number.parseInt(raw, 10);
106
+ return Number.isFinite(parsed) && parsed > 0 ? parsed : DEFAULT_FANOUT_LIMIT;
107
+ }
108
+ function isNonEmptyFile(path) {
109
+ try {
110
+ return existsSync(path) && statSync(path).size > 0;
111
+ }
112
+ catch (error) {
113
+ if (error instanceof Error)
114
+ return false;
115
+ throw error;
116
+ }
117
+ }
118
+ function hasOtherEvidenceFile(evidenceDir, excludedName) {
119
+ try {
120
+ return readdirSync(evidenceDir).some((name) => name !== excludedName && isNonEmptyFile(join(evidenceDir, name)));
121
+ }
122
+ catch (error) {
123
+ if (error instanceof Error)
124
+ return false;
125
+ throw error;
126
+ }
127
+ }
128
+ function readCount(counterPath) {
129
+ try {
130
+ const parsed = JSON.parse(readFileSync(counterPath, "utf8"));
131
+ return typeof parsed["count"] === "number" && parsed["count"] >= 0 ? parsed["count"] : 0;
132
+ }
133
+ catch (error) {
134
+ if (error instanceof Error)
135
+ return 0;
136
+ throw error;
137
+ }
138
+ }
139
+ function readPlan(goalsPath) {
140
+ try {
141
+ return JSON.parse(readFileSync(goalsPath, "utf8"));
142
+ }
143
+ catch (error) {
144
+ if (error instanceof Error)
145
+ return null;
146
+ throw error;
147
+ }
148
+ }
@@ -0,0 +1,2 @@
1
+ export declare function runStopResumeHook(input: unknown): string;
2
+ export declare function runStopResumeHookCli(stdin: NodeJS.ReadableStream, stdout: NodeJS.WritableStream): Promise<void>;
@@ -0,0 +1,209 @@
1
+ import { existsSync, readFileSync, writeFileSync } from "node:fs";
2
+ import { isAbsolute, join } from "node:path";
3
+ import { normalizeUlwLoopSessionId, ulwLoopDir } from "./paths.js";
4
+ // Turn-death recovery only: Codex emits Stop when a turn ends, so a run that
5
+ // dies mid-turn (crash, kill, context blowup before any Stop) never reaches
6
+ // this handler. Mid-turn stalls are out of scope by design.
7
+ const RESUME_CAP = 2;
8
+ // Mirrors start-work-continuation's context-pressure bail-out: injecting a
9
+ // resume directive into an already-overflowing context makes things worse.
10
+ const CONTEXT_PRESSURE_MARKERS = [
11
+ "context compacted",
12
+ "context_length_exceeded",
13
+ "skill descriptions were shortened",
14
+ "context_too_large",
15
+ "codex ran out of room in the model's context window",
16
+ "your input exceeds the context window",
17
+ "long threads and multiple compactions",
18
+ ];
19
+ export function runStopResumeHook(input) {
20
+ const payload = parseStopPayload(input);
21
+ if (payload === null || payload.stop_hook_active)
22
+ return "";
23
+ if (transcriptShowsContextPressure(payload.transcript_path))
24
+ return "";
25
+ if (boulderContinuationWillFire(payload.cwd, payload.session_id))
26
+ return "";
27
+ const stateDir = ulwLoopDir(payload.cwd, { sessionId: payload.session_id });
28
+ const plan = readPlan(join(stateDir, "goals.json"));
29
+ if (plan === null || plan.aggregateCompletion?.status === "complete")
30
+ return "";
31
+ const goal = resumableGoal(plan);
32
+ if (goal === undefined)
33
+ return "";
34
+ if (!consumeResumeBudget(stateDir, goal.id))
35
+ return "";
36
+ const output = {
37
+ decision: "block",
38
+ reason: renderResumeDirective(plan, goal, payload.session_id),
39
+ };
40
+ return JSON.stringify(output);
41
+ }
42
+ export async function runStopResumeHookCli(stdin, stdout) {
43
+ try {
44
+ const chunks = [];
45
+ for await (const chunk of stdin)
46
+ chunks.push(Buffer.from(chunk));
47
+ const output = runStopResumeHook(JSON.parse(Buffer.concat(chunks).toString("utf8")));
48
+ if (output.length > 0)
49
+ stdout.write(output);
50
+ }
51
+ catch (error) {
52
+ if (error instanceof Error)
53
+ return;
54
+ }
55
+ }
56
+ function resumableGoal(plan) {
57
+ const active = plan.goals.find((goal) => goal.id === plan.activeGoalId);
58
+ if (active !== undefined && isResumableStatus(active.status))
59
+ return active;
60
+ return plan.goals.find((goal) => isResumableStatus(goal.status));
61
+ }
62
+ function isResumableStatus(status) {
63
+ return status === "pending" || status === "in_progress";
64
+ }
65
+ // Two-strike cap keyed on ledger movement: an unchanged ledger.jsonl line
66
+ // count across resumes means the loop is not progressing. The stuck marker is
67
+ // a separate file — a ledger append would change the count and self-reset.
68
+ function consumeResumeBudget(stateDir, goalId) {
69
+ const ledgerLineCount = countLedgerLines(join(stateDir, "ledger.jsonl"));
70
+ const counterPath = join(stateDir, `auto-resume-${goalId}.json`);
71
+ const previous = readCounter(counterPath);
72
+ const count = previous !== null && previous.ledgerLineCount === ledgerLineCount ? previous.count : 0;
73
+ if (count >= RESUME_CAP) {
74
+ writeFileSync(join(stateDir, `auto-resume-${goalId}.stuck`), `no ledger progress after ${count} resumes\n`);
75
+ return false;
76
+ }
77
+ writeFileSync(counterPath, JSON.stringify({ count: count + 1, ledgerLineCount }));
78
+ return true;
79
+ }
80
+ function renderResumeDirective(plan, goal, sessionId) {
81
+ const normalized = normalizeUlwLoopSessionId(sessionId);
82
+ const option = normalized !== null && plan.goalsPath.includes(`/${normalized}/`) ? ` --session-id ${normalized}` : "";
83
+ return [
84
+ `The ulw-loop run in this session still has unfinished goals (next: ${goal.id} — ${goal.title}).`,
85
+ "The turn ended before the loop completed. Resume it now:",
86
+ `1. Run \`omo ulw-loop status${option} --json\` to reload the plan, the active goal, and currentAttemptDir.`,
87
+ "2. Continue the active goal's remaining success criteria, recording evidence with record-evidence.",
88
+ `3. Checkpoint through \`omo ulw-loop checkpoint${option}\` when the goal's criteria are proven.`,
89
+ "If the loop is genuinely blocked on the user, checkpoint the goal as blocked with the reason instead.",
90
+ ].join("\n");
91
+ }
92
+ function readPlan(goalsPath) {
93
+ try {
94
+ return JSON.parse(readFileSync(goalsPath, "utf8"));
95
+ }
96
+ catch (error) {
97
+ if (error instanceof Error)
98
+ return null;
99
+ throw error;
100
+ }
101
+ }
102
+ function countLedgerLines(ledgerPath) {
103
+ try {
104
+ return readFileSync(ledgerPath, "utf8").split("\n").filter(Boolean).length;
105
+ }
106
+ catch (error) {
107
+ if (error instanceof Error)
108
+ return 0;
109
+ throw error;
110
+ }
111
+ }
112
+ function readCounter(counterPath) {
113
+ try {
114
+ if (!existsSync(counterPath))
115
+ return null;
116
+ const parsed = JSON.parse(readFileSync(counterPath, "utf8"));
117
+ if (typeof parsed["count"] !== "number" || typeof parsed["ledgerLineCount"] !== "number")
118
+ return null;
119
+ return { count: parsed["count"], ledgerLineCount: parsed["ledgerLineCount"] };
120
+ }
121
+ catch (error) {
122
+ if (error instanceof Error)
123
+ return null;
124
+ throw error;
125
+ }
126
+ }
127
+ // Local ~10-LOC approximation of start-work-continuation's boulder check (no
128
+ // cross-component import allowed): any continuable work for this session means
129
+ // that hook owns the Stop event, so this one stays silent.
130
+ function boulderContinuationWillFire(cwd, sessionId) {
131
+ try {
132
+ const raw = JSON.parse(readFileSync(join(cwd, ".omo", "boulder.json"), "utf8"));
133
+ const works = raw["works"];
134
+ // The flat legacy shape has no works map: the top level is the single work.
135
+ const entries = typeof works === "object" && works !== null ? Object.values(works) : [raw];
136
+ return entries.some((work) => {
137
+ if (typeof work !== "object" || work === null)
138
+ return false;
139
+ const entry = work;
140
+ const sessionIds = Array.isArray(entry["session_ids"]) ? entry["session_ids"] : [];
141
+ const continuable = entry["status"] === "active" || entry["status"] === "paused";
142
+ return continuable && sessionIds.includes(`codex:${sessionId}`) && boulderPlanHasRemainingTask(cwd, entry);
143
+ });
144
+ }
145
+ catch (error) {
146
+ if (error instanceof Error)
147
+ return false;
148
+ throw error;
149
+ }
150
+ }
151
+ function transcriptShowsContextPressure(transcriptPath) {
152
+ try {
153
+ const transcript = readFileSync(transcriptPath, "utf8").toLowerCase();
154
+ return CONTEXT_PRESSURE_MARKERS.some((marker) => transcript.includes(marker));
155
+ }
156
+ catch (error) {
157
+ if (error instanceof Error)
158
+ return false;
159
+ throw error;
160
+ }
161
+ }
162
+ // start-work-continuation only fires while its plan checklist has remaining
163
+ // items; a boulder work whose plan is exhausted (or unreadable) leaves the
164
+ // Stop event to this hook.
165
+ function boulderPlanHasRemainingTask(cwd, entry) {
166
+ const activePlan = entry["active_plan"];
167
+ if (typeof activePlan !== "string" || activePlan.trim().length === 0)
168
+ return false;
169
+ const planPath = isAbsolute(activePlan) ? activePlan : join(cwd, activePlan);
170
+ const worktree = entry["worktree_path"];
171
+ const candidates = typeof worktree === "string" && worktree.trim().length > 0 && !isAbsolute(activePlan)
172
+ ? [join(isAbsolute(worktree) ? worktree : join(cwd, worktree), activePlan), planPath]
173
+ : [planPath];
174
+ for (const candidate of candidates) {
175
+ try {
176
+ return readFileSync(candidate, "utf8")
177
+ .split(/\r?\n/)
178
+ .some((line) => line.startsWith("- [ ] "));
179
+ }
180
+ catch (error) {
181
+ if (!(error instanceof Error))
182
+ throw error;
183
+ }
184
+ }
185
+ return false;
186
+ }
187
+ function parseStopPayload(value) {
188
+ if (typeof value !== "object" || value === null || Array.isArray(value))
189
+ return null;
190
+ const record = value;
191
+ const optionalMessage = record["last_assistant_message"];
192
+ const valid = record["hook_event_name"] === "Stop" &&
193
+ typeof record["session_id"] === "string" &&
194
+ typeof record["turn_id"] === "string" &&
195
+ typeof record["transcript_path"] === "string" &&
196
+ typeof record["cwd"] === "string" &&
197
+ typeof record["model"] === "string" &&
198
+ typeof record["permission_mode"] === "string" &&
199
+ typeof record["stop_hook_active"] === "boolean" &&
200
+ (optionalMessage === undefined || typeof optionalMessage === "string");
201
+ if (!valid)
202
+ return null;
203
+ return {
204
+ session_id: record["session_id"],
205
+ cwd: record["cwd"],
206
+ transcript_path: record["transcript_path"],
207
+ stop_hook_active: record["stop_hook_active"],
208
+ };
209
+ }
@@ -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 4.16.2) Checking Ulw-Loop Steering"
10
+ "statusMessage": "(OmO 4.17.0) Checking Ulw-Loop Steering"
11
11
  }
12
12
  ]
13
13
  }
@@ -20,7 +20,30 @@
20
20
  "type": "command",
21
21
  "command": "node \"${PLUGIN_ROOT}/dist/cli.js\" hook pre-tool-use",
22
22
  "timeout": 5,
23
- "statusMessage": "(OmO 4.16.2) Enforcing Unlimited Ulw-Loop Budget"
23
+ "statusMessage": "(OmO 4.17.0) Enforcing Unlimited Ulw-Loop Budget"
24
+ }
25
+ ]
26
+ },
27
+ {
28
+ "matcher": "^(spawn_agent|collaborationspawn_agent|collaboration\\.spawn_agent)$",
29
+ "hooks": [
30
+ {
31
+ "type": "command",
32
+ "command": "node \"${PLUGIN_ROOT}/dist/cli.js\" hook pre-tool-use-spawn",
33
+ "timeout": 5,
34
+ "statusMessage": "(OmO 4.17.0) Guarding Ulw-Loop Spawns"
35
+ }
36
+ ]
37
+ }
38
+ ],
39
+ "Stop": [
40
+ {
41
+ "hooks": [
42
+ {
43
+ "type": "command",
44
+ "command": "node \"${PLUGIN_ROOT}/dist/cli.js\" hook stop",
45
+ "timeout": 10,
46
+ "statusMessage": "(OmO 4.17.0) Checking Ulw-Loop Resume"
24
47
  }
25
48
  ]
26
49
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@code-yeongyu/codex-ulw-loop",
3
- "version": "4.16.2",
3
+ "version": "4.17.0",
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",
@@ -23,29 +23,28 @@ This skill is intentionally compact. The full workflow lives in `references/full
23
23
  - After any compaction or context loss, re-read brief + goals + ledger FIRST plus `omo ulw-loop status --json`, then resume; never re-plan from scratch.
24
24
  - If `omo ulw-loop create-goals` says the existing aggregate is already complete, start unrelated new work with a fresh `--session-id <new-id>` instead of steering or forcing the completed default state. Use `--force` only to intentionally overwrite completed evidence.
25
25
  - Every success criterion needs observable evidence from a real surface: a channel (terminal/TUI via the xterm.js web terminal, HTTP, browser, computer-use) or, for CLI- or data-shaped criteria, an auxiliary surface (CLI stdout, DB diff, parsed config dump).
26
- - Record evidence through the CLI only after cleanup receipts are available.
26
+ - Evidence is bound to its capture commit; a later fix, rebase, or merge makes it stale — re-run at the current HEAD and re-record, never relabel or regenerate. Record only after cleanup receipts exist.
27
27
  - Delegate code edits, test writes, fixes, and QA execution to right-sized Codex subagents when the workflow requires it.
28
- - Every `multi_agent_v1.spawn_agent` message starts with `TASK:`, then names `DELIVERABLE`, `SCOPE`, and `VERIFY`; put role and specialty instructions inside `message`; use `fork_context: false` unless full history is truly required.
29
- - Plan and reviewer agents may run for a long time; spawn them in the background, keep doing independent root work, and poll with short `multi_agent_v1.wait_agent` cycles. Never use a single long blocking wait for them.
28
+ - Every `spawn_agent` message starts with `TASK:`, then names `DELIVERABLE`, `SCOPE`, and `VERIFY`; put role and specialty instructions inside `message`; use `fork_turns: "none"` (v1: `fork_context: false`) unless full history is truly required.
29
+ - Plan and reviewer agents may run for a long time; spawn them in the background, keep doing independent root work, and poll with short `wait_agent` cycles. Never use a single long blocking wait for them.
30
30
  - For work likely to exceed one wait cycle, require the child to send `WORKING: <task> - <current phase>` before long reading, testing, or review passes, and `BLOCKED: <reason>` only when it cannot progress.
31
- - Track spawned agent names locally. Use `multi_agent_v1.wait_agent` for mailbox signals, not proof of completion. A timeout only means no new mailbox update arrived. Treat a running child as alive.
31
+ - Track spawned agent names locally. Use `wait_agent` for mailbox signals, not proof of completion. A timeout only means no new mailbox update arrived. Treat a running child as alive.
32
32
  - While children run, surface the active subagent count, agent names, and latest `WORKING:` phase.
33
- - Fallback only when the child is completed without the deliverable, ack-only after followup, explicitly `BLOCKED:`, or no longer running. Then record inconclusive and respawn a smaller `fork_context: false` task with the missing deliverable.
33
+ - Fallback only when the child is completed without the deliverable, ack-only after `followup_task`, explicitly `BLOCKED:`, or no longer running. Then record inconclusive and respawn a smaller `fork_turns: "none"` task with the missing deliverable.
34
34
  - Use `git-master` for git-tracked edits: inspect recent and touched-path commit history, then commit each verified work unit atomically in the repository's observed language, scope, and message style with only that unit's files staged.
35
35
 
36
36
  ## Codex Tool Mapping
37
37
 
38
- The full workflow may mention OpenCode-style orchestration examples. In Codex, translate them to native tools:
38
+ Codex exposes ONE subagent surface per session — check your tool list. GPT-5.6 (sol/terra) get the flat MultiAgentV2 tools (primary); GPT-5.5 and gpt-5.6-luna get the namespaced `multi_agent_v1.*` set (fallback row). The workflow's orchestration examples map to:
39
39
 
40
- | Workflow intent | Codex tool |
40
+ | Intent | MultiAgentV2 (gpt-5.6 sol/terra) |
41
41
  | --- | --- |
42
- | Plan agent | `multi_agent_v1.spawn_agent({"message":"TASK: act as a planning agent. ...","fork_context":false})` |
43
- | Search/read-only worker | `multi_agent_v1.spawn_agent({"message":"TASK: act as an explorer. ...","fork_context":false})` |
44
- | Implementation or QA worker | `multi_agent_v1.spawn_agent({"message":"TASK: act as an implementation or QA worker. ...","fork_context":false})` |
45
- | Final verification reviewer | `multi_agent_v1.spawn_agent({"message":"TASK: act as a rigorous reviewer. ...","fork_context":false})` |
46
- | Wait for background result | `multi_agent_v1.wait_agent(...)` |
47
- | Clean up finished worker | `multi_agent_v1.close_agent(...)` |
48
-
49
- Flat `spawn_agent` requiring `task_name` instead (`multi_agent_v2`)? Rewrite rows: add `"task_name"`, `"fork_context":false` → `"fork_turns":"none"`, `wait_agent` takes only `timeout_ms`, no `close_agent` — finished agents end on their own.
42
+ | Spawn a worker | `spawn_agent({"task_name":"<lower_snake_id>","message":"TASK: act as <role>. ...","fork_turns":"none"})` — `task_name`+`message` required; `fork_turns:"none"` = no parent history; do NOT set `agent_type`/`model`/`reasoning_effort` |
43
+ | Re-task an idle worker (wakes it) | `followup_task({"target":"<name>","message":"..."})` |
44
+ | Send context without interrupting | `send_message({"target":"<name>","message":"..."})` |
45
+ | Wait for a mailbox signal | `wait_agent({"timeout_ms":<ms>})` any live worker; a timeout only means no new update |
46
+ | Enumerate / stop a runaway | `list_agents()` / `interrupt_agent({"target":"<name>"})` — no `close_agent`; finished workers end on their own |
47
+
48
+ V1 fallback (gpt-5.5, gpt-5.6-luna): `multi_agent_v1.spawn_agent({...,"fork_context":false})`, `multi_agent_v1.send_input` (re-task), `multi_agent_v1.wait_agent({"targets":[...],"timeout_ms":...})`, `multi_agent_v1.close_agent`.
50
49
 
51
50
  When translating `load_skills=[...]`, include the requested skill names in the spawned agent's `message`.