oh-my-opencode 4.18.0 → 4.18.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 (226) hide show
  1. package/.agents/skills/codex-qa/scripts/lsp-e2e.sh +3677 -0
  2. package/.agents/skills/opencode-qa/scripts/lsp-e2e.sh +3154 -0
  3. package/.agents/skills/work-with-pr/SKILL.md +16 -37
  4. package/.agents/skills/work-with-pr-workspace/evals/evals.json +3 -3
  5. package/.opencode/skills/work-with-pr/SKILL.md +16 -37
  6. package/.opencode/skills/work-with-pr-workspace/evals/evals.json +3 -3
  7. package/bin/AGENTS.md +33 -0
  8. package/dist/cli/index.js +500 -158
  9. package/dist/cli-node/index.js +500 -158
  10. package/dist/hooks/anthropic-context-window-limit-recovery/empty-content-recovery-sdk.d.ts +6 -0
  11. package/dist/hooks/category-skill-reminder/hook.d.ts +9 -1
  12. package/dist/hooks/comment-checker/hook.d.ts +8 -1
  13. package/dist/hooks/todo-continuation-enforcer/types.d.ts +3 -0
  14. package/dist/index.js +956 -702
  15. package/dist/plugin/messages-transform.d.ts +1 -0
  16. package/dist/plugin-handlers/prometheus-agent-config-builder.d.ts +2 -0
  17. package/dist/tui.js +43 -4
  18. package/package.json +16 -16
  19. package/packages/git-bash-mcp/dist/cli.js +81 -19
  20. package/packages/lsp-core/package.json +4 -0
  21. package/packages/lsp-core/src/index.ts +1 -0
  22. package/packages/lsp-core/src/lsp/cleanup-errors.test.ts +18 -0
  23. package/packages/lsp-core/src/lsp/cleanup-errors.ts +12 -3
  24. package/packages/lsp-core/src/lsp/client-diagnostics-freshness.integration.test.ts +261 -0
  25. package/packages/lsp-core/src/lsp/client-wrapper.test.ts +63 -0
  26. package/packages/lsp-core/src/lsp/client-wrapper.ts +35 -5
  27. package/packages/lsp-core/src/lsp/client.ts +262 -80
  28. package/packages/lsp-core/src/lsp/config-loader.ts +5 -17
  29. package/packages/lsp-core/src/lsp/connection.ts +12 -6
  30. package/packages/lsp-core/src/lsp/directory-diagnostics.test.ts +221 -0
  31. package/packages/lsp-core/src/lsp/directory-diagnostics.ts +61 -28
  32. package/packages/lsp-core/src/lsp/errors.ts +11 -0
  33. package/packages/lsp-core/src/lsp/fixtures/diagnostics-freshness-contract-probe.ts +283 -0
  34. package/packages/lsp-core/src/lsp/fixtures/workspace-edit-contract-probe.ts +196 -0
  35. package/packages/lsp-core/src/lsp/fixtures/workspace-edit-server.mjs +215 -0
  36. package/packages/lsp-core/src/lsp/formatters.ts +3 -0
  37. package/packages/lsp-core/src/lsp/json-rpc-connection-cancellation.test.ts +97 -0
  38. package/packages/lsp-core/src/lsp/json-rpc-connection.ts +73 -5
  39. package/packages/lsp-core/src/lsp/server-install-state.ts +3 -6
  40. package/packages/lsp-core/src/lsp/transport-protocol.ts +52 -0
  41. package/packages/lsp-core/src/lsp/transport.ts +96 -70
  42. package/packages/lsp-core/src/lsp/workspace-apply-edit-failure.ts +19 -0
  43. package/packages/lsp-core/src/lsp/workspace-apply-edit-lease.integration.test.ts +214 -0
  44. package/packages/lsp-core/src/lsp/workspace-apply-edit-sync.integration.test.ts +113 -0
  45. package/packages/lsp-core/src/lsp/workspace-apply-edit-test-support.ts +163 -0
  46. package/packages/lsp-core/src/lsp/workspace-apply-edit.integration.test.ts +163 -0
  47. package/packages/lsp-core/src/lsp/workspace-document-state.test.ts +67 -0
  48. package/packages/lsp-core/src/lsp/workspace-document-state.ts +368 -0
  49. package/packages/lsp-core/src/lsp/workspace-edit-adversarial.test.ts +113 -0
  50. package/packages/lsp-core/src/lsp/workspace-edit-commit.test.ts +140 -0
  51. package/packages/lsp-core/src/lsp/workspace-edit-commit.ts +220 -0
  52. package/packages/lsp-core/src/lsp/workspace-edit-contract-evidence.test.ts +56 -0
  53. package/packages/lsp-core/src/lsp/workspace-edit-contract-evidence.ts +30 -0
  54. package/packages/lsp-core/src/lsp/workspace-edit-fingerprint.ts +44 -0
  55. package/packages/lsp-core/src/lsp/workspace-edit-options.test.ts +147 -0
  56. package/packages/lsp-core/src/lsp/workspace-edit-parse-helpers.ts +59 -0
  57. package/packages/lsp-core/src/lsp/workspace-edit-parser.ts +130 -0
  58. package/packages/lsp-core/src/lsp/workspace-edit-path.ts +98 -0
  59. package/packages/lsp-core/src/lsp/workspace-edit-plan-types.ts +60 -0
  60. package/packages/lsp-core/src/lsp/workspace-edit-plan.ts +73 -0
  61. package/packages/lsp-core/src/lsp/workspace-edit-prevalidation.test.ts +174 -0
  62. package/packages/lsp-core/src/lsp/workspace-edit-resource-parser.ts +89 -0
  63. package/packages/lsp-core/src/lsp/workspace-edit-simulation.ts +183 -0
  64. package/packages/lsp-core/src/lsp/workspace-edit-snapshot.ts +53 -0
  65. package/packages/lsp-core/src/lsp/workspace-edit-text.ts +125 -0
  66. package/packages/lsp-core/src/lsp/workspace-edit-types.ts +121 -0
  67. package/packages/lsp-core/src/lsp/workspace-edit.characterization.test.ts +95 -0
  68. package/packages/lsp-core/src/lsp/workspace-edit.ts +49 -200
  69. package/packages/lsp-core/src/lsp/workspace-mutation-controller.ts +182 -0
  70. package/packages/lsp-core/src/mcp.ts +18 -7
  71. package/packages/lsp-core/src/missing-dependency-result.test.ts +105 -0
  72. package/packages/lsp-core/src/missing-dependency-result.ts +57 -0
  73. package/packages/lsp-core/src/post-edit/index.ts +1 -0
  74. package/packages/lsp-core/src/post-edit/orchestration.test.ts +157 -0
  75. package/packages/lsp-core/src/post-edit/orchestration.ts +178 -0
  76. package/packages/lsp-core/src/request-context.test.ts +171 -0
  77. package/packages/lsp-core/src/request-context.ts +222 -9
  78. package/packages/lsp-core/src/tool-surface.test.ts +4 -1
  79. package/packages/lsp-core/src/tools/diagnostics.ts +32 -13
  80. package/packages/lsp-core/src/tools/navigation.ts +12 -12
  81. package/packages/lsp-core/src/tools/rename.ts +10 -15
  82. package/packages/lsp-core/src/tools/symbols.ts +11 -11
  83. package/packages/lsp-core/src/tools/types.ts +2 -1
  84. package/packages/lsp-daemon/dist/cli.js +3330 -764
  85. package/packages/lsp-daemon/dist/client.d.ts +105 -0
  86. package/packages/lsp-daemon/dist/client.js +5995 -0
  87. package/packages/lsp-daemon/dist/daemon-client.d.ts +12 -7
  88. package/packages/lsp-daemon/dist/daemon-client.js +139 -32
  89. package/packages/lsp-daemon/dist/daemon-server.d.ts +1 -0
  90. package/packages/lsp-daemon/dist/daemon-server.js +40 -15
  91. package/packages/lsp-daemon/dist/ensure-daemon.d.ts +10 -8
  92. package/packages/lsp-daemon/dist/ensure-daemon.js +135 -51
  93. package/packages/lsp-daemon/dist/index.d.ts +2 -2
  94. package/packages/lsp-daemon/dist/index.js +3093 -786
  95. package/packages/lsp-daemon/dist/ipc-protocol.d.ts +46 -0
  96. package/packages/lsp-daemon/dist/ipc-protocol.js +187 -0
  97. package/packages/lsp-daemon/dist/lock.js +14 -4
  98. package/packages/lsp-daemon/dist/ownership.d.ts +49 -0
  99. package/packages/lsp-daemon/dist/ownership.js +168 -0
  100. package/packages/lsp-daemon/dist/paths.d.ts +33 -9
  101. package/packages/lsp-daemon/dist/paths.js +72 -33
  102. package/packages/lsp-daemon/dist/proxy.d.ts +5 -0
  103. package/packages/lsp-daemon/dist/proxy.js +123 -16
  104. package/packages/lsp-daemon/dist/request-routing.d.ts +7 -2
  105. package/packages/lsp-daemon/dist/request-routing.js +71 -22
  106. package/packages/lsp-daemon/dist/run-daemon.js +9 -2
  107. package/packages/lsp-daemon/dist/runtime-contract.d.ts +21 -0
  108. package/packages/lsp-daemon/dist/runtime-contract.js +58 -0
  109. package/packages/lsp-daemon/dist/socket-jsonrpc.js +6 -1
  110. package/packages/lsp-daemon/package.json +12 -3
  111. package/packages/lsp-tools-mcp/dist/cli.js +2189 -454
  112. package/packages/lsp-tools-mcp/dist/lsp/manager.js +1741 -148
  113. package/packages/lsp-tools-mcp/dist/mcp.js +2206 -471
  114. package/packages/lsp-tools-mcp/dist/request-context.js +176 -6
  115. package/packages/lsp-tools-mcp/dist/tools.js +2119 -447
  116. package/packages/omo-codex/plugin/.codex-plugin/plugin.json +1 -1
  117. package/packages/omo-codex/plugin/.mcp.json +2 -1
  118. package/packages/omo-codex/plugin/components/bootstrap/hooks/hooks.json +1 -1
  119. package/packages/omo-codex/plugin/components/bootstrap/package.json +1 -1
  120. package/packages/omo-codex/plugin/components/codegraph/dist/cli.js +100 -28
  121. package/packages/omo-codex/plugin/components/codegraph/dist/serve.js +100 -28
  122. package/packages/omo-codex/plugin/components/codegraph/package.json +1 -1
  123. package/packages/omo-codex/plugin/components/codegraph/src/mcp-bridge.ts +21 -9
  124. package/packages/omo-codex/plugin/components/codegraph/test/mcp-bridge-fixtures.ts +35 -0
  125. package/packages/omo-codex/plugin/components/codegraph/test/serve-mcp-bridge-lifecycle.test.ts +69 -0
  126. package/packages/omo-codex/plugin/components/codegraph/test/serve-mcp-bridge.test.ts +57 -1
  127. package/packages/omo-codex/plugin/components/comment-checker/hooks/hooks.json +1 -1
  128. package/packages/omo-codex/plugin/components/comment-checker/package.json +1 -1
  129. package/packages/omo-codex/plugin/components/git-bash/hooks/hooks.json +2 -2
  130. package/packages/omo-codex/plugin/components/git-bash/package.json +1 -1
  131. package/packages/omo-codex/plugin/components/lazycodex-executor-verify/hooks/hooks.json +1 -1
  132. package/packages/omo-codex/plugin/components/lazycodex-executor-verify/package.json +1 -1
  133. package/packages/omo-codex/plugin/components/lsp/.mcp.json +2 -1
  134. package/packages/omo-codex/plugin/components/lsp/dist/.omo-runtime-manifest.json +55 -0
  135. package/packages/omo-codex/plugin/components/lsp/dist/cli.js +3033 -936
  136. package/packages/omo-codex/plugin/components/lsp/dist/codex-hook-cli.js +0 -4
  137. package/packages/omo-codex/plugin/components/lsp/dist/codex-hook.d.ts +5 -2
  138. package/packages/omo-codex/plugin/components/lsp/dist/codex-hook.js +41 -62
  139. package/packages/omo-codex/plugin/components/lsp/dist/daemon-cli-path.d.ts +1 -1
  140. package/packages/omo-codex/plugin/components/lsp/dist/daemon-cli-path.js +24 -15
  141. package/packages/omo-codex/plugin/components/lsp/dist/lsp-session-state.d.ts +3 -7
  142. package/packages/omo-codex/plugin/components/lsp/dist/lsp-session-state.js +23 -49
  143. package/packages/omo-codex/plugin/components/lsp/hooks/hooks.json +2 -2
  144. package/packages/omo-codex/plugin/components/lsp/package.json +3 -2
  145. package/packages/omo-codex/plugin/components/lsp/scripts/build-lsp-daemon.mjs +31 -1
  146. package/packages/omo-codex/plugin/components/lsp/scripts/build-lsp-daemon.test.mjs +76 -0
  147. package/packages/omo-codex/plugin/components/lsp/scripts/build-runtime.mjs +201 -0
  148. package/packages/omo-codex/plugin/components/lsp/scripts/build-runtime.test.mjs +55 -0
  149. package/packages/omo-codex/plugin/components/lsp/src/codex-hook-cli.ts +0 -4
  150. package/packages/omo-codex/plugin/components/lsp/src/codex-hook.ts +49 -71
  151. package/packages/omo-codex/plugin/components/lsp/src/daemon-cli-path.ts +26 -15
  152. package/packages/omo-codex/plugin/components/lsp/src/lsp-session-state.ts +26 -64
  153. package/packages/omo-codex/plugin/components/lsp/test/codex-hook-unavailable.test.ts +16 -17
  154. package/packages/omo-codex/plugin/components/lsp/test/codex-hook.test.ts +30 -4
  155. package/packages/omo-codex/plugin/components/lsp/test/package-smoke.test.ts +20 -5
  156. package/packages/omo-codex/plugin/components/rules/bundled-rules/hephaestus/gpt-5.5.md +1 -1
  157. package/packages/omo-codex/plugin/components/rules/bundled-rules/hephaestus/gpt-5.6.md +5 -3
  158. package/packages/omo-codex/plugin/components/rules/hooks/hooks.json +4 -4
  159. package/packages/omo-codex/plugin/components/rules/package.json +1 -1
  160. package/packages/omo-codex/plugin/components/start-work-continuation/README.md +2 -2
  161. package/packages/omo-codex/plugin/components/start-work-continuation/directive.md +3 -3
  162. package/packages/omo-codex/plugin/components/start-work-continuation/dist/cli.js +2 -2
  163. package/packages/omo-codex/plugin/components/start-work-continuation/hooks/hooks.json +2 -2
  164. package/packages/omo-codex/plugin/components/start-work-continuation/package.json +1 -1
  165. package/packages/omo-codex/plugin/components/start-work-continuation/src/boulder-reader.ts +1 -1
  166. package/packages/omo-codex/plugin/components/start-work-continuation/src/codex-hook.ts +1 -1
  167. package/packages/omo-codex/plugin/components/start-work-continuation/test/boulder-reader.test.ts +15 -1
  168. package/packages/omo-codex/plugin/components/start-work-continuation/test/codex-hook.test.ts +20 -0
  169. package/packages/omo-codex/plugin/components/teammode/hooks/hooks.json +1 -1
  170. package/packages/omo-codex/plugin/components/teammode/package.json +1 -1
  171. package/packages/omo-codex/plugin/components/telemetry/hooks/hooks.json +1 -1
  172. package/packages/omo-codex/plugin/components/telemetry/package.json +1 -1
  173. package/packages/omo-codex/plugin/components/ultrawork/directive.md +50 -33
  174. package/packages/omo-codex/plugin/components/ultrawork/hooks/hooks.json +1 -1
  175. package/packages/omo-codex/plugin/components/ultrawork/package.json +1 -1
  176. package/packages/omo-codex/plugin/components/ultrawork/skills/ultrawork/SKILL.md +50 -33
  177. package/packages/omo-codex/plugin/components/ulw-loop/directive.md +50 -33
  178. package/packages/omo-codex/plugin/components/ulw-loop/dist/cli.js +3 -3
  179. package/packages/omo-codex/plugin/components/ulw-loop/dist/stop-resume-hook.js +5 -6
  180. package/packages/omo-codex/plugin/components/ulw-loop/hooks/hooks.json +4 -4
  181. package/packages/omo-codex/plugin/components/ulw-loop/package.json +1 -1
  182. package/packages/omo-codex/plugin/components/ulw-loop/skills/ulw-loop/SKILL.md +1 -1
  183. package/packages/omo-codex/plugin/components/ulw-loop/skills/ulw-loop/references/full-workflow.md +6 -5
  184. package/packages/omo-codex/plugin/components/ulw-loop/src/stop-resume-hook.ts +5 -6
  185. package/packages/omo-codex/plugin/components/ulw-loop/test/stop-resume-hook.test.ts +2 -2
  186. package/packages/omo-codex/plugin/hooks/post-compact-resetting-git-bash-mcp-reminder.json +1 -1
  187. package/packages/omo-codex/plugin/hooks/post-compact-resetting-lsp-diagnostics-cache.json +1 -1
  188. package/packages/omo-codex/plugin/hooks/post-compact-resetting-project-rule-cache.json +1 -1
  189. package/packages/omo-codex/plugin/hooks/post-tool-use-checking-codegraph-init-guidance.json +1 -1
  190. package/packages/omo-codex/plugin/hooks/post-tool-use-checking-comments.json +1 -1
  191. package/packages/omo-codex/plugin/hooks/post-tool-use-checking-lsp-diagnostics.json +1 -1
  192. package/packages/omo-codex/plugin/hooks/post-tool-use-checking-thread-title-hygiene.json +1 -1
  193. package/packages/omo-codex/plugin/hooks/post-tool-use-matching-project-rules.json +1 -1
  194. package/packages/omo-codex/plugin/hooks/pre-tool-use-enforcing-unlimited-goal-budget.json +1 -1
  195. package/packages/omo-codex/plugin/hooks/pre-tool-use-guarding-ulw-loop-spawns.json +1 -1
  196. package/packages/omo-codex/plugin/hooks/pre-tool-use-recommending-git-bash-mcp.json +1 -1
  197. package/packages/omo-codex/plugin/hooks/session-start-checking-auto-update.json +1 -1
  198. package/packages/omo-codex/plugin/hooks/session-start-checking-bootstrap-provisioning.json +1 -1
  199. package/packages/omo-codex/plugin/hooks/session-start-checking-codegraph-bootstrap.json +1 -1
  200. package/packages/omo-codex/plugin/hooks/session-start-loading-project-rules.json +1 -1
  201. package/packages/omo-codex/plugin/hooks/session-start-recording-session-telemetry.json +1 -1
  202. package/packages/omo-codex/plugin/hooks/stop-checking-start-work-continuation.json +1 -1
  203. package/packages/omo-codex/plugin/hooks/stop-checking-ulw-loop-resume.json +1 -1
  204. package/packages/omo-codex/plugin/hooks/subagent-stop-checking-start-work-continuation.json +1 -1
  205. package/packages/omo-codex/plugin/hooks/subagent-stop-verifying-lazycodex-executor-evidence.json +1 -1
  206. package/packages/omo-codex/plugin/hooks/user-prompt-submit-checking-ultrawork-trigger.json +1 -1
  207. package/packages/omo-codex/plugin/hooks/user-prompt-submit-checking-ulw-loop-steering.json +1 -1
  208. package/packages/omo-codex/plugin/hooks/user-prompt-submit-loading-project-rules.json +1 -1
  209. package/packages/omo-codex/plugin/package-lock.json +26 -14
  210. package/packages/omo-codex/plugin/package.json +1 -1
  211. package/packages/omo-codex/plugin/scripts/build-bundled-mcp-runtimes.mjs +2 -3
  212. package/packages/omo-codex/plugin/scripts/build-components.mjs +13 -1
  213. package/packages/omo-codex/plugin/scripts/sync-skills.mjs +9 -1
  214. package/packages/omo-codex/plugin/skills/review-work/SKILL.md +7 -0
  215. package/packages/omo-codex/plugin/skills/start-work/SKILL.md +2 -1
  216. package/packages/omo-codex/plugin/skills/ultrawork/SKILL.md +50 -33
  217. package/packages/omo-codex/plugin/skills/ulw-loop/SKILL.md +1 -1
  218. package/packages/omo-codex/plugin/skills/ulw-loop/references/full-workflow.md +6 -5
  219. package/packages/omo-codex/plugin/test/aggregate-build.test.mjs +8 -0
  220. package/packages/omo-codex/plugin/test/component-bundled-cli.test.mjs +128 -15
  221. package/packages/omo-codex/plugin/test/install-time-build-runtime.test.mjs +10 -0
  222. package/packages/omo-codex/plugin/test/lsp-prebuild-layouts.test.mjs +2 -0
  223. package/packages/omo-codex/plugin/test/mcp-research-servers.test.mjs +1 -0
  224. package/packages/omo-codex/plugin/test/sync-skills-orchestration.test.mjs +7 -0
  225. package/packages/omo-codex/plugin/test/sync-skills-test-support.mjs +34 -3
  226. package/packages/omo-codex/scripts/install-dist/install-local.mjs +328 -63
@@ -0,0 +1,182 @@
1
+ import type { WorkspaceEdit } from "./types.js";
2
+ import { workspaceApplyEditConcurrentFailureReason } from "./workspace-apply-edit-failure.js";
3
+ import { commitWorkspaceEditPlan } from "./workspace-edit-commit.js";
4
+ import { fingerprintWorkspaceEdit, planWorkspaceEdit } from "./workspace-edit-plan.js";
5
+ import type {
6
+ ApplyResult,
7
+ LspRenameResult,
8
+ WorkspaceEditCommitIo,
9
+ } from "./workspace-edit-types.js";
10
+ import { WorkspaceDocumentState } from "./workspace-document-state.js";
11
+
12
+ export interface WorkspaceApplyEditResponse {
13
+ readonly applied: boolean;
14
+ readonly failureReason?: string;
15
+ readonly failedChange?: number;
16
+ }
17
+
18
+ export interface WorkspaceMutationLease {
19
+ readonly id: number;
20
+ }
21
+
22
+ interface ServerApplyRecord {
23
+ readonly fingerprint: string | null;
24
+ readonly result: ApplyResult;
25
+ }
26
+
27
+ interface ActiveLease extends WorkspaceMutationLease {
28
+ readonly signal?: AbortSignal;
29
+ phase: "idle" | "applying" | "settled" | "sealed";
30
+ serverApply?: ServerApplyRecord;
31
+ applyCompletion?: Promise<void>;
32
+ resolveApply?: () => void;
33
+ }
34
+
35
+ export type AcquireMutationLeaseResult =
36
+ | { readonly success: true; readonly lease: WorkspaceMutationLease }
37
+ | { readonly success: false; readonly result: ApplyResult };
38
+
39
+ function failure(message: string, failedChange?: number, base?: ApplyResult): ApplyResult {
40
+ return {
41
+ success: false,
42
+ filesModified: base?.filesModified ?? [],
43
+ totalEdits: base?.totalEdits ?? 0,
44
+ errors: [message],
45
+ ...(failedChange === undefined ? {} : { failedChange }),
46
+ ...(base?.lateAbort ? { lateAbort: true } : {}),
47
+ };
48
+ }
49
+
50
+ function responseFor(result: ApplyResult): WorkspaceApplyEditResponse {
51
+ if (result.success) return { applied: true };
52
+ return {
53
+ applied: false,
54
+ failureReason: result.errors[0] ?? "workspace edit failed",
55
+ ...(result.failedChange === undefined ? {} : { failedChange: result.failedChange }),
56
+ };
57
+ }
58
+
59
+ function isRecord(value: unknown): value is Record<string, unknown> {
60
+ return typeof value === "object" && value !== null && !Array.isArray(value);
61
+ }
62
+
63
+ export class WorkspaceMutationController {
64
+ private activeLease: ActiveLease | null = null;
65
+ private nextLeaseId = 1;
66
+ private io: Partial<WorkspaceEditCommitIo> | undefined;
67
+
68
+ constructor(
69
+ private readonly workspaceRoot: string,
70
+ private readonly documents: WorkspaceDocumentState,
71
+ ) {}
72
+
73
+ setIo(io: Partial<WorkspaceEditCommitIo>): void {
74
+ this.io = io;
75
+ }
76
+
77
+ acquire(signal?: AbortSignal): AcquireMutationLeaseResult {
78
+ if (this.activeLease) return { success: false, result: failure("workspace mutation is already in progress") };
79
+ if (signal?.aborted) return { success: false, result: failure("cancelled before mutating request") };
80
+ const lease: ActiveLease = {
81
+ id: this.nextLeaseId,
82
+ phase: "idle",
83
+ ...(signal === undefined ? {} : { signal }),
84
+ };
85
+ this.nextLeaseId += 1;
86
+ this.activeLease = lease;
87
+ return { success: true, lease };
88
+ }
89
+
90
+ release(lease: WorkspaceMutationLease): void {
91
+ if (this.activeLease?.id !== lease.id) return;
92
+ this.activeLease.phase = "sealed";
93
+ this.activeLease = null;
94
+ }
95
+
96
+ isBeforeCommit(lease: WorkspaceMutationLease): boolean {
97
+ return this.activeLease?.id === lease.id && this.activeLease.phase === "idle";
98
+ }
99
+
100
+ async handleApplyEdit(params: unknown): Promise<WorkspaceApplyEditResponse> {
101
+ const lease = this.activeLease;
102
+ if (!lease) return { applied: false, failureReason: "workspace/applyEdit requires an active workspace mutation" };
103
+ if (lease.phase !== "idle") {
104
+ return {
105
+ applied: false,
106
+ failureReason: workspaceApplyEditConcurrentFailureReason(lease.phase === "applying" ? "applying" : "settled"),
107
+ };
108
+ }
109
+
110
+ lease.phase = "applying";
111
+ lease.applyCompletion = new Promise<void>((resolve) => {
112
+ lease.resolveApply = resolve;
113
+ });
114
+ const edit = isRecord(params) ? params["edit"] : undefined;
115
+ const record =
116
+ edit === undefined
117
+ ? { fingerprint: null, result: failure("workspace/applyEdit params.edit is required", 0) }
118
+ : await this.applyEdit(edit, lease);
119
+ lease.serverApply = record;
120
+ lease.phase = "settled";
121
+ lease.resolveApply?.();
122
+ return responseFor(record.result);
123
+ }
124
+
125
+ async reconcileRename(
126
+ leaseToken: WorkspaceMutationLease,
127
+ edit: WorkspaceEdit | null,
128
+ ): Promise<LspRenameResult> {
129
+ const lease = this.requireActiveLease(leaseToken);
130
+ if (!lease) return { edit, apply: failure("workspace mutation lease ended before rename reconciliation") };
131
+ if (lease.phase === "applying") await lease.applyCompletion;
132
+ if (lease.serverApply) return this.reconcileServerApply(lease.serverApply, edit);
133
+ lease.phase = "sealed";
134
+ if (!edit) return { edit, apply: failure("No edit provided") };
135
+ const applied = await this.applyEdit(edit, lease);
136
+ return { edit, apply: applied.result };
137
+ }
138
+
139
+ private reconcileServerApply(record: ServerApplyRecord, edit: WorkspaceEdit | null): LspRenameResult {
140
+ if (!edit) return { edit, apply: record.result };
141
+ const fingerprint = fingerprintWorkspaceEdit(edit, this.workspaceRoot);
142
+ if (fingerprint.success && record.fingerprint !== null && fingerprint.fingerprint === record.fingerprint) {
143
+ return { edit, apply: record.result };
144
+ }
145
+ return {
146
+ edit,
147
+ apply: failure("rename result conflicts with server-applied workspace edit", 0, record.result),
148
+ };
149
+ }
150
+
151
+ private async applyEdit(edit: unknown, lease: ActiveLease): Promise<ServerApplyRecord> {
152
+ const planned = planWorkspaceEdit(edit, this.workspaceRoot);
153
+ if (!planned.success) return { fingerprint: null, result: planned.result };
154
+ const versionFailure = this.documents.validateVersions(planned.plan.operations);
155
+ if (versionFailure) {
156
+ return {
157
+ fingerprint: planned.plan.fingerprint,
158
+ result: failure(versionFailure.message, versionFailure.changeIndex),
159
+ };
160
+ }
161
+
162
+ const commit = commitWorkspaceEditPlan(planned.plan, {
163
+ ...(lease.signal === undefined ? {} : { signal: lease.signal }),
164
+ ...(this.io === undefined ? {} : { io: this.io }),
165
+ });
166
+ let result = commit.result;
167
+ if (commit.delta.operations.length > 0) {
168
+ try {
169
+ await this.documents.synchronize(commit.delta);
170
+ } catch (error) {
171
+ const message = error instanceof Error ? error.message : String(error);
172
+ result = failure(`document synchronization failed after filesystem commit: ${message}`, undefined, result);
173
+ }
174
+ }
175
+ if (lease.signal?.aborted && !result.lateAbort) result = { ...result, lateAbort: true };
176
+ return { fingerprint: planned.plan.fingerprint, result };
177
+ }
178
+
179
+ private requireActiveLease(lease: WorkspaceMutationLease): ActiveLease | null {
180
+ return this.activeLease?.id === lease.id ? this.activeLease : null;
181
+ }
182
+ }
@@ -3,7 +3,6 @@ import {
3
3
  errorResponse,
4
4
  isPlainRecord,
5
5
  jsonRpcId,
6
- messageFromError,
7
6
  runJsonRpcStdioServer,
8
7
  successResponse,
9
8
  type JsonRpcError,
@@ -13,13 +12,21 @@ import {
13
12
  type McpToolDescriptor,
14
13
  } from "@oh-my-opencode/mcp-stdio-core";
15
14
  import { coerceToolArguments, executeLspTool, LSP_MCP_TOOLS } from "./tools.js";
15
+ import { createStandaloneMcpRequestContext, runWithRequestContext } from "./request-context.js";
16
16
 
17
17
  export type { JsonRpcError, JsonRpcId, JsonRpcResponse, JsonRpcResult, McpToolDescriptor };
18
18
 
19
19
  const SERVER_NAME = "lsp";
20
20
  const SERVER_VERSION = "0.1.0";
21
21
 
22
- export async function handleLspMcpRequest(input: unknown): Promise<JsonRpcResponse | undefined> {
22
+ export interface HandleLspMcpRequestOptions {
23
+ readonly signal?: AbortSignal;
24
+ }
25
+
26
+ export async function handleLspMcpRequest(
27
+ input: unknown,
28
+ options: HandleLspMcpRequestOptions = {},
29
+ ): Promise<JsonRpcResponse | undefined> {
23
30
  if (!isPlainRecord(input)) {
24
31
  return errorResponse(null, -32600, "Invalid Request");
25
32
  }
@@ -42,7 +49,7 @@ export async function handleLspMcpRequest(input: unknown): Promise<JsonRpcRespon
42
49
  }
43
50
 
44
51
  if (method === "tools/call") {
45
- return handleToolCall(id, input["params"]);
52
+ return handleToolCall(id, input["params"], options.signal);
46
53
  }
47
54
 
48
55
  return errorResponse(id, -32601, `Method not found: ${String(method)}`);
@@ -52,30 +59,34 @@ export async function runMcpStdioServer(
52
59
  input: Readable = process.stdin,
53
60
  output: Writable = process.stdout,
54
61
  ): Promise<void> {
62
+ const requestContext = createStandaloneMcpRequestContext();
55
63
  await runJsonRpcStdioServer({
56
64
  input,
57
65
  output,
58
66
  idleTimeoutMs: 0,
59
- handler: handleLspMcpRequest,
67
+ handler: (request) => runWithRequestContext(requestContext, () => handleLspMcpRequest(request)),
60
68
  handlerOptions: undefined,
61
69
  });
62
70
  }
63
71
 
64
- async function handleToolCall(id: JsonRpcId, params: unknown): Promise<JsonRpcResponse> {
72
+ async function handleToolCall(id: JsonRpcId, params: unknown, signal?: AbortSignal): Promise<JsonRpcResponse> {
65
73
  if (!isPlainRecord(params) || typeof params["name"] !== "string") {
66
74
  return errorResponse(id, -32602, "tools/call requires params.name");
67
75
  }
68
76
 
69
77
  try {
70
- const result = await executeLspTool(params["name"], coerceToolArguments(params["arguments"]));
78
+ const result = await executeLspTool(params["name"], coerceToolArguments(params["arguments"]), signal);
71
79
  return successResponse(id, {
72
80
  content: result.content,
73
81
  isError: result.isError ?? false,
74
82
  details: result.details,
75
83
  });
76
84
  } catch (error) {
85
+ if (!(error instanceof Error)) {
86
+ throw error;
87
+ }
77
88
  return successResponse(id, {
78
- content: [{ type: "text", text: messageFromError(error) }],
89
+ content: [{ type: "text", text: error.message }],
79
90
  isError: true,
80
91
  });
81
92
  }
@@ -0,0 +1,105 @@
1
+ import { mkdtempSync, rmSync } from "node:fs";
2
+ import { homedir } from "node:os";
3
+ import { join } from "node:path";
4
+ import { afterEach, describe, expect, it } from "bun:test";
5
+
6
+ import { LspServerLookupError } from "./lsp/errors.js";
7
+ import { missingDependencyResult } from "./missing-dependency-result.js";
8
+ import { parseLspRequestContext, runWithRequestContext } from "./request-context.js";
9
+
10
+ const tempDirectories: string[] = [];
11
+
12
+ afterEach(() => {
13
+ for (const directory of tempDirectories.splice(0)) {
14
+ rmSync(directory, { recursive: true, force: true });
15
+ }
16
+ });
17
+
18
+ function tempRoot(): string {
19
+ const root = mkdtempSync(join(homedir(), "lsp-missing-dep-"));
20
+ tempDirectories.push(root);
21
+ return root;
22
+ }
23
+
24
+ describe("missingDependencyResult", () => {
25
+ it("#given not configured lookup #when converted #then includes structured availability and typed config paths", () => {
26
+ const root = tempRoot();
27
+ const projectConfigPath = join(root, ".codex", "lsp-client.json");
28
+ const userConfigPath = join(homedir(), ".codex", "lsp-client.json");
29
+ const installDecisionsPath = join(homedir(), ".codex", "lsp-install-decisions.json");
30
+ const context = parseLspRequestContext({
31
+ cwd: root,
32
+ projectConfigPaths: [projectConfigPath],
33
+ userConfigPath,
34
+ installDecisionsPath,
35
+ capabilities: { installDecisionTool: false },
36
+ });
37
+
38
+ const result = runWithRequestContext(context, () =>
39
+ missingDependencyResult(
40
+ new LspServerLookupError("No LSP server configured for extension: .foo", {
41
+ status: "not_configured",
42
+ extension: ".foo",
43
+ availableServers: ["typescript"],
44
+ }),
45
+ { filePath: "src/file.foo" },
46
+ ),
47
+ );
48
+
49
+ expect(result?.details).toEqual({
50
+ filePath: "src/file.foo",
51
+ error: "No LSP server configured for extension: .foo",
52
+ errorKind: "missing_dependency",
53
+ availability: {
54
+ kind: "not_configured",
55
+ extension: ".foo",
56
+ availableServers: ["typescript"],
57
+ projectConfigPaths: [projectConfigPath],
58
+ userConfigPath,
59
+ installDecisionTool: false,
60
+ },
61
+ });
62
+ });
63
+
64
+ it("#given not installed lookup #when converted #then includes install decision availability", () => {
65
+ const root = tempRoot();
66
+ const installDecisionsPath = join(homedir(), ".codex", "lsp-install-decisions.json");
67
+ const context = parseLspRequestContext({
68
+ cwd: root,
69
+ projectConfigPaths: [join(root, ".codex", "lsp-client.json")],
70
+ userConfigPath: join(homedir(), ".codex", "lsp-client.json"),
71
+ installDecisionsPath,
72
+ capabilities: { installDecisionTool: true },
73
+ });
74
+
75
+ const result = runWithRequestContext(context, () =>
76
+ missingDependencyResult(
77
+ new LspServerLookupError("LSP server 'typescript' for .ts is NOT INSTALLED.", {
78
+ status: "not_installed",
79
+ server: {
80
+ id: "typescript",
81
+ command: ["typescript-language-server", "--stdio"],
82
+ extensions: [".ts"],
83
+ },
84
+ installHint: "npm install -g typescript-language-server typescript",
85
+ }),
86
+ { filePath: "src/file.ts" },
87
+ ),
88
+ );
89
+
90
+ expect(result?.details).toEqual({
91
+ filePath: "src/file.ts",
92
+ error: "LSP server 'typescript' for .ts is NOT INSTALLED.",
93
+ errorKind: "missing_dependency",
94
+ availability: {
95
+ kind: "not_installed",
96
+ serverId: "typescript",
97
+ command: ["typescript-language-server", "--stdio"],
98
+ extensions: [".ts"],
99
+ installHint: "npm install -g typescript-language-server typescript",
100
+ installDecisionTool: true,
101
+ installDecisionsPath,
102
+ },
103
+ });
104
+ });
105
+ });
@@ -1,6 +1,27 @@
1
+ import { LspServerLookupError } from "./lsp/errors.js";
1
2
  import { handleMissingDependencyError } from "./lsp/startup-failure.js";
3
+ import { lspRequestContext } from "./request-context.js";
2
4
  import type { ToolExecutionResult } from "./tools.js";
3
5
 
6
+ export type MissingDependencyAvailability =
7
+ | {
8
+ readonly kind: "not_configured";
9
+ readonly extension: string;
10
+ readonly availableServers: readonly string[];
11
+ readonly projectConfigPaths: readonly string[];
12
+ readonly userConfigPath: string;
13
+ readonly installDecisionTool: boolean;
14
+ }
15
+ | {
16
+ readonly kind: "not_installed";
17
+ readonly serverId: string;
18
+ readonly command: readonly string[];
19
+ readonly extensions: readonly string[];
20
+ readonly installHint: string;
21
+ readonly installDecisionTool: boolean;
22
+ readonly installDecisionsPath: string;
23
+ };
24
+
4
25
  export function missingDependencyResult<TDetails extends object>(
5
26
  error: unknown,
6
27
  details: TDetails,
@@ -14,6 +35,42 @@ export function missingDependencyResult<TDetails extends object>(
14
35
  ...details,
15
36
  error: message,
16
37
  errorKind: "missing_dependency",
38
+ ...availabilityDetails(error),
17
39
  },
18
40
  };
19
41
  }
42
+
43
+ function availabilityDetails(error: unknown): { readonly availability: MissingDependencyAvailability } | object {
44
+ const availability = missingDependencyAvailability(error);
45
+ return availability === null ? {} : { availability };
46
+ }
47
+
48
+ function missingDependencyAvailability(error: unknown): MissingDependencyAvailability | null {
49
+ if (!(error instanceof LspServerLookupError) || error.lookup === undefined) return null;
50
+ const context = lspRequestContext();
51
+ switch (error.lookup.status) {
52
+ case "not_configured":
53
+ return {
54
+ kind: "not_configured",
55
+ extension: error.lookup.extension,
56
+ availableServers: [...error.lookup.availableServers],
57
+ projectConfigPaths: [...context.projectConfigPaths],
58
+ userConfigPath: context.userConfigPath,
59
+ installDecisionTool: context.capabilities.installDecisionTool,
60
+ };
61
+ case "not_installed":
62
+ return {
63
+ kind: "not_installed",
64
+ serverId: error.lookup.server.id,
65
+ command: [...error.lookup.server.command],
66
+ extensions: [...error.lookup.server.extensions],
67
+ installHint: error.lookup.installHint,
68
+ installDecisionTool: context.capabilities.installDecisionTool,
69
+ installDecisionsPath: context.installDecisionsPath,
70
+ };
71
+ default: {
72
+ const exhaustive: never = error.lookup;
73
+ return exhaustive;
74
+ }
75
+ }
76
+ }
@@ -0,0 +1 @@
1
+ export * from "./orchestration.js";
@@ -0,0 +1,157 @@
1
+ import { describe, expect, it } from "bun:test";
2
+
3
+ import {
4
+ collectPostEditDiagnostics,
5
+ createPostEditNotConfiguredCache,
6
+ resetPostEditNotConfiguredCache,
7
+ } from "./orchestration.js";
8
+
9
+ describe("collectPostEditDiagnostics", () => {
10
+ it("#given rendered not_configured text #when the same extension is checked again #then the text is not cached", async () => {
11
+ const cache = createPostEditNotConfiguredCache();
12
+ let calls = 0;
13
+
14
+ const first = await collectPostEditDiagnostics({
15
+ filePaths: ["a.foo"],
16
+ cache,
17
+ runDiagnostics: async () => {
18
+ calls += 1;
19
+ return "No LSP server configured for extension: .foo\n\nordinary text from a renderer";
20
+ },
21
+ });
22
+ const second = await collectPostEditDiagnostics({
23
+ filePaths: ["b.foo"],
24
+ cache,
25
+ runDiagnostics: async () => {
26
+ calls += 1;
27
+ return "diagnostic for b.foo";
28
+ },
29
+ });
30
+
31
+ expect(calls).toBe(2);
32
+ expect(first.blocks).toEqual([
33
+ { filePath: "a.foo", diagnostics: "No LSP server configured for extension: .foo\n\nordinary text from a renderer" },
34
+ ]);
35
+ expect(second.blocks).toEqual([{ filePath: "b.foo", diagnostics: "diagnostic for b.foo" }]);
36
+ expect([...cache.notConfiguredExtensions]).toEqual([]);
37
+ });
38
+
39
+ it("#given duplicate edited paths #when diagnostics run #then first seen paths are processed once and blocks stay ordered", async () => {
40
+ const calls: string[] = [];
41
+
42
+ const result = await collectPostEditDiagnostics({
43
+ filePaths: ["a.ts", "b.ts", "a.ts", "c.ts"],
44
+ runDiagnostics: async (filePath: string) => {
45
+ calls.push(filePath);
46
+ return filePath === "b.ts" ? "No diagnostics found" : `diagnostic for ${filePath}`;
47
+ },
48
+ });
49
+
50
+ expect(calls).toEqual(["a.ts", "b.ts", "c.ts"]);
51
+ expect(result.blocks).toEqual([
52
+ { filePath: "a.ts", diagnostics: "diagnostic for a.ts" },
53
+ { filePath: "c.ts", diagnostics: "diagnostic for c.ts" },
54
+ ]);
55
+ });
56
+
57
+ it("#given many edited paths #when diagnostics run #then concurrency is bounded to four and failures isolate", async () => {
58
+ let active = 0;
59
+ let maxActive = 0;
60
+
61
+ const result = await collectPostEditDiagnostics({
62
+ filePaths: ["a.ts", "b.ts", "c.ts", "d.ts", "e.ts", "f.ts"],
63
+ runDiagnostics: async (filePath: string) => {
64
+ active += 1;
65
+ maxActive = Math.max(maxActive, active);
66
+ await Promise.resolve();
67
+ active -= 1;
68
+ if (filePath === "c.ts") throw new Error("server exploded");
69
+ return filePath === "e.ts" ? "No diagnostics found" : `diagnostic for ${filePath}`;
70
+ },
71
+ });
72
+
73
+ expect(maxActive).toBe(4);
74
+ expect(result.blocks).toEqual([
75
+ { filePath: "a.ts", diagnostics: "diagnostic for a.ts" },
76
+ { filePath: "b.ts", diagnostics: "diagnostic for b.ts" },
77
+ { filePath: "c.ts", diagnostics: "server exploded" },
78
+ { filePath: "d.ts", diagnostics: "diagnostic for d.ts" },
79
+ { filePath: "f.ts", diagnostics: "diagnostic for f.ts" },
80
+ ]);
81
+ });
82
+
83
+ it("#given structured not_configured diagnostics #when cache is reused and reset #then only not_configured is cached", async () => {
84
+ const cache = createPostEditNotConfiguredCache();
85
+ const calls: string[] = [];
86
+ const responses = new Map<string, string | { readonly kind: "not_configured"; readonly extension: string }>([
87
+ ["a.foo", { kind: "not_configured", extension: ".foo" }],
88
+ ["b.foo", "diagnostic should be skipped"],
89
+ ["c.bar", "LSP server 'bar' for .bar is NOT INSTALLED."],
90
+ ]);
91
+
92
+ const first = await collectPostEditDiagnostics({
93
+ filePaths: ["a.foo", "c.bar"],
94
+ cache,
95
+ runDiagnostics: async (filePath: string) => {
96
+ calls.push(filePath);
97
+ return responses.get(filePath) ?? "No diagnostics found";
98
+ },
99
+ });
100
+
101
+ expect(calls).toEqual(["a.foo", "c.bar"]);
102
+ expect(first.blocks).toEqual([{ filePath: "c.bar", diagnostics: "LSP server 'bar' for .bar is NOT INSTALLED." }]);
103
+
104
+ const cached = await collectPostEditDiagnostics({
105
+ filePaths: ["b.foo"],
106
+ cache,
107
+ runDiagnostics: async (filePath: string) => {
108
+ calls.push(filePath);
109
+ return responses.get(filePath) ?? "No diagnostics found";
110
+ },
111
+ });
112
+
113
+ expect(cached.blocks).toEqual([]);
114
+ expect(calls).toEqual(["a.foo", "c.bar"]);
115
+
116
+ responses.set("a.foo", "No diagnostics found");
117
+ resetPostEditNotConfiguredCache(cache);
118
+ const second = await collectPostEditDiagnostics({
119
+ filePaths: ["a.foo", "b.foo"],
120
+ cache,
121
+ runDiagnostics: async (filePath: string) => {
122
+ calls.push(filePath);
123
+ return responses.get(filePath) ?? "No diagnostics found";
124
+ },
125
+ });
126
+
127
+ expect(second.blocks).toEqual([{ filePath: "b.foo", diagnostics: "diagnostic should be skipped" }]);
128
+ expect(calls).toEqual(["a.foo", "c.bar", "a.foo", "b.foo"]);
129
+ });
130
+
131
+ it("#given non-not_configured failures #when cache is reused #then every failure retries", async () => {
132
+ const cache = createPostEditNotConfiguredCache();
133
+ let calls = 0;
134
+
135
+ const first = await collectPostEditDiagnostics({
136
+ filePaths: ["a.foo"],
137
+ cache,
138
+ runDiagnostics: async () => {
139
+ calls += 1;
140
+ return "LSP server 'foo' for .foo is NOT INSTALLED.";
141
+ },
142
+ });
143
+ const second = await collectPostEditDiagnostics({
144
+ filePaths: ["b.foo"],
145
+ cache,
146
+ runDiagnostics: async () => {
147
+ calls += 1;
148
+ throw new Error("daemon unavailable");
149
+ },
150
+ });
151
+
152
+ expect(calls).toBe(2);
153
+ expect(first.blocks).toEqual([{ filePath: "a.foo", diagnostics: "LSP server 'foo' for .foo is NOT INSTALLED." }]);
154
+ expect(second.blocks).toEqual([{ filePath: "b.foo", diagnostics: "daemon unavailable" }]);
155
+ expect([...cache.notConfiguredExtensions]).toEqual([]);
156
+ });
157
+ });