oh-my-opencode 5.0.0-beta.62 → 5.0.0-beta.64

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 (249) hide show
  1. package/THIRD-PARTY-NOTICES.md +8 -6
  2. package/dist/cli/doctor/checks/browser-provider.d.ts +2 -0
  3. package/dist/cli/index.js +70 -41
  4. package/dist/cli-node/index.js +70 -41
  5. package/dist/config/schema/agent-names.d.ts +0 -1
  6. package/dist/config/schema/browser-automation.d.ts +0 -2
  7. package/dist/config/schema/oh-my-opencode-config.d.ts +0 -1
  8. package/dist/hooks/comment-checker/__fixtures__/download-probe.fixture.d.ts +1 -0
  9. package/dist/index.js +89 -588
  10. package/dist/oh-my-opencode.schema.json +4 -5
  11. package/dist/skills/ast-grep/SKILL.md +12 -0
  12. package/dist/skills/debugging/references/tools/playwright-cli.md +79 -161
  13. package/dist/skills/frontend/SKILL.md +1 -1
  14. package/dist/skills/frontend/references/design/aside.md +1 -1
  15. package/dist/skills/frontend/references/design/clone-from-url.md +1 -1
  16. package/dist/skills/frontend/scripts/perfection/lighthouse-audit.py +2 -2
  17. package/dist/skills/review-work/SKILL.md +1 -1
  18. package/dist/skills/ultimate-browsing/ATTRIBUTION.md +1 -82
  19. package/dist/skills/ultimate-browsing/SKILL.md +17 -29
  20. package/dist/skills/ultimate-browsing/engine/AGENTS.md +2 -1
  21. package/dist/skills/ultimate-browsing/engine/templates/package.json +2 -2
  22. package/dist/skills/ultimate-browsing/engine/templates/playwright_mobile_chrome.js +5 -2
  23. package/dist/skills/ultimate-browsing/engine/templates/playwright_real_chrome.js +8 -6
  24. package/dist/skills/ultimate-browsing/engine/tests/test_playwright_stealth.py +86 -0
  25. package/dist/skills/ultimate-browsing/engine/tests/test_playwright_templates.py +4 -7
  26. package/dist/skills/ultimate-browsing/references/chrome-stealth.md +86 -100
  27. package/dist/skills/ultimate-browsing/references/insane-search/README.md +9 -3
  28. package/dist/skills/ultimate-browsing/references/insane-search/playwright.md +16 -13
  29. package/dist/skills/ultimate-browsing/scripts/extract_cookies.py +3 -3
  30. package/dist/skills/ulw-execute/SKILL.md +1 -1
  31. package/dist/skills/ulw-plan/scripts/scaffold-plan.mjs +1 -1
  32. package/dist/skills/ulw-research/SKILL.md +3 -3
  33. package/dist/skills/visual-qa/AGENTS.md +1 -1
  34. package/dist/skills/visual-qa/SKILL.md +1 -1
  35. package/dist/skills/visual-qa/references/browser-setup.md +75 -0
  36. package/dist/tui.js +18 -11
  37. package/package.json +16 -23
  38. package/packages/omo-codex/plugin/.codex-plugin/plugin.json +1 -1
  39. package/packages/omo-codex/plugin/components/bootstrap/dist/cli.js +4 -2
  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/comment-checker/hooks/hooks.json +1 -1
  43. package/packages/omo-codex/plugin/components/comment-checker/package.json +1 -1
  44. package/packages/omo-codex/plugin/components/git-bash/hooks/hooks.json +2 -2
  45. package/packages/omo-codex/plugin/components/git-bash/package.json +1 -1
  46. package/packages/omo-codex/plugin/components/lazycodex-executor-verify/hooks/hooks.json +1 -1
  47. package/packages/omo-codex/plugin/components/lazycodex-executor-verify/package.json +1 -1
  48. package/packages/omo-codex/plugin/components/lsp/dist/.omo-runtime-manifest.json +2 -2
  49. package/packages/omo-codex/plugin/components/lsp/hooks/hooks.json +2 -2
  50. package/packages/omo-codex/plugin/components/lsp/package.json +1 -1
  51. package/packages/omo-codex/plugin/components/rules/hooks/hooks.json +4 -4
  52. package/packages/omo-codex/plugin/components/rules/package.json +1 -1
  53. package/packages/omo-codex/plugin/components/teammode/hooks/hooks.json +1 -1
  54. package/packages/omo-codex/plugin/components/teammode/package.json +1 -1
  55. package/packages/omo-codex/plugin/components/telemetry/hooks/hooks.json +1 -1
  56. package/packages/omo-codex/plugin/components/telemetry/package.json +1 -1
  57. package/packages/omo-codex/plugin/components/ultrawork/agents/plan.toml +2 -2
  58. package/packages/omo-codex/plugin/components/ultrawork/dist/cli.js +5 -4
  59. package/packages/omo-codex/plugin/components/ultrawork/hooks/hooks.json +1 -1
  60. package/packages/omo-codex/plugin/components/ultrawork/package.json +1 -1
  61. package/packages/omo-codex/plugin/components/ultrawork/src/directive-content.ts +1 -1
  62. package/packages/omo-codex/plugin/components/ulw-execute-continuation/directive.md +1 -1
  63. package/packages/omo-codex/plugin/components/ulw-execute-continuation/hooks/hooks.json +1 -1
  64. package/packages/omo-codex/plugin/components/ulw-execute-continuation/package.json +1 -1
  65. package/packages/omo-codex/plugin/components/ulw-loop/CHANGELOG.md +13 -0
  66. package/packages/omo-codex/plugin/components/ulw-loop/directive.md +5 -4
  67. package/packages/omo-codex/plugin/components/ulw-loop/dist/checkpoint-codex-validation.d.ts +7 -5
  68. package/packages/omo-codex/plugin/components/ulw-loop/dist/checkpoint-codex-validation.js +7 -2
  69. package/packages/omo-codex/plugin/components/ulw-loop/dist/checkpoint.js +7 -4
  70. package/packages/omo-codex/plugin/components/ulw-loop/dist/cli.js +931 -442
  71. package/packages/omo-codex/plugin/components/ulw-loop/dist/codex-goal-snapshot.d.ts +6 -0
  72. package/packages/omo-codex/plugin/components/ulw-loop/dist/codex-goal-snapshot.js +20 -12
  73. package/packages/omo-codex/plugin/components/ulw-loop/dist/domain-types.d.ts +8 -0
  74. package/packages/omo-codex/plugin/components/ulw-loop/dist/driver-objective-ack.d.ts +4 -0
  75. package/packages/omo-codex/plugin/components/ulw-loop/dist/driver-objective-ack.js +18 -0
  76. package/packages/omo-codex/plugin/components/ulw-loop/dist/evidence-artifacts.d.ts +1 -0
  77. package/packages/omo-codex/plugin/components/ulw-loop/dist/evidence-artifacts.js +37 -0
  78. package/packages/omo-codex/plugin/components/ulw-loop/dist/evidence.d.ts +1 -0
  79. package/packages/omo-codex/plugin/components/ulw-loop/dist/evidence.js +24 -12
  80. package/packages/omo-codex/plugin/components/ulw-loop/dist/ledger.d.ts +4 -0
  81. package/packages/omo-codex/plugin/components/ulw-loop/dist/ledger.js +34 -0
  82. package/packages/omo-codex/plugin/components/ulw-loop/dist/paths.d.ts +1 -0
  83. package/packages/omo-codex/plugin/components/ulw-loop/dist/paths.js +6 -0
  84. package/packages/omo-codex/plugin/components/ulw-loop/dist/plan-commit.d.ts +19 -0
  85. package/packages/omo-codex/plugin/components/ulw-loop/dist/plan-commit.js +107 -0
  86. package/packages/omo-codex/plugin/components/ulw-loop/dist/plan-crud.d.ts +6 -2
  87. package/packages/omo-codex/plugin/components/ulw-loop/dist/plan-crud.js +54 -25
  88. package/packages/omo-codex/plugin/components/ulw-loop/dist/plan-goal-factory.d.ts +10 -3
  89. package/packages/omo-codex/plugin/components/ulw-loop/dist/plan-goal-factory.js +38 -31
  90. package/packages/omo-codex/plugin/components/ulw-loop/dist/plan-io.d.ts +9 -7
  91. package/packages/omo-codex/plugin/components/ulw-loop/dist/plan-io.js +105 -114
  92. package/packages/omo-codex/plugin/components/ulw-loop/dist/plan-log.d.ts +15 -0
  93. package/packages/omo-codex/plugin/components/ulw-loop/dist/plan-log.js +86 -0
  94. package/packages/omo-codex/plugin/components/ulw-loop/dist/plan-missing-recovery.d.ts +5 -1
  95. package/packages/omo-codex/plugin/components/ulw-loop/dist/plan-missing-recovery.js +15 -3
  96. package/packages/omo-codex/plugin/components/ulw-loop/dist/review-blockers.js +11 -8
  97. package/packages/omo-codex/plugin/components/ulw-loop/dist/sdk/factory.js +28 -12
  98. package/packages/omo-codex/plugin/components/ulw-loop/dist/sdk/manifest.d.ts +335 -0
  99. package/packages/omo-codex/plugin/components/ulw-loop/dist/sdk/manifest.js +109 -10
  100. package/packages/omo-codex/plugin/components/ulw-loop/dist/sdk/types.d.ts +3 -0
  101. package/packages/omo-codex/plugin/components/ulw-loop/dist/spawn-budget-io.d.ts +5 -0
  102. package/packages/omo-codex/plugin/components/ulw-loop/dist/spawn-budget-io.js +67 -0
  103. package/packages/omo-codex/plugin/components/ulw-loop/dist/spawn-guard.js +8 -62
  104. package/packages/omo-codex/plugin/components/ulw-loop/dist/state-lock.d.ts +11 -1
  105. package/packages/omo-codex/plugin/components/ulw-loop/dist/state-lock.js +120 -88
  106. package/packages/omo-codex/plugin/components/ulw-loop/dist/status-next-actions.d.ts +3 -2
  107. package/packages/omo-codex/plugin/components/ulw-loop/dist/status-next-actions.js +18 -10
  108. package/packages/omo-codex/plugin/components/ulw-loop/dist/steering-batch.js +4 -4
  109. package/packages/omo-codex/plugin/components/ulw-loop/dist/steering-mutations.d.ts +3 -2
  110. package/packages/omo-codex/plugin/components/ulw-loop/dist/steering-mutations.js +4 -4
  111. package/packages/omo-codex/plugin/components/ulw-loop/dist/steering-types.d.ts +9 -1
  112. package/packages/omo-codex/plugin/components/ulw-loop/dist/steering.d.ts +3 -2
  113. package/packages/omo-codex/plugin/components/ulw-loop/dist/steering.js +11 -11
  114. package/packages/omo-codex/plugin/components/ulw-loop/dist/stop-resume-hook.js +8 -17
  115. package/packages/omo-codex/plugin/components/ulw-loop/dist/success-criteria-input.d.ts +9 -0
  116. package/packages/omo-codex/plugin/components/ulw-loop/dist/success-criteria-input.js +41 -0
  117. package/packages/omo-codex/plugin/components/ulw-loop/hooks/hooks.json +5 -5
  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/references/full-workflow.md +2 -2
  120. package/packages/omo-codex/plugin/components/ulw-loop/src/checkpoint-codex-validation.ts +15 -3
  121. package/packages/omo-codex/plugin/components/ulw-loop/src/checkpoint.ts +7 -4
  122. package/packages/omo-codex/plugin/components/ulw-loop/src/codex-goal-snapshot.ts +26 -14
  123. package/packages/omo-codex/plugin/components/ulw-loop/src/domain-types.ts +8 -0
  124. package/packages/omo-codex/plugin/components/ulw-loop/src/driver-objective-ack.ts +20 -0
  125. package/packages/omo-codex/plugin/components/ulw-loop/src/evidence-artifacts.ts +45 -0
  126. package/packages/omo-codex/plugin/components/ulw-loop/src/evidence.ts +23 -16
  127. package/packages/omo-codex/plugin/components/ulw-loop/src/ledger.ts +33 -0
  128. package/packages/omo-codex/plugin/components/ulw-loop/src/paths.ts +7 -0
  129. package/packages/omo-codex/plugin/components/ulw-loop/src/plan-commit.ts +124 -0
  130. package/packages/omo-codex/plugin/components/ulw-loop/src/plan-crud.ts +67 -54
  131. package/packages/omo-codex/plugin/components/ulw-loop/src/plan-goal-factory.ts +63 -31
  132. package/packages/omo-codex/plugin/components/ulw-loop/src/plan-io.ts +126 -140
  133. package/packages/omo-codex/plugin/components/ulw-loop/src/plan-log.ts +84 -0
  134. package/packages/omo-codex/plugin/components/ulw-loop/src/plan-missing-recovery.ts +26 -3
  135. package/packages/omo-codex/plugin/components/ulw-loop/src/review-blockers.ts +11 -7
  136. package/packages/omo-codex/plugin/components/ulw-loop/src/sdk/factory.ts +47 -12
  137. package/packages/omo-codex/plugin/components/ulw-loop/src/sdk/manifest.ts +124 -10
  138. package/packages/omo-codex/plugin/components/ulw-loop/src/sdk/types.ts +3 -0
  139. package/packages/omo-codex/plugin/components/ulw-loop/src/spawn-budget-io.ts +59 -0
  140. package/packages/omo-codex/plugin/components/ulw-loop/src/spawn-guard.ts +8 -59
  141. package/packages/omo-codex/plugin/components/ulw-loop/src/state-lock.ts +129 -111
  142. package/packages/omo-codex/plugin/components/ulw-loop/src/status-next-actions.ts +19 -10
  143. package/packages/omo-codex/plugin/components/ulw-loop/src/steering-batch.ts +4 -4
  144. package/packages/omo-codex/plugin/components/ulw-loop/src/steering-mutations.ts +5 -4
  145. package/packages/omo-codex/plugin/components/ulw-loop/src/steering-types.ts +13 -1
  146. package/packages/omo-codex/plugin/components/ulw-loop/src/steering.ts +12 -10
  147. package/packages/omo-codex/plugin/components/ulw-loop/src/stop-resume-hook.ts +8 -16
  148. package/packages/omo-codex/plugin/components/ulw-loop/src/success-criteria-input.ts +56 -0
  149. package/packages/omo-codex/plugin/components/ulw-loop/test/codex-goal-snapshot.test.ts +37 -1
  150. package/packages/omo-codex/plugin/components/ulw-loop/test/fixtures/barrier.ts +13 -0
  151. package/packages/omo-codex/plugin/components/ulw-loop/test/fixtures/commit-child.ts +65 -0
  152. package/packages/omo-codex/plugin/components/ulw-loop/test/fixtures/commit-cli-qa.mjs +76 -0
  153. package/packages/omo-codex/plugin/components/ulw-loop/test/fixtures/commit-crash.ts +23 -0
  154. package/packages/omo-codex/plugin/components/ulw-loop/test/fixtures/commit-mutations.mjs +58 -0
  155. package/packages/omo-codex/plugin/components/ulw-loop/test/fixtures/commit-writer.ts +65 -0
  156. package/packages/omo-codex/plugin/components/ulw-loop/test/fixtures/lease-clock.ts +23 -0
  157. package/packages/omo-codex/plugin/components/ulw-loop/test/plan-commit-log.test.ts +252 -0
  158. package/packages/omo-codex/plugin/components/ulw-loop/test/plan-commit-recovery.test.ts +190 -0
  159. package/packages/omo-codex/plugin/components/ulw-loop/test/plan-io.test.ts +50 -35
  160. package/packages/omo-codex/plugin/components/ulw-loop/test/plan-log-newest.test.ts +72 -0
  161. package/packages/omo-codex/plugin/components/ulw-loop/test/sdk-completed-plan.test.ts +43 -0
  162. package/packages/omo-codex/plugin/components/ulw-loop/test/sdk-driver-states.test.ts +33 -4
  163. package/packages/omo-codex/plugin/components/ulw-loop/test/sdk-dx.test.ts +219 -0
  164. package/packages/omo-codex/plugin/components/ulw-loop/test/sdk-session-context.test.ts +21 -0
  165. package/packages/omo-codex/plugin/components/ulw-loop/test/state-lock.test.ts +92 -0
  166. package/packages/omo-codex/plugin/components/ulw-loop/test/status-next-actions-surface.test.ts +133 -0
  167. package/packages/omo-codex/plugin/components/ulw-loop/test/steering-batch.test.ts +12 -12
  168. package/packages/omo-codex/plugin/components/ulw-loop/test/steering.test.ts +4 -1
  169. package/packages/omo-codex/plugin/hooks/post-compact-resetting-git-bash-mcp-reminder.json +1 -1
  170. package/packages/omo-codex/plugin/hooks/post-compact-resetting-lsp-diagnostics-cache.json +1 -1
  171. package/packages/omo-codex/plugin/hooks/post-compact-resetting-project-rule-cache.json +1 -1
  172. package/packages/omo-codex/plugin/hooks/post-tool-use-checking-comments.json +1 -1
  173. package/packages/omo-codex/plugin/hooks/post-tool-use-checking-lsp-diagnostics.json +1 -1
  174. package/packages/omo-codex/plugin/hooks/post-tool-use-checking-thread-title-hygiene.json +1 -1
  175. package/packages/omo-codex/plugin/hooks/post-tool-use-matching-project-rules.json +1 -1
  176. package/packages/omo-codex/plugin/hooks/post-tool-use-recording-spawn-admission.json +1 -1
  177. package/packages/omo-codex/plugin/hooks/pre-tool-use-enforcing-unlimited-goal-budget.json +1 -1
  178. package/packages/omo-codex/plugin/hooks/pre-tool-use-guarding-ulw-loop-spawns.json +1 -1
  179. package/packages/omo-codex/plugin/hooks/pre-tool-use-recommending-git-bash-mcp.json +1 -1
  180. package/packages/omo-codex/plugin/hooks/session-start-checking-auto-update.json +1 -1
  181. package/packages/omo-codex/plugin/hooks/session-start-checking-bootstrap-provisioning.json +1 -1
  182. package/packages/omo-codex/plugin/hooks/session-start-loading-project-rules.json +1 -1
  183. package/packages/omo-codex/plugin/hooks/session-start-recording-session-telemetry.json +1 -1
  184. package/packages/omo-codex/plugin/hooks/stop-checking-ulw-execute-continuation.json +1 -1
  185. package/packages/omo-codex/plugin/hooks/stop-checking-ulw-loop-resume.json +1 -1
  186. package/packages/omo-codex/plugin/hooks/subagent-stop-verifying-lazycodex-executor-evidence.json +1 -1
  187. package/packages/omo-codex/plugin/hooks/user-prompt-submit-checking-ultrawork-trigger.json +1 -1
  188. package/packages/omo-codex/plugin/hooks/user-prompt-submit-checking-ulw-loop-steering.json +1 -1
  189. package/packages/omo-codex/plugin/hooks/user-prompt-submit-loading-project-rules.json +1 -1
  190. package/packages/omo-codex/plugin/package-lock.json +12 -12
  191. package/packages/omo-codex/plugin/package.json +1 -1
  192. package/packages/omo-codex/plugin/scripts/sync-skills.d.mts +4 -0
  193. package/packages/omo-codex/plugin/scripts/sync-skills.mjs +15 -7
  194. package/packages/omo-codex/plugin/skills/ast-grep/SKILL.md +12 -0
  195. package/packages/omo-codex/plugin/skills/debugging/references/tools/playwright-cli.md +79 -161
  196. package/packages/omo-codex/plugin/skills/frontend/SKILL.md +1 -1
  197. package/packages/omo-codex/plugin/skills/frontend/references/design/aside.md +1 -1
  198. package/packages/omo-codex/plugin/skills/frontend/references/design/clone-from-url.md +1 -1
  199. package/packages/omo-codex/plugin/skills/frontend/scripts/perfection/lighthouse-audit.py +2 -2
  200. package/packages/omo-codex/plugin/skills/review-work/SKILL.md +1 -1
  201. package/packages/omo-codex/plugin/skills/ultimate-browsing/ATTRIBUTION.md +1 -82
  202. package/packages/omo-codex/plugin/skills/ultimate-browsing/SKILL.md +17 -29
  203. package/packages/omo-codex/plugin/skills/ultimate-browsing/engine/AGENTS.md +2 -1
  204. package/packages/omo-codex/plugin/skills/ultimate-browsing/engine/templates/package.json +2 -2
  205. package/packages/omo-codex/plugin/skills/ultimate-browsing/engine/templates/playwright_mobile_chrome.js +5 -2
  206. package/packages/omo-codex/plugin/skills/ultimate-browsing/engine/templates/playwright_real_chrome.js +8 -6
  207. package/packages/omo-codex/plugin/skills/ultimate-browsing/engine/tests/test_playwright_stealth.py +86 -0
  208. package/packages/omo-codex/plugin/skills/ultimate-browsing/engine/tests/test_playwright_templates.py +4 -7
  209. package/packages/omo-codex/plugin/skills/ultimate-browsing/references/chrome-stealth.md +86 -100
  210. package/packages/omo-codex/plugin/skills/ultimate-browsing/references/insane-search/README.md +9 -3
  211. package/packages/omo-codex/plugin/skills/ultimate-browsing/references/insane-search/playwright.md +16 -13
  212. package/packages/omo-codex/plugin/skills/ultimate-browsing/scripts/extract_cookies.py +3 -3
  213. package/packages/omo-codex/plugin/skills/ultrawork/SKILL.md +5 -4
  214. package/packages/omo-codex/plugin/skills/ulw-execute/SKILL.md +1 -1
  215. package/packages/omo-codex/plugin/skills/ulw-loop/references/full-workflow.md +2 -2
  216. package/packages/omo-codex/plugin/skills/ulw-research/SKILL.md +3 -3
  217. package/packages/omo-codex/plugin/skills/visual-qa/AGENTS.md +1 -1
  218. package/packages/omo-codex/plugin/skills/visual-qa/SKILL.md +1 -1
  219. package/packages/omo-codex/plugin/skills/visual-qa/references/browser-setup.md +75 -0
  220. package/packages/omo-codex/scripts/install-dist/install-local.mjs +6 -4
  221. package/packages/prompts-core/prompts/ultrawork/codex.md +5 -4
  222. package/packages/shared-skills/skills/ast-grep/SKILL.md +12 -0
  223. package/packages/shared-skills/skills/debugging/references/tools/playwright-cli.md +79 -161
  224. package/packages/shared-skills/skills/frontend/SKILL.md +1 -1
  225. package/packages/shared-skills/skills/frontend/references/design/aside.md +1 -1
  226. package/packages/shared-skills/skills/frontend/references/design/clone-from-url.md +1 -1
  227. package/packages/shared-skills/skills/frontend/scripts/perfection/lighthouse-audit.py +2 -2
  228. package/packages/shared-skills/skills/review-work/SKILL.md +1 -1
  229. package/packages/shared-skills/skills/ultimate-browsing/ATTRIBUTION.md +1 -82
  230. package/packages/shared-skills/skills/ultimate-browsing/SKILL.md +17 -29
  231. package/packages/shared-skills/skills/ultimate-browsing/engine/AGENTS.md +2 -1
  232. package/packages/shared-skills/skills/ultimate-browsing/engine/templates/package.json +2 -2
  233. package/packages/shared-skills/skills/ultimate-browsing/engine/templates/playwright_mobile_chrome.js +5 -2
  234. package/packages/shared-skills/skills/ultimate-browsing/engine/templates/playwright_real_chrome.js +8 -6
  235. package/packages/shared-skills/skills/ultimate-browsing/engine/tests/test_playwright_stealth.py +86 -0
  236. package/packages/shared-skills/skills/ultimate-browsing/engine/tests/test_playwright_templates.py +4 -7
  237. package/packages/shared-skills/skills/ultimate-browsing/references/chrome-stealth.md +86 -100
  238. package/packages/shared-skills/skills/ultimate-browsing/references/insane-search/README.md +9 -3
  239. package/packages/shared-skills/skills/ultimate-browsing/references/insane-search/playwright.md +16 -13
  240. package/packages/shared-skills/skills/ultimate-browsing/scripts/extract_cookies.py +3 -3
  241. package/packages/shared-skills/skills/ulw-execute/SKILL.md +1 -1
  242. package/packages/shared-skills/skills/ulw-plan/scripts/scaffold-plan.mjs +1 -1
  243. package/packages/shared-skills/skills/ulw-research/SKILL.md +3 -3
  244. package/packages/shared-skills/skills/visual-qa/AGENTS.md +1 -1
  245. package/packages/shared-skills/skills/visual-qa/SKILL.md +1 -1
  246. package/packages/shared-skills/skills/visual-qa/references/browser-setup.md +75 -0
  247. package/dist/skills/visual-qa/references/agent-browser-setup.md +0 -45
  248. package/packages/omo-codex/plugin/skills/visual-qa/references/agent-browser-setup.md +0 -45
  249. package/packages/shared-skills/skills/visual-qa/references/agent-browser-setup.md +0 -45
@@ -1,120 +1,159 @@
1
1
  import { randomUUID } from "node:crypto";
2
- import { closeSync, mkdirSync, openSync, readFileSync, statSync, unlinkSync, writeSync } from "node:fs";
2
+ import { closeSync, ftruncateSync, mkdirSync, openSync, readFileSync, statSync, unlinkSync, writeSync } from "node:fs";
3
3
  import { dirname } from "node:path";
4
- import { setTimeout as sleep } from "node:timers/promises";
5
-
6
4
  import { UlwLoopError } from "./types.js";
7
5
 
8
- // Cross-process exclusive lock for one ulw-loop state directory. Every CLI
9
- // invocation is its own process, so the in-process promise chain in plan-io
10
- // serializes nothing across them; this file-level lock is what makes the
11
- // read-modify-write of goals.json (and the counters next to it) atomic.
12
- //
13
- // Protocol: the lock is a file created with O_EXCL whose body records the
14
- // owner (pid + a per-acquisition token). A waiter reclaims it only when the
15
- // owner is provably gone (pid dead) or the body never became a record within
16
- // `staleMs` (a creator that died mid-write); a live owner is never reclaimed by
17
- // age alone, because overlapping two bodies is exactly the lost update this lock
18
- // exists to prevent. Waiters back off and fail closed at `timeoutMs`. Release
19
- // unlinks only a lock that still carries the releaser's own token.
20
-
6
+ // O_EXCL establishes ownership. Async holders keep the fd open and refresh a
7
+ // lease through that fd, never through the pathname: a reclaimed inode is invisible.
8
+ // Sync/legacy holders are lease-less and only dead pids retire their records.
9
+ // Expiry permits overlapping bodies; immutable revision publication fences them.
10
+ // Reclaim's reread/unlink is not atomic. On Windows an open inode may not unlink:
11
+ // treat that refusal as a live owner and fail closed. Release checks its token.
21
12
  export const ULW_LOOP_LOCK_TIMEOUT_CODE = "ULW_LOOP_LOCK_TIMEOUT";
22
-
13
+ export interface StateLockClock {
14
+ readonly now: () => number;
15
+ readonly schedule: (fn: () => void, ms: number) => { unref(): void; cancel(): void };
16
+ }
23
17
  export interface StateLockOptions {
24
18
  readonly timeoutMs?: number;
25
19
  readonly staleMs?: number;
20
+ readonly leaseMs?: number;
21
+ readonly heartbeatMs?: number;
22
+ readonly clock?: StateLockClock;
26
23
  }
27
-
28
24
  interface LockRecord {
29
25
  readonly pid: number;
30
26
  readonly createdAt: string;
31
27
  readonly token: string;
28
+ leaseUntil?: number;
29
+ }
30
+ interface Holder {
31
+ readonly fd: number;
32
+ readonly record: LockRecord;
32
33
  }
33
-
34
34
  interface LockSnapshot {
35
35
  readonly raw: string;
36
36
  readonly record: LockRecord | null;
37
37
  readonly ageMs: number;
38
38
  }
39
-
40
- type AttemptOutcome = { readonly kind: "acquired"; readonly token: string } | { readonly kind: "retry" | "wait" };
41
-
39
+ type AttemptOutcome = { readonly kind: "acquired"; readonly holder: Holder } | { readonly kind: "retry" | "wait" };
42
40
  const DEFAULT_TIMEOUT_MS = 10_000;
43
41
  const DEFAULT_STALE_MS = 60_000;
44
- const MIN_DELAY_MS = 5;
45
- const MAX_DELAY_MS = 100;
42
+ const DEFAULT_LEASE_MS = 30_000;
46
43
  const SLEEP_CELL = new Int32Array(new SharedArrayBuffer(4));
44
+ const systemClock: StateLockClock = {
45
+ now: Date.now,
46
+ schedule(fn, ms) {
47
+ const timer = setTimeout(fn, ms);
48
+ return {
49
+ unref: () => {
50
+ timer.unref();
51
+ },
52
+ cancel: () => clearTimeout(timer),
53
+ };
54
+ },
55
+ };
47
56
 
48
57
  export async function withStateLock<T>(
49
58
  lockPath: string,
50
- fn: () => Promise<T>,
59
+ fn: (token: string) => Promise<T>,
51
60
  options: StateLockOptions = {},
52
61
  ): Promise<T> {
53
- const token = await acquireAsync(lockPath, options);
62
+ const holder = await acquireAsync(lockPath, options);
63
+ const clock = options.clock ?? systemClock;
64
+ let timer: ReturnType<StateLockClock["schedule"]> | undefined;
65
+ let heartbeatError: unknown;
66
+ const beat = () => {
67
+ try {
68
+ holder.record.leaseUntil = clock.now() + (options.leaseMs ?? DEFAULT_LEASE_MS);
69
+ writeRecord(holder);
70
+ schedule();
71
+ } catch (error) {
72
+ heartbeatError = error;
73
+ }
74
+ };
75
+ const schedule = () => {
76
+ if (options.heartbeatMs === 0) return;
77
+ timer = clock.schedule(beat, options.heartbeatMs ?? (options.leaseMs ?? DEFAULT_LEASE_MS) / 3);
78
+ timer.unref();
79
+ };
80
+ schedule();
54
81
  try {
55
- return await fn();
82
+ const result = await fn(holder.record.token);
83
+ if (heartbeatError !== undefined) throw heartbeatError;
84
+ return result;
56
85
  } finally {
57
- release(lockPath, token);
86
+ timer?.cancel();
87
+ closeSync(holder.fd);
88
+ release(lockPath, holder.record.token);
58
89
  }
59
90
  }
60
-
61
91
  export function withStateLockSync<T>(lockPath: string, fn: () => T, options: StateLockOptions = {}): T {
62
- const token = acquireSync(lockPath, options);
92
+ const holder = acquireSync(lockPath, options);
63
93
  try {
64
94
  return fn();
65
95
  } finally {
66
- release(lockPath, token);
96
+ closeSync(holder.fd);
97
+ release(lockPath, holder.record.token);
67
98
  }
68
99
  }
69
-
70
100
  export function isStateLockTimeout(error: unknown): error is UlwLoopError {
71
101
  return error instanceof UlwLoopError && error.code === ULW_LOOP_LOCK_TIMEOUT_CODE;
72
102
  }
73
-
74
- async function acquireAsync(lockPath: string, options: StateLockOptions): Promise<string> {
75
- const deadline = Date.now() + (options.timeoutMs ?? DEFAULT_TIMEOUT_MS);
76
- const staleMs = options.staleMs ?? DEFAULT_STALE_MS;
103
+ async function acquireAsync(lockPath: string, options: StateLockOptions): Promise<Holder> {
104
+ const clock = options.clock ?? systemClock;
105
+ const deadline = clock.now() + (options.timeoutMs ?? DEFAULT_TIMEOUT_MS);
77
106
  mkdirSync(dirname(lockPath), { recursive: true });
78
107
  for (let attempt = 0; ; ) {
79
- const outcome = attemptOnce(lockPath, staleMs);
80
- if (outcome.kind === "acquired") return outcome.token;
108
+ const outcome = attemptOnce(lockPath, options, clock.now(), options.leaseMs ?? DEFAULT_LEASE_MS);
109
+ if (outcome.kind === "acquired") return outcome.holder;
81
110
  if (outcome.kind === "retry") continue;
82
- if (Date.now() >= deadline) throw lockTimeout(lockPath, options.timeoutMs ?? DEFAULT_TIMEOUT_MS);
83
- await sleep(backoffMs(attempt));
111
+ if (clock.now() >= deadline) throw lockTimeout(lockPath, options);
112
+ await new Promise<void>((resolve) => clock.schedule(resolve, backoffMs(attempt)));
84
113
  attempt += 1;
85
114
  }
86
115
  }
87
-
88
- function acquireSync(lockPath: string, options: StateLockOptions): string {
116
+ function acquireSync(lockPath: string, options: StateLockOptions): Holder {
89
117
  const deadline = Date.now() + (options.timeoutMs ?? DEFAULT_TIMEOUT_MS);
90
- const staleMs = options.staleMs ?? DEFAULT_STALE_MS;
91
118
  mkdirSync(dirname(lockPath), { recursive: true });
92
119
  for (let attempt = 0; ; ) {
93
- const outcome = attemptOnce(lockPath, staleMs);
94
- if (outcome.kind === "acquired") return outcome.token;
120
+ const outcome = attemptOnce(lockPath, options, Date.now());
121
+ if (outcome.kind === "acquired") return outcome.holder;
95
122
  if (outcome.kind === "retry") continue;
96
- if (Date.now() >= deadline) throw lockTimeout(lockPath, options.timeoutMs ?? DEFAULT_TIMEOUT_MS);
123
+ if (Date.now() >= deadline) throw lockTimeout(lockPath, options);
97
124
  Atomics.wait(SLEEP_CELL, 0, 0, backoffMs(attempt));
98
125
  attempt += 1;
99
126
  }
100
127
  }
101
-
102
- // EINTR surfaces raw from macOS fs syscalls under some runtimes; it is a retry, never a verdict.
103
- function attemptOnce(lockPath: string, staleMs: number): AttemptOutcome {
128
+ function attemptOnce(lockPath: string, options: StateLockOptions, now: number, leaseMs?: number): AttemptOutcome {
104
129
  try {
105
- const token = tryCreate(lockPath);
106
- if (token !== null) return { kind: "acquired", token };
107
- const snapshot = readSnapshot(lockPath);
130
+ const holder = tryCreate(lockPath, now, leaseMs);
131
+ if (holder !== null) return { kind: "acquired", holder };
132
+ const snapshot = readSnapshot(lockPath, now);
108
133
  if (snapshot === null) return { kind: "retry" };
109
- if (isStale(snapshot, staleMs) && reclaim(lockPath, snapshot.raw)) return { kind: "retry" };
134
+ const record = snapshot.record;
135
+ const stale =
136
+ record === null
137
+ ? snapshot.ageMs > (options.staleMs ?? DEFAULT_STALE_MS)
138
+ : !isProcessAlive(record.pid) || (record.leaseUntil !== undefined && now > record.leaseUntil);
139
+ if (stale && reclaim(lockPath, snapshot.raw)) return { kind: "retry" };
110
140
  return { kind: "wait" };
111
141
  } catch (error) {
112
142
  if (hasCode(error, "EINTR")) return { kind: "wait" };
113
143
  throw error;
114
144
  }
115
145
  }
116
-
117
- function tryCreate(lockPath: string): string | null {
146
+ function writeRecord(holder: Holder): void {
147
+ const buf = Buffer.from(JSON.stringify(holder.record));
148
+ ftruncateSync(holder.fd, 0);
149
+ let offset = 0;
150
+ while (offset < buf.length) {
151
+ const written = writeSync(holder.fd, buf, offset, buf.length - offset, offset);
152
+ if (written === 0) throw new Error("State lock write made no progress.");
153
+ offset += written;
154
+ }
155
+ }
156
+ function tryCreate(lockPath: string, now: number, leaseMs?: number): Holder | null {
118
157
  let fd: number;
119
158
  try {
120
159
  fd = openSync(lockPath, "wx");
@@ -122,60 +161,54 @@ function tryCreate(lockPath: string): string | null {
122
161
  if (hasCode(error, "EEXIST")) return null;
123
162
  throw error;
124
163
  }
125
- const record: LockRecord = { pid: process.pid, createdAt: new Date().toISOString(), token: randomUUID() };
164
+ const record: LockRecord = {
165
+ pid: process.pid,
166
+ createdAt: new Date(now).toISOString(),
167
+ token: randomUUID(),
168
+ ...(leaseMs === undefined ? {} : { leaseUntil: now + leaseMs }),
169
+ };
170
+ const holder = { fd, record };
126
171
  try {
127
- writeSync(fd, JSON.stringify(record));
172
+ writeRecord(holder);
128
173
  } catch (error) {
129
174
  closeSync(fd);
130
- // A failed write (e.g. EINTR) must not leave a partial lock the owner never recorded;
131
- // otherwise later readers see a young ownerless lock and only age can retire it.
132
175
  try {
133
176
  unlinkSync(lockPath);
134
- } catch {
135
- // another process already reclaimed the partial file; leave it alone
177
+ } catch (cleanup) {
178
+ if (!hasCode(cleanup, "ENOENT")) throw cleanup;
136
179
  }
137
180
  throw error;
138
181
  }
139
- closeSync(fd);
140
- return record.token;
182
+ return holder;
141
183
  }
142
-
143
- function readSnapshot(lockPath: string): LockSnapshot | null {
184
+ function readSnapshot(lockPath: string, now: number): LockSnapshot | null {
144
185
  try {
145
186
  const raw = readFileSync(lockPath, "utf8");
146
- const ageMs = Date.now() - statSync(lockPath).mtimeMs;
147
- return { raw, record: parseRecord(raw), ageMs };
187
+ return { raw, record: parseRecord(raw), ageMs: now - statSync(lockPath).mtimeMs };
148
188
  } catch (error) {
149
189
  if (hasCode(error, "ENOENT")) return null;
150
190
  throw error;
151
191
  }
152
192
  }
153
-
154
193
  function parseRecord(raw: string): LockRecord | null {
155
194
  try {
156
- const parsed: unknown = JSON.parse(raw);
157
- if (typeof parsed !== "object" || parsed === null) return null;
158
- const record = parsed as Record<string, unknown>;
159
- const pid = record["pid"];
160
- const createdAt = record["createdAt"];
161
- const token = record["token"];
162
- if (typeof pid !== "number" || !Number.isInteger(pid) || pid <= 0 || typeof createdAt !== "string") return null;
163
- if (typeof token !== "string" || token.length === 0) return null;
164
- return { pid, createdAt, token };
195
+ const record: unknown = JSON.parse(raw);
196
+ if (typeof record !== "object" || record === null) return null;
197
+ if (!("pid" in record) || typeof record.pid !== "number" || !Number.isInteger(record.pid) || record.pid <= 0)
198
+ return null;
199
+ if (!("createdAt" in record) || typeof record.createdAt !== "string") return null;
200
+ if (!("token" in record) || typeof record.token !== "string" || record.token.length === 0) return null;
201
+ return {
202
+ pid: record.pid,
203
+ createdAt: record.createdAt,
204
+ token: record.token,
205
+ ...("leaseUntil" in record && typeof record.leaseUntil === "number" ? { leaseUntil: record.leaseUntil } : {}),
206
+ };
165
207
  } catch (error) {
166
208
  if (error instanceof SyntaxError) return null;
167
209
  throw error;
168
210
  }
169
211
  }
170
-
171
- // A body that is not a record is a lock mid-write (or a foreign/legacy file); only
172
- // age retires it. A parsable record is stale only when its owner is dead: a live
173
- // owner running past staleMs is slow, not gone, and the waiter fails closed instead.
174
- function isStale(snapshot: LockSnapshot, staleMs: number): boolean {
175
- if (snapshot.record === null) return snapshot.ageMs > staleMs;
176
- return !isProcessAlive(snapshot.record.pid);
177
- }
178
-
179
212
  function isProcessAlive(pid: number): boolean {
180
213
  try {
181
214
  process.kill(pid, 0);
@@ -186,54 +219,39 @@ function isProcessAlive(pid: number): boolean {
186
219
  throw error;
187
220
  }
188
221
  }
189
-
190
- // Re-read right before unlinking so a lock that changed hands since the stale
191
- // verdict (a sibling waiter reclaimed and re-acquired it) is left alone.
192
222
  function reclaim(lockPath: string, expectedRaw: string): boolean {
193
- const current = readSnapshot(lockPath);
223
+ const current = readSnapshot(lockPath, Date.now());
194
224
  if (current === null) return true;
195
225
  if (current.raw !== expectedRaw) return false;
196
226
  try {
197
227
  unlinkSync(lockPath);
198
228
  } catch (error) {
229
+ if (hasCode(error, "EPERM") || hasCode(error, "EACCES")) return false;
199
230
  if (!hasCode(error, "ENOENT")) throw error;
200
231
  }
201
232
  return true;
202
233
  }
203
-
204
- // Only the acquisition that wrote this token may unlink: if the file now carries
205
- // another token, a waiter has legitimately taken over and its lock must stand.
206
234
  function release(lockPath: string, token: string): void {
207
- const current = readSnapshot(lockPath);
208
- if (current === null || current.record?.token !== token) return;
235
+ if (readSnapshot(lockPath, Date.now())?.record?.token !== token) return;
209
236
  try {
210
237
  unlinkSync(lockPath);
211
238
  } catch (error) {
212
239
  if (!hasCode(error, "ENOENT")) throw error;
213
240
  }
214
241
  }
215
-
216
242
  function backoffMs(attempt: number): number {
217
- const exponential = Math.min(MAX_DELAY_MS, MIN_DELAY_MS * 2 ** attempt);
218
- return exponential + Math.random() * MIN_DELAY_MS;
243
+ return Math.min(100, 5 * 2 ** attempt) + Math.random() * 5;
219
244
  }
220
-
221
- function lockTimeout(lockPath: string, timeoutMs: number): UlwLoopError {
222
- const holder = readSnapshot(lockPath)?.record;
223
- const owner = holder === undefined || holder === null ? "another process" : `pid ${holder.pid}`;
245
+ function lockTimeout(lockPath: string, options: StateLockOptions): UlwLoopError {
246
+ const holder = readSnapshot(lockPath, Date.now())?.record;
247
+ const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
248
+ const owner = holder == null ? "another process" : `pid ${holder.pid}`;
224
249
  return new UlwLoopError(
225
- `ulw-loop state lock ${lockPath} is held by ${owner} for more than ${timeoutMs}ms; retry once that process finishes, or delete the lock file if that process is gone.`,
250
+ `ulw-loop state lock ${lockPath} is held by ${owner} for more than ${timeoutMs}ms. The lock owner is still alive. If a JS eval kernel was interrupted while writing, wait for its lease to expire (${options.leaseMs ?? DEFAULT_LEASE_MS} ms) or restart the owning senpi process; never delete a lock owned by a live process.`,
226
251
  ULW_LOOP_LOCK_TIMEOUT_CODE,
227
- {
228
- details: {
229
- lockPath,
230
- timeoutMs,
231
- ...(holder === undefined || holder === null ? {} : { holderPid: holder.pid }),
232
- },
233
- },
252
+ { details: { lockPath, timeoutMs, ...(holder == null ? {} : { holderPid: holder.pid }) } },
234
253
  );
235
254
  }
236
-
237
255
  function hasCode(error: unknown, code: string): boolean {
238
256
  return error instanceof Error && "code" in error && error.code === code;
239
257
  }
@@ -1,36 +1,45 @@
1
1
  import { hasAllCriteriaPass, isFinalRunCompletionCandidate } from "./goal-status.js";
2
- import { ULW_LOOP_CREATE_GOALS_COMMAND } from "./plan-missing-recovery.js";
2
+ import { createGoalsAction } from "./plan-missing-recovery.js";
3
+ import type { UlwLoopToolkitSurface } from "./surface.js";
3
4
  import type { UlwLoopItem, UlwLoopPlan } from "./types.js";
4
5
 
5
6
  /**
6
- * `status --json` is the one call every agent already makes between steps, so it is
7
+ * Status is the one call every agent already makes between steps, so it is
7
8
  * also the cheapest place to answer "what now?" without a second round trip.
8
9
  */
9
- export function statusNextActions(plan: UlwLoopPlan): readonly string[] {
10
+ export function statusNextActions(plan: UlwLoopPlan, surface: UlwLoopToolkitSurface = "lazycodex"): readonly string[] {
10
11
  const actions: string[] = [];
11
12
  const active = plan.goals.find((goal) => goal.id === plan.activeGoalId);
12
- if (plan.goals.length === 0) actions.push(`No goals yet: bootstrap with \`${ULW_LOOP_CREATE_GOALS_COMMAND}\`.`);
13
- else if (active !== undefined) actions.push(...activeGoalActions(plan, active));
13
+ if (plan.goals.length === 0) actions.push(`No goals yet: bootstrap with \`${createGoalsAction(surface)}\`.`);
14
+ else if (active !== undefined) actions.push(...activeGoalActions(plan, active, surface));
14
15
  for (const goal of plan.goals.filter((candidate) => candidate.status === "review_blocked"))
15
16
  actions.push(
16
- `${goal.id} is review_blocked: capture the reviewer verdict with \`omo-agent-toolkit ulw-loop record-review-blockers --goal-id ${goal.id} --title "<title>" --objective "<objective>" --evidence "<verdict>" --codex-goal-json '<get_goal json>'\`.`,
17
+ surface === "omo-senpi"
18
+ ? `${goal.id} is review_blocked: capture the reviewer verdict with \`agentToolkit.recordReviewBlockers({ goalId: "${goal.id}", title: "<title>", objective: "<objective>", evidence: "<verdict>" })\`.`
19
+ : `${goal.id} is review_blocked: capture the reviewer verdict with \`omo-agent-toolkit ulw-loop record-review-blockers --goal-id ${goal.id} --title "<title>" --objective "<objective>" --evidence "<verdict>" --codex-goal-json '<get_goal json>'\`.`,
17
20
  );
18
21
  if (plan.evidenceLayoutVersion !== 2 || active === undefined)
19
22
  actions.push("plan is evidence-layout v1; artifacts go under .omo/evidence/");
20
23
  return actions;
21
24
  }
22
25
 
23
- function activeGoalActions(plan: UlwLoopPlan, active: UlwLoopItem): readonly string[] {
26
+ function activeGoalActions(plan: UlwLoopPlan, active: UlwLoopItem, surface: UlwLoopToolkitSurface): readonly string[] {
24
27
  const unresolved = active.successCriteria.filter((criterion) => criterion.status !== "pass");
25
28
  if (unresolved.length > 0)
26
29
  return [
27
- `${active.id} has ${unresolved.length} unresolved criterion(s) (${unresolved.map((criterion) => criterion.id).join(", ")}): record proof with \`omo-agent-toolkit ulw-loop record-evidence --goal-id ${active.id} --criterion-id <id> --status pass --evidence "<observable proof>"\`.`,
30
+ surface === "omo-senpi"
31
+ ? `${active.id} has ${unresolved.length} unresolved criterion(s) (${unresolved.map((criterion) => criterion.id).join(", ")}): record proof with \`agentToolkit.recordEvidence({ goalId: "${active.id}", criterionId: "<id>", status: "pass", evidence: "<observable proof>" })\`.`
32
+ : `${active.id} has ${unresolved.length} unresolved criterion(s) (${unresolved.map((criterion) => criterion.id).join(", ")}): record proof with \`omo-agent-toolkit ulw-loop record-evidence --goal-id ${active.id} --criterion-id <id> --status pass --evidence "<observable proof>"\`.`,
28
33
  ];
29
34
  if (hasAllCriteriaPass(active) && isFinalRunCompletionCandidate(plan, active))
30
35
  return [
31
- `${active.id} passes every criterion and is the final story: update_goal complete, then checkpoint --print-template to build the final quality gate. Use status --json's currentAttemptDir for all quality-gate artifacts.`,
36
+ surface === "omo-senpi"
37
+ ? `${active.id} passes every criterion and is the final story: complete the driver goal, then call \`agentToolkit.checkpoint({ goalId: "${active.id}", printTemplate: true })\` to build the final quality gate. Use status().result.currentAttemptDir for all quality-gate artifacts.`
38
+ : `${active.id} passes every criterion and is the final story: update_goal complete, then checkpoint --print-template to build the final quality gate. Use status --json's currentAttemptDir for all quality-gate artifacts.`,
32
39
  ];
33
40
  return [
34
- `${active.id} passes every criterion: close it with \`omo-agent-toolkit ulw-loop checkpoint --goal-id ${active.id} --status complete --evidence "<proof>" --codex-goal-json '<get_goal json>'\`. Put quality-gate artifacts under the currentAttemptDir shown by status --json.`,
41
+ surface === "omo-senpi"
42
+ ? `${active.id} passes every criterion: close it with \`agentToolkit.checkpoint({ goalId: "${active.id}", status: "complete", evidence: "<proof>" })\`. Put quality-gate artifacts under status().result.currentAttemptDir.`
43
+ : `${active.id} passes every criterion: close it with \`omo-agent-toolkit ulw-loop checkpoint --goal-id ${active.id} --status complete --evidence "<proof>" --codex-goal-json '<get_goal json>'\`. Put quality-gate artifacts under the currentAttemptDir shown by status --json.`,
35
44
  ];
36
45
  }
@@ -1,6 +1,7 @@
1
1
  // biome-ignore-all format: compact batch steering module stays below the pure LOC budget.
2
2
  import type { UlwLoopScope } from "./paths.js";
3
- import { appendLedger, appendLedgerEntries, findAcceptedSteeringLedgerEntry, readUlwLoopPlan, withUlwLoopMutationLock, writePlan } from "./plan-io.js";
3
+ import { commit } from "./plan-commit.js";
4
+ import { findAcceptedSteeringLedgerEntry, readUlwLoopPlan, withUlwLoopMutationLock } from "./plan-io.js";
4
5
  import { applySteeringMutation, validateUlwLoopSteeringProposal } from "./steering.js";
5
6
  import { buildSteeringPlanSnapshot, changedGoalIdsBetween } from "./steering-snapshot.js";
6
7
  import type { UlwLoopLedgerEntry, UlwLoopPlan, UlwLoopSteeringAudit, UlwLoopSteeringProposal } from "./types.js";
@@ -43,17 +44,16 @@ export async function steerUlwLoopBatch(
43
44
  const failed = prepared.results.find((item) => !item.accepted);
44
45
  if (failed !== undefined) {
45
46
  const entry = rejectedLedgerEntry(prepared.results);
46
- await appendLedger(repoRoot, entry, scope);
47
+ await commit(repoRoot, scope, { plan, entries: [entry] });
47
48
  return rejected(plan, prepared.results, failed.rejectedReasons);
48
49
  }
49
50
  let next = plan;
50
51
  for (const item of prepared.items) if (item.kind === "fresh") next = item.prepared.next;
51
52
  const fresh = prepared.items.filter((item): item is Extract<PreparedItem, { readonly kind: "fresh" }> => item.kind === "fresh");
52
53
  if (fresh.length > 0) {
53
- await writePlan(repoRoot, next, scope);
54
54
  const entries = fresh.map((item) => ledgerEntry(item.prepared.proposal, item.prepared.audit, item.prepared.proposal.now?.toISOString() ?? iso()));
55
55
  const batchEntry = batchUpdateLedgerEntry(plan, next, iso());
56
- await appendLedgerEntries(repoRoot, batchEntry === null ? entries : [...entries, batchEntry], scope);
56
+ await commit(repoRoot, scope, { plan: next, entries: batchEntry === null ? entries : [...entries, batchEntry] });
57
57
  }
58
58
  return { plan: next, accepted: true, results: prepared.results, rejectedReasons: [] };
59
59
  });
@@ -1,5 +1,6 @@
1
1
  // biome-ignore-all format: compact extracted steering mutations stay below the pure LOC budget.
2
2
  import { seedDefaultSuccessCriteria } from "./plan-crud.js";
3
+ import type { UlwLoopToolkitSurface } from "./surface.js";
3
4
  import type { UlwLoopItem, UlwLoopPlan, UlwLoopSteeringChildGoal, UlwLoopSteeringProposal, UlwLoopSuccessCriterionUserModel } from "./types.js";
4
5
  import { ULW_LOOP_SUCCESS_CRITERION_USER_MODELS } from "./types.js";
5
6
  import { updateBatchesAfterSupersede } from "./validation-batch.js";
@@ -30,9 +31,9 @@ function nextId(plan: UlwLoopPlan, offset: number): string {
30
31
  return `G${String(max + offset).padStart(3, "0")}`;
31
32
  }
32
33
 
33
- export function makeGoal(plan: UlwLoopPlan, childGoal: UlwLoopSteeringChildGoal, evidence: string, now: string, offset: number): UlwLoopItem {
34
+ export function makeGoal(plan: UlwLoopPlan, childGoal: UlwLoopSteeringChildGoal, evidence: string, now: string, offset: number, surface: UlwLoopToolkitSurface = "lazycodex"): UlwLoopItem {
34
35
  const id = nextId(plan, offset); const digits = /^G(\d+)/u.exec(id)?.[1]; const goalIndex = digits === undefined ? plan.goals.length + offset - 1 : Number(digits) - 1;
35
- return { id, title: childGoal.title, objective: childGoal.objective, status: "pending", successCriteria: seedDefaultSuccessCriteria(goalIndex, childGoal.objective), attempt: 0, createdAt: now, updatedAt: now, evidence };
36
+ return { id, title: childGoal.title, objective: childGoal.objective, status: "pending", successCriteria: seedDefaultSuccessCriteria(goalIndex, childGoal.objective, { goalId: id, surface }), attempt: 0, createdAt: now, updatedAt: now, evidence };
36
37
  }
37
38
 
38
39
  export function reviseWording(plan: UlwLoopPlan, proposal: UlwLoopSteeringProposal, now: string): void {
@@ -41,9 +42,9 @@ export function reviseWording(plan: UlwLoopPlan, proposal: UlwLoopSteeringPropos
41
42
  target.steeringEvidence = proposal.evidence; target.steeringRationale = proposal.rationale; target.updatedAt = now;
42
43
  }
43
44
 
44
- export function splitOrBlock(plan: UlwLoopPlan, proposal: UlwLoopSteeringProposal, now: string): void {
45
+ export function splitOrBlock(plan: UlwLoopPlan, proposal: UlwLoopSteeringProposal, now: string, surface: UlwLoopToolkitSurface = "lazycodex"): void {
45
46
  const target = goal(plan, targets(proposal)[0]); if (target === undefined) return;
46
- const replacements = children(proposal).map((item, index) => makeGoal(plan, item, proposal.evidence, now, index + 1));
47
+ const replacements = children(proposal).map((item, index) => makeGoal(plan, item, proposal.evidence, now, index + 1, surface));
47
48
  target.steeringEvidence = proposal.evidence; target.steeringRationale = proposal.rationale; target.updatedAt = now;
48
49
  if (replacements.length === 0) { target.status = "blocked"; target.steeringStatus = "blocked"; target.blockedReason = proposal.blockedReason ?? proposal.rationale; }
49
50
  else { target.steeringStatus = "superseded"; target.supersededBy = replacements.map((item) => item.id); for (const item of replacements) item.supersedes = [target.id]; plan.goals.splice(plan.goals.indexOf(target) + 1, 0, ...replacements); updateBatchesAfterSupersede(plan, target.id, replacements.map((item) => item.id)); }
@@ -1,4 +1,8 @@
1
- import type { UlwLoopSteeringMutationKind, UlwLoopSteeringSource } from "./constants.js";
1
+ import type {
2
+ UlwLoopSteeringMutationKind,
3
+ UlwLoopSteeringSource,
4
+ UlwLoopSuccessCriterionUserModel,
5
+ } from "./constants.js";
2
6
  import type { UlwLoopItem, UlwLoopPlan } from "./domain-types.js";
3
7
 
4
8
  export interface UlwLoopSteeringInvariantResult {
@@ -25,9 +29,17 @@ export interface UlwLoopSteeringAfterPayload {
25
29
  export interface UlwLoopSteeringProposal {
26
30
  kind: UlwLoopSteeringMutationKind;
27
31
  source: UlwLoopSteeringSource;
32
+ /** Alias of `targetGoalId`; the SDK documentation uses `goalId` like every other operation. */
33
+ goalId?: string;
28
34
  targetGoalId?: string;
29
35
  targetGoalIds?: string[];
30
36
  criterionId?: string;
37
+ /** `revise_criterion` payload: the replacement scenario text. */
38
+ scenario?: string;
39
+ /** `revise_criterion` payload: the replacement observable-proof text. */
40
+ expectedEvidence?: string;
41
+ /** `revise_criterion` payload: the replacement user model. */
42
+ userModel?: UlwLoopSuccessCriterionUserModel;
31
43
  evidence: string;
32
44
  rationale: string;
33
45
  title?: string;
@@ -1,9 +1,11 @@
1
1
  // biome-ignore-all format: compact steering module must stay below the 240 pure-LOC budget
2
2
  import { isUlwLoopDone } from "./goal-status.js";
3
3
  import type { UlwLoopScope } from "./paths.js";
4
- import { appendLedger, findAcceptedSteeringLedgerEntry, readUlwLoopPlan, withUlwLoopMutationLock, writePlan } from "./plan-io.js";
4
+ import { commit } from "./plan-commit.js";
5
+ import { findAcceptedSteeringLedgerEntry, readUlwLoopPlan, withUlwLoopMutationLock } from "./plan-io.js";
5
6
  import { makeGoal, reviseCriterion, reviseWording, splitOrBlock } from "./steering-mutations.js";
6
7
  import { buildSteeringPlanSnapshot, changedGoalIdsBetween } from "./steering-snapshot.js";
8
+ import type { UlwLoopToolkitSurface } from "./surface.js";
7
9
  import type {
8
10
  SteerUlwLoopResult,
9
11
  UlwLoopItem,
@@ -20,7 +22,7 @@ import { iso, ULW_LOOP_STEERING_MUTATION_KINDS, ULW_LOOP_SUCCESS_CRITERION_USER_
20
22
  import { batchUpdateLedgerEntry } from "./validation-batch.js";
21
23
 
22
24
  const SOURCES = ["user_prompt_submit", "finding", "cli"] as const satisfies readonly UlwLoopSteeringSource[];
23
- const PROTECTED = new Set(["aggregateCompletion", "codexObjective", "codexObjectiveAliases", "originalConstraints", "qualityGate", "status", "completedAt", "completionStatus"]);
25
+ const PROTECTED = new Set(["aggregateCompletion", "codexObjective", "codexObjectiveAliases", "acknowledgedDriverObjectives", "originalConstraints", "qualityGate", "status", "completedAt", "completionStatus"]);
24
26
  const isObject = (value: unknown): value is object => typeof value === "object" && value !== null; const isPlain = (value: unknown): value is object => isObject(value) && !Array.isArray(value);
25
27
  const read = (value: object, key: string): unknown => Object.entries(value).find(([name]) => name === key)?.[1];
26
28
  const isText = (value: unknown): value is string => typeof value === "string" && value.trim().length > 0;
@@ -155,17 +157,17 @@ function validateCriterion(plan: UlwLoopPlan, proposal: object, reasons: string[
155
157
  if (model !== undefined && !isModel(model)) reasons.push("invalid userModel");
156
158
  }
157
159
 
158
- export function applySteeringMutation(plan: UlwLoopPlan, proposal: UlwLoopSteeringProposal, audit: UlwLoopSteeringAudit): UlwLoopPlan {
160
+ export function applySteeringMutation(plan: UlwLoopPlan, proposal: UlwLoopSteeringProposal, audit: UlwLoopSteeringAudit, surface: UlwLoopToolkitSurface = "lazycodex"): UlwLoopPlan {
159
161
  const next = structuredClone(plan);
160
162
  if (!audit.invariant.accepted) return next;
161
163
  const now = proposal.now?.toISOString() ?? iso();
162
- if (proposal.kind === "add_subgoal") next.goals.push(makeGoal(next, { title: proposal.title ?? "", objective: proposal.objective ?? "" }, proposal.evidence, now, 1));
164
+ if (proposal.kind === "add_subgoal") next.goals.push(makeGoal(next, { title: proposal.title ?? "", objective: proposal.objective ?? "" }, proposal.evidence, now, 1, surface));
163
165
  if (proposal.kind === "reorder_pending") {
164
166
  const order = pendingOrder(proposal);
165
167
  next.goals = [...order.map((id) => goal(next, id)).filter((item): item is UlwLoopItem => item !== undefined), ...next.goals.filter((item) => !order.includes(item.id))];
166
168
  }
167
169
  if (proposal.kind === "revise_pending_wording") reviseWording(next, proposal, now);
168
- if (proposal.kind === "split_subgoal" || proposal.kind === "mark_blocked_superseded") splitOrBlock(next, proposal, now);
170
+ if (proposal.kind === "split_subgoal" || proposal.kind === "mark_blocked_superseded") splitOrBlock(next, proposal, now, surface);
169
171
  if (proposal.kind === "revise_criterion") reviseCriterion(next, proposal, now);
170
172
  if (proposal.kind !== "annotate_ledger") next.updatedAt = now;
171
173
  return next;
@@ -187,7 +189,7 @@ export function parseUlwLoopSteeringDirective(text: string): UlwLoopSteeringProp
187
189
  }
188
190
  }
189
191
 
190
- export async function steerUlwLoop(repoRoot: string, proposal: UlwLoopSteeringProposal, scope?: UlwLoopScope): Promise<SteerUlwLoopResult> {
192
+ export async function steerUlwLoop(repoRoot: string, proposal: UlwLoopSteeringProposal, scope?: UlwLoopScope, surface: UlwLoopToolkitSurface = "lazycodex"): Promise<SteerUlwLoopResult> {
191
193
  return withUlwLoopMutationLock(repoRoot, scope, async () => {
192
194
  const plan = await readUlwLoopPlan(repoRoot, scope);
193
195
  const key = proposal.idempotencyKey ?? proposal.promptSignature;
@@ -200,7 +202,7 @@ export async function steerUlwLoop(repoRoot: string, proposal: UlwLoopSteeringPr
200
202
  }
201
203
  const audit = validateUlwLoopSteeringProposal(plan, proposal);
202
204
  const accepted = audit.invariant.accepted;
203
- const next = accepted ? applySteeringMutation(plan, proposal, audit) : plan;
205
+ const next = accepted ? applySteeringMutation(plan, proposal, audit, surface) : plan;
204
206
  const finalAudit: UlwLoopSteeringAudit = { ...audit };
205
207
  if (accepted) {
206
208
  const changed = changedGoalIdsBetween(plan, next);
@@ -209,9 +211,9 @@ export async function steerUlwLoop(repoRoot: string, proposal: UlwLoopSteeringPr
209
211
  }
210
212
  const at = proposal.now?.toISOString() ?? iso();
211
213
  const batchEntry = accepted ? batchUpdateLedgerEntry(plan, next, at) : null;
212
- if (accepted) await writePlan(repoRoot, next, scope);
213
- await appendLedger(repoRoot, ledgerEntry(proposal, finalAudit, at), scope);
214
- if (batchEntry !== null) await appendLedger(repoRoot, batchEntry, scope);
214
+ const entries = [ledgerEntry(proposal, finalAudit, at)];
215
+ if (batchEntry !== null) entries.push(batchEntry);
216
+ await commit(repoRoot, scope, { plan: next, entries });
215
217
  return { plan: next, accepted, audit: finalAudit, rejectedReasons: audit.invariant.rejectedReasons, deduped: false };
216
218
  });
217
219
  }