oh-my-opencode 4.6.0 → 4.7.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 (75) 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 +577 -304
  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 +811 -450
  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 +12 -13
  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/rules/bundled-rules/hephaestus.md +6 -4
  25. package/packages/omo-codex/plugin/components/rules/src/post-compact-budget.ts +0 -2
  26. package/packages/omo-codex/plugin/components/start-work-continuation/directive.md +1 -1
  27. package/packages/omo-codex/plugin/components/ultrawork/CHANGELOG.md +1 -1
  28. package/packages/omo-codex/plugin/components/ultrawork/README.md +1 -1
  29. package/packages/omo-codex/plugin/components/ultrawork/agents/codex-ultrawork-reviewer.toml +3 -1
  30. package/packages/omo-codex/plugin/components/ultrawork/agents/plan.toml +7 -7
  31. package/packages/omo-codex/plugin/components/ultrawork/directive.md +1 -1
  32. package/packages/omo-codex/plugin/components/ulw-loop/skills/ulw-loop/SKILL.md +5 -4
  33. package/packages/omo-codex/plugin/components/ulw-loop/skills/ulw-loop/references/full-workflow.md +4 -3
  34. package/packages/omo-codex/plugin/components/ulw-loop/src/checkpoint.ts +12 -1
  35. package/packages/omo-codex/plugin/components/ulw-loop/test/checkpoint.test.ts +19 -1
  36. package/packages/omo-codex/plugin/hooks/hooks.json +11 -0
  37. package/packages/omo-codex/plugin/model-catalog.json +49 -0
  38. package/packages/omo-codex/plugin/scripts/auto-update.mjs +159 -0
  39. package/packages/omo-codex/plugin/scripts/migrate-codex-config.mjs +269 -0
  40. package/packages/omo-codex/plugin/scripts/sync-hook-status-messages.mjs +3 -1
  41. package/packages/omo-codex/plugin/scripts/sync-skills.mjs +6 -6
  42. package/packages/omo-codex/plugin/skills/init-deep/SKILL.md +6 -6
  43. package/packages/omo-codex/plugin/skills/lcx-report-bug/SKILL.md +127 -0
  44. package/packages/omo-codex/plugin/skills/lcx-report-bug/agents/openai.yaml +9 -0
  45. package/packages/omo-codex/plugin/skills/refactor/SKILL.md +6 -6
  46. package/packages/omo-codex/plugin/skills/remove-ai-slops/SKILL.md +6 -6
  47. package/packages/omo-codex/plugin/skills/review-work/SKILL.md +7 -7
  48. package/packages/omo-codex/plugin/skills/start-work/SKILL.md +6 -6
  49. package/packages/omo-codex/plugin/skills/ulw-loop/SKILL.md +5 -4
  50. package/packages/omo-codex/plugin/skills/ulw-loop/references/full-workflow.md +4 -3
  51. package/packages/omo-codex/plugin/skills/ulw-plan/SKILL.md +17 -17
  52. package/packages/omo-codex/plugin/test/aggregate.test.mjs +172 -19
  53. package/packages/omo-codex/plugin/test/auto-update.test.mjs +129 -0
  54. package/packages/omo-codex/plugin/test/hook-status-message.test.mjs +2 -0
  55. package/packages/omo-codex/plugin/test/migrate-codex-config.test.mjs +146 -0
  56. package/packages/omo-codex/plugin/test/sync-hook-status-messages.test.mjs +1 -0
  57. package/packages/omo-codex/plugin/test/sync-skills.test.mjs +22 -0
  58. package/packages/omo-codex/scripts/install/cli-args.mjs +1 -1
  59. package/packages/omo-codex/scripts/install/config.mjs +2 -15
  60. package/packages/omo-codex/scripts/install/delegated-command.mjs +1 -1
  61. package/packages/omo-codex/scripts/install/legacy-bins.mjs +1 -0
  62. package/packages/omo-codex/scripts/install/model-catalog.mjs +66 -0
  63. package/packages/omo-codex/scripts/install/reasoning-config.mjs +65 -7
  64. package/packages/omo-codex/scripts/install-bin-links.test.mjs +23 -0
  65. package/packages/omo-codex/scripts/install-config-reasoning.test.mjs +82 -3
  66. package/packages/omo-codex/scripts/install-config.test.mjs +5 -6
  67. package/packages/omo-codex/scripts/install-local-entrypoint.test.mjs +30 -2
  68. package/packages/omo-codex/scripts/install-local.mjs +1 -1
  69. package/packages/shared-skills/skills/lcx-report-bug/SKILL.md +127 -0
  70. package/packages/shared-skills/skills/lcx-report-bug/agents/openai.yaml +9 -0
  71. package/packages/shared-skills/skills/review-work/SKILL.md +7 -7
  72. package/packages/shared-skills/skills/start-work/SKILL.md +6 -6
  73. package/packages/shared-skills/skills/ulw-plan/SKILL.md +11 -11
  74. package/postinstall.mjs +36 -3
  75. package/dist/cli/install-codex/codex-config-mcp.d.ts +0 -1
@@ -0,0 +1,146 @@
1
+ import assert from "node:assert/strict";
2
+ import { mkdir, mkdtemp, readFile, writeFile } from "node:fs/promises";
3
+ import { tmpdir } from "node:os";
4
+ import { dirname, join } from "node:path";
5
+ import test from "node:test";
6
+
7
+ import { ensureCodexReasoningConfig, migrateCodexConfig } from "../scripts/migrate-codex-config.mjs";
8
+
9
+ test("#given stale root reasoning config #when ensuring config #then replaces stale values without duplicate keys", () => {
10
+ const result = ensureCodexReasoningConfig(
11
+ [
12
+ 'model = "gpt-5.2"',
13
+ "model_context_window = 272000",
14
+ 'model_reasoning_effort = "low"',
15
+ 'plan_mode_reasoning_effort = "medium"',
16
+ "",
17
+ "[features]",
18
+ "plugins = true",
19
+ "",
20
+ ].join("\n"),
21
+ );
22
+
23
+ assert.equal(result.match(/^model\s*=/gm)?.length, 1);
24
+ assert.equal(result.match(/^model_context_window\s*=/gm)?.length, 1);
25
+ assert.equal(result.match(/^model_reasoning_effort\s*=/gm)?.length, 1);
26
+ assert.equal(result.match(/^plan_mode_reasoning_effort\s*=/gm)?.length, 1);
27
+ assert.match(result, /model = "gpt-5\.5"/);
28
+ assert.match(result, /model_context_window = 400000/);
29
+ assert.match(result, /model_reasoning_effort = "high"/);
30
+ assert.match(result, /plan_mode_reasoning_effort = "xhigh"/);
31
+ assert.doesNotMatch(result, /gpt-5\.2/);
32
+ assert.match(result, /\[features\]/);
33
+ });
34
+
35
+ test("#given global and project-local stale Codex configs #when migrating #then both configs are forced to current defaults", async () => {
36
+ const root = await mkdtemp(join(tmpdir(), "lazycodex-config-migration-"));
37
+ const codexHome = join(root, "codex-home");
38
+ const project = join(root, "project", "nested");
39
+ const projectConfig = join(root, "project", ".codex", "config.toml");
40
+ await mkdir(codexHome, { recursive: true });
41
+ await mkdir(dirname(projectConfig), { recursive: true });
42
+ await writeFile(join(codexHome, "config.toml"), 'model = "gpt-5.2"\n');
43
+ await writeFile(projectConfig, 'model = "gpt-5.2"\nmodel_context_window = 272000\n');
44
+
45
+ const result = await migrateCodexConfig({
46
+ env: { CODEX_HOME: codexHome, LAZYCODEX_MODEL_CATALOG_STATE_PATH: join(root, "model-state.json") },
47
+ cwd: project,
48
+ });
49
+
50
+ assert.deepEqual(result.changed.sort(), [join(codexHome, "config.toml"), projectConfig].sort());
51
+ assert.match(await readFile(join(codexHome, "config.toml"), "utf8"), /model = "gpt-5\.5"/);
52
+ assert.match(await readFile(projectConfig, "utf8"), /model_context_window = 400000/);
53
+ });
54
+
55
+ test("#given user-customized Codex model config #when migrating #then user values are preserved", async () => {
56
+ const root = await mkdtemp(join(tmpdir(), "lazycodex-config-custom-"));
57
+ const codexHome = join(root, "codex-home");
58
+ await mkdir(codexHome, { recursive: true });
59
+ await writeFile(
60
+ join(codexHome, "config.toml"),
61
+ [
62
+ 'model = "gpt-5.4"',
63
+ "model_context_window = 123456",
64
+ 'model_reasoning_effort = "medium"',
65
+ 'plan_mode_reasoning_effort = "medium"',
66
+ "",
67
+ ].join("\n"),
68
+ );
69
+
70
+ const result = await migrateCodexConfig({
71
+ env: { CODEX_HOME: codexHome, LAZYCODEX_MODEL_CATALOG_STATE_PATH: join(root, "model-state.json") },
72
+ cwd: root,
73
+ });
74
+
75
+ const content = await readFile(join(codexHome, "config.toml"), "utf8");
76
+ assert.deepEqual(result.changed, []);
77
+ assert.match(content, /model = "gpt-5\.4"/);
78
+ assert.match(content, /model_context_window = 123456/);
79
+ assert.match(content, /model_reasoning_effort = "medium"/);
80
+ assert.match(content, /plan_mode_reasoning_effort = "medium"/);
81
+ });
82
+
83
+ test("#given managed catalog state #when catalog version advances #then only previously managed config is updated", async () => {
84
+ const root = await mkdtemp(join(tmpdir(), "lazycodex-config-catalog-state-"));
85
+ const codexHome = join(root, "codex-home");
86
+ const catalogPath = join(root, "catalog.json");
87
+ const statePath = join(root, "model-state.json");
88
+ await mkdir(codexHome, { recursive: true });
89
+ await writeFile(
90
+ catalogPath,
91
+ JSON.stringify(
92
+ {
93
+ version: "test.v1",
94
+ current: {
95
+ model: "gpt-5.4",
96
+ model_context_window: 1000000,
97
+ model_reasoning_effort: "high",
98
+ plan_mode_reasoning_effort: "xhigh",
99
+ },
100
+ managedProfiles: [],
101
+ },
102
+ null,
103
+ 2,
104
+ ),
105
+ );
106
+
107
+ const first = await migrateCodexConfig({
108
+ env: {
109
+ CODEX_HOME: codexHome,
110
+ LAZYCODEX_MODEL_CATALOG_PATH: catalogPath,
111
+ LAZYCODEX_MODEL_CATALOG_STATE_PATH: statePath,
112
+ },
113
+ cwd: root,
114
+ });
115
+ await writeFile(
116
+ catalogPath,
117
+ JSON.stringify(
118
+ {
119
+ version: "test.v2",
120
+ current: {
121
+ model: "gpt-5.5",
122
+ model_context_window: 400000,
123
+ model_reasoning_effort: "high",
124
+ plan_mode_reasoning_effort: "xhigh",
125
+ },
126
+ managedProfiles: [],
127
+ },
128
+ null,
129
+ 2,
130
+ ),
131
+ );
132
+ const second = await migrateCodexConfig({
133
+ env: {
134
+ CODEX_HOME: codexHome,
135
+ LAZYCODEX_MODEL_CATALOG_PATH: catalogPath,
136
+ LAZYCODEX_MODEL_CATALOG_STATE_PATH: statePath,
137
+ },
138
+ cwd: root,
139
+ });
140
+
141
+ const content = await readFile(join(codexHome, "config.toml"), "utf8");
142
+ assert.deepEqual(first.changed, [join(codexHome, "config.toml")]);
143
+ assert.deepEqual(second.changed, [join(codexHome, "config.toml")]);
144
+ assert.match(content, /model = "gpt-5\.5"/);
145
+ assert.match(content, /model_context_window = 400000/);
146
+ });
@@ -21,6 +21,7 @@ test("#given a component without hooks #when hook status messages sync #then bui
21
21
  await mkdir(join(root, "hooks"), { recursive: true });
22
22
  await mkdir(join(root, "components", "comment-checker", "hooks"), { recursive: true });
23
23
  await mkdir(join(root, "components", "git-bash"), { recursive: true });
24
+ await mkdir(join(root, "components", "stale-build-output", "dist"), { recursive: true });
24
25
  await writeJson(join(root, ".codex-plugin", "plugin.json"), { version: "0.1.0" });
25
26
  await writeJson(join(root, "components", "comment-checker", "package.json"), { version: "0.1.1" });
26
27
  await writeJson(join(root, "components", "git-bash", "package.json"), { version: "0.3.0" });
@@ -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,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");
@@ -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");
@@ -57,14 +57,14 @@ test("#given lazycodex runs through an npm bin symlink #when running the Node in
57
57
  }
58
58
  });
59
59
 
60
- test("#given dry-run install flags #when running the Node installer entrypoint #then prints delegated codex install command", () => {
60
+ test("#given dry-run install flags #when running the Node installer entrypoint #then prints delegated autonomous codex install command", () => {
61
61
  // given
62
62
  const scriptPath = fileURLToPath(new URL("./install-local.mjs", import.meta.url));
63
63
 
64
64
  // when
65
65
  const output = execFileSync(
66
66
  process.execPath,
67
- [scriptPath, "--dry-run", "install", "--no-tui", "--codex-autonomous"],
67
+ [scriptPath, "--dry-run", "install", "--no-tui"],
68
68
  { encoding: "utf8" },
69
69
  ).trim();
70
70
 
@@ -72,6 +72,21 @@ test("#given dry-run install flags #when running the Node installer entrypoint #
72
72
  assert.equal(output, "npx --yes --package oh-my-openagent omo install --platform=codex --no-tui --codex-autonomous");
73
73
  });
74
74
 
75
+ test("#given dry-run install opt-out #when running the Node installer entrypoint #then preserves existing Codex permission settings", () => {
76
+ // given
77
+ const scriptPath = fileURLToPath(new URL("./install-local.mjs", import.meta.url));
78
+
79
+ // when
80
+ const output = execFileSync(
81
+ process.execPath,
82
+ [scriptPath, "--dry-run", "install", "--no-tui", "--no-codex-autonomous"],
83
+ { encoding: "utf8" },
84
+ ).trim();
85
+
86
+ // then
87
+ assert.equal(output, "npx --yes --package oh-my-openagent omo install --platform=codex --no-tui --no-codex-autonomous");
88
+ });
89
+
75
90
  test("#given dry-run doctor #when running the Node installer entrypoint #then prints delegated doctor command", () => {
76
91
  // given
77
92
  const scriptPath = fileURLToPath(new URL("./install-local.mjs", import.meta.url));
@@ -100,6 +115,19 @@ test("#given dry-run cleanup #when running the Node installer entrypoint #then p
100
115
  assert.equal(output, "npx --yes --package oh-my-openagent omo cleanup --platform=codex --project /tmp/lazycodex-qa");
101
116
  });
102
117
 
118
+ test("#given dry-run ulw-loop #when running the Node installer entrypoint #then prints delegated ulw-loop command", () => {
119
+ // given
120
+ const scriptPath = fileURLToPath(new URL("./install-local.mjs", import.meta.url));
121
+
122
+ // when
123
+ const output = execFileSync(process.execPath, [scriptPath, "--dry-run", "ulw-loop", "help"], {
124
+ encoding: "utf8",
125
+ }).trim();
126
+
127
+ // then
128
+ assert.equal(output, "npx --yes --package oh-my-openagent omo ulw-loop help");
129
+ });
130
+
103
131
  test("#given the invoking argv path disappears #when importing the Node installer module #then the entrypoint guard does not throw", () => {
104
132
  // given
105
133
  const scriptPath = fileURLToPath(new URL("./install-local.mjs", import.meta.url));