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,249 @@
1
+ import assert from "node:assert/strict";
2
+ import { chmod, mkdir, mkdtemp, readFile, rm, stat, writeFile } from "node:fs/promises";
3
+ import { tmpdir } from "node:os";
4
+ import { join } from "node:path";
5
+ import test from "node:test";
6
+ import { fileURLToPath } from "node:url";
7
+
8
+ const CLI_URL = new URL("../components/bootstrap/dist/cli.js", import.meta.url);
9
+ const { runBootstrapWorker, runWorkerSetup } = await import(CLI_URL.href);
10
+
11
+ const REPO_MODEL_CATALOG_PATH = fileURLToPath(new URL("../model-catalog.json", import.meta.url));
12
+ const MARKETPLACE_SOURCE_LINE = 'source = "https://github.com/code-yeongyu/lazycodex.git"';
13
+ const PERMISSIONS_KEY_PATTERN = /approval_policy|sandbox_mode|network_access/;
14
+ const PLUGIN_VERSION = "9.9.9";
15
+
16
+ const BUNDLED_EXPLORER_TOML = 'description = "Explorer agent"\nmodel_reasoning_effort = "medium"\n';
17
+ const BUNDLED_METIS_TOML = 'description = "Metis agent"\nmodel_reasoning_effort = "high"\n';
18
+
19
+ async function withSetupFixture(run) {
20
+ const root = await mkdtemp(join(tmpdir(), "omo-bootstrap-setup-"));
21
+ try {
22
+ const pluginRoot = join(root, "plugin");
23
+ const pluginData = join(root, "plugin-data");
24
+ const codexHome = join(root, "codex-home");
25
+ await mkdir(join(pluginRoot, ".codex-plugin"), { recursive: true });
26
+ await mkdir(join(pluginRoot, "hooks"), { recursive: true });
27
+ await mkdir(join(pluginRoot, "components", "ultrawork", "agents"), { recursive: true });
28
+ // A complete npx-style payload ships dist/cli; the marketplace-payload
29
+ // (no dist/cli -> degraded omo-cli) path is covered by
30
+ // bootstrap-binlinks.test.mjs.
31
+ await mkdir(join(pluginRoot, "dist", "cli"), { recursive: true });
32
+ await writeFile(join(pluginRoot, "dist", "cli", "index.js"), "");
33
+ await mkdir(pluginData, { recursive: true });
34
+ await mkdir(codexHome, { recursive: true });
35
+ await writeFile(
36
+ join(pluginRoot, ".codex-plugin", "plugin.json"),
37
+ `${JSON.stringify({ hooks: "./hooks/hooks.json", name: "omo", version: PLUGIN_VERSION })}\n`,
38
+ );
39
+ await writeFile(
40
+ join(pluginRoot, "hooks", "hooks.json"),
41
+ `${JSON.stringify({
42
+ hooks: {
43
+ SessionStart: [
44
+ {
45
+ hooks: [
46
+ {
47
+ command: 'node "${PLUGIN_ROOT}/components/bootstrap/dist/cli.js" hook session-start',
48
+ statusMessage: "LazyCodex(9.9.9): Checking Bootstrap Provisioning",
49
+ timeout: 30,
50
+ type: "command",
51
+ },
52
+ ],
53
+ },
54
+ ],
55
+ },
56
+ })}\n`,
57
+ );
58
+ await writeFile(
59
+ join(pluginRoot, ".mcp.json"),
60
+ `${JSON.stringify({ mcpServers: { git_bash: { args: ["serve"], command: "node", env: {} } } }, null, "\t")}\n`,
61
+ );
62
+ await writeFile(join(pluginRoot, "components", "ultrawork", "agents", "explorer.toml"), BUNDLED_EXPLORER_TOML);
63
+ await writeFile(join(pluginRoot, "components", "ultrawork", "agents", "metis.toml"), BUNDLED_METIS_TOML);
64
+ await writeFile(join(codexHome, "config.toml"), `[marketplaces.sisyphuslabs]\n${MARKETPLACE_SOURCE_LINE}\n`);
65
+ await run({ codexHome, pluginData, pluginRoot, root });
66
+ } finally {
67
+ await rm(root, { force: true, recursive: true });
68
+ }
69
+ }
70
+
71
+ function setupOptions(fixture, overrides = {}) {
72
+ return {
73
+ codexHome: fixture.codexHome,
74
+ env: {},
75
+ platform: "darwin",
76
+ pluginData: fixture.pluginData,
77
+ pluginRoot: fixture.pluginRoot,
78
+ ...overrides,
79
+ };
80
+ }
81
+
82
+ async function readConfig(fixture) {
83
+ return readFile(join(fixture.codexHome, "config.toml"), "utf8");
84
+ }
85
+
86
+ test("#given a marketplace-flow CODEX_HOME #when the worker setup runs #then config gains plugin, hook-state, and agent blocks without permissions keys", async () => {
87
+ await withSetupFixture(async (fixture) => {
88
+ const outcome = await runWorkerSetup(setupOptions(fixture));
89
+
90
+ assert.deepEqual(outcome.degraded, []);
91
+ const config = await readConfig(fixture);
92
+ assert.match(config, /\[plugins\."omo@sisyphuslabs"\]\nenabled = true/);
93
+ assert.match(config, /\[plugins\."omo@sisyphuslabs"\.mcp_servers\.context7\]\nenabled = true/);
94
+ assert.match(config, /\[plugins\."omo@sisyphuslabs"\.mcp_servers\.git_bash\]\nenabled = false/);
95
+ assert.match(
96
+ config,
97
+ /\[hooks\.state\."omo@sisyphuslabs:hooks\/hooks\.json:session_start:0:0"\]\ntrusted_hash = "sha256:[0-9a-f]{64}"/,
98
+ );
99
+ assert.match(config, /\[agents\.explorer\]\nconfig_file = "\.\/agents\/explorer\.toml"/);
100
+ assert.match(config, /\[agents\.metis\]\nconfig_file = "\.\/agents\/metis\.toml"/);
101
+ assert.doesNotMatch(config, PERMISSIONS_KEY_PATTERN);
102
+ assert.equal(await readFile(join(fixture.codexHome, "agents", "explorer.toml"), "utf8"), BUNDLED_EXPLORER_TOML);
103
+ assert.equal(await readFile(join(fixture.codexHome, "agents", "metis.toml"), "utf8"), BUNDLED_METIS_TOML);
104
+ });
105
+ });
106
+
107
+ test("#given an existing git marketplace source #when the worker setup runs #then the [marketplaces.sisyphuslabs] block stays byte-identical", async () => {
108
+ await withSetupFixture(async (fixture) => {
109
+ await runWorkerSetup(setupOptions(fixture));
110
+
111
+ const config = await readConfig(fixture);
112
+ assert.ok(config.includes(`[marketplaces.sisyphuslabs]\n${MARKETPLACE_SOURCE_LINE}`), "git source line must stay verbatim");
113
+ assert.doesNotMatch(config, /source_type/);
114
+ assert.doesNotMatch(config, /last_updated/);
115
+ });
116
+ });
117
+
118
+ test("#given a completed first run #when the worker setup runs again #then config.toml is byte-identical (idempotent)", async () => {
119
+ await withSetupFixture(async (fixture) => {
120
+ await runWorkerSetup(setupOptions(fixture));
121
+ const firstRun = await readConfig(fixture);
122
+
123
+ const outcome = await runWorkerSetup(setupOptions(fixture));
124
+
125
+ assert.deepEqual(outcome.degraded, []);
126
+ assert.equal(await readConfig(fixture), firstRun);
127
+ });
128
+ });
129
+
130
+ test("#given bootstrap-managed staging #when agents are linked #then nothing is persisted under PLUGIN_ROOT", async () => {
131
+ await withSetupFixture(async (fixture) => {
132
+ await runWorkerSetup(setupOptions(fixture));
133
+
134
+ await assert.rejects(() => stat(join(fixture.pluginRoot, ".installed-agents.json")));
135
+ const manifest = JSON.parse(
136
+ await readFile(join(fixture.pluginData, "bootstrap", "agents-stage", ".installed-agents.json"), "utf8"),
137
+ );
138
+ assert.equal(manifest.agents.length, 2);
139
+ });
140
+ });
141
+
142
+ test("#given user-tuned reasoning and service tier on an installed agent #when agents are re-linked #then both are preserved", async () => {
143
+ await withSetupFixture(async (fixture) => {
144
+ await mkdir(join(fixture.codexHome, "agents"), { recursive: true });
145
+ await writeFile(
146
+ join(fixture.codexHome, "agents", "explorer.toml"),
147
+ 'description = "Explorer agent"\nmodel_reasoning_effort = "low"\nservice_tier = "flex"\n',
148
+ );
149
+
150
+ await runWorkerSetup(setupOptions(fixture));
151
+
152
+ const linked = await readFile(join(fixture.codexHome, "agents", "explorer.toml"), "utf8");
153
+ assert.match(linked, /model_reasoning_effort = "low"/);
154
+ assert.match(linked, /service_tier = "flex"/);
155
+ });
156
+ });
157
+
158
+ test("#given an unwritable config.toml #when the worker setup runs #then it degrades naming config.toml and leaves the file untouched", async () => {
159
+ await withSetupFixture(async (fixture) => {
160
+ const configPath = join(fixture.codexHome, "config.toml");
161
+ const before = await readFile(configPath, "utf8");
162
+ await chmod(configPath, 0o444);
163
+ try {
164
+ const outcome = await runWorkerSetup(setupOptions(fixture));
165
+
166
+ const configEntries = outcome.degraded.filter((entry) => entry.component === "config");
167
+ assert.equal(configEntries.length, 1);
168
+ assert.match(configEntries[0].reason, /config\.toml/);
169
+ assert.equal(await readFile(configPath, "utf8"), before);
170
+ assert.equal((await stat(configPath)).mode & 0o777, 0o444);
171
+ } finally {
172
+ await chmod(configPath, 0o644);
173
+ }
174
+ });
175
+ });
176
+
177
+ test("#given win32 without Git Bash and auto-install skipped #when the worker setup runs #then it degrades instead of throwing and disables the git_bash MCP", async () => {
178
+ await withSetupFixture(async (fixture) => {
179
+ const commands = [];
180
+ const outcome = await runWorkerSetup(
181
+ setupOptions(fixture, {
182
+ env: { OMO_CODEX_SKIP_GIT_BASH_AUTO_INSTALL: "1" },
183
+ platform: "win32",
184
+ resolveGitBash: () => ({ checkedPaths: [], found: false, installHint: "install git bash" }),
185
+ runCommand: async (command, args) => {
186
+ commands.push([command, ...args]);
187
+ },
188
+ }),
189
+ );
190
+
191
+ assert.deepEqual(commands, []);
192
+ const gitBashEntries = outcome.degraded.filter((entry) => entry.component === "git-bash");
193
+ assert.equal(gitBashEntries.length, 1);
194
+ const config = await readConfig(fixture);
195
+ assert.match(config, /\[plugins\."omo@sisyphuslabs"\.mcp_servers\.git_bash\]\nenabled = false/);
196
+ assert.match(config, /\[plugins\."omo@sisyphuslabs"\]\nenabled = true/, "setup must continue past a missing Git Bash");
197
+ });
198
+ });
199
+
200
+ test("#given win32 with Git Bash and OMO_CODEX_GIT_BASH_PATH #when the worker setup runs #then git_bash is enabled and the MCP env is stamped", async () => {
201
+ await withSetupFixture(async (fixture) => {
202
+ const bashPath = "C:\\Tools\\Git\\bin\\bash.exe";
203
+ const outcome = await runWorkerSetup(
204
+ setupOptions(fixture, {
205
+ env: { OMO_CODEX_GIT_BASH_PATH: bashPath },
206
+ platform: "win32",
207
+ resolveGitBash: () => ({ checkedPaths: [bashPath], found: true, path: bashPath, source: "env" }),
208
+ }),
209
+ );
210
+
211
+ assert.deepEqual(outcome.degraded, []);
212
+ const config = await readConfig(fixture);
213
+ assert.match(config, /\[plugins\."omo@sisyphuslabs"\.mcp_servers\.git_bash\]\nenabled = true/);
214
+ const manifest = JSON.parse(await readFile(join(fixture.pluginRoot, ".mcp.json"), "utf8"));
215
+ assert.equal(manifest.mcpServers.git_bash.env.OMO_CODEX_GIT_BASH_PATH, bashPath);
216
+ });
217
+ });
218
+
219
+ test("#given the shipped plugin model catalog #when the worker setup stamps reasoning config #then the bundled fallback has not drifted from it", async () => {
220
+ await withSetupFixture(async (fixture) => {
221
+ const catalog = JSON.parse(await readFile(REPO_MODEL_CATALOG_PATH, "utf8"));
222
+
223
+ await runWorkerSetup(setupOptions(fixture));
224
+
225
+ const config = await readConfig(fixture);
226
+ assert.ok(config.includes(`model = ${JSON.stringify(catalog.current.model)}`));
227
+ assert.ok(config.includes(`model_context_window = ${catalog.current.model_context_window}`));
228
+ assert.ok(config.includes(`model_reasoning_effort = ${JSON.stringify(catalog.current.model_reasoning_effort)}`));
229
+ assert.ok(config.includes(`plan_mode_reasoning_effort = ${JSON.stringify(catalog.current.plan_mode_reasoning_effort)}`));
230
+ });
231
+ });
232
+
233
+ test("#given the default worker step list #when the worker runs end to end #then the setup step updates config and records a success marker", async () => {
234
+ await withSetupFixture(async (fixture) => {
235
+ const result = await runBootstrapWorker({
236
+ argv: ["--codex-home", fixture.codexHome, "--only", "setup"],
237
+ env: { PLUGIN_DATA: fixture.pluginData, PLUGIN_ROOT: fixture.pluginRoot },
238
+ });
239
+
240
+ assert.equal(result.ran, true);
241
+ assert.equal(result.status, "success");
242
+ const state = JSON.parse(await readFile(join(fixture.pluginData, "bootstrap", "state.json"), "utf8"));
243
+ assert.equal(state.completedForVersion, PLUGIN_VERSION);
244
+ assert.equal(state.lastStatus, "success");
245
+ const config = await readConfig(fixture);
246
+ assert.match(config, /\[plugins\."omo@sisyphuslabs"\]\nenabled = true/);
247
+ assert.ok(config.includes(`[marketplaces.sisyphuslabs]\n${MARKETPLACE_SOURCE_LINE}`));
248
+ });
249
+ });
@@ -16,12 +16,16 @@ test("#given synced lcx-report-bug skill #when inspected #then it files LazyCode
16
16
 
17
17
  // then
18
18
  assert.match(skill, /^---\r?\nname: lcx-report-bug\r?\n/m);
19
+ assert.match(skill, /Never create a PR or push a branch against `code-yeongyu\/lazycodex`/);
20
+ assert.match(skill, /gh pr create --repo openai\/codex/);
21
+ assert.doesNotMatch(skill, /gh pr create --repo "\$TARGET_REPO"/);
22
+ assert.doesNotMatch(skill, /gh pr create --repo code-yeongyu\/lazycodex/);
19
23
  assert.match(interfaceMetadata, /display_name: "lcx-report-bug \(omo\)"/);
20
24
  assert.match(interfaceMetadata, /- "lazycodex bug"/);
21
25
  assert.match(interfaceMetadata, /- "openai codex bug"/);
22
26
  });
23
27
 
24
- test("#given synced lcx-contribute-bug-fix skill #when inspected #then it contributes LazyCodex bug-fix PRs from fresh temp workspaces", async () => {
28
+ test("#given synced lcx-contribute-bug-fix skill #when inspected #then it delivers LazyCodex fixes as issues and upstream fixes as fork PRs", async () => {
25
29
  // given
26
30
  const skillRoot = join(root, "skills", "lcx-contribute-bug-fix");
27
31
 
@@ -31,6 +35,11 @@ test("#given synced lcx-contribute-bug-fix skill #when inspected #then it contri
31
35
 
32
36
  // then
33
37
  assert.match(skill, /^---\r?\nname: lcx-contribute-bug-fix\r?\n/m);
38
+ assert.match(skill, /NEVER open a PR or push a branch against this repo/);
39
+ assert.match(skill, /gh issue create --repo code-yeongyu\/lazycodex/);
40
+ assert.match(skill, /gh pr create --repo openai\/codex/);
41
+ assert.doesNotMatch(skill, /gh pr create --repo "\$TARGET_REPO"/);
42
+ assert.doesNotMatch(skill, /gh pr create --repo code-yeongyu\/lazycodex/);
34
43
  assert.match(interfaceMetadata, /display_name: "lcx-contribute-bug-fix \(omo\)"/);
35
44
  assert.match(interfaceMetadata, /- "contribute a bug fix"/);
36
45
  assert.match(interfaceMetadata, /- "fix bug pr"/);
@@ -30,6 +30,30 @@ test("#given ulw-plan skill #when the planning gate is inspected #then it explor
30
30
  assert.doesNotMatch(skill, /Proceeding to plan generation/);
31
31
  });
32
32
 
33
+ test("#given ulw-plan skill #when the execution gate is inspected #then plan mode is sticky, execution needs an explicit start, and the high-accuracy ask is mandatory", async () => {
34
+ // given
35
+ const skill = await readFile(skillPath, "utf8");
36
+
37
+ // then
38
+ assert.match(skill, /plan mode is sticky/i);
39
+ assert.match(skill, /never\s+(?:start|begin)[^.]{0,80}(?:implement|execut)/i);
40
+ assert.match(skill, /ask[^.]{0,160}high[- ]accuracy/i);
41
+ assert.match(skill, /two filters/i);
42
+ assert.match(skill, /\bWHY\b/);
43
+ });
44
+
45
+ test("#given ulw-plan full workflow reference #when the delivery phase is inspected #then it mandates the start-or-high-accuracy question and forbids self-started execution", async () => {
46
+ // given
47
+ const workflow = await readFile(workflowPath, "utf8");
48
+
49
+ // then
50
+ assert.match(workflow, /plan mode is sticky/i);
51
+ assert.match(workflow, /two filters/i);
52
+ assert.match(workflow, /ask[^.]{0,160}high[- ]accuracy/i);
53
+ assert.match(workflow, /execution belongs to the worker/i);
54
+ assert.doesNotMatch(workflow, /High-accuracy review \(optional\)/);
55
+ });
56
+
33
57
  test("#given ulw-plan full workflow reference #when inspected #then it documents the approval gate and .omo plan output with Codex-native tools only", async () => {
34
58
  // given
35
59
  const workflow = await readFile(workflowPath, "utf8");
@@ -41,3 +65,25 @@ test("#given ulw-plan full workflow reference #when inspected #then it documents
41
65
  assert.doesNotMatch(workflow, opencodeOnlyToolPattern);
42
66
  assert.doesNotMatch(workflow, /Proceeding to plan generation/);
43
67
  });
68
+
69
+ test("#given ulw-plan approval gate #when inspected #then it is a durable decision rule that ends the approval loop (lazycodex #48)", async () => {
70
+ // given
71
+ const skill = await readFile(skillPath, "utf8");
72
+ const workflow = await readFile(workflowPath, "utf8");
73
+ const combined = `${skill}\n${workflow}`;
74
+
75
+ // then — a durable approval-pending checkpoint guards against re-running exploration after compaction
76
+ assert.match(combined, /awaiting-approval/);
77
+ assert.match(combined, /instead of re-running exploration/i);
78
+
79
+ // then — approval is decided from intent, not a fixed passphrase
80
+ assert.match(combined, /"proceed"/);
81
+ assert.match(combined, /"write the plan"/);
82
+
83
+ // then — approval authorizes writing the plan, never implementation (resolves sticky-mode ambiguity)
84
+ assert.match(combined, /authoriz[^.]{0,80}writing the plan/i);
85
+ assert.match(combined, /never authorization to implement/i);
86
+
87
+ // then — the still-unclear path is a single prompt, never a re-exploration loop
88
+ assert.match(combined, /do not re-explore/i);
89
+ });
@@ -0,0 +1,82 @@
1
+ import assert from "node:assert/strict";
2
+ import { lstat, mkdir, mkdtemp, readFile, readdir, symlink, writeFile } from "node:fs/promises";
3
+ import { tmpdir } from "node:os";
4
+ import { join } from "node:path";
5
+ import test from "node:test";
6
+
7
+ import { isRetriableRenameError, writeFileAtomic } from "./install/atomic-write.mjs";
8
+
9
+ test("#given a fresh target path #when writeFileAtomic writes content #then the file holds exactly that content with no temp residue", async () => {
10
+ // given
11
+ const root = await mkdtemp(join(tmpdir(), "omo-atomic-write-fresh-"));
12
+ const target = join(root, "config.toml");
13
+
14
+ // when
15
+ await writeFileAtomic(target, "alpha = 1\n");
16
+
17
+ // then
18
+ assert.equal(await readFile(target, "utf8"), "alpha = 1\n");
19
+ assert.deepEqual(await readdir(root), ["config.toml"]);
20
+ });
21
+
22
+ test("#given an existing file #when writeFileAtomic overwrites it #then the content is replaced with no temp residue", async () => {
23
+ // given
24
+ const root = await mkdtemp(join(tmpdir(), "omo-atomic-write-replace-"));
25
+ const target = join(root, "config.toml");
26
+ await writeFile(target, "old = true\n");
27
+
28
+ // when
29
+ await writeFileAtomic(target, "new = true\n");
30
+
31
+ // then
32
+ assert.equal(await readFile(target, "utf8"), "new = true\n");
33
+ const residue = (await readdir(root)).filter((entry) => entry.startsWith(".tmp-"));
34
+ assert.deepEqual(residue, []);
35
+ });
36
+
37
+ test("#given the rename target cannot be replaced #when writeFileAtomic fails #then the original is untouched and the temp file is cleaned up", async () => {
38
+ // given
39
+ const root = await mkdtemp(join(tmpdir(), "omo-atomic-write-fail-"));
40
+ const target = join(root, "config.toml");
41
+ await mkdir(target);
42
+ await writeFile(join(target, "sentinel"), "keep\n");
43
+
44
+ // when
45
+ await assert.rejects(writeFileAtomic(target, "should not land\n"));
46
+
47
+ // then
48
+ assert.equal(await readFile(join(target, "sentinel"), "utf8"), "keep\n");
49
+ const residue = (await readdir(root)).filter((entry) => entry.startsWith(".tmp-"));
50
+ assert.deepEqual(residue, []);
51
+ });
52
+
53
+ test("#given the target is a symlink #when writeFileAtomic writes #then it writes through to the link target and preserves the symlink", async () => {
54
+ // given
55
+ const root = await mkdtemp(join(tmpdir(), "omo-atomic-write-symlink-"));
56
+ const realTarget = join(root, "actual.toml");
57
+ const link = join(root, "config.toml");
58
+ await writeFile(realTarget, "old = true\n");
59
+ await symlink(realTarget, link);
60
+
61
+ // when
62
+ await writeFileAtomic(link, "new = true\n");
63
+
64
+ // then
65
+ assert.equal(await readFile(realTarget, "utf8"), "new = true\n");
66
+ assert.equal((await lstat(link)).isSymbolicLink(), true);
67
+ const residue = (await readdir(root)).filter((entry) => entry.startsWith(".tmp-"));
68
+ assert.deepEqual(residue, []);
69
+ });
70
+
71
+ test("#given various rename errors #when classifying retriability #then only Windows file-lock codes are retriable", () => {
72
+ // given
73
+ const busy = Object.assign(new Error("busy"), { code: "EBUSY" });
74
+ const perm = Object.assign(new Error("perm"), { code: "EPERM" });
75
+ const missing = Object.assign(new Error("missing"), { code: "ENOENT" });
76
+
77
+ // when / then
78
+ assert.equal(isRetriableRenameError(busy), true);
79
+ assert.equal(isRetriableRenameError(perm), true);
80
+ assert.equal(isRetriableRenameError(missing), false);
81
+ assert.equal(isRetriableRenameError("not an error"), false);
82
+ });
@@ -0,0 +1,18 @@
1
+ export interface LinkedPluginAgent {
2
+ name: string;
3
+ path: string;
4
+ target: string;
5
+ }
6
+
7
+ export declare function capturePreservedAgentReasoning(options: { codexHome: string }): Promise<Map<string, string>>;
8
+
9
+ export declare function capturePreservedAgentServiceTier(options: {
10
+ codexHome: string;
11
+ }): Promise<Map<string, string | null>>;
12
+
13
+ export declare function linkCachedPluginAgents(options: {
14
+ codexHome: string;
15
+ pluginRoot: string;
16
+ preservedReasoning?: Map<string, string>;
17
+ preservedServiceTier?: Map<string, string | null>;
18
+ }): Promise<LinkedPluginAgent[]>;
@@ -22,7 +22,22 @@ export async function capturePreservedAgentReasoning({ codexHome }) {
22
22
  return preserved;
23
23
  }
24
24
 
25
- export async function linkCachedPluginAgents({ codexHome, pluginRoot, preservedReasoning = new Map() }) {
25
+ export async function capturePreservedAgentServiceTier({ codexHome }) {
26
+ const agentsDir = join(codexHome, "agents");
27
+ if (!(await exists(agentsDir))) return new Map();
28
+
29
+ const preserved = new Map();
30
+ const agentEntries = await readdir(agentsDir, { withFileTypes: true });
31
+ for (const entry of agentEntries) {
32
+ if (!entry.name.endsWith(".toml")) continue;
33
+ const content = await readTextIfExists(join(agentsDir, entry.name));
34
+ if (content === null) continue;
35
+ preserved.set(agentNameFromToml(entry.name), extractServiceTier(content));
36
+ }
37
+ return preserved;
38
+ }
39
+
40
+ export async function linkCachedPluginAgents({ codexHome, pluginRoot, preservedReasoning = new Map(), preservedServiceTier = new Map() }) {
26
41
  const bundledAgents = await discoverBundledAgents(pluginRoot);
27
42
  if (bundledAgents.length === 0) {
28
43
  await writeManifest(pluginRoot, []);
@@ -38,12 +53,26 @@ export async function linkCachedPluginAgents({ codexHome, pluginRoot, preservedR
38
53
  const linkPath = join(agentsDir, agentFileName);
39
54
  await replaceWithCopy(linkPath, agentPath);
40
55
  await restorePreservedReasoning({ linkPath, target: agentPath, value: preservedReasoning.get(agentName) });
56
+ await restorePreservedServiceTier({
57
+ linkPath,
58
+ preserved: preservedServiceTier.has(agentName),
59
+ value: preservedServiceTier.get(agentName) ?? null,
60
+ });
41
61
  linked.push({ name: agentFileName, path: linkPath, target: agentPath });
42
62
  }
43
63
  await writeManifest(pluginRoot, linked.map((entry) => entry.path));
44
64
  return linked;
45
65
  }
46
66
 
67
+ async function restorePreservedServiceTier({ linkPath, preserved, value }) {
68
+ if (!preserved) return;
69
+ const content = await readFile(linkPath, "utf8");
70
+ if (extractServiceTier(content) === value) return;
71
+ const replacement = replaceServiceTier(content, value);
72
+ if (!replacement.replaced) return;
73
+ await writeFile(linkPath, replacement.content);
74
+ }
75
+
47
76
  async function discoverBundledAgents(pluginRoot) {
48
77
  const componentsRoot = join(pluginRoot, "components");
49
78
  if (!(await exists(componentsRoot))) return [];
@@ -106,29 +135,73 @@ async function readTextIfExists(path) {
106
135
  }
107
136
 
108
137
  function extractReasoningEffort(content) {
138
+ return extractTopLevelStringSetting(content, "model_reasoning_effort");
139
+ }
140
+
141
+ function extractServiceTier(content) {
142
+ return extractTopLevelStringSetting(content, "service_tier");
143
+ }
144
+
145
+ function extractTopLevelStringSetting(content, key) {
109
146
  for (const line of content.split(/\n/)) {
110
147
  if (isSectionHeader(line)) return null;
111
- const match = line.match(/^\s*model_reasoning_effort\s*=\s*("(?:[^"\\]|\\.)*")/);
112
- if (match === null) continue;
113
- return JSON.parse(match[1]);
148
+ const rawValue = topLevelStringSettingRawValue(line, key);
149
+ if (rawValue === undefined) continue;
150
+ return JSON.parse(rawValue);
114
151
  }
115
152
  return null;
116
153
  }
117
154
 
118
155
  function replaceReasoningEffort(content, value) {
156
+ return replaceTopLevelStringSetting(content, "model_reasoning_effort", value, { insertIfMissing: false });
157
+ }
158
+
159
+ function replaceServiceTier(content, value) {
160
+ return replaceTopLevelStringSetting(content, "service_tier", value, { insertIfMissing: true });
161
+ }
162
+
163
+ function replaceTopLevelStringSetting(content, key, value, options) {
119
164
  let replaced = false;
120
165
  const lines = content.split(/\n/);
121
166
  for (let index = 0; index < lines.length; index += 1) {
122
167
  const line = lines[index];
123
168
  if (isSectionHeader(line)) break;
124
- if (!/^\s*model_reasoning_effort\s*=/.test(line)) continue;
169
+ if (topLevelStringSettingRawValue(line, key) === undefined) continue;
170
+ if (value === null) {
171
+ lines.splice(index, 1);
172
+ replaced = true;
173
+ break;
174
+ }
125
175
  lines[index] = line.replace(/=\s*"(?:[^"\\]|\\.)*"/, `= ${JSON.stringify(value)}`);
126
176
  replaced = true;
127
177
  break;
128
178
  }
179
+ if (!replaced && value !== null && options.insertIfMissing) {
180
+ lines.splice(topLevelInsertionIndex(lines), 0, `${key} = ${JSON.stringify(value)}`);
181
+ replaced = true;
182
+ }
129
183
  return { content: lines.join("\n"), replaced };
130
184
  }
131
185
 
186
+ function topLevelStringSettingRawValue(line, key) {
187
+ const match = line.match(/^\s*([A-Za-z0-9_]+)\s*=\s*("(?:[^"\\]|\\.)*")/);
188
+ if (match === null) return undefined;
189
+ const settingKey = match[1];
190
+ const rawValue = match[2];
191
+ if (settingKey !== key || rawValue === undefined) return undefined;
192
+ return rawValue;
193
+ }
194
+
195
+ function topLevelInsertionIndex(lines) {
196
+ const sectionIndex = lines.findIndex((line) => isSectionHeader(line));
197
+ const topLevelEnd = sectionIndex === -1 ? lines.length : sectionIndex;
198
+ let insertionIndex = topLevelEnd;
199
+ while (insertionIndex > 0 && lines[insertionIndex - 1] === "") {
200
+ insertionIndex -= 1;
201
+ }
202
+ return insertionIndex;
203
+ }
204
+
132
205
  function isSectionHeader(line) {
133
206
  const trimmed = line.trim();
134
207
  return trimmed.startsWith("[") && trimmed.endsWith("]");
@@ -0,0 +1,59 @@
1
+ import { lstat, readlink, realpath, rename, unlink, writeFile } from "node:fs/promises";
2
+ import { basename, dirname, isAbsolute, join, resolve } from "node:path";
3
+
4
+ const RENAME_RETRY_DELAYS_MS = [10, 25, 50];
5
+ const RETRIABLE_RENAME_CODES = new Set(["EPERM", "EBUSY"]);
6
+
7
+ export function isRetriableRenameError(error) {
8
+ if (!(error instanceof Error)) return false;
9
+ return RETRIABLE_RENAME_CODES.has(Reflect.get(error, "code"));
10
+ }
11
+
12
+ export async function writeFileAtomic(targetPath, data) {
13
+ const writeTarget = await resolveSymlinkTarget(targetPath);
14
+ const temporaryPath = join(
15
+ dirname(writeTarget),
16
+ `.tmp-${basename(writeTarget)}-${process.pid}-${Date.now()}`,
17
+ );
18
+ await writeFile(temporaryPath, data);
19
+ try {
20
+ await renameWithRetry(temporaryPath, writeTarget);
21
+ } catch (renameError) {
22
+ await unlink(temporaryPath).catch(() => {});
23
+ throw renameError;
24
+ }
25
+ }
26
+
27
+ async function resolveSymlinkTarget(targetPath) {
28
+ let linkStats;
29
+ try {
30
+ linkStats = await lstat(targetPath);
31
+ } catch {
32
+ return targetPath;
33
+ }
34
+ if (!linkStats.isSymbolicLink()) return targetPath;
35
+ try {
36
+ return await realpath(targetPath);
37
+ } catch {
38
+ const linkValue = await readlink(targetPath);
39
+ return isAbsolute(linkValue) ? linkValue : resolve(dirname(targetPath), linkValue);
40
+ }
41
+ }
42
+
43
+ async function renameWithRetry(fromPath, toPath) {
44
+ for (let attempt = 0; ; attempt += 1) {
45
+ try {
46
+ await rename(fromPath, toPath);
47
+ return;
48
+ } catch (renameError) {
49
+ if (!isRetriableRenameError(renameError) || attempt >= RENAME_RETRY_DELAYS_MS.length) {
50
+ throw renameError;
51
+ }
52
+ await delay(RENAME_RETRY_DELAYS_MS[attempt]);
53
+ }
54
+ }
55
+ }
56
+
57
+ function delay(milliseconds) {
58
+ return new Promise((resolve) => setTimeout(resolve, milliseconds));
59
+ }
@@ -0,0 +1,7 @@
1
+ export declare function resolveCodexInstallerBinDir(options?: {
2
+ codexHome?: string;
3
+ env?: Record<string, string | undefined>;
4
+ homeDir?: string;
5
+ }): string;
6
+
7
+ export declare function nonEmptyEnvValue(env: Record<string, string | undefined>, key: string): string | undefined;
@@ -0,0 +1,18 @@
1
+ export interface LinkedPluginBin {
2
+ name: string;
3
+ path: string;
4
+ target: string;
5
+ }
6
+
7
+ export declare function linkCachedPluginBins(options: {
8
+ binDir: string;
9
+ pluginRoot: string;
10
+ platform?: string;
11
+ }): Promise<LinkedPluginBin[]>;
12
+
13
+ export declare function linkRootRuntimeBin(options: {
14
+ binDir: string;
15
+ codexHome: string;
16
+ repoRoot: string;
17
+ platform?: string;
18
+ }): Promise<LinkedPluginBin | null>;