oh-my-opencode 4.9.1 → 4.10.0

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 (220) hide show
  1. package/.agents/skills/opencode-qa/SKILL.md +1 -0
  2. package/.agents/skills/opencode-qa/scripts/lib/common.sh +39 -1
  3. package/.agents/skills/opencode-qa/scripts/lib/fake-openai-branches.mjs +39 -0
  4. package/.agents/skills/opencode-qa/scripts/lib/fake-openai-events.mjs +106 -0
  5. package/.agents/skills/opencode-qa/scripts/lib/fake-openai-server.mjs +117 -0
  6. package/.agents/skills/opencode-qa/scripts/serve-wake-split-probe.sh +716 -0
  7. package/.agents/skills/tech-debt-audit/SKILL.md +277 -0
  8. package/.agents/skills/work-with-pr-workspace/iteration-1/eval-5/with_skill/outputs/execution-plan.md +1 -1
  9. package/.opencode/skills/work-with-pr-workspace/iteration-1/eval-5/with_skill/outputs/execution-plan.md +1 -1
  10. package/bin/platform.js +5 -0
  11. package/bin/platform.test.ts +56 -0
  12. package/dist/agents/atlas/agent.d.ts +4 -3
  13. package/dist/agents/gpt-apply-patch-guard.d.ts +2 -2
  14. package/dist/agents/hephaestus/agent.d.ts +5 -0
  15. package/dist/agents/hephaestus/index.d.ts +1 -1
  16. package/dist/agents/metis.d.ts +1 -0
  17. package/dist/agents/prometheus/system-prompt.d.ts +1 -1
  18. package/dist/agents/sisyphus/kimi-k2-7.d.ts +17 -0
  19. package/dist/agents/sisyphus-junior/agent.d.ts +1 -1
  20. package/dist/agents/sisyphus-junior/kimi-k2-7.d.ts +11 -0
  21. package/dist/agents/types.d.ts +2 -2
  22. package/dist/cli/doctor/checks/codex-components.d.ts +13 -0
  23. package/dist/cli/doctor/checks/tui-plugin-config.d.ts +1 -0
  24. package/dist/cli/doctor/constants.d.ts +1 -1
  25. package/dist/cli/index.js +32329 -31437
  26. package/dist/cli/install-codex/codex-cleanup.d.ts +4 -0
  27. package/dist/cli/install-codex/install-codex-test-fixtures.d.ts +34 -0
  28. package/dist/cli/install-codex/link-cached-plugin-agents.d.ts +4 -0
  29. package/dist/cli/model-fallback.d.ts +1 -0
  30. package/dist/cli/provider-availability.d.ts +2 -0
  31. package/dist/cli-node/index.js +32329 -31437
  32. package/dist/config/schema/agent-overrides.d.ts +80 -16
  33. package/dist/config/schema/experimental.d.ts +1 -1
  34. package/dist/config/schema/hooks.d.ts +0 -1
  35. package/dist/config/schema/internal/permission.d.ts +5 -1
  36. package/dist/config/schema/oh-my-opencode-config.d.ts +76 -16
  37. package/dist/create-hooks.d.ts +0 -1
  38. package/dist/features/background-agent/index.d.ts +1 -1
  39. package/dist/features/background-agent/manager.d.ts +6 -0
  40. package/dist/features/background-agent/types.d.ts +2 -0
  41. package/dist/features/claude-code-plugin-loader/types.d.ts +3 -0
  42. package/dist/features/claude-code-session-state/state.d.ts +1 -0
  43. package/dist/features/skill-mcp-manager/manager.d.ts +11 -7
  44. package/dist/features/team-mode/team-mailbox/pending-delivery-recovery.d.ts +31 -0
  45. package/dist/features/team-mode/team-runtime/delete-team.d.ts +2 -1
  46. package/dist/features/team-mode/tools/lifecycle-inline-spec.d.ts +2 -2
  47. package/dist/features/tmux-subagent/stale-tmux-resource-sweeper.d.ts +12 -0
  48. package/dist/features/tool-metadata-store/store.d.ts +5 -0
  49. package/dist/hooks/anthropic-context-window-limit-recovery/storage/constants.d.ts +3 -0
  50. package/dist/hooks/{session-recovery → anthropic-context-window-limit-recovery}/storage/messages-reader.d.ts +1 -1
  51. package/dist/hooks/{session-recovery → anthropic-context-window-limit-recovery}/storage/part-content.d.ts +1 -1
  52. package/dist/hooks/{session-recovery → anthropic-context-window-limit-recovery}/storage/parts-reader.d.ts +1 -1
  53. package/dist/hooks/{session-recovery → anthropic-context-window-limit-recovery/storage}/types.d.ts +0 -13
  54. package/dist/hooks/auto-update-checker/checker/bundled-version.d.ts +1 -0
  55. package/dist/hooks/auto-update-checker/checker.d.ts +1 -0
  56. package/dist/hooks/auto-update-checker/constants.d.ts +3 -3
  57. package/dist/hooks/auto-update-checker/hook.d.ts +2 -1
  58. package/dist/hooks/claude-code-hooks/types.d.ts +4 -0
  59. package/dist/hooks/index.d.ts +0 -1
  60. package/dist/hooks/team-session-events/team-idle-wake-hint.d.ts +5 -0
  61. package/dist/index.js +6061 -3714
  62. package/dist/oh-my-opencode.schema.json +123 -18
  63. package/dist/plugin/build-team-idle-wake-hint-client.d.ts +2 -0
  64. package/dist/plugin/event-session-lifecycle.d.ts +0 -3
  65. package/dist/plugin/hooks/create-continuation-hooks.d.ts +0 -6
  66. package/dist/plugin/hooks/create-core-hooks.d.ts +0 -1
  67. package/dist/plugin/hooks/create-session-hooks.d.ts +1 -2
  68. package/dist/shared/command-executor/execute-hook-command.d.ts +7 -0
  69. package/dist/shared/internal-initiator-marker.d.ts +7 -0
  70. package/dist/shared/live-server-route.d.ts +24 -0
  71. package/dist/shared/plugin-identity.d.ts +2 -2
  72. package/dist/shared/prompt-async-gate/prompt-message-state.d.ts +1 -0
  73. package/dist/shared/tmux/tmux-utils/server-health.d.ts +2 -1
  74. package/dist/shared/tmux/tmux-utils/stale-attach-pane-sweep.d.ts +16 -0
  75. package/dist/shared/tmux/tmux-utils.d.ts +1 -0
  76. package/dist/testing/create-plugin-module.d.ts +4 -0
  77. package/dist/tools/background-task/clients.d.ts +2 -0
  78. package/dist/tools/background-task/full-session-format.d.ts +1 -0
  79. package/dist/tools/background-task/types.d.ts +1 -0
  80. package/dist/tools/delegate-task/sync-prompt-sender.d.ts +1 -1
  81. package/dist/tools/delegate-task/sync-session-lifecycle.d.ts +2 -1
  82. package/dist/tools/look-at/look-at-input-preparer.d.ts +6 -2
  83. package/dist/tools/look-at/look-at-prompt.d.ts +2 -1
  84. package/dist/tools/look-at/look-at-session-runner.d.ts +3 -4
  85. package/dist/tools/look-at/types.d.ts +2 -0
  86. package/dist/tools/session-manager/types.d.ts +1 -0
  87. package/dist/tools/skill-mcp/types.d.ts +1 -0
  88. package/package.json +14 -13
  89. package/packages/ast-grep-mcp/dist/cli.js +50 -17
  90. package/packages/lsp-daemon/dist/cli.js +8 -5
  91. package/packages/lsp-daemon/dist/index.js +8 -5
  92. package/packages/lsp-tools-mcp/dist/lsp/connection.js +1 -1
  93. package/packages/lsp-tools-mcp/dist/lsp/server-definitions.js +2 -2
  94. package/packages/lsp-tools-mcp/dist/lsp/transport.d.ts +10 -1
  95. package/packages/lsp-tools-mcp/dist/lsp/transport.js +6 -3
  96. package/packages/omo-codex/lazycodex-repository/.github/workflows/pr-source-guidance.yml +11 -12
  97. package/packages/omo-codex/plugin/.codex-plugin/plugin.json +1 -1
  98. package/packages/omo-codex/plugin/components/bootstrap/dist/cli.js +2583 -0
  99. package/packages/omo-codex/plugin/components/bootstrap/hooks/hooks.json +17 -0
  100. package/packages/omo-codex/plugin/components/bootstrap/manifests/ast-grep.json +22 -0
  101. package/packages/omo-codex/plugin/components/bootstrap/manifests/node.json +10 -0
  102. package/packages/omo-codex/plugin/components/bootstrap/package.json +20 -0
  103. package/packages/omo-codex/plugin/components/bootstrap/scripts/bootstrap.ps1 +310 -0
  104. package/packages/omo-codex/plugin/components/bootstrap/scripts/build.mjs +35 -0
  105. package/packages/omo-codex/plugin/components/bootstrap/scripts/generate-manifests.mjs +115 -0
  106. package/packages/omo-codex/plugin/components/bootstrap/src/cli.ts +153 -0
  107. package/packages/omo-codex/plugin/components/bootstrap/src/download.ts +212 -0
  108. package/packages/omo-codex/plugin/components/bootstrap/src/environment.ts +286 -0
  109. package/packages/omo-codex/plugin/components/bootstrap/src/hook.ts +108 -0
  110. package/packages/omo-codex/plugin/components/bootstrap/src/provision.ts +243 -0
  111. package/packages/omo-codex/plugin/components/bootstrap/src/setup.ts +294 -0
  112. package/packages/omo-codex/plugin/components/bootstrap/src/worker.ts +279 -0
  113. package/packages/omo-codex/plugin/components/bootstrap/test/download.test.ts +295 -0
  114. package/packages/omo-codex/plugin/components/bootstrap/test/environment.test.ts +375 -0
  115. package/packages/omo-codex/plugin/components/bootstrap/test/provision.test.ts +464 -0
  116. package/packages/omo-codex/plugin/components/bootstrap/tsconfig.json +25 -0
  117. package/packages/omo-codex/plugin/components/comment-checker/hooks/hooks.json +1 -1
  118. package/packages/omo-codex/plugin/components/comment-checker/package.json +4 -4
  119. package/packages/omo-codex/plugin/components/git-bash/hooks/hooks.json +2 -2
  120. package/packages/omo-codex/plugin/components/git-bash/package.json +2 -2
  121. package/packages/omo-codex/plugin/components/lsp/dist/codex-hook-cli.js +6 -10
  122. package/packages/omo-codex/plugin/components/lsp/hooks/hooks.json +2 -2
  123. package/packages/omo-codex/plugin/components/lsp/package.json +4 -4
  124. package/packages/omo-codex/plugin/components/lsp/scripts/build-lsp-tools.test.mjs +8 -3
  125. package/packages/omo-codex/plugin/components/lsp/src/codex-hook-cli.ts +5 -8
  126. package/packages/omo-codex/plugin/components/lsp/test/codex-hook-cli.test.ts +24 -1
  127. package/packages/omo-codex/plugin/components/rules/bundled-rules/windows-git-bash.md +3 -1
  128. package/packages/omo-codex/plugin/components/rules/hooks/hooks.json +4 -4
  129. package/packages/omo-codex/plugin/components/rules/package.json +4 -4
  130. package/packages/omo-codex/plugin/components/rules/test/windows-git-bash-bundled-rule.test.ts +35 -1
  131. package/packages/omo-codex/plugin/components/start-work-continuation/hooks/hooks.json +2 -2
  132. package/packages/omo-codex/plugin/components/start-work-continuation/package.json +4 -4
  133. package/packages/omo-codex/plugin/components/telemetry/hooks/hooks.json +1 -1
  134. package/packages/omo-codex/plugin/components/telemetry/package.json +4 -4
  135. package/packages/omo-codex/plugin/components/ultrawork/biome.json +1 -1
  136. package/packages/omo-codex/plugin/components/ultrawork/directive.md +155 -99
  137. package/packages/omo-codex/plugin/components/ultrawork/hooks/hooks.json +1 -1
  138. package/packages/omo-codex/plugin/components/ultrawork/package.json +4 -4
  139. package/packages/omo-codex/plugin/components/ultrawork/skills/ulw-plan/SKILL.md +19 -51
  140. package/packages/omo-codex/plugin/components/ultrawork/skills/ulw-plan/references/full-workflow.md +46 -51
  141. package/packages/omo-codex/plugin/components/ultrawork/test/codex-hook.test.ts +19 -0
  142. package/packages/omo-codex/plugin/components/ultrawork/test/package-smoke.test.ts +0 -1
  143. package/packages/omo-codex/plugin/components/ulw-loop/dist/cli-commands.js +9 -1
  144. package/packages/omo-codex/plugin/components/ulw-loop/dist/cli-output.d.ts +1 -0
  145. package/packages/omo-codex/plugin/components/ulw-loop/dist/cli-output.js +18 -0
  146. package/packages/omo-codex/plugin/components/ulw-loop/dist/plan-crud.js +1 -3
  147. package/packages/omo-codex/plugin/components/ulw-loop/hooks/hooks.json +2 -2
  148. package/packages/omo-codex/plugin/components/ulw-loop/package.json +4 -4
  149. package/packages/omo-codex/plugin/components/ulw-loop/src/cli-commands.ts +6 -2
  150. package/packages/omo-codex/plugin/components/ulw-loop/src/cli-output.ts +19 -0
  151. package/packages/omo-codex/plugin/components/ulw-loop/src/plan-crud.ts +1 -1
  152. package/packages/omo-codex/plugin/components/ulw-loop/test/cli-commands.test.ts +6 -0
  153. package/packages/omo-codex/plugin/components/ulw-loop/test/cli-complete-goals.test.ts +26 -1
  154. package/packages/omo-codex/plugin/components/ulw-loop/test/cli-json-errors.test.ts +89 -0
  155. package/packages/omo-codex/plugin/hooks/hooks.json +27 -16
  156. package/packages/omo-codex/plugin/package-lock.json +193 -193
  157. package/packages/omo-codex/plugin/package.json +1 -1
  158. package/packages/omo-codex/plugin/scripts/auto-update-state.d.mts +20 -0
  159. package/packages/omo-codex/plugin/scripts/auto-update.mjs +28 -8
  160. package/packages/omo-codex/plugin/scripts/build-components.mjs +36 -5
  161. package/packages/omo-codex/plugin/scripts/install-flow.mjs +43 -0
  162. package/packages/omo-codex/plugin/skills/lcx-contribute-bug-fix/SKILL.md +79 -28
  163. package/packages/omo-codex/plugin/skills/lcx-contribute-bug-fix/agents/openai.yaml +2 -2
  164. package/packages/omo-codex/plugin/skills/lcx-report-bug/SKILL.md +7 -6
  165. package/packages/omo-codex/plugin/skills/lcx-report-bug/agents/openai.yaml +1 -1
  166. package/packages/omo-codex/plugin/skills/ulw-plan/SKILL.md +19 -51
  167. package/packages/omo-codex/plugin/skills/ulw-plan/references/full-workflow.md +46 -51
  168. package/packages/omo-codex/plugin/test/aggregate-manifest.test.mjs +1 -0
  169. package/packages/omo-codex/plugin/test/auto-update.test.mjs +145 -0
  170. package/packages/omo-codex/plugin/test/bootstrap-binlinks.test.mjs +250 -0
  171. package/packages/omo-codex/plugin/test/bootstrap-hooks.test.mjs +166 -0
  172. package/packages/omo-codex/plugin/test/bootstrap-orchestration.test.mjs +371 -0
  173. package/packages/omo-codex/plugin/test/bootstrap-ps-guard.test.mjs +134 -0
  174. package/packages/omo-codex/plugin/test/bootstrap-setup.test.mjs +249 -0
  175. package/packages/omo-codex/plugin/test/lcx-bug-skills.test.mjs +10 -1
  176. package/packages/omo-codex/plugin/test/ulw-plan-skill.test.mjs +46 -0
  177. package/packages/omo-codex/scripts/atomic-write.test.mjs +82 -0
  178. package/packages/omo-codex/scripts/install/agents.d.mts +18 -0
  179. package/packages/omo-codex/scripts/install/agents.mjs +78 -5
  180. package/packages/omo-codex/scripts/install/atomic-write.mjs +59 -0
  181. package/packages/omo-codex/scripts/install/bin-dir.d.mts +7 -0
  182. package/packages/omo-codex/scripts/install/bin-links.d.mts +18 -0
  183. package/packages/omo-codex/scripts/install/config.d.mts +35 -0
  184. package/packages/omo-codex/scripts/install/config.mjs +13 -3
  185. package/packages/omo-codex/scripts/install/git-bash-mcp-env.d.mts +5 -0
  186. package/packages/omo-codex/scripts/install/git-bash.d.mts +23 -0
  187. package/packages/omo-codex/scripts/install/hook-trust.d.mts +10 -0
  188. package/packages/omo-codex/scripts/install-agent-links.test.mjs +41 -0
  189. package/packages/omo-codex/scripts/install-local.mjs +3 -2
  190. package/packages/shared-skills/skills/lcx-contribute-bug-fix/SKILL.md +79 -28
  191. package/packages/shared-skills/skills/lcx-contribute-bug-fix/agents/openai.yaml +2 -2
  192. package/packages/shared-skills/skills/lcx-report-bug/SKILL.md +7 -6
  193. package/packages/shared-skills/skills/lcx-report-bug/agents/openai.yaml +1 -1
  194. package/dist/hooks/session-recovery/constants.d.ts +0 -4
  195. package/dist/hooks/session-recovery/detect-error-type.d.ts +0 -4
  196. package/dist/hooks/session-recovery/error-recovery.d.ts +0 -4
  197. package/dist/hooks/session-recovery/hook-types.d.ts +0 -22
  198. package/dist/hooks/session-recovery/hook.d.ts +0 -4
  199. package/dist/hooks/session-recovery/index.d.ts +0 -5
  200. package/dist/hooks/session-recovery/interrupted-idle-message-fetch-timeout.d.ts +0 -7
  201. package/dist/hooks/session-recovery/interrupted-tool-results.d.ts +0 -3
  202. package/dist/hooks/session-recovery/message-state.d.ts +0 -4
  203. package/dist/hooks/session-recovery/recover-thinking-block-order.d.ts +0 -5
  204. package/dist/hooks/session-recovery/recover-thinking-disabled-violation.d.ts +0 -5
  205. package/dist/hooks/session-recovery/recover-tool-result-missing.d.ts +0 -10
  206. package/dist/hooks/session-recovery/recover-unavailable-tool.d.ts +0 -5
  207. package/dist/hooks/session-recovery/resume.d.ts +0 -7
  208. package/dist/hooks/session-recovery/storage/latest-assistant-message.d.ts +0 -5
  209. package/dist/hooks/session-recovery/storage/orphan-thinking-search.d.ts +0 -2
  210. package/dist/hooks/session-recovery/storage/thinking-block-search.d.ts +0 -2
  211. package/dist/hooks/session-recovery/storage/thinking-prepend.d.ts +0 -33
  212. package/dist/hooks/session-recovery/storage/thinking-strip.d.ts +0 -11
  213. package/dist/hooks/session-recovery/storage.d.ts +0 -20
  214. package/dist/plugin/event-session-recovery.d.ts +0 -9
  215. package/dist/plugin/user-abort-interrupted-recovery-guard.d.ts +0 -6
  216. /package/dist/hooks/{session-recovery → anthropic-context-window-limit-recovery}/storage/empty-messages.d.ts +0 -0
  217. /package/dist/hooks/{session-recovery → anthropic-context-window-limit-recovery}/storage/empty-text.d.ts +0 -0
  218. /package/dist/hooks/{session-recovery → anthropic-context-window-limit-recovery}/storage/message-dir.d.ts +0 -0
  219. /package/dist/hooks/{session-recovery → anthropic-context-window-limit-recovery}/storage/part-id.d.ts +0 -0
  220. /package/dist/hooks/{session-recovery → anthropic-context-window-limit-recovery}/storage/text-part-injector.d.ts +0 -0
@@ -0,0 +1,375 @@
1
+ import { describe, expect, it } from "bun:test";
2
+ import { spawnSync } from "node:child_process";
3
+ import { existsSync } from "node:fs";
4
+ import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises";
5
+ import { homedir, tmpdir } from "node:os";
6
+ import { dirname, join } from "node:path";
7
+ import { fileURLToPath } from "node:url";
8
+
9
+ import {
10
+ bootstrapLocks,
11
+ detectInstallFlow,
12
+ detectInstallFlowForTest,
13
+ detectInstallFlowFromEnvironment,
14
+ resolveBootstrapLockPath,
15
+ resolveBootstrapStatePath,
16
+ resolveCodexHome,
17
+ } from "../src/environment.ts";
18
+
19
+ const componentRoot = dirname(dirname(fileURLToPath(import.meta.url)));
20
+ const repoRoot = dirname(dirname(dirname(dirname(dirname(componentRoot)))));
21
+
22
+ const GIT_SOURCE_CONFIG = [
23
+ "[marketplaces.sisyphuslabs]",
24
+ 'last_updated = "2026-06-12T00:00:00Z"',
25
+ 'source_type = "git"',
26
+ 'source = "https://github.com/lazycodex-ai/lazycodex.git"',
27
+ "",
28
+ ].join("\n");
29
+
30
+ const LOCAL_SOURCE_CONFIG = [
31
+ "[marketplaces.sisyphuslabs]",
32
+ 'last_updated = "2026-06-12T00:00:00Z"',
33
+ 'source_type = "local"',
34
+ 'source = "/Users/someone/local-workspaces/omo"',
35
+ "",
36
+ ].join("\n");
37
+
38
+ async function withTempDir<T>(prefix: string, run: (directory: string) => Promise<T>): Promise<T> {
39
+ const directory = await mkdtemp(join(tmpdir(), prefix));
40
+ try {
41
+ return await run(directory);
42
+ } finally {
43
+ await rm(directory, { force: true, recursive: true });
44
+ }
45
+ }
46
+
47
+ async function writePluginRoot(directory: string, options: { readonly withSnapshot: boolean }): Promise<string> {
48
+ const pluginRoot = join(directory, "plugins", "sisyphuslabs", "omo", "4.9.2");
49
+ await mkdir(pluginRoot, { recursive: true });
50
+ if (options.withSnapshot) {
51
+ await writeFile(join(pluginRoot, "lazycodex-install.json"), '{"packageName":"lazycodex-ai","version":"4.9.2"}\n');
52
+ }
53
+ return pluginRoot;
54
+ }
55
+
56
+ describe("detectInstallFlow", () => {
57
+ it("#given an install snapshot and no config #when detecting #then reports npx-local", async () => {
58
+ await withTempDir("omo-flow-", async (directory) => {
59
+ const pluginRoot = await writePluginRoot(directory, { withSnapshot: true });
60
+ expect(await detectInstallFlow({ pluginRoot })).toBe("npx-local");
61
+ });
62
+ });
63
+
64
+ it("#given no snapshot and no config #when detecting #then reports marketplace", async () => {
65
+ await withTempDir("omo-flow-", async (directory) => {
66
+ const pluginRoot = await writePluginRoot(directory, { withSnapshot: false });
67
+ expect(await detectInstallFlow({ pluginRoot })).toBe("marketplace");
68
+ });
69
+ });
70
+
71
+ it("#given no snapshot and a git marketplace source #when detecting #then both signals agree on marketplace", async () => {
72
+ await withTempDir("omo-flow-", async (directory) => {
73
+ const pluginRoot = await writePluginRoot(directory, { withSnapshot: false });
74
+ expect(await detectInstallFlow({ configToml: GIT_SOURCE_CONFIG, pluginRoot })).toBe("marketplace");
75
+ });
76
+ });
77
+
78
+ it("#given a snapshot and a local absolute marketplace source #when detecting #then both signals agree on npx-local", async () => {
79
+ await withTempDir("omo-flow-", async (directory) => {
80
+ const pluginRoot = await writePluginRoot(directory, { withSnapshot: true });
81
+ expect(await detectInstallFlow({ configToml: LOCAL_SOURCE_CONFIG, pluginRoot })).toBe("npx-local");
82
+ });
83
+ });
84
+
85
+ it("#given a snapshot but a git marketplace source #when detecting #then the disagreement reports unknown", async () => {
86
+ await withTempDir("omo-flow-", async (directory) => {
87
+ const pluginRoot = await writePluginRoot(directory, { withSnapshot: true });
88
+ expect(await detectInstallFlow({ configToml: GIT_SOURCE_CONFIG, pluginRoot })).toBe("unknown");
89
+ });
90
+ });
91
+
92
+ it("#given no snapshot but a local marketplace source #when detecting #then the disagreement reports unknown", async () => {
93
+ await withTempDir("omo-flow-", async (directory) => {
94
+ const pluginRoot = await writePluginRoot(directory, { withSnapshot: false });
95
+ expect(await detectInstallFlow({ configToml: LOCAL_SOURCE_CONFIG, pluginRoot })).toBe("unknown");
96
+ });
97
+ });
98
+
99
+ it("#given an unclassifiable marketplace source #when detecting #then reports unknown", async () => {
100
+ await withTempDir("omo-flow-", async (directory) => {
101
+ const pluginRoot = await writePluginRoot(directory, { withSnapshot: false });
102
+ const config = ['[marketplaces.sisyphuslabs]', 'source = "./relative/checkout"', ""].join("\n");
103
+ expect(await detectInstallFlow({ configToml: config, pluginRoot })).toBe("unknown");
104
+ });
105
+ });
106
+
107
+ it("#given a quoted marketplace header #when detecting #then the section is still recognized", async () => {
108
+ await withTempDir("omo-flow-", async (directory) => {
109
+ const pluginRoot = await writePluginRoot(directory, { withSnapshot: false });
110
+ const config = ['[marketplaces."sisyphuslabs"]', 'source = "https://github.com/lazycodex-ai/lazycodex"', ""].join("\n");
111
+ expect(await detectInstallFlow({ configToml: config, pluginRoot })).toBe("marketplace");
112
+ });
113
+ });
114
+
115
+ it("#given a config without the sisyphuslabs marketplace #when detecting #then only the snapshot signal decides", async () => {
116
+ await withTempDir("omo-flow-", async (directory) => {
117
+ const pluginRoot = await writePluginRoot(directory, { withSnapshot: true });
118
+ const config = ['[marketplaces.other]', 'source = "https://github.com/other/marketplace.git"', ""].join("\n");
119
+ expect(await detectInstallFlow({ configToml: config, pluginRoot })).toBe("npx-local");
120
+ });
121
+ });
122
+
123
+ it("#given a windows drive marketplace source #when detecting with a snapshot #then reports npx-local", async () => {
124
+ await withTempDir("omo-flow-", async (directory) => {
125
+ const pluginRoot = await writePluginRoot(directory, { withSnapshot: true });
126
+ const config = ["[marketplaces.sisyphuslabs]", 'source = "C:\\\\workspaces\\\\omo"', ""].join("\n");
127
+ expect(await detectInstallFlow({ configToml: config, pluginRoot })).toBe("npx-local");
128
+ });
129
+ });
130
+ });
131
+
132
+ describe("detectInstallFlowFromEnvironment", () => {
133
+ it("#given a codex store layout with a git marketplace config #when detecting from environment #then reports marketplace", async () => {
134
+ await withTempDir("omo-flow-env-", async (directory) => {
135
+ const codexHome = join(directory, ".codex");
136
+ const pluginRoot = join(codexHome, "plugins", "sisyphuslabs", "omo", "4.9.2");
137
+ await mkdir(pluginRoot, { recursive: true });
138
+ await writeFile(join(codexHome, "config.toml"), GIT_SOURCE_CONFIG);
139
+
140
+ const detection = await detectInstallFlowFromEnvironment({ env: {}, pluginRoot });
141
+
142
+ expect(detection.flow).toBe("marketplace");
143
+ expect(detection.snapshotPresent).toBe(false);
144
+ });
145
+ });
146
+
147
+ it("#given a codex store layout with a local source and an install snapshot #when detecting from environment #then reports npx-local", async () => {
148
+ await withTempDir("omo-flow-env-", async (directory) => {
149
+ const codexHome = join(directory, ".codex");
150
+ const pluginRoot = join(codexHome, "plugins", "sisyphuslabs", "omo", "4.9.2");
151
+ await mkdir(pluginRoot, { recursive: true });
152
+ await writeFile(join(codexHome, "config.toml"), LOCAL_SOURCE_CONFIG);
153
+ await writeFile(join(pluginRoot, "lazycodex-install.json"), '{"packageName":"lazycodex-ai","version":"4.9.2"}\n');
154
+
155
+ const detection = await detectInstallFlowFromEnvironment({ env: {}, pluginRoot });
156
+
157
+ expect(detection.flow).toBe("npx-local");
158
+ expect(detection.snapshotPresent).toBe(true);
159
+ });
160
+ });
161
+ });
162
+
163
+ describe("sync-lazycodex-marketplace output", () => {
164
+ it("#given a synced marketplace tree #when looking for the install snapshot #then it is absent and detection reports marketplace", async () => {
165
+ await withTempDir("omo-flow-sync-", async (directory) => {
166
+ const sourceRoot = join(directory, "source");
167
+ const lazycodexRoot = join(directory, "lazycodex");
168
+ await writeSyncSourceFixture(sourceRoot);
169
+
170
+ const sync = spawnSync(
171
+ process.execPath,
172
+ ["run", join(repoRoot, "script", "sync-lazycodex-marketplace.ts"), sourceRoot, lazycodexRoot],
173
+ { encoding: "utf8" },
174
+ );
175
+ expect(sync.status).toBe(0);
176
+
177
+ const syncedPluginRoot = join(lazycodexRoot, "plugins", "omo");
178
+ expect(existsSync(syncedPluginRoot)).toBe(true);
179
+ expect(existsSync(join(syncedPluginRoot, "lazycodex-install.json"))).toBe(false);
180
+ expect(await detectInstallFlowForTest(syncedPluginRoot)).toBe("marketplace");
181
+ });
182
+ }, 30_000);
183
+
184
+ it("#given the repo plugin source tree the sync copies #when looking for the install snapshot #then it never exists there", () => {
185
+ const pluginSourceRoot = join(repoRoot, "packages", "omo-codex", "plugin");
186
+ expect(existsSync(join(pluginSourceRoot, ".codex-plugin", "plugin.json"))).toBe(true);
187
+ expect(existsSync(join(pluginSourceRoot, "lazycodex-install.json"))).toBe(false);
188
+ });
189
+ });
190
+
191
+ describe("resolveCodexHome", () => {
192
+ it("#given CODEX_HOME in the environment #when resolving #then the env value wins", async () => {
193
+ await withTempDir("omo-home-", async (directory) => {
194
+ const resolution = await resolveCodexHome({
195
+ env: { CODEX_HOME: directory },
196
+ pluginRoot: join(directory, "plugins", "sisyphuslabs", "omo", "4.9.2"),
197
+ });
198
+ expect(resolution).toEqual({ path: directory, source: "env" });
199
+ });
200
+ });
201
+
202
+ it("#given a codex store layout #when resolving without env #then walking up finds the config.toml dir", async () => {
203
+ await withTempDir("omo-home-", async (directory) => {
204
+ const codexHome = join(directory, ".codex");
205
+ const pluginRoot = join(codexHome, "plugins", "sisyphuslabs", "omo", "4.9.2");
206
+ await mkdir(pluginRoot, { recursive: true });
207
+ await writeFile(join(codexHome, "config.toml"), GIT_SOURCE_CONFIG);
208
+
209
+ const resolution = await resolveCodexHome({ env: {}, pluginRoot });
210
+
211
+ expect(resolution).toEqual({ path: codexHome, source: "walk-up" });
212
+ });
213
+ });
214
+
215
+ it("#given config.toml six levels above the plugin root #when resolving #then the walk-up still finds it", async () => {
216
+ await withTempDir("omo-home-", async (directory) => {
217
+ const pluginRoot = join(directory, "a", "b", "c", "d", "e", "f");
218
+ await mkdir(pluginRoot, { recursive: true });
219
+ await writeFile(join(directory, "config.toml"), "");
220
+
221
+ const resolution = await resolveCodexHome({ env: {}, pluginRoot });
222
+
223
+ expect(resolution).toEqual({ path: directory, source: "walk-up" });
224
+ });
225
+ });
226
+
227
+ it("#given config.toml seven levels above the plugin root #when resolving #then the bounded walk falls back to the default home", async () => {
228
+ await withTempDir("omo-home-", async (directory) => {
229
+ const pluginRoot = join(directory, "a", "b", "c", "d", "e", "f", "g");
230
+ await mkdir(pluginRoot, { recursive: true });
231
+ await writeFile(join(directory, "config.toml"), "");
232
+
233
+ const resolution = await resolveCodexHome({ env: {}, pluginRoot });
234
+
235
+ expect(resolution).toEqual({ path: join(homedir(), ".codex"), source: "default" });
236
+ });
237
+ });
238
+
239
+ it("#given no env and no config.toml ancestor #when resolving #then defaults to ~/.codex", async () => {
240
+ await withTempDir("omo-home-", async (directory) => {
241
+ const pluginRoot = join(directory, "plugins", "sisyphuslabs", "omo", "4.9.2");
242
+ await mkdir(pluginRoot, { recursive: true });
243
+
244
+ const resolution = await resolveCodexHome({ env: {}, pluginRoot });
245
+
246
+ expect(resolution).toEqual({ path: join(homedir(), ".codex"), source: "default" });
247
+ });
248
+ });
249
+ });
250
+
251
+ describe("bootstrapLocks", () => {
252
+ it("#given free locks #when acquiring twice #then the second acquirer gets null and release leaves no lock files", async () => {
253
+ await withTempDir("omo-locks-", async (directory) => {
254
+ const pluginData = join(directory, "plugin-data");
255
+ const env = { PLUGIN_DATA: pluginData };
256
+
257
+ const first = await bootstrapLocks({ env, pluginData });
258
+ expect(first).not.toBeNull();
259
+ if (first === null) throw new Error("unreachable");
260
+ expect(first.bootstrapLockPath).toBe(join(pluginData, "bootstrap", "state.json.lock"));
261
+ expect(first.statePath).toBe(join(pluginData, "bootstrap", "state.json"));
262
+ expect(first.autoUpdateLockPath).toBe(join(pluginData, "auto-update.json.lock"));
263
+ expect(existsSync(first.bootstrapLockPath)).toBe(true);
264
+ expect(existsSync(first.autoUpdateLockPath)).toBe(true);
265
+
266
+ const second = await bootstrapLocks({ env, pluginData });
267
+ expect(second).toBeNull();
268
+
269
+ await first.release();
270
+ expect(existsSync(first.bootstrapLockPath)).toBe(false);
271
+ expect(existsSync(first.autoUpdateLockPath)).toBe(false);
272
+ });
273
+ });
274
+
275
+ it("#given the auto-update lock already held #when acquiring #then returns null without leaking the bootstrap lock", async () => {
276
+ await withTempDir("omo-locks-", async (directory) => {
277
+ const pluginData = join(directory, "plugin-data");
278
+ const env = { PLUGIN_DATA: pluginData };
279
+ const autoUpdateLockPath = join(pluginData, "auto-update.json.lock");
280
+ await mkdir(pluginData, { recursive: true });
281
+ await writeFile(autoUpdateLockPath, `${Date.now()}\n`);
282
+
283
+ const handle = await bootstrapLocks({ env, pluginData });
284
+
285
+ expect(handle).toBeNull();
286
+ expect(existsSync(join(pluginData, "bootstrap", "state.json.lock"))).toBe(false);
287
+ expect(existsSync(autoUpdateLockPath)).toBe(true);
288
+ });
289
+ });
290
+
291
+ it("#given an explicit auto-update lock override #when acquiring #then the override path is honored", async () => {
292
+ await withTempDir("omo-locks-", async (directory) => {
293
+ const pluginData = join(directory, "plugin-data");
294
+ const overridePath = join(directory, "custom-auto-update.lock");
295
+ const env = { LAZYCODEX_AUTO_UPDATE_LOCK_PATH: overridePath, PLUGIN_DATA: pluginData };
296
+
297
+ const handle = await bootstrapLocks({ env, pluginData });
298
+
299
+ expect(handle).not.toBeNull();
300
+ if (handle === null) throw new Error("unreachable");
301
+ expect(handle.autoUpdateLockPath).toBe(overridePath);
302
+ expect(existsSync(overridePath)).toBe(true);
303
+ await handle.release();
304
+ expect(existsSync(overridePath)).toBe(false);
305
+ });
306
+ });
307
+
308
+ it("#given the path helpers #when resolving state and lock paths #then they follow the PLUGIN_DATA bootstrap layout", () => {
309
+ expect(resolveBootstrapStatePath("/data/omo-sisyphuslabs")).toBe(join("/data/omo-sisyphuslabs", "bootstrap", "state.json"));
310
+ expect(resolveBootstrapLockPath("/data/omo-sisyphuslabs")).toBe(
311
+ join("/data/omo-sisyphuslabs", "bootstrap", "state.json.lock"),
312
+ );
313
+ });
314
+ });
315
+
316
+ async function writeJson(path: string, value: unknown): Promise<void> {
317
+ await mkdir(dirname(path), { recursive: true });
318
+ await writeFile(path, `${JSON.stringify(value, null, 2)}\n`);
319
+ }
320
+
321
+ async function writeExecutableStub(path: string): Promise<void> {
322
+ await mkdir(dirname(path), { recursive: true });
323
+ await writeFile(path, "#!/usr/bin/env node\n");
324
+ }
325
+
326
+ // Mirrors the minimal source tree script/sync-lazycodex-marketplace.test.ts uses so the
327
+ // real sync (including bundle validation) runs against a hermetic fixture.
328
+ async function writeSyncSourceFixture(sourceRoot: string): Promise<void> {
329
+ const pluginSource = join(sourceRoot, "packages", "omo-codex", "plugin");
330
+ await writeJson(join(sourceRoot, "packages", "omo-codex", "marketplace.json"), {
331
+ name: "sisyphuslabs",
332
+ plugins: [{ name: "omo", source: "./plugins/omo" }],
333
+ });
334
+ await writeJson(join(pluginSource, ".codex-plugin", "plugin.json"), { name: "omo", version: "1.2.3" });
335
+ await writeJson(join(pluginSource, "package.json"), { name: "@sisyphuslabs/omo-codex-plugin", version: "1.2.3" });
336
+ const bootstrapSessionStart = {
337
+ hooks: {
338
+ SessionStart: [
339
+ {
340
+ hooks: [
341
+ {
342
+ command: 'node "${PLUGIN_ROOT}/components/bootstrap/dist/cli.js" hook session-start',
343
+ commandWindows:
344
+ 'powershell -NoProfile -ExecutionPolicy Bypass -File "${PLUGIN_ROOT}\\components\\bootstrap\\scripts\\bootstrap.ps1"',
345
+ statusMessage: "LazyCodex(1.2.3): Checking Bootstrap Provisioning",
346
+ timeout: 30,
347
+ type: "command",
348
+ },
349
+ ],
350
+ },
351
+ ],
352
+ },
353
+ };
354
+ await writeJson(join(pluginSource, "hooks", "hooks.json"), bootstrapSessionStart);
355
+ await writeJson(join(pluginSource, "components", "bootstrap", "hooks", "hooks.json"), bootstrapSessionStart);
356
+ await writeJson(join(pluginSource, ".mcp.json"), {
357
+ mcpServers: {
358
+ ast_grep: { args: ["../../ast-grep-mcp/dist/cli.js", "mcp"], command: "node", cwd: "." },
359
+ git_bash: { args: ["../../git-bash-mcp/dist/cli.js", "mcp"], command: "node", cwd: "." },
360
+ lsp: { args: ["../../lsp-daemon/dist/cli.js", "mcp"], command: "node", cwd: "." },
361
+ },
362
+ });
363
+ await mkdir(join(sourceRoot, "packages", "omo-codex", "lazycodex-repository", ".github", "workflows"), { recursive: true });
364
+ await writeFile(
365
+ join(sourceRoot, "packages", "omo-codex", "lazycodex-repository", ".github", "workflows", "pr-source-guidance.yml"),
366
+ "name: PR source guidance\n\non:\n pull_request_target:\n",
367
+ );
368
+ await writeExecutableStub(join(pluginSource, "components", "bootstrap", "dist", "cli.js"));
369
+ await mkdir(join(pluginSource, "components", "bootstrap", "scripts"), { recursive: true });
370
+ await writeFile(join(pluginSource, "components", "bootstrap", "scripts", "bootstrap.ps1"), "exit 0\n");
371
+ await writeExecutableStub(join(sourceRoot, "packages", "ast-grep-mcp", "dist", "cli.js"));
372
+ await writeExecutableStub(join(sourceRoot, "packages", "git-bash-mcp", "dist", "cli.js"));
373
+ await writeExecutableStub(join(sourceRoot, "packages", "lsp-tools-mcp", "dist", "cli.js"));
374
+ await writeExecutableStub(join(sourceRoot, "packages", "lsp-daemon", "dist", "cli.js"));
375
+ }