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
@@ -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
+ });
@@ -0,0 +1,145 @@
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
+
6
+ import { readUlwLoopPlan } from "../src/plan-io.js";
7
+ import { createAgentToolkit } from "../src/sdk.js";
8
+
9
+ const workDirs: string[] = [];
10
+
11
+ afterEach(async () => {
12
+ for (const dir of workDirs.splice(0)) await rm(dir, { recursive: true, force: true });
13
+ });
14
+
15
+ interface Seeded {
16
+ readonly cwd: string;
17
+ readonly sessionId: string;
18
+ readonly goalId: string;
19
+ readonly objective: string;
20
+ }
21
+
22
+ // One fixture shape for every driver state: a real plan whose first goal is ready to close, so the
23
+ // only thing under test is what the driver snapshot does to the checkpoint.
24
+ async function seed(sessionId: string): Promise<Seeded> {
25
+ const cwd = await mkdtemp(join(tmpdir(), "ulw-driver-"));
26
+ workDirs.push(cwd);
27
+ const toolkit = createAgentToolkit({ cwd, sessionId, surface: "omo-senpi" });
28
+ const created = await toolkit.createGoals({ brief: "- alpha goal\n- beta goal" });
29
+ if (!created.ok) throw new Error("fixture could not create goals");
30
+ const started = await toolkit.completeGoals({});
31
+ if (!started.ok || started.operation !== "complete-goals" || "done" in started.result)
32
+ throw new Error("fixture could not start a goal");
33
+ const goalId = started.result.goal.id;
34
+ const criteria = await toolkit.criteria({ goalId });
35
+ if (!criteria.ok || criteria.operation !== "criteria") throw new Error("fixture has no criteria");
36
+ for (const criterion of criteria.result.criteria) {
37
+ const recorded = await toolkit.recordEvidence({
38
+ goalId,
39
+ criterionId: criterion.id,
40
+ status: "pass",
41
+ evidence: "driver-state fixture proof",
42
+ });
43
+ if (!recorded.ok) throw new Error("fixture could not record evidence");
44
+ }
45
+ const plan = await readUlwLoopPlan(cwd, { sessionId });
46
+ return { cwd, sessionId, goalId, objective: plan.codexObjective ?? "" };
47
+ }
48
+
49
+ async function closeWith(seeded: Seeded, snapshot?: Record<string, unknown>) {
50
+ const toolkit = createAgentToolkit({ cwd: seeded.cwd, sessionId: seeded.sessionId, surface: "omo-senpi" });
51
+ return toolkit.checkpoint({
52
+ goalId: seeded.goalId,
53
+ status: "complete",
54
+ evidence: "driver-state checkpoint",
55
+ ...(snapshot === undefined ? {} : { codexGoalJson: JSON.stringify(snapshot) }),
56
+ });
57
+ }
58
+
59
+ describe("SDK driver-goal states", () => {
60
+ describe("#given no driver snapshot", () => {
61
+ it("#when the goal is checkpointed #then it completes and advises creating the driver goal", async () => {
62
+ const seeded = await seed("driver-absent");
63
+
64
+ const closed = await closeWith(seeded);
65
+
66
+ expect(closed.ok).toBe(true);
67
+ if (closed.ok) expect(closed.nextActions.join(" ")).toContain("create_goal");
68
+ });
69
+ });
70
+
71
+ describe("#given an active driver carrying the plan objective", () => {
72
+ it("#when the goal is checkpointed #then it completes with no objective difference reported", async () => {
73
+ const seeded = await seed("driver-active");
74
+
75
+ const closed = await closeWith(seeded, { goal: { objective: seeded.objective, status: "active" } });
76
+
77
+ expect(closed.ok).toBe(true);
78
+ if (closed.ok) {
79
+ expect([...(closed.warnings ?? []), ...closed.nextActions].join(" ")).not.toContain(
80
+ "driver_objective_differs",
81
+ );
82
+ }
83
+ });
84
+ });
85
+
86
+ describe("#given a driver completed before the plan finished", () => {
87
+ it("#when the goal is checkpointed #then it completes and advises re-creating the driver", async () => {
88
+ const seeded = await seed("driver-early");
89
+
90
+ const closed = await closeWith(seeded, { goal: { objective: seeded.objective, status: "complete" } });
91
+
92
+ expect(closed.ok).toBe(true);
93
+ if (closed.ok) expect(closed.nextActions.join(" ")).toContain("create_goal");
94
+ });
95
+ });
96
+
97
+ describe("#given a driver the harness has limited", () => {
98
+ for (const status of ["paused", "usage_limited", "budget_limited"] as const) {
99
+ it(`#when the driver is ${status} #then the checkpoint still completes and advises resuming it`, async () => {
100
+ const seeded = await seed(`driver-${status}`);
101
+
102
+ const closed = await closeWith(seeded, { goal: { objective: seeded.objective, status } });
103
+
104
+ expect(closed.ok).toBe(true);
105
+ if (closed.ok) expect(closed.nextActions.join(" ")).toContain("/goal resume");
106
+ });
107
+ }
108
+ });
109
+
110
+ describe("#given a driver whose objective differs from the plan", () => {
111
+ it("#when the goal is checkpointed #then the difference is reported, never a rejection", async () => {
112
+ const seeded = await seed("driver-differs");
113
+
114
+ const closed = await closeWith(seeded, { goal: { objective: "a different objective", status: "active" } });
115
+
116
+ expect(closed.ok).toBe(true);
117
+ if (closed.ok) {
118
+ expect([...(closed.warnings ?? []), ...closed.nextActions].join(" ")).toContain("driver_objective_differs");
119
+ }
120
+ });
121
+ });
122
+
123
+ describe("#given snapshot input that is neither JSON nor a readable path", () => {
124
+ it("#when the goal is checkpointed #then the typed error is returned and the goal stays open", async () => {
125
+ const seeded = await seed("driver-malformed");
126
+
127
+ const closed = await closeWith(seeded);
128
+ expect(closed.ok).toBe(true);
129
+
130
+ const second = await seed("driver-malformed-2");
131
+ const toolkit = createAgentToolkit({ cwd: second.cwd, sessionId: second.sessionId, surface: "omo-senpi" });
132
+ const rejected = await toolkit.checkpoint({
133
+ goalId: second.goalId,
134
+ status: "complete",
135
+ evidence: "driver-state checkpoint",
136
+ codexGoalJson: "not json and not a path",
137
+ });
138
+
139
+ expect(rejected.ok).toBe(false);
140
+ if (!rejected.ok) expect(rejected.error.code).toBe("ULW_LOOP_CODEX_GOAL_JSON_INVALID");
141
+ const plan = await readUlwLoopPlan(second.cwd, { sessionId: second.sessionId });
142
+ expect(plan.goals.find((goal) => goal.id === second.goalId)?.status).not.toBe("complete");
143
+ });
144
+ });
145
+ });
@@ -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();
@@ -6,7 +6,13 @@ import { Readable, Writable } from "node:stream";
6
6
  import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
7
7
 
8
8
  import type { PreToolUsePayload } from "../src/codex-hook.ts";
9
- import { applySpawnGuards, DEFAULT_FANOUT_LIMIT, runSpawnAdmissionRecorderCli } from "../src/spawn-guard.ts";
9
+ // Budget/artifact behavior is independent of role admission; the native entry
10
+ // point and its composition are covered in spawn-role-matrix.test.ts.
11
+ import {
12
+ applySpawnBudgetGuards as applySpawnGuards,
13
+ DEFAULT_FANOUT_LIMIT,
14
+ runSpawnAdmissionRecorderCli,
15
+ } from "../src/spawn-guard.ts";
10
16
 
11
17
  let workDir: string;
12
18
  let originalLimit: string | undefined;