oh-my-opencode 4.5.12 → 4.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (147) hide show
  1. package/.agents/skills/opencode-qa/SKILL.md +194 -0
  2. package/.agents/skills/opencode-qa/references/cli-commands.md +188 -0
  3. package/.agents/skills/opencode-qa/references/db-investigation.md +197 -0
  4. package/.agents/skills/opencode-qa/references/events-hooks.md +110 -0
  5. package/.agents/skills/opencode-qa/references/sdk.md +96 -0
  6. package/.agents/skills/opencode-qa/references/server-api.md +200 -0
  7. package/.agents/skills/opencode-qa/references/testing-harness.md +218 -0
  8. package/.agents/skills/opencode-qa/references/tui-tmux.md +52 -0
  9. package/.agents/skills/opencode-qa/scripts/db-session-by-id.sh +53 -0
  10. package/.agents/skills/opencode-qa/scripts/db-session-by-name.sh +57 -0
  11. package/.agents/skills/opencode-qa/scripts/db-session-by-text.sh +158 -0
  12. package/.agents/skills/opencode-qa/scripts/export-roundtrip.sh +57 -0
  13. package/.agents/skills/opencode-qa/scripts/lib/common.sh +216 -0
  14. package/.agents/skills/opencode-qa/scripts/server-smoke.sh +64 -0
  15. package/.agents/skills/opencode-qa/scripts/sse-hook-probe.sh +106 -0
  16. package/.agents/skills/opencode-qa/scripts/tui-smoke.sh +89 -0
  17. package/README.ja.md +13 -3
  18. package/README.ko.md +13 -3
  19. package/README.md +24 -14
  20. package/README.ru.md +13 -3
  21. package/README.zh-cn.md +13 -3
  22. package/bin/oh-my-opencode.js +4 -3
  23. package/bin/oh-my-opencode.test.ts +35 -7
  24. package/bin/platform.d.ts +1 -1
  25. package/bin/platform.js +4 -4
  26. package/bin/platform.test.ts +31 -9
  27. package/dist/cli/cleanup-command.d.ts +4 -0
  28. package/dist/cli/cleanup.d.ts +11 -0
  29. package/dist/cli/cli-program.d.ts +2 -1
  30. package/dist/cli/index.js +1837 -450
  31. package/dist/cli/install-codex/codex-cache.d.ts +1 -0
  32. package/dist/cli/install-codex/codex-cleanup-config.d.ts +6 -0
  33. package/dist/cli/install-codex/codex-cleanup.d.ts +21 -0
  34. package/dist/cli/install-codex/codex-config-mcp.d.ts +1 -0
  35. package/dist/cli/install-codex/codex-config-permissions.d.ts +1 -0
  36. package/dist/cli/install-codex/codex-config-reasoning.d.ts +1 -0
  37. package/dist/cli/install-codex/codex-config-toml.d.ts +2 -1
  38. package/dist/cli/install-codex/codex-installation-detection.d.ts +36 -0
  39. package/dist/cli/install-codex/codex-package-layout.d.ts +1 -0
  40. package/dist/cli/install-codex/codex-project-local-cleanup-best-effort.d.ts +7 -0
  41. package/dist/cli/install-codex/codex-project-local-cleanup.d.ts +35 -0
  42. package/dist/cli/install-codex/git-bash.d.ts +35 -0
  43. package/dist/cli/install-codex/index.d.ts +4 -0
  44. package/dist/cli/install-codex/toml-section-editor.d.ts +2 -0
  45. package/dist/cli/install-codex/types.d.ts +20 -0
  46. package/dist/cli/run/event-state.d.ts +1 -0
  47. package/dist/cli/run/poll-for-completion.d.ts +1 -0
  48. package/dist/cli/run/prompt-start.d.ts +7 -0
  49. package/dist/cli/star-request.d.ts +9 -0
  50. package/dist/config/schema/hooks.d.ts +0 -1
  51. package/dist/create-hooks.d.ts +0 -1
  52. package/dist/features/builtin-skills/skills/debugging.d.ts +2 -0
  53. package/dist/features/builtin-skills/skills/index.d.ts +1 -0
  54. package/dist/hooks/index.d.ts +0 -1
  55. package/dist/index.js +267 -114
  56. package/dist/plugin/hooks/create-core-hooks.d.ts +0 -1
  57. package/dist/plugin/hooks/create-session-hooks.d.ts +1 -2
  58. package/dist/plugin/messages-transform.d.ts +8 -1
  59. package/dist/plugin/user-abort-interrupted-recovery-guard.d.ts +6 -0
  60. package/dist/shared/prompt-async-gate/recent-dispatches.d.ts +14 -0
  61. package/dist/shared/prompt-async-gate/semantic-dedupe.d.ts +7 -0
  62. package/dist/shared/prompt-async-gate/session-idle-dispatch.d.ts +1 -0
  63. package/dist/shared/prompt-async-gate/timing.d.ts +1 -0
  64. package/dist/shared/prompt-async-gate/types.d.ts +2 -0
  65. package/dist/shared/prompt-async-gate.d.ts +1 -1
  66. package/package.json +22 -17
  67. package/packages/git-bash-mcp/dist/cli.js +367 -0
  68. package/packages/omo-codex/plugin/.mcp.json +11 -0
  69. package/packages/omo-codex/plugin/components/comment-checker/README.md +1 -1
  70. package/packages/omo-codex/plugin/components/git-bash/hooks/hooks.json +29 -0
  71. package/packages/omo-codex/plugin/components/git-bash/package.json +23 -0
  72. package/packages/omo-codex/plugin/components/git-bash/src/cli.ts +33 -0
  73. package/packages/omo-codex/plugin/components/git-bash/src/codex-hook.ts +180 -0
  74. package/packages/omo-codex/plugin/components/git-bash/src/index.ts +10 -0
  75. package/packages/omo-codex/plugin/components/git-bash/test/codex-hook.test.ts +195 -0
  76. package/packages/omo-codex/plugin/components/git-bash/tsconfig.build.json +13 -0
  77. package/packages/omo-codex/plugin/components/git-bash/tsconfig.json +25 -0
  78. package/packages/omo-codex/plugin/components/lsp/README.md +1 -1
  79. package/packages/omo-codex/plugin/components/lsp/src/cli.ts +5 -5
  80. package/packages/omo-codex/plugin/components/lsp/src/codex-hook-cli.ts +33 -0
  81. package/packages/omo-codex/plugin/components/lsp/src/codex-hook.ts +19 -27
  82. package/packages/omo-codex/plugin/components/lsp/test/codex-hook-cli.test.ts +28 -0
  83. package/packages/omo-codex/plugin/components/lsp/test/codex-hook-errors.test.ts +55 -0
  84. package/packages/omo-codex/plugin/components/lsp/test/package-smoke.test.ts +7 -5
  85. package/packages/omo-codex/plugin/components/rules/README.md +1 -1
  86. package/packages/omo-codex/plugin/components/rules/bundled-rules/windows-git-bash.md +10 -0
  87. package/packages/omo-codex/plugin/components/rules/test/package-smoke.test.ts +3 -1
  88. package/packages/omo-codex/plugin/components/rules/test/windows-git-bash-bundled-rule.test.ts +97 -0
  89. package/packages/omo-codex/plugin/components/start-work-continuation/directive.md +5 -4
  90. package/packages/omo-codex/plugin/components/start-work-continuation/test/codex-hook.test.ts +22 -0
  91. package/packages/omo-codex/plugin/components/ultrawork/README.md +2 -2
  92. package/packages/omo-codex/plugin/components/ultrawork/agents/codex-ultrawork-reviewer.toml +1 -0
  93. package/packages/omo-codex/plugin/components/ultrawork/agents/librarian.toml +8 -7
  94. package/packages/omo-codex/plugin/components/ultrawork/agents/plan.toml +2 -1
  95. package/packages/omo-codex/plugin/components/ultrawork/directive.md +31 -5
  96. package/packages/omo-codex/plugin/components/ultrawork/test/codex-hook.test.ts +27 -4
  97. package/packages/omo-codex/plugin/components/ultrawork/test/package-smoke.test.ts +25 -0
  98. package/packages/omo-codex/plugin/components/ulw-loop/README.md +1 -1
  99. package/packages/omo-codex/plugin/components/ulw-loop/skills/ulw-loop/SKILL.md +27 -205
  100. package/packages/omo-codex/plugin/components/ulw-loop/skills/ulw-loop/references/full-workflow.md +230 -0
  101. package/packages/omo-codex/plugin/components/ulw-loop/test/package-smoke.test.ts +102 -5
  102. package/packages/omo-codex/plugin/hooks/hooks.json +24 -2
  103. package/packages/omo-codex/plugin/package-lock.json +19 -0
  104. package/packages/omo-codex/plugin/package.json +3 -1
  105. package/packages/omo-codex/plugin/scripts/build-bundled-mcp-runtimes.mjs +16 -1
  106. package/packages/omo-codex/plugin/scripts/build-components.mjs +2 -1
  107. package/packages/omo-codex/plugin/scripts/sync-hook-status-messages.mjs +87 -0
  108. package/packages/omo-codex/plugin/skills/review-work/SKILL.md +27 -2
  109. package/packages/omo-codex/plugin/skills/start-work/SKILL.md +20 -0
  110. package/packages/omo-codex/plugin/skills/ulw-loop/SKILL.md +27 -205
  111. package/packages/omo-codex/plugin/skills/ulw-loop/references/full-workflow.md +230 -0
  112. package/packages/omo-codex/plugin/test/aggregate.test.mjs +23 -8
  113. package/packages/omo-codex/plugin/test/hook-status-message.test.mjs +56 -11
  114. package/packages/omo-codex/plugin/test/install-time-build-runtime.test.mjs +34 -0
  115. package/packages/omo-codex/plugin/test/mcp-research-servers.test.mjs +21 -0
  116. package/packages/omo-codex/plugin/test/node-install-surface.test.mjs +48 -0
  117. package/packages/omo-codex/plugin/test/subagent-guidance.test.mjs +76 -0
  118. package/packages/omo-codex/plugin/test/sync-hook-status-messages.test.mjs +66 -0
  119. package/packages/omo-codex/plugin/test/sync-skills.test.mjs +32 -2
  120. package/packages/omo-codex/scripts/install/cache.mjs +5 -3
  121. package/packages/omo-codex/scripts/install/cli-args.mjs +112 -0
  122. package/packages/omo-codex/scripts/install/config.mjs +36 -1
  123. package/packages/omo-codex/scripts/install/delegated-command.mjs +25 -0
  124. package/packages/omo-codex/scripts/install/git-bash.mjs +99 -0
  125. package/packages/omo-codex/scripts/install/git-bash.test.mjs +174 -0
  126. package/packages/omo-codex/scripts/install/mcp-runtime-cache.mjs +5 -1
  127. package/packages/omo-codex/scripts/install/multi-agent-v2-config.mjs +7 -1
  128. package/packages/omo-codex/scripts/install/permissions.d.mts +1 -0
  129. package/packages/omo-codex/scripts/install/permissions.mjs +26 -0
  130. package/packages/omo-codex/scripts/install/project-local-cleanup.mjs +229 -0
  131. package/packages/omo-codex/scripts/install/reasoning-config.mjs +14 -0
  132. package/packages/omo-codex/scripts/install/source-package-build.mjs +20 -0
  133. package/packages/omo-codex/scripts/install/toml-editor.mjs +19 -2
  134. package/packages/omo-codex/scripts/install-cli-args.test.mjs +146 -0
  135. package/packages/omo-codex/scripts/install-config-autonomous.test.mjs +48 -0
  136. package/packages/omo-codex/scripts/install-config-reasoning.test.mjs +62 -0
  137. package/packages/omo-codex/scripts/install-config.test.mjs +206 -0
  138. package/packages/omo-codex/scripts/install-local-entrypoint.test.mjs +129 -0
  139. package/packages/omo-codex/scripts/install-local-git-bash-preflight.test.mjs +145 -0
  140. package/packages/omo-codex/scripts/install-local.mjs +91 -8
  141. package/packages/omo-codex/scripts/install-local.test.mjs +15 -0
  142. package/packages/omo-codex/scripts/install-mcp-runtime.test.mjs +60 -0
  143. package/packages/omo-codex/scripts/install-packaged-local.test.mjs +67 -0
  144. package/packages/omo-codex/scripts/install-project-local-cleanup.test.mjs +277 -0
  145. package/packages/shared-skills/skills/review-work/SKILL.md +27 -2
  146. package/packages/shared-skills/skills/start-work/SKILL.md +20 -0
  147. package/dist/hooks/context-window-monitor.d.ts +0 -19
@@ -1,5 +1,5 @@
1
1
  import assert from "node:assert/strict";
2
- import { readdir, readFile } from "node:fs/promises";
2
+ import { access, readdir, readFile } from "node:fs/promises";
3
3
  import { dirname, join } from "node:path";
4
4
  import test from "node:test";
5
5
  import { fileURLToPath } from "node:url";
@@ -46,18 +46,48 @@ async function readJson(relativePath) {
46
46
  return JSON.parse(await readFile(join(root, relativePath), "utf8"));
47
47
  }
48
48
 
49
+ async function exists(relativePath) {
50
+ try {
51
+ await access(join(root, relativePath));
52
+ return true;
53
+ } catch (error) {
54
+ if (error instanceof Error && "code" in error && error.code === "ENOENT") return false;
55
+ throw error;
56
+ }
57
+ }
58
+
49
59
  async function readComponentHookManifests() {
50
60
  const components = await readdir(join(root, "components"), { withFileTypes: true });
51
61
  const manifests = [];
52
62
  for (const entry of components) {
53
63
  if (!entry.isDirectory()) continue;
54
64
  const source = join("components", entry.name, "hooks", "hooks.json");
65
+ if (!(await exists(source))) continue;
55
66
  const packageJson = await readJson(join("components", entry.name, "package.json"));
56
67
  manifests.push({ source, version: packageJson.version, hooks: await readJson(source) });
57
68
  }
58
69
  return manifests.sort((left, right) => left.source.localeCompare(right.source));
59
70
  }
60
71
 
72
+ async function readComponentVersions() {
73
+ const components = await readdir(join(root, "components"), { withFileTypes: true });
74
+ const versions = new Map();
75
+ for (const entry of components) {
76
+ if (!entry.isDirectory()) continue;
77
+ const packageJson = await readJson(join("components", entry.name, "package.json"));
78
+ versions.set(entry.name, packageJson.version);
79
+ }
80
+ return versions;
81
+ }
82
+
83
+ function hookOwnerVersion(hook, aggregateVersion, componentVersions) {
84
+ const command = hook.command;
85
+ for (const [componentName, version] of componentVersions.entries()) {
86
+ if (command.includes(`/components/${componentName}/dist/cli.js`)) return version;
87
+ }
88
+ return aggregateVersion;
89
+ }
90
+
61
91
  function collectCommandHooks(hooks, source, version) {
62
92
  const commandHooks = [];
63
93
  const normalizedSource = source.replaceAll("\\", "/");
@@ -68,6 +98,7 @@ function collectCommandHooks(hooks, source, version) {
68
98
  commandHooks.push({
69
99
  id: `${normalizedSource}:${eventName}:${groupIndex}:${handlerIndex}`,
70
100
  version,
101
+ command: handler.command,
71
102
  statusMessage: handler.statusMessage,
72
103
  });
73
104
  });
@@ -88,6 +119,18 @@ test("#given hook status label #when formatting #then prefixes LazyCodex with ve
88
119
  assert.equal(message, "LazyCodex(0.1.0): Checking Comments");
89
120
  });
90
121
 
122
+ test("#given hook status label with blank version #when formatting #then prefixes LazyCodex with local version", () => {
123
+ // given
124
+ const version = " ";
125
+ const label = "Checking Comments";
126
+
127
+ // when
128
+ const message = formatLazyCodexHookStatusMessage(version, label);
129
+
130
+ // then
131
+ assert.equal(message, "LazyCodex(local): Checking Comments");
132
+ });
133
+
91
134
  test("#given loose legacy status label #when normalizing #then removes OMO wording and title-cases label", () => {
92
135
  // given
93
136
  const version = "0.1.0";
@@ -104,15 +147,15 @@ test("#given loose legacy status label #when normalizing #then removes OMO wordi
104
147
 
105
148
  test("#given aggregate comment-checker hook #when status is inspected #then it uses LazyCodex comments label", async () => {
106
149
  // given
107
- const aggregateVersion = (await readJson(".codex-plugin/plugin.json")).version;
108
150
  const aggregateHooks = await readJson("hooks/hooks.json");
151
+ const componentVersions = await readComponentVersions();
109
152
 
110
153
  // when
111
- const hooks = collectCommandHooks(aggregateHooks, "hooks/hooks.json", aggregateVersion);
154
+ const hooks = collectCommandHooks(aggregateHooks, "hooks/hooks.json", "0.1.0");
112
155
  const commentCheckerHook = hooks.find((hook) => hook.id === "hooks/hooks.json:PostToolUse:0:0");
113
156
 
114
157
  // then
115
- assert.equal(commentCheckerHook?.statusMessage, formatLazyCodexHookStatusMessage("0.1.0", "Checking Comments"));
158
+ assert.equal(commentCheckerHook?.statusMessage, formatLazyCodexHookStatusMessage(componentVersions.get("comment-checker"), "Checking Comments"));
116
159
  assert.doesNotMatch(JSON.stringify(aggregateHooks), /checking\s+OMO\s+comments/i);
117
160
  });
118
161
 
@@ -121,18 +164,22 @@ test("#given aggregate and component hooks #when status messages are inspected #
121
164
  const aggregateVersion = (await readJson(".codex-plugin/plugin.json")).version;
122
165
  const aggregateHooks = await readJson("hooks/hooks.json");
123
166
  const componentManifests = await readComponentHookManifests();
167
+ const componentVersions = await readComponentVersions();
124
168
 
125
169
  // when
126
170
  const commandHooks = [
127
- ...collectCommandHooks(aggregateHooks, "hooks/hooks.json", aggregateVersion),
171
+ ...collectCommandHooks(aggregateHooks, "hooks/hooks.json", aggregateVersion).map((hook) => ({
172
+ ...hook,
173
+ version: hookOwnerVersion(hook, aggregateVersion, componentVersions),
174
+ })),
128
175
  ...componentManifests.flatMap((manifest) => collectCommandHooks(manifest.hooks, manifest.source, manifest.version)),
129
176
  ];
130
177
  const expectedLabels = new Map([...AGGREGATE_EXPECTED_LABELS, ...COMPONENT_EXPECTED_LABELS]);
131
178
  const mismatches = commandHooks
132
179
  .map((hook) => {
133
- const label = expectedLabels.get(hook.id);
134
- const expected = label === undefined ? undefined : formatLazyCodexHookStatusMessage(hook.version, label);
135
180
  const parsed = parseLazyCodexHookStatusMessage(hook.statusMessage);
181
+ const label = parsed?.label;
182
+ const expected = label === undefined ? undefined : formatLazyCodexHookStatusMessage(hook.version, label);
136
183
  return { ...hook, expected, parsed };
137
184
  })
138
185
  .filter((hook) => hook.expected === undefined || hook.statusMessage !== hook.expected || hook.parsed === null)
@@ -140,10 +187,8 @@ test("#given aggregate and component hooks #when status messages are inspected #
140
187
 
141
188
  // then
142
189
  assert.deepEqual(mismatches, []);
143
- assert.deepEqual(
144
- commandHooks.map((hook) => hook.id).sort(),
145
- [...expectedLabels.keys()].sort(),
146
- );
190
+ const actualLabels = new Set(commandHooks.map((hook) => parseLazyCodexHookStatusMessage(hook.statusMessage)?.label));
191
+ assert.deepEqual([...expectedLabels.values()].filter((label) => !actualLabels.has(label)), []);
147
192
  for (const hook of commandHooks) {
148
193
  assert.doesNotMatch(hook.statusMessage, /\bOMO\b/i);
149
194
  }
@@ -0,0 +1,34 @@
1
+ import assert from "node:assert/strict";
2
+ import { readFile } from "node:fs/promises";
3
+ import { dirname, join } from "node:path";
4
+ import test from "node:test";
5
+ import { fileURLToPath } from "node:url";
6
+
7
+ const root = dirname(dirname(fileURLToPath(import.meta.url)));
8
+
9
+ test("#given aggregate build scripts #when inspected #then install-time build does not invoke Bun", async () => {
10
+ // given
11
+ const buildComponentsScript = await readFile(join(root, "scripts", "build-components.mjs"), "utf8");
12
+ const buildBundledMcpRuntimesScript = await readFile(join(root, "scripts", "build-bundled-mcp-runtimes.mjs"), "utf8");
13
+
14
+ // when
15
+ const installTimeBuildScripts = [buildComponentsScript, buildBundledMcpRuntimesScript].join("\n");
16
+
17
+ // then
18
+ assert.doesNotMatch(installTimeBuildScripts, /spawnSync\("bun"/);
19
+ assert.doesNotMatch(installTimeBuildScripts, /\bbun\s+run\b/);
20
+ });
21
+
22
+ test("#given aggregate build scripts #when inspected #then npm subprocesses resolve on Windows", async () => {
23
+ // given
24
+ const buildComponentsScript = await readFile(join(root, "scripts", "build-components.mjs"), "utf8");
25
+ const buildBundledMcpRuntimesScript = await readFile(join(root, "scripts", "build-bundled-mcp-runtimes.mjs"), "utf8");
26
+
27
+ // when
28
+ const installTimeBuildScripts = [buildComponentsScript, buildBundledMcpRuntimesScript].join("\n");
29
+
30
+ // then
31
+ assert.match(installTimeBuildScripts, /process\.platform === "win32"/);
32
+ assert.match(installTimeBuildScripts, /shell: process\.platform === "win32"/);
33
+ assert.doesNotMatch(installTimeBuildScripts, /npm\.cmd/);
34
+ });
@@ -0,0 +1,21 @@
1
+ import assert from "node:assert/strict";
2
+ import { readFile } from "node:fs/promises";
3
+ import { dirname, join } from "node:path";
4
+ import test from "node:test";
5
+ import { fileURLToPath } from "node:url";
6
+
7
+ const root = dirname(dirname(fileURLToPath(import.meta.url)));
8
+
9
+ test("#given aggregate MCP config #when inspected #then registers research and structural-search MCPs", async () => {
10
+ // given
11
+ const mcp = JSON.parse(await readFile(join(root, ".mcp.json"), "utf8"));
12
+
13
+ // when
14
+ const serverNames = Object.keys(mcp.mcpServers).sort();
15
+
16
+ // then
17
+ assert.deepEqual(serverNames, ["ast_grep", "context7", "git_bash", "grep_app", "lsp"]);
18
+ assert.equal(mcp.mcpServers.grep_app.url, "https://mcp.grep.app");
19
+ assert.equal(mcp.mcpServers.context7.url, "https://mcp.context7.com/mcp");
20
+ assert.deepEqual(mcp.mcpServers.ast_grep.args, ["../../ast-grep-mcp/dist/cli.js", "mcp"]);
21
+ });
@@ -0,0 +1,48 @@
1
+ import assert from "node:assert/strict";
2
+ import { readFile } from "node:fs/promises";
3
+ import { dirname, join } from "node:path";
4
+ import test from "node:test";
5
+ import { fileURLToPath } from "node:url";
6
+
7
+ const pluginRoot = dirname(dirname(fileURLToPath(import.meta.url)));
8
+ const repoRoot = join(pluginRoot, "..", "..", "..");
9
+
10
+ test("#given Codex Light install docs #when inspected #then lazycodex is npm-first and Bun-free", async () => {
11
+ // given
12
+ const files = [
13
+ join(repoRoot, "README.md"),
14
+ join(repoRoot, "docs", "guide", "installation.md"),
15
+ join(repoRoot, "packages", "omo-codex", "README.md"),
16
+ join(repoRoot, "packages", "omo-codex", "MARKETPLACE.md"),
17
+ join(pluginRoot, "components", "ultrawork", "README.md"),
18
+ join(pluginRoot, "components", "ulw-loop", "README.md"),
19
+ ];
20
+
21
+ // when
22
+ const docs = await Promise.all(files.map(async (path) => [path, await readFile(path, "utf8")]));
23
+
24
+ // then
25
+ for (const [path, text] of docs) {
26
+ assert.match(text, /\bnpx lazycodex-ai install\b/, `${path} should document the Node/npm install command`);
27
+ assert.doesNotMatch(text, /\bbunx lazycodex-ai\b/, `${path} should not require Bun for lazycodex`);
28
+ }
29
+ });
30
+
31
+ test("#given cleanup troubleshooting docs #when inspected #then project-local cleanup and command delegation are documented", async () => {
32
+ // given
33
+ const files = [
34
+ join(repoRoot, "README.md"),
35
+ join(repoRoot, "docs", "guide", "installation.md"),
36
+ join(repoRoot, "packages", "omo-codex", "README.md"),
37
+ ];
38
+
39
+ // when
40
+ const docs = await Promise.all(files.map(async (path) => [path, await readFile(path, "utf8")]));
41
+
42
+ // then
43
+ for (const [path, text] of docs) {
44
+ assert.match(text, /\bnpx lazycodex-ai cleanup\b/, `${path} should document the LazyCodex cleanup command`);
45
+ assert.match(text, /project-local .*\.codex\/config\.toml/i, `${path} should mention project-local config repair`);
46
+ assert.match(text, /\.codex.*\.omx|\.omx.*\.codex/s, `${path} should distinguish project-local .codex and .omx artifacts`);
47
+ }
48
+ });
@@ -0,0 +1,76 @@
1
+ import assert from "node:assert/strict";
2
+ import { readFile } from "node:fs/promises";
3
+ import { dirname, join } from "node:path";
4
+ import test from "node:test";
5
+ import { fileURLToPath } from "node:url";
6
+
7
+ const root = dirname(dirname(fileURLToPath(import.meta.url)));
8
+
9
+ const SKILLS = [
10
+ "review-work",
11
+ "start-work",
12
+ "ulw-loop",
13
+ ];
14
+
15
+ const AGENT_FILES = [
16
+ "components/ultrawork/agents/codex-ultrawork-reviewer.toml",
17
+ "components/ultrawork/agents/plan.toml",
18
+ ];
19
+
20
+ test("#given orchestration skills #when inspected #then Codex subagent delegation is hardened", async () => {
21
+ // given
22
+ const skillPaths = SKILLS.map((skillName) => join("skills", skillName, "SKILL.md"));
23
+
24
+ // when
25
+ const missing = [];
26
+ for (const skillPath of skillPaths) {
27
+ const text = await readFile(join(root, skillPath), "utf8");
28
+ if (
29
+ !/TASK:/.test(text) ||
30
+ !/fork_turns:\s*"none"/.test(text) ||
31
+ !/wait_agent.*signal, not proof/s.test(text) ||
32
+ !/one targeted followup/.test(text) ||
33
+ !/respawn.*smaller/s.test(text) ||
34
+ !/model.*reasoning_effort.*default agent/s.test(text) ||
35
+ !/Plan and reviewer agents may run for a long time/.test(text) ||
36
+ !/short wait_agent cycles/.test(text) ||
37
+ !/single long blocking wait/.test(text)
38
+ ) {
39
+ missing.push(skillPath);
40
+ }
41
+ }
42
+
43
+ // then
44
+ assert.deepEqual(missing, []);
45
+ });
46
+
47
+ test("#given ultrawork directive #when inspected #then reviewer fallback keeps an agent role", async () => {
48
+ // given
49
+ const directivePath = "components/ultrawork/directive.md";
50
+
51
+ // when
52
+ const text = await readFile(join(root, directivePath), "utf8");
53
+
54
+ // then
55
+ assert.doesNotMatch(text, /any `gpt-5\.2`\s+xhigh reviewer/);
56
+ assert.match(text, /codex-ultrawork-reviewer/);
57
+ assert.match(text, /agent_type.*worker/s);
58
+ assert.match(text, /model.*reasoning_effort.*default agent/s);
59
+ });
60
+
61
+ test("#given ultrawork agents #when inspected #then inter-agent commentary is treated as assignments", async () => {
62
+ // given
63
+ const agentPaths = AGENT_FILES;
64
+
65
+ // when
66
+ const missing = [];
67
+ for (const agentPath of agentPaths) {
68
+ const text = await readFile(join(root, agentPath), "utf8");
69
+ if (!/TASK:|active review assignment/.test(text) || !/context|commentary/.test(text)) {
70
+ missing.push(agentPath);
71
+ }
72
+ }
73
+
74
+ // then
75
+ assert.deepEqual(missing, []);
76
+ });
@@ -0,0 +1,66 @@
1
+ import assert from "node:assert/strict";
2
+ import { mkdtemp, mkdir, readFile, writeFile } from "node:fs/promises";
3
+ import { join } from "node:path";
4
+ import { tmpdir } from "node:os";
5
+ import test from "node:test";
6
+
7
+ import { syncHookStatusMessages } from "../scripts/sync-hook-status-messages.mjs";
8
+
9
+ async function writeJson(path, value) {
10
+ await writeFile(path, `${JSON.stringify(value, null, "\t")}\n`);
11
+ }
12
+
13
+ async function readJson(path) {
14
+ return JSON.parse(await readFile(path, "utf8"));
15
+ }
16
+
17
+ test("#given a component without hooks #when hook status messages sync #then build-time version sync skips it", async () => {
18
+ // given
19
+ const root = await mkdtemp(join(tmpdir(), "omo-codex-hook-status-"));
20
+ await mkdir(join(root, ".codex-plugin"), { recursive: true });
21
+ await mkdir(join(root, "hooks"), { recursive: true });
22
+ await mkdir(join(root, "components", "comment-checker", "hooks"), { recursive: true });
23
+ await mkdir(join(root, "components", "git-bash"), { recursive: true });
24
+ await writeJson(join(root, ".codex-plugin", "plugin.json"), { version: "0.1.0" });
25
+ await writeJson(join(root, "components", "comment-checker", "package.json"), { version: "0.1.1" });
26
+ await writeJson(join(root, "components", "git-bash", "package.json"), { version: "0.3.0" });
27
+ await writeJson(join(root, "hooks", "hooks.json"), {
28
+ hooks: {
29
+ PostToolUse: [
30
+ {
31
+ hooks: [
32
+ {
33
+ type: "command",
34
+ command: 'node "${PLUGIN_ROOT}/components/comment-checker/dist/cli.js" hook post-tool-use',
35
+ statusMessage: "LazyCodex(0.1.0): Checking Comments",
36
+ },
37
+ ],
38
+ },
39
+ ],
40
+ },
41
+ });
42
+ await writeJson(join(root, "components", "comment-checker", "hooks", "hooks.json"), {
43
+ hooks: {
44
+ PostToolUse: [
45
+ {
46
+ hooks: [
47
+ {
48
+ type: "command",
49
+ command: 'node "${PLUGIN_ROOT}/dist/cli.js" hook post-tool-use',
50
+ statusMessage: "LazyCodex(0.1.0): Checking Comments",
51
+ },
52
+ ],
53
+ },
54
+ ],
55
+ },
56
+ });
57
+
58
+ // when
59
+ await syncHookStatusMessages(root);
60
+
61
+ // then
62
+ const aggregateHooks = await readJson(join(root, "hooks", "hooks.json"));
63
+ 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");
65
+ assert.equal(componentHooks.hooks.PostToolUse[0].hooks[0].statusMessage, "LazyCodex(0.1.1): Checking Comments");
66
+ });
@@ -7,6 +7,7 @@ import { sharedSkillsRootPath } from "@oh-my-opencode/shared-skills";
7
7
 
8
8
  const root = dirname(dirname(fileURLToPath(import.meta.url)));
9
9
  const repoRoot = join(root, "..", "..", "..");
10
+ const CONTEXT_PRESSURE_SKILL_BUDGET_BYTES = 25_000;
10
11
 
11
12
  const expectedSkills = [
12
13
  "comment-checker",
@@ -154,8 +155,12 @@ test("#given synced ulw-loop skill #when Codex hint metadata is inspected #then
154
155
 
155
156
  test("#given synced ulw-loop skill #when worker guidance is inspected #then context-hygiene guidance matches the source", async () => {
156
157
  // given
157
- const sourceSkill = await readFile(join(root, "components", "ulw-loop", "skills", "ulw-loop", "SKILL.md"), "utf8");
158
+ const sourceSkill = await readFile(
159
+ join(root, "components", "ulw-loop", "skills", "ulw-loop", "references", "full-workflow.md"),
160
+ "utf8",
161
+ );
158
162
  const syncedSkill = await readFile(join(root, "skills", "ulw-loop", "SKILL.md"), "utf8");
163
+ const syncedWorkflow = await readFile(join(root, "skills", "ulw-loop", "references", "full-workflow.md"), "utf8");
159
164
  const requiredPatterns = [
160
165
  ["list_agents polling guard", /list_agents/],
161
166
  ["status polling warning", /polling or status tool/],
@@ -164,13 +169,38 @@ test("#given synced ulw-loop skill #when worker guidance is inspected #then cont
164
169
  ["wait_agent completion path", /wait_agent.*completion/],
165
170
  ["targeted followups", /targeted followups only when needed/],
166
171
  ["close_agent cleanup", /close_agent.*after integrating each result/],
172
+ ["long-running plan/reviewer background guidance", /Plan and reviewer agents may run for a long time/],
173
+ ["bounded plan/reviewer polling", /short wait_agent cycles/],
174
+ ["single long wait guard", /single long blocking wait/],
167
175
  ];
168
176
 
169
177
  // when / then
170
178
  for (const [label, pattern] of requiredPatterns) {
171
179
  assert.match(sourceSkill, pattern, `source skill missing ${label}`);
172
- assert.match(syncedSkill, pattern, `synced skill missing ${label}`);
180
+ assert.match(syncedWorkflow, pattern, `synced workflow missing ${label}`);
173
181
  }
182
+ assert.match(syncedSkill, /references\/full-workflow\.md/);
183
+ assert.match(syncedSkill, /wait_agent/);
184
+ assert.match(syncedSkill, /close_agent/);
185
+ });
186
+
187
+ test("#given context-pressure-prone skills #when bundled for Codex #then the eagerly loaded payload stays budgeted", async () => {
188
+ // given
189
+ const skillsRoot = join(root, "skills");
190
+ const skillNames = ["debugging", "ulw-loop"];
191
+
192
+ // when
193
+ let totalBytes = 0;
194
+ for (const skillName of skillNames) {
195
+ const content = await readFile(join(skillsRoot, skillName, "SKILL.md"), "utf8");
196
+ totalBytes += Buffer.byteLength(content, "utf8");
197
+ }
198
+
199
+ // then
200
+ assert.ok(
201
+ totalBytes <= CONTEXT_PRESSURE_SKILL_BUDGET_BYTES,
202
+ `debugging + ulw-loop eager payload is ${totalBytes} bytes, above ${CONTEXT_PRESSURE_SKILL_BUDGET_BYTES}`,
203
+ );
174
204
  });
175
205
 
176
206
  test("#given synced aggregate Codex skills #when they contain OpenCode orchestration examples #then Codex tool compatibility guidance is injected", async () => {
@@ -6,9 +6,11 @@ import { exists, isRecord } from "./utils.mjs";
6
6
  import { COMMAND_SHIM_MARKER } from "./command-shim.mjs";
7
7
  import { removeLegacyCodexComponentBins } from "./legacy-bins.mjs";
8
8
 
9
- export async function installCachedPlugin({ codexHome, marketplaceName, name, runCommand, sourcePath, version }) {
10
- await maybeRunNpmInstall(sourcePath, runCommand);
11
- await maybeRunNpmBuild(sourcePath, runCommand);
9
+ export async function installCachedPlugin({ buildSource = true, codexHome, marketplaceName, name, runCommand, sourcePath, version }) {
10
+ if (buildSource) {
11
+ await maybeRunNpmInstall(sourcePath, runCommand);
12
+ await maybeRunNpmBuild(sourcePath, runCommand);
13
+ }
12
14
 
13
15
  const targetPath = join(codexHome, "plugins", "cache", marketplaceName, name, version);
14
16
  await replaceDirectory(sourcePath, targetPath, shouldCopyPluginPath);
@@ -0,0 +1,112 @@
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"]);
3
+
4
+ export function parseLazyCodexInstallCliArgs(argv) {
5
+ const args = [...argv];
6
+ if (args.length === 0) return { kind: "install", autonomousPermissions: undefined, repoRoot: undefined };
7
+
8
+ let repoRoot;
9
+ let command;
10
+ let dryRun = false;
11
+ let noTui = false;
12
+ let skipAuth = false;
13
+ let autonomousPermissions;
14
+ let index = 0;
15
+ while (index < args.length) {
16
+ const arg = args[index];
17
+ if (arg === "--help" || arg === "-h" || arg === "help") return { kind: "help" };
18
+ if (arg === "--version" || arg === "-v" || arg === "version") return { kind: "version" };
19
+ if (arg === "--dry-run") {
20
+ dryRun = true;
21
+ index += 1;
22
+ continue;
23
+ }
24
+ if (arg === "--no-tui") {
25
+ noTui = true;
26
+ index += 1;
27
+ continue;
28
+ }
29
+ if (arg === "--skip-auth") {
30
+ skipAuth = true;
31
+ index += 1;
32
+ continue;
33
+ }
34
+ if (arg === "--codex-autonomous") {
35
+ autonomousPermissions = true;
36
+ index += 1;
37
+ continue;
38
+ }
39
+ if (arg === "--no-codex-autonomous") {
40
+ autonomousPermissions = false;
41
+ index += 1;
42
+ continue;
43
+ }
44
+ if (arg === "--platform") {
45
+ const platform = readOptionValue(args, index, "--platform");
46
+ if (platform !== "codex") throw new Error(CODEX_ONLY_ERROR);
47
+ index += 2;
48
+ continue;
49
+ }
50
+ if (typeof arg === "string" && arg.startsWith("--platform=")) {
51
+ const platform = arg.slice("--platform=".length);
52
+ if (platform.trim().length === 0) throw new Error("--platform requires a value");
53
+ if (platform !== "codex") throw new Error(CODEX_ONLY_ERROR);
54
+ index += 1;
55
+ continue;
56
+ }
57
+ if (arg === "--repo-root") {
58
+ repoRoot = readOptionValue(args, index, "--repo-root");
59
+ index += 2;
60
+ continue;
61
+ }
62
+ if (typeof arg === "string" && arg.startsWith("--repo-root=")) {
63
+ const value = arg.slice("--repo-root=".length);
64
+ if (value.trim().length === 0) throw new Error("--repo-root requires a path");
65
+ repoRoot = value;
66
+ index += 1;
67
+ continue;
68
+ }
69
+ if (arg === "install" || arg === "setup") {
70
+ if (command !== undefined) throw new Error(`Unsupported lazycodex-ai install option: ${String(arg)}`);
71
+ command = "install";
72
+ index += 1;
73
+ continue;
74
+ }
75
+ if (PASSTHROUGH_COMMANDS.has(arg)) {
76
+ return { kind: "command", command: arg, dryRun, args: args.slice(index + 1) };
77
+ }
78
+ if (command === undefined && typeof arg === "string" && !arg.startsWith("-")) {
79
+ throw new Error(`Unsupported lazycodex-ai command: ${String(arg)}`);
80
+ }
81
+ throw new Error(`Unsupported lazycodex-ai install option: ${String(arg)}`);
82
+ }
83
+
84
+ if (!dryRun) return { kind: "install", autonomousPermissions, repoRoot };
85
+
86
+ return {
87
+ kind: "command",
88
+ command: command ?? "install",
89
+ dryRun,
90
+ noTui,
91
+ skipAuth,
92
+ autonomousPermissions,
93
+ repoRoot,
94
+ args: [],
95
+ };
96
+ }
97
+
98
+ function readOptionValue(args, index, option) {
99
+ const value = args[index + 1];
100
+ if (typeof value !== "string" || value.trim().length === 0) {
101
+ throw new Error(`${option} requires a value`);
102
+ }
103
+ return value;
104
+ }
105
+
106
+ export function formatLazyCodexInstallHelp() {
107
+ return [
108
+ "Usage: lazycodex-ai install [--no-tui] [--codex-autonomous|--no-codex-autonomous] [--repo-root <path>]",
109
+ "",
110
+ "Installs the Codex Light edition into ~/.codex using Node/npm.",
111
+ ].join("\n");
112
+ }
@@ -2,6 +2,8 @@ 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 { ensureCodexReasoningConfig } from "./reasoning-config.mjs";
6
+ import { ensureAutonomousPermissions } from "./permissions.mjs";
5
7
  import { appendBlock, findTomlSection, replaceOrInsertSetting } from "./toml-editor.mjs";
6
8
  import { exists } from "./utils.mjs";
7
9
 
@@ -15,6 +17,14 @@ const MANAGED_CODEX_AGENT_NAMES = [
15
17
  "momus",
16
18
  "plan",
17
19
  ];
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");
18
28
 
19
29
  export async function updateCodexConfig({
20
30
  configPath,
@@ -22,8 +32,10 @@ export async function updateCodexConfig({
22
32
  marketplaceName,
23
33
  marketplaceSource = defaultMarketplaceSource(marketplaceName, repoRoot),
24
34
  pluginNames,
35
+ platform = process.platform,
25
36
  trustedHookStates = [],
26
37
  agentConfigs = [],
38
+ autonomousPermissions = false,
27
39
  }) {
28
40
  await mkdir(dirname(configPath), { recursive: true });
29
41
  let config = "";
@@ -39,11 +51,15 @@ export async function updateCodexConfig({
39
51
  config = removeStaleManagedAgentBlocks(config, new Set(agentConfigs.map((agentConfig) => agentConfig.name)));
40
52
  config = ensureFeatureEnabled(config, "plugins");
41
53
  config = ensureFeatureEnabled(config, "plugin_hooks");
54
+ config = ensureCodexReasoningConfig(config);
42
55
  config = ensureCodexMultiAgentV2Config(config);
56
+ if (autonomousPermissions === true) config = ensureAutonomousPermissions(config);
57
+ config = ensureContext7McpServer(config);
43
58
  config = ensureMarketplaceBlock(config, marketplaceName, marketplaceSource);
44
59
  for (const pluginName of pluginNames) {
45
60
  config = ensurePluginEnabled(config, `${pluginName}@${marketplaceName}`);
46
61
  }
62
+ config = ensureOmoGitBashMcpPolicy(config, { marketplaceName, pluginNames, platform });
47
63
  for (const state of trustedHookStates) {
48
64
  config = ensureHookTrusted(config, state.key, state.trustedHash);
49
65
  }
@@ -129,6 +145,11 @@ function ensureMarketplaceBlock(config, marketplaceName, source) {
129
145
  return appendBlock(config, block);
130
146
  }
131
147
 
148
+ function ensureContext7McpServer(config) {
149
+ if (findTomlSection(config, CONTEXT7_MCP_SERVER_HEADER)) return config;
150
+ return appendBlock(config, CONTEXT7_MCP_SERVER_BLOCK);
151
+ }
152
+
132
153
  function ensurePluginEnabled(config, pluginKey) {
133
154
  const header = `plugins.${JSON.stringify(pluginKey)}`;
134
155
  const section = findTomlSection(config, header);
@@ -136,6 +157,19 @@ function ensurePluginEnabled(config, pluginKey) {
136
157
  return replaceOrInsertSetting(config, section, "enabled", "true");
137
158
  }
138
159
 
160
+ function ensurePluginMcpEnabled(config, pluginKey, serverName, enabled) {
161
+ const header = `plugins.${JSON.stringify(pluginKey)}.mcp_servers.${serverName}`;
162
+ const section = findTomlSection(config, header);
163
+ const enabledValue = enabled ? "true" : "false";
164
+ if (!section) return appendBlock(config, `[${header}]\nenabled = ${enabledValue}\n`);
165
+ return replaceOrInsertSetting(config, section, "enabled", enabledValue);
166
+ }
167
+
168
+ function ensureOmoGitBashMcpPolicy(config, { marketplaceName, pluginNames, platform }) {
169
+ if (marketplaceName !== "sisyphuslabs" || !pluginNames.includes("omo")) return config;
170
+ return ensurePluginMcpEnabled(config, "omo@sisyphuslabs", "git_bash", platform === "win32");
171
+ }
172
+
139
173
  function ensureHookTrusted(config, key, trustedHash) {
140
174
  const header = `hooks.state.${JSON.stringify(key)}`;
141
175
  const section = findTomlSection(config, header);
@@ -223,7 +257,8 @@ function parseJsonString(value) {
223
257
  try {
224
258
  const parsed = JSON.parse(value);
225
259
  return typeof parsed === "string" ? parsed : null;
226
- } catch {
260
+ } catch (error) {
261
+ if (error instanceof Error) return null;
227
262
  return null;
228
263
  }
229
264
  }