oh-my-opencode 5.0.0-beta.62 → 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 (213) 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 +64 -35
  4. package/dist/cli-node/index.js +64 -35
  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 +2 -0
  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.js +5 -4
  68. package/packages/omo-codex/plugin/components/ulw-loop/dist/cli.js +582 -322
  69. package/packages/omo-codex/plugin/components/ulw-loop/dist/domain-types.d.ts +5 -0
  70. package/packages/omo-codex/plugin/components/ulw-loop/dist/evidence.js +16 -12
  71. package/packages/omo-codex/plugin/components/ulw-loop/dist/ledger.d.ts +4 -0
  72. package/packages/omo-codex/plugin/components/ulw-loop/dist/ledger.js +34 -0
  73. package/packages/omo-codex/plugin/components/ulw-loop/dist/plan-commit.d.ts +19 -0
  74. package/packages/omo-codex/plugin/components/ulw-loop/dist/plan-commit.js +107 -0
  75. package/packages/omo-codex/plugin/components/ulw-loop/dist/plan-crud.d.ts +2 -1
  76. package/packages/omo-codex/plugin/components/ulw-loop/dist/plan-crud.js +48 -22
  77. package/packages/omo-codex/plugin/components/ulw-loop/dist/plan-io.d.ts +9 -7
  78. package/packages/omo-codex/plugin/components/ulw-loop/dist/plan-io.js +105 -114
  79. package/packages/omo-codex/plugin/components/ulw-loop/dist/plan-log.d.ts +15 -0
  80. package/packages/omo-codex/plugin/components/ulw-loop/dist/plan-log.js +86 -0
  81. package/packages/omo-codex/plugin/components/ulw-loop/dist/plan-missing-recovery.d.ts +5 -1
  82. package/packages/omo-codex/plugin/components/ulw-loop/dist/plan-missing-recovery.js +15 -3
  83. package/packages/omo-codex/plugin/components/ulw-loop/dist/review-blockers.js +3 -4
  84. package/packages/omo-codex/plugin/components/ulw-loop/dist/sdk/factory.js +12 -5
  85. package/packages/omo-codex/plugin/components/ulw-loop/dist/spawn-budget-io.d.ts +5 -0
  86. package/packages/omo-codex/plugin/components/ulw-loop/dist/spawn-budget-io.js +67 -0
  87. package/packages/omo-codex/plugin/components/ulw-loop/dist/spawn-guard.js +8 -62
  88. package/packages/omo-codex/plugin/components/ulw-loop/dist/state-lock.d.ts +11 -1
  89. package/packages/omo-codex/plugin/components/ulw-loop/dist/state-lock.js +120 -88
  90. package/packages/omo-codex/plugin/components/ulw-loop/dist/status-next-actions.d.ts +3 -2
  91. package/packages/omo-codex/plugin/components/ulw-loop/dist/status-next-actions.js +18 -10
  92. package/packages/omo-codex/plugin/components/ulw-loop/dist/steering-batch.js +4 -4
  93. package/packages/omo-codex/plugin/components/ulw-loop/dist/steering.js +5 -5
  94. package/packages/omo-codex/plugin/components/ulw-loop/dist/stop-resume-hook.js +8 -17
  95. package/packages/omo-codex/plugin/components/ulw-loop/hooks/hooks.json +5 -5
  96. package/packages/omo-codex/plugin/components/ulw-loop/package.json +1 -1
  97. package/packages/omo-codex/plugin/components/ulw-loop/skills/ulw-loop/references/full-workflow.md +2 -2
  98. package/packages/omo-codex/plugin/components/ulw-loop/src/checkpoint.ts +5 -4
  99. package/packages/omo-codex/plugin/components/ulw-loop/src/domain-types.ts +5 -0
  100. package/packages/omo-codex/plugin/components/ulw-loop/src/evidence.ts +16 -16
  101. package/packages/omo-codex/plugin/components/ulw-loop/src/ledger.ts +33 -0
  102. package/packages/omo-codex/plugin/components/ulw-loop/src/plan-commit.ts +124 -0
  103. package/packages/omo-codex/plugin/components/ulw-loop/src/plan-crud.ts +58 -51
  104. package/packages/omo-codex/plugin/components/ulw-loop/src/plan-io.ts +126 -140
  105. package/packages/omo-codex/plugin/components/ulw-loop/src/plan-log.ts +84 -0
  106. package/packages/omo-codex/plugin/components/ulw-loop/src/plan-missing-recovery.ts +26 -3
  107. package/packages/omo-codex/plugin/components/ulw-loop/src/review-blockers.ts +3 -3
  108. package/packages/omo-codex/plugin/components/ulw-loop/src/sdk/factory.ts +30 -5
  109. package/packages/omo-codex/plugin/components/ulw-loop/src/spawn-budget-io.ts +59 -0
  110. package/packages/omo-codex/plugin/components/ulw-loop/src/spawn-guard.ts +8 -59
  111. package/packages/omo-codex/plugin/components/ulw-loop/src/state-lock.ts +129 -111
  112. package/packages/omo-codex/plugin/components/ulw-loop/src/status-next-actions.ts +19 -10
  113. package/packages/omo-codex/plugin/components/ulw-loop/src/steering-batch.ts +4 -4
  114. package/packages/omo-codex/plugin/components/ulw-loop/src/steering.ts +5 -4
  115. package/packages/omo-codex/plugin/components/ulw-loop/src/stop-resume-hook.ts +8 -16
  116. package/packages/omo-codex/plugin/components/ulw-loop/test/fixtures/barrier.ts +13 -0
  117. package/packages/omo-codex/plugin/components/ulw-loop/test/fixtures/commit-child.ts +65 -0
  118. package/packages/omo-codex/plugin/components/ulw-loop/test/fixtures/commit-cli-qa.mjs +76 -0
  119. package/packages/omo-codex/plugin/components/ulw-loop/test/fixtures/commit-crash.ts +23 -0
  120. package/packages/omo-codex/plugin/components/ulw-loop/test/fixtures/commit-mutations.mjs +58 -0
  121. package/packages/omo-codex/plugin/components/ulw-loop/test/fixtures/commit-writer.ts +65 -0
  122. package/packages/omo-codex/plugin/components/ulw-loop/test/fixtures/lease-clock.ts +23 -0
  123. package/packages/omo-codex/plugin/components/ulw-loop/test/plan-commit-log.test.ts +252 -0
  124. package/packages/omo-codex/plugin/components/ulw-loop/test/plan-commit-recovery.test.ts +190 -0
  125. package/packages/omo-codex/plugin/components/ulw-loop/test/plan-io.test.ts +50 -35
  126. package/packages/omo-codex/plugin/components/ulw-loop/test/plan-log-newest.test.ts +72 -0
  127. package/packages/omo-codex/plugin/components/ulw-loop/test/sdk-completed-plan.test.ts +43 -0
  128. package/packages/omo-codex/plugin/components/ulw-loop/test/sdk-session-context.test.ts +21 -0
  129. package/packages/omo-codex/plugin/components/ulw-loop/test/state-lock.test.ts +92 -0
  130. package/packages/omo-codex/plugin/components/ulw-loop/test/status-next-actions-surface.test.ts +133 -0
  131. package/packages/omo-codex/plugin/components/ulw-loop/test/steering-batch.test.ts +12 -12
  132. package/packages/omo-codex/plugin/components/ulw-loop/test/steering.test.ts +4 -1
  133. package/packages/omo-codex/plugin/hooks/post-compact-resetting-git-bash-mcp-reminder.json +1 -1
  134. package/packages/omo-codex/plugin/hooks/post-compact-resetting-lsp-diagnostics-cache.json +1 -1
  135. package/packages/omo-codex/plugin/hooks/post-compact-resetting-project-rule-cache.json +1 -1
  136. package/packages/omo-codex/plugin/hooks/post-tool-use-checking-comments.json +1 -1
  137. package/packages/omo-codex/plugin/hooks/post-tool-use-checking-lsp-diagnostics.json +1 -1
  138. package/packages/omo-codex/plugin/hooks/post-tool-use-checking-thread-title-hygiene.json +1 -1
  139. package/packages/omo-codex/plugin/hooks/post-tool-use-matching-project-rules.json +1 -1
  140. package/packages/omo-codex/plugin/hooks/post-tool-use-recording-spawn-admission.json +1 -1
  141. package/packages/omo-codex/plugin/hooks/pre-tool-use-enforcing-unlimited-goal-budget.json +1 -1
  142. package/packages/omo-codex/plugin/hooks/pre-tool-use-guarding-ulw-loop-spawns.json +1 -1
  143. package/packages/omo-codex/plugin/hooks/pre-tool-use-recommending-git-bash-mcp.json +1 -1
  144. package/packages/omo-codex/plugin/hooks/session-start-checking-auto-update.json +1 -1
  145. package/packages/omo-codex/plugin/hooks/session-start-checking-bootstrap-provisioning.json +1 -1
  146. package/packages/omo-codex/plugin/hooks/session-start-loading-project-rules.json +1 -1
  147. package/packages/omo-codex/plugin/hooks/session-start-recording-session-telemetry.json +1 -1
  148. package/packages/omo-codex/plugin/hooks/stop-checking-ulw-execute-continuation.json +1 -1
  149. package/packages/omo-codex/plugin/hooks/stop-checking-ulw-loop-resume.json +1 -1
  150. package/packages/omo-codex/plugin/hooks/subagent-stop-verifying-lazycodex-executor-evidence.json +1 -1
  151. package/packages/omo-codex/plugin/hooks/user-prompt-submit-checking-ultrawork-trigger.json +1 -1
  152. package/packages/omo-codex/plugin/hooks/user-prompt-submit-checking-ulw-loop-steering.json +1 -1
  153. package/packages/omo-codex/plugin/hooks/user-prompt-submit-loading-project-rules.json +1 -1
  154. package/packages/omo-codex/plugin/package-lock.json +12 -12
  155. package/packages/omo-codex/plugin/package.json +1 -1
  156. package/packages/omo-codex/plugin/scripts/sync-skills.d.mts +4 -0
  157. package/packages/omo-codex/plugin/scripts/sync-skills.mjs +15 -7
  158. package/packages/omo-codex/plugin/skills/ast-grep/SKILL.md +12 -0
  159. package/packages/omo-codex/plugin/skills/debugging/references/tools/playwright-cli.md +79 -161
  160. package/packages/omo-codex/plugin/skills/frontend/SKILL.md +1 -1
  161. package/packages/omo-codex/plugin/skills/frontend/references/design/aside.md +1 -1
  162. package/packages/omo-codex/plugin/skills/frontend/references/design/clone-from-url.md +1 -1
  163. package/packages/omo-codex/plugin/skills/frontend/scripts/perfection/lighthouse-audit.py +2 -2
  164. package/packages/omo-codex/plugin/skills/review-work/SKILL.md +1 -1
  165. package/packages/omo-codex/plugin/skills/ultimate-browsing/ATTRIBUTION.md +1 -82
  166. package/packages/omo-codex/plugin/skills/ultimate-browsing/SKILL.md +17 -29
  167. package/packages/omo-codex/plugin/skills/ultimate-browsing/engine/AGENTS.md +2 -1
  168. package/packages/omo-codex/plugin/skills/ultimate-browsing/engine/templates/package.json +2 -2
  169. package/packages/omo-codex/plugin/skills/ultimate-browsing/engine/templates/playwright_mobile_chrome.js +5 -2
  170. package/packages/omo-codex/plugin/skills/ultimate-browsing/engine/templates/playwright_real_chrome.js +8 -6
  171. package/packages/omo-codex/plugin/skills/ultimate-browsing/engine/tests/test_playwright_stealth.py +86 -0
  172. package/packages/omo-codex/plugin/skills/ultimate-browsing/engine/tests/test_playwright_templates.py +4 -7
  173. package/packages/omo-codex/plugin/skills/ultimate-browsing/references/chrome-stealth.md +86 -100
  174. package/packages/omo-codex/plugin/skills/ultimate-browsing/references/insane-search/README.md +9 -3
  175. package/packages/omo-codex/plugin/skills/ultimate-browsing/references/insane-search/playwright.md +16 -13
  176. package/packages/omo-codex/plugin/skills/ultimate-browsing/scripts/extract_cookies.py +3 -3
  177. package/packages/omo-codex/plugin/skills/ultrawork/SKILL.md +5 -4
  178. package/packages/omo-codex/plugin/skills/ulw-execute/SKILL.md +1 -1
  179. package/packages/omo-codex/plugin/skills/ulw-loop/references/full-workflow.md +2 -2
  180. package/packages/omo-codex/plugin/skills/ulw-research/SKILL.md +3 -3
  181. package/packages/omo-codex/plugin/skills/visual-qa/AGENTS.md +1 -1
  182. package/packages/omo-codex/plugin/skills/visual-qa/SKILL.md +1 -1
  183. package/packages/omo-codex/plugin/skills/visual-qa/references/browser-setup.md +75 -0
  184. package/packages/omo-codex/scripts/install-dist/install-local.mjs +4 -2
  185. package/packages/prompts-core/prompts/ultrawork/codex.md +5 -4
  186. package/packages/shared-skills/skills/ast-grep/SKILL.md +12 -0
  187. package/packages/shared-skills/skills/debugging/references/tools/playwright-cli.md +79 -161
  188. package/packages/shared-skills/skills/frontend/SKILL.md +1 -1
  189. package/packages/shared-skills/skills/frontend/references/design/aside.md +1 -1
  190. package/packages/shared-skills/skills/frontend/references/design/clone-from-url.md +1 -1
  191. package/packages/shared-skills/skills/frontend/scripts/perfection/lighthouse-audit.py +2 -2
  192. package/packages/shared-skills/skills/review-work/SKILL.md +1 -1
  193. package/packages/shared-skills/skills/ultimate-browsing/ATTRIBUTION.md +1 -82
  194. package/packages/shared-skills/skills/ultimate-browsing/SKILL.md +17 -29
  195. package/packages/shared-skills/skills/ultimate-browsing/engine/AGENTS.md +2 -1
  196. package/packages/shared-skills/skills/ultimate-browsing/engine/templates/package.json +2 -2
  197. package/packages/shared-skills/skills/ultimate-browsing/engine/templates/playwright_mobile_chrome.js +5 -2
  198. package/packages/shared-skills/skills/ultimate-browsing/engine/templates/playwright_real_chrome.js +8 -6
  199. package/packages/shared-skills/skills/ultimate-browsing/engine/tests/test_playwright_stealth.py +86 -0
  200. package/packages/shared-skills/skills/ultimate-browsing/engine/tests/test_playwright_templates.py +4 -7
  201. package/packages/shared-skills/skills/ultimate-browsing/references/chrome-stealth.md +86 -100
  202. package/packages/shared-skills/skills/ultimate-browsing/references/insane-search/README.md +9 -3
  203. package/packages/shared-skills/skills/ultimate-browsing/references/insane-search/playwright.md +16 -13
  204. package/packages/shared-skills/skills/ultimate-browsing/scripts/extract_cookies.py +3 -3
  205. package/packages/shared-skills/skills/ulw-execute/SKILL.md +1 -1
  206. package/packages/shared-skills/skills/ulw-plan/scripts/scaffold-plan.mjs +1 -1
  207. package/packages/shared-skills/skills/ulw-research/SKILL.md +3 -3
  208. package/packages/shared-skills/skills/visual-qa/AGENTS.md +1 -1
  209. package/packages/shared-skills/skills/visual-qa/SKILL.md +1 -1
  210. package/packages/shared-skills/skills/visual-qa/references/browser-setup.md +75 -0
  211. package/dist/skills/visual-qa/references/agent-browser-setup.md +0 -45
  212. package/packages/omo-codex/plugin/skills/visual-qa/references/agent-browser-setup.md +0 -45
  213. package/packages/shared-skills/skills/visual-qa/references/agent-browser-setup.md +0 -45
@@ -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
+ });
@@ -2,7 +2,6 @@ import { existsSync } from "node:fs";
2
2
  import { appendFile, copyFile, mkdir, mkdtemp, readdir, readFile, writeFile } from "node:fs/promises";
3
3
  import { tmpdir } from "node:os";
4
4
  import { join } from "node:path";
5
- import { setImmediate as tick } from "node:timers/promises";
6
5
  import { beforeEach, describe, expect, it } from "vitest";
7
6
  import { ulwLoopDir, ulwLoopGoalsPath, ulwLoopLedgerPath } from "../src/paths.js";
8
7
  import {
@@ -15,6 +14,7 @@ import {
15
14
  } from "../src/plan-io.js";
16
15
  import type { UlwLoopItem, UlwLoopLedgerEntry, UlwLoopPlan, UlwLoopSteeringAudit } from "../src/types.js";
17
16
  import { UlwLoopError } from "../src/types.js";
17
+ import { barrier } from "./fixtures/barrier.js";
18
18
 
19
19
  const NOW = "2026-05-23T00:00:00.000Z";
20
20
  const STABLE_OBJECTIVE =
@@ -97,6 +97,12 @@ async function makeRepo(): Promise<string> {
97
97
  return mkdtemp(join(tmpdir(), "ug-io-"));
98
98
  }
99
99
 
100
+ async function makeLedgerRepo(): Promise<string> {
101
+ const root = await makeRepo();
102
+ await writePlan(root, makePlan());
103
+ return root;
104
+ }
105
+
100
106
  async function writeRawPlan(repoRoot: string, plan: UlwLoopPlan): Promise<void> {
101
107
  await mkdir(ulwLoopDir(repoRoot), { recursive: true });
102
108
  await writeFile(ulwLoopGoalsPath(repoRoot), `${JSON.stringify(plan, null, 2)}\n`, "utf8");
@@ -136,13 +142,17 @@ describe("readUlwLoopPlan", () => {
136
142
  expect(plan.goals[0]?.successCriteria).toHaveLength(3);
137
143
  });
138
144
 
139
- it("migrates legacy aggregate objective on read + writes aggregate_objective_migrated ledger entry + retains alias", async () => {
145
+ it("migrates legacy aggregate objective in memory and folds its audit into the enclosing commit", async () => {
140
146
  // given
141
147
  const legacyObjective = "Complete all ulw-loop stories in .omo/ulw-loop/goals.json: G001 Build auth service";
142
148
  await writeRawPlan(repoRoot, makePlan({ codexObjective: legacyObjective }));
143
149
 
144
150
  // when: read inside the mutation lock, the way every mutating command does
145
- const plan = await withUlwLoopMutationLock(repoRoot, () => readUlwLoopPlan(repoRoot));
151
+ const plan = await withUlwLoopMutationLock(repoRoot, async () => {
152
+ const migrated = await readUlwLoopPlan(repoRoot);
153
+ await writePlan(repoRoot, migrated);
154
+ return migrated;
155
+ });
146
156
 
147
157
  // then
148
158
  expect(plan.codexObjective).toBe(STABLE_OBJECTIVE);
@@ -157,22 +167,17 @@ describe("readUlwLoopPlan", () => {
157
167
  });
158
168
  });
159
169
 
160
- it("readUlwLoopPlan refuses to migrate a legacy aggregate plan outside the mutation lock (no unlocked write on a read path)", async () => {
170
+ it("readUlwLoopPlan migrates a legacy aggregate plan in memory outside the mutation lock without writing", async () => {
161
171
  // given: the same legacy plan, but the read is not wrapped in withUlwLoopMutationLock
162
172
  const legacyObjective = "Complete all ulw-loop stories in .omo/ulw-loop/goals.json: G001 Build auth service";
163
173
  await writeRawPlan(repoRoot, makePlan({ codexObjective: legacyObjective }));
164
174
 
165
175
  // when
166
- let caught: unknown;
167
- try {
168
- await readUlwLoopPlan(repoRoot);
169
- } catch (error) {
170
- caught = error;
171
- }
172
-
173
- // then: it refuses with a recovery hint and writes nothing
174
- expect(caught).toBeInstanceOf(UlwLoopError);
175
- expect((caught as UlwLoopError).code).toBe("ULW_LOOP_MIGRATION_REQUIRED");
176
+ const migrated = await readUlwLoopPlan(repoRoot);
177
+
178
+ // then: only the returned shape changes
179
+ expect(migrated.codexObjective).toBe(STABLE_OBJECTIVE);
180
+ expect(migrated.codexObjectiveAliases).toContain(legacyObjective);
176
181
  const persisted = JSON.parse(await readFile(ulwLoopGoalsPath(repoRoot), "utf8"));
177
182
  expect(persisted.codexObjective).toBe(legacyObjective);
178
183
  expect(existsSync(ulwLoopLedgerPath(repoRoot))).toBe(false);
@@ -211,7 +216,8 @@ describe("writePlan", () => {
211
216
  await writePlan(repoRoot, makePlan({ codexObjective: "first" }));
212
217
 
213
218
  // when
214
- await writePlan(repoRoot, makePlan({ codexObjective: "second" }));
219
+ const current = await readUlwLoopPlan(repoRoot);
220
+ await writePlan(repoRoot, { ...current, codexObjective: "second" });
215
221
 
216
222
  // then
217
223
  expect(JSON.parse(await readFile(ulwLoopGoalsPath(repoRoot), "utf8"))).toMatchObject({
@@ -223,7 +229,7 @@ describe("writePlan", () => {
223
229
  describe("appendLedger", () => {
224
230
  it("appends a single JSONL line to ledger.jsonl", async () => {
225
231
  // given
226
- const repoRoot = await makeRepo();
232
+ const repoRoot = await makeLedgerRepo();
227
233
  const ledgerEntry = entry("goal_started");
228
234
 
229
235
  // when
@@ -236,6 +242,7 @@ describe("appendLedger", () => {
236
242
  it("creates ledger.jsonl if missing", async () => {
237
243
  // given
238
244
  const repoRoot = await makeRepo();
245
+ await writeRawPlan(repoRoot, makePlan());
239
246
 
240
247
  // when
241
248
  await appendLedger(repoRoot, entry("goal_completed"));
@@ -246,7 +253,7 @@ describe("appendLedger", () => {
246
253
 
247
254
  it("preserves prior entries", async () => {
248
255
  // given
249
- const repoRoot = await makeRepo();
256
+ const repoRoot = await makeLedgerRepo();
250
257
  const first = entry("goal_started");
251
258
  const second = entry("goal_completed");
252
259
 
@@ -262,7 +269,7 @@ describe("appendLedger", () => {
262
269
  describe("readSteeringLedgerEntries", () => {
263
270
  it("returns only steering-related event kinds", async () => {
264
271
  // given
265
- const repoRoot = await makeRepo();
272
+ const repoRoot = await makeLedgerRepo();
266
273
  await appendLedger(repoRoot, entry("steering_accepted"));
267
274
  await appendLedger(repoRoot, entry("goal_started"));
268
275
  await appendLedger(repoRoot, entry("steering_rejected"));
@@ -297,7 +304,7 @@ describe("findAcceptedSteeringLedgerEntry", () => {
297
304
  ],
298
305
  ] as const)("finds an accepted entry by %s", async (_name, target) => {
299
306
  // given
300
- const repoRoot = await makeRepo();
307
+ const repoRoot = await makeLedgerRepo();
301
308
  await appendLedger(repoRoot, steeringEntry("steering_accepted", steeringAudit({ idempotencyKey: "other-key" })));
302
309
  await appendLedger(repoRoot, target);
303
310
 
@@ -310,7 +317,7 @@ describe("findAcceptedSteeringLedgerEntry", () => {
310
317
 
311
318
  it("skips a rejected entry carrying the same key", async () => {
312
319
  // given
313
- const repoRoot = await makeRepo();
320
+ const repoRoot = await makeLedgerRepo();
314
321
  await appendLedger(
315
322
  repoRoot,
316
323
  steeringEntry("steering_rejected", rejectedSteeringAudit({ idempotencyKey: "needle-key" })),
@@ -322,7 +329,7 @@ describe("findAcceptedSteeringLedgerEntry", () => {
322
329
 
323
330
  it("returns the accepted entry even when a rejected one with the same key precedes it", async () => {
324
331
  // given
325
- const repoRoot = await makeRepo();
332
+ const repoRoot = await makeLedgerRepo();
326
333
  const accepted = steeringEntry("steering_accepted", steeringAudit({ idempotencyKey: "needle-key" }));
327
334
  await appendLedger(
328
335
  repoRoot,
@@ -344,7 +351,7 @@ describe("findAcceptedSteeringLedgerEntry", () => {
344
351
 
345
352
  it("ignores the key when it only appears in non-steering entries", async () => {
346
353
  // given
347
- const repoRoot = await makeRepo();
354
+ const repoRoot = await makeLedgerRepo();
348
355
  await appendLedger(repoRoot, { at: NOW, kind: "goal_completed", goalId: "G001", idempotencyKey: "needle-key" });
349
356
  await appendLedger(repoRoot, { at: NOW, kind: "evidence_captured", goalId: "G001", evidence: "needle-key" });
350
357
 
@@ -354,7 +361,7 @@ describe("findAcceptedSteeringLedgerEntry", () => {
354
361
 
355
362
  it("ignores an accepted entry whose key only appears in evidence text", async () => {
356
363
  // given
357
- const repoRoot = await makeRepo();
364
+ const repoRoot = await makeLedgerRepo();
358
365
  await appendLedger(repoRoot, steeringEntry("steering_accepted", steeringAudit({ evidence: "needle-key" })));
359
366
 
360
367
  // when/then
@@ -440,13 +447,13 @@ describe("withUlwLoopMutationLock", () => {
440
447
  const body = async (): Promise<void> => {
441
448
  active += 1;
442
449
  maxActive = Math.max(maxActive, active);
443
- await tick();
450
+ await Promise.resolve();
444
451
  active -= 1;
445
452
  };
446
453
 
447
454
  // when
448
455
  await Promise.all([1, 2, 3].map(() => withUlwLoopMutationLock(repoRoot, body)));
449
- await tick();
456
+ await Promise.resolve();
450
457
  await Promise.all([1, 2].map(() => withUlwLoopMutationLock(repoRoot, body)));
451
458
 
452
459
  // then
@@ -457,7 +464,8 @@ describe("withUlwLoopMutationLock", () => {
457
464
  // given
458
465
  const repoRoot = await makeRepo();
459
466
  const events: string[] = [];
460
- let releaseSecond = false;
467
+ const releaseSecond = barrier();
468
+ const enteredSecond = barrier();
461
469
 
462
470
  // when
463
471
  const first = withUlwLoopMutationLock(repoRoot, async () => {
@@ -465,15 +473,16 @@ describe("withUlwLoopMutationLock", () => {
465
473
  });
466
474
  const second = withUlwLoopMutationLock(repoRoot, async () => {
467
475
  events.push("second:start");
468
- while (!releaseSecond) await tick();
476
+ enteredSecond.resolve();
477
+ await releaseSecond.promise;
469
478
  events.push("second:end");
470
479
  });
471
480
  await first;
472
- await tick();
481
+ await enteredSecond.promise;
473
482
  const third = withUlwLoopMutationLock(repoRoot, async () => {
474
483
  events.push("third");
475
484
  });
476
- releaseSecond = true;
485
+ releaseSecond.resolve();
477
486
  await Promise.all([second, third]);
478
487
 
479
488
  // then
@@ -485,18 +494,21 @@ describe("withUlwLoopMutationLock", () => {
485
494
  const rootA = await makeRepo();
486
495
  const rootB = await makeRepo();
487
496
  const events: string[] = [];
488
- let releaseA = false;
497
+ const releaseA = barrier();
498
+ const enteredA = barrier();
489
499
 
490
500
  // when
491
501
  const held = withUlwLoopMutationLock(rootA, async () => {
492
502
  events.push("a:start");
493
- while (!releaseA) await tick();
503
+ enteredA.resolve();
504
+ await releaseA.promise;
494
505
  events.push("a:end");
495
506
  });
507
+ await enteredA.promise;
496
508
  await withUlwLoopMutationLock(rootB, async () => {
497
509
  events.push("b");
498
510
  });
499
- releaseA = true;
511
+ releaseA.resolve();
500
512
  await held;
501
513
 
502
514
  // then
@@ -508,21 +520,24 @@ describe("withUlwLoopMutationLock", () => {
508
520
  const repoRoot = await makeRepo();
509
521
  const scoped = { sessionId: "session-a" };
510
522
  const events: string[] = [];
511
- let releaseScoped = false;
523
+ const releaseScoped = barrier();
524
+ const enteredScoped = barrier();
512
525
 
513
526
  // when
514
527
  const heldScoped = withUlwLoopMutationLock(repoRoot, scoped, async () => {
515
528
  events.push("scoped-1:start");
516
- while (!releaseScoped) await tick();
529
+ enteredScoped.resolve();
530
+ await releaseScoped.promise;
517
531
  events.push("scoped-1:end");
518
532
  });
533
+ await enteredScoped.promise;
519
534
  const queuedScoped = withUlwLoopMutationLock(repoRoot, scoped, async () => {
520
535
  events.push("scoped-2");
521
536
  });
522
537
  await withUlwLoopMutationLock(repoRoot, async () => {
523
538
  events.push("root-scope");
524
539
  });
525
- releaseScoped = true;
540
+ releaseScoped.resolve();
526
541
  await Promise.all([heldScoped, queuedScoped]);
527
542
 
528
543
  // then
@@ -0,0 +1,72 @@
1
+ import { mkdirSync, rmSync, 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 { readNewestRecord, readRecords, reconcilePlan } from "../src/plan-log.js";
9
+ import type { UlwLoopPlan } from "../src/types.js";
10
+
11
+ let root: string;
12
+ let dir: string;
13
+ let base: UlwLoopPlan;
14
+ const scope = { sessionId: "newest-record" };
15
+ beforeEach(async () => {
16
+ root = await mkdtemp(join(tmpdir(), "ulw-newest-"));
17
+ dir = ulwLoopDir(root, scope);
18
+ base = await createUlwLoopPlan(root, { brief: "initial" }, scope);
19
+ });
20
+ afterEach(async () => {
21
+ await rm(root, { recursive: true, force: true });
22
+ });
23
+ // The names are deliberately mixed-width: lexicographically "000000010.json" sorts BEFORE "00000009.json",
24
+ // so only numeric ordering of the parsed revision picks revision 10 as the newest record.
25
+ function seedRecord(name: string, revision: number, brief: string): UlwLoopPlan {
26
+ const plan: UlwLoopPlan = { ...base, revision, brief };
27
+ mkdirSync(join(dir, "revisions"), { recursive: true });
28
+ writeFileSync(
29
+ join(dir, "revisions", name),
30
+ `${JSON.stringify({ version: 1, revision, plan, ledger: [{ at: "now", kind: "goal_added", revision, id: `${revision}-0` }] })}\n`,
31
+ );
32
+ return plan;
33
+ }
34
+
35
+ describe("#given a commit log whose newest record is the only one a plan read needs", () => {
36
+ it("#when the newest record is valid #then it wins over older records and a stale goals cache", async () => {
37
+ seedRecord("00000009.json", 9, "nine");
38
+ const newest = seedRecord("000000010.json", 10, "ten");
39
+
40
+ expect(base.revision).toBe(1);
41
+ expect(readNewestRecord(dir)?.revision).toBe(10);
42
+ expect(reconcilePlan(dir)).toEqual(newest);
43
+ });
44
+ it("#when the newest record is corrupt #then the newest valid older record wins", () => {
45
+ const older = seedRecord("00000009.json", 9, "nine");
46
+ writeFileSync(join(dir, "revisions", "000000010.json"), "{not json");
47
+
48
+ expect(readNewestRecord(dir)?.revision).toBe(9);
49
+ expect(reconcilePlan(dir)).toEqual(older);
50
+ });
51
+ it("#when an OLDER record is unreadable for a non-syntax reason #then the read stops at the newest record", () => {
52
+ const newest = seedRecord("000000010.json", 10, "ten");
53
+ rmSync(join(dir, "revisions", "00000001.json"));
54
+ mkdirSync(join(dir, "revisions", "00000001.json"));
55
+
56
+ expect(reconcilePlan(dir)).toEqual(newest);
57
+ expect(readNewestRecord(dir)).toEqual({
58
+ version: 1,
59
+ revision: 10,
60
+ plan: newest,
61
+ ledger: [{ at: "now", kind: "goal_added", revision: 10, id: "10-0" }],
62
+ });
63
+ expect(() => readRecords(dir)).toThrow(/EISDIR/);
64
+ });
65
+ it("#when the NEWEST record is unreadable for a non-syntax reason #then the error propagates", () => {
66
+ seedRecord("00000009.json", 9, "nine");
67
+ mkdirSync(join(dir, "revisions", "000000010.json"));
68
+
69
+ expect(() => readNewestRecord(dir)).toThrow(/EISDIR/);
70
+ expect(() => reconcilePlan(dir)).toThrow(/EISDIR/);
71
+ });
72
+ });
@@ -0,0 +1,43 @@
1
+ import { mkdtemp, rm } from "node:fs/promises";
2
+ import { tmpdir } from "node:os";
3
+ import { join } from "node:path";
4
+ import { afterEach, describe, expect, it } from "vitest";
5
+ import { commit } from "../src/plan-commit.js";
6
+ import { readUlwLoopPlan, withUlwLoopMutationLock } from "../src/plan-io.js";
7
+ import { createAgentToolkit } from "../src/sdk.js";
8
+ import type { UlwLoopToolkitSurface } from "../src/surface.js";
9
+
10
+ const directories: string[] = [];
11
+ afterEach(async () => {
12
+ for (const directory of directories.splice(0)) await rm(directory, { recursive: true, force: true });
13
+ });
14
+
15
+ const surfaces: UlwLoopToolkitSurface[] = ["lazycodex", "omo-senpi"];
16
+ describe.each(surfaces)("#given a completed plan on %s", (surface) => {
17
+ it("#when createGoals is called #then it rejects without overwriting, unless explicitly forced", async () => {
18
+ const cwd = await mkdtemp(join(tmpdir(), "sdk-completed-plan-"));
19
+ directories.push(cwd);
20
+ const scope = { sessionId: "completed-plan" };
21
+ const toolkit = createAgentToolkit({ cwd, ...scope, surface });
22
+ expect((await toolkit.createGoals({ brief: "- original" })).ok).toBe(true);
23
+ await withUlwLoopMutationLock(cwd, scope, async () => {
24
+ const plan = await readUlwLoopPlan(cwd, scope);
25
+ for (const goal of plan.goals) goal.status = "complete";
26
+ await commit(cwd, scope, { plan, entries: [] });
27
+ });
28
+ const before = await readUlwLoopPlan(cwd, scope);
29
+ const response = await toolkit.createGoals({ brief: "- replacement" });
30
+ expect(response).toMatchObject({ ok: false, error: { code: "ULW_LOOP_PLAN_EXISTS_COMPLETE" } });
31
+ if (response.ok) throw new Error("expected createGoals to fail");
32
+ if (surface === "omo-senpi") {
33
+ expect(response.error.message).toMatch(/agentToolkit\.createGoals\(/);
34
+ expect(response.error.message).not.toMatch(/omo-agent-toolkit ulw-loop/);
35
+ } else {
36
+ expect(response.error.message).toMatch(/omo-agent-toolkit ulw-loop create-goals/);
37
+ expect(response.error.message).not.toMatch(/agentToolkit\./);
38
+ }
39
+ expect(await readUlwLoopPlan(cwd, scope)).toEqual(before);
40
+ expect((await toolkit.createGoals({ brief: "- replacement", force: true })).ok).toBe(true);
41
+ expect((await readUlwLoopPlan(cwd, scope)).goals[0]?.id).toBe("G001-replacement");
42
+ });
43
+ });
@@ -72,6 +72,27 @@ async function seedSession(
72
72
  }
73
73
 
74
74
  describe("agent toolkit session context", () => {
75
+ describe("#given a session id that normalizes to null", () => {
76
+ it("#when an omo-senpi SDK context is created #then it rejects without creating session state", async () => {
77
+ const cwd = await makeWorkdir();
78
+
79
+ expect(() => createAgentToolkit({ cwd, sessionId: "../../x", surface: "omo-senpi" })).toThrowError(
80
+ expect.objectContaining({ code: "ULW_LOOP_SESSION_ID_INVALID" }),
81
+ );
82
+ expect(existsSync(join(cwd, ".omo", "ulw-loop"))).toBe(false);
83
+ });
84
+ });
85
+
86
+ describe("#given a blank session id", () => {
87
+ it("#when an SDK context is created #then it reports the required-id error", async () => {
88
+ const cwd = await makeWorkdir();
89
+
90
+ expect(() => createAgentToolkit({ cwd, sessionId: " ", surface: "omo-senpi" })).toThrowError(
91
+ expect.objectContaining({ code: "ULW_LOOP_SESSION_ID_REQUIRED" }),
92
+ );
93
+ });
94
+ });
95
+
75
96
  describe("#given a plan written under session A", () => {
76
97
  it("#when session B reads through its own context #then the read fails closed instead of borrowing A", async () => {
77
98
  const cwd = await makeWorkdir();