oh-my-opencode 4.6.0 → 4.7.1

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 (90) hide show
  1. package/bin/version-mismatch.js +47 -0
  2. package/bin/version-mismatch.test.ts +120 -0
  3. package/dist/cli/codex-ulw-loop.d.ts +12 -0
  4. package/dist/cli/doctor/checks/tui-plugin-config.d.ts +2 -0
  5. package/dist/cli/index.js +5999 -5542
  6. package/dist/cli/install-codex/codex-config-reasoning.d.ts +2 -1
  7. package/dist/cli/install-codex/codex-model-catalog.d.ts +13 -0
  8. package/dist/features/background-agent/concurrency.d.ts +1 -0
  9. package/dist/features/background-agent/process-cleanup.d.ts +6 -0
  10. package/dist/features/claude-code-session-state/state.d.ts +1 -0
  11. package/dist/features/opencode-skill-loader/index.d.ts +1 -0
  12. package/dist/features/opencode-skill-loader/opencode-config-skills-reader.d.ts +5 -0
  13. package/dist/features/tmux-subagent/attachable-session-status.d.ts +1 -1
  14. package/dist/features/tmux-subagent/session-status-parser.d.ts +1 -0
  15. package/dist/hooks/comment-checker/cli.d.ts +1 -0
  16. package/dist/hooks/tasks-todowrite-disabler/constants.d.ts +1 -1
  17. package/dist/index.js +4250 -3776
  18. package/dist/shared/command-executor/execute-hook-command.d.ts +2 -0
  19. package/dist/tools/skill/description-formatter.d.ts +5 -1
  20. package/dist/tools/skill/types.d.ts +1 -0
  21. package/package.json +13 -14
  22. package/packages/ast-grep-mcp/dist/cli.js +53 -9
  23. package/packages/lsp-tools-mcp/dist/lsp/process.js +1 -1
  24. package/packages/omo-codex/plugin/components/lsp/hooks/hooks.json +13 -0
  25. package/packages/omo-codex/plugin/components/lsp/src/cli.ts +6 -2
  26. package/packages/omo-codex/plugin/components/lsp/src/codex-hook-cli.ts +13 -2
  27. package/packages/omo-codex/plugin/components/lsp/src/codex-hook.ts +30 -79
  28. package/packages/omo-codex/plugin/components/lsp/src/lsp-session-state.ts +116 -0
  29. package/packages/omo-codex/plugin/components/lsp/src/mutated-file-paths.ts +88 -0
  30. package/packages/omo-codex/plugin/components/lsp/test/codex-hook-unavailable.test.ts +206 -0
  31. package/packages/omo-codex/plugin/components/lsp/test/package-smoke.test.ts +5 -3
  32. package/packages/omo-codex/plugin/components/rules/bundled-rules/hephaestus.md +6 -4
  33. package/packages/omo-codex/plugin/components/rules/src/codex-hook-options.ts +1 -0
  34. package/packages/omo-codex/plugin/components/rules/src/post-compact-budget.ts +0 -2
  35. package/packages/omo-codex/plugin/components/rules/src/rules/finder.ts +15 -2
  36. package/packages/omo-codex/plugin/components/rules/src/rules-engine-factory.ts +4 -1
  37. package/packages/omo-codex/plugin/components/rules/test/windows-git-bash-bundled-rule.test.ts +28 -5
  38. package/packages/omo-codex/plugin/components/start-work-continuation/directive.md +1 -1
  39. package/packages/omo-codex/plugin/components/ultrawork/CHANGELOG.md +1 -1
  40. package/packages/omo-codex/plugin/components/ultrawork/README.md +1 -1
  41. package/packages/omo-codex/plugin/components/ultrawork/agents/codex-ultrawork-reviewer.toml +3 -1
  42. package/packages/omo-codex/plugin/components/ultrawork/agents/plan.toml +7 -7
  43. package/packages/omo-codex/plugin/components/ultrawork/directive.md +1 -1
  44. package/packages/omo-codex/plugin/components/ulw-loop/skills/ulw-loop/SKILL.md +5 -4
  45. package/packages/omo-codex/plugin/components/ulw-loop/skills/ulw-loop/references/full-workflow.md +4 -3
  46. package/packages/omo-codex/plugin/components/ulw-loop/src/checkpoint.ts +12 -1
  47. package/packages/omo-codex/plugin/components/ulw-loop/test/checkpoint.test.ts +19 -1
  48. package/packages/omo-codex/plugin/hooks/hooks.json +24 -2
  49. package/packages/omo-codex/plugin/model-catalog.json +49 -0
  50. package/packages/omo-codex/plugin/scripts/auto-update.mjs +159 -0
  51. package/packages/omo-codex/plugin/scripts/migrate-codex-config.mjs +269 -0
  52. package/packages/omo-codex/plugin/scripts/sync-hook-status-messages.mjs +4 -9
  53. package/packages/omo-codex/plugin/scripts/sync-skills.mjs +6 -6
  54. package/packages/omo-codex/plugin/skills/init-deep/SKILL.md +6 -6
  55. package/packages/omo-codex/plugin/skills/lcx-report-bug/SKILL.md +127 -0
  56. package/packages/omo-codex/plugin/skills/lcx-report-bug/agents/openai.yaml +9 -0
  57. package/packages/omo-codex/plugin/skills/refactor/SKILL.md +6 -6
  58. package/packages/omo-codex/plugin/skills/remove-ai-slops/SKILL.md +6 -6
  59. package/packages/omo-codex/plugin/skills/review-work/SKILL.md +7 -7
  60. package/packages/omo-codex/plugin/skills/start-work/SKILL.md +6 -6
  61. package/packages/omo-codex/plugin/skills/ulw-loop/SKILL.md +5 -4
  62. package/packages/omo-codex/plugin/skills/ulw-loop/references/full-workflow.md +4 -3
  63. package/packages/omo-codex/plugin/skills/ulw-plan/SKILL.md +17 -17
  64. package/packages/omo-codex/plugin/test/aggregate.test.mjs +188 -19
  65. package/packages/omo-codex/plugin/test/auto-update.test.mjs +129 -0
  66. package/packages/omo-codex/plugin/test/hook-status-message.test.mjs +7 -27
  67. package/packages/omo-codex/plugin/test/migrate-codex-config.test.mjs +146 -0
  68. package/packages/omo-codex/plugin/test/sync-hook-status-messages.test.mjs +27 -1
  69. package/packages/omo-codex/plugin/test/sync-skills.test.mjs +22 -0
  70. package/packages/omo-codex/scripts/install/cli-args.mjs +1 -1
  71. package/packages/omo-codex/scripts/install/config.mjs +2 -15
  72. package/packages/omo-codex/scripts/install/delegated-command.mjs +1 -1
  73. package/packages/omo-codex/scripts/install/legacy-bins.mjs +1 -0
  74. package/packages/omo-codex/scripts/install/model-catalog.mjs +66 -0
  75. package/packages/omo-codex/scripts/install/permissions.mjs +11 -0
  76. package/packages/omo-codex/scripts/install/reasoning-config.mjs +65 -7
  77. package/packages/omo-codex/scripts/install-bin-links.test.mjs +23 -0
  78. package/packages/omo-codex/scripts/install-config-autonomous-features.test.mjs +83 -0
  79. package/packages/omo-codex/scripts/install-config-reasoning.test.mjs +82 -3
  80. package/packages/omo-codex/scripts/install-config.test.mjs +5 -6
  81. package/packages/omo-codex/scripts/install-local-entrypoint.test.mjs +30 -2
  82. package/packages/omo-codex/scripts/install-local.mjs +1 -1
  83. package/packages/omo-codex/scripts/install-local.test.mjs +3 -1
  84. package/packages/shared-skills/skills/lcx-report-bug/SKILL.md +127 -0
  85. package/packages/shared-skills/skills/lcx-report-bug/agents/openai.yaml +9 -0
  86. package/packages/shared-skills/skills/review-work/SKILL.md +7 -7
  87. package/packages/shared-skills/skills/start-work/SKILL.md +6 -6
  88. package/packages/shared-skills/skills/ulw-plan/SKILL.md +11 -11
  89. package/postinstall.mjs +36 -3
  90. package/dist/cli/install-codex/codex-config-mcp.d.ts +0 -1
@@ -20,9 +20,12 @@ test("#given a component without hooks #when hook status messages sync #then bui
20
20
  await mkdir(join(root, ".codex-plugin"), { recursive: true });
21
21
  await mkdir(join(root, "hooks"), { recursive: true });
22
22
  await mkdir(join(root, "components", "comment-checker", "hooks"), { recursive: true });
23
+ await mkdir(join(root, "components", "lsp", "hooks"), { recursive: true });
23
24
  await mkdir(join(root, "components", "git-bash"), { recursive: true });
25
+ await mkdir(join(root, "components", "stale-build-output", "dist"), { recursive: true });
24
26
  await writeJson(join(root, ".codex-plugin", "plugin.json"), { version: "0.1.0" });
25
27
  await writeJson(join(root, "components", "comment-checker", "package.json"), { version: "0.1.1" });
28
+ await writeJson(join(root, "components", "lsp", "package.json"), { version: "0.2.0" });
26
29
  await writeJson(join(root, "components", "git-bash", "package.json"), { version: "0.3.0" });
27
30
  await writeJson(join(root, "hooks", "hooks.json"), {
28
31
  hooks: {
@@ -34,6 +37,11 @@ test("#given a component without hooks #when hook status messages sync #then bui
34
37
  command: 'node "${PLUGIN_ROOT}/components/comment-checker/dist/cli.js" hook post-tool-use',
35
38
  statusMessage: "LazyCodex(0.1.0): Checking Comments",
36
39
  },
40
+ {
41
+ type: "command",
42
+ command: 'node "${PLUGIN_ROOT}/components/lsp/dist/cli.js" hook post-tool-use',
43
+ statusMessage: "LazyCodex(0.1.0): Checking LSP Diagnostics",
44
+ },
37
45
  ],
38
46
  },
39
47
  ],
@@ -54,6 +62,21 @@ test("#given a component without hooks #when hook status messages sync #then bui
54
62
  ],
55
63
  },
56
64
  });
65
+ await writeJson(join(root, "components", "lsp", "hooks", "hooks.json"), {
66
+ hooks: {
67
+ PostToolUse: [
68
+ {
69
+ hooks: [
70
+ {
71
+ type: "command",
72
+ command: 'node "${PLUGIN_ROOT}/dist/cli.js" hook post-tool-use',
73
+ statusMessage: "LazyCodex(0.1.0): Checking LSP Diagnostics",
74
+ },
75
+ ],
76
+ },
77
+ ],
78
+ },
79
+ });
57
80
 
58
81
  // when
59
82
  await syncHookStatusMessages(root);
@@ -61,6 +84,9 @@ test("#given a component without hooks #when hook status messages sync #then bui
61
84
  // then
62
85
  const aggregateHooks = await readJson(join(root, "hooks", "hooks.json"));
63
86
  const componentHooks = await readJson(join(root, "components", "comment-checker", "hooks", "hooks.json"));
64
- assert.equal(aggregateHooks.hooks.PostToolUse[0].hooks[0].statusMessage, "LazyCodex(0.1.1): Checking Comments");
87
+ const lspHooks = await readJson(join(root, "components", "lsp", "hooks", "hooks.json"));
88
+ assert.equal(aggregateHooks.hooks.PostToolUse[0].hooks[0].statusMessage, "LazyCodex(0.1.0): Checking Comments");
89
+ assert.equal(aggregateHooks.hooks.PostToolUse[0].hooks[1].statusMessage, "LazyCodex(0.1.0): Checking LSP Diagnostics");
65
90
  assert.equal(componentHooks.hooks.PostToolUse[0].hooks[0].statusMessage, "LazyCodex(0.1.1): Checking Comments");
91
+ assert.equal(lspHooks.hooks.PostToolUse[0].hooks[0].statusMessage, "LazyCodex(0.2.0): Checking LSP Diagnostics");
66
92
  });
@@ -14,6 +14,7 @@ const expectedSkills = [
14
14
  "debugging",
15
15
  "frontend-ui-ux",
16
16
  "init-deep",
17
+ "lcx-report-bug",
17
18
  "lsp",
18
19
  "programming",
19
20
  "refactor",
@@ -33,6 +34,7 @@ const componentSkillSources = [
33
34
  ];
34
35
 
35
36
  const codexCompatibilityEndMarkers = [
37
+ "Codex full-history forks inherit the parent agent type, model, and reasoning effort, so role-specific spawns with `agent_type` must use a non-full-history fork mode such as `fork_turns=\"none\"`. Include any required conversation context, files, diffs, constraints, and requested skill names directly in the spawned agent's `message`. If a code block below conflicts with this section, this section wins.\n\n",
36
38
  "When translating `load_skills=[...]`, include the requested skill names in the spawned agent's `message`. If a code block below conflicts with this section, this section wins.\n\n",
37
39
  "When translating `load_skills=[...]`, name the skills inside the spawned agent's `message`. If a code block below conflicts with this section, this section wins.\n\n",
38
40
  ];
@@ -153,6 +155,26 @@ test("#given synced ulw-loop skill #when Codex hint metadata is inspected #then
153
155
  assert.match(interfaceMetadata, /- "ulw-loop"/);
154
156
  });
155
157
 
158
+ test("#given synced lcx-report-bug skill #when inspected #then it files LazyCodex bug issues from proven debugging evidence", async () => {
159
+ // given
160
+ const skillRoot = join(root, "skills", "lcx-report-bug");
161
+
162
+ // when
163
+ const skill = await readFile(join(skillRoot, "SKILL.md"), "utf8");
164
+ const interfaceMetadata = await readFile(join(skillRoot, "agents", "openai.yaml"), "utf8");
165
+
166
+ // then
167
+ assert.match(skill, /^---\r?\nname: lcx-report-bug\r?\n/m);
168
+ assert.match(skill, /code-yeongyu\/lazycodex/);
169
+ assert.match(skill, /\$omo:debugging/);
170
+ assert.match(skill, /gh issue create --repo code-yeongyu\/lazycodex/);
171
+ assert.match(skill, /Browser use fallback/);
172
+ assert.match(skill, /Computer use fallback/);
173
+ assert.match(skill, /## Issue Body Template/);
174
+ assert.match(interfaceMetadata, /display_name: "lcx-report-bug \(omo\)"/);
175
+ assert.match(interfaceMetadata, /- "lazycodex bug"/);
176
+ });
177
+
156
178
  test("#given synced ulw-loop skill #when worker guidance is inspected #then context-hygiene guidance matches the source", async () => {
157
179
  // given
158
180
  const sourceSkill = await readFile(
@@ -1,5 +1,5 @@
1
1
  const CODEX_ONLY_ERROR = "lazycodex-ai installs the Codex Light edition only. Use the omo installer for OpenCode or both-platform installs.";
2
- const PASSTHROUGH_COMMANDS = new Set(["doctor", "cleanup", "get-local-version", "boulder", "refresh-model-capabilities", "run"]);
2
+ const PASSTHROUGH_COMMANDS = new Set(["doctor", "cleanup", "get-local-version", "boulder", "refresh-model-capabilities", "run", "ulw-loop"]);
3
3
 
4
4
  export function parseLazyCodexInstallCliArgs(argv) {
5
5
  const args = [...argv];
@@ -2,6 +2,7 @@ import { mkdir, readFile, writeFile } from "node:fs/promises";
2
2
  import { dirname } from "node:path";
3
3
 
4
4
  import { ensureCodexMultiAgentV2Config } from "./multi-agent-v2-config.mjs";
5
+ import { readCodexModelCatalog } from "./model-catalog.mjs";
5
6
  import { ensureCodexReasoningConfig } from "./reasoning-config.mjs";
6
7
  import { ensureAutonomousPermissions } from "./permissions.mjs";
7
8
  import { appendBlock, findTomlSection, replaceOrInsertSetting } from "./toml-editor.mjs";
@@ -17,14 +18,6 @@ const MANAGED_CODEX_AGENT_NAMES = [
17
18
  "momus",
18
19
  "plan",
19
20
  ];
20
- const CONTEXT7_MCP_SERVER_HEADER = "mcp_servers.context7";
21
- const CONTEXT7_MCP_SERVER_BLOCK = [
22
- `[${CONTEXT7_MCP_SERVER_HEADER}]`,
23
- `command = "npx"`,
24
- `args = ["-y", "@upstash/context7-mcp", "--api-key", "YOUR_API_KEY"]`,
25
- `startup_timeout_sec = 20`,
26
- "",
27
- ].join("\n");
28
21
 
29
22
  export async function updateCodexConfig({
30
23
  configPath,
@@ -51,10 +44,9 @@ export async function updateCodexConfig({
51
44
  config = removeStaleManagedAgentBlocks(config, new Set(agentConfigs.map((agentConfig) => agentConfig.name)));
52
45
  config = ensureFeatureEnabled(config, "plugins");
53
46
  config = ensureFeatureEnabled(config, "plugin_hooks");
54
- config = ensureCodexReasoningConfig(config);
47
+ config = ensureCodexReasoningConfig(config, await readCodexModelCatalog(repoRoot));
55
48
  config = ensureCodexMultiAgentV2Config(config);
56
49
  if (autonomousPermissions === true) config = ensureAutonomousPermissions(config);
57
- config = ensureContext7McpServer(config);
58
50
  config = ensureMarketplaceBlock(config, marketplaceName, marketplaceSource);
59
51
  for (const pluginName of pluginNames) {
60
52
  config = ensurePluginEnabled(config, `${pluginName}@${marketplaceName}`);
@@ -145,11 +137,6 @@ function ensureMarketplaceBlock(config, marketplaceName, source) {
145
137
  return appendBlock(config, block);
146
138
  }
147
139
 
148
- function ensureContext7McpServer(config) {
149
- if (findTomlSection(config, CONTEXT7_MCP_SERVER_HEADER)) return config;
150
- return appendBlock(config, CONTEXT7_MCP_SERVER_BLOCK);
151
- }
152
-
153
140
  function ensurePluginEnabled(config, pluginKey) {
154
141
  const header = `plugins.${JSON.stringify(pluginKey)}`;
155
142
  const section = findTomlSection(config, header);
@@ -13,7 +13,7 @@ export function buildDelegatedOmoInvocation(parsed) {
13
13
  args.push("--platform=codex");
14
14
  if (parsed.noTui) args.push("--no-tui");
15
15
  if (parsed.skipAuth) args.push("--skip-auth");
16
- if (parsed.autonomousPermissions === true) args.push("--codex-autonomous");
16
+ if (parsed.autonomousPermissions !== false) args.push("--codex-autonomous");
17
17
  if (parsed.autonomousPermissions === false) args.push("--no-codex-autonomous");
18
18
  if (parsed.repoRoot) args.push(`--repo-root=${parsed.repoRoot}`);
19
19
  } else if (parsed.command === "cleanup") {
@@ -5,6 +5,7 @@ import { COMMAND_SHIM_MARKER } from "./command-shim.mjs";
5
5
 
6
6
  const LEGACY_CODEX_COMPONENT_BINS = [
7
7
  { name: "codex-comment-checker", component: "comment-checker" },
8
+ { name: "codex-lsp", component: "lsp" },
8
9
  { name: "codex-rules", component: "rules" },
9
10
  { name: "codex-start-work-continuation", component: "start-work-continuation" },
10
11
  { name: "codex-telemetry", component: "telemetry" },
@@ -0,0 +1,66 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import { join } from "node:path";
3
+
4
+ const FALLBACK_CODEX_MODEL_CATALOG = {
5
+ current: {
6
+ model: "gpt-5.5",
7
+ modelContextWindow: 400_000,
8
+ modelReasoningEffort: "high",
9
+ planModeReasoningEffort: "xhigh",
10
+ },
11
+ managedProfiles: [{ model: "gpt-5.2" }],
12
+ };
13
+
14
+ export async function readCodexModelCatalog(codexPackageRoot) {
15
+ try {
16
+ const parsed = JSON.parse(await readFile(join(codexPackageRoot, "plugin", "model-catalog.json"), "utf8"));
17
+ return parseCodexModelCatalog(parsed) ?? FALLBACK_CODEX_MODEL_CATALOG;
18
+ } catch (error) {
19
+ if (!(error instanceof Error)) throw error;
20
+ return FALLBACK_CODEX_MODEL_CATALOG;
21
+ }
22
+ }
23
+
24
+ export async function readCodexReasoningProfile(codexPackageRoot) {
25
+ return (await readCodexModelCatalog(codexPackageRoot)).current;
26
+ }
27
+
28
+ function parseCodexModelCatalog(value) {
29
+ if (!isRecord(value) || !isRecord(value.current) || !Array.isArray(value.managedProfiles)) return null;
30
+ const { current } = value;
31
+ if (
32
+ typeof current.model !== "string" ||
33
+ typeof current.model_context_window !== "number" ||
34
+ typeof current.model_reasoning_effort !== "string" ||
35
+ typeof current.plan_mode_reasoning_effort !== "string"
36
+ ) {
37
+ return null;
38
+ }
39
+ const managedProfiles = [];
40
+ for (const profile of value.managedProfiles) {
41
+ if (!isRecord(profile) || !isRecord(profile.match)) return null;
42
+ managedProfiles.push(parseProfileMatch(profile.match));
43
+ }
44
+ return {
45
+ current: {
46
+ model: current.model,
47
+ modelContextWindow: current.model_context_window,
48
+ modelReasoningEffort: current.model_reasoning_effort,
49
+ planModeReasoningEffort: current.plan_mode_reasoning_effort,
50
+ },
51
+ managedProfiles,
52
+ };
53
+ }
54
+
55
+ function parseProfileMatch(match) {
56
+ const profile = {};
57
+ if (typeof match.model === "string") profile.model = match.model;
58
+ if (typeof match.model_context_window === "number") profile.modelContextWindow = match.model_context_window;
59
+ if (typeof match.model_reasoning_effort === "string") profile.modelReasoningEffort = match.model_reasoning_effort;
60
+ if (typeof match.plan_mode_reasoning_effort === "string") profile.planModeReasoningEffort = match.plan_mode_reasoning_effort;
61
+ return profile;
62
+ }
63
+
64
+ function isRecord(value) {
65
+ return typeof value === "object" && value !== null && !Array.isArray(value);
66
+ }
@@ -1,9 +1,14 @@
1
1
  import { appendBlock, findTomlSection, removeSetting, replaceOrInsertRootSetting, replaceOrInsertSetting } from "./toml-editor.mjs";
2
2
 
3
+ const AUTONOMOUS_FEATURES = ["multi_agent", "child_agents_md", "unified_exec", "goals"];
4
+
3
5
  export function ensureAutonomousPermissions(config) {
4
6
  let next = replaceOrInsertRootSetting(config, "approval_policy", JSON.stringify("never"));
5
7
  next = replaceOrInsertRootSetting(next, "sandbox_mode", JSON.stringify("danger-full-access"));
6
8
  next = replaceOrInsertRootSetting(next, "network_access", JSON.stringify("enabled"));
9
+ for (const featureName of AUTONOMOUS_FEATURES) {
10
+ next = ensureFeatureEnabled(next, featureName);
11
+ }
7
12
  next = removeWindowsSandboxSetting(next);
8
13
  next = ensureNoticeEnabled(next, "hide_full_access_warning");
9
14
  return ensureNoticeEnabled(next, "hide_world_writable_warning");
@@ -21,6 +26,12 @@ function ensureNoticeEnabled(config, key) {
21
26
  return replaceOrInsertSetting(config, section, key, "true");
22
27
  }
23
28
 
29
+ function ensureFeatureEnabled(config, key) {
30
+ const section = findTomlSection(config, "features");
31
+ if (!section) return appendBlock(config, `[features]\n${key} = true\n`);
32
+ return replaceOrInsertSetting(config, section, key, "true");
33
+ }
34
+
24
35
  function appendNoticeBlock(config, key) {
25
36
  return appendBlock(config, `[notice]\n${key} = true\n`);
26
37
  }
@@ -1,14 +1,72 @@
1
1
  import { replaceOrInsertRootSetting } from "./toml-editor.mjs";
2
2
 
3
- const DEFAULT_MODE_REASONING_EFFORT = "high";
4
- const PLAN_MODE_REASONING_EFFORT = "xhigh";
3
+ const MANAGED_KEYS = ["model", "model_context_window", "model_reasoning_effort", "plan_mode_reasoning_effort"];
5
4
 
6
- export function ensureCodexReasoningConfig(config) {
7
- let next = replaceOrInsertRootSetting(
8
- config,
5
+ export function ensureCodexReasoningConfig(config, catalog) {
6
+ const current = readRootReasoningSettings(config);
7
+ if (
8
+ Object.keys(current).length > 0 &&
9
+ !matchesProfile(current, catalog.current) &&
10
+ !catalog.managedProfiles.some((profile) => matchesProfile(current, profile))
11
+ ) {
12
+ return config;
13
+ }
14
+ let next = replaceOrInsertRootSetting(config, "model", JSON.stringify(catalog.current.model));
15
+ next = replaceOrInsertRootSetting(next, "model_context_window", catalog.current.modelContextWindow.toString());
16
+ next = replaceOrInsertRootSetting(
17
+ next,
9
18
  "model_reasoning_effort",
10
- JSON.stringify(DEFAULT_MODE_REASONING_EFFORT),
19
+ JSON.stringify(catalog.current.modelReasoningEffort),
11
20
  );
12
- next = replaceOrInsertRootSetting(next, "plan_mode_reasoning_effort", JSON.stringify(PLAN_MODE_REASONING_EFFORT));
21
+ next = replaceOrInsertRootSetting(next, "plan_mode_reasoning_effort", JSON.stringify(catalog.current.planModeReasoningEffort));
13
22
  return next;
14
23
  }
24
+
25
+ function readRootReasoningSettings(config) {
26
+ const settings = {};
27
+ for (const line of config.split(/\n/)) {
28
+ if (isSectionHeader(line)) break;
29
+ for (const key of MANAGED_KEYS) {
30
+ if (!isRootSetting(line, key)) continue;
31
+ const value = parseTomlScalar(line.slice(line.indexOf("=") + 1));
32
+ if (key === "model" && typeof value === "string") settings.model = value;
33
+ if (key === "model_context_window" && typeof value === "number") settings.modelContextWindow = value;
34
+ if (key === "model_reasoning_effort" && typeof value === "string") settings.modelReasoningEffort = value;
35
+ if (key === "plan_mode_reasoning_effort" && typeof value === "string") settings.planModeReasoningEffort = value;
36
+ }
37
+ }
38
+ return settings;
39
+ }
40
+
41
+ function matchesProfile(current, profile) {
42
+ for (const [key, value] of Object.entries(profile)) {
43
+ if (current[key] !== value) return false;
44
+ }
45
+ return true;
46
+ }
47
+
48
+ function parseTomlScalar(value) {
49
+ const trimmed = value.trim();
50
+ if (trimmed.startsWith('"') && trimmed.endsWith('"')) {
51
+ try {
52
+ return JSON.parse(trimmed);
53
+ } catch (error) {
54
+ if (error instanceof SyntaxError) return undefined;
55
+ throw error;
56
+ }
57
+ }
58
+ const numeric = Number(trimmed);
59
+ return Number.isFinite(numeric) ? numeric : undefined;
60
+ }
61
+
62
+ function isSectionHeader(line) {
63
+ const trimmed = line.trim();
64
+ return trimmed.startsWith("[") && trimmed.endsWith("]");
65
+ }
66
+
67
+ function isRootSetting(line, key) {
68
+ const trimmed = line.trimStart();
69
+ if (trimmed.startsWith("#") || trimmed.startsWith("[")) return false;
70
+ const match = trimmed.match(/^([A-Za-z_][A-Za-z0-9_]*)\s*=/);
71
+ return match?.[1] === key;
72
+ }
@@ -76,6 +76,29 @@ test("#given managed legacy Codex component symlink #when linking bins #then rem
76
76
  assert.equal(await readlink(join(binDir, "omo-rules")), join(pluginRoot, "dist", "cli.js"));
77
77
  });
78
78
 
79
+ test("#given managed legacy Codex LSP symlink #when linking bins #then removes stale lsp symlink", async () => {
80
+ const root = await makeTempDir();
81
+ const pluginRoot = join(root, "plugin");
82
+ const binDir = join(root, "bin");
83
+ const oldTarget = join(root, "codex-home", "plugins", "cache", "legacy-market", "omo", "0.0.1", "components", "lsp", "dist", "cli.js");
84
+
85
+ await mkdir(join(pluginRoot, "dist"), { recursive: true });
86
+ await mkdir(join(root, "codex-home", "plugins", "cache", "legacy-market", "omo", "0.0.1", "components", "lsp", "dist"), { recursive: true });
87
+ await mkdir(binDir, { recursive: true });
88
+ await writeJson(join(pluginRoot, "package.json"), {
89
+ name: "@example/omo",
90
+ bin: { omo: "./dist/cli.js" },
91
+ });
92
+ await writeFile(join(pluginRoot, "dist", "cli.js"), "#!/usr/bin/env node\n");
93
+ await writeFile(oldTarget, "#!/usr/bin/env node\n");
94
+ await symlink(oldTarget, join(binDir, "codex-lsp"));
95
+
96
+ await linkCachedPluginBins({ binDir, pluginRoot, platform: "linux" });
97
+
98
+ await assert.rejects(readlink(join(binDir, "codex-lsp")));
99
+ assert.equal(await readlink(join(binDir, "omo")), join(pluginRoot, "dist", "cli.js"));
100
+ });
101
+
79
102
  test("#given user-owned legacy Codex symlink #when linking bins #then preserves the user symlink", async () => {
80
103
  const root = await makeTempDir();
81
104
  const pluginRoot = join(root, "plugin");
@@ -0,0 +1,83 @@
1
+ import assert from "node:assert/strict";
2
+ import { mkdtemp, readFile, 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 { updateCodexConfig } from "./install/config.mjs";
8
+
9
+ const AUTONOMOUS_FEATURES = ["multi_agent", "child_agents_md", "unified_exec", "goals"];
10
+
11
+ test("#given autonomous permissions requested #when script installer updates config #then enables Codex autonomy feature flags", async () => {
12
+ // given
13
+ const root = await mkdtemp(join(tmpdir(), "omo-codex-script-config-autonomous-features-"));
14
+ const configPath = join(root, "config.toml");
15
+ await writeFile(
16
+ configPath,
17
+ [
18
+ 'network_access = "disabled"',
19
+ "",
20
+ "[features]",
21
+ "multi_agent = false",
22
+ "child_agents_md = false",
23
+ "unified_exec = false",
24
+ "goals = false",
25
+ "",
26
+ ].join("\n"),
27
+ );
28
+
29
+ // when
30
+ await updateCodexConfig({
31
+ configPath,
32
+ repoRoot: "/repo/packages/omo-codex",
33
+ marketplaceName: "debug",
34
+ marketplaceSource: { sourceType: "local", source: "/repo/packages/omo-codex" },
35
+ pluginNames: ["omo"],
36
+ autonomousPermissions: true,
37
+ });
38
+
39
+ // then
40
+ const content = await readFile(configPath, "utf8");
41
+ assert.match(content, /network_access = "enabled"/);
42
+ for (const featureName of AUTONOMOUS_FEATURES) {
43
+ assert.match(content, new RegExp(`${featureName} = true`));
44
+ }
45
+ });
46
+
47
+ test("#given autonomous permissions disabled #when script installer updates config #then preserves autonomy feature opt-outs", async () => {
48
+ // given
49
+ const root = await mkdtemp(join(tmpdir(), "omo-codex-script-config-autonomous-features-disabled-"));
50
+ const configPath = join(root, "config.toml");
51
+ await writeFile(
52
+ configPath,
53
+ [
54
+ 'network_access = "disabled"',
55
+ "",
56
+ "[features]",
57
+ "multi_agent = false",
58
+ "child_agents_md = false",
59
+ "unified_exec = false",
60
+ "goals = false",
61
+ "",
62
+ ].join("\n"),
63
+ );
64
+
65
+ // when
66
+ await updateCodexConfig({
67
+ configPath,
68
+ repoRoot: "/repo/packages/omo-codex",
69
+ marketplaceName: "debug",
70
+ marketplaceSource: { sourceType: "local", source: "/repo/packages/omo-codex" },
71
+ pluginNames: ["omo"],
72
+ autonomousPermissions: false,
73
+ });
74
+
75
+ // then
76
+ const content = await readFile(configPath, "utf8");
77
+ assert.match(content, /network_access = "disabled"/);
78
+ for (const featureName of AUTONOMOUS_FEATURES) {
79
+ assert.match(content, new RegExp(`${featureName} = false`));
80
+ }
81
+ assert.match(content, /plugins = true/);
82
+ assert.match(content, /plugin_hooks = true/);
83
+ });
@@ -1,12 +1,12 @@
1
1
  import assert from "node:assert/strict";
2
- import { mkdtemp, readFile, writeFile } from "node:fs/promises";
2
+ import { mkdir, mkdtemp, readFile, writeFile } from "node:fs/promises";
3
3
  import { tmpdir } from "node:os";
4
4
  import { join } from "node:path";
5
5
  import test from "node:test";
6
6
 
7
7
  import { updateCodexConfig } from "./install/config.mjs";
8
8
 
9
- test("#given empty Codex config #when script installer updates config #then sets default and Plan-mode reasoning effort", async () => {
9
+ test("#given empty Codex config #when script installer updates config #then sets worker model and reasoning defaults", async () => {
10
10
  // given
11
11
  const root = await mkdtemp(join(tmpdir(), "omo-codex-script-config-reasoning-"));
12
12
  const configPath = join(root, "config.toml");
@@ -22,17 +22,21 @@ test("#given empty Codex config #when script installer updates config #then sets
22
22
 
23
23
  // then
24
24
  const content = await readFile(configPath, "utf8");
25
+ assert.match(content, /model = "gpt-5\.5"/);
26
+ assert.match(content, /model_context_window = 400000/);
25
27
  assert.match(content, /model_reasoning_effort = "high"/);
26
28
  assert.match(content, /plan_mode_reasoning_effort = "xhigh"/);
27
29
  });
28
30
 
29
- test("#given existing reasoning config #when script installer updates config #then replaces stale defaults without duplicate keys", async () => {
31
+ test("#given existing model and reasoning config #when script installer updates config #then replaces stale defaults without duplicate keys", async () => {
30
32
  // given
31
33
  const root = await mkdtemp(join(tmpdir(), "omo-codex-script-config-reasoning-existing-"));
32
34
  const configPath = join(root, "config.toml");
33
35
  await writeFile(
34
36
  configPath,
35
37
  [
38
+ 'model = "gpt-5.2"',
39
+ "model_context_window = 272000",
36
40
  'model_reasoning_effort = "low"',
37
41
  'plan_mode_reasoning_effort = "medium"',
38
42
  "",
@@ -53,10 +57,85 @@ test("#given existing reasoning config #when script installer updates config #th
53
57
 
54
58
  // then
55
59
  const content = await readFile(configPath, "utf8");
60
+ assert.equal(content.match(/^model\s*=/gm)?.length, 1);
61
+ assert.equal(content.match(/^model_context_window\s*=/gm)?.length, 1);
56
62
  assert.equal(content.match(/^model_reasoning_effort\s*=/gm)?.length, 1);
57
63
  assert.equal(content.match(/^plan_mode_reasoning_effort\s*=/gm)?.length, 1);
64
+ assert.match(content, /model = "gpt-5\.5"/);
65
+ assert.match(content, /model_context_window = 400000/);
58
66
  assert.match(content, /model_reasoning_effort = "high"/);
59
67
  assert.match(content, /plan_mode_reasoning_effort = "xhigh"/);
68
+ assert.doesNotMatch(content, /model = "gpt-5\.2"/);
69
+ assert.doesNotMatch(content, /model_context_window = 272000/);
60
70
  assert.doesNotMatch(content, /model_reasoning_effort = "low"/);
61
71
  assert.doesNotMatch(content, /plan_mode_reasoning_effort = "medium"/);
62
72
  });
73
+
74
+ test("#given user-customized model config #when script installer updates config #then preserves user reasoning values", async () => {
75
+ // given
76
+ const root = await mkdtemp(join(tmpdir(), "omo-codex-script-config-reasoning-custom-"));
77
+ const configPath = join(root, "config.toml");
78
+ await writeFile(
79
+ configPath,
80
+ [
81
+ 'model = "my-private-model"',
82
+ "model_context_window = 123456",
83
+ 'model_reasoning_effort = "medium"',
84
+ 'plan_mode_reasoning_effort = "medium"',
85
+ "",
86
+ ].join("\n"),
87
+ );
88
+
89
+ // when
90
+ await updateCodexConfig({
91
+ configPath,
92
+ repoRoot: "/repo/packages/omo-codex",
93
+ marketplaceName: "debug",
94
+ marketplaceSource: { sourceType: "local", source: "/repo/packages/omo-codex" },
95
+ pluginNames: ["omo"],
96
+ });
97
+
98
+ // then
99
+ const content = await readFile(configPath, "utf8");
100
+ assert.match(content, /model = "my-private-model"/);
101
+ assert.match(content, /model_context_window = 123456/);
102
+ assert.match(content, /model_reasoning_effort = "medium"/);
103
+ assert.match(content, /plan_mode_reasoning_effort = "medium"/);
104
+ });
105
+
106
+ test("#given bundled model catalog #when script installer updates config #then reads defaults from catalog", async () => {
107
+ // given
108
+ const root = await mkdtemp(join(tmpdir(), "omo-codex-script-config-reasoning-catalog-"));
109
+ const repoRoot = join(root, "omo-codex");
110
+ const configPath = join(root, "config.toml");
111
+ await mkdir(join(repoRoot, "plugin"), { recursive: true });
112
+ await writeFile(
113
+ join(repoRoot, "plugin", "model-catalog.json"),
114
+ JSON.stringify({
115
+ version: "test.catalog",
116
+ current: {
117
+ model: "catalog-default",
118
+ model_context_window: 123456,
119
+ model_reasoning_effort: "medium",
120
+ plan_mode_reasoning_effort: "high",
121
+ },
122
+ managedProfiles: [],
123
+ }),
124
+ );
125
+
126
+ // when
127
+ await updateCodexConfig({
128
+ configPath,
129
+ repoRoot,
130
+ marketplaceName: "debug",
131
+ marketplaceSource: { sourceType: "local", source: repoRoot },
132
+ pluginNames: ["omo"],
133
+ });
134
+
135
+ // then
136
+ const content = await readFile(configPath, "utf8");
137
+ assert.match(content, /model = "catalog-default"/);
138
+ assert.match(content, /model_context_window = 123456/);
139
+ assert.match(content, /model_reasoning_effort = "medium"/);
140
+ assert.match(content, /plan_mode_reasoning_effort = "high"/);
141
+ });
@@ -27,7 +27,7 @@ test("#given empty Codex config #when script installer updates config #then enab
27
27
  assert.match(config, /max_concurrent_threads_per_session = 10000/);
28
28
  });
29
29
 
30
- test("#given empty Codex config #when script installer updates config #then installs Context7 MCP", async () => {
30
+ test("#given empty Codex config #when script installer updates config #then leaves Context7 to the plugin MCP manifest", async () => {
31
31
  // given
32
32
  const root = await mkdtemp(join(tmpdir(), "omo-codex-script-config-context7-"));
33
33
  const configPath = join(root, "config.toml");
@@ -43,13 +43,12 @@ test("#given empty Codex config #when script installer updates config #then inst
43
43
 
44
44
  // then
45
45
  const config = await readFile(configPath, "utf8");
46
- assert.match(config, /\[mcp_servers\.context7\]/);
47
- assert.match(config, /command = "npx"/);
48
- assert.match(config, /args = \["-y", "@upstash\/context7-mcp", "--api-key", "YOUR_API_KEY"\]/);
49
- assert.match(config, /startup_timeout_sec = 20/);
46
+ assert.doesNotMatch(config, /\[mcp_servers\.context7\]/);
47
+ assert.doesNotMatch(config, /@upstash\/context7-mcp/);
48
+ assert.doesNotMatch(config, /YOUR_API_KEY/);
50
49
  });
51
50
 
52
- test("#given existing Context7 MCP config #when script installer updates config #then preserves user setup", async () => {
51
+ test("#given existing Context7 MCP config #when script installer updates config #then leaves user setup untouched", async () => {
53
52
  // given
54
53
  const root = await mkdtemp(join(tmpdir(), "omo-codex-script-config-context7-existing-"));
55
54
  const configPath = join(root, "config.toml");