oh-my-opencode 5.0.0-beta.30 → 5.0.0-beta.32

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 (278) hide show
  1. package/dist/cli/index.js +178 -37
  2. package/dist/cli-node/index.js +178 -37
  3. package/dist/config/schema/background-task.d.ts +1 -0
  4. package/dist/config/schema/oh-my-opencode-config.d.ts +1 -0
  5. package/dist/features/background-agent/constants.d.ts +0 -7
  6. package/dist/features/background-agent/manager.d.ts +2 -1
  7. package/dist/features/background-agent/parent-wake-dispatched-tracker.d.ts +0 -1
  8. package/dist/features/background-agent/parent-wake-notifier-types.d.ts +1 -1
  9. package/dist/features/background-agent/parent-wake-prompt-dispatch.d.ts +1 -1
  10. package/dist/features/background-agent/parent-wake-session-inspector.d.ts +1 -1
  11. package/dist/features/background-agent/subagent-spawn-limits.d.ts +8 -0
  12. package/dist/features/monitor/manager-internals.d.ts +1 -1
  13. package/dist/features/monitor/output-injector-types.d.ts +1 -1
  14. package/dist/features/task-toast-manager/manager.d.ts +0 -24
  15. package/dist/features/team-mode/tools/messaging-live-delivery-recipient.d.ts +1 -0
  16. package/dist/features/team-mode/tools/messaging-runtime.d.ts +1 -0
  17. package/dist/hooks/anthropic-context-window-limit-recovery/message-storage-directory.d.ts +0 -3
  18. package/dist/hooks/anthropic-context-window-limit-recovery/state.d.ts +1 -0
  19. package/dist/hooks/anthropic-context-window-limit-recovery/storage/empty-messages.d.ts +0 -1
  20. package/dist/hooks/anthropic-context-window-limit-recovery/tool-result-storage.d.ts +11 -0
  21. package/dist/hooks/claude-code-hooks/session-hook-state.d.ts +1 -0
  22. package/dist/hooks/claude-code-hooks/stop.d.ts +2 -0
  23. package/dist/hooks/claude-code-hooks/todo.d.ts +0 -1
  24. package/dist/hooks/claude-code-hooks/transcript.d.ts +2 -0
  25. package/dist/hooks/fsync-skip-warning/index.d.ts +4 -0
  26. package/dist/hooks/hashline-edit-diff-enhancer/hook.d.ts +1 -0
  27. package/dist/hooks/hashline-edit-diff-enhancer/pending-captures.d.ts +13 -0
  28. package/dist/hooks/keyword-detector/hook.d.ts +7 -0
  29. package/dist/hooks/keyword-detector/ultrawork/source-detector.d.ts +0 -1
  30. package/dist/hooks/model-fallback/hook.d.ts +1 -1
  31. package/dist/index.js +681 -273
  32. package/dist/oh-my-opencode.schema.json +5 -0
  33. package/dist/plugin-dispose.d.ts +3 -0
  34. package/dist/shared/agent-tool-restrictions.d.ts +0 -1
  35. package/dist/skills/ultimate-browsing/references/insane-search/README.md +9 -2
  36. package/dist/tools/delegate-task/sync-session-cleanup.d.ts +3 -0
  37. package/dist/tools/delegate-task/sync-session-poller.d.ts +1 -0
  38. package/dist/tools/delegate-task/types.d.ts +1 -0
  39. package/dist/tools/session-manager/directory-filter.d.ts +2 -0
  40. package/dist/tui.js +148 -9
  41. package/package.json +16 -15
  42. package/packages/git-bash-mcp/dist/cli.js +1 -0
  43. package/packages/lsp-core/src/lsp/client-diagnostics-freshness.integration.test.ts +47 -19
  44. package/packages/lsp-core/src/lsp/client-diagnostics-pull-timeout.test.ts +64 -0
  45. package/packages/lsp-core/src/lsp/client-wrapper.test.ts +97 -0
  46. package/packages/lsp-core/src/lsp/client-wrapper.ts +21 -6
  47. package/packages/lsp-core/src/lsp/client.ts +6 -3
  48. package/packages/lsp-core/src/lsp/constants.ts +5 -0
  49. package/packages/lsp-core/src/lsp/controlled-clock-test-support.ts +61 -0
  50. package/packages/lsp-core/src/lsp/errors.ts +15 -0
  51. package/packages/lsp-core/src/lsp/fixtures/workspace-edit-server.mjs +10 -3
  52. package/packages/lsp-core/src/lsp/manager-lifecycle.test.ts +262 -0
  53. package/packages/lsp-core/src/lsp/manager.ts +106 -13
  54. package/packages/lsp-core/src/lsp/timer-provider.ts +13 -0
  55. package/packages/lsp-core/src/lsp/transport-request-timeout.test.ts +73 -0
  56. package/packages/lsp-core/src/lsp/transport.ts +7 -2
  57. package/packages/lsp-core/src/lsp/workspace-document-state.test.ts +84 -1
  58. package/packages/lsp-core/src/lsp/workspace-document-state.ts +30 -11
  59. package/packages/lsp-core/src/lsp/workspace-markers.ts +18 -1
  60. package/packages/lsp-core/src/mcp.ts +3 -0
  61. package/packages/lsp-core/src/tools/parameters.test.ts +51 -0
  62. package/packages/lsp-daemon/dist/cli.js +155 -34
  63. package/packages/lsp-daemon/dist/client.js +155 -34
  64. package/packages/lsp-daemon/dist/index.js +155 -34
  65. package/packages/lsp-daemon/dist/proxy.js +3 -0
  66. package/packages/lsp-tools-mcp/dist/cli.js +155 -34
  67. package/packages/lsp-tools-mcp/dist/lsp/manager.js +134 -28
  68. package/packages/lsp-tools-mcp/dist/mcp.js +155 -34
  69. package/packages/lsp-tools-mcp/dist/tools.js +154 -34
  70. package/packages/lsp-tools-mcp/package.json +1 -1
  71. package/packages/omo-codex/plugin/.codex-plugin/plugin.json +1 -1
  72. package/packages/omo-codex/plugin/components/bootstrap/hooks/hooks.json +1 -1
  73. package/packages/omo-codex/plugin/components/bootstrap/package.json +1 -1
  74. package/packages/omo-codex/plugin/components/codegraph/dist/cli.js +17 -2
  75. package/packages/omo-codex/plugin/components/codegraph/dist/serve.js +17 -2
  76. package/packages/omo-codex/plugin/components/codegraph/package.json +1 -1
  77. package/packages/omo-codex/plugin/components/codegraph/src/mcp-unavailable.ts +12 -0
  78. package/packages/omo-codex/plugin/components/codegraph/test/serve-unavailable-idle.test.ts +47 -0
  79. package/packages/omo-codex/plugin/components/comment-checker/hooks/hooks.json +1 -1
  80. package/packages/omo-codex/plugin/components/comment-checker/package.json +1 -1
  81. package/packages/omo-codex/plugin/components/git-bash/hooks/hooks.json +2 -2
  82. package/packages/omo-codex/plugin/components/git-bash/package.json +1 -1
  83. package/packages/omo-codex/plugin/components/lazycodex-executor-verify/hooks/hooks.json +1 -1
  84. package/packages/omo-codex/plugin/components/lazycodex-executor-verify/package.json +1 -1
  85. package/packages/omo-codex/plugin/components/lsp/dist/.omo-runtime-manifest.json +3 -3
  86. package/packages/omo-codex/plugin/components/lsp/dist/cli.js +152 -34
  87. package/packages/omo-codex/plugin/components/lsp/hooks/hooks.json +2 -2
  88. package/packages/omo-codex/plugin/components/lsp/package.json +1 -1
  89. package/packages/omo-codex/plugin/components/rules/hooks/hooks.json +4 -4
  90. package/packages/omo-codex/plugin/components/rules/package.json +1 -1
  91. package/packages/omo-codex/plugin/components/teammode/hooks/hooks.json +1 -1
  92. package/packages/omo-codex/plugin/components/teammode/package.json +1 -1
  93. package/packages/omo-codex/plugin/components/telemetry/hooks/hooks.json +1 -1
  94. package/packages/omo-codex/plugin/components/telemetry/package.json +1 -1
  95. package/packages/omo-codex/plugin/components/ultrawork/AGENTS.md +5 -4
  96. package/packages/omo-codex/plugin/components/ultrawork/dist/cli.js +489 -4
  97. package/packages/omo-codex/plugin/components/ultrawork/hooks/hooks.json +1 -1
  98. package/packages/omo-codex/plugin/components/ultrawork/package.json +1 -2
  99. package/packages/omo-codex/plugin/components/ultrawork/scripts/sync-directive.mjs +30 -14
  100. package/packages/omo-codex/plugin/components/ultrawork/src/directive-content.ts +4 -0
  101. package/packages/omo-codex/plugin/components/ultrawork/src/directive.ts +5 -2
  102. package/packages/omo-codex/plugin/components/ultrawork/test/directive-source.test.ts +14 -8
  103. package/packages/omo-codex/plugin/components/ultrawork/test/package-smoke.test.ts +12 -1
  104. package/packages/omo-codex/plugin/components/ulw-execute-continuation/hooks/hooks.json +2 -2
  105. package/packages/omo-codex/plugin/components/ulw-execute-continuation/package.json +1 -1
  106. package/packages/omo-codex/plugin/components/ulw-loop/AGENTS.md +5 -2
  107. package/packages/omo-codex/plugin/components/ulw-loop/README.md +1 -0
  108. package/packages/omo-codex/plugin/components/ulw-loop/dist/checkpoint-continuation.js +9 -0
  109. package/packages/omo-codex/plugin/components/ulw-loop/dist/checkpoint-reconciliation.d.ts +14 -0
  110. package/packages/omo-codex/plugin/components/ulw-loop/dist/checkpoint-reconciliation.js +10 -1
  111. package/packages/omo-codex/plugin/components/ulw-loop/dist/checkpoint-template.d.ts +8 -0
  112. package/packages/omo-codex/plugin/components/ulw-loop/dist/checkpoint-template.js +113 -0
  113. package/packages/omo-codex/plugin/components/ulw-loop/dist/checkpoint.js +9 -5
  114. package/packages/omo-codex/plugin/components/ulw-loop/dist/cli-commands.js +2 -1
  115. package/packages/omo-codex/plugin/components/ulw-loop/dist/cli-output.d.ts +1 -1
  116. package/packages/omo-codex/plugin/components/ulw-loop/dist/cli-output.js +1 -0
  117. package/packages/omo-codex/plugin/components/ulw-loop/dist/cli-subcommands.js +2 -0
  118. package/packages/omo-codex/plugin/components/ulw-loop/dist/cli.js +856 -347
  119. package/packages/omo-codex/plugin/components/ulw-loop/dist/codex-goal-instruction.d.ts +2 -0
  120. package/packages/omo-codex/plugin/components/ulw-loop/dist/codex-goal-instruction.js +27 -6
  121. package/packages/omo-codex/plugin/components/ulw-loop/dist/codex-goal-snapshot.d.ts +13 -0
  122. package/packages/omo-codex/plugin/components/ulw-loop/dist/codex-goal-snapshot.js +14 -0
  123. package/packages/omo-codex/plugin/components/ulw-loop/dist/domain-types.d.ts +17 -9
  124. package/packages/omo-codex/plugin/components/ulw-loop/dist/plan-io.js +17 -2
  125. package/packages/omo-codex/plugin/components/ulw-loop/dist/plan-missing-recovery.d.ts +13 -0
  126. package/packages/omo-codex/plugin/components/ulw-loop/dist/plan-missing-recovery.js +19 -0
  127. package/packages/omo-codex/plugin/components/ulw-loop/dist/quality-gate-aggregate.d.ts +2 -0
  128. package/packages/omo-codex/plugin/components/ulw-loop/dist/quality-gate-aggregate.js +53 -0
  129. package/packages/omo-codex/plugin/components/ulw-loop/dist/quality-gate-artifacts.d.ts +9 -0
  130. package/packages/omo-codex/plugin/components/ulw-loop/dist/quality-gate-artifacts.js +85 -0
  131. package/packages/omo-codex/plugin/components/ulw-loop/dist/quality-gate-fields.js +1 -1
  132. package/packages/omo-codex/plugin/components/ulw-loop/dist/quality-gate.d.ts +4 -2
  133. package/packages/omo-codex/plugin/components/ulw-loop/dist/quality-gate.js +41 -102
  134. package/packages/omo-codex/plugin/components/ulw-loop/dist/review-blockers.js +4 -2
  135. package/packages/omo-codex/plugin/components/ulw-loop/dist/spawn-guard.js +185 -14
  136. package/packages/omo-codex/plugin/components/ulw-loop/dist/status-next-actions.d.ts +6 -0
  137. package/packages/omo-codex/plugin/components/ulw-loop/dist/status-next-actions.js +33 -0
  138. package/packages/omo-codex/plugin/components/ulw-loop/dist/surface.d.ts +18 -0
  139. package/packages/omo-codex/plugin/components/ulw-loop/dist/surface.js +65 -0
  140. package/packages/omo-codex/plugin/components/ulw-loop/hooks/hooks.json +4 -4
  141. package/packages/omo-codex/plugin/components/ulw-loop/package.json +1 -1
  142. package/packages/omo-codex/plugin/components/ulw-loop/skills/ulw-loop/references/full-workflow.md +17 -0
  143. package/packages/omo-codex/plugin/components/ulw-loop/src/checkpoint-continuation.ts +7 -0
  144. package/packages/omo-codex/plugin/components/ulw-loop/src/checkpoint-reconciliation.ts +25 -1
  145. package/packages/omo-codex/plugin/components/ulw-loop/src/checkpoint-template.ts +127 -0
  146. package/packages/omo-codex/plugin/components/ulw-loop/src/checkpoint.ts +10 -12
  147. package/packages/omo-codex/plugin/components/ulw-loop/src/cli-commands.ts +2 -1
  148. package/packages/omo-codex/plugin/components/ulw-loop/src/cli-output.ts +1 -0
  149. package/packages/omo-codex/plugin/components/ulw-loop/src/cli-subcommands.ts +2 -0
  150. package/packages/omo-codex/plugin/components/ulw-loop/src/codex-goal-instruction.ts +34 -5
  151. package/packages/omo-codex/plugin/components/ulw-loop/src/codex-goal-snapshot.ts +23 -0
  152. package/packages/omo-codex/plugin/components/ulw-loop/src/domain-types.ts +19 -9
  153. package/packages/omo-codex/plugin/components/ulw-loop/src/plan-io.ts +18 -3
  154. package/packages/omo-codex/plugin/components/ulw-loop/src/plan-missing-recovery.ts +27 -0
  155. package/packages/omo-codex/plugin/components/ulw-loop/src/quality-gate-aggregate.ts +57 -0
  156. package/packages/omo-codex/plugin/components/ulw-loop/src/quality-gate-artifacts.ts +103 -0
  157. package/packages/omo-codex/plugin/components/ulw-loop/src/quality-gate-fields.ts +1 -1
  158. package/packages/omo-codex/plugin/components/ulw-loop/src/quality-gate.ts +54 -123
  159. package/packages/omo-codex/plugin/components/ulw-loop/src/review-blockers.ts +4 -2
  160. package/packages/omo-codex/plugin/components/ulw-loop/src/spawn-guard.ts +187 -12
  161. package/packages/omo-codex/plugin/components/ulw-loop/src/status-next-actions.ts +36 -0
  162. package/packages/omo-codex/plugin/components/ulw-loop/src/surface.ts +95 -0
  163. package/packages/omo-codex/plugin/components/ulw-loop/test/checkpoint-final.test.ts +107 -1
  164. package/packages/omo-codex/plugin/components/ulw-loop/test/checkpoint-template.test.ts +92 -0
  165. package/packages/omo-codex/plugin/components/ulw-loop/test/cli-status-next-actions.test.ts +183 -0
  166. package/packages/omo-codex/plugin/components/ulw-loop/test/codex-goal-instruction.test.ts +35 -0
  167. package/packages/omo-codex/plugin/components/ulw-loop/test/fixtures/quality-gate-builder.ts +7 -3
  168. package/packages/omo-codex/plugin/components/ulw-loop/test/guided-recovery.test.ts +105 -0
  169. package/packages/omo-codex/plugin/components/ulw-loop/test/quality-gate-aggregate.test.ts +101 -0
  170. package/packages/omo-codex/plugin/components/ulw-loop/test/quality-gate-roles.test.ts +46 -2
  171. package/packages/omo-codex/plugin/components/ulw-loop/test/quality-gate-senpi-surface.test.ts +131 -0
  172. package/packages/omo-codex/plugin/components/ulw-loop/test/quality-gate.test.ts +5 -0
  173. package/packages/omo-codex/plugin/components/ulw-loop/test/spawn-guard.test.ts +318 -1
  174. package/packages/omo-codex/plugin/components/ulw-loop/test/surface.test.ts +74 -0
  175. package/packages/omo-codex/plugin/components/ulw-loop/test/ultrawork-directive.test.ts +9 -3
  176. package/packages/omo-codex/plugin/hooks/post-compact-resetting-git-bash-mcp-reminder.json +1 -1
  177. package/packages/omo-codex/plugin/hooks/post-compact-resetting-lsp-diagnostics-cache.json +1 -1
  178. package/packages/omo-codex/plugin/hooks/post-compact-resetting-project-rule-cache.json +1 -1
  179. package/packages/omo-codex/plugin/hooks/post-tool-use-checking-codegraph-init-guidance.json +1 -1
  180. package/packages/omo-codex/plugin/hooks/post-tool-use-checking-comments.json +1 -1
  181. package/packages/omo-codex/plugin/hooks/post-tool-use-checking-lsp-diagnostics.json +1 -1
  182. package/packages/omo-codex/plugin/hooks/post-tool-use-checking-thread-title-hygiene.json +1 -1
  183. package/packages/omo-codex/plugin/hooks/post-tool-use-matching-project-rules.json +1 -1
  184. package/packages/omo-codex/plugin/hooks/pre-tool-use-enforcing-unlimited-goal-budget.json +1 -1
  185. package/packages/omo-codex/plugin/hooks/pre-tool-use-guarding-ulw-loop-spawns.json +1 -1
  186. package/packages/omo-codex/plugin/hooks/pre-tool-use-recommending-git-bash-mcp.json +1 -1
  187. package/packages/omo-codex/plugin/hooks/session-start-checking-auto-update.json +1 -1
  188. package/packages/omo-codex/plugin/hooks/session-start-checking-bootstrap-provisioning.json +1 -1
  189. package/packages/omo-codex/plugin/hooks/session-start-checking-codegraph-bootstrap.json +1 -1
  190. package/packages/omo-codex/plugin/hooks/session-start-loading-project-rules.json +1 -1
  191. package/packages/omo-codex/plugin/hooks/session-start-recording-session-telemetry.json +1 -1
  192. package/packages/omo-codex/plugin/hooks/stop-checking-ulw-execute-continuation.json +1 -1
  193. package/packages/omo-codex/plugin/hooks/stop-checking-ulw-loop-resume.json +1 -1
  194. package/packages/omo-codex/plugin/hooks/subagent-stop-checking-ulw-execute-continuation.json +1 -1
  195. package/packages/omo-codex/plugin/hooks/subagent-stop-verifying-lazycodex-executor-evidence.json +1 -1
  196. package/packages/omo-codex/plugin/hooks/user-prompt-submit-checking-ultrawork-trigger.json +1 -1
  197. package/packages/omo-codex/plugin/hooks/user-prompt-submit-checking-ulw-loop-steering.json +1 -1
  198. package/packages/omo-codex/plugin/hooks/user-prompt-submit-loading-project-rules.json +1 -1
  199. package/packages/omo-codex/plugin/package-lock.json +13 -13
  200. package/packages/omo-codex/plugin/package.json +1 -1
  201. package/packages/omo-codex/plugin/scripts/sync-skills.mjs +31 -2
  202. package/packages/omo-codex/plugin/skills/ultimate-browsing/references/insane-search/README.md +9 -2
  203. package/packages/omo-codex/plugin/skills/ulw-loop/references/full-workflow.md +17 -0
  204. package/packages/omo-codex/plugin/test/sync-skills-test-support.mjs +11 -1
  205. package/packages/omo-codex/plugin/test/sync-skills.test.mjs +13 -0
  206. package/packages/omo-codex/plugin/test/ultrawork-skill-pointer.test.mjs +5 -2
  207. package/packages/omo-codex/scripts/install-dist/install-local.mjs +2 -2
  208. package/packages/shared-skills/skills/ultimate-browsing/references/insane-search/README.md +9 -2
  209. package/dist/features/builtin-skills/index.d.ts +0 -1
  210. package/dist/features/builtin-skills/skill-file-loader.d.ts +0 -1
  211. package/dist/features/builtin-skills/skills/agent-browser-skill.d.ts +0 -1
  212. package/dist/features/builtin-skills/skills/agent-browser-template.d.ts +0 -1
  213. package/dist/features/builtin-skills/skills/debugging.d.ts +0 -1
  214. package/dist/features/builtin-skills/skills/dev-browser.d.ts +0 -1
  215. package/dist/features/builtin-skills/skills/frontend.d.ts +0 -1
  216. package/dist/features/builtin-skills/skills/git-master-sections/commit-atomic-planning.d.ts +0 -1
  217. package/dist/features/builtin-skills/skills/git-master-sections/commit-context-analysis.d.ts +0 -1
  218. package/dist/features/builtin-skills/skills/git-master-sections/commit-execution-verification.d.ts +0 -1
  219. package/dist/features/builtin-skills/skills/git-master-sections/commit-workflow.d.ts +0 -1
  220. package/dist/features/builtin-skills/skills/git-master-sections/history-search-workflow.d.ts +0 -1
  221. package/dist/features/builtin-skills/skills/git-master-sections/overview.d.ts +0 -1
  222. package/dist/features/builtin-skills/skills/git-master-sections/quick-reference.d.ts +0 -1
  223. package/dist/features/builtin-skills/skills/git-master-sections/rebase-workflow.d.ts +0 -1
  224. package/dist/features/builtin-skills/skills/git-master-skill-metadata.d.ts +0 -1
  225. package/dist/features/builtin-skills/skills/git-master.d.ts +0 -1
  226. package/dist/features/builtin-skills/skills/index.d.ts +0 -1
  227. package/dist/features/builtin-skills/skills/init-deep.d.ts +0 -1
  228. package/dist/features/builtin-skills/skills/playwright-cli.d.ts +0 -1
  229. package/dist/features/builtin-skills/skills/playwright-mcp-skill.d.ts +0 -1
  230. package/dist/features/builtin-skills/skills/playwright.d.ts +0 -1
  231. package/dist/features/builtin-skills/skills/remove-ai-slops.d.ts +0 -1
  232. package/dist/features/builtin-skills/skills/review-work.d.ts +0 -1
  233. package/dist/features/builtin-skills/skills/security-research.d.ts +0 -1
  234. package/dist/features/builtin-skills/skills/security-review.d.ts +0 -1
  235. package/dist/features/builtin-skills/skills/team-mode.d.ts +0 -1
  236. package/dist/features/builtin-skills/skills/visual-qa.d.ts +0 -1
  237. package/dist/features/builtin-skills/skills.d.ts +0 -1
  238. package/dist/features/builtin-skills/types.d.ts +0 -1
  239. package/dist/features/claude-code-plugin-loader/agent-loader.d.ts +0 -1
  240. package/dist/features/claude-code-plugin-loader/command-loader.d.ts +0 -1
  241. package/dist/features/claude-code-plugin-loader/discovery-core.d.ts +0 -1
  242. package/dist/features/claude-code-plugin-loader/discovery-paths.d.ts +0 -1
  243. package/dist/features/claude-code-plugin-loader/discovery.d.ts +0 -1
  244. package/dist/features/claude-code-plugin-loader/hook-loader.d.ts +0 -1
  245. package/dist/features/claude-code-plugin-loader/index.d.ts +0 -1
  246. package/dist/features/claude-code-plugin-loader/install-path-resolver.d.ts +0 -1
  247. package/dist/features/claude-code-plugin-loader/installed-plugin-database.d.ts +0 -1
  248. package/dist/features/claude-code-plugin-loader/loaded-plugin.d.ts +0 -1
  249. package/dist/features/claude-code-plugin-loader/loader.d.ts +0 -1
  250. package/dist/features/claude-code-plugin-loader/mcp-server-loader.d.ts +0 -1
  251. package/dist/features/claude-code-plugin-loader/plugin-key.d.ts +0 -1
  252. package/dist/features/claude-code-plugin-loader/plugin-manifest.d.ts +0 -1
  253. package/dist/features/claude-code-plugin-loader/plugin-path-resolver.d.ts +0 -1
  254. package/dist/features/claude-code-plugin-loader/plugin-settings.d.ts +0 -1
  255. package/dist/features/claude-code-plugin-loader/scope-filter.d.ts +0 -1
  256. package/dist/features/claude-code-plugin-loader/skill-loader.d.ts +0 -1
  257. package/dist/features/claude-code-plugin-loader/types.d.ts +0 -1
  258. package/dist/hooks/ralph-loop/completion-promise-detector-test-input.d.ts +0 -11
  259. package/dist/hooks/ralph-loop/loop-session-recovery.d.ts +0 -7
  260. package/dist/shared/prompt-async-gate/message-inspection-error.d.ts +0 -1
  261. package/dist/shared/prompt-async-gate/pending-tool-turn.d.ts +0 -1
  262. package/dist/shared/prompt-async-gate/prompt-message-state.d.ts +0 -1
  263. package/dist/shared/prompt-async-gate/queue.d.ts +0 -1
  264. package/dist/shared/prompt-async-gate/recent-dispatches.d.ts +0 -1
  265. package/dist/shared/prompt-async-gate/reservations.d.ts +0 -1
  266. package/dist/shared/prompt-async-gate/semantic-dedupe.d.ts +0 -1
  267. package/dist/shared/prompt-async-gate/session-idle-dispatch.d.ts +0 -1
  268. package/dist/shared/prompt-async-gate/timing.d.ts +0 -1
  269. package/dist/shared/prompt-async-gate/types.d.ts +0 -1
  270. package/dist/tools/call-omo-agent/message-storage-directory.d.ts +0 -1
  271. package/dist/tools/delegate-task/model-string-parser.d.ts +0 -1
  272. package/packages/lsp-tools-mcp/dist/lsp/cleanup-errors.d.ts +0 -1
  273. package/packages/lsp-tools-mcp/dist/lsp/constants.d.ts +0 -1
  274. package/packages/lsp-tools-mcp/dist/lsp/language-mappings.d.ts +0 -1
  275. package/packages/lsp-tools-mcp/dist/lsp/process-signal-cleanup.d.ts +0 -1
  276. package/packages/lsp-tools-mcp/dist/missing-dependency-result.d.ts +0 -1
  277. package/packages/omo-codex/plugin/components/ultrawork/directive.md +0 -483
  278. package/packages/omo-codex/plugin/components/ultrawork/skills/ultrawork/SKILL.md +0 -490
@@ -1,6 +1,14 @@
1
1
  import { reportBestEffortCleanupError } from "./cleanup-errors.js";
2
2
  import { LspClient } from "./client.js";
3
- import { IDLE_TIMEOUT_MS, INIT_TIMEOUT_MS, MAX_RESIDENT_CLIENTS, REAPER_INTERVAL_MS } from "./constants.js";
3
+ import {
4
+ CLIENT_RESPAWN_COOLDOWN_MS,
5
+ CLIENT_RESPAWN_RETRY_LIMIT,
6
+ IDLE_TIMEOUT_MS,
7
+ INIT_TIMEOUT_MS,
8
+ MAX_RESIDENT_CLIENTS,
9
+ REAPER_INTERVAL_MS,
10
+ } from "./constants.js";
11
+ import { LspClientRespawnBudgetExceededError } from "./errors.js";
4
12
  import { installProcessSignalCleanup } from "./process-signal-cleanup.js";
5
13
  import type { ResolvedServer } from "./types.js";
6
14
 
@@ -14,6 +22,11 @@ interface ManagedClient {
14
22
  initializingSince: number | null;
15
23
  }
16
24
 
25
+ interface RespawnBudget {
26
+ deadGenerations: number;
27
+ exhaustedAt: number | null;
28
+ }
29
+
17
30
  export interface ClientSnapshot {
18
31
  root: string;
19
32
  serverId: string;
@@ -75,6 +88,9 @@ function awaitWithSignal<T>(promise: Promise<T>, signal: AbortSignal | undefined
75
88
 
76
89
  export class LspManager {
77
90
  private readonly clients = new Map<string, ManagedClient>();
91
+ /** In-flight stops per key; getClient awaits these instead of spawning a duplicate client. */
92
+ private readonly pendingStops = new Map<string, Promise<void>>();
93
+ private readonly respawnBudgets = new Map<string, RespawnBudget>();
78
94
  private reaperHandle: NodeJS.Timeout | null = null;
79
95
  private signalDisposer: (() => void) | null = null;
80
96
  private disposed = false;
@@ -112,6 +128,50 @@ export class LspManager {
112
128
  return `${root}::${serverId}`;
113
129
  }
114
130
 
131
+ /**
132
+ * Deletes-then-stops with a tombstone: the pending stop is recorded so a concurrent getClient
133
+ * for the same key awaits it instead of spawning a duplicate client while the old one lingers
134
+ * (production stops reach ~21s). The tombstone removes itself once the stop settles.
135
+ */
136
+ private tombstoneStop(key: string, client: LspClient): Promise<void> {
137
+ const stop = stopClientBestEffort(client);
138
+ this.pendingStops.set(key, stop);
139
+ void stop.finally(() => {
140
+ if (this.pendingStops.get(key) === stop) {
141
+ this.pendingStops.delete(key);
142
+ }
143
+ });
144
+ return stop;
145
+ }
146
+
147
+ private recordDeadGeneration(key: string): void {
148
+ let budget = this.respawnBudgets.get(key);
149
+ if (budget === undefined) {
150
+ budget = { deadGenerations: 0, exhaustedAt: null };
151
+ this.respawnBudgets.set(key, budget);
152
+ }
153
+ budget.deadGenerations += 1;
154
+ }
155
+
156
+ private markHealthyGeneration(key: string): void {
157
+ this.respawnBudgets.delete(key);
158
+ }
159
+
160
+ /** Throws while the respawn budget is spent and the cooldown has not elapsed. */
161
+ private ensureRespawnAllowed(key: string, root: string, serverId: string): void {
162
+ const budget = this.respawnBudgets.get(key);
163
+ if (budget === undefined || budget.deadGenerations < CLIENT_RESPAWN_RETRY_LIMIT) return;
164
+ if (budget.exhaustedAt === null) {
165
+ budget.exhaustedAt = this.now();
166
+ throw new LspClientRespawnBudgetExceededError(serverId, root, CLIENT_RESPAWN_RETRY_LIMIT);
167
+ }
168
+ if (this.now() - budget.exhaustedAt < CLIENT_RESPAWN_COOLDOWN_MS) {
169
+ throw new LspClientRespawnBudgetExceededError(serverId, root, CLIENT_RESPAWN_RETRY_LIMIT);
170
+ }
171
+ budget.deadGenerations = 0;
172
+ budget.exhaustedAt = null;
173
+ }
174
+
115
175
  private reapStale(): void {
116
176
  const t = this.now();
117
177
  for (const [key, managed] of this.clients) {
@@ -120,8 +180,8 @@ export class LspManager {
120
180
  managed.initializingSince !== null &&
121
181
  t - managed.initializingSince > this.initTimeoutMs
122
182
  ) {
123
- void stopClientBestEffort(managed.client);
124
183
  this.clients.delete(key);
184
+ void this.tombstoneStop(key, managed.client);
125
185
  continue;
126
186
  }
127
187
 
@@ -131,8 +191,8 @@ export class LspManager {
131
191
  managed.pendingWaiters === 0 &&
132
192
  t - managed.lastUsedAt > this.idleTimeoutMs
133
193
  ) {
134
- void stopClientBestEffort(managed.client);
135
194
  this.clients.delete(key);
195
+ void this.tombstoneStop(key, managed.client);
136
196
  }
137
197
  }
138
198
  }
@@ -148,7 +208,7 @@ export class LspManager {
148
208
  const victim = this.leastRecentlyUsedIdleClient();
149
209
  if (!victim) return;
150
210
  this.clients.delete(victim.key);
151
- void stopClientBestEffort(victim.managed.client);
211
+ void this.tombstoneStop(victim.key, victim.managed.client);
152
212
  }
153
213
  }
154
214
 
@@ -171,7 +231,7 @@ export class LspManager {
171
231
  this.clients.get(key) === managed
172
232
  ) {
173
233
  this.clients.delete(key);
174
- await stopClientBestEffort(managed.client);
234
+ await this.tombstoneStop(key, managed.client);
175
235
  }
176
236
  }
177
237
 
@@ -182,6 +242,16 @@ export class LspManager {
182
242
  signal?.throwIfAborted();
183
243
 
184
244
  const key = this.getKey(root, server.id);
245
+ for (;;) {
246
+ const pendingStop = this.pendingStops.get(key);
247
+ if (pendingStop === undefined) break;
248
+ await awaitWithSignal(pendingStop, signal);
249
+ signal?.throwIfAborted();
250
+ }
251
+ if (this.disposed) {
252
+ throw new Error("LspManager has been disposed");
253
+ }
254
+
185
255
  let managed = this.clients.get(key);
186
256
 
187
257
  if (managed) {
@@ -191,8 +261,8 @@ export class LspManager {
191
261
  managed.initializingSince !== null &&
192
262
  t - managed.initializingSince > this.initTimeoutMs
193
263
  ) {
194
- await stopClientBestEffort(managed.client);
195
264
  this.clients.delete(key);
265
+ await this.tombstoneStop(key, managed.client);
196
266
  managed = undefined;
197
267
  }
198
268
  }
@@ -216,17 +286,22 @@ export class LspManager {
216
286
  }
217
287
 
218
288
  if (!managed.client.isAlive()) {
219
- await stopClientBestEffort(managed.client);
220
- this.clients.delete(key);
289
+ this.recordDeadGeneration(key);
290
+ if (this.clients.get(key) === managed) {
291
+ this.clients.delete(key);
292
+ }
293
+ await this.tombstoneStop(key, managed.client);
221
294
  return this.getClient(root, server, signal);
222
295
  }
223
296
 
297
+ this.markHealthyGeneration(key);
224
298
  managed.refCount++;
225
299
  managed.lastUsedAt = this.now();
226
300
  return managed.client;
227
301
  }
228
302
 
229
303
  this.evictForAdmission();
304
+ this.ensureRespawnAllowed(key, root, server.id);
230
305
 
231
306
  const client = this.clientFactory(root, server);
232
307
  const initStartedAt = this.now();
@@ -253,7 +328,7 @@ export class LspManager {
253
328
  if (this.clients.get(key) === newManaged) {
254
329
  this.clients.delete(key);
255
330
  }
256
- await stopClientBestEffort(client);
331
+ await this.tombstoneStop(key, client);
257
332
  throw err;
258
333
  }
259
334
 
@@ -267,6 +342,16 @@ export class LspManager {
267
342
  signal.throwIfAborted();
268
343
  }
269
344
 
345
+ if (!client.isAlive()) {
346
+ this.recordDeadGeneration(key);
347
+ if (this.clients.get(key) === newManaged) {
348
+ this.clients.delete(key);
349
+ }
350
+ await this.tombstoneStop(key, client);
351
+ return this.getClient(root, server, signal);
352
+ }
353
+
354
+ this.markHealthyGeneration(key);
270
355
  newManaged.refCount++;
271
356
  newManaged.lastUsedAt = this.now();
272
357
  return client;
@@ -287,15 +372,20 @@ export class LspManager {
287
372
  if (!managed) return;
288
373
  if (client && managed.client !== client) return;
289
374
  this.clients.delete(key);
290
- void stopClientBestEffort(managed.client);
375
+ void this.tombstoneStop(key, managed.client);
291
376
  }
292
377
 
293
378
  warmupClient(root: string, server: ResolvedServer): void {
294
379
  if (this.disposed) return;
295
380
  const key = this.getKey(root, server.id);
296
- if (this.clients.has(key)) return;
381
+ if (this.clients.has(key) || this.pendingStops.has(key)) return;
297
382
 
298
383
  this.evictForAdmission();
384
+ try {
385
+ this.ensureRespawnAllowed(key, root, server.id);
386
+ } catch {
387
+ return;
388
+ }
299
389
 
300
390
  const client = this.clientFactory(root, server);
301
391
  const initStartedAt = this.now();
@@ -326,7 +416,7 @@ export class LspManager {
326
416
  if (this.clients.get(key) === managed) {
327
417
  this.clients.delete(key);
328
418
  }
329
- void stopClientBestEffort(client);
419
+ void this.tombstoneStop(key, client);
330
420
  },
331
421
  );
332
422
  }
@@ -379,8 +469,11 @@ export class LspManager {
379
469
  for (const managed of this.clients.values()) {
380
470
  stopPromises.push(stopClientBestEffort(managed.client));
381
471
  }
472
+ const tombstonedStops = [...this.pendingStops.values()];
382
473
  this.clients.clear();
383
- await Promise.allSettled(stopPromises);
474
+ this.respawnBudgets.clear();
475
+ await Promise.allSettled([...stopPromises, ...tombstonedStops]);
476
+ this.pendingStops.clear();
384
477
  }
385
478
  }
386
479
 
@@ -0,0 +1,13 @@
1
+ export type TimerHandle = ReturnType<typeof setTimeout>;
2
+
3
+ export interface TimerProvider {
4
+ readonly now: () => number;
5
+ readonly setTimeout: (callback: () => void, delayMs: number) => TimerHandle;
6
+ readonly clearTimeout: (handle: TimerHandle) => void;
7
+ }
8
+
9
+ export const realTimerProvider: TimerProvider = {
10
+ now: () => Date.now(),
11
+ setTimeout: (callback, delayMs) => setTimeout(callback, delayMs),
12
+ clearTimeout: (handle) => clearTimeout(handle),
13
+ };
@@ -0,0 +1,73 @@
1
+ import { PassThrough } from "node:stream";
2
+
3
+ import { describe, expect, it } from "bun:test";
4
+
5
+ import { ControlledClock } from "./controlled-clock-test-support.js";
6
+ import { LspRequestTimeoutError } from "./errors.js";
7
+ import { JsonRpcConnection } from "./json-rpc-connection.js";
8
+ import { LspClientTransport } from "./transport.js";
9
+ import type { LspClientTimeoutOptions } from "./transport.js";
10
+ import type { ResolvedServer } from "./types.js";
11
+
12
+ const fakeServer: ResolvedServer = {
13
+ id: "controlled-clock-fake",
14
+ command: ["controlled-clock-fake"],
15
+ extensions: [".ts"],
16
+ priority: 0,
17
+ };
18
+
19
+ class WiredTransport extends LspClientTransport {
20
+ constructor(timeouts: LspClientTimeoutOptions, connection: JsonRpcConnection) {
21
+ super("/tmp", fakeServer, timeouts);
22
+ this.connection = connection;
23
+ }
24
+
25
+ request<T>(method: string, params: unknown, options: { timeoutMs?: number }): Promise<T> {
26
+ return this.sendRequest<T>(method, params, options);
27
+ }
28
+ }
29
+
30
+ function encodeMessage(message: Record<string, unknown>): string {
31
+ const body = JSON.stringify(message);
32
+ return `Content-Length: ${Buffer.byteLength(body, "utf8")}\r\n\r\n${body}`;
33
+ }
34
+
35
+ function makeWiredTransport(clock: ControlledClock): { transport: WiredTransport; serverToClient: PassThrough } {
36
+ const serverToClient = new PassThrough();
37
+ const clientToServer = new PassThrough();
38
+ const connection = new JsonRpcConnection(serverToClient, clientToServer);
39
+ connection.listen();
40
+ return { transport: new WiredTransport({ timerProvider: clock }, connection), serverToClient };
41
+ }
42
+
43
+ describe("LspClientTransport request timeout via TimerProvider", () => {
44
+ it("#given a request the server never answers #when the injected clock passes the timeout #then the request rejects with LspRequestTimeoutError", async () => {
45
+ const clock = new ControlledClock();
46
+ const { transport } = makeWiredTransport(clock);
47
+
48
+ const pending = transport.request("textDocument/diagnostic", { textDocument: { uri: "file:///a.ts" } }, {
49
+ timeoutMs: 50,
50
+ });
51
+ await clock.waitForTimer(50);
52
+ expect(clock.scheduledDelays.at(-1)).toBe(50);
53
+ clock.advanceBy(50);
54
+
55
+ await expect(pending).rejects.toBeInstanceOf(LspRequestTimeoutError);
56
+ });
57
+
58
+ it("#given a request answered before the deadline #when the injected clock later passes the timeout #then the settled result stands", async () => {
59
+ const clock = new ControlledClock();
60
+ const { transport, serverToClient } = makeWiredTransport(clock);
61
+
62
+ const pending = transport.request<{ items: unknown[] }>(
63
+ "textDocument/diagnostic",
64
+ { textDocument: { uri: "file:///a.ts" } },
65
+ { timeoutMs: 50 },
66
+ );
67
+ await clock.waitForTimer(50);
68
+ serverToClient.write(encodeMessage({ jsonrpc: "2.0", id: 1, result: { items: [] } }));
69
+
70
+ await expect(pending).resolves.toEqual({ items: [] });
71
+ clock.advanceBy(50);
72
+ });
73
+ });
@@ -1,4 +1,6 @@
1
1
  import { reportBestEffortCleanupError } from "./cleanup-errors.js";
2
+ import { realTimerProvider } from "./timer-provider.js";
3
+ import type { TimerProvider } from "./timer-provider.js";
2
4
  import { INIT_TIMEOUT_MS, REQUEST_TIMEOUT_MS, STOP_HARD_KILL_TIMEOUT_MS, STOP_SIGKILL_GRACE_MS } from "./constants.js";
3
5
  import { LspConnectionClosedError, LspProcessExitedError, LspRequestTimeoutError } from "./errors.js";
4
6
  import { JsonRpcConnection } from "./json-rpc-connection.js";
@@ -12,6 +14,7 @@ export { createLspSpawnEnv } from "./transport-protocol.js";
12
14
  export interface LspClientTimeoutOptions {
13
15
  requestTimeoutMs?: number;
14
16
  initializeTimeoutMs?: number;
17
+ timerProvider?: TimerProvider;
15
18
  }
16
19
 
17
20
  type WorkspaceApplyEditHandler = (params: unknown) => Promise<WorkspaceApplyEditResponse>;
@@ -35,6 +38,7 @@ export class LspClientTransport {
35
38
  protected readonly diagnosticsStore = new Map<string, Diagnostic[]>();
36
39
  protected readonly requestTimeoutMs: number;
37
40
  protected readonly initializeTimeoutMs: number;
41
+ protected readonly timerProvider: TimerProvider;
38
42
  private workspaceApplyEditHandler: WorkspaceApplyEditHandler | null = null;
39
43
  private diagnosticPullSupported = false;
40
44
  private documentFormattingSupported = false;
@@ -46,6 +50,7 @@ export class LspClientTransport {
46
50
  ) {
47
51
  this.requestTimeoutMs = timeouts.requestTimeoutMs ?? REQUEST_TIMEOUT_MS;
48
52
  this.initializeTimeoutMs = timeouts.initializeTimeoutMs ?? INIT_TIMEOUT_MS;
53
+ this.timerProvider = timeouts.timerProvider ?? realTimerProvider;
49
54
  }
50
55
 
51
56
  pid(): number | undefined {
@@ -187,7 +192,7 @@ export class LspClientTransport {
187
192
  const options = args[1];
188
193
  const timeoutMs = options?.timeoutMs ?? this.requestTimeoutMs;
189
194
  const timeoutController = new AbortController();
190
- const timeoutHandle = setTimeout(() => {
195
+ const timeoutHandle = this.timerProvider.setTimeout(() => {
191
196
  const stderrTail = this.stderrBuffer.slice(-5).join("\n");
192
197
  timeoutController.abort(new LspRequestTimeoutError(method, stderrTail || undefined));
193
198
  }, timeoutMs);
@@ -213,7 +218,7 @@ export class LspClientTransport {
213
218
  }
214
219
  throw error;
215
220
  } finally {
216
- clearTimeout(timeoutHandle);
221
+ this.timerProvider.clearTimeout(timeoutHandle);
217
222
  combinedSignal.dispose();
218
223
  }
219
224
  }
@@ -1,8 +1,91 @@
1
- import { describe, expect, it } from "bun:test";
1
+ import { mkdirSync, mkdtempSync, realpathSync, rmSync, writeFileSync } from "node:fs";
2
+ import { tmpdir } from "node:os";
3
+ import { join } from "node:path";
4
+ import { pathToFileURL } from "node:url";
2
5
 
6
+ import { afterEach, describe, expect, it } from "bun:test";
7
+
8
+ import type { Diagnostic } from "./types.js";
3
9
  import { WorkspaceDocumentState } from "./workspace-document-state.js";
4
10
  import type { WorkspaceMutation } from "./workspace-edit.js";
5
11
 
12
+ const workspaces: string[] = [];
13
+
14
+ afterEach(() => {
15
+ while (workspaces.length > 0) {
16
+ const workspace = workspaces.pop();
17
+ if (workspace) rmSync(workspace, { recursive: true, force: true });
18
+ }
19
+ });
20
+
21
+ function makeDocuments(): WorkspaceDocumentState {
22
+ return new WorkspaceDocumentState(
23
+ async () => {},
24
+ () => {},
25
+ { versionlessPublishQuiescenceMs: 0 },
26
+ );
27
+ }
28
+
29
+ function makeSourceFile(directorySegment: string): string {
30
+ const workspace = realpathSync(mkdtempSync(join(tmpdir(), "lsp-document-uri-")));
31
+ workspaces.push(workspace);
32
+ const directory = join(workspace, directorySegment);
33
+ mkdirSync(directory, { recursive: true });
34
+ const source = join(directory, "page.ts");
35
+ writeFileSync(source, "const value: number = 'text';\n", "utf-8");
36
+ return source;
37
+ }
38
+
39
+ const diagnostic: Diagnostic = {
40
+ range: { start: { line: 0, character: 6 }, end: { line: 0, character: 11 } },
41
+ severity: 1,
42
+ message: "Type 'string' is not assignable to type 'number'.",
43
+ };
44
+
45
+ describe("WorkspaceDocumentState published diagnostics under a re-encoded uri", () => {
46
+ it("#given a server that percent-encodes a path segment #when it publishes #then the diagnostics reach the open document", async () => {
47
+ const documents = makeDocuments();
48
+ const source = makeSourceFile("(marketing)");
49
+ await documents.openFile(source);
50
+ const snapshot = documents.captureDiagnosticSnapshot(source);
51
+ expect(snapshot).not.toBeNull();
52
+ const publishedUri = snapshot!.uri.replaceAll("(", "%28").replaceAll(")", "%29");
53
+ expect(publishedUri).not.toBe(snapshot!.uri);
54
+
55
+ documents.recordPublishedDiagnostics({ uri: publishedUri, diagnostics: [diagnostic], version: snapshot!.version });
56
+
57
+ expect(documents.getStoredDiagnostics(snapshot!.uri)).toEqual([diagnostic]);
58
+ expect(documents.resolvePushDiagnostics(snapshot!)).toEqual({ status: "ready", diagnostics: [diagnostic] });
59
+ });
60
+
61
+ it.skipIf(process.platform !== "win32")(
62
+ "#given a server that lowercases and percent-encodes the drive #when it publishes #then the diagnostics reach the open document",
63
+ async () => {
64
+ const documents = makeDocuments();
65
+ const source = makeSourceFile("routes");
66
+ await documents.openFile(source);
67
+ const snapshot = documents.captureDiagnosticSnapshot(source);
68
+ expect(snapshot).not.toBeNull();
69
+ const publishedUri = snapshot!.uri.replace(/^file:\/\/\/([A-Za-z]):/, (_match, drive: string) => `file:///${drive.toLowerCase()}%3A`);
70
+ expect(publishedUri).not.toBe(snapshot!.uri);
71
+
72
+ documents.recordPublishedDiagnostics({ uri: publishedUri, diagnostics: [diagnostic], version: snapshot!.version });
73
+
74
+ expect(documents.getStoredDiagnostics(snapshot!.uri)).toEqual([diagnostic]);
75
+ expect(documents.resolvePushDiagnostics(snapshot!)).toEqual({ status: "ready", diagnostics: [diagnostic] });
76
+ },
77
+ );
78
+
79
+ it("#given a document opened from a path the client already encodes #when the server echoes it verbatim #then the outgoing uri is unchanged", async () => {
80
+ const documents = makeDocuments();
81
+ const source = makeSourceFile("plain");
82
+ await documents.openFile(source);
83
+ const snapshot = documents.captureDiagnosticSnapshot(source);
84
+
85
+ expect(snapshot?.uri).toBe(pathToFileURL(source).href);
86
+ });
87
+ });
88
+
6
89
  describe("WorkspaceDocumentState watched-file bounds", () => {
7
90
  it("#given more closed-file mutations than one batch #when synchronized #then every notification stays bounded", async () => {
8
91
  const notifications: unknown[] = [];
@@ -5,6 +5,7 @@ import { pathToFileURL } from "node:url";
5
5
  import { effectiveExtension } from "./effective-extension.js";
6
6
  import { getLanguageId } from "./language-mappings.js";
7
7
  import type { Diagnostic } from "./types.js";
8
+ import type { TimerProvider } from "./timer-provider.js";
8
9
  import type { PlannedWorkspaceOperation, WorkspaceMutation, WorkspaceMutationDelta } from "./workspace-edit-types.js";
9
10
 
10
11
  const WATCHED_FILE_BATCH_SIZE = 128;
@@ -76,6 +77,7 @@ export type PushDiagnosticsResolution =
76
77
 
77
78
  export interface WorkspaceDocumentStateOptions {
78
79
  readonly now?: () => number;
80
+ readonly timerProvider?: TimerProvider;
79
81
  readonly versionlessPublishQuiescenceMs?: number;
80
82
  }
81
83
 
@@ -88,6 +90,17 @@ function canonicalPath(filePath: string): string {
88
90
  }
89
91
  }
90
92
 
93
+ function normalizeDocumentUri(uri: string): string {
94
+ let decoded = uri;
95
+ try {
96
+ decoded = decodeURIComponent(uri);
97
+ } catch {
98
+ decoded = uri;
99
+ }
100
+ if (process.platform !== "win32") return decoded;
101
+ return decoded.replace(/^(file:\/\/\/)([a-z]):/, (_match, prefix: string, drive: string) => `${prefix}${drive.toUpperCase()}:`);
102
+ }
103
+
91
104
  function isSameOrDescendant(candidate: string, parent: string): boolean {
92
105
  const suffix = relative(parent, candidate);
93
106
  return suffix === "" || (!suffix.startsWith("..") && suffix !== "..");
@@ -103,6 +116,7 @@ export class WorkspaceDocumentState {
103
116
  private readonly openByUri = new Map<string, OpenDocumentState>();
104
117
  private readonly openPromises = new Map<string, Promise<void>>();
105
118
  private readonly now: () => number;
119
+ private readonly timerProvider: TimerProvider;
106
120
  private readonly versionlessPublishQuiescenceMs: number;
107
121
 
108
122
  constructor(
@@ -110,7 +124,12 @@ export class WorkspaceDocumentState {
110
124
  private readonly clearDiagnostics: (uri: string) => void,
111
125
  options: WorkspaceDocumentStateOptions = {},
112
126
  ) {
113
- this.now = options.now ?? (() => Date.now());
127
+ this.timerProvider = options.timerProvider ?? {
128
+ now: options.now ?? (() => Date.now()),
129
+ setTimeout: (callback, delayMs) => setTimeout(callback, delayMs),
130
+ clearTimeout: (handle) => clearTimeout(handle),
131
+ };
132
+ this.now = () => this.timerProvider.now();
114
133
  this.versionlessPublishQuiescenceMs =
115
134
  options.versionlessPublishQuiescenceMs ?? DEFAULT_VERSIONLESS_PUBLISH_QUIESCENCE_MS;
116
135
  }
@@ -135,7 +154,7 @@ export class WorkspaceDocumentState {
135
154
  }
136
155
 
137
156
  getStoredDiagnostics(uri: string): readonly Diagnostic[] {
138
- const state = this.openByUri.get(uri);
157
+ const state = this.openByUri.get(normalizeDocumentUri(uri));
139
158
  if (!state) return [];
140
159
  return state.lastPublish?.diagnostics ?? state.pullCache?.diagnostics ?? [];
141
160
  }
@@ -163,13 +182,13 @@ export class WorkspaceDocumentState {
163
182
  }
164
183
 
165
184
  getPullCache(snapshot: DiagnosticSnapshot): PullDiagnosticsCacheHit | null {
166
- const state = this.openByUri.get(snapshot.uri);
185
+ const state = this.openByUri.get(normalizeDocumentUri(snapshot.uri));
167
186
  if (!state?.pullCache || state.pullCache.documentVersion !== snapshot.version) return null;
168
187
  return state.pullCache;
169
188
  }
170
189
 
171
190
  recordPullDiagnostics(snapshot: DiagnosticSnapshot, report: PullDiagnosticsReport): void {
172
- const state = this.openByUri.get(snapshot.uri);
191
+ const state = this.openByUri.get(normalizeDocumentUri(snapshot.uri));
173
192
  if (!state) return;
174
193
  state.pullCache = {
175
194
  documentVersion: snapshot.version,
@@ -183,7 +202,7 @@ export class WorkspaceDocumentState {
183
202
  readonly diagnostics: readonly Diagnostic[];
184
203
  readonly version?: number;
185
204
  }): void {
186
- const state = this.openByUri.get(params.uri);
205
+ const state = this.openByUri.get(normalizeDocumentUri(params.uri));
187
206
  if (!state) return;
188
207
  state.publishGeneration += 1;
189
208
  state.lastPublish = {
@@ -197,7 +216,7 @@ export class WorkspaceDocumentState {
197
216
  }
198
217
 
199
218
  resolvePushDiagnostics(snapshot: DiagnosticSnapshot): PushDiagnosticsResolution {
200
- const state = this.openByUri.get(snapshot.uri);
219
+ const state = this.openByUri.get(normalizeDocumentUri(snapshot.uri));
201
220
  if (!state?.lastPublish) return { status: "missing" };
202
221
  const publish = state.lastPublish;
203
222
  if (publish.version !== undefined) {
@@ -214,18 +233,18 @@ export class WorkspaceDocumentState {
214
233
  }
215
234
 
216
235
  waitForDiagnosticsActivity(snapshot: DiagnosticSnapshot, timeoutMs: number): Promise<void> {
217
- const state = this.openByUri.get(snapshot.uri);
236
+ const state = this.openByUri.get(normalizeDocumentUri(snapshot.uri));
218
237
  if (!state || timeoutMs <= 0) return Promise.resolve();
219
238
  return new Promise((resolveActivity) => {
220
239
  let settled = false;
221
240
  const finish = () => {
222
241
  if (settled) return;
223
242
  settled = true;
224
- clearTimeout(timer);
243
+ this.timerProvider.clearTimeout(timer);
225
244
  state.waiters.delete(finish);
226
245
  resolveActivity();
227
246
  };
228
- const timer = setTimeout(finish, timeoutMs);
247
+ const timer = this.timerProvider.setTimeout(finish, timeoutMs);
229
248
  if (typeof timer.unref === "function") timer.unref();
230
249
  state.waiters.add(finish);
231
250
  });
@@ -329,7 +348,7 @@ export class WorkspaceDocumentState {
329
348
  waiters: new Set(),
330
349
  };
331
350
  this.openDocuments.set(path, state);
332
- this.openByUri.set(state.uri, state);
351
+ this.openByUri.set(normalizeDocumentUri(state.uri), state);
333
352
  this.notifyWaiters(state);
334
353
  await this.sendNotification("textDocument/didOpen", {
335
354
  textDocument: { uri: state.uri, languageId: state.languageId, version: state.version, text },
@@ -356,7 +375,7 @@ export class WorkspaceDocumentState {
356
375
 
357
376
  private async closeDocument(state: OpenDocumentState): Promise<void> {
358
377
  this.openDocuments.delete(state.path);
359
- this.openByUri.delete(state.uri);
378
+ this.openByUri.delete(normalizeDocumentUri(state.uri));
360
379
  this.clearDiagnostics(state.uri);
361
380
  this.notifyWaiters(state);
362
381
  await this.sendNotification("textDocument/didClose", { textDocument: { uri: state.uri } });
@@ -1 +1,18 @@
1
- export const WORKSPACE_MARKERS = [".git", "package.json", "pyproject.toml", "Cargo.toml", "go.mod", "pom.xml", "build.gradle"] as const;
1
+ /** Repository marker: an ancestor containing this always wins over intermediate package markers. */
2
+ export const GIT_WORKSPACE_MARKER = ".git" as const;
3
+
4
+ /**
5
+ * Package/project markers. On their own they resolve to the NEAREST marked directory, but inside a
6
+ * request cwd they are only a fallback: a `.git` ancestor further up collapses nested package roots
7
+ * (one client per repository instead of one per monorepo package).
8
+ */
9
+ export const PROJECT_WORKSPACE_MARKERS = [
10
+ "package.json",
11
+ "pyproject.toml",
12
+ "Cargo.toml",
13
+ "go.mod",
14
+ "pom.xml",
15
+ "build.gradle",
16
+ ] as const;
17
+
18
+ export const WORKSPACE_MARKERS = [GIT_WORKSPACE_MARKER, ...PROJECT_WORKSPACE_MARKERS] as const;
@@ -71,6 +71,9 @@ export async function runMcpStdioServer(
71
71
  await runJsonRpcStdioServer({
72
72
  input,
73
73
  output,
74
+ // Idle stays disabled: hosts of this CLI (legacy opencode configs and
75
+ // standalone lsp-tools-mcp users) have no evidenced respawn for an exited
76
+ // stdio server; opencode in particular marks it failed without retry.
74
77
  idleTimeoutMs: 0,
75
78
  parentWatchdog: options.parentWatchdog ?? {},
76
79
  handler: (request) => runWithRequestContext(requestContext, () => handleLspMcpRequest(request)),