oh-my-opencode 4.5.12 → 4.6.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 (147) hide show
  1. package/.agents/skills/opencode-qa/SKILL.md +194 -0
  2. package/.agents/skills/opencode-qa/references/cli-commands.md +188 -0
  3. package/.agents/skills/opencode-qa/references/db-investigation.md +197 -0
  4. package/.agents/skills/opencode-qa/references/events-hooks.md +110 -0
  5. package/.agents/skills/opencode-qa/references/sdk.md +96 -0
  6. package/.agents/skills/opencode-qa/references/server-api.md +200 -0
  7. package/.agents/skills/opencode-qa/references/testing-harness.md +218 -0
  8. package/.agents/skills/opencode-qa/references/tui-tmux.md +52 -0
  9. package/.agents/skills/opencode-qa/scripts/db-session-by-id.sh +53 -0
  10. package/.agents/skills/opencode-qa/scripts/db-session-by-name.sh +57 -0
  11. package/.agents/skills/opencode-qa/scripts/db-session-by-text.sh +158 -0
  12. package/.agents/skills/opencode-qa/scripts/export-roundtrip.sh +57 -0
  13. package/.agents/skills/opencode-qa/scripts/lib/common.sh +216 -0
  14. package/.agents/skills/opencode-qa/scripts/server-smoke.sh +64 -0
  15. package/.agents/skills/opencode-qa/scripts/sse-hook-probe.sh +106 -0
  16. package/.agents/skills/opencode-qa/scripts/tui-smoke.sh +89 -0
  17. package/README.ja.md +13 -3
  18. package/README.ko.md +13 -3
  19. package/README.md +24 -14
  20. package/README.ru.md +13 -3
  21. package/README.zh-cn.md +13 -3
  22. package/bin/oh-my-opencode.js +4 -3
  23. package/bin/oh-my-opencode.test.ts +35 -7
  24. package/bin/platform.d.ts +1 -1
  25. package/bin/platform.js +4 -4
  26. package/bin/platform.test.ts +31 -9
  27. package/dist/cli/cleanup-command.d.ts +4 -0
  28. package/dist/cli/cleanup.d.ts +11 -0
  29. package/dist/cli/cli-program.d.ts +2 -1
  30. package/dist/cli/index.js +1837 -450
  31. package/dist/cli/install-codex/codex-cache.d.ts +1 -0
  32. package/dist/cli/install-codex/codex-cleanup-config.d.ts +6 -0
  33. package/dist/cli/install-codex/codex-cleanup.d.ts +21 -0
  34. package/dist/cli/install-codex/codex-config-mcp.d.ts +1 -0
  35. package/dist/cli/install-codex/codex-config-permissions.d.ts +1 -0
  36. package/dist/cli/install-codex/codex-config-reasoning.d.ts +1 -0
  37. package/dist/cli/install-codex/codex-config-toml.d.ts +2 -1
  38. package/dist/cli/install-codex/codex-installation-detection.d.ts +36 -0
  39. package/dist/cli/install-codex/codex-package-layout.d.ts +1 -0
  40. package/dist/cli/install-codex/codex-project-local-cleanup-best-effort.d.ts +7 -0
  41. package/dist/cli/install-codex/codex-project-local-cleanup.d.ts +35 -0
  42. package/dist/cli/install-codex/git-bash.d.ts +35 -0
  43. package/dist/cli/install-codex/index.d.ts +4 -0
  44. package/dist/cli/install-codex/toml-section-editor.d.ts +2 -0
  45. package/dist/cli/install-codex/types.d.ts +20 -0
  46. package/dist/cli/run/event-state.d.ts +1 -0
  47. package/dist/cli/run/poll-for-completion.d.ts +1 -0
  48. package/dist/cli/run/prompt-start.d.ts +7 -0
  49. package/dist/cli/star-request.d.ts +9 -0
  50. package/dist/config/schema/hooks.d.ts +0 -1
  51. package/dist/create-hooks.d.ts +0 -1
  52. package/dist/features/builtin-skills/skills/debugging.d.ts +2 -0
  53. package/dist/features/builtin-skills/skills/index.d.ts +1 -0
  54. package/dist/hooks/index.d.ts +0 -1
  55. package/dist/index.js +267 -114
  56. package/dist/plugin/hooks/create-core-hooks.d.ts +0 -1
  57. package/dist/plugin/hooks/create-session-hooks.d.ts +1 -2
  58. package/dist/plugin/messages-transform.d.ts +8 -1
  59. package/dist/plugin/user-abort-interrupted-recovery-guard.d.ts +6 -0
  60. package/dist/shared/prompt-async-gate/recent-dispatches.d.ts +14 -0
  61. package/dist/shared/prompt-async-gate/semantic-dedupe.d.ts +7 -0
  62. package/dist/shared/prompt-async-gate/session-idle-dispatch.d.ts +1 -0
  63. package/dist/shared/prompt-async-gate/timing.d.ts +1 -0
  64. package/dist/shared/prompt-async-gate/types.d.ts +2 -0
  65. package/dist/shared/prompt-async-gate.d.ts +1 -1
  66. package/package.json +22 -17
  67. package/packages/git-bash-mcp/dist/cli.js +367 -0
  68. package/packages/omo-codex/plugin/.mcp.json +11 -0
  69. package/packages/omo-codex/plugin/components/comment-checker/README.md +1 -1
  70. package/packages/omo-codex/plugin/components/git-bash/hooks/hooks.json +29 -0
  71. package/packages/omo-codex/plugin/components/git-bash/package.json +23 -0
  72. package/packages/omo-codex/plugin/components/git-bash/src/cli.ts +33 -0
  73. package/packages/omo-codex/plugin/components/git-bash/src/codex-hook.ts +180 -0
  74. package/packages/omo-codex/plugin/components/git-bash/src/index.ts +10 -0
  75. package/packages/omo-codex/plugin/components/git-bash/test/codex-hook.test.ts +195 -0
  76. package/packages/omo-codex/plugin/components/git-bash/tsconfig.build.json +13 -0
  77. package/packages/omo-codex/plugin/components/git-bash/tsconfig.json +25 -0
  78. package/packages/omo-codex/plugin/components/lsp/README.md +1 -1
  79. package/packages/omo-codex/plugin/components/lsp/src/cli.ts +5 -5
  80. package/packages/omo-codex/plugin/components/lsp/src/codex-hook-cli.ts +33 -0
  81. package/packages/omo-codex/plugin/components/lsp/src/codex-hook.ts +19 -27
  82. package/packages/omo-codex/plugin/components/lsp/test/codex-hook-cli.test.ts +28 -0
  83. package/packages/omo-codex/plugin/components/lsp/test/codex-hook-errors.test.ts +55 -0
  84. package/packages/omo-codex/plugin/components/lsp/test/package-smoke.test.ts +7 -5
  85. package/packages/omo-codex/plugin/components/rules/README.md +1 -1
  86. package/packages/omo-codex/plugin/components/rules/bundled-rules/windows-git-bash.md +10 -0
  87. package/packages/omo-codex/plugin/components/rules/test/package-smoke.test.ts +3 -1
  88. package/packages/omo-codex/plugin/components/rules/test/windows-git-bash-bundled-rule.test.ts +97 -0
  89. package/packages/omo-codex/plugin/components/start-work-continuation/directive.md +5 -4
  90. package/packages/omo-codex/plugin/components/start-work-continuation/test/codex-hook.test.ts +22 -0
  91. package/packages/omo-codex/plugin/components/ultrawork/README.md +2 -2
  92. package/packages/omo-codex/plugin/components/ultrawork/agents/codex-ultrawork-reviewer.toml +1 -0
  93. package/packages/omo-codex/plugin/components/ultrawork/agents/librarian.toml +8 -7
  94. package/packages/omo-codex/plugin/components/ultrawork/agents/plan.toml +2 -1
  95. package/packages/omo-codex/plugin/components/ultrawork/directive.md +31 -5
  96. package/packages/omo-codex/plugin/components/ultrawork/test/codex-hook.test.ts +27 -4
  97. package/packages/omo-codex/plugin/components/ultrawork/test/package-smoke.test.ts +25 -0
  98. package/packages/omo-codex/plugin/components/ulw-loop/README.md +1 -1
  99. package/packages/omo-codex/plugin/components/ulw-loop/skills/ulw-loop/SKILL.md +27 -205
  100. package/packages/omo-codex/plugin/components/ulw-loop/skills/ulw-loop/references/full-workflow.md +230 -0
  101. package/packages/omo-codex/plugin/components/ulw-loop/test/package-smoke.test.ts +102 -5
  102. package/packages/omo-codex/plugin/hooks/hooks.json +24 -2
  103. package/packages/omo-codex/plugin/package-lock.json +19 -0
  104. package/packages/omo-codex/plugin/package.json +3 -1
  105. package/packages/omo-codex/plugin/scripts/build-bundled-mcp-runtimes.mjs +16 -1
  106. package/packages/omo-codex/plugin/scripts/build-components.mjs +2 -1
  107. package/packages/omo-codex/plugin/scripts/sync-hook-status-messages.mjs +87 -0
  108. package/packages/omo-codex/plugin/skills/review-work/SKILL.md +27 -2
  109. package/packages/omo-codex/plugin/skills/start-work/SKILL.md +20 -0
  110. package/packages/omo-codex/plugin/skills/ulw-loop/SKILL.md +27 -205
  111. package/packages/omo-codex/plugin/skills/ulw-loop/references/full-workflow.md +230 -0
  112. package/packages/omo-codex/plugin/test/aggregate.test.mjs +23 -8
  113. package/packages/omo-codex/plugin/test/hook-status-message.test.mjs +56 -11
  114. package/packages/omo-codex/plugin/test/install-time-build-runtime.test.mjs +34 -0
  115. package/packages/omo-codex/plugin/test/mcp-research-servers.test.mjs +21 -0
  116. package/packages/omo-codex/plugin/test/node-install-surface.test.mjs +48 -0
  117. package/packages/omo-codex/plugin/test/subagent-guidance.test.mjs +76 -0
  118. package/packages/omo-codex/plugin/test/sync-hook-status-messages.test.mjs +66 -0
  119. package/packages/omo-codex/plugin/test/sync-skills.test.mjs +32 -2
  120. package/packages/omo-codex/scripts/install/cache.mjs +5 -3
  121. package/packages/omo-codex/scripts/install/cli-args.mjs +112 -0
  122. package/packages/omo-codex/scripts/install/config.mjs +36 -1
  123. package/packages/omo-codex/scripts/install/delegated-command.mjs +25 -0
  124. package/packages/omo-codex/scripts/install/git-bash.mjs +99 -0
  125. package/packages/omo-codex/scripts/install/git-bash.test.mjs +174 -0
  126. package/packages/omo-codex/scripts/install/mcp-runtime-cache.mjs +5 -1
  127. package/packages/omo-codex/scripts/install/multi-agent-v2-config.mjs +7 -1
  128. package/packages/omo-codex/scripts/install/permissions.d.mts +1 -0
  129. package/packages/omo-codex/scripts/install/permissions.mjs +26 -0
  130. package/packages/omo-codex/scripts/install/project-local-cleanup.mjs +229 -0
  131. package/packages/omo-codex/scripts/install/reasoning-config.mjs +14 -0
  132. package/packages/omo-codex/scripts/install/source-package-build.mjs +20 -0
  133. package/packages/omo-codex/scripts/install/toml-editor.mjs +19 -2
  134. package/packages/omo-codex/scripts/install-cli-args.test.mjs +146 -0
  135. package/packages/omo-codex/scripts/install-config-autonomous.test.mjs +48 -0
  136. package/packages/omo-codex/scripts/install-config-reasoning.test.mjs +62 -0
  137. package/packages/omo-codex/scripts/install-config.test.mjs +206 -0
  138. package/packages/omo-codex/scripts/install-local-entrypoint.test.mjs +129 -0
  139. package/packages/omo-codex/scripts/install-local-git-bash-preflight.test.mjs +145 -0
  140. package/packages/omo-codex/scripts/install-local.mjs +91 -8
  141. package/packages/omo-codex/scripts/install-local.test.mjs +15 -0
  142. package/packages/omo-codex/scripts/install-mcp-runtime.test.mjs +60 -0
  143. package/packages/omo-codex/scripts/install-packaged-local.test.mjs +67 -0
  144. package/packages/omo-codex/scripts/install-project-local-cleanup.test.mjs +277 -0
  145. package/packages/shared-skills/skills/review-work/SKILL.md +27 -2
  146. package/packages/shared-skills/skills/start-work/SKILL.md +20 -0
  147. package/dist/hooks/context-window-monitor.d.ts +0 -19
@@ -0,0 +1,25 @@
1
+ export async function runDelegatedOmoCommand(parsed, options) {
2
+ const invocation = buildDelegatedOmoInvocation(parsed);
3
+ if (parsed.dryRun) {
4
+ options.log(`${invocation.command} ${invocation.args.join(" ")}`);
5
+ return;
6
+ }
7
+ await options.runCommand(invocation.command, invocation.args, { cwd: options.cwd });
8
+ }
9
+
10
+ export function buildDelegatedOmoInvocation(parsed) {
11
+ const args = ["--yes", "--package", "oh-my-openagent", "omo", parsed.command];
12
+ if (parsed.command === "install") {
13
+ args.push("--platform=codex");
14
+ if (parsed.noTui) args.push("--no-tui");
15
+ if (parsed.skipAuth) args.push("--skip-auth");
16
+ if (parsed.autonomousPermissions === true) args.push("--codex-autonomous");
17
+ if (parsed.autonomousPermissions === false) args.push("--no-codex-autonomous");
18
+ if (parsed.repoRoot) args.push(`--repo-root=${parsed.repoRoot}`);
19
+ } else if (parsed.command === "cleanup") {
20
+ args.push("--platform=codex", ...parsed.args);
21
+ } else {
22
+ args.push(...parsed.args);
23
+ }
24
+ return { command: "npx", args };
25
+ }
@@ -0,0 +1,99 @@
1
+ import { execFileSync } from "node:child_process";
2
+ import { existsSync } from "node:fs";
3
+
4
+ const GIT_BASH_ENV_KEY = "OMO_CODEX_GIT_BASH_PATH";
5
+ const SKIP_GIT_BASH_AUTO_INSTALL_ENV_KEY = "OMO_CODEX_SKIP_GIT_BASH_AUTO_INSTALL";
6
+ const PROGRAM_FILES_GIT_BASH = "C:\\Program Files\\Git\\bin\\bash.exe";
7
+ const PROGRAM_FILES_X86_GIT_BASH = "C:\\Program Files (x86)\\Git\\bin\\bash.exe";
8
+ const WINGET_INSTALL_ARGS = ["install", "--id", "Git.Git", "-e", "--source", "winget"];
9
+
10
+ export function resolveGitBash({ platform, env, exists, where }) {
11
+ if (platform !== "win32") return { found: true, path: null, source: "not-required" };
12
+
13
+ const checkedPaths = [];
14
+ const envPath = nonEmptyEnvValue(env, GIT_BASH_ENV_KEY);
15
+ if (envPath !== undefined) {
16
+ checkedPaths.push(envPath);
17
+ if (isBashExePath(envPath) && exists(envPath)) return { found: true, path: envPath, source: "env" };
18
+ return missingGitBash(checkedPaths);
19
+ }
20
+
21
+ for (const candidate of [
22
+ { path: PROGRAM_FILES_GIT_BASH, source: "program-files" },
23
+ { path: PROGRAM_FILES_X86_GIT_BASH, source: "program-files-x86" },
24
+ ]) {
25
+ checkedPaths.push(candidate.path);
26
+ if (exists(candidate.path)) return { found: true, path: candidate.path, source: candidate.source };
27
+ }
28
+
29
+ for (const pathCandidate of where("bash")) {
30
+ const candidate = pathCandidate.trim();
31
+ if (candidate.length === 0) continue;
32
+ checkedPaths.push(candidate);
33
+ if (isBashExePath(candidate) && exists(candidate)) return { found: true, path: candidate, source: "path" };
34
+ }
35
+
36
+ return missingGitBash(checkedPaths);
37
+ }
38
+
39
+ export function resolveGitBashForCurrentProcess(options = {}) {
40
+ return resolveGitBash({
41
+ platform: options.platform ?? process.platform,
42
+ env: options.env ?? process.env,
43
+ exists: existsSync,
44
+ where: whereCommand,
45
+ });
46
+ }
47
+
48
+ export async function prepareGitBashForInstall(options) {
49
+ const resolveGitBashWithDefaults = options.resolveGitBash
50
+ ?? (() => resolveGitBashForCurrentProcess({ platform: options.platform, env: options.env }));
51
+ const initialResolution = resolveGitBashWithDefaults();
52
+ if (options.platform !== "win32" || initialResolution.found) return initialResolution;
53
+ if (options.env[SKIP_GIT_BASH_AUTO_INSTALL_ENV_KEY] === "1") return initialResolution;
54
+
55
+ try {
56
+ await options.runCommand("winget", WINGET_INSTALL_ARGS, { cwd: options.cwd });
57
+ } catch (error) {
58
+ if (!(error instanceof Error)) throw error;
59
+ return initialResolution;
60
+ }
61
+
62
+ return resolveGitBashWithDefaults();
63
+ }
64
+
65
+ function missingGitBash(checkedPaths) {
66
+ return {
67
+ found: false,
68
+ checkedPaths,
69
+ installHint: [
70
+ "Git Bash is required for native Windows Codex profile installs.",
71
+ "Install it with: winget install --id Git.Git -e --source winget",
72
+ `For a custom install, set ${GIT_BASH_ENV_KEY}=C:\\path\\to\\bash.exe`,
73
+ "Then rerun `npx lazycodex-ai install`.",
74
+ ].join("\n"),
75
+ };
76
+ }
77
+
78
+ function nonEmptyEnvValue(env, key) {
79
+ const value = env[key];
80
+ if (typeof value !== "string") return undefined;
81
+ const trimmed = value.trim();
82
+ return trimmed.length === 0 ? undefined : trimmed;
83
+ }
84
+
85
+ function isBashExePath(path) {
86
+ return path.toLowerCase().endsWith("bash.exe");
87
+ }
88
+
89
+ function whereCommand(command) {
90
+ try {
91
+ return execFileSync("where", [command], { encoding: "utf8" })
92
+ .split(/\r?\n/)
93
+ .map((line) => line.trim())
94
+ .filter((line) => line.length > 0);
95
+ } catch (error) {
96
+ if (error instanceof Error) return [];
97
+ throw error;
98
+ }
99
+ }
@@ -0,0 +1,174 @@
1
+ import assert from "node:assert/strict";
2
+ import test from "node:test";
3
+
4
+ import { prepareGitBashForInstall, resolveGitBash } from "./git-bash.mjs";
5
+
6
+ const programFilesGitBash = "C:\\Program Files\\Git\\bin\\bash.exe";
7
+ const programFilesX86GitBash = "C:\\Program Files (x86)\\Git\\bin\\bash.exe";
8
+
9
+ test("#given non-Windows platform #when resolving Git Bash #then no preflight is required", () => {
10
+ const result = resolveGitBash({
11
+ platform: "linux",
12
+ env: {},
13
+ exists: () => false,
14
+ where: () => [],
15
+ });
16
+
17
+ assert.deepEqual(result, { found: true, path: null, source: "not-required" });
18
+ });
19
+
20
+ test("#given Windows env override to bash.exe #when the file exists #then env path wins", () => {
21
+ const overridePath = "D:\\Tools\\Git\\bin\\bash.exe";
22
+ const result = resolveGitBash({
23
+ platform: "win32",
24
+ env: { OMO_CODEX_GIT_BASH_PATH: overridePath },
25
+ exists: (path) => path === overridePath,
26
+ where: () => [programFilesGitBash],
27
+ });
28
+
29
+ assert.deepEqual(result, { found: true, path: overridePath, source: "env" });
30
+ });
31
+
32
+ test("#given Windows standard paths are absent and PATH contains bash #when resolving #then uses where bash candidate", () => {
33
+ const pathCandidate = "E:\\Git\\bin\\bash.exe";
34
+ const result = resolveGitBash({
35
+ platform: "win32",
36
+ env: {},
37
+ exists: (path) => path === pathCandidate,
38
+ where: () => ["C:\\Windows\\System32\\bash.exe", pathCandidate],
39
+ });
40
+
41
+ assert.deepEqual(result, { found: true, path: pathCandidate, source: "path" });
42
+ });
43
+
44
+ test("#given Windows invalid env override #when resolving #then returns guidance without falling through", () => {
45
+ const overridePath = "D:\\Tools\\Git\\bin\\git.exe";
46
+ const result = resolveGitBash({
47
+ platform: "win32",
48
+ env: { OMO_CODEX_GIT_BASH_PATH: overridePath },
49
+ exists: () => true,
50
+ where: () => [programFilesGitBash],
51
+ });
52
+
53
+ assert.equal(result.found, false);
54
+ assert.deepEqual(result.checkedPaths, [overridePath]);
55
+ assert.match(result.installHint, /OMO_CODEX_GIT_BASH_PATH=C:\\path\\to\\bash\.exe/);
56
+ });
57
+
58
+ test("#given Windows without Git Bash #when resolving #then returns install guidance", () => {
59
+ const result = resolveGitBash({
60
+ platform: "win32",
61
+ env: {},
62
+ exists: () => false,
63
+ where: () => [],
64
+ });
65
+
66
+ assert.equal(result.found, false);
67
+ assert.deepEqual(result.checkedPaths, [programFilesGitBash, programFilesX86GitBash]);
68
+ assert.match(result.installHint, /winget install --id Git\.Git -e --source winget/);
69
+ assert.match(result.installHint, /rerun `npx lazycodex-ai install`/);
70
+ assert.doesNotMatch(result.installHint, /bunx/);
71
+ });
72
+
73
+ test("#given Windows without Git Bash and winget is allowed #when preparing #then winget runs and resolver retries", async () => {
74
+ const runCalls = [];
75
+ const resolutions = [
76
+ { found: false, checkedPaths: [programFilesGitBash], installHint: "install hint" },
77
+ { found: true, path: programFilesGitBash, source: "program-files" },
78
+ ];
79
+ let resolveCallCount = 0;
80
+
81
+ const result = await prepareGitBashForInstall({
82
+ platform: "win32",
83
+ env: {},
84
+ cwd: "C:\\repo",
85
+ resolveGitBash: () => resolutions[resolveCallCount++] ?? resolutions[resolutions.length - 1],
86
+ runCommand: async (command, args, options) => {
87
+ runCalls.push([command, ...args, options.cwd].join(" "));
88
+ },
89
+ });
90
+
91
+ assert.deepEqual(runCalls, ["winget install --id Git.Git -e --source winget C:\\repo"]);
92
+ assert.equal(resolveCallCount, 2);
93
+ assert.deepEqual(result, { found: true, path: programFilesGitBash, source: "program-files" });
94
+ });
95
+
96
+ test("#given Windows without Git Bash and skip env is set #when preparing #then winget is not run and install hint remains", async () => {
97
+ const runCalls = [];
98
+ const missingResolution = {
99
+ found: false,
100
+ checkedPaths: [programFilesGitBash, programFilesX86GitBash],
101
+ installHint: "install hint",
102
+ };
103
+
104
+ const result = await prepareGitBashForInstall({
105
+ platform: "win32",
106
+ env: { OMO_CODEX_SKIP_GIT_BASH_AUTO_INSTALL: "1" },
107
+ cwd: "C:\\repo",
108
+ resolveGitBash: () => missingResolution,
109
+ runCommand: async (command, args, options) => {
110
+ runCalls.push([command, ...args, options.cwd].join(" "));
111
+ },
112
+ });
113
+
114
+ assert.deepEqual(runCalls, []);
115
+ assert.deepEqual(result, missingResolution);
116
+ });
117
+
118
+ test("#given non-Windows platform #when preparing #then winget is never called", async () => {
119
+ const runCalls = [];
120
+ const result = await prepareGitBashForInstall({
121
+ platform: "linux",
122
+ env: {},
123
+ cwd: "/repo",
124
+ runCommand: async (command, args, options) => {
125
+ runCalls.push([command, ...args, options.cwd].join(" "));
126
+ },
127
+ });
128
+
129
+ assert.deepEqual(runCalls, []);
130
+ assert.deepEqual(result, { found: true, path: null, source: "not-required" });
131
+ });
132
+
133
+ test("#given Windows without Git Bash and winget fails #when preparing #then original install hint is preserved", async () => {
134
+ const missingResolution = {
135
+ found: false,
136
+ checkedPaths: [programFilesGitBash, programFilesX86GitBash],
137
+ installHint: "install hint",
138
+ };
139
+
140
+ const result = await prepareGitBashForInstall({
141
+ platform: "win32",
142
+ env: {},
143
+ cwd: "C:\\repo",
144
+ resolveGitBash: () => missingResolution,
145
+ runCommand: async () => {
146
+ throw new Error("winget unavailable");
147
+ },
148
+ });
149
+
150
+ assert.deepEqual(result, missingResolution);
151
+ });
152
+
153
+ test("#given Windows without Git Bash and winget succeeds but bash is still missing #when preparing #then install hint remains", async () => {
154
+ const missingResolution = {
155
+ found: false,
156
+ checkedPaths: [programFilesGitBash, programFilesX86GitBash],
157
+ installHint: "install hint",
158
+ };
159
+ let resolveCallCount = 0;
160
+
161
+ const result = await prepareGitBashForInstall({
162
+ platform: "win32",
163
+ env: {},
164
+ cwd: "C:\\repo",
165
+ resolveGitBash: () => {
166
+ resolveCallCount += 1;
167
+ return missingResolution;
168
+ },
169
+ runCommand: async () => {},
170
+ });
171
+
172
+ assert.equal(resolveCallCount, 2);
173
+ assert.deepEqual(result, missingResolution);
174
+ });
@@ -40,7 +40,7 @@ function resolveExternalMcpPackageRoot(runtimePath, sourceRoot) {
40
40
  if (!isPathInside(runtimePath, packagesRoot)) return undefined;
41
41
  let packageRoot = dirname(runtimePath);
42
42
  while (packageRoot !== packagesRoot) {
43
- if (existsSync(join(packageRoot, "package.json")) && isPathInside(runtimePath, join(packageRoot, "dist"))) {
43
+ if (isPathInside(runtimePath, join(packageRoot, "dist")) && isRuntimePackageRoot(packageRoot)) {
44
44
  return packageRoot;
45
45
  }
46
46
  const parent = dirname(packageRoot);
@@ -50,6 +50,10 @@ function resolveExternalMcpPackageRoot(runtimePath, sourceRoot) {
50
50
  return undefined;
51
51
  }
52
52
 
53
+ function isRuntimePackageRoot(packageRoot) {
54
+ return existsSync(join(packageRoot, "package.json")) || existsSync(join(packageRoot, "dist"));
55
+ }
56
+
53
57
  function findPackagesRoot(path) {
54
58
  let current = resolve(path);
55
59
  for (let index = 0; index < 8; index++) {
@@ -4,7 +4,7 @@ const CODEX_MULTI_AGENT_V2_HEADER = "features.multi_agent_v2";
4
4
  const CODEX_MULTI_AGENT_V2_MAX_CONCURRENT_THREADS_PER_SESSION = 10000;
5
5
 
6
6
  export function ensureCodexMultiAgentV2Config(config) {
7
- const normalizedConfig = removeFeatureFlagSetting(config, "multi_agent_v2");
7
+ const normalizedConfig = removeLegacyAgentsMaxThreadsSetting(removeFeatureFlagSetting(config, "multi_agent_v2"));
8
8
  const section = findTomlSection(normalizedConfig, CODEX_MULTI_AGENT_V2_HEADER);
9
9
  const maxThreadsValue = CODEX_MULTI_AGENT_V2_MAX_CONCURRENT_THREADS_PER_SESSION.toString();
10
10
  if (!section) {
@@ -30,3 +30,9 @@ function removeFeatureFlagSetting(config, featureName) {
30
30
  if (!section) return config;
31
31
  return removeSetting(config, section, featureName);
32
32
  }
33
+
34
+ function removeLegacyAgentsMaxThreadsSetting(config) {
35
+ const section = findTomlSection(config, "agents");
36
+ if (!section) return config;
37
+ return removeSetting(config, section, "max_threads");
38
+ }
@@ -0,0 +1 @@
1
+ export function ensureAutonomousPermissions(config: string): string;
@@ -0,0 +1,26 @@
1
+ import { appendBlock, findTomlSection, removeSetting, replaceOrInsertRootSetting, replaceOrInsertSetting } from "./toml-editor.mjs";
2
+
3
+ export function ensureAutonomousPermissions(config) {
4
+ let next = replaceOrInsertRootSetting(config, "approval_policy", JSON.stringify("never"));
5
+ next = replaceOrInsertRootSetting(next, "sandbox_mode", JSON.stringify("danger-full-access"));
6
+ next = replaceOrInsertRootSetting(next, "network_access", JSON.stringify("enabled"));
7
+ next = removeWindowsSandboxSetting(next);
8
+ next = ensureNoticeEnabled(next, "hide_full_access_warning");
9
+ return ensureNoticeEnabled(next, "hide_world_writable_warning");
10
+ }
11
+
12
+ function removeWindowsSandboxSetting(config) {
13
+ const section = findTomlSection(config, "windows");
14
+ if (!section) return config;
15
+ return removeSetting(config, section, "sandbox");
16
+ }
17
+
18
+ function ensureNoticeEnabled(config, key) {
19
+ const section = findTomlSection(config, "notice");
20
+ if (!section) return appendNoticeBlock(config, key);
21
+ return replaceOrInsertSetting(config, section, key, "true");
22
+ }
23
+
24
+ function appendNoticeBlock(config, key) {
25
+ return appendBlock(config, `[notice]\n${key} = true\n`);
26
+ }
@@ -0,0 +1,229 @@
1
+ import { copyFile, lstat, readFile, writeFile } from "node:fs/promises";
2
+ import { dirname, join, resolve } from "node:path";
3
+
4
+ import { escapeRegExp, findTomlSection, removeSetting } from "./toml-editor.mjs";
5
+
6
+ const LEGACY_AGENT_CONFLICT_KEYS = ["max_threads"];
7
+ const PROJECT_LOCAL_ARTIFACT_PATHS = [
8
+ ".omx",
9
+ ".codex/hooks.json",
10
+ ".codex/agents",
11
+ ".codex/prompts",
12
+ ".codex/skills",
13
+ ];
14
+
15
+ export async function repairNearestProjectLocalCodexArtifacts({ startDirectory, codexHome, now = () => new Date() }) {
16
+ const project = await findProjectLocalCodexConfigs(startDirectory, codexHome);
17
+ if (project === null) {
18
+ return emptyProjectLocalCodexCleanupResult();
19
+ }
20
+
21
+ const artifacts = await collectProjectLocalArtifacts(project.artifactRoots);
22
+ const configs = [];
23
+ for (const configPath of project.configPaths) {
24
+ const original = await readFile(configPath, "utf8");
25
+ const repair = repairProjectLocalCodexConfigText(original);
26
+ if (!repair.changed) {
27
+ configs.push({
28
+ projectRoot: project.projectRoot,
29
+ configPath,
30
+ changed: false,
31
+ removedKeys: repair.removedKeys,
32
+ });
33
+ continue;
34
+ }
35
+
36
+ const backupPath = `${configPath}.backup-${formatBackupTimestamp(now())}`;
37
+ await copyFile(configPath, backupPath);
38
+ await writeFile(configPath, `${repair.config.trimEnd()}\n`);
39
+ configs.push({
40
+ projectRoot: project.projectRoot,
41
+ configPath,
42
+ changed: true,
43
+ removedKeys: repair.removedKeys,
44
+ backupPath,
45
+ });
46
+ }
47
+
48
+ const changedConfigs = configs.filter((config) => config.changed);
49
+ const nearestChangedConfig = lastValue(changedConfigs);
50
+ const nearestConfig = lastValue(configs);
51
+ return {
52
+ projectRoot: project.projectRoot,
53
+ configPath: nearestChangedConfig?.configPath ?? nearestConfig?.configPath ?? null,
54
+ changed: changedConfigs.length > 0,
55
+ removedKeys: uniqueRemovedKeys(changedConfigs),
56
+ backupPath: nearestChangedConfig?.backupPath,
57
+ configs,
58
+ artifacts,
59
+ };
60
+ }
61
+
62
+ export function emptyProjectLocalCodexCleanupResult() {
63
+ return {
64
+ projectRoot: null,
65
+ configPath: null,
66
+ changed: false,
67
+ removedKeys: [],
68
+ configs: [],
69
+ artifacts: [],
70
+ };
71
+ }
72
+
73
+ function uniqueRemovedKeys(configs) {
74
+ const keys = [];
75
+ for (const config of configs) {
76
+ for (const key of config.removedKeys) {
77
+ if (!keys.includes(key)) keys.push(key);
78
+ }
79
+ }
80
+ return keys;
81
+ }
82
+
83
+ function lastValue(values) {
84
+ return values.length > 0 ? (values[values.length - 1] ?? null) : null;
85
+ }
86
+
87
+ export function repairProjectLocalCodexConfigText(config) {
88
+ if (!isMultiAgentV2Enabled(config)) return { config, changed: false, removedKeys: [] };
89
+
90
+ let nextConfig = config;
91
+ const removedKeys = [];
92
+ for (const key of LEGACY_AGENT_CONFLICT_KEYS) {
93
+ const section = findTomlSection(nextConfig, "agents");
94
+ if (section === null || !hasSetting(section.text, key)) continue;
95
+ nextConfig = removeSetting(nextConfig, section, key);
96
+ removedKeys.push(key);
97
+ }
98
+
99
+ return {
100
+ config: nextConfig,
101
+ changed: removedKeys.length > 0,
102
+ removedKeys,
103
+ };
104
+ }
105
+
106
+ async function findProjectLocalCodexConfigs(startDirectory, codexHome) {
107
+ if (typeof startDirectory !== "string" || startDirectory.includes("\0")) return null;
108
+
109
+ const startDirectoryStat = await maybeLstat(startDirectory);
110
+ if (startDirectoryStat !== null && !startDirectoryStat.isDirectory()) {
111
+ throw new ProjectLocalCleanupStartDirectoryError(startDirectory);
112
+ }
113
+
114
+ const codexHomeConfigPath = codexHome === undefined ? null : join(resolve(codexHome), "config.toml");
115
+ let current = resolve(startDirectory);
116
+ const configPathsFromCwd = [];
117
+ while (true) {
118
+ const configPath = join(current, ".codex", "config.toml");
119
+ if (await isRegularProjectLocalConfig(current, configPath)) {
120
+ if (codexHomeConfigPath === null || resolve(configPath) !== codexHomeConfigPath) {
121
+ configPathsFromCwd.push(configPath);
122
+ }
123
+ }
124
+
125
+ if (await exists(join(current, ".git"))) {
126
+ return configPathsFromCwd.length === 0
127
+ ? null
128
+ : {
129
+ projectRoot: current,
130
+ configPaths: [...configPathsFromCwd].reverse(),
131
+ artifactRoots: artifactRootsForConfigPaths(configPathsFromCwd),
132
+ };
133
+ }
134
+
135
+ const parent = dirname(current);
136
+ if (parent === current) {
137
+ const nearestConfigPath = configPathsFromCwd[0];
138
+ return nearestConfigPath === undefined
139
+ ? null
140
+ : {
141
+ projectRoot: dirname(dirname(nearestConfigPath)),
142
+ configPaths: [nearestConfigPath],
143
+ artifactRoots: [dirname(dirname(nearestConfigPath))],
144
+ };
145
+ }
146
+ current = parent;
147
+ }
148
+ }
149
+
150
+ async function isRegularProjectLocalConfig(directory, configPath) {
151
+ const codexDirStat = await maybeLstat(join(directory, ".codex"));
152
+ if (codexDirStat === null || !codexDirStat.isDirectory() || codexDirStat.isSymbolicLink()) return false;
153
+ const configStat = await maybeLstat(configPath);
154
+ return configStat !== null && configStat.isFile() && !configStat.isSymbolicLink();
155
+ }
156
+
157
+ function artifactRootsForConfigPaths(configPaths) {
158
+ const roots = [];
159
+ for (const configPath of configPaths) {
160
+ const root = dirname(dirname(configPath));
161
+ if (!roots.includes(root)) roots.push(root);
162
+ }
163
+ return roots.reverse();
164
+ }
165
+
166
+ async function collectProjectLocalArtifacts(projectRoots) {
167
+ const artifacts = [];
168
+ const seenPaths = new Set();
169
+ for (const projectRoot of projectRoots) {
170
+ for (const relativePath of PROJECT_LOCAL_ARTIFACT_PATHS) {
171
+ const artifactPath = join(projectRoot, relativePath);
172
+ if (seenPaths.has(artifactPath)) continue;
173
+ const entryStat = await maybeLstat(artifactPath);
174
+ if (entryStat === null) continue;
175
+ seenPaths.add(artifactPath);
176
+ artifacts.push({
177
+ relativePath,
178
+ path: artifactPath,
179
+ kind: entryStat.isDirectory() ? "directory" : entryStat.isFile() ? "file" : "other",
180
+ });
181
+ }
182
+ }
183
+ return artifacts;
184
+ }
185
+
186
+ function isMultiAgentV2Enabled(config) {
187
+ const featuresSection = findTomlSection(config, "features");
188
+ if (featuresSection !== null && settingIsBooleanTrue(featuresSection.text, "multi_agent_v2")) return true;
189
+
190
+ const multiAgentSection = findTomlSection(config, "features.multi_agent_v2");
191
+ return multiAgentSection !== null && settingIsBooleanTrue(multiAgentSection.text, "enabled");
192
+ }
193
+
194
+ function settingIsBooleanTrue(sectionText, key) {
195
+ return new RegExp(`^\\s*${escapeRegExp(key)}\\s*=\\s*true\\s*(?:#.*)?$`, "m").test(sectionText);
196
+ }
197
+
198
+ function hasSetting(sectionText, key) {
199
+ return new RegExp(`^\\s*${escapeRegExp(key)}\\s*=`, "m").test(sectionText);
200
+ }
201
+
202
+ function formatBackupTimestamp(date) {
203
+ return date.toISOString().replace(/[:.]/g, "-");
204
+ }
205
+
206
+ async function maybeLstat(path) {
207
+ try {
208
+ return await lstat(path);
209
+ } catch (error) {
210
+ if (nodeErrorCode(error) === "ENOENT") return null;
211
+ throw error;
212
+ }
213
+ }
214
+
215
+ async function exists(path) {
216
+ return (await maybeLstat(path)) !== null;
217
+ }
218
+
219
+ function nodeErrorCode(error) {
220
+ if (!(error instanceof Error) || !("code" in error)) return null;
221
+ return typeof error.code === "string" ? error.code : null;
222
+ }
223
+
224
+ class ProjectLocalCleanupStartDirectoryError extends Error {
225
+ constructor(startDirectory) {
226
+ super(`Project-local Codex cleanup start path is not a directory: ${startDirectory}`);
227
+ this.name = "ProjectLocalCleanupStartDirectoryError";
228
+ }
229
+ }
@@ -0,0 +1,14 @@
1
+ import { replaceOrInsertRootSetting } from "./toml-editor.mjs";
2
+
3
+ const DEFAULT_MODE_REASONING_EFFORT = "high";
4
+ const PLAN_MODE_REASONING_EFFORT = "xhigh";
5
+
6
+ export function ensureCodexReasoningConfig(config) {
7
+ let next = replaceOrInsertRootSetting(
8
+ config,
9
+ "model_reasoning_effort",
10
+ JSON.stringify(DEFAULT_MODE_REASONING_EFFORT),
11
+ );
12
+ next = replaceOrInsertRootSetting(next, "plan_mode_reasoning_effort", JSON.stringify(PLAN_MODE_REASONING_EFFORT));
13
+ return next;
14
+ }
@@ -0,0 +1,20 @@
1
+ import { existsSync } from "node:fs";
2
+ import { readFile } from "node:fs/promises";
3
+ import { join } from "node:path";
4
+
5
+ const PACKAGED_INSTALLER_PACKAGE_NAMES = new Set([
6
+ "@code-yeongyu/lazycodex",
7
+ "@code-yeongyu/lazycodex-ai",
8
+ "lazycodex",
9
+ "lazycodex-ai",
10
+ "oh-my-opencode",
11
+ "oh-my-openagent",
12
+ ]);
13
+
14
+ export async function shouldBuildSourcePackages(repoRoot) {
15
+ if (existsSync(join(repoRoot, "src", "index.ts"))) return true;
16
+ const packageJsonPath = join(repoRoot, "package.json");
17
+ if (!existsSync(packageJsonPath)) return true;
18
+ const packageJson = JSON.parse(await readFile(packageJsonPath, "utf8"));
19
+ return !PACKAGED_INSTALLER_PACKAGE_NAMES.has(packageJson?.name);
20
+ }
@@ -26,22 +26,39 @@ export function replaceOrInsertSetting(config, section, key, value) {
26
26
  }
27
27
 
28
28
  export function removeSetting(config, section, key) {
29
- const linePattern = new RegExp(`^${escapeRegExp(key)}\\s*=.*(?:\\n|$)`, "m");
29
+ const linePattern = new RegExp(`^\\s*${escapeRegExp(key)}\\s*=.*(?:\\n|$)`, "m");
30
30
  const replacement = section.text.replace(linePattern, "");
31
31
  return config.slice(0, section.start) + replacement + config.slice(section.end);
32
32
  }
33
33
 
34
+ export function replaceOrInsertRootSetting(config, key, value) {
35
+ const sectionStart = findFirstTableStart(config);
36
+ const root = config.slice(0, sectionStart);
37
+ const suffix = config.slice(sectionStart);
38
+ const linePattern = new RegExp(`^${escapeRegExp(key)}\\s*=.*$`, "m");
39
+ const replacement = linePattern.test(root)
40
+ ? root.replace(linePattern, `${key} = ${value}`)
41
+ : `${root.trimEnd()}${root.trimEnd().length > 0 ? "\n" : ""}${key} = ${value}\n`;
42
+ if (suffix.length === 0) return replacement;
43
+ return `${replacement.trimEnd()}\n\n${suffix.trimStart()}`;
44
+ }
45
+
34
46
  export function appendBlock(config, block) {
35
47
  const prefix = config.trimEnd();
36
48
  return `${prefix}${prefix.length > 0 ? "\n\n" : ""}${block.trimEnd()}\n`;
37
49
  }
38
50
 
51
+ function findFirstTableStart(config) {
52
+ const match = config.match(/^[[].*$/m);
53
+ return match?.index ?? config.length;
54
+ }
55
+
39
56
  function insertSetting(sectionText, key, value) {
40
57
  const lines = sectionText.split("\n");
41
58
  lines.splice(1, 0, `${key} = ${value}`);
42
59
  return lines.join("\n");
43
60
  }
44
61
 
45
- function escapeRegExp(value) {
62
+ export function escapeRegExp(value) {
46
63
  return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
47
64
  }