oh-my-opencode 5.0.0-beta.60 → 5.0.0-beta.63

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 (227) 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 +465 -348
  4. package/dist/cli-node/index.js +465 -348
  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 +61 -535
  10. package/dist/oh-my-opencode.schema.json +0 -1
  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 +12 -5
  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 +6887 -249
  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/bootstrap/src/setup.ts +7 -1
  43. package/packages/omo-codex/plugin/components/comment-checker/hooks/hooks.json +1 -1
  44. package/packages/omo-codex/plugin/components/comment-checker/package.json +1 -1
  45. package/packages/omo-codex/plugin/components/git-bash/hooks/hooks.json +2 -2
  46. package/packages/omo-codex/plugin/components/git-bash/package.json +1 -1
  47. package/packages/omo-codex/plugin/components/lazycodex-executor-verify/hooks/hooks.json +1 -1
  48. package/packages/omo-codex/plugin/components/lazycodex-executor-verify/package.json +1 -1
  49. package/packages/omo-codex/plugin/components/lsp/dist/.omo-runtime-manifest.json +2 -2
  50. package/packages/omo-codex/plugin/components/lsp/hooks/hooks.json +2 -2
  51. package/packages/omo-codex/plugin/components/lsp/package.json +1 -1
  52. package/packages/omo-codex/plugin/components/rules/hooks/hooks.json +4 -4
  53. package/packages/omo-codex/plugin/components/rules/package.json +1 -1
  54. package/packages/omo-codex/plugin/components/teammode/hooks/hooks.json +1 -1
  55. package/packages/omo-codex/plugin/components/teammode/package.json +1 -1
  56. package/packages/omo-codex/plugin/components/telemetry/hooks/hooks.json +1 -1
  57. package/packages/omo-codex/plugin/components/telemetry/package.json +1 -1
  58. package/packages/omo-codex/plugin/components/ultrawork/agents/plan.toml +2 -2
  59. package/packages/omo-codex/plugin/components/ultrawork/dist/cli.js +25 -15
  60. package/packages/omo-codex/plugin/components/ultrawork/hooks/hooks.json +1 -1
  61. package/packages/omo-codex/plugin/components/ultrawork/package.json +1 -1
  62. package/packages/omo-codex/plugin/components/ultrawork/src/directive-content.ts +1 -1
  63. package/packages/omo-codex/plugin/components/ulw-execute-continuation/directive.md +1 -1
  64. package/packages/omo-codex/plugin/components/ulw-execute-continuation/hooks/hooks.json +1 -1
  65. package/packages/omo-codex/plugin/components/ulw-execute-continuation/package.json +1 -1
  66. package/packages/omo-codex/plugin/components/ulw-loop/CHANGELOG.md +15 -0
  67. package/packages/omo-codex/plugin/components/ulw-loop/directive.md +25 -15
  68. package/packages/omo-codex/plugin/components/ulw-loop/dist/checkpoint.js +5 -4
  69. package/packages/omo-codex/plugin/components/ulw-loop/dist/cli.js +640 -327
  70. package/packages/omo-codex/plugin/components/ulw-loop/dist/domain-types.d.ts +5 -0
  71. package/packages/omo-codex/plugin/components/ulw-loop/dist/evidence.js +16 -12
  72. package/packages/omo-codex/plugin/components/ulw-loop/dist/ledger.d.ts +4 -0
  73. package/packages/omo-codex/plugin/components/ulw-loop/dist/ledger.js +34 -0
  74. package/packages/omo-codex/plugin/components/ulw-loop/dist/plan-commit.d.ts +19 -0
  75. package/packages/omo-codex/plugin/components/ulw-loop/dist/plan-commit.js +107 -0
  76. package/packages/omo-codex/plugin/components/ulw-loop/dist/plan-crud.d.ts +2 -1
  77. package/packages/omo-codex/plugin/components/ulw-loop/dist/plan-crud.js +48 -22
  78. package/packages/omo-codex/plugin/components/ulw-loop/dist/plan-io.d.ts +9 -7
  79. package/packages/omo-codex/plugin/components/ulw-loop/dist/plan-io.js +105 -114
  80. package/packages/omo-codex/plugin/components/ulw-loop/dist/plan-log.d.ts +15 -0
  81. package/packages/omo-codex/plugin/components/ulw-loop/dist/plan-log.js +86 -0
  82. package/packages/omo-codex/plugin/components/ulw-loop/dist/plan-missing-recovery.d.ts +5 -1
  83. package/packages/omo-codex/plugin/components/ulw-loop/dist/plan-missing-recovery.js +15 -3
  84. package/packages/omo-codex/plugin/components/ulw-loop/dist/review-blockers.js +3 -4
  85. package/packages/omo-codex/plugin/components/ulw-loop/dist/sdk/factory.js +29 -6
  86. package/packages/omo-codex/plugin/components/ulw-loop/dist/spawn-budget-io.d.ts +5 -0
  87. package/packages/omo-codex/plugin/components/ulw-loop/dist/spawn-budget-io.js +67 -0
  88. package/packages/omo-codex/plugin/components/ulw-loop/dist/spawn-guard.d.ts +1 -0
  89. package/packages/omo-codex/plugin/components/ulw-loop/dist/spawn-guard.js +29 -66
  90. package/packages/omo-codex/plugin/components/ulw-loop/dist/spawn-role-guard.d.ts +2 -0
  91. package/packages/omo-codex/plugin/components/ulw-loop/dist/spawn-role-guard.js +22 -0
  92. package/packages/omo-codex/plugin/components/ulw-loop/dist/state-lock.d.ts +11 -1
  93. package/packages/omo-codex/plugin/components/ulw-loop/dist/state-lock.js +120 -88
  94. package/packages/omo-codex/plugin/components/ulw-loop/dist/status-next-actions.d.ts +3 -2
  95. package/packages/omo-codex/plugin/components/ulw-loop/dist/status-next-actions.js +18 -10
  96. package/packages/omo-codex/plugin/components/ulw-loop/dist/steering-batch.js +4 -4
  97. package/packages/omo-codex/plugin/components/ulw-loop/dist/steering.js +5 -5
  98. package/packages/omo-codex/plugin/components/ulw-loop/dist/stop-resume-hook.js +8 -17
  99. package/packages/omo-codex/plugin/components/ulw-loop/hooks/hooks.json +7 -7
  100. package/packages/omo-codex/plugin/components/ulw-loop/package.json +1 -1
  101. package/packages/omo-codex/plugin/components/ulw-loop/skills/ulw-loop/references/full-workflow.md +2 -2
  102. package/packages/omo-codex/plugin/components/ulw-loop/src/checkpoint.ts +5 -4
  103. package/packages/omo-codex/plugin/components/ulw-loop/src/domain-types.ts +5 -0
  104. package/packages/omo-codex/plugin/components/ulw-loop/src/evidence.ts +16 -16
  105. package/packages/omo-codex/plugin/components/ulw-loop/src/ledger.ts +33 -0
  106. package/packages/omo-codex/plugin/components/ulw-loop/src/plan-commit.ts +124 -0
  107. package/packages/omo-codex/plugin/components/ulw-loop/src/plan-crud.ts +58 -51
  108. package/packages/omo-codex/plugin/components/ulw-loop/src/plan-io.ts +126 -140
  109. package/packages/omo-codex/plugin/components/ulw-loop/src/plan-log.ts +84 -0
  110. package/packages/omo-codex/plugin/components/ulw-loop/src/plan-missing-recovery.ts +26 -3
  111. package/packages/omo-codex/plugin/components/ulw-loop/src/review-blockers.ts +3 -3
  112. package/packages/omo-codex/plugin/components/ulw-loop/src/sdk/factory.ts +55 -6
  113. package/packages/omo-codex/plugin/components/ulw-loop/src/spawn-budget-io.ts +59 -0
  114. package/packages/omo-codex/plugin/components/ulw-loop/src/spawn-guard.ts +29 -62
  115. package/packages/omo-codex/plugin/components/ulw-loop/src/spawn-role-guard.ts +22 -0
  116. package/packages/omo-codex/plugin/components/ulw-loop/src/state-lock.ts +129 -111
  117. package/packages/omo-codex/plugin/components/ulw-loop/src/status-next-actions.ts +19 -10
  118. package/packages/omo-codex/plugin/components/ulw-loop/src/steering-batch.ts +4 -4
  119. package/packages/omo-codex/plugin/components/ulw-loop/src/steering.ts +5 -4
  120. package/packages/omo-codex/plugin/components/ulw-loop/src/stop-resume-hook.ts +8 -16
  121. package/packages/omo-codex/plugin/components/ulw-loop/test/fixtures/barrier.ts +13 -0
  122. package/packages/omo-codex/plugin/components/ulw-loop/test/fixtures/commit-child.ts +65 -0
  123. package/packages/omo-codex/plugin/components/ulw-loop/test/fixtures/commit-cli-qa.mjs +76 -0
  124. package/packages/omo-codex/plugin/components/ulw-loop/test/fixtures/commit-crash.ts +23 -0
  125. package/packages/omo-codex/plugin/components/ulw-loop/test/fixtures/commit-mutations.mjs +58 -0
  126. package/packages/omo-codex/plugin/components/ulw-loop/test/fixtures/commit-writer.ts +65 -0
  127. package/packages/omo-codex/plugin/components/ulw-loop/test/fixtures/lease-clock.ts +23 -0
  128. package/packages/omo-codex/plugin/components/ulw-loop/test/plan-commit-log.test.ts +252 -0
  129. package/packages/omo-codex/plugin/components/ulw-loop/test/plan-commit-recovery.test.ts +190 -0
  130. package/packages/omo-codex/plugin/components/ulw-loop/test/plan-io.test.ts +50 -35
  131. package/packages/omo-codex/plugin/components/ulw-loop/test/plan-log-newest.test.ts +72 -0
  132. package/packages/omo-codex/plugin/components/ulw-loop/test/sdk-completed-plan.test.ts +43 -0
  133. package/packages/omo-codex/plugin/components/ulw-loop/test/sdk-driver-states.test.ts +145 -0
  134. package/packages/omo-codex/plugin/components/ulw-loop/test/sdk-session-context.test.ts +21 -0
  135. package/packages/omo-codex/plugin/components/ulw-loop/test/spawn-guard.test.ts +7 -1
  136. package/packages/omo-codex/plugin/components/ulw-loop/test/spawn-role-matrix.test.ts +113 -0
  137. package/packages/omo-codex/plugin/components/ulw-loop/test/state-lock.test.ts +92 -0
  138. package/packages/omo-codex/plugin/components/ulw-loop/test/status-next-actions-surface.test.ts +133 -0
  139. package/packages/omo-codex/plugin/components/ulw-loop/test/steering-batch.test.ts +12 -12
  140. package/packages/omo-codex/plugin/components/ulw-loop/test/steering.test.ts +4 -1
  141. package/packages/omo-codex/plugin/hooks/post-compact-resetting-git-bash-mcp-reminder.json +1 -1
  142. package/packages/omo-codex/plugin/hooks/post-compact-resetting-lsp-diagnostics-cache.json +1 -1
  143. package/packages/omo-codex/plugin/hooks/post-compact-resetting-project-rule-cache.json +1 -1
  144. package/packages/omo-codex/plugin/hooks/post-tool-use-checking-comments.json +1 -1
  145. package/packages/omo-codex/plugin/hooks/post-tool-use-checking-lsp-diagnostics.json +1 -1
  146. package/packages/omo-codex/plugin/hooks/post-tool-use-checking-thread-title-hygiene.json +1 -1
  147. package/packages/omo-codex/plugin/hooks/post-tool-use-matching-project-rules.json +1 -1
  148. package/packages/omo-codex/plugin/hooks/post-tool-use-recording-spawn-admission.json +1 -1
  149. package/packages/omo-codex/plugin/hooks/pre-tool-use-enforcing-unlimited-goal-budget.json +1 -1
  150. package/packages/omo-codex/plugin/hooks/pre-tool-use-guarding-ulw-loop-spawns.json +1 -1
  151. package/packages/omo-codex/plugin/hooks/pre-tool-use-recommending-git-bash-mcp.json +1 -1
  152. package/packages/omo-codex/plugin/hooks/session-start-checking-auto-update.json +1 -1
  153. package/packages/omo-codex/plugin/hooks/session-start-checking-bootstrap-provisioning.json +1 -1
  154. package/packages/omo-codex/plugin/hooks/session-start-loading-project-rules.json +1 -1
  155. package/packages/omo-codex/plugin/hooks/session-start-recording-session-telemetry.json +1 -1
  156. package/packages/omo-codex/plugin/hooks/stop-checking-ulw-execute-continuation.json +1 -1
  157. package/packages/omo-codex/plugin/hooks/stop-checking-ulw-loop-resume.json +1 -1
  158. package/packages/omo-codex/plugin/hooks/subagent-stop-verifying-lazycodex-executor-evidence.json +1 -1
  159. package/packages/omo-codex/plugin/hooks/user-prompt-submit-checking-ultrawork-trigger.json +1 -1
  160. package/packages/omo-codex/plugin/hooks/user-prompt-submit-checking-ulw-loop-steering.json +1 -1
  161. package/packages/omo-codex/plugin/hooks/user-prompt-submit-loading-project-rules.json +1 -1
  162. package/packages/omo-codex/plugin/package-lock.json +12 -12
  163. package/packages/omo-codex/plugin/package.json +1 -1
  164. package/packages/omo-codex/plugin/scripts/sync-skills.d.mts +4 -0
  165. package/packages/omo-codex/plugin/scripts/sync-skills.mjs +18 -10
  166. package/packages/omo-codex/plugin/skills/ast-grep/SKILL.md +12 -0
  167. package/packages/omo-codex/plugin/skills/debugging/references/tools/playwright-cli.md +79 -161
  168. package/packages/omo-codex/plugin/skills/frontend/SKILL.md +1 -1
  169. package/packages/omo-codex/plugin/skills/frontend/references/design/aside.md +1 -1
  170. package/packages/omo-codex/plugin/skills/frontend/references/design/clone-from-url.md +1 -1
  171. package/packages/omo-codex/plugin/skills/frontend/scripts/perfection/lighthouse-audit.py +2 -2
  172. package/packages/omo-codex/plugin/skills/init-deep/SKILL.md +3 -3
  173. package/packages/omo-codex/plugin/skills/refactor/SKILL.md +3 -3
  174. package/packages/omo-codex/plugin/skills/remove-ai-slops/SKILL.md +3 -3
  175. package/packages/omo-codex/plugin/skills/review-work/SKILL.md +4 -4
  176. package/packages/omo-codex/plugin/skills/ultimate-browsing/ATTRIBUTION.md +1 -82
  177. package/packages/omo-codex/plugin/skills/ultimate-browsing/SKILL.md +17 -29
  178. package/packages/omo-codex/plugin/skills/ultimate-browsing/engine/AGENTS.md +2 -1
  179. package/packages/omo-codex/plugin/skills/ultimate-browsing/engine/templates/package.json +2 -2
  180. package/packages/omo-codex/plugin/skills/ultimate-browsing/engine/templates/playwright_mobile_chrome.js +5 -2
  181. package/packages/omo-codex/plugin/skills/ultimate-browsing/engine/templates/playwright_real_chrome.js +8 -6
  182. package/packages/omo-codex/plugin/skills/ultimate-browsing/engine/tests/test_playwright_stealth.py +86 -0
  183. package/packages/omo-codex/plugin/skills/ultimate-browsing/engine/tests/test_playwright_templates.py +4 -7
  184. package/packages/omo-codex/plugin/skills/ultimate-browsing/references/chrome-stealth.md +86 -100
  185. package/packages/omo-codex/plugin/skills/ultimate-browsing/references/insane-search/README.md +9 -3
  186. package/packages/omo-codex/plugin/skills/ultimate-browsing/references/insane-search/playwright.md +16 -13
  187. package/packages/omo-codex/plugin/skills/ultimate-browsing/scripts/extract_cookies.py +3 -3
  188. package/packages/omo-codex/plugin/skills/ultrawork/SKILL.md +25 -15
  189. package/packages/omo-codex/plugin/skills/ulw-execute/SKILL.md +4 -4
  190. package/packages/omo-codex/plugin/skills/ulw-loop/SKILL.md +3 -3
  191. package/packages/omo-codex/plugin/skills/ulw-loop/references/full-workflow.md +2 -2
  192. package/packages/omo-codex/plugin/skills/ulw-research/SKILL.md +6 -6
  193. package/packages/omo-codex/plugin/skills/visual-qa/AGENTS.md +1 -1
  194. package/packages/omo-codex/plugin/skills/visual-qa/SKILL.md +4 -4
  195. package/packages/omo-codex/plugin/skills/visual-qa/references/browser-setup.md +75 -0
  196. package/packages/omo-codex/plugin/test/bootstrap-default-role.test.mjs +66 -0
  197. package/packages/omo-codex/plugin/test/component-hook-contract-cases.mjs +13 -1
  198. package/packages/omo-codex/scripts/install-dist/install-local.mjs +7060 -417
  199. package/packages/prompts-core/prompts/ultrawork/codex.md +25 -15
  200. package/packages/shared-skills/skills/ast-grep/SKILL.md +12 -0
  201. package/packages/shared-skills/skills/debugging/references/tools/playwright-cli.md +79 -161
  202. package/packages/shared-skills/skills/frontend/SKILL.md +1 -1
  203. package/packages/shared-skills/skills/frontend/references/design/aside.md +1 -1
  204. package/packages/shared-skills/skills/frontend/references/design/clone-from-url.md +1 -1
  205. package/packages/shared-skills/skills/frontend/scripts/perfection/lighthouse-audit.py +2 -2
  206. package/packages/shared-skills/skills/review-work/SKILL.md +1 -1
  207. package/packages/shared-skills/skills/ultimate-browsing/ATTRIBUTION.md +1 -82
  208. package/packages/shared-skills/skills/ultimate-browsing/SKILL.md +17 -29
  209. package/packages/shared-skills/skills/ultimate-browsing/engine/AGENTS.md +2 -1
  210. package/packages/shared-skills/skills/ultimate-browsing/engine/templates/package.json +2 -2
  211. package/packages/shared-skills/skills/ultimate-browsing/engine/templates/playwright_mobile_chrome.js +5 -2
  212. package/packages/shared-skills/skills/ultimate-browsing/engine/templates/playwright_real_chrome.js +8 -6
  213. package/packages/shared-skills/skills/ultimate-browsing/engine/tests/test_playwright_stealth.py +86 -0
  214. package/packages/shared-skills/skills/ultimate-browsing/engine/tests/test_playwright_templates.py +4 -7
  215. package/packages/shared-skills/skills/ultimate-browsing/references/chrome-stealth.md +86 -100
  216. package/packages/shared-skills/skills/ultimate-browsing/references/insane-search/README.md +9 -3
  217. package/packages/shared-skills/skills/ultimate-browsing/references/insane-search/playwright.md +16 -13
  218. package/packages/shared-skills/skills/ultimate-browsing/scripts/extract_cookies.py +3 -3
  219. package/packages/shared-skills/skills/ulw-execute/SKILL.md +1 -1
  220. package/packages/shared-skills/skills/ulw-plan/scripts/scaffold-plan.mjs +1 -1
  221. package/packages/shared-skills/skills/ulw-research/SKILL.md +3 -3
  222. package/packages/shared-skills/skills/visual-qa/AGENTS.md +1 -1
  223. package/packages/shared-skills/skills/visual-qa/SKILL.md +1 -1
  224. package/packages/shared-skills/skills/visual-qa/references/browser-setup.md +75 -0
  225. package/dist/skills/visual-qa/references/agent-browser-setup.md +0 -45
  226. package/packages/omo-codex/plugin/skills/visual-qa/references/agent-browser-setup.md +0 -45
  227. package/packages/shared-skills/skills/visual-qa/references/agent-browser-setup.md +0 -45
@@ -0,0 +1,252 @@
1
+ import { existsSync, readdirSync, readFileSync, writeFileSync } from "node:fs";
2
+ import { mkdtemp, rm } from "node:fs/promises";
3
+ import { tmpdir } from "node:os";
4
+ import { join } from "node:path";
5
+ import { afterEach, beforeEach, describe, expect, it } from "vitest";
6
+ import { ulwLoopDir } from "../src/paths.js";
7
+ import { createUlwLoopPlan } from "../src/plan-crud.js";
8
+ import * as io from "../src/plan-io.js";
9
+ import { childWriter, cleanupWriters } from "./fixtures/commit-child.js";
10
+ import { seedCrash } from "./fixtures/commit-crash.js";
11
+ import { leaseClock } from "./fixtures/lease-clock.js";
12
+
13
+ let root: string;
14
+ let dir: string;
15
+ const scope = { sessionId: "commit-log" };
16
+ beforeEach(async () => {
17
+ root = await mkdtemp(join(tmpdir(), "ulw-commit-"));
18
+ dir = ulwLoopDir(root, scope);
19
+ });
20
+ afterEach(async () => {
21
+ await cleanupWriters();
22
+ await rm(root, { recursive: true, force: true });
23
+ });
24
+ function snapshot() {
25
+ return Object.fromEntries(
26
+ ["goals.json", "ledger.jsonl", "brief.md"].map((name) => [
27
+ name,
28
+ existsSync(join(dir, name)) ? readFileSync(join(dir, name), "utf8") : null,
29
+ ]),
30
+ );
31
+ }
32
+
33
+ describe("#given an immutable plan and audit log", () => {
34
+ it("#when (7a) a writer dies after publishing #then reads reconcile without writes and locked reads repair views", async () => {
35
+ const next = await seedCrash(root, scope, false);
36
+ const before = snapshot();
37
+ expect(await io.readUlwLoopPlan(root, scope)).toEqual(next);
38
+ expect(io.readLedger(root, scope).at(-1)?.id).toBe(`${next.revision}-0`);
39
+ expect(snapshot()).toEqual(before);
40
+ await io.withUlwLoopMutationLock(root, scope, () => io.readUlwLoopPlan(root, scope));
41
+ expect(JSON.parse(readFileSync(join(dir, "goals.json"), "utf8"))).toEqual(next);
42
+ expect(readFileSync(join(dir, "brief.md"), "utf8")).toBe(next.brief);
43
+ });
44
+ it("#when (7b) first creation has no goals cache #then existence and reads use the log", async () => {
45
+ const next = await seedCrash(root, scope, true);
46
+ expect(io.planExists(root, scope)).toBe(true);
47
+ expect(await io.readUlwLoopPlan(root, scope)).toEqual(next);
48
+ });
49
+ it("#when (8) a legacy plan first commits #then revision zero and legacy ledger and brief survive", async () => {
50
+ const plan = await createUlwLoopPlan(root, { brief: "legacy" }, scope);
51
+ delete plan.revision;
52
+ delete plan.brief;
53
+ delete plan.ledgerResetRevision;
54
+ await rm(join(dir, "revisions"), { recursive: true, force: true });
55
+ writeFileSync(join(dir, "goals.json"), JSON.stringify(plan));
56
+ writeFileSync(join(dir, "ledger.jsonl"), '{"at":"legacy","kind":"goal_started"}\n{"torn":');
57
+ expect(io.readLedger(root, scope)[0]).toMatchObject({ revision: 0, id: "legacy-1" });
58
+ const { commit } = await import("../src/plan-commit.js");
59
+ await io.withUlwLoopMutationLock(root, scope, async () => {
60
+ const legacy = await io.readUlwLoopPlan(root, scope);
61
+ await commit(root, scope, { plan: legacy, entries: [] });
62
+ });
63
+ expect(readdirSync(join(dir, "revisions"))).toEqual(["00000001.json"]);
64
+ expect(io.readLedger(root, scope)[0]?.id).toBe("legacy-1");
65
+ expect(readFileSync(join(dir, "brief.md"), "utf8")).toBe("legacy\n");
66
+ });
67
+ it("#when (9) force recreation succeeds #then the reset is a new revision with filtered views", async () => {
68
+ const first = await createUlwLoopPlan(root, { brief: "old" }, scope);
69
+ const next = await createUlwLoopPlan(root, { brief: "new", force: true }, scope);
70
+ expect(next.revision).toBe((first.revision ?? 0) + 1);
71
+ expect(next.ledgerResetRevision).toBe(next.revision);
72
+ expect(io.readLedger(root, scope)).toHaveLength(1);
73
+ expect(readFileSync(join(dir, "brief.md"), "utf8")).toBe(next.brief);
74
+ expect(readdirSync(join(dir, "revisions"))).toHaveLength(2);
75
+ });
76
+ it("#when (12) hard links are unsupported #then publication fails closed without a record or view changes", async () => {
77
+ await createUlwLoopPlan(root, { brief: "old" }, scope);
78
+ const before = snapshot();
79
+ const { withCommitHooks, commit } = await import("../src/plan-commit.js");
80
+ await expect(
81
+ withCommitHooks(
82
+ {
83
+ link: () => {
84
+ throw Object.assign(new Error("unsupported"), { code: "EPERM" });
85
+ },
86
+ },
87
+ () =>
88
+ io.withUlwLoopMutationLock(root, scope, async () => {
89
+ const plan = await io.readUlwLoopPlan(root, scope);
90
+ plan.brief = "forbidden";
91
+ await commit(root, scope, { plan, entries: [] });
92
+ }),
93
+ ),
94
+ ).rejects.toMatchObject({ code: "ULW_LOOP_PUBLISH_UNSUPPORTED_FS" });
95
+ expect(snapshot()).toEqual(before);
96
+ expect(readdirSync(join(dir, "revisions"))).toHaveLength(1);
97
+ });
98
+ });
99
+
100
+ describe("#given paused writers in separate processes", () => {
101
+ it.each(["write", "force"])(
102
+ "#when (5)/(9) B wins %s after A's precheck #then A loses without publishing or changing views",
103
+ async (mode) => {
104
+ await createUlwLoopPlan(root, { brief: "initial" }, scope);
105
+ const { withCommitHooks, commit } = await import("../src/plan-commit.js");
106
+ let successor: ReturnType<typeof snapshot> | undefined;
107
+ let writes = 0;
108
+ const clock = leaseClock();
109
+ await expect(
110
+ withCommitHooks(
111
+ {
112
+ beforeCommit: async () => {
113
+ clock.advance(201);
114
+ const writer = childWriter(root, mode, clock.now());
115
+ expect(await writer.exited).toBe(0);
116
+ successor = snapshot();
117
+ },
118
+ writeView: async () => {
119
+ writes += 1;
120
+ throw new Error("loser wrote a view");
121
+ },
122
+ },
123
+ () =>
124
+ io.withUlwLoopMutationLock(
125
+ root,
126
+ scope,
127
+ async () => {
128
+ const plan = await io.readUlwLoopPlan(root, scope);
129
+ plan.brief = "A\n";
130
+ await commit(root, scope, { plan, entries: [{ at: "A", kind: "goal_added", message: "A" }] });
131
+ },
132
+ { clock, leaseMs: 200, heartbeatMs: 0 },
133
+ ),
134
+ ),
135
+ ).rejects.toMatchObject({ code: "ULW_LOOP_LOCK_LOST" });
136
+ expect(writes).toBe(0);
137
+ expect(snapshot()).toEqual(successor);
138
+ const published = JSON.parse(readFileSync(join(dir, "revisions", "00000002.json"), "utf8"));
139
+ expect(published.plan).toEqual(await io.readUlwLoopPlan(root, scope));
140
+ expect(published.ledger).toHaveLength(1);
141
+ expect(io.readLedger(root, scope).at(-1)).toEqual(published.ledger[0]);
142
+ expect(published.ledger[0].kind).toBe(mode === "force" ? "plan_created" : "goal_added");
143
+ expect(io.readLedger(root, scope).some((entry) => entry.message === "A")).toBe(false);
144
+ expect(readdirSync(join(dir, "revisions"))).toEqual(["00000001.json", "00000002.json"]);
145
+ },
146
+ );
147
+ it("#when (6) A wins before the valid holder B #then B retries once from A's revision", async () => {
148
+ await createUlwLoopPlan(root, { brief: "initial" }, scope);
149
+ const { withCommitHooks, commit } = await import("../src/plan-commit.js");
150
+ let writer: ReturnType<typeof childWriter> | undefined;
151
+ const clock = leaseClock();
152
+ await withCommitHooks(
153
+ {
154
+ beforeCommit: async () => {
155
+ clock.advance(201);
156
+ writer = childWriter(root, "mirror", clock.now());
157
+ await writer.wait("before");
158
+ },
159
+ },
160
+ () =>
161
+ io.withUlwLoopMutationLock(
162
+ root,
163
+ scope,
164
+ async () => {
165
+ const plan = await io.readUlwLoopPlan(root, scope);
166
+ plan.codexObjective = "A";
167
+ await commit(root, scope, { plan, entries: [{ at: "A", kind: "goal_added", message: "A" }] });
168
+ },
169
+ { clock, leaseMs: 200, heartbeatMs: 0 },
170
+ ),
171
+ );
172
+ if (writer === undefined) throw new Error("B not started");
173
+ writer.child.send("resume");
174
+ expect(await writer.exited).toBe(0);
175
+ expect((await io.readUlwLoopPlan(root, scope)).codexObjective).toBe("A|B");
176
+ expect(
177
+ io
178
+ .readLedger(root, scope)
179
+ .slice(-2)
180
+ .map((entry) => entry.message),
181
+ ).toEqual(["A", "B"]);
182
+ expect(readdirSync(join(dir, "revisions"))).toHaveLength(3);
183
+ });
184
+ it("#when (7c) a child dies after an acknowledged partial temp write #then every public view remains complete", async () => {
185
+ await createUlwLoopPlan(root, { brief: "initial" }, scope);
186
+ const before = snapshot();
187
+ const writer = childWriter(root, "partial");
188
+ await writer.wait("partial");
189
+ writer.child.kill("SIGKILL");
190
+ await writer.exited;
191
+ const after = snapshot();
192
+ const published = JSON.parse(readFileSync(join(dir, "revisions", "00000002.json"), "utf8"));
193
+ expect([before["goals.json"], `${JSON.stringify(published.plan, null, 2)}\n`]).toContain(after["goals.json"]);
194
+ expect([before["brief.md"], published.plan.brief]).toContain(after["brief.md"]);
195
+ expect(after["ledger.jsonl"]).toBe(before["ledger.jsonl"]);
196
+ for (const line of readFileSync(join(dir, "ledger.jsonl"), "utf8").trim().split("\n"))
197
+ expect(() => JSON.parse(line)).not.toThrow();
198
+ expect(io.readLedger(root, scope).at(-1)?.message).toBe("B");
199
+ });
200
+ it.each(["accepted", "rejected"])(
201
+ "#when (11) %s steering dies after link #then its audit survives and accepted retries dedupe",
202
+ async (mode) => {
203
+ const original = await createUlwLoopPlan(root, { brief: "initial" }, scope);
204
+ const writer = childWriter(root, mode);
205
+ await writer.wait("linked");
206
+ writer.child.kill("SIGKILL");
207
+ await writer.exited;
208
+ expect(io.readLedger(root, scope).at(-1)?.kind).toBe(`steering_${mode}`);
209
+ const prior = await io.findAcceptedSteeringLedgerEntry(root, "crash-key", scope);
210
+ if (mode === "rejected") {
211
+ expect(prior).toBeUndefined();
212
+ expect(await io.readUlwLoopPlan(root, scope)).toEqual({ ...original, revision: 2 });
213
+ } else {
214
+ const { steerUlwLoop } = await import("../src/steering.js");
215
+ const result = await steerUlwLoop(
216
+ root,
217
+ {
218
+ kind: "annotate_ledger",
219
+ source: "cli",
220
+ evidence: "observed",
221
+ rationale: "audit",
222
+ idempotencyKey: "crash-key",
223
+ },
224
+ scope,
225
+ );
226
+ expect(result.deduped).toBe(true);
227
+ expect(io.readLedger(root, scope)).toHaveLength(2);
228
+ expect(readdirSync(join(dir, "revisions"))).toHaveLength(2);
229
+ }
230
+ },
231
+ );
232
+ it("#when (10) retry fails before commit #then no goal_retried audit escapes", async () => {
233
+ const { startNextUlwLoop } = await import("../src/plan-crud.js");
234
+ const plan = await createUlwLoopPlan(root, { brief: "initial" }, scope);
235
+ const { commit, withCommitHooks } = await import("../src/plan-commit.js");
236
+ for (const goal of plan.goals) goal.status = "failed";
237
+ await commit(root, scope, { plan, entries: [] });
238
+ const before = snapshot();
239
+ await expect(
240
+ withCommitHooks(
241
+ {
242
+ beforeCommit: () => {
243
+ throw new Error("stop before commit");
244
+ },
245
+ },
246
+ () => startNextUlwLoop(root, { retryFailed: true }, scope),
247
+ ),
248
+ ).rejects.toThrow("stop before commit");
249
+ expect(snapshot()).toEqual(before);
250
+ expect(io.readLedger(root, scope).some((entry) => entry.kind === "goal_retried")).toBe(false);
251
+ });
252
+ });
@@ -0,0 +1,190 @@
1
+ import { readdirSync, readFileSync } from "node:fs";
2
+ import { mkdtemp, rm, writeFile } from "node:fs/promises";
3
+ import { tmpdir } from "node:os";
4
+ import { join } from "node:path";
5
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
6
+ import type { PreToolUsePayload } from "../src/codex-hook.js";
7
+ import { ulwLoopDir } from "../src/paths.js";
8
+ import { commit, materialize, withCommitHooks } from "../src/plan-commit.js";
9
+ import { createUlwLoopPlan } from "../src/plan-crud.js";
10
+ import {
11
+ planExists,
12
+ readLedger,
13
+ readUlwLoopPlan,
14
+ withMutationLockOptions,
15
+ withUlwLoopMutationLock,
16
+ } from "../src/plan-io.js";
17
+ import { applySpawnBudgetGuards } from "../src/spawn-guard.js";
18
+ import { runStopResumeHook } from "../src/stop-resume-hook.js";
19
+ import { childWriter, cleanupWriters } from "./fixtures/commit-child.js";
20
+ import { leaseClock } from "./fixtures/lease-clock.js";
21
+
22
+ let root: string;
23
+ let dir: string;
24
+ const scope = { sessionId: "commit-log" };
25
+ beforeEach(async () => {
26
+ root = await mkdtemp(join(tmpdir(), "ulw-recovery-"));
27
+ dir = ulwLoopDir(root, scope);
28
+ });
29
+ afterEach(async () => {
30
+ await cleanupWriters();
31
+ vi.unstubAllEnvs();
32
+ await rm(root, { recursive: true, force: true });
33
+ });
34
+ function signal() {
35
+ let resolve: () => void = () => {
36
+ throw new Error("signal not initialized");
37
+ };
38
+ const promise = new Promise<void>((done, reject) => {
39
+ const timeout = setTimeout(() => reject(new Error("signal timed out")), 5_000);
40
+ resolve = () => {
41
+ clearTimeout(timeout);
42
+ done();
43
+ };
44
+ });
45
+ return { promise, resolve };
46
+ }
47
+ function views() {
48
+ return ["goals.json", "ledger.jsonl", "brief.md"].map((name) => readFileSync(join(dir, name), "utf8"));
49
+ }
50
+ describe("#given crash recovery through public surfaces", () => {
51
+ it("#when (7b) only revision one exists #then both hooks and existence gates use it", async () => {
52
+ const plan = await createUlwLoopPlan(root, { brief: "unfinished" }, scope);
53
+ for (const name of ["goals.json", "ledger.jsonl", "brief.md"]) await rm(join(dir, name));
54
+ expect(readdirSync(join(dir, "revisions"))).toEqual(["00000001.json"]);
55
+ expect(planExists(root, scope)).toBe(true);
56
+ expect((await readUlwLoopPlan(root, scope)).revision).toBe(1);
57
+ const payload = {
58
+ session_id: scope.sessionId,
59
+ turn_id: "turn",
60
+ cwd: root,
61
+ model: "test",
62
+ permission_mode: "default",
63
+ };
64
+ const stop = {
65
+ ...payload,
66
+ hook_event_name: "Stop",
67
+ transcript_path: join(root, "transcript"),
68
+ stop_hook_active: false,
69
+ };
70
+ expect(JSON.parse(runStopResumeHook(stop)).decision).toBe("block");
71
+ expect(JSON.parse(readFileSync(join(dir, `auto-resume-${plan.goals[0]?.id}.json`), "utf8")).ledgerLineCount).toBe(
72
+ 1,
73
+ );
74
+ vi.stubEnv("OMO_SPAWN_FANOUT_LIMIT", "1");
75
+ vi.stubEnv("PLUGIN_DATA", join(root, "plugin-data"));
76
+ const spawn: PreToolUsePayload = {
77
+ ...payload,
78
+ hook_event_name: "PreToolUse",
79
+ transcript_path: null,
80
+ tool_name: "spawn_agent",
81
+ tool_use_id: "tool",
82
+ tool_input: {},
83
+ };
84
+ expect(applySpawnBudgetGuards(spawn)).toBe("");
85
+ expect(JSON.parse(applySpawnBudgetGuards(spawn)).hookSpecificOutput.permissionDecision).toBe("deny");
86
+ });
87
+ it("#when (7c) two materializers overlap #then every public inode is complete even if a view regresses", async () => {
88
+ const plan = await createUlwLoopPlan(root, { brief: "old" }, scope);
89
+ const old = views();
90
+ await withCommitHooks(
91
+ {
92
+ afterCommit: () => {
93
+ throw new Error("crash");
94
+ },
95
+ },
96
+ () =>
97
+ commit(root, scope, {
98
+ plan: { ...plan, brief: "middle\n" },
99
+ entries: [{ at: "middle", kind: "goal_added" }],
100
+ }),
101
+ ).catch((error: unknown) => {
102
+ expect(error).toBeInstanceOf(Error);
103
+ });
104
+ const middleViews = [
105
+ `${JSON.stringify(await readUlwLoopPlan(root, scope), null, 2)}\n`,
106
+ `${readLedger(root, scope)
107
+ .map((entry) => JSON.stringify(entry))
108
+ .join("\n")}\n`,
109
+ "middle\n",
110
+ ];
111
+ const partial = signal();
112
+ const resume = signal();
113
+ const first = withCommitHooks(
114
+ {
115
+ writeView: async (path, content) => {
116
+ if (path.endsWith("ledger.jsonl")) {
117
+ await writeFile(path, content.slice(0, 5));
118
+ partial.resolve();
119
+ await resume.promise;
120
+ }
121
+ await writeFile(path, content);
122
+ },
123
+ },
124
+ () => materialize(dir),
125
+ );
126
+ await partial.promise;
127
+ expect(readFileSync(join(dir, "ledger.jsonl"), "utf8")).toBe(old[1]);
128
+ const latest = await readUlwLoopPlan(root, scope);
129
+ await commit(root, scope, { plan: { ...latest, brief: "new\n" }, entries: [{ at: "new", kind: "goal_added" }] });
130
+ const newViews = views();
131
+ resume.resolve();
132
+ await first;
133
+ const final = views();
134
+ expect(() => JSON.parse(final[0] ?? "")).not.toThrow();
135
+ for (const line of (final[1] ?? "").trim().split("\n")) expect(() => JSON.parse(line)).not.toThrow();
136
+ for (const [index, content] of final.entries()) expect([middleViews[index], newViews[index]]).toContain(content);
137
+ expect(
138
+ readLedger(root, scope)
139
+ .map((entry) => entry.at)
140
+ .slice(-2),
141
+ ).toEqual(["middle", "new"]);
142
+ });
143
+ it("#when (9) a force-recreator loses before its first possible view write #then its successor stays untouched", async () => {
144
+ await createUlwLoopPlan(root, { brief: "old" }, scope);
145
+ let successor: string[] = [];
146
+ let writes = 0;
147
+ const clock = leaseClock();
148
+ await expect(
149
+ withCommitHooks(
150
+ {
151
+ beforeMutation: async () => {
152
+ clock.advance(201);
153
+ const writer = childWriter(root, "force", clock.now());
154
+ expect(await writer.exited).toBe(0);
155
+ successor = views();
156
+ },
157
+ writeView: async (path, content) => {
158
+ writes += 1;
159
+ await writeFile(path, content);
160
+ },
161
+ },
162
+ () =>
163
+ withMutationLockOptions({ clock, leaseMs: 200, heartbeatMs: 0 }, () =>
164
+ createUlwLoopPlan(root, { brief: "A", force: true }, scope),
165
+ ),
166
+ ),
167
+ ).rejects.toMatchObject({ code: "ULW_LOOP_LOCK_LOST" });
168
+ expect(writes).toBe(0);
169
+ expect(views()).toEqual(successor);
170
+ expect(readLedger(root, scope)).toHaveLength(1);
171
+ });
172
+ it("#when a locked ledger-only read follows a crash #then all views are repaired without another revision", async () => {
173
+ const plan = await createUlwLoopPlan(root, { brief: "old" }, scope);
174
+ await expect(
175
+ withCommitHooks(
176
+ {
177
+ afterCommit: () => {
178
+ throw new Error("crash");
179
+ },
180
+ },
181
+ () => commit(root, scope, { plan: { ...plan, brief: "new\n" }, entries: [] }),
182
+ ),
183
+ ).rejects.toThrow("crash");
184
+ await withUlwLoopMutationLock(root, scope, async () => {
185
+ expect(readLedger(root, scope)).toHaveLength(1);
186
+ });
187
+ expect(readFileSync(join(dir, "brief.md"), "utf8")).toBe("new\n");
188
+ expect(readdirSync(join(dir, "revisions"))).toHaveLength(2);
189
+ });
190
+ });