oh-my-opencode 4.11.0 → 4.12.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 (177) hide show
  1. package/.agents/skills/codex-qa/scripts/lib/app-server-client.mjs +132 -82
  2. package/.agents/skills/codex-qa/scripts/lib/app-server-client.test.js +48 -0
  3. package/.agents/skills/opencode-qa/scripts/serve-wake-split-probe.sh +361 -84
  4. package/.agents/skills/work-with-pr/SKILL.md +19 -5
  5. package/.opencode/skills/work-with-pr/SKILL.md +19 -5
  6. package/dist/agents/atlas/agent.d.ts +3 -2
  7. package/dist/agents/sisyphus/glm-5-2.d.ts +4 -0
  8. package/dist/agents/sisyphus/index.d.ts +1 -0
  9. package/dist/agents/sisyphus-agent-config.d.ts +1 -0
  10. package/dist/agents/sisyphus-junior/agent.d.ts +1 -1
  11. package/dist/agents/sisyphus-junior/glm-5-2.d.ts +1 -0
  12. package/dist/agents/sisyphus-junior/index.d.ts +1 -0
  13. package/dist/agents/types.d.ts +7 -6
  14. package/dist/cli/index.js +449 -318
  15. package/dist/cli-node/index.js +449 -318
  16. package/dist/features/background-agent/parent-wake-flush-runner.d.ts +1 -0
  17. package/dist/features/background-agent/parent-wake-history-state.d.ts +1 -0
  18. package/dist/features/background-agent/parent-wake-session-history.d.ts +1 -47
  19. package/dist/features/background-agent/parent-wake-session-message.d.ts +47 -0
  20. package/dist/hooks/keyword-detector/ultrawork/glm.d.ts +2 -0
  21. package/dist/hooks/keyword-detector/ultrawork/index.d.ts +4 -2
  22. package/dist/hooks/keyword-detector/ultrawork/source-detector.d.ts +5 -4
  23. package/dist/index.js +1185 -87
  24. package/dist/plugin/tool-execute-before.d.ts +2 -0
  25. package/dist/plugin-handlers/plugin-components-loader.d.ts +1 -0
  26. package/dist/skills/init-deep/SKILL.md +21 -26
  27. package/dist/skills/programming/SKILL.md +25 -121
  28. package/dist/skills/programming/references/code-smells.md +390 -0
  29. package/dist/skills/ulw-plan/SKILL.md +2 -1
  30. package/dist/skills/ulw-plan/references/full-workflow.md +1 -1
  31. package/dist/skills/ulw-plan/references/intent-clear.md +2 -2
  32. package/dist/tools/delegate-task/sync-session-poller.d.ts +1 -2
  33. package/dist/tools/delegate-task/sync-session-turns.d.ts +3 -0
  34. package/dist/tui.js +16 -7
  35. package/package.json +13 -13
  36. package/packages/omo-codex/plugin/.codex-plugin/plugin.json +22 -2
  37. package/packages/omo-codex/plugin/components/bootstrap/dist/cli.js +51 -21
  38. package/packages/omo-codex/plugin/components/bootstrap/hooks/hooks.json +1 -1
  39. package/packages/omo-codex/plugin/components/bootstrap/package.json +1 -1
  40. package/packages/omo-codex/plugin/components/codegraph/dist/cli.js +18 -7
  41. package/packages/omo-codex/plugin/components/codegraph/dist/serve.js +9 -2
  42. package/packages/omo-codex/plugin/components/codegraph/package.json +1 -1
  43. package/packages/omo-codex/plugin/components/codegraph/src/hook.ts +8 -7
  44. package/packages/omo-codex/plugin/components/codegraph/src/serve.ts +7 -2
  45. package/packages/omo-codex/plugin/components/codegraph/src/session-start-worker.ts +2 -1
  46. package/packages/omo-codex/plugin/components/codegraph/test/hook.test.ts +20 -19
  47. package/packages/omo-codex/plugin/components/codegraph/test/provisioned-node-guard.test.ts +94 -0
  48. package/packages/omo-codex/plugin/components/codegraph/test/serve-node-support.test.ts +33 -0
  49. package/packages/omo-codex/plugin/components/comment-checker/hooks/hooks.json +1 -1
  50. package/packages/omo-codex/plugin/components/comment-checker/package.json +1 -1
  51. package/packages/omo-codex/plugin/components/comment-checker/test/package-smoke.test.ts +2 -71
  52. package/packages/omo-codex/plugin/components/git-bash/hooks/hooks.json +2 -2
  53. package/packages/omo-codex/plugin/components/git-bash/package.json +1 -1
  54. package/packages/omo-codex/plugin/components/lazycodex-executor-verify/hooks/hooks.json +1 -1
  55. package/packages/omo-codex/plugin/components/lazycodex-executor-verify/package.json +1 -1
  56. package/packages/omo-codex/plugin/components/lsp/hooks/hooks.json +2 -2
  57. package/packages/omo-codex/plugin/components/lsp/package.json +2 -2
  58. package/packages/omo-codex/plugin/components/lsp/test/package-smoke.test.ts +26 -104
  59. package/packages/omo-codex/plugin/components/rules/dist/cli.js +2 -2
  60. package/packages/omo-codex/plugin/components/rules/hooks/hooks.json +4 -4
  61. package/packages/omo-codex/plugin/components/rules/package.json +1 -1
  62. package/packages/omo-codex/plugin/components/rules/src/sparkshell-awareness.ts +2 -2
  63. package/packages/omo-codex/plugin/components/rules/test/package-smoke.test.ts +12 -92
  64. package/packages/omo-codex/plugin/components/rules/test/sparkshell-awareness.test.ts +3 -0
  65. package/packages/omo-codex/plugin/components/start-work-continuation/dist/cli.js +157 -234
  66. package/packages/omo-codex/plugin/components/start-work-continuation/hooks/hooks.json +2 -2
  67. package/packages/omo-codex/plugin/components/start-work-continuation/package.json +1 -2
  68. package/packages/omo-codex/plugin/components/start-work-continuation/src/boulder-reader.ts +227 -15
  69. package/packages/omo-codex/plugin/components/start-work-continuation/test/boulder-reader.test.ts +62 -2
  70. package/packages/omo-codex/plugin/components/start-work-continuation/test/cli.test.ts +34 -3
  71. package/packages/omo-codex/plugin/components/teammode/skills/teammode/SKILL.md +139 -0
  72. package/packages/omo-codex/plugin/components/teammode/skills/teammode/scripts/team-guide.mjs +111 -0
  73. package/packages/omo-codex/plugin/components/teammode/skills/teammode/scripts/team-state.mjs +250 -0
  74. package/packages/omo-codex/plugin/components/teammode/skills/teammode/scripts/team.mjs +220 -0
  75. package/packages/omo-codex/plugin/components/telemetry/hooks/hooks.json +1 -1
  76. package/packages/omo-codex/plugin/components/telemetry/package.json +1 -1
  77. package/packages/omo-codex/plugin/components/test-support/package-smoke-fixture.ts +158 -0
  78. package/packages/omo-codex/plugin/components/ultrawork/hooks/hooks.json +1 -1
  79. package/packages/omo-codex/plugin/components/ultrawork/package.json +1 -1
  80. package/packages/omo-codex/plugin/components/ultrawork/skills/ulw-plan/SKILL.md +2 -1
  81. package/packages/omo-codex/plugin/components/ultrawork/skills/ulw-plan/agents/openai.yaml +1 -1
  82. package/packages/omo-codex/plugin/components/ultrawork/skills/ulw-plan/references/full-workflow.md +1 -1
  83. package/packages/omo-codex/plugin/components/ultrawork/skills/ulw-plan/references/intent-clear.md +2 -2
  84. package/packages/omo-codex/plugin/components/ultrawork/test/package-smoke.test.ts +76 -49
  85. package/packages/omo-codex/plugin/components/ulw-loop/hooks/hooks.json +2 -2
  86. package/packages/omo-codex/plugin/components/ulw-loop/package.json +1 -1
  87. package/packages/omo-codex/plugin/components/ulw-loop/skills/ulw-loop/agents/openai.yaml +1 -1
  88. package/packages/omo-codex/plugin/components/ulw-loop/test/package-smoke.test.ts +1 -1
  89. package/packages/omo-codex/plugin/hooks/post-compact-resetting-git-bash-mcp-reminder.json +17 -0
  90. package/packages/omo-codex/plugin/hooks/post-compact-resetting-lsp-diagnostics-cache.json +17 -0
  91. package/packages/omo-codex/plugin/hooks/post-compact-resetting-project-rule-cache.json +17 -0
  92. package/packages/omo-codex/plugin/hooks/post-tool-use-checking-comments.json +17 -0
  93. package/packages/omo-codex/plugin/hooks/post-tool-use-checking-lsp-diagnostics.json +17 -0
  94. package/packages/omo-codex/plugin/hooks/post-tool-use-matching-project-rules.json +17 -0
  95. package/packages/omo-codex/plugin/hooks/pre-tool-use-enforcing-unlimited-goal-budget.json +17 -0
  96. package/packages/omo-codex/plugin/hooks/pre-tool-use-recommending-git-bash-mcp.json +17 -0
  97. package/packages/omo-codex/plugin/hooks/session-start-checking-auto-update.json +17 -0
  98. package/packages/omo-codex/plugin/hooks/session-start-checking-bootstrap-provisioning.json +17 -0
  99. package/packages/omo-codex/plugin/hooks/session-start-checking-codegraph-bootstrap.json +16 -0
  100. package/packages/omo-codex/plugin/hooks/session-start-loading-project-rules.json +16 -0
  101. package/packages/omo-codex/plugin/hooks/session-start-recording-session-telemetry.json +16 -0
  102. package/packages/omo-codex/plugin/hooks/stop-checking-start-work-continuation.json +16 -0
  103. package/packages/omo-codex/plugin/hooks/subagent-stop-checking-start-work-continuation.json +16 -0
  104. package/packages/omo-codex/plugin/hooks/subagent-stop-verifying-lazycodex-executor-evidence.json +17 -0
  105. package/packages/omo-codex/plugin/hooks/user-prompt-submit-checking-ultrawork-trigger.json +16 -0
  106. package/packages/omo-codex/plugin/hooks/user-prompt-submit-checking-ulw-loop-steering.json +16 -0
  107. package/packages/omo-codex/plugin/hooks/user-prompt-submit-loading-project-rules.json +16 -0
  108. package/packages/omo-codex/plugin/package-lock.json +12 -22
  109. package/packages/omo-codex/plugin/package.json +1 -1
  110. package/packages/omo-codex/plugin/scripts/auto-update.mjs +1 -1
  111. package/packages/omo-codex/plugin/scripts/hook-status-message.mjs +14 -11
  112. package/packages/omo-codex/plugin/scripts/sync-hook-status-messages.mjs +15 -4
  113. package/packages/omo-codex/plugin/scripts/sync-skills.mjs +37 -0
  114. package/packages/omo-codex/plugin/skills/ast-grep/agents/openai.yaml +2 -0
  115. package/packages/omo-codex/plugin/skills/comment-checker/agents/openai.yaml +2 -0
  116. package/packages/omo-codex/plugin/skills/debugging/agents/openai.yaml +2 -0
  117. package/packages/omo-codex/plugin/skills/frontend/agents/openai.yaml +2 -0
  118. package/packages/omo-codex/plugin/skills/git-master/agents/openai.yaml +1 -1
  119. package/packages/omo-codex/plugin/skills/init-deep/SKILL.md +21 -26
  120. package/packages/omo-codex/plugin/skills/init-deep/agents/openai.yaml +2 -0
  121. package/packages/omo-codex/plugin/skills/lcx-contribute-bug-fix/agents/openai.yaml +1 -1
  122. package/packages/omo-codex/plugin/skills/lcx-doctor/agents/openai.yaml +1 -1
  123. package/packages/omo-codex/plugin/skills/lcx-report-bug/agents/openai.yaml +1 -1
  124. package/packages/omo-codex/plugin/skills/lsp/agents/openai.yaml +2 -0
  125. package/packages/omo-codex/plugin/skills/lsp-setup/agents/openai.yaml +2 -0
  126. package/packages/omo-codex/plugin/skills/programming/SKILL.md +25 -121
  127. package/packages/omo-codex/plugin/skills/programming/agents/openai.yaml +2 -0
  128. package/packages/omo-codex/plugin/skills/programming/references/code-smells.md +390 -0
  129. package/packages/omo-codex/plugin/skills/refactor/agents/openai.yaml +2 -0
  130. package/packages/omo-codex/plugin/skills/remove-ai-slops/agents/openai.yaml +2 -0
  131. package/packages/omo-codex/plugin/skills/review-work/agents/openai.yaml +2 -0
  132. package/packages/omo-codex/plugin/skills/rules/agents/openai.yaml +2 -0
  133. package/packages/omo-codex/plugin/skills/start-work/agents/openai.yaml +2 -0
  134. package/packages/omo-codex/plugin/skills/teammode/SKILL.md +139 -0
  135. package/packages/omo-codex/plugin/skills/teammode/agents/openai.yaml +2 -0
  136. package/packages/omo-codex/plugin/skills/teammode/scripts/team-guide.mjs +111 -0
  137. package/packages/omo-codex/plugin/skills/teammode/scripts/team-state.mjs +250 -0
  138. package/packages/omo-codex/plugin/skills/teammode/scripts/team.mjs +220 -0
  139. package/packages/omo-codex/plugin/skills/ultraresearch/agents/openai.yaml +2 -0
  140. package/packages/omo-codex/plugin/skills/ulw-loop/agents/openai.yaml +1 -1
  141. package/packages/omo-codex/plugin/skills/ulw-plan/SKILL.md +2 -1
  142. package/packages/omo-codex/plugin/skills/ulw-plan/agents/openai.yaml +1 -1
  143. package/packages/omo-codex/plugin/skills/ulw-plan/references/full-workflow.md +1 -1
  144. package/packages/omo-codex/plugin/skills/ulw-plan/references/intent-clear.md +2 -2
  145. package/packages/omo-codex/plugin/skills/visual-qa/agents/openai.yaml +2 -0
  146. package/packages/omo-codex/plugin/test/aggregate-hooks.test.mjs +34 -33
  147. package/packages/omo-codex/plugin/test/aggregate-manifest.test.mjs +5 -2
  148. package/packages/omo-codex/plugin/test/aggregate-plugin-fixture.mjs +13 -0
  149. package/packages/omo-codex/plugin/test/auto-update.test.mjs +1 -1
  150. package/packages/omo-codex/plugin/test/bootstrap-binlinks.test.mjs +1 -1
  151. package/packages/omo-codex/plugin/test/bootstrap-hooks.test.mjs +14 -6
  152. package/packages/omo-codex/plugin/test/bootstrap-ps-guard.test.mjs +7 -3
  153. package/packages/omo-codex/plugin/test/bootstrap-setup.test.mjs +31 -0
  154. package/packages/omo-codex/plugin/test/component-bundled-cli.test.mjs +3 -3
  155. package/packages/omo-codex/plugin/test/display-metadata.test.mjs +83 -0
  156. package/packages/omo-codex/plugin/test/hook-status-message.test.mjs +30 -19
  157. package/packages/omo-codex/plugin/test/lcx-bug-skills.test.mjs +3 -3
  158. package/packages/omo-codex/plugin/test/sync-hook-status-messages.test.mjs +6 -6
  159. package/packages/omo-codex/plugin/test/sync-skills-test-support.mjs +2 -0
  160. package/packages/omo-codex/plugin/test/sync-skills.test.mjs +9 -4
  161. package/packages/omo-codex/plugin/test/teammode-safety-fixture.mjs +88 -0
  162. package/packages/omo-codex/plugin/test/teammode-safety.test.mjs +240 -0
  163. package/packages/omo-codex/scripts/install-dist/install-local.mjs +412 -216
  164. package/packages/omo-codex/scripts/install-git-bash-mcp-env.test.mjs +24 -0
  165. package/packages/omo-codex/scripts/install-hook-targets.test.mjs +93 -0
  166. package/packages/omo-codex/scripts/install-lazycodex-version-stamp.test.mjs +2 -2
  167. package/packages/omo-codex/scripts/install-local-bun-global-update.test.mjs +63 -0
  168. package/packages/omo-codex/scripts/install-local-entrypoint.test.mjs +109 -1
  169. package/packages/omo-codex/scripts/install-local.mjs +1 -0
  170. package/packages/omo-codex/scripts/install-mcp-runtime.test.mjs +36 -0
  171. package/packages/shared-skills/skills/init-deep/SKILL.md +21 -26
  172. package/packages/shared-skills/skills/programming/SKILL.md +25 -121
  173. package/packages/shared-skills/skills/programming/references/code-smells.md +390 -0
  174. package/packages/shared-skills/skills/ulw-plan/SKILL.md +2 -1
  175. package/packages/shared-skills/skills/ulw-plan/references/full-workflow.md +1 -1
  176. package/packages/shared-skills/skills/ulw-plan/references/intent-clear.md +2 -2
  177. package/packages/omo-codex/plugin/hooks/hooks.json +0 -214
@@ -2144,30 +2144,44 @@ async function exists3(path) {
2144
2144
  import { readFile as readFile10, writeFile as writeFile6 } from "node:fs/promises";
2145
2145
  import { join as join15 } from "node:path";
2146
2146
  var GIT_BASH_ENV_KEY = "OMO_CODEX_GIT_BASH_PATH";
2147
+ var CODEGRAPH_RELATIVE_ARGS = new Set(["components/codegraph/dist/serve.js", "./components/codegraph/dist/serve.js"]);
2147
2148
  async function stampGitBashMcpEnv(input) {
2148
- if (input.platform !== "win32")
2149
- return false;
2150
- const rawOverride = input.env?.[GIT_BASH_ENV_KEY];
2151
- const override = typeof rawOverride === "string" ? rawOverride.trim() : "";
2152
- if (override === "")
2153
- return false;
2154
2149
  const manifestPath = join15(input.pluginRoot, ".mcp.json");
2155
2150
  if (!await fileExistsStrict(manifestPath))
2156
2151
  return false;
2157
2152
  const parsed = JSON.parse(await readFile10(manifestPath, "utf8"));
2158
2153
  if (!isPlainRecord(parsed) || !isPlainRecord(parsed["mcpServers"]))
2159
2154
  return false;
2160
- const gitBashServer = parsed["mcpServers"]["git_bash"];
2161
- if (!isPlainRecord(gitBashServer))
2162
- return false;
2163
- const serverEnv = isPlainRecord(gitBashServer["env"]) ? gitBashServer["env"] : {};
2164
- if (serverEnv[GIT_BASH_ENV_KEY] === override)
2155
+ let changed = stampCodegraphMcpPath(parsed["mcpServers"], input.pluginRoot);
2156
+ if (input.platform === "win32") {
2157
+ const rawOverride = input.env?.[GIT_BASH_ENV_KEY];
2158
+ const override = typeof rawOverride === "string" ? rawOverride.trim() : "";
2159
+ const gitBashServer = parsed["mcpServers"]["git_bash"];
2160
+ if (override !== "" && isPlainRecord(gitBashServer)) {
2161
+ const serverEnv = isPlainRecord(gitBashServer["env"]) ? gitBashServer["env"] : {};
2162
+ if (serverEnv[GIT_BASH_ENV_KEY] !== override) {
2163
+ gitBashServer["env"] = { ...serverEnv, [GIT_BASH_ENV_KEY]: override };
2164
+ changed = true;
2165
+ }
2166
+ }
2167
+ }
2168
+ if (!changed)
2165
2169
  return false;
2166
- gitBashServer["env"] = { ...serverEnv, [GIT_BASH_ENV_KEY]: override };
2167
2170
  await writeFile6(manifestPath, `${JSON.stringify(parsed, null, "\t")}
2168
2171
  `);
2169
2172
  return true;
2170
2173
  }
2174
+ function stampCodegraphMcpPath(mcpServers, pluginRoot) {
2175
+ const codegraphServer = mcpServers["codegraph"];
2176
+ if (!isPlainRecord(codegraphServer) || !Array.isArray(codegraphServer["args"]))
2177
+ return false;
2178
+ const args = codegraphServer["args"];
2179
+ const entrypoint = args[0];
2180
+ if (typeof entrypoint !== "string" || !CODEGRAPH_RELATIVE_ARGS.has(entrypoint))
2181
+ return false;
2182
+ codegraphServer["args"] = [join15(pluginRoot, "components", "codegraph", "dist", "serve.js"), ...args.slice(1)];
2183
+ return true;
2184
+ }
2171
2185
 
2172
2186
  // ../src/install/codex-hook-trust.ts
2173
2187
  import { createHash as createHash3 } from "node:crypto";
@@ -2190,17 +2204,33 @@ async function trustedHookStatesForPlugin(input) {
2190
2204
  if (!await exists4(manifestPath))
2191
2205
  return [];
2192
2206
  const manifest = JSON.parse(await readFile11(manifestPath, "utf8"));
2193
- if (!isPlainRecord(manifest) || typeof manifest.hooks !== "string")
2194
- return [];
2195
- const hooksPath = join16(input.pluginRoot, manifest.hooks);
2196
- if (!await exists4(hooksPath))
2207
+ if (!isPlainRecord(manifest))
2197
2208
  return [];
2198
- const parsed = JSON.parse(await readFile11(hooksPath, "utf8"));
2199
- if (!isPlainRecord(parsed) || !isPlainRecord(parsed.hooks))
2209
+ const states = [];
2210
+ for (const hookPath of hookManifestPaths(manifest.hooks)) {
2211
+ const hooksPath = join16(input.pluginRoot, hookPath);
2212
+ if (!await exists4(hooksPath))
2213
+ continue;
2214
+ const parsed = JSON.parse(await readFile11(hooksPath, "utf8"));
2215
+ if (!isPlainRecord(parsed) || !isPlainRecord(parsed.hooks))
2216
+ continue;
2217
+ states.push(...trustedHookStatesForHooksFile({
2218
+ keySource: `${input.pluginName}@${input.marketplaceName}:${hookPath}`,
2219
+ hooks: parsed.hooks
2220
+ }));
2221
+ }
2222
+ return states;
2223
+ }
2224
+ function hookManifestPaths(value) {
2225
+ if (typeof value === "string" && value.trim() !== "")
2226
+ return [stripDotSlash(value)];
2227
+ if (!Array.isArray(value))
2200
2228
  return [];
2201
- const keySource = `${input.pluginName}@${input.marketplaceName}:${stripDotSlash(manifest.hooks)}`;
2229
+ return value.filter((item) => typeof item === "string" && item.trim() !== "").map(stripDotSlash);
2230
+ }
2231
+ function trustedHookStatesForHooksFile(input) {
2202
2232
  const states = [];
2203
- for (const [eventName, groups] of Object.entries(parsed.hooks)) {
2233
+ for (const [eventName, groups] of Object.entries(input.hooks)) {
2204
2234
  if (!Array.isArray(groups))
2205
2235
  continue;
2206
2236
  const eventLabel = EVENT_LABELS.get(eventName);
@@ -2216,7 +2246,7 @@ async function trustedHookStatesForPlugin(input) {
2216
2246
  continue;
2217
2247
  if (typeof handler.command !== "string" || handler.command.trim() === "")
2218
2248
  continue;
2219
- const key = `${keySource}:${eventLabel}:${groupIndex}:${handlerIndex}`;
2249
+ const key = `${input.keySource}:${eventLabel}:${groupIndex}:${handlerIndex}`;
2220
2250
  states.push({ key, trustedHash: commandHookHash(eventLabel, group.matcher, handler) });
2221
2251
  }
2222
2252
  }
@@ -8,7 +8,7 @@
8
8
  "command": "node \"${PLUGIN_ROOT}/components/bootstrap/dist/cli.js\" hook session-start",
9
9
  "commandWindows": "powershell -NoProfile -ExecutionPolicy Bypass -File \"${PLUGIN_ROOT}\\components\\bootstrap\\scripts\\bootstrap.ps1\"",
10
10
  "timeout": 30,
11
- "statusMessage": "LazyCodex(4.11.0): Checking Bootstrap Provisioning"
11
+ "statusMessage": "(OmO) Checking Bootstrap Provisioning"
12
12
  }
13
13
  ]
14
14
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sisyphuslabs/codex-bootstrap",
3
- "version": "4.11.0",
3
+ "version": "4.12.0",
4
4
  "description": "Codex SessionStart bootstrap component that provisions LazyCodex runtime dependencies from a detached worker.",
5
5
  "type": "module",
6
6
  "private": true,
@@ -1595,6 +1595,9 @@ function bunWhich(commandName) {
1595
1595
  }
1596
1596
 
1597
1597
  // ../../utils/src/codegraph/resolve.ts
1598
+ function codegraphCommandRequiresSupportedLocalNode(resolution) {
1599
+ return resolution.source !== "bundled" && resolution.source !== "env" && resolution.source !== "provisioned";
1600
+ }
1598
1601
  var CODEGRAPH_PACKAGE = "@colbymchenry/codegraph";
1599
1602
  var CODEGRAPH_ENV_BIN = "OMO_CODEGRAPH_BIN";
1600
1603
  var CODEGRAPH_LEGACY_ENV_BIN = "CODEGRAPH_BIN";
@@ -1864,7 +1867,7 @@ function finish(action, detail, logOutcome) {
1864
1867
  async function resolveOrProvisionCommand(deps, config, env, homeDir, nodeSupport) {
1865
1868
  const resolved = deps.resolveCommand({ env, homeDir, provisioned: () => provisionedBinFromInstallDir(config.install_dir) });
1866
1869
  if (resolved.exists) {
1867
- if (resolved.source !== "bundled" && resolved.source !== "env" && !nodeSupport.supported) {
1870
+ if (codegraphCommandRequiresSupportedLocalNode(resolved) && !nodeSupport.supported) {
1868
1871
  return { kind: "unsupported-node" };
1869
1872
  }
1870
1873
  return { kind: "resolved", resolution: resolved };
@@ -1990,7 +1993,6 @@ async function executeCodegraphSessionStartHook(options = {}) {
1990
1993
  const homeDir = resolveHomeDir2(env);
1991
1994
  const config = options.config ?? getCodexOmoConfig({ cwd: projectRoot, env, homeDir });
1992
1995
  if (config.codegraph?.enabled === false) {
1993
- writeHookJson(options.stdout ?? processStdout, "skipped-disabled");
1994
1996
  return { action: "skipped-disabled", exitCode: 0 };
1995
1997
  }
1996
1998
  (options.spawnWorker ?? spawnDetachedWorker)({
@@ -1998,11 +2000,16 @@ async function executeCodegraphSessionStartHook(options = {}) {
1998
2000
  command: process.execPath,
1999
2001
  env: { ...env, [SESSION_START_CWD_ENV]: projectRoot }
2000
2002
  });
2001
- writeHookJson(options.stdout ?? processStdout, "spawned");
2003
+ writeHookJson(options.stdout ?? processStdout);
2002
2004
  return { action: "spawned", exitCode: 0 };
2003
2005
  }
2004
- function writeHookJson(stdout, action) {
2005
- const output = action === "spawned" ? { hookSpecificOutput: { hookEventName: "SessionStart", additionalContext: CODEGRAPH_SESSION_START_NOTICE }, codegraph: { action } } : { hookSpecificOutput: { hookEventName: "SessionStart" }, codegraph: { action } };
2006
+ function writeHookJson(stdout) {
2007
+ const output = {
2008
+ hookSpecificOutput: {
2009
+ hookEventName: "SessionStart",
2010
+ additionalContext: CODEGRAPH_SESSION_START_NOTICE
2011
+ }
2012
+ };
2006
2013
  stdout.write(`${JSON.stringify(output)}
2007
2014
  `);
2008
2015
  }
@@ -2078,12 +2085,16 @@ async function runCodegraphServe(options = {}) {
2078
2085
  provisioned: () => provisionedBinFromInstallDir2(codegraphConfig.install_dir)
2079
2086
  };
2080
2087
  const resolution = options.resolve?.(resolutionOptions) ?? resolveCodegraphCommand(resolutionOptions);
2088
+ const nodeSupport = evaluateCodegraphNodeSupport({ env, nodeVersion: options.nodeVersion });
2081
2089
  if (!resolution.exists || shouldSkipResolvedCommand(resolution, options.commandExists ?? existsSync7)) {
2090
+ if (resolution.source === "path" && !nodeSupport.supported) {
2091
+ (options.stderr ?? processStderr2).write(buildCodegraphNodeSkipHint(nodeSupport));
2092
+ return 1;
2093
+ }
2082
2094
  (options.stderr ?? processStderr2).write(CODEGRAPH_SKIP_HINT);
2083
2095
  return 1;
2084
2096
  }
2085
- const nodeSupport = evaluateCodegraphNodeSupport({ env, nodeVersion: options.nodeVersion });
2086
- if (resolution.source !== "bundled" && resolution.source !== "env" && !nodeSupport.supported) {
2097
+ if (codegraphCommandRequiresSupportedLocalNode(resolution) && !nodeSupport.supported) {
2087
2098
  (options.stderr ?? processStderr2).write(buildCodegraphNodeSkipHint(nodeSupport));
2088
2099
  return 1;
2089
2100
  }
@@ -132,6 +132,9 @@ function bunWhich(commandName) {
132
132
  }
133
133
 
134
134
  // ../../../../utils/src/codegraph/resolve.ts
135
+ function codegraphCommandRequiresSupportedLocalNode(resolution) {
136
+ return resolution.source !== "bundled" && resolution.source !== "env" && resolution.source !== "provisioned";
137
+ }
135
138
  var CODEGRAPH_PACKAGE = "@colbymchenry/codegraph";
136
139
  var CODEGRAPH_ENV_BIN = "OMO_CODEGRAPH_BIN";
137
140
  var CODEGRAPH_LEGACY_ENV_BIN = "CODEGRAPH_BIN";
@@ -1511,12 +1514,16 @@ async function runCodegraphServe(options = {}) {
1511
1514
  provisioned: () => provisionedBinFromInstallDir(codegraphConfig.install_dir)
1512
1515
  };
1513
1516
  const resolution = options.resolve?.(resolutionOptions) ?? resolveCodegraphCommand(resolutionOptions);
1517
+ const nodeSupport = evaluateCodegraphNodeSupport({ env, nodeVersion: options.nodeVersion });
1514
1518
  if (!resolution.exists || shouldSkipResolvedCommand(resolution, options.commandExists ?? existsSync4)) {
1519
+ if (resolution.source === "path" && !nodeSupport.supported) {
1520
+ (options.stderr ?? processStderr).write(buildCodegraphNodeSkipHint(nodeSupport));
1521
+ return 1;
1522
+ }
1515
1523
  (options.stderr ?? processStderr).write(CODEGRAPH_SKIP_HINT);
1516
1524
  return 1;
1517
1525
  }
1518
- const nodeSupport = evaluateCodegraphNodeSupport({ env, nodeVersion: options.nodeVersion });
1519
- if (resolution.source !== "bundled" && resolution.source !== "env" && !nodeSupport.supported) {
1526
+ if (codegraphCommandRequiresSupportedLocalNode(resolution) && !nodeSupport.supported) {
1520
1527
  (options.stderr ?? processStderr).write(buildCodegraphNodeSkipHint(nodeSupport));
1521
1528
  return 1;
1522
1529
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sisyphuslabs/codex-codegraph",
3
- "version": "4.11.0",
3
+ "version": "4.12.0",
4
4
  "description": "Codex plugin MCP wrapper for CodeGraph.",
5
5
  "type": "module",
6
6
  "private": true,
@@ -8,7 +8,6 @@ import { getCodexOmoConfig } from "../../../shared/src/config-loader.ts";
8
8
  import { SESSION_START_CWD_ENV } from "./session-start-worker.js";
9
9
  import type {
10
10
  HookStdout,
11
- SessionStartAction,
12
11
  SessionStartHookOptions,
13
12
  SessionStartHookResult,
14
13
  WorkerSpawnInvocation,
@@ -47,7 +46,6 @@ export async function executeCodegraphSessionStartHook(options: SessionStartHook
47
46
  const config = options.config ?? getCodexOmoConfig({ cwd: projectRoot, env, homeDir });
48
47
 
49
48
  if (config.codegraph?.enabled === false) {
50
- writeHookJson(options.stdout ?? processStdout, "skipped-disabled");
51
49
  return { action: "skipped-disabled", exitCode: 0 };
52
50
  }
53
51
 
@@ -56,14 +54,17 @@ export async function executeCodegraphSessionStartHook(options: SessionStartHook
56
54
  command: process.execPath,
57
55
  env: { ...env, [SESSION_START_CWD_ENV]: projectRoot },
58
56
  });
59
- writeHookJson(options.stdout ?? processStdout, "spawned");
57
+ writeHookJson(options.stdout ?? processStdout);
60
58
  return { action: "spawned", exitCode: 0 };
61
59
  }
62
60
 
63
- function writeHookJson(stdout: HookStdout, action: SessionStartAction): void {
64
- const output = action === "spawned"
65
- ? { hookSpecificOutput: { hookEventName: "SessionStart", additionalContext: CODEGRAPH_SESSION_START_NOTICE }, codegraph: { action } }
66
- : { hookSpecificOutput: { hookEventName: "SessionStart" }, codegraph: { action } };
61
+ function writeHookJson(stdout: HookStdout): void {
62
+ const output = {
63
+ hookSpecificOutput: {
64
+ hookEventName: "SessionStart",
65
+ additionalContext: CODEGRAPH_SESSION_START_NOTICE,
66
+ },
67
+ };
67
68
  stdout.write(`${JSON.stringify(output)}\n`);
68
69
  }
69
70
 
@@ -17,6 +17,7 @@ import {
17
17
  evaluateCodegraphNodeSupport,
18
18
  } from "../../../../../utils/src/codegraph/node-support.ts";
19
19
  import {
20
+ codegraphCommandRequiresSupportedLocalNode,
20
21
  resolveCodegraphCommand,
21
22
  type CodegraphCommandResolution,
22
23
  type ResolveCodegraphCommandOptions,
@@ -82,13 +83,17 @@ export async function runCodegraphServe(options: RunCodegraphServeOptions = {}):
82
83
  provisioned: () => provisionedBinFromInstallDir(codegraphConfig.install_dir),
83
84
  } satisfies ResolveCodegraphCommandOptions;
84
85
  const resolution = options.resolve?.(resolutionOptions) ?? resolveCodegraphCommand(resolutionOptions);
86
+ const nodeSupport = evaluateCodegraphNodeSupport({ env, nodeVersion: options.nodeVersion });
85
87
  if (!resolution.exists || shouldSkipResolvedCommand(resolution, options.commandExists ?? existsSync)) {
88
+ if (resolution.source === "path" && !nodeSupport.supported) {
89
+ (options.stderr ?? processStderr).write(buildCodegraphNodeSkipHint(nodeSupport));
90
+ return 1;
91
+ }
86
92
  (options.stderr ?? processStderr).write(CODEGRAPH_SKIP_HINT);
87
93
  return 1;
88
94
  }
89
95
 
90
- const nodeSupport = evaluateCodegraphNodeSupport({ env, nodeVersion: options.nodeVersion });
91
- if (resolution.source !== "bundled" && resolution.source !== "env" && !nodeSupport.supported) {
96
+ if (codegraphCommandRequiresSupportedLocalNode(resolution) && !nodeSupport.supported) {
92
97
  (options.stderr ?? processStderr).write(buildCodegraphNodeSkipHint(nodeSupport));
93
98
  return 1;
94
99
  }
@@ -9,6 +9,7 @@ import { buildCodegraphEnv } from "../../../../../utils/src/codegraph/env.ts";
9
9
  import { evaluateCodegraphNodeSupport, type CodegraphNodeSupport } from "../../../../../utils/src/codegraph/node-support.ts";
10
10
  import { ensureCodegraphProvisioned } from "../../../../../utils/src/codegraph/provision.ts";
11
11
  import {
12
+ codegraphCommandRequiresSupportedLocalNode,
12
13
  resolveCodegraphCommand,
13
14
  type CodegraphCommandResolution,
14
15
  } from "../../../../../utils/src/codegraph/resolve.ts";
@@ -103,7 +104,7 @@ async function resolveOrProvisionCommand(
103
104
  ): Promise<ResolutionResult> {
104
105
  const resolved = deps.resolveCommand({ env, homeDir, provisioned: () => provisionedBinFromInstallDir(config.install_dir) });
105
106
  if (resolved.exists) {
106
- if (resolved.source !== "bundled" && resolved.source !== "env" && !nodeSupport.supported) {
107
+ if (codegraphCommandRequiresSupportedLocalNode(resolved) && !nodeSupport.supported) {
107
108
  return { kind: "unsupported-node" };
108
109
  }
109
110
  return { kind: "resolved", resolution: resolved };
@@ -14,7 +14,7 @@ import {
14
14
  } from "../src/hook.ts";
15
15
 
16
16
  const pluginRoot = resolve(fileURLToPath(new URL("../../..", import.meta.url)));
17
- const hooksConfigPath = resolve(pluginRoot, "hooks/hooks.json");
17
+ const pluginConfigPath = resolve(pluginRoot, ".codex-plugin/plugin.json");
18
18
 
19
19
  describe("CodeGraph SessionStart hook", () => {
20
20
  it("#given hook session-start cli args #when invoked with empty JSON input #then it emits valid JSON and exits zero", async () => {
@@ -42,7 +42,6 @@ describe("CodeGraph SessionStart hook", () => {
42
42
  hookEventName: "SessionStart",
43
43
  additionalContext: "LazyCodex CodeGraph bootstrap scheduled in background",
44
44
  },
45
- codegraph: { action: "spawned" },
46
45
  });
47
46
  } finally {
48
47
  rmSync(homeDir, { recursive: true, force: true });
@@ -66,10 +65,7 @@ describe("CodeGraph SessionStart hook", () => {
66
65
  // then
67
66
  expect(result).toEqual({ action: "skipped-disabled", exitCode: 0 });
68
67
  expect(spawned).toEqual([]);
69
- expect(JSON.parse(stdout.join(""))).toEqual({
70
- hookSpecificOutput: { hookEventName: "SessionStart" },
71
- codegraph: { action: "skipped-disabled" },
72
- });
68
+ expect(stdout.join("")).toBe("");
73
69
  });
74
70
 
75
71
  it("#given HOME OMO config disables Codex CodeGraph #when SessionStart fires #then it skips without spawning", async () => {
@@ -97,7 +93,7 @@ describe("CodeGraph SessionStart hook", () => {
97
93
  // then
98
94
  expect(result).toEqual({ action: "skipped-disabled", exitCode: 0 });
99
95
  expect(spawned).toEqual([]);
100
- expect(JSON.parse(stdout.join("")).codegraph).toEqual({ action: "skipped-disabled" });
96
+ expect(stdout.join("")).toBe("");
101
97
  } finally {
102
98
  rmSync(homeDir, { recursive: true, force: true });
103
99
  rmSync(workspace, { recursive: true, force: true });
@@ -128,7 +124,7 @@ describe("CodeGraph SessionStart hook", () => {
128
124
  // then
129
125
  expect(result).toEqual({ action: "skipped-disabled", exitCode: 0 });
130
126
  expect(spawned).toEqual([]);
131
- expect(JSON.parse(stdout.join("")).codegraph).toEqual({ action: "skipped-disabled" });
127
+ expect(stdout.join("")).toBe("");
132
128
  } finally {
133
129
  rmSync(homeDir, { recursive: true, force: true });
134
130
  rmSync(workspace, { recursive: true, force: true });
@@ -191,7 +187,12 @@ describe("CodeGraph SessionStart hook", () => {
191
187
  },
192
188
  },
193
189
  ]);
194
- expect(JSON.parse(stdout.join("")).codegraph).toEqual({ action: "spawned" });
190
+ expect(JSON.parse(stdout.join(""))).toEqual({
191
+ hookSpecificOutput: {
192
+ additionalContext: "LazyCodex CodeGraph bootstrap scheduled in background",
193
+ hookEventName: "SessionStart",
194
+ },
195
+ });
195
196
  } finally {
196
197
  rmSync(workspace, { recursive: true, force: true });
197
198
  }
@@ -514,7 +515,7 @@ describe("CodeGraph SessionStart hook", () => {
514
515
  }
515
516
  });
516
517
 
517
- it("#given malformed hook input #when SessionStart fires #then it still emits JSON and exits zero", async () => {
518
+ it("#given malformed hook input with CodeGraph disabled #when SessionStart fires #then it stays silent and exits zero", async () => {
518
519
  // given
519
520
  const stdout: string[] = [];
520
521
  const spawned: WorkerSpawnInvocation[] = [];
@@ -531,23 +532,23 @@ describe("CodeGraph SessionStart hook", () => {
531
532
  // then
532
533
  expect(result.exitCode).toBe(0);
533
534
  expect(spawned).toEqual([]);
534
- expect(JSON.parse(stdout.join("")).codegraph).toEqual({ action: "skipped-disabled" });
535
+ expect(stdout.join("")).toBe("");
535
536
  });
536
537
 
537
538
  it("#given plugin hook config #when inspected #then CodeGraph is registered after bootstrap SessionStart", () => {
538
539
  // given
539
- const hooksConfig = JSON.parse(readFileSync(hooksConfigPath, "utf8"));
540
+ const pluginConfig: unknown = JSON.parse(readFileSync(pluginConfigPath, "utf8"));
540
541
 
541
542
  // when
542
- const sessionStartHooks = hooksConfig.hooks.SessionStart;
543
- const commands = sessionStartHooks.map((entry: { readonly hooks: readonly [{ readonly command: string }] }) => {
544
- return entry.hooks[0].command;
545
- });
543
+ const hookPaths =
544
+ typeof pluginConfig === "object" && pluginConfig !== null && "hooks" in pluginConfig && Array.isArray(pluginConfig.hooks)
545
+ ? pluginConfig.hooks.filter((hookPath): hookPath is string => typeof hookPath === "string")
546
+ : [];
546
547
 
547
548
  // then
548
- expect(commands).toContain('node "${PLUGIN_ROOT}/components/codegraph/dist/cli.js" hook session-start');
549
- expect(commands.indexOf('node "${PLUGIN_ROOT}/components/bootstrap/dist/cli.js" hook session-start')).toBeLessThan(
550
- commands.indexOf('node "${PLUGIN_ROOT}/components/codegraph/dist/cli.js" hook session-start'),
549
+ expect(hookPaths).toContain("./hooks/session-start-checking-codegraph-bootstrap.json");
550
+ expect(hookPaths.indexOf("./hooks/session-start-checking-bootstrap-provisioning.json")).toBeLessThan(
551
+ hookPaths.indexOf("./hooks/session-start-checking-codegraph-bootstrap.json"),
551
552
  );
552
553
  });
553
554
  });
@@ -0,0 +1,94 @@
1
+ import { describe, expect, it } from "bun:test";
2
+ import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
3
+ import { tmpdir } from "node:os";
4
+ import { join } from "node:path";
5
+
6
+ import { runCodegraphSessionStartWorker } from "../src/hook.ts";
7
+ import { runCodegraphServe } from "../src/serve.ts";
8
+
9
+ describe("CodeGraph provisioned launcher Node guard", () => {
10
+ it("#given provisioned CodeGraph binary #when serve runs under unsupported local Node #then it trusts the launcher", async () => {
11
+ // given
12
+ const commandPath = "/home/test/.omo/codegraph/bin/codegraph";
13
+ const spawned: Array<{ readonly args: readonly string[]; readonly command: string }> = [];
14
+
15
+ // when
16
+ const exitCode = await runCodegraphServe({
17
+ env: {},
18
+ nodeVersion: "26.3.0",
19
+ buildEnv: () => ({}),
20
+ resolve: () => ({ argsPrefix: [], command: commandPath, exists: true, source: "provisioned" }),
21
+ runProcess: (command, args) => {
22
+ spawned.push({ args, command });
23
+ return Promise.resolve(0);
24
+ },
25
+ stderr: { write: () => undefined },
26
+ });
27
+
28
+ // then
29
+ expect(exitCode).toBe(0);
30
+ expect(spawned).toEqual([{ args: ["serve", "--mcp"], command: commandPath }]);
31
+ });
32
+
33
+ it("#given provisioned CodeGraph exists #when SessionStart worker runs under unsupported local Node #then it bootstraps through the launcher", async () => {
34
+ // given
35
+ const workspace = mkdtempSync(join(tmpdir(), "omo-codegraph-worker-node25-"));
36
+ const homeDir = mkdtempSync(join(tmpdir(), "omo-codegraph-worker-node25-home-"));
37
+ const installDir = mkdtempSync(join(tmpdir(), "omo-codegraph-worker-node25-install-"));
38
+ const binPath = join(installDir, "bin", "codegraph");
39
+ const calls: Array<{ readonly args: readonly string[]; readonly command: string }> = [];
40
+ const outcomes: unknown[] = [];
41
+
42
+ try {
43
+ mkdirSync(join(installDir, "bin"), { recursive: true });
44
+ writeFileSync(binPath, "");
45
+
46
+ // when
47
+ const result = await runCodegraphSessionStartWorker({
48
+ config: { codegraph: { enabled: true, install_dir: installDir }, sources: [], warnings: [] },
49
+ nodeVersion: "26.3.0",
50
+ cwd: workspace,
51
+ env: { HOME: homeDir },
52
+ logOutcome: (outcome) => outcomes.push(outcome),
53
+ deps: {
54
+ ensureGitignored: () => true,
55
+ ensureProvisioned: () => {
56
+ throw new Error("provisioning should not run when install_dir binary exists");
57
+ },
58
+ prepareWorkspace: () => ({
59
+ dataDir: join(homeDir, ".omo/codegraph/projects/test"),
60
+ dataRoot: join(homeDir, ".omo/codegraph"),
61
+ linked: true,
62
+ mode: "global-linked",
63
+ projectLink: join(workspace, ".codegraph"),
64
+ }),
65
+ resolveCommand: (options) => {
66
+ const provisioned = options?.provisioned?.() ?? null;
67
+ return {
68
+ argsPrefix: [],
69
+ command: provisioned ?? "missing-codegraph",
70
+ exists: provisioned !== null,
71
+ source: provisioned === null ? "path" : "provisioned",
72
+ };
73
+ },
74
+ runCommand: (_projectRoot, command, args) => {
75
+ calls.push({ args, command });
76
+ return Promise.resolve({ exitCode: 0, stdout: calls.length === 1 ? '{"initialized":false}' : "", timedOut: false });
77
+ },
78
+ },
79
+ });
80
+
81
+ // then
82
+ expect(result).toEqual({ action: "initialized" });
83
+ expect(calls).toEqual([
84
+ { args: ["status", "--json"], command: binPath },
85
+ { args: ["init"], command: binPath },
86
+ ]);
87
+ expect(outcomes).toEqual([{ action: "initialized", exitCode: 0, projectRoot: workspace, source: "provisioned", timedOut: false }]);
88
+ } finally {
89
+ rmSync(workspace, { recursive: true, force: true });
90
+ rmSync(homeDir, { recursive: true, force: true });
91
+ rmSync(installDir, { recursive: true, force: true });
92
+ }
93
+ });
94
+ });
@@ -0,0 +1,33 @@
1
+ import { describe, expect, it } from "bun:test";
2
+
3
+ import { CODEGRAPH_UNSAFE_NODE_ENV } from "../../../../../utils/src/codegraph/node-support.ts";
4
+ import { runCodegraphServe } from "../src/serve.ts";
5
+
6
+ describe("runCodegraphServe node support", () => {
7
+ it("#given Node is too new and no command resolves #when serving MCP #then the unsupported-node hint wins", async () => {
8
+ // given
9
+ const stderr: string[] = [];
10
+ const spawned: string[] = [];
11
+
12
+ // when
13
+ const exitCode = await runCodegraphServe({
14
+ env: { PATH: "/bin" },
15
+ nodeVersion: "26.3.0",
16
+ buildEnv: () => ({}),
17
+ resolve: () => ({ argsPrefix: [], command: "codegraph", exists: false, source: "path" }),
18
+ runProcess: (command: string) => {
19
+ spawned.push(command);
20
+ return Promise.resolve(0);
21
+ },
22
+ stderr: { write: (chunk: string) => stderr.push(chunk) },
23
+ });
24
+
25
+ // then
26
+ expect(exitCode).toBe(1);
27
+ expect(spawned).toEqual([]);
28
+ expect(stderr).toHaveLength(1);
29
+ expect(stderr[0]).toContain("CodeGraph MCP skipped");
30
+ expect(stderr[0]).toContain("Node 26 is unsupported");
31
+ expect(stderr[0]).toContain(CODEGRAPH_UNSAFE_NODE_ENV);
32
+ });
33
+ });
@@ -8,7 +8,7 @@
8
8
  "type": "command",
9
9
  "command": "node \"${PLUGIN_ROOT}/dist/cli.js\" hook post-tool-use",
10
10
  "timeout": 30,
11
- "statusMessage": "LazyCodex(4.11.0): Checking Comments"
11
+ "statusMessage": "(OmO) Checking Comments"
12
12
  }
13
13
  ]
14
14
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@code-yeongyu/codex-comment-checker",
3
- "version": "4.11.0",
3
+ "version": "4.12.0",
4
4
  "description": "Codex plugin that runs comment-checker after edit-like PostToolUse hooks.",
5
5
  "type": "module",
6
6
  "packageManager": "npm@11.12.1",
@@ -1,44 +1,12 @@
1
- import { readFileSync } from "node:fs";
2
1
  import { describe, expect, it } from "vitest";
3
-
4
- type PackageJson = {
5
- readonly type: string;
6
- readonly packageManager: string;
7
- readonly bin: Record<string, string>;
8
- readonly dependencies?: Record<string, unknown>;
9
- readonly optionalDependencies: Record<string, string>;
10
- };
11
-
12
- type HookCommand = {
13
- readonly command: string;
14
- };
15
-
16
- type HookEntry = {
17
- readonly hooks: readonly HookCommand[];
18
- };
19
-
20
- type HooksJson = {
21
- readonly hooks: Record<string, readonly HookEntry[]>;
22
- };
23
-
24
- function readPackageJson(path: string): PackageJson {
25
- const parsed: unknown = JSON.parse(readFileSync(path, "utf8"));
26
- if (!isPackageJson(parsed)) throw new TypeError(`Invalid package metadata: ${path}`);
27
- return parsed;
28
- }
29
-
30
- function readHooksJson(path: string): HooksJson {
31
- const parsed: unknown = JSON.parse(readFileSync(path, "utf8"));
32
- if (!isHooksJson(parsed)) throw new TypeError(`Invalid hooks metadata: ${path}`);
33
- return parsed;
34
- }
2
+ import { readHooksJson, readPackageJson, readTextFile } from "../../test-support/package-smoke-fixture.js";
35
3
 
36
4
  describe("plugin package metadata", () => {
37
5
  it("#given packaged plugin files #when validating entrypoints #then hook command uses portable plugin root interpolation", () => {
38
6
  // given
39
7
  const packageJson = readPackageJson("package.json");
40
8
  const hooksJson = readHooksJson("hooks/hooks.json");
41
- const cliSource = readFileSync("src/cli.ts", "utf8");
9
+ const cliSource = readTextFile("src/cli.ts");
42
10
 
43
11
  // when
44
12
  const command = hooksJson.hooks["PostToolUse"]?.[0]?.hooks[0]?.command;
@@ -54,40 +22,3 @@ describe("plugin package metadata", () => {
54
22
  expect(command).toBe(`node "${pluginRoot}/dist/cli.js" hook post-tool-use`);
55
23
  });
56
24
  });
57
-
58
- function isPackageJson(value: unknown): value is PackageJson {
59
- if (!isRecord(value)) return false;
60
- const dependencies = value["dependencies"];
61
- return (
62
- value["type"] === "module" &&
63
- value["packageManager"] === "npm@11.12.1" &&
64
- isStringRecord(value["bin"]) &&
65
- isStringRecord(value["optionalDependencies"]) &&
66
- (dependencies === undefined || isRecord(dependencies))
67
- );
68
- }
69
-
70
- function isHooksJson(value: unknown): value is HooksJson {
71
- if (!isRecord(value) || !isRecord(value["hooks"])) return false;
72
- return Object.values(value["hooks"]).every(isHookEntries);
73
- }
74
-
75
- function isHookEntries(value: unknown): value is readonly HookEntry[] {
76
- return Array.isArray(value) && value.every(isHookEntry);
77
- }
78
-
79
- function isHookEntry(value: unknown): value is HookEntry {
80
- return isRecord(value) && Array.isArray(value["hooks"]) && value["hooks"].every(isHookCommand);
81
- }
82
-
83
- function isHookCommand(value: unknown): value is HookCommand {
84
- return isRecord(value) && typeof value["command"] === "string";
85
- }
86
-
87
- function isStringRecord(value: unknown): value is Record<string, string> {
88
- return isRecord(value) && Object.values(value).every((item) => typeof item === "string");
89
- }
90
-
91
- function isRecord(value: unknown): value is Record<string, unknown> {
92
- return typeof value === "object" && value !== null && !Array.isArray(value);
93
- }