oh-my-opencode 4.14.0 → 4.14.2

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 (140) hide show
  1. package/.agents/skills/codex-qa/references/isolation.md +1 -1
  2. package/README.md +1 -1
  3. package/dist/cli/doctor/framework/types.d.ts +2 -0
  4. package/dist/cli/index.js +223 -1447
  5. package/dist/cli-node/index.js +223 -1447
  6. package/dist/index.js +4255 -714
  7. package/dist/plugin/native-skills.d.ts +9 -0
  8. package/dist/plugin/system-transform.d.ts +1 -1
  9. package/dist/skills/frontend/ATTRIBUTION.md +7 -1
  10. package/dist/skills/frontend/SKILL.md +4 -3
  11. package/dist/skills/frontend/references/design/README.md +4 -3
  12. package/dist/skills/frontend/references/design/_INDEX.md +5 -3
  13. package/dist/skills/frontend/references/design/aside.md +209 -0
  14. package/dist/skills/lcx-contribute-bug-fix/SKILL.md +44 -13
  15. package/dist/skills/lcx-contribute-bug-fix/agents/openai.yaml +1 -1
  16. package/dist/skills/lcx-doctor/SKILL.md +48 -13
  17. package/dist/skills/lcx-doctor/agents/openai.yaml +1 -1
  18. package/dist/skills/lcx-report-bug/SKILL.md +44 -13
  19. package/dist/skills/lcx-report-bug/agents/openai.yaml +1 -1
  20. package/dist/skills/visual-qa/SKILL.md +5 -5
  21. package/dist/skills/visual-qa/references/agent-browser-setup.md +5 -4
  22. package/dist/skills/visual-qa/scripts/cli.test.ts +7 -2
  23. package/dist/skills/visual-qa/scripts/skill-prompt-contract.test.ts +10 -2
  24. package/dist/skills/visual-qa/scripts/visual-qa.mjs +530 -0
  25. package/dist/tools/delegate-task/skill-resolver.d.ts +1 -0
  26. package/dist/tools/delegate-task/types.d.ts +2 -0
  27. package/dist/tools/skill/types.d.ts +1 -0
  28. package/package.json +26 -13
  29. package/packages/git-bash-mcp/dist/cli.js +2 -0
  30. package/packages/lsp-daemon/dist/cli.js +15 -4
  31. package/packages/lsp-daemon/dist/ensure-daemon.d.ts +1 -0
  32. package/packages/lsp-daemon/dist/ensure-daemon.js +9 -3
  33. package/packages/lsp-daemon/dist/index.js +15 -4
  34. package/packages/lsp-daemon/dist/paths.d.ts +1 -0
  35. package/packages/lsp-daemon/dist/paths.js +6 -1
  36. package/packages/omo-codex/plugin/.codex-plugin/plugin.json +1 -1
  37. package/packages/omo-codex/plugin/components/bootstrap/dist/cli.js +4 -7
  38. package/packages/omo-codex/plugin/components/bootstrap/package.json +1 -1
  39. package/packages/omo-codex/plugin/components/bootstrap/src/setup.ts +1 -1
  40. package/packages/omo-codex/plugin/components/codegraph/dist/cli.js +123 -10
  41. package/packages/omo-codex/plugin/components/codegraph/dist/serve.js +123 -10
  42. package/packages/omo-codex/plugin/components/codegraph/package.json +1 -1
  43. package/packages/omo-codex/plugin/components/codegraph/src/mcp-bridge.ts +126 -9
  44. package/packages/omo-codex/plugin/components/codegraph/test/serve-mcp-bridge.test.ts +141 -0
  45. package/packages/omo-codex/plugin/components/comment-checker/package.json +1 -1
  46. package/packages/omo-codex/plugin/components/git-bash/package.json +1 -1
  47. package/packages/omo-codex/plugin/components/lazycodex-executor-verify/package.json +1 -1
  48. package/packages/omo-codex/plugin/components/lsp/dist/cli.js +90 -25
  49. package/packages/omo-codex/plugin/components/lsp/dist/codex-hook.js +2 -0
  50. package/packages/omo-codex/plugin/components/lsp/dist/daemon-cli-path.d.ts +2 -0
  51. package/packages/omo-codex/plugin/components/lsp/dist/daemon-cli-path.js +56 -0
  52. package/packages/omo-codex/plugin/components/lsp/package.json +1 -1
  53. package/packages/omo-codex/plugin/components/lsp/src/cli.ts +2 -5
  54. package/packages/omo-codex/plugin/components/lsp/src/codex-hook.ts +2 -0
  55. package/packages/omo-codex/plugin/components/lsp/src/daemon-cli-path.ts +65 -0
  56. package/packages/omo-codex/plugin/components/lsp/test/package-smoke.test.ts +6 -1
  57. package/packages/omo-codex/plugin/components/rules/dist/cli.js +8 -106
  58. package/packages/omo-codex/plugin/components/rules/package.json +1 -1
  59. package/packages/omo-codex/plugin/components/rules/src/static-injection.ts +4 -17
  60. package/packages/omo-codex/plugin/components/start-work-continuation/package.json +1 -1
  61. package/packages/omo-codex/plugin/components/teammode/package.json +1 -1
  62. package/packages/omo-codex/plugin/components/teammode/skills/teammode/SKILL.md +17 -5
  63. package/packages/omo-codex/plugin/components/teammode/skills/teammode/scripts/team-state.mjs +63 -1
  64. package/packages/omo-codex/plugin/components/teammode/skills/teammode/scripts/team.mjs +80 -50
  65. package/packages/omo-codex/plugin/components/telemetry/package.json +1 -1
  66. package/packages/omo-codex/plugin/components/ultrawork/agents/explorer.toml +1 -1
  67. package/packages/omo-codex/plugin/components/ultrawork/directive.md +4 -12
  68. package/packages/omo-codex/plugin/components/ultrawork/package.json +1 -1
  69. package/packages/omo-codex/plugin/components/ultrawork/test/package-smoke.test.ts +7 -33
  70. package/packages/omo-codex/plugin/components/ulw-loop/directive.md +4 -12
  71. package/packages/omo-codex/plugin/components/ulw-loop/package.json +4 -2
  72. package/packages/omo-codex/plugin/components/ulw-loop/skills/ulw-loop/SKILL.md +1 -1
  73. package/packages/omo-codex/plugin/components/ulw-loop/skills/ulw-loop/references/full-workflow.md +4 -4
  74. package/packages/omo-codex/plugin/components/ulw-loop/test/fixtures/sample-quality-gate.json +7 -7
  75. package/packages/omo-codex/plugin/components/ulw-loop/test/package-smoke.test.ts +3 -1
  76. package/packages/omo-codex/plugin/components/ulw-loop/test/quality-gate-doc.test.ts +2 -2
  77. package/packages/omo-codex/plugin/components/ulw-loop/test/quality-gate.test.ts +7 -6
  78. package/packages/omo-codex/plugin/package-lock.json +16 -56
  79. package/packages/omo-codex/plugin/package.json +1 -1
  80. package/packages/omo-codex/plugin/skills/frontend/ATTRIBUTION.md +7 -1
  81. package/packages/omo-codex/plugin/skills/frontend/SKILL.md +4 -3
  82. package/packages/omo-codex/plugin/skills/frontend/references/design/README.md +4 -3
  83. package/packages/omo-codex/plugin/skills/frontend/references/design/_INDEX.md +5 -3
  84. package/packages/omo-codex/plugin/skills/frontend/references/design/aside.md +209 -0
  85. package/packages/omo-codex/plugin/skills/lcx-contribute-bug-fix/SKILL.md +44 -13
  86. package/packages/omo-codex/plugin/skills/lcx-contribute-bug-fix/agents/openai.yaml +1 -1
  87. package/packages/omo-codex/plugin/skills/lcx-doctor/SKILL.md +48 -13
  88. package/packages/omo-codex/plugin/skills/lcx-doctor/agents/openai.yaml +1 -1
  89. package/packages/omo-codex/plugin/skills/lcx-report-bug/SKILL.md +44 -13
  90. package/packages/omo-codex/plugin/skills/lcx-report-bug/agents/openai.yaml +1 -1
  91. package/packages/omo-codex/plugin/skills/teammode/SKILL.md +17 -5
  92. package/packages/omo-codex/plugin/skills/teammode/scripts/team-state.mjs +63 -1
  93. package/packages/omo-codex/plugin/skills/teammode/scripts/team.mjs +80 -50
  94. package/packages/omo-codex/plugin/skills/ulw-loop/SKILL.md +1 -1
  95. package/packages/omo-codex/plugin/skills/ulw-loop/references/full-workflow.md +4 -4
  96. package/packages/omo-codex/plugin/skills/visual-qa/SKILL.md +5 -5
  97. package/packages/omo-codex/plugin/skills/visual-qa/references/agent-browser-setup.md +5 -4
  98. package/packages/omo-codex/plugin/skills/visual-qa/scripts/visual-qa.mjs +530 -0
  99. package/packages/omo-codex/plugin/test/aggregate-skills.test.mjs +23 -0
  100. package/packages/omo-codex/plugin/test/bootstrap-binlinks.test.mjs +38 -2
  101. package/packages/omo-codex/plugin/test/component-bin-names.test.mjs +12 -11
  102. package/packages/omo-codex/plugin/test/component-bundled-cli.test.mjs +99 -1
  103. package/packages/omo-codex/plugin/test/lcx-bug-skills.test.mjs +62 -8
  104. package/packages/omo-codex/plugin/test/node-install-surface.test.mjs +1 -0
  105. package/packages/omo-codex/plugin/test/teammode-archive-ambiguity.test.mjs +102 -0
  106. package/packages/omo-codex/plugin/test/teammode-safety-fixture.mjs +41 -1
  107. package/packages/omo-codex/plugin/test/teammode-safety.test.mjs +93 -0
  108. package/packages/omo-codex/scripts/install-bin-links.test.mjs +6 -0
  109. package/packages/omo-codex/scripts/install-cache-copy.test.mjs +28 -0
  110. package/packages/omo-codex/scripts/install-config-autonomous-features.test.mjs +43 -3
  111. package/packages/omo-codex/scripts/install-delegated-command.test.mjs +93 -0
  112. package/packages/omo-codex/scripts/install-dist/install-local.mjs +169 -41
  113. package/packages/omo-codex/scripts/install-local-entrypoint.test.mjs +21 -2
  114. package/packages/omo-codex/scripts/install-local.test.mjs +0 -1
  115. package/packages/shared-skills/skills/frontend/ATTRIBUTION.md +7 -1
  116. package/packages/shared-skills/skills/frontend/SKILL.md +4 -3
  117. package/packages/shared-skills/skills/frontend/references/design/README.md +4 -3
  118. package/packages/shared-skills/skills/frontend/references/design/_INDEX.md +5 -3
  119. package/packages/shared-skills/skills/frontend/references/design/aside.md +209 -0
  120. package/packages/shared-skills/skills/lcx-contribute-bug-fix/SKILL.md +44 -13
  121. package/packages/shared-skills/skills/lcx-contribute-bug-fix/agents/openai.yaml +1 -1
  122. package/packages/shared-skills/skills/lcx-doctor/SKILL.md +48 -13
  123. package/packages/shared-skills/skills/lcx-doctor/agents/openai.yaml +1 -1
  124. package/packages/shared-skills/skills/lcx-report-bug/SKILL.md +44 -13
  125. package/packages/shared-skills/skills/lcx-report-bug/agents/openai.yaml +1 -1
  126. package/packages/shared-skills/skills/visual-qa/SKILL.md +5 -5
  127. package/packages/shared-skills/skills/visual-qa/references/agent-browser-setup.md +5 -4
  128. package/packages/shared-skills/skills/visual-qa/scripts/cli.test.ts +7 -2
  129. package/packages/shared-skills/skills/visual-qa/scripts/skill-prompt-contract.test.ts +10 -2
  130. package/packages/shared-skills/skills/visual-qa/scripts/visual-qa.mjs +530 -0
  131. package/dist/cli/sparkshell-appserver-websocket.d.ts +0 -4
  132. package/dist/cli/sparkshell-appserver.d.ts +0 -16
  133. package/dist/cli/sparkshell-condense.d.ts +0 -10
  134. package/dist/cli/sparkshell-parse.d.ts +0 -24
  135. package/dist/cli/sparkshell-session-context.d.ts +0 -20
  136. package/dist/cli/sparkshell-spark.d.ts +0 -23
  137. package/dist/cli/sparkshell.d.ts +0 -33
  138. package/dist/shared/sparkshell-awareness.d.ts +0 -5
  139. package/packages/omo-codex/plugin/components/rules/src/sparkshell-awareness.ts +0 -106
  140. package/packages/omo-codex/plugin/components/rules/test/sparkshell-awareness.test.ts +0 -355
@@ -20,6 +20,21 @@ class CodegraphBridgeStdioError extends Error {
20
20
  }
21
21
  }
22
22
 
23
+ interface PendingClientResponse {
24
+ readonly method: string | null;
25
+ readonly responseMode: StdioJsonRpcResponseMode;
26
+ readonly toolName: string | null;
27
+ }
28
+
29
+ const CODEGRAPH_NODE_DESCRIPTION =
30
+ "Inspect one named symbol or file. In symbol mode, includeCode=true includes leaf-symbol source when available. Container symbols such as classes, interfaces, structs, enums, modules, and namespaces return structural outlines with member lists by design. For container source, request a specific member symbol or use file mode with symbolsOnly=false plus offset/limit.";
31
+
32
+ const CODEGRAPH_NODE_INCLUDE_CODE_DESCRIPTION =
33
+ "Symbol mode: include leaf-symbol source when available. Container symbols such as classes, interfaces, structs, enums, modules, and namespaces intentionally return structural outlines with members; request a specific member symbol or use file mode with symbolsOnly=false plus offset/limit for source.";
34
+
35
+ const CODEGRAPH_CONTAINER_OUTLINE_GUIDANCE =
36
+ "Container symbols intentionally return structural outlines with members. For source, request a specific member symbol or call codegraph_node in file mode with symbolsOnly=false plus offset/limit around the symbol location.";
37
+
23
38
  export async function runBridgedCodegraphProcess(
24
39
  command: string,
25
40
  args: readonly string[],
@@ -36,7 +51,7 @@ export async function runBridgedCodegraphProcess(
36
51
  if (childInput === null) throw new CodegraphBridgeStdioError("stdin");
37
52
  if (childOutput === null) throw new CodegraphBridgeStdioError("stdout");
38
53
 
39
- const responseModes = new Map<string, StdioJsonRpcResponseMode>();
54
+ const pendingResponses = new Map<string, PendingClientResponse>();
40
55
  let defaultResponseMode: StdioJsonRpcResponseMode = "framed";
41
56
  const childExit = new Promise<number>((resolveExit, reject) => {
42
57
  child.once("error", reject);
@@ -49,10 +64,10 @@ export async function runBridgedCodegraphProcess(
49
64
  });
50
65
  });
51
66
  const bridgeDone = Promise.all([
52
- forwardClientToCodegraph(options.input, childInput, responseModes, (mode) => {
67
+ forwardClientToCodegraph(options.input, childInput, pendingResponses, (mode) => {
53
68
  defaultResponseMode = mode;
54
69
  }),
55
- forwardCodegraphToClient(childOutput, options.output, responseModes, () => defaultResponseMode),
70
+ forwardCodegraphToClient(childOutput, options.output, pendingResponses, () => defaultResponseMode),
56
71
  ]);
57
72
  const destroyChildPipes = (): void => {
58
73
  childInput.destroy();
@@ -65,7 +80,7 @@ export async function runBridgedCodegraphProcess(
65
80
  async function forwardClientToCodegraph(
66
81
  input: Readable,
67
82
  childInput: Writable,
68
- responseModes: Map<string, StdioJsonRpcResponseMode>,
83
+ pendingResponses: Map<string, PendingClientResponse>,
69
84
  setDefaultResponseMode: (mode: StdioJsonRpcResponseMode) => void,
70
85
  ): Promise<void> {
71
86
  for await (const message of readStdioJsonRpcMessages(input)) {
@@ -75,7 +90,13 @@ async function forwardClientToCodegraph(
75
90
  const responseMode = message.responseMode;
76
91
  setDefaultResponseMode(responseMode);
77
92
  const key = responseModeKey(message.payload);
78
- if (key !== null) responseModes.set(key, responseMode);
93
+ if (key !== null) {
94
+ pendingResponses.set(key, {
95
+ method: jsonRpcMethod(message.payload),
96
+ responseMode,
97
+ toolName: jsonRpcToolName(message.payload),
98
+ });
99
+ }
79
100
  await writeLine(childInput, JSON.stringify(message.payload));
80
101
  }
81
102
  childInput.end();
@@ -84,7 +105,7 @@ async function forwardClientToCodegraph(
84
105
  async function forwardCodegraphToClient(
85
106
  childOutput: Readable,
86
107
  output: Writable,
87
- responseModes: Map<string, StdioJsonRpcResponseMode>,
108
+ pendingResponses: Map<string, PendingClientResponse>,
88
109
  defaultResponseMode: () => StdioJsonRpcResponseMode,
89
110
  ): Promise<void> {
90
111
  for await (const message of readStdioJsonRpcMessages(childOutput)) {
@@ -93,9 +114,10 @@ async function forwardCodegraphToClient(
93
114
  continue;
94
115
  }
95
116
  const key = responseModeKey(message.payload);
96
- const responseMode = key === null ? defaultResponseMode() : (responseModes.get(key) ?? defaultResponseMode());
97
- if (key !== null) responseModes.delete(key);
98
- writeStdioJsonRpcResponse(output, message.payload, responseMode);
117
+ const pendingResponse = key === null ? undefined : pendingResponses.get(key);
118
+ const responseMode = pendingResponse?.responseMode ?? defaultResponseMode();
119
+ if (key !== null) pendingResponses.delete(key);
120
+ writeStdioJsonRpcResponse(output, clarifyCodegraphResponse(message.payload, pendingResponse), responseMode);
99
121
  }
100
122
  }
101
123
 
@@ -105,6 +127,101 @@ function responseModeKey(payload: unknown): string | null {
105
127
  return `${typeof id}:${String(id)}`;
106
128
  }
107
129
 
130
+ function jsonRpcMethod(payload: unknown): string | null {
131
+ if (!isPlainRecord(payload)) return null;
132
+ const method = payload["method"];
133
+ return typeof method === "string" ? method : null;
134
+ }
135
+
136
+ function jsonRpcToolName(payload: unknown): string | null {
137
+ if (jsonRpcMethod(payload) !== "tools/call" || !isPlainRecord(payload)) return null;
138
+ const params = payload["params"];
139
+ if (!isPlainRecord(params)) return null;
140
+ const name = params["name"];
141
+ return typeof name === "string" ? name : null;
142
+ }
143
+
144
+ function clarifyCodegraphResponse(payload: unknown, pendingResponse: PendingClientResponse | undefined): unknown {
145
+ if (pendingResponse?.method === "tools/list") return clarifyCodegraphToolsList(payload);
146
+ if (pendingResponse?.method === "tools/call" && pendingResponse.toolName === "codegraph_node") {
147
+ return clarifyCodegraphNodeCallResult(payload);
148
+ }
149
+ return payload;
150
+ }
151
+
152
+ function clarifyCodegraphToolsList(payload: unknown): unknown {
153
+ if (!isPlainRecord(payload)) return payload;
154
+ const result = payload["result"];
155
+ if (!isPlainRecord(result) || !Array.isArray(result["tools"])) return payload;
156
+
157
+ let changed = false;
158
+ const tools = result["tools"].map((tool) => {
159
+ if (!isPlainRecord(tool) || tool["name"] !== "codegraph_node") return tool;
160
+ if (!hasCodegraphNodeContractMetadata(tool)) return tool;
161
+ changed = true;
162
+ return clarifyCodegraphNodeTool(tool);
163
+ });
164
+ if (!changed) return payload;
165
+ return { ...payload, result: { ...result, tools } };
166
+ }
167
+
168
+ function clarifyCodegraphNodeTool(tool: Record<string, unknown>): Record<string, unknown> {
169
+ const clarified: Record<string, unknown> = {
170
+ ...tool,
171
+ description: CODEGRAPH_NODE_DESCRIPTION,
172
+ };
173
+ const inputSchema = tool["inputSchema"];
174
+ if (isPlainRecord(inputSchema)) clarified["inputSchema"] = clarifyCodegraphNodeInputSchema(inputSchema);
175
+ return clarified;
176
+ }
177
+
178
+ function hasCodegraphNodeContractMetadata(tool: Record<string, unknown>): boolean {
179
+ if (typeof tool["description"] === "string") return true;
180
+ const inputSchema = tool["inputSchema"];
181
+ if (!isPlainRecord(inputSchema)) return false;
182
+ const properties = inputSchema["properties"];
183
+ return isPlainRecord(properties) && isPlainRecord(properties["includeCode"]);
184
+ }
185
+
186
+ function clarifyCodegraphNodeInputSchema(inputSchema: Record<string, unknown>): Record<string, unknown> {
187
+ const properties = inputSchema["properties"];
188
+ if (!isPlainRecord(properties)) return inputSchema;
189
+ const includeCode = properties["includeCode"];
190
+ if (!isPlainRecord(includeCode)) return inputSchema;
191
+ return {
192
+ ...inputSchema,
193
+ properties: {
194
+ ...properties,
195
+ includeCode: {
196
+ ...includeCode,
197
+ description: CODEGRAPH_NODE_INCLUDE_CODE_DESCRIPTION,
198
+ },
199
+ },
200
+ };
201
+ }
202
+
203
+ function clarifyCodegraphNodeCallResult(payload: unknown): unknown {
204
+ if (!isPlainRecord(payload)) return payload;
205
+ const result = payload["result"];
206
+ if (!isPlainRecord(result) || !Array.isArray(result["content"])) return payload;
207
+
208
+ let changed = false;
209
+ const content = result["content"].map((item) => {
210
+ if (!isPlainRecord(item) || item["type"] !== "text" || typeof item["text"] !== "string") return item;
211
+ const text = clarifyContainerOutlineGuidance(item["text"]);
212
+ if (text === item["text"]) return item;
213
+ changed = true;
214
+ return { ...item, text };
215
+ });
216
+ if (!changed) return payload;
217
+ return { ...payload, result: { ...result, content } };
218
+ }
219
+
220
+ function clarifyContainerOutlineGuidance(text: string): string {
221
+ if (!text.includes("Structural outline only")) return text;
222
+ return text.replace(/Structural outline only[^\n]*(?:\n[^\n]*(?:Read|read)[^\n]*)?/g, CODEGRAPH_CONTAINER_OUTLINE_GUIDANCE);
223
+ }
224
+
108
225
  async function writeLine(output: Writable, line: string): Promise<void> {
109
226
  if (output.write(`${line}\n`)) return;
110
227
  await new Promise<void>((resolveDrain, reject) => {
@@ -93,6 +93,90 @@ describe("runCodegraphServe MCP protocol bridge", () => {
93
93
  rmSync(tempRoot, { recursive: true, force: true });
94
94
  }
95
95
  });
96
+
97
+ it("#given old upstream codegraph_node metadata and container outline guidance #when bridging #then it exposes the corrected LazyCodex contract", async () => {
98
+ // given
99
+ const tempRoot = mkdtempSync(join(tmpdir(), "omo-codegraph-contract-"));
100
+ const projectRoot = join(tempRoot, "project");
101
+ const fakeCodegraph = join(tempRoot, "codegraph-contract-fake.cjs");
102
+ const childLog = join(tempRoot, "child.log");
103
+ const input = new PassThrough();
104
+ const output = new PassThrough();
105
+ let stdout = "";
106
+ output.on("data", (chunk: Buffer) => {
107
+ stdout += chunk.toString("utf8");
108
+ });
109
+
110
+ try {
111
+ mkdirSync(projectRoot, { recursive: true });
112
+ writeFakeContractCodegraph(fakeCodegraph);
113
+
114
+ const run = runCodegraphServe({
115
+ cwd: tempRoot,
116
+ env: {
117
+ CODEGRAPH_ALLOW_UNSAFE_NODE: "1",
118
+ CODEGRAPH_FAKE_LOG: childLog,
119
+ OMO_CODEGRAPH_BIN: fakeCodegraph,
120
+ OMO_CODEGRAPH_PROJECT_CWD: projectRoot,
121
+ PATH: process.env["PATH"],
122
+ },
123
+ nodeVersion: "22.14.0",
124
+ buildEnv: () => ({}),
125
+ resolve: () => ({ argsPrefix: [], command: fakeCodegraph, exists: true, source: "env" }),
126
+ stderr: { write: () => undefined },
127
+ stdin: input,
128
+ stdout: output,
129
+ });
130
+
131
+ // when
132
+ input.end([
133
+ frameMcpRequest({
134
+ id: 1,
135
+ method: "tools/list",
136
+ params: {},
137
+ }),
138
+ frameMcpRequest({
139
+ id: 2,
140
+ method: "tools/call",
141
+ params: {
142
+ arguments: { includeCode: true, symbol: "JsonlMCP" },
143
+ name: "codegraph_node",
144
+ },
145
+ }),
146
+ ].join(""));
147
+ const exitCode = await run;
148
+
149
+ // then
150
+ expect(exitCode).toBe(0);
151
+ const bodies = parseMcpBodies(stdout);
152
+ const tools = arrayProperty(recordProperty(recordValue(bodies[0]), "result"), "tools");
153
+ const nodeTool = findTool(tools, "codegraph_node");
154
+ const searchTool = findTool(tools, "codegraph_search");
155
+ expect(stringProperty(nodeTool, "description")).toContain("Container symbols");
156
+ expect(stringProperty(nodeTool, "description")).toContain("file mode");
157
+ expect(stringProperty(nodeTool, "description")).not.toContain("verbatim source");
158
+ expect(stringProperty(nodeTool, "description")).not.toContain("full body");
159
+ expect(
160
+ stringProperty(
161
+ recordProperty(recordProperty(recordProperty(nodeTool, "inputSchema"), "properties"), "includeCode"),
162
+ "description",
163
+ ),
164
+ ).toBe(
165
+ "Symbol mode: include leaf-symbol source when available. Container symbols such as classes, interfaces, structs, enums, modules, and namespaces intentionally return structural outlines with members; request a specific member symbol or use file mode with symbolsOnly=false plus offset/limit for source.",
166
+ );
167
+ expect(searchTool).toEqual({ description: "search stays unchanged", name: "codegraph_search" });
168
+
169
+ const callResult = recordProperty(recordValue(bodies[1]), "result");
170
+ const text = stringProperty(recordValue(arrayProperty(callResult, "content")[0]), "text");
171
+ expect(text).toContain("Container symbols intentionally return structural outlines");
172
+ expect(text).toContain("specific member symbol");
173
+ expect(text).toContain("file mode");
174
+ expect(text).toContain("symbolsOnly=false");
175
+ expect(text).not.toContain("Read tool");
176
+ } finally {
177
+ rmSync(tempRoot, { recursive: true, force: true });
178
+ }
179
+ });
96
180
  });
97
181
 
98
182
  function writeFakeNewlineCodegraph(filePath: string): void {
@@ -118,6 +202,32 @@ function writeFakeNewlineCodegraph(filePath: string): void {
118
202
  chmodSync(filePath, 0o755);
119
203
  }
120
204
 
205
+ function writeFakeContractCodegraph(filePath: string): void {
206
+ writeFileSync(
207
+ filePath,
208
+ [
209
+ "#!/usr/bin/env node",
210
+ "const fs = require('node:fs');",
211
+ "const readline = require('node:readline');",
212
+ "fs.writeFileSync(process.env.CODEGRAPH_FAKE_LOG, `cwd=${process.cwd()}\\n`);",
213
+ "const rl = readline.createInterface({ input: process.stdin });",
214
+ "rl.on('line', (line) => {",
215
+ " const request = JSON.parse(line);",
216
+ " if (request.method === 'tools/list') {",
217
+ " process.stdout.write(JSON.stringify({ jsonrpc: '2.0', id: request.id, result: { tools: [",
218
+ " { name: 'codegraph_search', description: 'search stays unchanged' },",
219
+ " { name: 'codegraph_node', description: 'ONE SYMBOL you can name - its location, signature, verbatim source (includeCode=true) and caller/callee trail in one call', inputSchema: { type: 'object', properties: { includeCode: { type: 'boolean', description: \"Symbol mode: include the symbol's full body (default: false).\" }, symbol: { type: 'string' } } } }",
220
+ " ] } }) + '\\n');",
221
+ " }",
222
+ " if (request.method === 'tools/call') {",
223
+ " process.stdout.write(JSON.stringify({ jsonrpc: '2.0', id: request.id, result: { content: [{ type: 'text', text: 'JsonlMCP\\nMembers (7)\\nStructural outline only. For full source, use the Read tool on this file or request a member.' }] } }) + '\\n');",
224
+ " }",
225
+ "});",
226
+ ].join("\n"),
227
+ );
228
+ chmodSync(filePath, 0o755);
229
+ }
230
+
121
231
  function frameMcpRequest(request: {
122
232
  readonly id: number;
123
233
  readonly method: string;
@@ -144,3 +254,34 @@ function parseMcpBodies(transcript: string): readonly unknown[] {
144
254
  }
145
255
  return bodies;
146
256
  }
257
+
258
+ function findTool(tools: readonly unknown[], name: string): Record<string, unknown> {
259
+ for (const tool of tools) {
260
+ const record = recordValue(tool);
261
+ if (record["name"] === name) return record;
262
+ }
263
+ throw new Error(`Missing tool ${name}`);
264
+ }
265
+
266
+ function arrayProperty(record: Record<string, unknown>, key: string): readonly unknown[] {
267
+ const value = record[key];
268
+ if (!Array.isArray(value)) throw new Error(`Expected ${key} to be an array`);
269
+ return value;
270
+ }
271
+
272
+ function recordProperty(record: Record<string, unknown>, key: string): Record<string, unknown> {
273
+ return recordValue(record[key]);
274
+ }
275
+
276
+ function stringProperty(record: Record<string, unknown>, key: string): string {
277
+ const value = record[key];
278
+ if (typeof value !== "string") throw new Error(`Expected ${key} to be a string`);
279
+ return value;
280
+ }
281
+
282
+ function recordValue(value: unknown): Record<string, unknown> {
283
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
284
+ throw new Error("Expected record value");
285
+ }
286
+ return value as Record<string, unknown>;
287
+ }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@code-yeongyu/codex-comment-checker",
3
- "version": "4.14.0",
3
+ "version": "4.14.2",
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,6 +1,6 @@
1
1
  {
2
2
  "name": "@sisyphuslabs/codex-git-bash-hook",
3
- "version": "4.14.0",
3
+ "version": "4.14.2",
4
4
  "description": "Codex hook component that reminds Windows sessions to prefer the OMO git_bash MCP.",
5
5
  "type": "module",
6
6
  "private": true,
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@code-yeongyu/codex-lazycodex-executor-verify",
3
- "version": "4.14.0",
3
+ "version": "4.14.2",
4
4
  "description": "Codex SubagentStop evidence verifier for LazyCodex executor completions.",
5
5
  "type": "module",
6
6
  "packageManager": "npm@11.12.1",
@@ -2,7 +2,6 @@
2
2
 
3
3
  // components/lsp/src/cli.ts
4
4
  import { spawn as spawn3 } from "node:child_process";
5
- import { createRequire as createRequire2 } from "node:module";
6
5
  import { argv, execPath as execPath2, stderr } from "node:process";
7
6
 
8
7
  // components/lsp/src/codex-hook-cli.ts
@@ -1481,6 +1480,7 @@ function unlinkQuietly(path) {
1481
1480
  var PROBE_TIMEOUT_MS = 500;
1482
1481
  var DEFAULT_READY_TIMEOUT_MS = 5000;
1483
1482
  var DEFAULT_POLL_INTERVAL_MS = 100;
1483
+ var CODEX_LSP_DAEMON_CLI_ENV = "CODEX_LSP_DAEMON_CLI";
1484
1484
 
1485
1485
  class DaemonUnreachableError extends Error {
1486
1486
  constructor(socketPath) {
@@ -1541,7 +1541,7 @@ function spawnDaemonProcess(paths) {
1541
1541
  mkdirSync2(dirname2(paths.log), { recursive: true });
1542
1542
  const logFd = openSync2(paths.log, "a");
1543
1543
  try {
1544
- const cliPath = fileURLToPath(new URL("./cli.js", import.meta.url));
1544
+ const cliPath = resolveDaemonCliPath();
1545
1545
  const child = spawn2(execPath, [cliPath, "daemon"], {
1546
1546
  detached: true,
1547
1547
  stdio: ["ignore", logFd, logFd]
@@ -1551,6 +1551,12 @@ function spawnDaemonProcess(paths) {
1551
1551
  closeSync2(logFd);
1552
1552
  }
1553
1553
  }
1554
+ function resolveDaemonCliPath(env = process.env) {
1555
+ const override = env[CODEX_LSP_DAEMON_CLI_ENV]?.trim();
1556
+ if (override)
1557
+ return override;
1558
+ return fileURLToPath(new URL("./cli.js", import.meta.url));
1559
+ }
1554
1560
  function defaultEnsureDaemonDeps() {
1555
1561
  return {
1556
1562
  probe: (socketPath) => probeSocket(socketPath),
@@ -1561,14 +1567,14 @@ function defaultEnsureDaemonDeps() {
1561
1567
  },
1562
1568
  spawnDaemon: (paths) => spawnDaemonProcess(paths),
1563
1569
  sleep: (ms) => new Promise((resolve22) => {
1564
- const timer = setTimeout(resolve22, ms);
1565
- timer.unref?.();
1570
+ setTimeout(resolve22, ms);
1566
1571
  }),
1567
1572
  now: () => Date.now()
1568
1573
  };
1569
1574
  }
1570
1575
  var requireFromHere = createRequire(import.meta.url);
1571
1576
  var MAX_SOCKET_PATH_LENGTH = 100;
1577
+ var CODEX_LSP_DAEMON_VERSION_ENV = "CODEX_LSP_DAEMON_VERSION";
1572
1578
  function resolveDaemonVersion(requireFn = requireFromHere) {
1573
1579
  for (const candidate of ["./package.json", "../package.json"]) {
1574
1580
  try {
@@ -1590,7 +1596,7 @@ function daemonBaseDir(env = process.env) {
1590
1596
  const home = codexHome && codexHome.length > 0 ? codexHome : join2(homedir(), ".codex");
1591
1597
  return join2(home, "codex-lsp", "daemon");
1592
1598
  }
1593
- function daemonPaths(env = process.env, version = resolveDaemonVersion()) {
1599
+ function daemonPaths(env = process.env, version = resolveDaemonVersionFromEnv(env) ?? resolveDaemonVersion()) {
1594
1600
  const dir = join2(daemonBaseDir(env), `v${version}`);
1595
1601
  return {
1596
1602
  version,
@@ -1601,6 +1607,10 @@ function daemonPaths(env = process.env, version = resolveDaemonVersion()) {
1601
1607
  log: join2(dir, "daemon.log")
1602
1608
  };
1603
1609
  }
1610
+ function resolveDaemonVersionFromEnv(env = process.env) {
1611
+ const version = env[CODEX_LSP_DAEMON_VERSION_ENV]?.trim();
1612
+ return version && version.length > 0 ? version : null;
1613
+ }
1604
1614
  function resolveSocketPath(dir, version) {
1605
1615
  const digest = createHash("sha256").update(dir).digest("hex").slice(0, 16);
1606
1616
  if (process.platform === "win32") {
@@ -3322,12 +3332,68 @@ function errorText(error) {
3322
3332
  }
3323
3333
 
3324
3334
  // components/lsp/src/codex-hook.ts
3325
- import { readFileSync as readFileSync8 } from "node:fs";
3335
+ import { readFileSync as readFileSync9 } from "node:fs";
3336
+
3337
+ // components/lsp/src/daemon-cli-path.ts
3338
+ import { existsSync as existsSync9, readFileSync as readFileSync6 } from "node:fs";
3339
+ import { createRequire as createRequire2 } from "node:module";
3340
+ import { dirname as dirname6, join as join9 } from "node:path";
3341
+ import { fileURLToPath as fileURLToPath4 } from "node:url";
3342
+ var requireFromHere2 = createRequire2(import.meta.url);
3343
+ var PACKAGE_LSP_DAEMON_CLI = "@code-yeongyu/lsp-daemon/dist/cli.js";
3344
+ var CODEX_LSP_DAEMON_CLI_ENV2 = "CODEX_LSP_DAEMON_CLI";
3345
+ var CODEX_LSP_DAEMON_VERSION_ENV2 = "CODEX_LSP_DAEMON_VERSION";
3346
+ function ensureLspDaemonCliEnv(env = process.env) {
3347
+ const configuredCli = env[CODEX_LSP_DAEMON_CLI_ENV2]?.trim();
3348
+ const resolution = configuredCli ? resolveConfiguredLspDaemonCli(configuredCli) : resolveLspDaemonCli();
3349
+ if (!configuredCli)
3350
+ env[CODEX_LSP_DAEMON_CLI_ENV2] = resolution.cliPath;
3351
+ if (!env[CODEX_LSP_DAEMON_VERSION_ENV2]?.trim() && resolution.version !== null) {
3352
+ env[CODEX_LSP_DAEMON_VERSION_ENV2] = resolution.version;
3353
+ }
3354
+ }
3355
+ function resolveLspDaemonCliPath() {
3356
+ return resolveLspDaemonCli().cliPath;
3357
+ }
3358
+ function resolveLspDaemonCli() {
3359
+ const packageCli = resolvePackageLspDaemonCliPath();
3360
+ if (packageCli !== null)
3361
+ return resolveConfiguredLspDaemonCli(packageCli);
3362
+ const bundledCli = fileURLToPath4(new URL("../../lsp-daemon/dist/cli.js", import.meta.url));
3363
+ if (existsSync9(bundledCli))
3364
+ return resolveConfiguredLspDaemonCli(bundledCli);
3365
+ return resolveConfiguredLspDaemonCli(bundledCli);
3366
+ }
3367
+ function resolvePackageLspDaemonCliPath() {
3368
+ try {
3369
+ return requireFromHere2.resolve(PACKAGE_LSP_DAEMON_CLI);
3370
+ } catch {
3371
+ return null;
3372
+ }
3373
+ }
3374
+ function resolveConfiguredLspDaemonCli(cliPath) {
3375
+ return {
3376
+ cliPath,
3377
+ version: readDaemonPackageVersion(cliPath)
3378
+ };
3379
+ }
3380
+ function readDaemonPackageVersion(cliPath) {
3381
+ try {
3382
+ const parsed = JSON.parse(readFileSync6(join9(dirname6(cliPath), "package.json"), "utf8"));
3383
+ if (isRecord4(parsed) && typeof parsed["version"] === "string" && parsed["version"].length > 0) {
3384
+ return parsed["version"];
3385
+ }
3386
+ } catch {}
3387
+ return null;
3388
+ }
3389
+ function isRecord4(value) {
3390
+ return typeof value === "object" && value !== null && !Array.isArray(value);
3391
+ }
3326
3392
 
3327
3393
  // components/lsp/src/lsp-session-state.ts
3328
- import { mkdirSync as mkdirSync4, readFileSync as readFileSync6, writeFileSync as writeFileSync3 } from "node:fs";
3394
+ import { mkdirSync as mkdirSync4, readFileSync as readFileSync8, writeFileSync as writeFileSync3 } from "node:fs";
3329
3395
  import { homedir as homedir4 } from "node:os";
3330
- import { dirname as dirname6, extname as extname2, join as join9 } from "node:path";
3396
+ import { dirname as dirname8, extname as extname2, join as join11 } from "node:path";
3331
3397
  function sessionIdFrom(input) {
3332
3398
  return typeof input.session_id === "string" && input.session_id.length > 0 ? input.session_id : undefined;
3333
3399
  }
@@ -3374,23 +3440,23 @@ function isLspDaemonUnreachableDiagnostics(diagnostics) {
3374
3440
  return diagnostics.includes("LSP daemon unreachable");
3375
3441
  }
3376
3442
  function sessionStatePath(sessionId) {
3377
- const root = process.env["PLUGIN_DATA"] ?? join9(homedir4(), ".codex", "codex-lsp");
3378
- return join9(root, "sessions", `${safePathSegment(sessionId)}.json`);
3443
+ const root = process.env["PLUGIN_DATA"] ?? join11(homedir4(), ".codex", "codex-lsp");
3444
+ return join11(root, "sessions", `${safePathSegment(sessionId)}.json`);
3379
3445
  }
3380
3446
  function readSessionState(path) {
3381
3447
  try {
3382
- const parsed = JSON.parse(readFileSync6(path, "utf8"));
3448
+ const parsed = JSON.parse(readFileSync8(path, "utf8"));
3383
3449
  if (isLspSessionState(parsed))
3384
3450
  return parsed;
3385
3451
  return emptyState();
3386
3452
  } catch (error) {
3387
- if (error instanceof SyntaxError || isRecord4(error) && error["code"] === "ENOENT")
3453
+ if (error instanceof SyntaxError || isRecord7(error) && error["code"] === "ENOENT")
3388
3454
  return emptyState();
3389
3455
  throw error;
3390
3456
  }
3391
3457
  }
3392
3458
  function writeSessionState(path, state) {
3393
- mkdirSync4(dirname6(path), { recursive: true });
3459
+ mkdirSync4(dirname8(path), { recursive: true });
3394
3460
  writeFileSync3(path, `${JSON.stringify(state)}
3395
3461
  `);
3396
3462
  }
@@ -3405,12 +3471,12 @@ function safePathSegment(value) {
3405
3471
  return value.replace(/[^A-Za-z0-9._-]/g, "_").slice(0, 120) || "unknown-session";
3406
3472
  }
3407
3473
  function isLspSessionState(value) {
3408
- if (!isRecord4(value) || !Array.isArray(value["unavailableExtensions"]))
3474
+ if (!isRecord7(value) || !Array.isArray(value["unavailableExtensions"]))
3409
3475
  return false;
3410
3476
  const postCompactProbePending = value["postCompactProbePending"];
3411
3477
  return value["unavailableExtensions"].every((item) => typeof item === "string") && (postCompactProbePending === undefined || typeof postCompactProbePending === "boolean");
3412
3478
  }
3413
- function isRecord4(value) {
3479
+ function isRecord7(value) {
3414
3480
  return typeof value === "object" && value !== null && !Array.isArray(value);
3415
3481
  }
3416
3482
 
@@ -3421,7 +3487,7 @@ function extractMutatedFilePaths(input) {
3421
3487
  return [];
3422
3488
  if (isFailedToolResponse(input.tool_response))
3423
3489
  return [];
3424
- const toolInput = isRecord7(input.tool_input) ? input.tool_input : {};
3490
+ const toolInput = isRecord9(input.tool_input) ? input.tool_input : {};
3425
3491
  const paths = new Set;
3426
3492
  addStringValue(paths, toolInput["path"]);
3427
3493
  addStringValue(paths, toolInput["filePath"]);
@@ -3440,7 +3506,7 @@ function isMutationTool(value) {
3440
3506
  return MUTATION_TOOL_NAMES.has(value.toLowerCase());
3441
3507
  }
3442
3508
  function isFailedToolResponse(value) {
3443
- if (!isRecord7(value))
3509
+ if (!isRecord9(value))
3444
3510
  return false;
3445
3511
  return value["isError"] === true || value["is_error"] === true || value["error"] === true || value["status"] === "error";
3446
3512
  }
@@ -3483,7 +3549,7 @@ function addPatchFiles(paths, value) {
3483
3549
  if (!Array.isArray(value))
3484
3550
  return;
3485
3551
  for (const item of value) {
3486
- if (!isRecord7(item))
3552
+ if (!isRecord9(item))
3487
3553
  continue;
3488
3554
  addStringValue(paths, item["path"]);
3489
3555
  addStringValue(paths, item["filePath"]);
@@ -3492,7 +3558,7 @@ function addPatchFiles(paths, value) {
3492
3558
  addStringValue(paths, item["move_path"]);
3493
3559
  }
3494
3560
  }
3495
- function isRecord7(value) {
3561
+ function isRecord9(value) {
3496
3562
  return typeof value === "object" && value !== null && !Array.isArray(value);
3497
3563
  }
3498
3564
 
@@ -3514,6 +3580,7 @@ var CONTEXT_PRESSURE_MARKERS = [
3514
3580
  "long threads and multiple compactions"
3515
3581
  ];
3516
3582
  async function runLspDiagnosticsText(filePath) {
3583
+ ensureLspDaemonCliEnv();
3517
3584
  const result = await callDiagnosticsViaDaemon(filePath, { context: currentRequestContext() });
3518
3585
  return result.content.map((block) => block.text).join(`
3519
3586
  `);
@@ -3650,7 +3717,7 @@ function isContextPressureTranscript(transcriptPath) {
3650
3717
  if (typeof transcriptPath !== "string")
3651
3718
  return false;
3652
3719
  try {
3653
- return hasContextPressureMarker(readFileSync8(transcriptPath, "utf8"));
3720
+ return hasContextPressureMarker(readFileSync9(transcriptPath, "utf8"));
3654
3721
  } catch (error) {
3655
3722
  if (error instanceof Error)
3656
3723
  return false;
@@ -3675,7 +3742,7 @@ function limitHookText(text2, maxChars) {
3675
3742
  function isCleanDiagnostics(diagnostics) {
3676
3743
  return diagnostics.length === 0 || diagnostics === CLEAN_DIAGNOSTICS_TEXT || diagnostics.startsWith(UNSUPPORTED_EXTENSION_TEXT);
3677
3744
  }
3678
- function isRecord9(value) {
3745
+ function isRecord10(value) {
3679
3746
  return typeof value === "object" && value !== null && !Array.isArray(value);
3680
3747
  }
3681
3748
 
@@ -3692,7 +3759,7 @@ async function runHookCli(runHook, stdin) {
3692
3759
  if (!raw.trim())
3693
3760
  return;
3694
3761
  const parsed = JSON.parse(raw);
3695
- const input = isRecord9(parsed) ? parsed : {};
3762
+ const input = isRecord10(parsed) ? parsed : {};
3696
3763
  const output = await runHook(input);
3697
3764
  if (output)
3698
3765
  process.stdout.write(output);
@@ -3714,8 +3781,6 @@ async function readStdin(stdin) {
3714
3781
  }
3715
3782
 
3716
3783
  // components/lsp/src/cli.ts
3717
- var require2 = createRequire2(import.meta.url);
3718
- var PACKAGE_LSP_MCP_CLI = "@code-yeongyu/lsp-daemon/dist/cli.js";
3719
3784
  async function main() {
3720
3785
  const [command = "mcp", subcommand = ""] = argv.slice(2);
3721
3786
  if (command === "hook" && subcommand === "post-tool-use") {
@@ -3740,7 +3805,7 @@ main().catch((error) => {
3740
3805
  process.exitCode = 1;
3741
3806
  });
3742
3807
  async function runPackageLspMcpCli() {
3743
- const cliPath = require2.resolve(PACKAGE_LSP_MCP_CLI);
3808
+ const cliPath = resolveLspDaemonCliPath();
3744
3809
  const child = spawn3(execPath2, [cliPath, "mcp"], { stdio: "inherit" });
3745
3810
  await new Promise((resolve6, reject) => {
3746
3811
  child.once("error", reject);
@@ -1,5 +1,6 @@
1
1
  import { readFileSync } from "node:fs";
2
2
  import { callDiagnosticsViaDaemon, currentRequestContext } from "@code-yeongyu/lsp-daemon";
3
+ import { ensureLspDaemonCliEnv } from "./daemon-cli-path.js";
3
4
  import { isLspDaemonUnreachableDiagnostics, isUnavailableLspDiagnostics, markLspSessionCompacted, recordLspDiagnosticsObservations, sessionIdFrom, shouldSkipUnavailableLspDiagnostics, } from "./lsp-session-state.js";
4
5
  import { extractMutatedFilePaths } from "./mutated-file-paths.js";
5
6
  export { extractMutatedFilePaths } from "./mutated-file-paths.js";
@@ -20,6 +21,7 @@ const CONTEXT_PRESSURE_MARKERS = [
20
21
  "long threads and multiple compactions",
21
22
  ];
22
23
  export async function runLspDiagnosticsText(filePath) {
24
+ ensureLspDaemonCliEnv();
23
25
  const result = await callDiagnosticsViaDaemon(filePath, { context: currentRequestContext() });
24
26
  return result.content.map((block) => block.text).join("\n");
25
27
  }
@@ -0,0 +1,2 @@
1
+ export declare function ensureLspDaemonCliEnv(env?: NodeJS.ProcessEnv): void;
2
+ export declare function resolveLspDaemonCliPath(): string;