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,7 +1,9 @@
1
1
  import { existsSync, readFileSync, writeFileSync } from "node:fs";
2
2
  import { isAbsolute, join, resolve, sep } from "node:path";
3
3
 
4
+ import { readLedgerAt } from "./ledger.js";
4
5
  import { normalizeUlwLoopSessionId, ulwLoopDir, ulwLoopStateLockPath } from "./paths.js";
6
+ import { readUlwLoopPlanSync } from "./plan-io.js";
5
7
  import { isStateLockTimeout, withStateLockSync } from "./state-lock.js";
6
8
  import type { UlwLoopItem, UlwLoopPlan } from "./types.js";
7
9
 
@@ -37,7 +39,7 @@ export function runStopResumeHook(input: unknown): string {
37
39
  if (boulderContinuationWillFire(payload.cwd, payload.session_id)) return "";
38
40
  const scope = { sessionId: payload.session_id } as const;
39
41
  const stateDir = ulwLoopDir(payload.cwd, scope);
40
- const plan = readPlan(join(stateDir, "goals.json"));
42
+ const plan = readPlan(payload.cwd, payload.session_id);
41
43
  if (plan === null || plan.aggregateCompletion?.status === "complete") return "";
42
44
  const goal = resumableGoal(plan);
43
45
  if (goal === undefined) return "";
@@ -81,11 +83,10 @@ function consumeResumeBudgetLocked(lockPath: string, stateDir: string, goalId: s
81
83
  }
82
84
  }
83
85
 
84
- // Two-strike cap keyed on ledger movement: an unchanged ledger.jsonl line
85
- // count across resumes means the loop is not progressing. The stuck marker is
86
- // a separate file — a ledger append would change the count and self-reset.
86
+ // Hook budget counters are exempt from plan/audit commits. Count reconciled
87
+ // entries, not cache lines: a writer dying after link still made progress.
87
88
  function consumeResumeBudget(stateDir: string, goalId: string): boolean {
88
- const ledgerLineCount = countLedgerLines(join(stateDir, "ledger.jsonl"));
89
+ const ledgerLineCount = readLedgerAt(stateDir).length;
89
90
  const counterPath = resolve(stateDir, `auto-resume-${goalId}.json`);
90
91
  const stuckPath = resolve(stateDir, `auto-resume-${goalId}.stuck`);
91
92
  // goals.json is untrusted input: a crafted goal id (e.g. `../../x`) must
@@ -119,24 +120,15 @@ function renderResumeDirective(plan: UlwLoopPlan, goal: UlwLoopItem, sessionId:
119
120
  ].join("\n");
120
121
  }
121
122
 
122
- function readPlan(goalsPath: string): UlwLoopPlan | null {
123
+ function readPlan(repoRoot: string, sessionId: string): UlwLoopPlan | null {
123
124
  try {
124
- return JSON.parse(readFileSync(goalsPath, "utf8")) as UlwLoopPlan;
125
+ return readUlwLoopPlanSync(repoRoot, { sessionId });
125
126
  } catch (error) {
126
127
  if (error instanceof Error) return null;
127
128
  throw error;
128
129
  }
129
130
  }
130
131
 
131
- function countLedgerLines(ledgerPath: string): number {
132
- try {
133
- return readFileSync(ledgerPath, "utf8").split("\n").filter(Boolean).length;
134
- } catch (error) {
135
- if (error instanceof Error) return 0;
136
- throw error;
137
- }
138
- }
139
-
140
132
  function readCounter(counterPath: string): { count: number; ledgerLineCount: number } | null {
141
133
  try {
142
134
  if (!existsSync(counterPath)) return null;
@@ -0,0 +1,56 @@
1
+ import type { UlwLoopSuccessCriterion, UlwLoopSuccessCriterionUserModel } from "./types.js";
2
+ import { ULW_LOOP_SUCCESS_CRITERION_USER_MODELS, UlwLoopError } from "./types.js";
3
+
4
+ export interface SuccessCriterionInput {
5
+ readonly scenario: string;
6
+ readonly expectedEvidence: string;
7
+ readonly userModel?: UlwLoopSuccessCriterionUserModel;
8
+ readonly essential?: boolean;
9
+ }
10
+
11
+ function invalid(message: string, details: Record<string, unknown>): never {
12
+ throw new UlwLoopError(`Invalid successCriteria: ${message}`, "ULW_LOOP_ARGUMENT_INVALID", { details });
13
+ }
14
+
15
+ function isUserModel(value: unknown): value is UlwLoopSuccessCriterionUserModel {
16
+ return typeof value === "string" && ULW_LOOP_SUCCESS_CRITERION_USER_MODELS.some((model) => model === value);
17
+ }
18
+
19
+ function requireText(value: unknown, field: string, index: number): string {
20
+ const trimmed = typeof value === "string" ? value.trim() : "";
21
+ return trimmed || invalid(`entry ${index + 1} needs a non-empty ${field}.`, { index, field });
22
+ }
23
+
24
+ export function criterionId(index: number): string {
25
+ return `C${String(index + 1).padStart(3, "0")}`;
26
+ }
27
+
28
+ export function criteriaFromInput(input: readonly unknown[]): UlwLoopSuccessCriterion[] {
29
+ if (input.length === 0) invalid("provide at least one criterion or omit the field for placeholders.", { count: 0 });
30
+ return input.map((entry, index) => {
31
+ if (typeof entry !== "object" || entry === null || Array.isArray(entry))
32
+ return invalid(`entry ${index + 1} must be an object.`, { index });
33
+ const record: Record<string, unknown> = { ...entry };
34
+ const userModel = record["userModel"] ?? "happy";
35
+ if (!isUserModel(userModel))
36
+ return invalid(
37
+ `entry ${index + 1} userModel must be one of ${ULW_LOOP_SUCCESS_CRITERION_USER_MODELS.join(", ")}.`,
38
+ {
39
+ index,
40
+ userModel: String(userModel),
41
+ },
42
+ );
43
+ const essential = record["essential"];
44
+ if (essential !== undefined && typeof essential !== "boolean")
45
+ return invalid(`entry ${index + 1} essential must be a boolean.`, { index });
46
+ return {
47
+ id: criterionId(index),
48
+ scenario: requireText(record["scenario"], "scenario", index),
49
+ userModel,
50
+ expectedEvidence: requireText(record["expectedEvidence"], "expectedEvidence", index),
51
+ essential: essential ?? true,
52
+ capturedEvidence: null,
53
+ status: "pending",
54
+ };
55
+ });
56
+ }
@@ -152,6 +152,41 @@ describe("reconcileCodexGoalSnapshot", () => {
152
152
  expect(reconciliation.ok).toBe(true);
153
153
  expect(reconciliation.errors).toHaveLength(0);
154
154
  expect(reconciliation.warnings.join(" ")).toContain("driver_objective_differs");
155
+ expect(reconciliation.nextActions).toEqual([]);
156
+ expect(reconciliation.unacknowledgedObjective).toBe("X");
157
+ });
158
+
159
+ it("#given the differing objective was acknowledged #when reconciled again #then it is silent", () => {
160
+ const reconciliation = reconcileCodexGoalSnapshot(
161
+ { available: true, objective: " X \n", status: "active", raw: null },
162
+ { expectedObjective: "Y", acknowledgedObjectives: ["X"] },
163
+ );
164
+
165
+ expect(reconciliation.warnings).toEqual([]);
166
+ expect(reconciliation.nextActions).toEqual([]);
167
+ expect(reconciliation.unacknowledgedObjective).toBeUndefined();
168
+ });
169
+
170
+ it("#given no driver snapshot #when reconciled #then create_goal is a next action, not a warning", () => {
171
+ const reconciliation = reconcileCodexGoalSnapshot(null, { expectedObjective: "Y" });
172
+
173
+ expect(reconciliation.nextActions.join(" ")).toContain("create_goal");
174
+ expect(reconciliation.warnings).toEqual([]);
175
+ });
176
+
177
+ it("#given a completed or limited driver #when reconciled #then the advice is a next action", () => {
178
+ const completed = reconcileCodexGoalSnapshot(
179
+ { available: true, objective: "Y", status: "complete", raw: null },
180
+ { expectedObjective: "Y" },
181
+ );
182
+ const limited = reconcileCodexGoalSnapshot(
183
+ { available: true, objective: "Y", status: "usage_limited", raw: null },
184
+ { expectedObjective: "Y" },
185
+ );
186
+
187
+ expect(completed.nextActions.join(" ")).toContain("create_goal");
188
+ expect(limited.nextActions.join(" ")).toContain("/goal resume");
189
+ expect([...completed.warnings, ...limited.warnings]).toEqual([]);
155
190
  });
156
191
 
157
192
  it("accepts a parsable snapshot without objective as advisory", () => {
@@ -176,7 +211,8 @@ describe("reconcileCodexGoalSnapshot", () => {
176
211
  // then
177
212
  expect(reconciliation.ok).toBe(true);
178
213
  expect(reconciliation.errors).toHaveLength(0);
179
- expect(reconciliation.warnings.join(" ")).toContain("/goal resume");
214
+ expect(reconciliation.nextActions.join(" ")).toContain("/goal resume");
215
+ expect(reconciliation.warnings).toEqual([]);
180
216
  });
181
217
  });
182
218
 
@@ -0,0 +1,13 @@
1
+ export function barrier() {
2
+ let resolve: () => void = () => {
3
+ throw new Error("barrier not initialized");
4
+ };
5
+ const promise = new Promise<void>((done, reject) => {
6
+ const timeout = setTimeout(() => reject(new Error("barrier timed out")), 5_000);
7
+ resolve = () => {
8
+ clearTimeout(timeout);
9
+ done();
10
+ };
11
+ });
12
+ return { promise, resolve };
13
+ }
@@ -0,0 +1,65 @@
1
+ import { fork } from "node:child_process";
2
+ import { join } from "node:path";
3
+
4
+ const running = new Set<ReturnType<typeof childWriter>>();
5
+ export async function cleanupWriters(): Promise<void> {
6
+ await Promise.all(
7
+ [...running].map(async (writer) => {
8
+ writer.child.kill("SIGKILL");
9
+ await writer.exited;
10
+ }),
11
+ );
12
+ }
13
+ export function childWriter(rootPath: string, mode: string, now = Date.now()) {
14
+ const child = fork(join(import.meta.dirname, "commit-writer.ts"), [rootPath, mode, String(now)], {
15
+ execPath: "bun",
16
+ execArgv: [],
17
+ stdio: ["ignore", "inherit", "inherit", "ipc"],
18
+ });
19
+ const pending = new Map<string, { resolve: () => void; reject: (error: Error) => void }>();
20
+ const seen = new Set<string>();
21
+ child.on("message", (message) => {
22
+ if (typeof message !== "string") return;
23
+ seen.add(message);
24
+ pending.get(message)?.resolve();
25
+ });
26
+ const exited = new Promise<number | null>((resolve, reject) => {
27
+ const timeout = setTimeout(() => {
28
+ child.kill("SIGKILL");
29
+ reject(new Error("child exit timed out"));
30
+ }, 15_000);
31
+ child.once("error", (error) => {
32
+ clearTimeout(timeout);
33
+ reject(error);
34
+ });
35
+ child.once("exit", (code) => {
36
+ clearTimeout(timeout);
37
+ running.delete(writer);
38
+ resolve(code);
39
+ for (const [name, waiter] of pending)
40
+ if (!seen.has(name)) waiter.reject(new Error(`child exited before ${name}: ${code}`));
41
+ });
42
+ });
43
+ const writer = {
44
+ child,
45
+ exited,
46
+ wait(name: string) {
47
+ if (seen.has(name)) return Promise.resolve();
48
+ return new Promise<void>((resolve, reject) => {
49
+ const timeout = setTimeout(() => reject(new Error(`IPC timeout: ${name}`)), 10_000);
50
+ pending.set(name, {
51
+ resolve: () => {
52
+ clearTimeout(timeout);
53
+ resolve();
54
+ },
55
+ reject: (error) => {
56
+ clearTimeout(timeout);
57
+ reject(error);
58
+ },
59
+ });
60
+ });
61
+ },
62
+ };
63
+ running.add(writer);
64
+ return writer;
65
+ }
@@ -0,0 +1,76 @@
1
+ import assert from "node:assert/strict";
2
+ import { fork, spawnSync } from "node:child_process";
3
+ import { existsSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, rmSync, writeFileSync } from "node:fs";
4
+ import { tmpdir } from "node:os";
5
+ import { join, resolve } from "node:path";
6
+ import { withCommitHooks } from "../../dist/plan-commit.js";
7
+ import { commit } from "../../dist/plan-commit.js";
8
+ import { readLedger, readUlwLoopPlan, withUlwLoopMutationLock } from "../../dist/plan-io.js";
9
+ const cli = resolve("dist/cli.js");
10
+ const root = mkdtempSync(join(tmpdir(), "ulw-cli-qa-"));
11
+ const legacyRoot = mkdtempSync(join(tmpdir(), "ulw-legacy-qa-"));
12
+ const scope = { sessionId: "commit-log" };
13
+ const dir = join(root, ".omo", "ulw-loop", scope.sessionId);
14
+ const json = (path) => JSON.parse(readFileSync(path, "utf8"));
15
+ function run(cwd, ...args) {
16
+ const argv = [...args, "--session-id", scope.sessionId, "--json"];
17
+ const result = spawnSync(process.execPath, [cli, ...argv], { cwd, encoding: "utf8", timeout: 15_000, env: { ...process.env, PI_SESSION_ID: "", CODEX_SESSION_ID: "", CODEX_THREAD_ID: "", OMO_ULW_LOOP_SESSION_ID: "" } });
18
+ console.log(`command=${process.execPath} ${cli} ${argv.map((arg) => JSON.stringify(arg)).join(" ")} cwd=${cwd}\n${result.stdout}${result.stderr}\nexit=${result.status}`);
19
+ assert.equal(result.status, 0);
20
+ return JSON.parse(result.stdout);
21
+ }
22
+ try {
23
+ run(root, "create-goals", "--brief", "Build durable state");
24
+ const original = json(join(dir, "goals.json"));
25
+ assert.equal(run(root, "status").plan.revision, 1);
26
+ rmSync(join(dir, "goals.json"));
27
+ assert.equal(run(root, "status").plan.revision, 1);
28
+ assert.equal(existsSync(join(dir, "goals.json")), false);
29
+ const goal = original.goals[0];
30
+ run(root, "record-evidence", "--goal-id", goal.id, "--criterion-id", goal.successCriteria[0].id, "--status", "pass", "--evidence", "Observed real CLI publication");
31
+ assert.equal(json(join(dir, "revisions", "00000002.json")).ledger[0].kind, "evidence_captured");
32
+ assert.equal(json(join(dir, "goals.json")).revision, 2);
33
+ console.log("QA=PASS log-only status is read-only; CLI evidence repairs and commits plan plus audit");
34
+ const clock = { now: () => 0, schedule: () => { throw new Error("Unexpected scheduler use"); } };
35
+ await assert.rejects(withCommitHooks({ beforeCommit: async () => {
36
+ const child = fork(resolve("test/fixtures/commit-writer.ts"), [root, "write", "201"], { execPath: "bun", execArgv: [], stdio: ["ignore", "inherit", "inherit", "ipc"] });
37
+ const exit = await new Promise((done, reject) => {
38
+ const timeout = setTimeout(() => { child.kill("SIGKILL"); reject(new Error("writer B timed out")); }, 10_000);
39
+ child.once("error", (error) => { clearTimeout(timeout); reject(error); });
40
+ child.once("exit", (code) => { clearTimeout(timeout); done(code); });
41
+ });
42
+ assert.equal(exit, 0);
43
+ } }, () => withUlwLoopMutationLock(root, scope, async () => {
44
+ const plan = await readUlwLoopPlan(root, scope);
45
+ plan.brief = "zombie";
46
+ await commit(root, scope, { plan, entries: [{ at: "A", kind: "goal_added", message: "A" }] });
47
+ }, { clock, leaseMs: 200, heartbeatMs: 0 })), { code: "ULW_LOOP_LOCK_LOST" });
48
+ assert.equal(readLedger(root, scope).at(-1).message, "B");
49
+ assert.equal(readLedger(root, scope).some((entry) => entry.message === "A"), false);
50
+ assert.deepEqual(readdirSync(join(dir, "revisions")), ["00000001.json", "00000002.json", "00000003.json"]);
51
+ console.log(`QA=PASS real fork zombie fencing ULW_LOOP_LOCK_LOST record=${JSON.stringify(json(join(dir, "revisions", "00000003.json")))}`);
52
+ run(root, "create-goals", "--brief", "Replacement brief", "--force");
53
+ const recreated = json(join(dir, "goals.json"));
54
+ assert.equal(recreated.revision, 4);
55
+ assert.equal(recreated.ledgerResetRevision, 4);
56
+ assert.equal(readFileSync(join(dir, "brief.md"), "utf8"), recreated.brief);
57
+ assert.equal(readLedger(root, scope).length, 1);
58
+ console.log("QA=PASS force recreation retains revision history and resets the logical audit");
59
+ const legacyDir = join(legacyRoot, ".omo", "ulw-loop", scope.sessionId);
60
+ mkdirSync(legacyDir, { recursive: true });
61
+ delete original.revision; delete original.brief; delete original.ledgerResetRevision;
62
+ writeFileSync(join(legacyDir, "goals.json"), JSON.stringify(original));
63
+ writeFileSync(join(legacyDir, "brief.md"), "Legacy brief\n");
64
+ writeFileSync(join(legacyDir, "ledger.jsonl"), '{"at":"legacy","kind":"goal_started"}\n');
65
+ assert.equal(run(legacyRoot, "status").plan.revision ?? 0, 0);
66
+ assert.equal(existsSync(join(legacyDir, "revisions")), false);
67
+ run(legacyRoot, "add-goal", "--title", "Extra", "--objective", "Verify legacy hydration");
68
+ assert.equal(json(join(legacyDir, "revisions", "00000001.json")).plan.brief, "Legacy brief\n");
69
+ assert.equal(readLedger(legacyRoot, scope)[0].id, "legacy-1");
70
+ console.log("QA=PASS legacy CLI status and first commit preserve brief and legacy audit");
71
+ } finally {
72
+ rmSync(root, { recursive: true, force: true });
73
+ rmSync(legacyRoot, { recursive: true, force: true });
74
+ assert.equal(existsSync(root) || existsSync(legacyRoot), false);
75
+ console.log(`cleanup=REMOVED ${root} ${legacyRoot}; real home directories not accessed by this driver`);
76
+ }
@@ -0,0 +1,23 @@
1
+ import { mkdirSync, writeFileSync } from "node:fs";
2
+ import { rm } from "node:fs/promises";
3
+ import { join } from "node:path";
4
+ import { type UlwLoopScope, ulwLoopDir } from "../../src/paths.js";
5
+ import { createUlwLoopPlan } from "../../src/plan-crud.js";
6
+
7
+ export async function seedCrash(root: string, scope: UlwLoopScope, first: boolean) {
8
+ const dir = ulwLoopDir(root, scope);
9
+ const plan = await createUlwLoopPlan(root, { brief: "original" }, scope);
10
+ const next = { ...plan, revision: (plan.revision ?? 0) + 1, brief: "committed\n", codexObjective: "committed" };
11
+ mkdirSync(join(dir, "revisions"), { recursive: true });
12
+ writeFileSync(
13
+ join(dir, "revisions", `${String(next.revision).padStart(8, "0")}.json`),
14
+ JSON.stringify({
15
+ version: 1,
16
+ revision: next.revision,
17
+ plan: next,
18
+ ledger: [{ at: "now", kind: "goal_added", revision: next.revision, id: `${next.revision}-0` }],
19
+ }),
20
+ );
21
+ if (first) await rm(join(dir, "goals.json"));
22
+ return next;
23
+ }
@@ -0,0 +1,58 @@
1
+ // Run only in an isolated remote test checkout. Each mutant is restored before the next.
2
+ import { createHash } from "node:crypto";
3
+ import { readFileSync, writeFileSync } from "node:fs";
4
+ import { spawnSync } from "node:child_process";
5
+
6
+ function replace(source, before, after) {
7
+ if (source.split(before).length !== 2) throw new Error(`Mutation anchor is not unique: ${before}`);
8
+ return source.replace(before, after);
9
+ }
10
+ const publication = '(hooks.getStore()?.link ?? linkSync)(temp, join(dir, "revisions", `${String(next).padStart(8, "0")}.json`));';
11
+ const logTest = "test/plan-commit-log.test.ts";
12
+ const recoveryTest = "test/plan-commit-recovery.test.ts";
13
+ const mutations = [
14
+ { name: "heartbeat-no-refresh", file: "src/state-lock.ts", test: "test/state-lock.test.ts", pattern: "\\(3\\)", change: (s) => replace(s, "holder.record.leaseUntil = clock.now()", "holder.record.leaseUntil ??= clock.now()") },
15
+ { name: "heartbeat-by-path", file: "src/state-lock.ts", test: "test/state-lock.test.ts", pattern: "\\(2\\)", change: (s) => 'import { writeFileSync } from "node:fs";\n' + replace(s, "writeRecord(holder);\n\t\t\tschedule();", "writeFileSync(lockPath, JSON.stringify(holder.record));\n\t\t\tschedule();") },
16
+ { name: "rename-over-goals", file: "src/plan-commit.ts", test: logTest, pattern: "B wins write", change: (s) => replace(s, publication, `if (next === 1) { ${publication} } else { writeFileSync(temp, JSON.stringify(record.plan)); renameSync(temp, join(dir, "goals.json")); }`) },
17
+ { name: "delete-published-record", file: "src/plan-commit.ts", test: logTest, pattern: "B wins write", change: (s) => replace(s, "await materialize(dir);", 'await materialize(dir);\n if (next > 1) rmSync(join(dir, "revisions", `${String(next).padStart(8, "0")}.json`));') },
18
+ { name: "next-from-directory", file: "src/plan-commit.ts", test: logTest, pattern: "\\(6\\)", change: (s) => replace(s, "const next = (plan.revision ?? 0) + 1;", "const next = Math.max(plan.revision ?? 0, ...readRecords(dir).map((record) => record.revision)) + 1;") },
19
+ { name: "no-holder-retry", file: "src/plan-io.ts", test: logTest, pattern: "\\(6\\)", change: (s) => replace(s, "if (attempt !== 0) throw error;", "throw error;") },
20
+ { name: "no-plan-reconciliation", file: "src/plan-log.ts", test: logTest, pattern: "\\(7a\\)", change: (s) => replace(s, "const latest = readNewestRecord(dir);", "const latest = cached === undefined ? readNewestRecord(dir) : undefined;") },
21
+ { name: "no-ledger-reconciliation", file: "src/ledger.ts", test: logTest, pattern: "\\(7a\\)", change: (s) => replace(s, "for (const record of readRecords(dir))", "for (const record of [])") },
22
+ { name: "cache-only-existence", file: "src/plan-log.ts", test: recoveryTest, pattern: "\\(7b\\)", change: (s) => replace(s, 'return existsSync(join(dir, "goals.json")) || logNames(dir).length > 0;', 'return existsSync(join(dir, "goals.json"));') },
23
+ { name: "in-place-ledger-append", file: "src/plan-commit.ts", test: logTest, pattern: "\\(7c\\)", change: (s) => {
24
+ const start = s.indexOf("export async function materialize(dir:");
25
+ if (start < 0) throw new Error("materializer not found");
26
+ const body = replace(s.slice(start), 'const temp = join(dir, "tmp", `${process.pid}-${randomUUID()}-${name}`);', 'const temp = name === "ledger.jsonl" && readRecords(dir).length > 1 ? join(dir, name) : join(dir, "tmp", `${process.pid}-${randomUUID()}-${name}`);');
27
+ return 'import { appendFile } from "node:fs/promises";\n' + s.slice(0, start) + replace(body, 'await writeViewFile(temp, content);', 'if (name === "ledger.jsonl" && readRecords(dir).length > 1) await (hooks.getStore()?.writeView ?? appendFile)(temp, content); else await writeViewFile(temp, content);');
28
+ } },
29
+ { name: "brief-before-publication", file: "src/plan-crud.ts", test: recoveryTest, pattern: "\\(9\\)", change: (s) => replace(replace(s, 'import { beforePlanMutation, commit }', 'import { beforePlanMutation, commit, writeViewFile }'), "await beforePlanMutation();", 'await beforePlanMutation();\n await writeViewFile(`${repoRoot}/${plan.briefPath}`, plan.brief ?? "");') },
30
+ { name: "steering-audit-outside-record", file: "src/steering.ts", test: logTest, pattern: "\\(11\\)", change: (s) => replace(s, "await commit(repoRoot, scope, { plan: next, entries });", 'await commit(repoRoot, scope, { plan: next, entries: [] });\n await (await import("node:fs/promises")).appendFile(`${repoRoot}/${next.ledgerPath}`, entries.map((entry) => JSON.stringify(entry)).join("\\n") + "\\n");') },
31
+ { name: "copy-fallback", file: "src/plan-commit.ts", test: logTest, pattern: "\\(12\\)", change: (s) => 'import { copyFileSync } from "node:fs";\n' + replace(s, publication, `try { ${publication} } catch (error) { if (hasCode(error, "EPERM")) copyFileSync(temp, join(dir, "revisions", \`\${String(next).padStart(8, "0")}.json\`)); else throw error; }`) },
32
+ ];
33
+ const digest = (content) => createHash("sha256").update(content).digest("hex");
34
+ let failures = 0;
35
+ for (const mutation of mutations) {
36
+ const original = readFileSync(mutation.file, "utf8");
37
+ console.log(`\n## MUTATION ${mutation.name}\nfile=${mutation.file}\nbefore_sha256=${digest(original)}`);
38
+ try {
39
+ writeFileSync(mutation.file, mutation.change(original));
40
+ const args = ["run", "test", "--", mutation.test, "-t", mutation.pattern];
41
+ console.log(`command=bun ${args.map((arg) => JSON.stringify(arg)).join(" ")}`);
42
+ const result = spawnSync("bun", args, { encoding: "utf8", timeout: 60_000 });
43
+ const output = `${result.stdout ?? ""}${result.stderr ?? ""}`;
44
+ console.log(output);
45
+ console.log(`mutation_exit=${result.status}`);
46
+ if (result.status !== 1 || !/[1-9]\d* failed/.test(output.replace(/\x1b\[[0-9;]*m/g, "")) || !output.includes("AssertionError")) {
47
+ failures += 1;
48
+ console.log(`PROOF=FAILED ${mutation.name}`);
49
+ } else console.log(`PROOF=RED ${mutation.name}`);
50
+ } finally {
51
+ writeFileSync(mutation.file, original);
52
+ const restored = readFileSync(mutation.file, "utf8");
53
+ if (restored !== original) throw new Error(`Failed to restore ${mutation.file}`);
54
+ console.log(`restored_sha256=${digest(restored)} cleanup=RESTORED`);
55
+ }
56
+ }
57
+ console.log(`mutation_proofs=${mutations.length} failed_proofs=${failures}`);
58
+ process.exitCode = failures === 0 ? 0 : 1;
@@ -0,0 +1,65 @@
1
+ import { writeFile } from "node:fs/promises";
2
+ import { commit, withCommitHooks } from "../../src/plan-commit.js";
3
+ import { createUlwLoopPlan } from "../../src/plan-crud.js";
4
+ import { readUlwLoopPlan, withMutationLockOptions, withUlwLoopMutationLock } from "../../src/plan-io.js";
5
+ import { steerUlwLoop } from "../../src/steering.js";
6
+ import { leaseClock } from "./lease-clock.js";
7
+
8
+ const root = process.argv[2];
9
+ const mode = process.argv[3];
10
+ if (root === undefined) throw new Error("root required");
11
+ const scope = { sessionId: "commit-log" };
12
+ function barrier(name: string): Promise<void> {
13
+ return new Promise((resolve, reject) => {
14
+ const timeout = setTimeout(() => reject(new Error(`barrier timed out: ${name}`)), 10_000);
15
+ process.once("message", () => {
16
+ clearTimeout(timeout);
17
+ resolve();
18
+ });
19
+ process.send?.(name);
20
+ });
21
+ }
22
+ let calls = 0;
23
+ await withMutationLockOptions({ clock: leaseClock(Number(process.argv[4])), heartbeatMs: 0, timeoutMs: 0 }, () =>
24
+ withCommitHooks(
25
+ {
26
+ beforeCommit: async () => {
27
+ calls += 1;
28
+ if (mode === "mirror" && calls === 1) await barrier("before");
29
+ },
30
+ afterCommit: async () => {
31
+ if (mode === "accepted" || mode === "rejected") await barrier("linked");
32
+ },
33
+ writeView: async (path, content) => {
34
+ if (mode === "partial" && path.endsWith("ledger.jsonl")) {
35
+ await writeFile(path, content.slice(0, Math.max(1, Math.floor(content.length / 2))));
36
+ await barrier("partial");
37
+ }
38
+ await writeFile(path, content);
39
+ },
40
+ },
41
+ async () => {
42
+ if (mode === "force") await createUlwLoopPlan(root, { brief: "B\n", force: true }, scope);
43
+ else if (mode === "accepted" || mode === "rejected")
44
+ await steerUlwLoop(
45
+ root,
46
+ {
47
+ kind: "annotate_ledger",
48
+ source: "cli",
49
+ evidence: mode === "rejected" ? "" : "observed",
50
+ rationale: "audit",
51
+ idempotencyKey: "crash-key",
52
+ },
53
+ scope,
54
+ );
55
+ else
56
+ await withUlwLoopMutationLock(root, scope, async () => {
57
+ const plan = await readUlwLoopPlan(root, scope);
58
+ plan.codexObjective = `${plan.codexObjective}|B`;
59
+ await commit(root, scope, { plan, entries: [{ at: "B", kind: "goal_added", message: "B" }] });
60
+ });
61
+ },
62
+ ),
63
+ );
64
+ process.send?.("done");
65
+ process.disconnect?.();
@@ -0,0 +1,23 @@
1
+ export function leaseClock(initial = 0) {
2
+ let now = initial;
3
+ const tasks = new Set<{ at: number; fn: () => void }>();
4
+ return {
5
+ now: () => now,
6
+ schedule(fn: () => void, ms: number) {
7
+ const task = { at: now + ms, fn };
8
+ tasks.add(task);
9
+ return { unref() {}, cancel: () => tasks.delete(task) };
10
+ },
11
+ advance(ms: number) {
12
+ const end = now + ms;
13
+ for (;;) {
14
+ const next = [...tasks].filter((task) => task.at <= end).sort((a, b) => a.at - b.at)[0];
15
+ if (next === undefined) break;
16
+ now = next.at;
17
+ tasks.delete(next);
18
+ next.fn();
19
+ }
20
+ now = end;
21
+ },
22
+ };
23
+ }