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
@@ -2,6 +2,7 @@
2
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
+ import { fileURLToPath } from "node:url";
5
6
  import { describe, expect, it } from "vitest";
6
7
 
7
8
  import { checkpointUlwLoop } from "../src/checkpoint.js";
@@ -12,7 +13,7 @@ import type { UlwLoopItem, UlwLoopLedgerEntry, UlwLoopPlan, UlwLoopSuccessCriter
12
13
  import { UlwLoopError } from "../src/types.js";
13
14
 
14
15
  const NOW = "2026-05-23T00:00:00.000Z";
15
- const QUALITY_GATE_PATH = join(process.cwd(), "test", "fixtures", "sample-quality-gate.json");
16
+ const QUALITY_GATE_PATH = fileURLToPath(new URL("./fixtures/sample-quality-gate.json", import.meta.url));
16
17
 
17
18
  function criterion(id: string, status: UlwLoopSuccessCriterion["status"]): UlwLoopSuccessCriterion {
18
19
  return { id, scenario: `${id} scenario`, userModel: "happy", expectedEvidence: `${id} proof`, capturedEvidence: status === "pass" ? `${id} passed` : null, status };
@@ -142,6 +143,23 @@ describe("checkpointUlwLoop final story", () => {
142
143
  expect(result.ledgerEntry.kind).toBe("aggregate_completed");
143
144
  });
144
145
 
146
+ it("ACCEPTS complete when active task-scoped Codex objective maps to the ulw-loop brief", async () => {
147
+ const taskObjective = "Create only research artifacts with source evidence";
148
+ const repo = await repoWith(plan([passGoal("G001")], { activeGoalId: "G001" }));
149
+ await writeFile(ulwLoopBriefPath(repo), `${taskObjective}\n`, "utf8");
150
+
151
+ const result = await checkpointUlwLoop(repo, {
152
+ goalId: "G001",
153
+ status: "complete",
154
+ evidence: "final implementation complete and quality gate passed",
155
+ codexGoalJson: snapshot("active", taskObjective),
156
+ qualityGateJson: QUALITY_GATE_PATH,
157
+ });
158
+
159
+ expect(result.aggregateCompletion?.status).toBe("complete");
160
+ expect(result.ledgerEntry.kind).toBe("aggregate_completed");
161
+ });
162
+
145
163
  it("explains final task-scoped objective mapping when completed Codex objective is unrelated", async () => {
146
164
  const repo = await repoWith(plan([passGoal("G001")], { activeGoalId: "G001" }));
147
165
  await writeFile(ulwLoopBriefPath(repo), "Fix ulw-loop objective mismatch and install local ulw\n", "utf8");
@@ -20,6 +20,17 @@
20
20
  "statusMessage": "LazyCodex(0.1.0): Recording Session Telemetry"
21
21
  }
22
22
  ]
23
+ },
24
+ {
25
+ "matcher": "^startup$",
26
+ "hooks": [
27
+ {
28
+ "type": "command",
29
+ "command": "node \"${PLUGIN_ROOT}/scripts/auto-update.mjs\" hook session-start",
30
+ "timeout": 5,
31
+ "statusMessage": "LazyCodex(0.1.0): Checking Auto Update"
32
+ }
33
+ ]
23
34
  }
24
35
  ],
25
36
  "UserPromptSubmit": [
@@ -86,13 +97,13 @@
86
97
  "type": "command",
87
98
  "command": "node \"${PLUGIN_ROOT}/components/comment-checker/dist/cli.js\" hook post-tool-use",
88
99
  "timeout": 30,
89
- "statusMessage": "LazyCodex(0.1.1): Checking Comments"
100
+ "statusMessage": "LazyCodex(0.1.0): Checking Comments"
90
101
  },
91
102
  {
92
103
  "type": "command",
93
104
  "command": "node \"${PLUGIN_ROOT}/components/lsp/dist/cli.js\" hook post-tool-use",
94
105
  "timeout": 60,
95
- "statusMessage": "LazyCodex(0.2.0): Checking LSP Diagnostics"
106
+ "statusMessage": "LazyCodex(0.1.0): Checking LSP Diagnostics"
96
107
  }
97
108
  ]
98
109
  },
@@ -130,6 +141,17 @@
130
141
  "statusMessage": "LazyCodex(0.1.0): Resetting Project Rule Cache"
131
142
  }
132
143
  ]
144
+ },
145
+ {
146
+ "matcher": "manual|auto",
147
+ "hooks": [
148
+ {
149
+ "type": "command",
150
+ "command": "node \"${PLUGIN_ROOT}/components/lsp/dist/cli.js\" hook post-compact",
151
+ "timeout": 5,
152
+ "statusMessage": "LazyCodex(0.1.0): Resetting LSP Diagnostics Cache"
153
+ }
154
+ ]
133
155
  }
134
156
  ],
135
157
  "Stop": [
@@ -0,0 +1,49 @@
1
+ {
2
+ "version": "2026-06-03.gpt-5.5-400k",
3
+ "current": {
4
+ "model": "gpt-5.5",
5
+ "model_context_window": 400000,
6
+ "model_reasoning_effort": "high",
7
+ "plan_mode_reasoning_effort": "xhigh"
8
+ },
9
+ "roles": {
10
+ "default": {
11
+ "model": "gpt-5.5",
12
+ "model_context_window": 400000,
13
+ "model_reasoning_effort": "high",
14
+ "plan_mode_reasoning_effort": "xhigh"
15
+ },
16
+ "verifier": {
17
+ "model": "gpt-5.5",
18
+ "model_reasoning_effort": "xhigh"
19
+ },
20
+ "worker": {
21
+ "model": "gpt-5.4",
22
+ "model_reasoning_effort": "high"
23
+ }
24
+ },
25
+ "managedProfiles": [
26
+ {
27
+ "version": "legacy.gpt-5.2",
28
+ "match": {
29
+ "model": "gpt-5.2"
30
+ }
31
+ },
32
+ {
33
+ "version": "legacy.gpt-5.4-1m",
34
+ "match": {
35
+ "model": "gpt-5.4",
36
+ "model_context_window": 1000000,
37
+ "model_reasoning_effort": "high",
38
+ "plan_mode_reasoning_effort": "xhigh"
39
+ }
40
+ },
41
+ {
42
+ "version": "legacy.gpt-5.5-272k",
43
+ "match": {
44
+ "model": "gpt-5.5",
45
+ "model_context_window": 272000
46
+ }
47
+ }
48
+ ]
49
+ }
@@ -0,0 +1,159 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { spawn, spawnSync } from "node:child_process";
4
+ import { mkdir, open, readFile, rm, stat, writeFile } from "node:fs/promises";
5
+ import { homedir } from "node:os";
6
+ import { dirname, join } from "node:path";
7
+ import { pathToFileURL } from "node:url";
8
+ import { migrateCodexConfig } from "./migrate-codex-config.mjs";
9
+
10
+ const DEFAULT_INTERVAL_MS = 24 * 60 * 60 * 1_000;
11
+ const DEFAULT_LOCK_STALE_MS = 10 * 60 * 1_000;
12
+
13
+ export function resolveAutoUpdatePlan({ env = process.env, now = Date.now(), lastCheckedAt } = {}) {
14
+ if (env.LAZYCODEX_AUTO_UPDATE_DISABLED === "1" || env.OMO_CODEX_AUTO_UPDATE_DISABLED === "1") {
15
+ return { shouldRun: false, reason: "disabled" };
16
+ }
17
+
18
+ const intervalMs = parsePositiveInteger(env.LAZYCODEX_AUTO_UPDATE_INTERVAL_MS, DEFAULT_INTERVAL_MS);
19
+ if (typeof lastCheckedAt === "number" && intervalMs > 0 && now - lastCheckedAt < intervalMs) {
20
+ return { shouldRun: false, reason: "throttled" };
21
+ }
22
+
23
+ return {
24
+ shouldRun: true,
25
+ command: resolveCommand(env),
26
+ args: resolveArgs(env),
27
+ env: {
28
+ ...env,
29
+ LAZYCODEX_AUTO_UPDATE_DISABLED: "1",
30
+ OMO_CODEX_AUTO_UPDATE_DISABLED: "1",
31
+ },
32
+ };
33
+ }
34
+
35
+ export async function runAutoUpdateCheck({ env = process.env, now = Date.now() } = {}) {
36
+ await runConfigMigration({ env });
37
+ const statePath = resolveStatePath(env);
38
+ const state = await readState(statePath);
39
+ const plan = resolveAutoUpdatePlan({ env, now, lastCheckedAt: state.lastCheckedAt });
40
+ if (!plan.shouldRun) return { started: false, reason: plan.reason };
41
+
42
+ const lock = await acquireLock(resolveLockPath(env, statePath), now, env);
43
+ if (lock === null) return { started: false, reason: "locked" };
44
+ try {
45
+ await writeState(statePath, { lastCheckedAt: now });
46
+ if (env.LAZYCODEX_AUTO_UPDATE_WAIT === "1") {
47
+ const result = spawnSync(plan.command, plan.args, {
48
+ env: plan.env,
49
+ stdio: "ignore",
50
+ });
51
+ return { started: true, status: result.status ?? 0 };
52
+ }
53
+
54
+ const child = spawn(plan.command, plan.args, {
55
+ env: plan.env,
56
+ stdio: "ignore",
57
+ detached: true,
58
+ });
59
+ child.unref();
60
+ return { started: true };
61
+ } finally {
62
+ await lock.release();
63
+ }
64
+ }
65
+
66
+ async function runConfigMigration({ env }) {
67
+ if (env.LAZYCODEX_CONFIG_MIGRATION_DISABLED === "1" || env.OMO_CODEX_CONFIG_MIGRATION_DISABLED === "1") return;
68
+ try {
69
+ await migrateCodexConfig({ env });
70
+ } catch (error) {
71
+ if (!(error instanceof Error)) throw error;
72
+ return;
73
+ }
74
+ }
75
+
76
+ function resolveCommand(env) {
77
+ return env.LAZYCODEX_AUTO_UPDATE_COMMAND?.trim() || "npx";
78
+ }
79
+
80
+ function resolveArgs(env) {
81
+ if (env.LAZYCODEX_AUTO_UPDATE_ARGS_JSON) {
82
+ const parsed = JSON.parse(env.LAZYCODEX_AUTO_UPDATE_ARGS_JSON);
83
+ if (!Array.isArray(parsed) || parsed.some((value) => typeof value !== "string")) {
84
+ throw new TypeError("LAZYCODEX_AUTO_UPDATE_ARGS_JSON must be a JSON string array");
85
+ }
86
+ return parsed;
87
+ }
88
+ return ["--yes", "lazycodex-ai@latest", "install", "--no-tui", "--skip-auth"];
89
+ }
90
+
91
+ function resolveStatePath(env) {
92
+ if (env.LAZYCODEX_AUTO_UPDATE_STATE_PATH?.trim()) return env.LAZYCODEX_AUTO_UPDATE_STATE_PATH;
93
+ const dataRoot = env.PLUGIN_DATA?.trim() || join(homedir(), ".local", "share", "lazycodex");
94
+ return join(dataRoot, "auto-update.json");
95
+ }
96
+
97
+ function resolveLockPath(env, statePath) {
98
+ if (env.LAZYCODEX_AUTO_UPDATE_LOCK_PATH?.trim()) return env.LAZYCODEX_AUTO_UPDATE_LOCK_PATH;
99
+ return `${statePath}.lock`;
100
+ }
101
+
102
+ async function acquireLock(lockPath, now, env) {
103
+ await mkdir(dirname(lockPath), { recursive: true });
104
+ const staleMs = parsePositiveInteger(env.LAZYCODEX_AUTO_UPDATE_LOCK_STALE_MS, DEFAULT_LOCK_STALE_MS);
105
+ try {
106
+ const handle = await open(lockPath, "wx");
107
+ await handle.writeFile(`${now}\n`);
108
+ await handle.close();
109
+ return {
110
+ release: () => rm(lockPath, { force: true }),
111
+ };
112
+ } catch (error) {
113
+ if (!(error instanceof Error && "code" in error && error.code === "EEXIST")) throw error;
114
+ if (!(await removeStaleLock(lockPath, now, staleMs))) return null;
115
+ return acquireLock(lockPath, now, { ...env, LAZYCODEX_AUTO_UPDATE_LOCK_STALE_MS: "0" });
116
+ }
117
+ }
118
+
119
+ async function removeStaleLock(lockPath, now, staleMs) {
120
+ if (staleMs <= 0) return false;
121
+ try {
122
+ const lockStat = await stat(lockPath);
123
+ if (now - lockStat.mtimeMs < staleMs) return false;
124
+ await rm(lockPath, { force: true });
125
+ return true;
126
+ } catch (error) {
127
+ if (error instanceof Error && "code" in error && error.code === "ENOENT") return true;
128
+ throw error;
129
+ }
130
+ }
131
+
132
+ async function readState(statePath) {
133
+ try {
134
+ const raw = await readFile(statePath, "utf8");
135
+ const parsed = JSON.parse(raw);
136
+ return typeof parsed === "object" && parsed !== null ? parsed : {};
137
+ } catch (error) {
138
+ if (error instanceof Error && "code" in error && error.code === "ENOENT") return {};
139
+ return {};
140
+ }
141
+ }
142
+
143
+ async function writeState(statePath, state) {
144
+ await mkdir(dirname(statePath), { recursive: true });
145
+ await writeFile(statePath, `${JSON.stringify(state, null, 2)}\n`);
146
+ }
147
+
148
+ function parsePositiveInteger(value, fallback) {
149
+ if (value === undefined || value === "") return fallback;
150
+ const parsed = Number.parseInt(value, 10);
151
+ return Number.isFinite(parsed) && parsed >= 0 ? parsed : fallback;
152
+ }
153
+
154
+ if (process.argv[1] !== undefined && import.meta.url === pathToFileURL(process.argv[1]).href) {
155
+ runAutoUpdateCheck().catch((error) => {
156
+ console.error(error instanceof Error ? error.message : String(error));
157
+ process.exit(0);
158
+ });
159
+ }
@@ -0,0 +1,269 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { mkdir, readFile, stat, writeFile } from "node:fs/promises";
4
+ import { homedir } from "node:os";
5
+ import { dirname, join, resolve } from "node:path";
6
+ import { fileURLToPath } from "node:url";
7
+ import { pathToFileURL } from "node:url";
8
+
9
+ const FALLBACK_CATALOG = {
10
+ version: "fallback.gpt-5.5-400k",
11
+ current: {
12
+ model: "gpt-5.5",
13
+ model_context_window: 400_000,
14
+ model_reasoning_effort: "high",
15
+ plan_mode_reasoning_effort: "xhigh",
16
+ },
17
+ roles: {
18
+ default: {
19
+ model: "gpt-5.5",
20
+ model_context_window: 400_000,
21
+ model_reasoning_effort: "high",
22
+ plan_mode_reasoning_effort: "xhigh",
23
+ },
24
+ verifier: { model: "gpt-5.5", model_reasoning_effort: "xhigh" },
25
+ worker: { model: "gpt-5.4", model_reasoning_effort: "high" },
26
+ },
27
+ managedProfiles: [
28
+ { version: "legacy.gpt-5.2", match: { model: "gpt-5.2" } },
29
+ {
30
+ version: "legacy.gpt-5.4-1m",
31
+ match: {
32
+ model: "gpt-5.4",
33
+ model_context_window: 1_000_000,
34
+ model_reasoning_effort: "high",
35
+ plan_mode_reasoning_effort: "xhigh",
36
+ },
37
+ },
38
+ ],
39
+ };
40
+
41
+ const MANAGED_KEYS = ["model", "model_context_window", "model_reasoning_effort", "plan_mode_reasoning_effort"];
42
+
43
+ export async function migrateCodexConfig({ env = process.env, cwd = process.cwd() } = {}) {
44
+ const catalog = await readModelCatalog(env);
45
+ const statePath = resolveStatePath(env);
46
+ const state = await readState(statePath);
47
+ const paths = await configPaths({ env, cwd });
48
+ const changed = [];
49
+ const nextState = { catalogVersion: catalog.version, files: {} };
50
+ for (const configPath of paths) {
51
+ const result = await migrateConfigFile(configPath, {
52
+ catalog,
53
+ previousState: state.files?.[configPath],
54
+ });
55
+ if (result.changed) changed.push(configPath);
56
+ nextState.files[configPath] = {
57
+ catalogVersion: catalog.version,
58
+ written: result.written,
59
+ managed: result.managed,
60
+ };
61
+ }
62
+ await writeState(statePath, nextState);
63
+ return { changed };
64
+ }
65
+
66
+ export async function migrateConfigFile(configPath, { catalog = FALLBACK_CATALOG, previousState } = {}) {
67
+ const before = await readConfig(configPath);
68
+ const decision = shouldApplyCatalog(before, catalog, previousState);
69
+ if (!decision.apply) return { changed: false, written: readRootSettings(before), managed: false };
70
+ const after = ensureCodexReasoningConfig(before, catalog.current);
71
+ if (after === before) return { changed: false, written: catalog.current, managed: true };
72
+ await mkdir(dirname(configPath), { recursive: true });
73
+ await writeFile(configPath, `${after.trimEnd()}\n`);
74
+ return { changed: true, written: catalog.current, managed: true };
75
+ }
76
+
77
+ export function ensureCodexReasoningConfig(config, profile = FALLBACK_CATALOG.current) {
78
+ let next = replaceOrInsertRootSetting(config, "model", JSON.stringify(profile.model));
79
+ next = replaceOrInsertRootSetting(next, "model_context_window", profile.model_context_window.toString());
80
+ next = replaceOrInsertRootSetting(next, "model_reasoning_effort", JSON.stringify(profile.model_reasoning_effort));
81
+ next = replaceOrInsertRootSetting(next, "plan_mode_reasoning_effort", JSON.stringify(profile.plan_mode_reasoning_effort));
82
+ return next;
83
+ }
84
+
85
+ export async function readModelCatalog(env = process.env) {
86
+ const catalogPath = env.LAZYCODEX_MODEL_CATALOG_PATH?.trim() || join(dirname(fileURLToPath(import.meta.url)), "..", "model-catalog.json");
87
+ try {
88
+ return parseCatalog(JSON.parse(await readFile(catalogPath, "utf8"))) ?? FALLBACK_CATALOG;
89
+ } catch (error) {
90
+ if (error instanceof Error) return FALLBACK_CATALOG;
91
+ throw error;
92
+ }
93
+ }
94
+
95
+ function shouldApplyCatalog(config, catalog, previousState) {
96
+ const current = readRootSettings(config);
97
+ if (Object.keys(current).length === 0) return { apply: true, reason: "empty" };
98
+ if (matchesProfile(current, catalog.current)) return { apply: false, reason: "current" };
99
+ if (previousState?.managed === true && matchesProfile(current, previousState.written)) {
100
+ return { apply: true, reason: "managed-state" };
101
+ }
102
+ for (const profile of catalog.managedProfiles) {
103
+ if (matchesProfile(current, profile.match)) return { apply: true, reason: profile.version };
104
+ }
105
+ return { apply: false, reason: "user-modified" };
106
+ }
107
+
108
+ function matchesProfile(current, profile) {
109
+ if (!isRecord(profile)) return false;
110
+ for (const [key, value] of Object.entries(profile)) {
111
+ if (current[key] !== value) return false;
112
+ }
113
+ return true;
114
+ }
115
+
116
+ function readRootSettings(config) {
117
+ const settings = {};
118
+ for (const line of config.split(/\n/)) {
119
+ if (isSectionHeader(line)) break;
120
+ for (const key of MANAGED_KEYS) {
121
+ if (!isRootSetting(line, key)) continue;
122
+ const value = parseTomlScalar(line.slice(line.indexOf("=") + 1));
123
+ if (value !== undefined) settings[key] = value;
124
+ }
125
+ }
126
+ return settings;
127
+ }
128
+
129
+ function parseTomlScalar(value) {
130
+ const trimmed = value.trim();
131
+ if (trimmed.startsWith('"') && trimmed.endsWith('"')) {
132
+ try {
133
+ return JSON.parse(trimmed);
134
+ } catch (error) {
135
+ if (error instanceof SyntaxError) return undefined;
136
+ throw error;
137
+ }
138
+ }
139
+ const numeric = Number(trimmed);
140
+ return Number.isFinite(numeric) ? numeric : undefined;
141
+ }
142
+
143
+ function parseCatalog(value) {
144
+ if (!isRecord(value) || !isRecord(value.current) || !Array.isArray(value.managedProfiles)) return null;
145
+ if (typeof value.version !== "string" || !isReasoningProfile(value.current)) return null;
146
+ const managedProfiles = [];
147
+ for (const profile of value.managedProfiles) {
148
+ if (!isRecord(profile) || typeof profile.version !== "string" || !isRecord(profile.match)) return null;
149
+ managedProfiles.push({ version: profile.version, match: profile.match });
150
+ }
151
+ return { version: value.version, current: value.current, managedProfiles, roles: isRecord(value.roles) ? value.roles : {} };
152
+ }
153
+
154
+ function isReasoningProfile(value) {
155
+ return (
156
+ isRecord(value) &&
157
+ typeof value.model === "string" &&
158
+ typeof value.model_context_window === "number" &&
159
+ typeof value.model_reasoning_effort === "string" &&
160
+ typeof value.plan_mode_reasoning_effort === "string"
161
+ );
162
+ }
163
+
164
+ function isRecord(value) {
165
+ return typeof value === "object" && value !== null && !Array.isArray(value);
166
+ }
167
+
168
+ async function configPaths({ env, cwd }) {
169
+ const codexHome = resolve(env.CODEX_HOME?.trim() || join(homedir(), ".codex"));
170
+ const paths = new Set([join(codexHome, "config.toml")]);
171
+ for (const projectConfig of projectConfigPaths({ cwd, stopAt: homedir() })) {
172
+ if (await pathExists(projectConfig)) paths.add(projectConfig);
173
+ }
174
+ return [...paths];
175
+ }
176
+
177
+ function projectConfigPaths({ cwd, stopAt }) {
178
+ const paths = [];
179
+ let current = resolve(cwd);
180
+ const stop = resolve(stopAt);
181
+ while (true) {
182
+ paths.push(join(current, ".codex", "config.toml"));
183
+ if (current === stop || current === dirname(current)) break;
184
+ current = dirname(current);
185
+ }
186
+ return paths;
187
+ }
188
+
189
+ async function readConfig(configPath) {
190
+ try {
191
+ return await readFile(configPath, "utf8");
192
+ } catch (error) {
193
+ if (error instanceof Error && "code" in error && error.code === "ENOENT") return "";
194
+ throw error;
195
+ }
196
+ }
197
+
198
+ function resolveStatePath(env) {
199
+ if (env.LAZYCODEX_MODEL_CATALOG_STATE_PATH?.trim()) return env.LAZYCODEX_MODEL_CATALOG_STATE_PATH;
200
+ const dataRoot = env.PLUGIN_DATA?.trim() || join(homedir(), ".local", "share", "lazycodex");
201
+ return join(dataRoot, "model-catalog-state.json");
202
+ }
203
+
204
+ async function readState(statePath) {
205
+ try {
206
+ const parsed = JSON.parse(await readFile(statePath, "utf8"));
207
+ return isRecord(parsed) ? parsed : {};
208
+ } catch (error) {
209
+ if (error instanceof Error) return {};
210
+ throw error;
211
+ }
212
+ }
213
+
214
+ async function writeState(statePath, state) {
215
+ await mkdir(dirname(statePath), { recursive: true });
216
+ await writeFile(statePath, `${JSON.stringify(state, null, 2)}\n`);
217
+ }
218
+
219
+ async function pathExists(path) {
220
+ try {
221
+ await stat(path);
222
+ return true;
223
+ } catch (error) {
224
+ if (error instanceof Error && "code" in error && error.code === "ENOENT") return false;
225
+ throw error;
226
+ }
227
+ }
228
+
229
+ function replaceOrInsertRootSetting(config, key, value) {
230
+ const lines = config.split(/\n/);
231
+ const output = [];
232
+ let replaced = false;
233
+ let inserted = false;
234
+ for (const line of lines) {
235
+ if (!inserted && isSectionHeader(line)) {
236
+ if (!replaced) output.push(`${key} = ${value}`);
237
+ inserted = true;
238
+ }
239
+ if (isRootSetting(line, key)) {
240
+ if (!replaced) {
241
+ output.push(`${key} = ${value}`);
242
+ replaced = true;
243
+ }
244
+ continue;
245
+ }
246
+ output.push(line);
247
+ }
248
+ if (!replaced && !inserted) output.push(`${key} = ${value}`);
249
+ return output.join("\n");
250
+ }
251
+
252
+ function isSectionHeader(line) {
253
+ const trimmed = line.trim();
254
+ return trimmed.startsWith("[") && trimmed.endsWith("]");
255
+ }
256
+
257
+ function isRootSetting(line, key) {
258
+ const trimmed = line.trimStart();
259
+ if (trimmed.startsWith("#") || trimmed.startsWith("[")) return false;
260
+ const match = trimmed.match(/^([A-Za-z_][A-Za-z0-9_]*)\s*=/);
261
+ return match?.[1] === key;
262
+ }
263
+
264
+ if (process.argv[1] !== undefined && import.meta.url === pathToFileURL(process.argv[1]).href) {
265
+ migrateCodexConfig().catch((error) => {
266
+ if (!(error instanceof Error)) throw error;
267
+ process.exit(0);
268
+ });
269
+ }
@@ -37,18 +37,13 @@ async function readComponentVersions(root) {
37
37
  const versions = new Map();
38
38
  for (const entry of entries) {
39
39
  if (!entry.isDirectory()) continue;
40
- versions.set(entry.name, await readPackageVersion(join(componentsRoot, entry.name, "package.json")));
40
+ const packageJsonPath = join(componentsRoot, entry.name, "package.json");
41
+ if (!(await exists(packageJsonPath))) continue;
42
+ versions.set(entry.name, await readPackageVersion(packageJsonPath));
41
43
  }
42
44
  return versions;
43
45
  }
44
46
 
45
- function componentVersionForCommand(command, componentVersions, fallbackVersion) {
46
- for (const [componentName, version] of componentVersions.entries()) {
47
- if (command.includes(`/components/${componentName}/dist/cli.js`)) return version;
48
- }
49
- return fallbackVersion;
50
- }
51
-
52
47
  function syncHooksJson(hooksJson, versionForCommand) {
53
48
  for (const groups of Object.values(hooksJson.hooks)) {
54
49
  for (const group of groups) {
@@ -74,7 +69,7 @@ export async function syncHookStatusMessages(root = defaultRoot) {
74
69
  const componentVersions = await readComponentVersions(root);
75
70
  const aggregateHooksPath = join(root, "hooks", "hooks.json");
76
71
  const aggregateHooks = await readJson(aggregateHooksPath);
77
- syncHooksJson(aggregateHooks, (command) => componentVersionForCommand(command, componentVersions, aggregateVersion));
72
+ syncHooksJson(aggregateHooks, () => aggregateVersion);
78
73
  await writeJson(aggregateHooksPath, aggregateHooks);
79
74
 
80
75
  for (const [componentName, version] of componentVersions.entries()) {
@@ -22,15 +22,15 @@ This skill may include examples copied from the OpenCode harness. In Codex, do n
22
22
 
23
23
  | OpenCode example | Codex tool to use |
24
24
  | --- | --- |
25
- | \`call_omo_agent(subagent_type="explore", ...)\` | \`spawn_agent(agent_type="explorer", task_name="...", message="...")\` |
26
- | \`call_omo_agent(subagent_type="librarian", ...)\` | \`spawn_agent(agent_type="librarian", task_name="...", message="...")\` |
27
- | \`task(subagent_type="plan", ...)\` | \`spawn_agent(agent_type="plan", task_name="...", message="...")\` |
28
- | \`task(subagent_type="oracle", ...)\` for final verification | \`spawn_agent(agent_type="codex-ultrawork-reviewer", task_name="...", message="...")\` |
29
- | \`task(category="...", ...)\` for implementation or QA | \`spawn_agent(agent_type="worker", task_name="...", message="...")\` |
25
+ | \`call_omo_agent(subagent_type="explore", ...)\` | \`spawn_agent(agent_type="explorer", task_name="...", message="...", fork_turns="none")\` |
26
+ | \`call_omo_agent(subagent_type="librarian", ...)\` | \`spawn_agent(agent_type="librarian", task_name="...", message="...", fork_turns="none")\` |
27
+ | \`task(subagent_type="plan", ...)\` | \`spawn_agent(agent_type="plan", task_name="...", message="...", fork_turns="none")\` |
28
+ | \`task(subagent_type="oracle", ...)\` for final verification | \`spawn_agent(agent_type="codex-ultrawork-reviewer", task_name="...", message="...", fork_turns="none")\` |
29
+ | \`task(category="...", ...)\` for implementation or QA | \`spawn_agent(agent_type="worker", task_name="...", message="...", fork_turns="none")\` |
30
30
  | \`background_output(task_id="...")\` | \`wait_agent(...)\` to wait for subagent completion and mailbox updates |
31
31
  | \`team_*(...)\` | Use Codex native subagents plus \`send_message\`, \`followup_task\`, \`wait_agent\`, and \`close_agent\` |
32
32
 
33
- 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.
33
+ 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.
34
34
 
35
35
  `;
36
36
 
@@ -8,15 +8,15 @@ This skill may include examples copied from the OpenCode harness. In Codex, do n
8
8
 
9
9
  | OpenCode example | Codex tool to use |
10
10
  | --- | --- |
11
- | `call_omo_agent(subagent_type="explore", ...)` | `spawn_agent(agent_type="explorer", task_name="...", message="...")` |
12
- | `call_omo_agent(subagent_type="librarian", ...)` | `spawn_agent(agent_type="librarian", task_name="...", message="...")` |
13
- | `task(subagent_type="plan", ...)` | `spawn_agent(agent_type="plan", task_name="...", message="...")` |
14
- | `task(subagent_type="oracle", ...)` for final verification | `spawn_agent(agent_type="codex-ultrawork-reviewer", task_name="...", message="...")` |
15
- | `task(category="...", ...)` for implementation or QA | `spawn_agent(agent_type="worker", task_name="...", message="...")` |
11
+ | `call_omo_agent(subagent_type="explore", ...)` | `spawn_agent(agent_type="explorer", task_name="...", message="...", fork_turns="none")` |
12
+ | `call_omo_agent(subagent_type="librarian", ...)` | `spawn_agent(agent_type="librarian", task_name="...", message="...", fork_turns="none")` |
13
+ | `task(subagent_type="plan", ...)` | `spawn_agent(agent_type="plan", task_name="...", message="...", fork_turns="none")` |
14
+ | `task(subagent_type="oracle", ...)` for final verification | `spawn_agent(agent_type="codex-ultrawork-reviewer", task_name="...", message="...", fork_turns="none")` |
15
+ | `task(category="...", ...)` for implementation or QA | `spawn_agent(agent_type="worker", task_name="...", message="...", fork_turns="none")` |
16
16
  | `background_output(task_id="...")` | `wait_agent(...)` to wait for subagent completion and mailbox updates |
17
17
  | `team_*(...)` | Use Codex native subagents plus `send_message`, `followup_task`, `wait_agent`, and `close_agent` |
18
18
 
19
- 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.
19
+ 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.
20
20
 
21
21
  # /init-deep
22
22