ultimate-pi 0.18.1 → 0.19.1

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 (325) hide show
  1. package/.agents/skills/harness-debate-plan/SKILL.md +1 -1
  2. package/.agents/skills/harness-decisions/SKILL.md +1 -2
  3. package/.agents/skills/harness-governor/SKILL.md +6 -5
  4. package/.agents/skills/web-retrieval/SKILL.md +163 -0
  5. package/.agents/skills/wiki-autoresearch/SKILL.md +6 -6
  6. package/.pi/PACKAGING.md +4 -4
  7. package/.pi/SYSTEM.md +75 -123
  8. package/.pi/agents/harness/incident-recorder.md +0 -1
  9. package/.pi/agents/harness/planning/decompose.md +0 -2
  10. package/.pi/agents/harness/planning/execution-plan-author.md +0 -2
  11. package/.pi/agents/harness/planning/hypothesis-validator.md +0 -2
  12. package/.pi/agents/harness/planning/hypothesis.md +0 -2
  13. package/.pi/agents/harness/planning/implementation-researcher.md +1 -3
  14. package/.pi/agents/harness/planning/plan-adversary.md +0 -2
  15. package/.pi/agents/harness/planning/plan-evaluator.md +1 -3
  16. package/.pi/agents/harness/planning/planning-context.md +0 -2
  17. package/.pi/agents/harness/planning/review-integrator.md +0 -2
  18. package/.pi/agents/harness/planning/sprint-contract-auditor.md +0 -2
  19. package/.pi/agents/harness/planning/stack-researcher.md +5 -3
  20. package/.pi/agents/harness/reviewing/adversary.md +0 -2
  21. package/.pi/agents/harness/reviewing/evaluator.md +0 -2
  22. package/.pi/agents/harness/reviewing/tie-breaker.md +0 -2
  23. package/.pi/agents/harness/running/executor.md +0 -2
  24. package/.pi/agents/harness/sentrux-bootstrap.md +0 -1
  25. package/.pi/agents/harness/sentrux-steward.md +0 -2
  26. package/.pi/agents/harness/trace-librarian.md +0 -1
  27. package/.pi/agents/harness/web-retrieval/web-answerer.md +35 -0
  28. package/.pi/agents/harness/web-retrieval/web-criteria-verifier.md +28 -0
  29. package/.pi/agents/harness/web-retrieval/web-gap-analyzer.md +31 -0
  30. package/.pi/agents/harness/web-retrieval/web-query-expander-fast.md +34 -0
  31. package/.pi/agents/harness/web-retrieval/web-query-expander.md +60 -0
  32. package/.pi/agents/harness/web-retrieval/web-summarizer.md +18 -0
  33. package/.pi/extensions/agt-kill-switch.ts +57 -0
  34. package/.pi/extensions/agt-prompt-guard.ts +32 -0
  35. package/.pi/extensions/custom-footer.ts +46 -145
  36. package/.pi/extensions/custom-header.ts +1 -1
  37. package/.pi/extensions/custom-system-prompt.ts +1 -1
  38. package/.pi/extensions/debate-orchestrator.ts +6 -6
  39. package/.pi/extensions/harness-ask-user.ts +7 -7
  40. package/.pi/extensions/harness-debate-tools.ts +26 -42
  41. package/.pi/extensions/harness-lens.ts +94 -0
  42. package/.pi/extensions/harness-plan-approval.ts +11 -11
  43. package/.pi/extensions/harness-run-context.ts +1070 -876
  44. package/.pi/extensions/harness-subagent-governance.ts +8 -0
  45. package/.pi/extensions/harness-subagent-submit.ts +34 -163
  46. package/.pi/extensions/harness-subagents.ts +3 -3
  47. package/.pi/extensions/harness-telemetry.ts +2 -2
  48. package/.pi/extensions/harness-web-guard.ts +2 -1
  49. package/.pi/extensions/harness-web-tools.ts +691 -53
  50. package/.pi/extensions/policy-gate.ts +25 -5
  51. package/.pi/extensions/sentrux-rules-sync.ts +1 -1
  52. package/.pi/extensions/subagent-governance.ts +92 -0
  53. package/.pi/extensions/trace-recorder.ts +1 -1
  54. package/.pi/extensions/{ultimate-pi-vcc.ts → vcc-compaction.ts} +1 -1
  55. package/.pi/harness/README.md +6 -2
  56. package/.pi/harness/agents.manifest.json +46 -25
  57. package/.pi/harness/agents.policy.yaml +309 -0
  58. package/.pi/harness/docs/adrs/0030-inhouse-vcc-compaction.md +1 -1
  59. package/.pi/harness/docs/adrs/0035-plan-phase-review-gate.md +1 -1
  60. package/.pi/harness/docs/adrs/0045-harness-lens-minimal-contract.md +49 -0
  61. package/.pi/harness/docs/adrs/0046-agt-policy-engine.md +51 -0
  62. package/.pi/harness/docs/adrs/0047-agt-layered-security.md +39 -0
  63. package/.pi/harness/docs/adrs/0048-tool-call-hook-order.md +25 -0
  64. package/.pi/harness/docs/adrs/0049-agents-policy-manifest.md +36 -0
  65. package/.pi/harness/docs/adrs/0050-agentic-web-retrieval-stack.md +46 -0
  66. package/.pi/harness/docs/adrs/README.md +5 -0
  67. package/.pi/harness/docs/harness-web-search.md +97 -0
  68. package/.pi/harness/env.harness.template +9 -1
  69. package/.pi/harness/evolution/README.md +1 -2
  70. package/.pi/harness/examples/agents.policy.project.yaml +19 -0
  71. package/.pi/harness/examples/policies/custom-deny-bash.yaml +9 -0
  72. package/.pi/harness/examples/web-heuristic-angles.project.yaml +22 -0
  73. package/.pi/harness/policies/bash-denylists.yaml +5 -0
  74. package/.pi/harness/policies/defaults.yaml +51 -0
  75. package/.pi/harness/policies/orchestrator.yaml +18 -0
  76. package/.pi/harness/policies/phases.yaml +10 -0
  77. package/.pi/harness/policies/roles.yaml +5 -0
  78. package/.pi/harness/policies/web-guard.yaml +5 -0
  79. package/.pi/harness/policies/workflow-sequences.yaml +9 -0
  80. package/.pi/harness/sentrux/architecture.manifest.json +26 -4
  81. package/.pi/harness/specs/observation.schema.json +2 -1
  82. package/.pi/harness/web-heuristic-angles.json +278 -0
  83. package/.pi/harness/web-heuristic-angles.yaml +182 -0
  84. package/.pi/lib/agents-policy.d.mts +70 -0
  85. package/.pi/lib/agents-policy.mjs +331 -0
  86. package/.pi/lib/agents-policy.ts +19 -0
  87. package/.pi/lib/agt/audit-run-sink.ts +52 -0
  88. package/.pi/lib/agt/build-evaluation-context.ts +285 -0
  89. package/.pi/lib/agt/config.ts +28 -0
  90. package/.pi/lib/agt/delegation.ts +69 -0
  91. package/.pi/lib/agt/evaluate-policy.ts +56 -0
  92. package/.pi/lib/agt/identity-registry.ts +41 -0
  93. package/.pi/lib/agt/index.ts +55 -0
  94. package/.pi/lib/agt/kill-switch-state.ts +11 -0
  95. package/.pi/lib/agt/legacy-evaluate.ts +101 -0
  96. package/.pi/lib/agt/policy-engine.ts +154 -0
  97. package/.pi/lib/agt/rings.ts +21 -0
  98. package/.pi/lib/agt/sre-hooks.ts +45 -0
  99. package/.pi/lib/agt/trust-run-store.ts +26 -0
  100. package/.pi/lib/agt/workflow-history.ts +29 -0
  101. package/.pi/lib/agt-governance-active.ts +14 -0
  102. package/.pi/lib/agt-tool-guard.ts +78 -0
  103. package/.pi/lib/ask-user/dialog.ts +314 -0
  104. package/.pi/{extensions/lib → lib}/debate-bus-core.ts +10 -10
  105. package/.pi/{extensions/lib → lib}/debate-bus-state.ts +1 -1
  106. package/.pi/{extensions/lib → lib}/extension-load-guard.ts +13 -2
  107. package/.pi/lib/harness-agt-tool-guard.ts +5 -0
  108. package/.pi/{extensions/lib → lib}/harness-artifact-gate.ts +1 -1
  109. package/.pi/lib/harness-debate-core-deps.ts +14 -0
  110. package/.pi/lib/harness-debate-workflow-deps.ts +43 -0
  111. package/.pi/lib/harness-lens/.gitattributes +1 -0
  112. package/.pi/lib/harness-lens/clients/edit-autopatch.ts +88 -0
  113. package/.pi/lib/harness-lens/clients/file-kinds.ts +380 -0
  114. package/.pi/lib/harness-lens/clients/file-time.ts +215 -0
  115. package/.pi/lib/harness-lens/clients/file-utils.ts +484 -0
  116. package/.pi/lib/harness-lens/clients/format-service.ts +276 -0
  117. package/.pi/lib/harness-lens/clients/formatters.ts +1000 -0
  118. package/.pi/lib/harness-lens/clients/git-guard.ts +31 -0
  119. package/.pi/lib/harness-lens/clients/indent-retarget.ts +90 -0
  120. package/.pi/lib/harness-lens/clients/installer/index.ts +2368 -0
  121. package/.pi/lib/harness-lens/clients/latency-logger.ts +80 -0
  122. package/.pi/lib/harness-lens/clients/lens-config.ts +43 -0
  123. package/.pi/lib/harness-lens/clients/lens-events.ts +164 -0
  124. package/.pi/lib/harness-lens/clients/lsp/aggregation.ts +91 -0
  125. package/.pi/lib/harness-lens/clients/lsp/client.ts +1466 -0
  126. package/.pi/lib/harness-lens/clients/lsp/config.ts +216 -0
  127. package/.pi/lib/harness-lens/clients/lsp/edits.ts +297 -0
  128. package/.pi/lib/harness-lens/clients/lsp/index.ts +1355 -0
  129. package/.pi/lib/harness-lens/clients/lsp/interactive-install.ts +424 -0
  130. package/.pi/lib/harness-lens/clients/lsp/language.ts +223 -0
  131. package/.pi/lib/harness-lens/clients/lsp/launch.ts +939 -0
  132. package/.pi/lib/harness-lens/clients/lsp/lsp-index.ts +11 -0
  133. package/.pi/lib/harness-lens/clients/lsp/path-utils.ts +12 -0
  134. package/.pi/lib/harness-lens/clients/lsp/server-strategies.ts +81 -0
  135. package/.pi/lib/harness-lens/clients/lsp/server.ts +1971 -0
  136. package/.pi/lib/harness-lens/clients/path-utils.ts +182 -0
  137. package/.pi/lib/harness-lens/clients/pipeline.ts +360 -0
  138. package/.pi/lib/harness-lens/clients/project-profile.ts +117 -0
  139. package/.pi/lib/harness-lens/clients/runtime-agent-end.ts +112 -0
  140. package/.pi/lib/harness-lens/clients/runtime-config.ts +33 -0
  141. package/.pi/lib/harness-lens/clients/runtime-coordinator.ts +186 -0
  142. package/.pi/lib/harness-lens/clients/runtime-tool-result.ts +171 -0
  143. package/.pi/lib/harness-lens/clients/safe-spawn.ts +339 -0
  144. package/.pi/lib/harness-lens/clients/secrets-scanner.ts +214 -0
  145. package/.pi/lib/harness-lens/clients/tool-policy.ts +2072 -0
  146. package/.pi/lib/harness-lens/clients/types.ts +59 -0
  147. package/.pi/lib/harness-lens/clients/widget-state.ts +283 -0
  148. package/.pi/lib/harness-lens/index.ts +532 -0
  149. package/.pi/lib/harness-lens/tools/lsp-diagnostics.ts +706 -0
  150. package/.pi/lib/harness-lens/tools/lsp-navigation.ts +1246 -0
  151. package/.pi/{extensions/lib → lib}/harness-posthog.ts +3 -0
  152. package/.pi/lib/harness-run-context-responses.ts +9 -0
  153. package/.pi/lib/harness-run-context.ts +0 -2
  154. package/.pi/{extensions/lib/spawn-policy.ts → lib/harness-spawn-policy.ts} +1 -0
  155. package/.pi/{extensions/lib → lib}/harness-spawn-topology.ts +1 -1
  156. package/.pi/lib/harness-subagent-auth.ts +81 -0
  157. package/.pi/{extensions/lib → lib}/harness-subagent-precheck.ts +10 -7
  158. package/.pi/{extensions/lib → lib}/harness-subagent-submit-pipeline.ts +3 -3
  159. package/.pi/lib/harness-subagent-submit-register.ts +163 -0
  160. package/.pi/{extensions/lib → lib}/harness-subagent-submit-registry.ts +1 -37
  161. package/.pi/{extensions/lib → lib}/harness-subagents-bridge.ts +74 -14
  162. package/.pi/{extensions/lib → lib}/harness-subprocess-bootstrap.ts +1 -1
  163. package/.pi/lib/harness-web/artifacts.ts +200 -0
  164. package/.pi/lib/harness-web/cache.ts +369 -0
  165. package/.pi/{extensions/lib → lib}/harness-web/run-cli.ts +42 -2
  166. package/.pi/{extensions/lib → lib}/plan-approval/create-plan.ts +2 -2
  167. package/.pi/{extensions/lib → lib}/plan-approval/format-plan.ts +2 -2
  168. package/.pi/{extensions/lib → lib}/plan-approval/plan-review.ts +162 -201
  169. package/.pi/{extensions/lib → lib}/plan-approval/render.ts +1 -1
  170. package/.pi/{extensions/lib → lib}/plan-approval/resolve-disk.ts +2 -2
  171. package/.pi/{extensions/lib → lib}/plan-approval/types.ts +1 -1
  172. package/.pi/{extensions/lib → lib}/plan-approval/validate.ts +3 -3
  173. package/.pi/{extensions/lib → lib}/plan-debate-envelope.ts +1 -1
  174. package/.pi/{extensions/lib → lib}/plan-debate-gate.ts +1 -1
  175. package/.pi/{extensions/lib → lib}/plan-debate-lane.ts +1 -4
  176. package/.pi/{extensions/lib → lib}/plan-messenger.ts +1 -1
  177. package/.pi/prompts/harness-plan.md +2 -1
  178. package/.pi/prompts/harness-setup.md +40 -65
  179. package/.pi/scripts/README.md +2 -5
  180. package/.pi/scripts/gen-web-heuristic-angles-json.mjs +24 -0
  181. package/.pi/scripts/generate-agents-policy-yaml.mjs +148 -0
  182. package/.pi/scripts/harness-agents-manifest.mjs +60 -3
  183. package/.pi/scripts/harness-agt-doctor.ts +36 -0
  184. package/.pi/scripts/harness-cli-verify.sh +14 -2
  185. package/.pi/scripts/harness-verify.mjs +191 -39
  186. package/.pi/scripts/harness-web-policy-guard.mjs +3 -3
  187. package/.pi/scripts/harness-web.py +218 -15
  188. package/.pi/scripts/harness_web/deep_search.py +55 -0
  189. package/.pi/scripts/harness_web/evidence_bundle.py +47 -0
  190. package/.pi/scripts/harness_web/find_similar.py +88 -0
  191. package/.pi/scripts/harness_web/heuristic_angles_shipped.py +85 -0
  192. package/.pi/scripts/harness_web/heuristic_config.py +251 -0
  193. package/.pi/scripts/harness_web/highlights.py +47 -0
  194. package/.pi/scripts/harness_web/multi_search.py +59 -0
  195. package/.pi/scripts/harness_web/output.py +24 -0
  196. package/.pi/scripts/harness_web/query_angles.py +116 -0
  197. package/.pi/scripts/harness_web/rank.py +163 -0
  198. package/.pi/scripts/harness_web/scrape.py +30 -0
  199. package/.pi/scripts/tests/test_harness_web_heuristic_config.py +132 -0
  200. package/.pi/scripts/tests/test_harness_web_query_angles.py +45 -0
  201. package/.pi/scripts/tests/test_harness_web_rank.py +56 -0
  202. package/.pi/scripts/validate-plan-dag.mjs +65 -74
  203. package/.pi/scripts/vendor-pi-vcc-settings.stub.ts +2 -2
  204. package/.pi/scripts/vendor-sync-pi-vcc.sh +1 -1
  205. package/.pi/skills/architecture/broker-domain/SKILL.md +65 -0
  206. package/.pi/skills/architecture/cqrs/SKILL.md +63 -0
  207. package/.pi/skills/architecture/event-driven/SKILL.md +60 -0
  208. package/.pi/skills/architecture/hexagonal-ports-adapters/SKILL.md +66 -0
  209. package/.pi/skills/architecture/layered/SKILL.md +68 -0
  210. package/.pi/skills/architecture/microkernel/SKILL.md +62 -0
  211. package/.pi/skills/architecture/microservices/SKILL.md +64 -0
  212. package/.pi/skills/architecture/modular-monolith/SKILL.md +65 -0
  213. package/.pi/skills/architecture/orchestration-driven-soa/SKILL.md +61 -0
  214. package/.pi/skills/architecture/pipeline/SKILL.md +63 -0
  215. package/.pi/skills/architecture/service-based/SKILL.md +64 -0
  216. package/.pi/skills/architecture/service-mesh/SKILL.md +60 -0
  217. package/.pi/skills/architecture/space-based/SKILL.md +60 -0
  218. package/.pi/skills/ast-grep/SKILL.md +40 -321
  219. package/.pi/skills/delivery/debugging-discipline/SKILL.md +36 -0
  220. package/.pi/skills/delivery/documentation-update/SKILL.md +33 -0
  221. package/.pi/skills/delivery/requirements-to-implementation/SKILL.md +34 -0
  222. package/.pi/skills/delivery/risk-based-verification/SKILL.md +43 -0
  223. package/.pi/skills/delivery/tradeoff-analysis/SKILL.md +34 -0
  224. package/.pi/skills/engineering/api-contract-design/SKILL.md +38 -0
  225. package/.pi/skills/engineering/cohesion-coupling/SKILL.md +43 -0
  226. package/.pi/skills/engineering/complexity-control/SKILL.md +31 -0
  227. package/.pi/skills/engineering/defensive-programming/SKILL.md +38 -0
  228. package/.pi/skills/engineering/dependency-management/SKILL.md +29 -0
  229. package/.pi/skills/engineering/domain-modeling/SKILL.md +32 -0
  230. package/.pi/skills/engineering/error-handling/SKILL.md +37 -0
  231. package/.pi/skills/engineering/legacy-code-seams/SKILL.md +35 -0
  232. package/.pi/skills/engineering/naming-and-intent/SKILL.md +29 -0
  233. package/.pi/skills/engineering/refactoring-safe-evolution/SKILL.md +35 -0
  234. package/.pi/skills/engineering/routine-function-design/SKILL.md +34 -0
  235. package/.pi/skills/engineering/small-change-discipline/SKILL.md +35 -0
  236. package/.pi/skills/lsp-navigation/SKILL.md +89 -0
  237. package/.pi/skills/quality/code-review-self-check/SKILL.md +35 -0
  238. package/.pi/skills/quality/privacy-data-handling/SKILL.md +26 -0
  239. package/.pi/skills/quality/security-review/SKILL.md +34 -0
  240. package/.pi/skills/quality/test-strategy/SKILL.md +33 -0
  241. package/.pi/skills/quality/testability-design/SKILL.md +33 -0
  242. package/.pi/skills/systems/concurrency-safety/SKILL.md +32 -0
  243. package/.pi/skills/systems/data-modeling-migrations/SKILL.md +31 -0
  244. package/.pi/skills/systems/observability-instrumentation/SKILL.md +32 -0
  245. package/.pi/skills/systems/performance-measurement/SKILL.md +35 -0
  246. package/.pi/skills/systems/reliability-design/SKILL.md +32 -0
  247. package/.sentrux/rules.toml +20 -4
  248. package/AGENTS.md +7 -2
  249. package/CHANGELOG.md +20 -0
  250. package/README.md +3 -12
  251. package/THIRD_PARTY_NOTICES.md +12 -21
  252. package/package.json +17 -7
  253. package/vendor/pi-subagents/src/agents.ts +45 -1
  254. package/vendor/pi-subagents/src/subagents.ts +866 -811
  255. package/vendor/pi-vcc/src/core/brief.ts +68 -99
  256. package/vendor/pi-vcc/src/core/settings.ts +2 -2
  257. package/.agents/skills/caveman/SKILL.md +0 -67
  258. package/.agents/skills/scrapling-web/SKILL.md +0 -98
  259. package/.pi/agents/harness/meta-optimizer.md +0 -36
  260. package/.pi/extensions/00-posthog-network-bootstrap.ts +0 -11
  261. package/.pi/extensions/lib/ask-user/dialog.ts +0 -260
  262. package/.pi/extensions/lib/harness-subagent-auth.ts +0 -207
  263. package/.pi/extensions/lib/harness-subagent-policy.ts +0 -236
  264. package/.pi/extensions/pi-model-router-harness.ts +0 -42
  265. package/.pi/harness/evolution/meta-optimizer.mjs +0 -99
  266. package/.pi/harness/specs/router-tuning-proposal.schema.json +0 -114
  267. package/.pi/model-router.example.json +0 -36
  268. package/.pi/prompts/harness-critic.md +0 -10
  269. package/.pi/prompts/harness-eval.md +0 -10
  270. package/.pi/prompts/harness-router-tune.md +0 -52
  271. package/.pi/scripts/harness-generate-model-router.mjs +0 -327
  272. package/.pi/scripts/harness-model-router-routing.test.mjs +0 -97
  273. package/.pi/scripts/harness-sync-model-router.mjs +0 -97
  274. package/.pi/scripts/harness_web/__pycache__/__init__.cpython-314.pyc +0 -0
  275. package/.pi/scripts/harness_web/__pycache__/config.cpython-314.pyc +0 -0
  276. package/.pi/scripts/harness_web/__pycache__/output.cpython-314.pyc +0 -0
  277. package/.pi/scripts/harness_web/__pycache__/scrape.cpython-314.pyc +0 -0
  278. package/.pi/scripts/harness_web/__pycache__/search.cpython-314.pyc +0 -0
  279. package/.pi/scripts/harness_web/__pycache__/search_ddg.cpython-314.pyc +0 -0
  280. package/.pi/scripts/harness_web/__pycache__/search_searxng.cpython-314.pyc +0 -0
  281. package/.pi/scripts/vendor-sync-pi-model-router.sh +0 -47
  282. package/vendor/pi-model-router/.prettierignore +0 -4
  283. package/vendor/pi-model-router/.prettierrc +0 -5
  284. package/vendor/pi-model-router/AGENTS.md +0 -39
  285. package/vendor/pi-model-router/LICENSE +0 -21
  286. package/vendor/pi-model-router/README.md +0 -99
  287. package/vendor/pi-model-router/UPSTREAM_PIN.md +0 -10
  288. package/vendor/pi-model-router/docs/ARCHITECTURE.md +0 -54
  289. package/vendor/pi-model-router/extensions/commands.ts +0 -720
  290. package/vendor/pi-model-router/extensions/config.ts +0 -348
  291. package/vendor/pi-model-router/extensions/constants.ts +0 -1
  292. package/vendor/pi-model-router/extensions/index.ts +0 -478
  293. package/vendor/pi-model-router/extensions/provider.ts +0 -580
  294. package/vendor/pi-model-router/extensions/routing.ts +0 -564
  295. package/vendor/pi-model-router/extensions/state.ts +0 -52
  296. package/vendor/pi-model-router/extensions/types.ts +0 -95
  297. package/vendor/pi-model-router/extensions/ui.ts +0 -144
  298. package/vendor/pi-model-router/model-router.example.json +0 -48
  299. package/vendor/pi-model-router/package.json +0 -48
  300. package/vendor/pi-model-router/tsconfig.json +0 -16
  301. /package/.pi/{prompts → harness/docs}/planning-rubrics.md +0 -0
  302. /package/.pi/{extensions/lib → lib}/ask-user/fallback.ts +0 -0
  303. /package/.pi/{extensions/lib → lib}/ask-user/render.ts +0 -0
  304. /package/.pi/{extensions/lib → lib}/ask-user/schema.ts +0 -0
  305. /package/.pi/{extensions/lib → lib}/ask-user/types.ts +0 -0
  306. /package/.pi/{extensions/lib → lib}/ask-user/validate-core.mjs +0 -0
  307. /package/.pi/{extensions/lib → lib}/ask-user/validate.ts +0 -0
  308. /package/.pi/{extensions/lib → lib}/harness-cocoindex-refresh.ts +0 -0
  309. /package/.pi/{extensions/lib → lib}/harness-paths.ts +0 -0
  310. /package/.pi/{extensions/lib → lib}/harness-spawn-budget.ts +0 -0
  311. /package/.pi/{extensions/lib → lib}/harness-vcc-settings.ts +0 -0
  312. /package/.pi/{extensions/lib → lib}/plan-approval/dialog.ts +0 -0
  313. /package/.pi/{extensions/lib → lib}/plan-approval/schema.ts +0 -0
  314. /package/.pi/{extensions/lib → lib}/plan-approval-readiness.ts +0 -0
  315. /package/.pi/{extensions/lib → lib}/plan-debate-eligibility.ts +0 -0
  316. /package/.pi/{extensions/lib → lib}/plan-debate-focus.ts +0 -0
  317. /package/.pi/{extensions/lib → lib}/plan-debate-id.ts +0 -0
  318. /package/.pi/{extensions/lib → lib}/plan-debate-lanes.ts +0 -0
  319. /package/.pi/{extensions/lib → lib}/plan-debate-round-status.ts +0 -0
  320. /package/.pi/{extensions/lib → lib}/plan-debate-write-guard.ts +0 -0
  321. /package/.pi/{extensions/lib → lib}/plan-review-gate.ts +0 -0
  322. /package/.pi/{extensions/lib → lib}/plan-review-integrator-rules.ts +0 -0
  323. /package/.pi/{extensions/lib → lib}/plan-scope-guard.ts +0 -0
  324. /package/.pi/{extensions/lib → lib}/posthog-client.ts +0 -0
  325. /package/.pi/{extensions/lib → lib}/posthog-node.d.ts +0 -0
@@ -0,0 +1,1466 @@
1
+ /**
2
+ * LSP Client for pi-lens
3
+ *
4
+ * Handles JSON-RPC communication with language servers:
5
+ * - Initialize/shutdown lifecycle
6
+ * - Document synchronization (didOpen, didChange)
7
+ * - Diagnostics with debouncing
8
+ * - Request/response handling
9
+ */
10
+
11
+ import { spawn as nodeSpawn } from "node:child_process";
12
+ import { EventEmitter } from "node:events";
13
+ import { existsSync } from "node:fs";
14
+ import { pathToFileURL } from "node:url";
15
+ import type { MessageConnection } from "vscode-jsonrpc";
16
+ import {
17
+ createMessageConnection,
18
+ StreamMessageReader,
19
+ StreamMessageWriter,
20
+ } from "vscode-jsonrpc/node.js";
21
+ import { logLatency } from "../latency-logger.js";
22
+
23
+ import type { LSPProcess } from "./launch.js";
24
+ import { normalizeMapKey, uriToPath } from "./path-utils.js";
25
+ import { getStrategy } from "./server-strategies.js";
26
+
27
+ // --- Types ---
28
+
29
+ export interface LSPDiagnostic {
30
+ severity: 1 | 2 | 3 | 4; // Error, Warning, Info, Hint
31
+ message: string;
32
+ range: {
33
+ start: { line: number; character: number };
34
+ end: { line: number; character: number };
35
+ };
36
+ code?: string | number;
37
+ source?: string;
38
+ }
39
+
40
+ export interface LSPLocation {
41
+ uri: string;
42
+ range: {
43
+ start: { line: number; character: number };
44
+ end: { line: number; character: number };
45
+ };
46
+ }
47
+
48
+ export interface LSPHover {
49
+ contents:
50
+ | string
51
+ | { kind: string; value: string }
52
+ | Array<string | { language: string; value: string }>;
53
+ range?: LSPLocation["range"];
54
+ }
55
+
56
+ export interface LSPSignatureHelp {
57
+ signatures: Array<{
58
+ label: string;
59
+ documentation?: string | { kind: string; value: string };
60
+ parameters?: Array<{
61
+ label: string | [number, number];
62
+ documentation?: string | { kind: string; value: string };
63
+ }>;
64
+ }>;
65
+ activeSignature?: number;
66
+ activeParameter?: number;
67
+ }
68
+
69
+ export interface LSPCodeAction {
70
+ title: string;
71
+ kind?: string;
72
+ diagnostics?: LSPDiagnostic[];
73
+ edit?: unknown;
74
+ command?: unknown;
75
+ data?: unknown;
76
+ isPreferred?: boolean;
77
+ disabled?: { reason?: string };
78
+ }
79
+
80
+ export interface LSPWorkspaceEdit {
81
+ changes?: Record<string, unknown[]>;
82
+ documentChanges?: unknown[];
83
+ changeAnnotations?: Record<string, unknown>;
84
+ }
85
+
86
+ export interface LSPWorkspaceDiagnosticsSupport {
87
+ advertised: boolean;
88
+ mode: "pull" | "push-only";
89
+ diagnosticProviderKind: string;
90
+ }
91
+
92
+ export interface LSPOperationSupport {
93
+ definition: boolean;
94
+ references: boolean;
95
+ hover: boolean;
96
+ signatureHelp: boolean;
97
+ documentSymbol: boolean;
98
+ workspaceSymbol: boolean;
99
+ codeAction: boolean;
100
+ rename: boolean;
101
+ implementation: boolean;
102
+ callHierarchy: boolean;
103
+ }
104
+
105
+ export interface LSPSymbol {
106
+ name: string;
107
+ kind: number;
108
+ location?: LSPLocation;
109
+ range?: LSPLocation["range"];
110
+ selectionRange?: LSPLocation["range"];
111
+ detail?: string;
112
+ children?: LSPSymbol[];
113
+ }
114
+
115
+ // --- Call Hierarchy Types ---
116
+
117
+ export interface LSPCallHierarchyItem {
118
+ name: string;
119
+ kind: number;
120
+ uri: string;
121
+ range: LSPLocation["range"];
122
+ selectionRange: LSPLocation["range"];
123
+ }
124
+
125
+ export interface LSPCallHierarchyIncomingCall {
126
+ from: LSPCallHierarchyItem;
127
+ fromRanges: LSPLocation["range"][];
128
+ }
129
+
130
+ export interface LSPCallHierarchyOutgoingCall {
131
+ to: LSPCallHierarchyItem;
132
+ fromRanges: LSPLocation["range"][];
133
+ }
134
+
135
+ export interface LSPClientInfo {
136
+ serverId: string;
137
+ root: string;
138
+ connection: MessageConnection;
139
+ /** Check if the connection is still alive */
140
+ isAlive: () => boolean;
141
+ /** True if the server process has exited or been killed */
142
+ processExited: () => boolean;
143
+ /** Last N lines of server stderr for diagnostics */
144
+ recentStderr: (lines?: number) => string;
145
+ /** Pre-request health check — returns error string if process is dead */
146
+ checkAlive: () => string | undefined;
147
+ notify: {
148
+ open(
149
+ filePath: string,
150
+ content: string,
151
+ languageId: string,
152
+ preserveDiagnostics?: boolean,
153
+ silent?: boolean,
154
+ ): Promise<void>;
155
+ change(filePath: string, content: string): Promise<void>;
156
+ };
157
+ getDiagnostics(filePath: string): LSPDiagnostic[];
158
+ waitForDiagnostics(filePath: string, timeoutMs?: number): Promise<void>;
159
+ /** Get all tracked diagnostics with timestamps (for cascade checking) */
160
+ getAllDiagnostics(): Map<string, { diags: LSPDiagnostic[]; ts: number }>;
161
+ pruneDiagnostics(
162
+ predicate: (
163
+ filePath: string,
164
+ ts: number,
165
+ diags: LSPDiagnostic[],
166
+ ) => boolean,
167
+ ): number;
168
+ /** Capability snapshot for workspace diagnostics support */
169
+ getWorkspaceDiagnosticsSupport(): LSPWorkspaceDiagnosticsSupport;
170
+ /** Capability snapshot for navigation/edit operations */
171
+ getOperationSupport(): LSPOperationSupport;
172
+ /** Go to definition — returns Location[] */
173
+ definition(
174
+ filePath: string,
175
+ line: number,
176
+ character: number,
177
+ ): Promise<LSPLocation[]>;
178
+ /** Find all references */
179
+ references(
180
+ filePath: string,
181
+ line: number,
182
+ character: number,
183
+ includeDeclaration?: boolean,
184
+ ): Promise<LSPLocation[]>;
185
+ /** Hover info at position */
186
+ hover(
187
+ filePath: string,
188
+ line: number,
189
+ character: number,
190
+ ): Promise<LSPHover | null>;
191
+ /** Signature help at position */
192
+ signatureHelp(
193
+ filePath: string,
194
+ line: number,
195
+ character: number,
196
+ ): Promise<LSPSignatureHelp | null>;
197
+ /** Symbols in a document */
198
+ documentSymbol(filePath: string): Promise<LSPSymbol[]>;
199
+ /** Workspace-wide symbol search */
200
+ workspaceSymbol(query: string): Promise<LSPSymbol[]>;
201
+ /** Available code actions at a range */
202
+ codeAction(
203
+ filePath: string,
204
+ line: number,
205
+ character: number,
206
+ endLine: number,
207
+ endCharacter: number,
208
+ ): Promise<LSPCodeAction[]>;
209
+ /** Rename symbol at position */
210
+ rename(
211
+ filePath: string,
212
+ line: number,
213
+ character: number,
214
+ newName: string,
215
+ ): Promise<LSPWorkspaceEdit | null>;
216
+ /** Go to implementation */
217
+ implementation(
218
+ filePath: string,
219
+ line: number,
220
+ character: number,
221
+ ): Promise<LSPLocation[]>;
222
+ /** Prepare call hierarchy at position */
223
+ prepareCallHierarchy(
224
+ filePath: string,
225
+ line: number,
226
+ character: number,
227
+ ): Promise<LSPCallHierarchyItem[]>;
228
+ /** Find incoming calls (callers) */
229
+ incomingCalls(
230
+ item: LSPCallHierarchyItem,
231
+ ): Promise<LSPCallHierarchyIncomingCall[]>;
232
+ /** Find outgoing calls (callees) */
233
+ outgoingCalls(
234
+ item: LSPCallHierarchyItem,
235
+ ): Promise<LSPCallHierarchyOutgoingCall[]>;
236
+ shutdown(): Promise<void>;
237
+ }
238
+
239
+ // --- Constants ---
240
+
241
+ const INITIALIZE_TIMEOUT_MS = positiveIntFromEnv(
242
+ "PI_LENS_LSP_INIT_TIMEOUT_MS",
243
+ 15_000,
244
+ ); // 15s — npx downloads are handled by ensureTool, not here
245
+ const NAV_REQUEST_TIMEOUT_MS = positiveIntFromEnv(
246
+ "PI_LENS_LSP_NAV_REQUEST_TIMEOUT_MS",
247
+ 10_000,
248
+ ); // 10s — per-request ceiling; prevents heavy servers (vue, svelte) from hanging
249
+ const DIAGNOSTICS_WAIT_TIMEOUT_MS = positiveIntFromEnv(
250
+ "PI_LENS_LSP_DIAGNOSTICS_WAIT_MS",
251
+ 10_000,
252
+ );
253
+ const PULL_DIAGNOSTICS_RETRY_INTERVAL_MS = positiveIntFromEnv(
254
+ "PI_LENS_LSP_PULL_RETRY_INTERVAL_MS",
255
+ 250,
256
+ );
257
+ const SHUTDOWN_REQUEST_TIMEOUT_MS = positiveIntFromEnv(
258
+ "PI_LENS_LSP_SHUTDOWN_TIMEOUT_MS",
259
+ 1000,
260
+ );
261
+
262
+ const LSP_CRASH_CODES = new Set([
263
+ "ERR_STREAM_DESTROYED",
264
+ "ERR_STREAM_WRITE_AFTER_END",
265
+ "EPIPE",
266
+ "ECONNRESET",
267
+ ]);
268
+
269
+ let crashGuardInstalled = false;
270
+
271
+ function isIgnorableLspRuntimeCrash(err: unknown): boolean {
272
+ if (!(err instanceof Error)) return false;
273
+ const code = (err as { code?: string }).code;
274
+ if (code && LSP_CRASH_CODES.has(code)) return true;
275
+ const msg = err.message.toLowerCase();
276
+ const stack = (err.stack ?? "").toLowerCase();
277
+ return (
278
+ msg.includes("stream") ||
279
+ msg.includes("write after end") ||
280
+ stack.includes("vscode-jsonrpc/lib/node/ril.js")
281
+ );
282
+ }
283
+
284
+ function installCrashGuard(): void {
285
+ if (crashGuardInstalled) return;
286
+ crashGuardInstalled = true;
287
+
288
+ process.on("uncaughtException", (err) => {
289
+ if (isIgnorableLspRuntimeCrash(err)) {
290
+ return;
291
+ }
292
+ throw err;
293
+ });
294
+
295
+ process.on("unhandledRejection", (reason) => {
296
+ if (isIgnorableLspRuntimeCrash(reason)) {
297
+ return;
298
+ }
299
+ throw reason instanceof Error ? reason : new Error(String(reason));
300
+ });
301
+ }
302
+
303
+ // --- Client State + Module-level helpers ---
304
+
305
+ export interface LSPClientState {
306
+ isConnected: boolean;
307
+ isDestroyed: boolean;
308
+ connectionDisposed: boolean;
309
+ lastError: Error | undefined;
310
+ readonly connection: MessageConnection;
311
+ readonly pushDiagnostics: Map<string, LSPDiagnostic[]>;
312
+ readonly pushDiagnosticTimestamps: Map<string, number>;
313
+ readonly documentPullDiagnostics: Map<string, LSPDiagnostic[]>;
314
+ readonly documentPullDiagnosticTimestamps: Map<string, number>;
315
+ readonly pendingDiagnostics: Map<string, ReturnType<typeof setTimeout>>;
316
+ readonly diagnosticEmitter: EventEmitter;
317
+ readonly documentVersions: Map<string, number>;
318
+ readonly openDocuments: Set<string>;
319
+ readonly pendingOpens: Set<string>;
320
+ /** Mutable: updated by applyDynamicCapabilities after registerCapability events */
321
+ workspaceDiagnosticsSupport: LSPWorkspaceDiagnosticsSupport;
322
+ /** Mutable: upgraded by applyDynamicCapabilities after registerCapability events */
323
+ operationSupport: LSPOperationSupport;
324
+ /** Baseline mode from static initResult — used to revert on unregister */
325
+ staticDiagnosticsMode: "pull" | "push-only";
326
+ /** Live dynamic registrations from client/registerCapability: id → method */
327
+ readonly dynamicRegistrations: Map<string, string>;
328
+ readonly serverId: string;
329
+ readonly root: string;
330
+ readonly lspProcess: LSPProcess;
331
+ }
332
+
333
+ function isClientAlive(state: LSPClientState): boolean {
334
+ return (
335
+ state.isConnected && !state.isDestroyed && !state.lspProcess.process.killed
336
+ );
337
+ }
338
+
339
+ function disposeClientConnection(state: LSPClientState): void {
340
+ if (state.connectionDisposed) return;
341
+ state.connectionDisposed = true;
342
+ try {
343
+ state.connection.dispose();
344
+ } catch {
345
+ // ignore
346
+ }
347
+ }
348
+
349
+ async function killProcessTree(
350
+ proc: { kill(signal?: NodeJS.Signals | number): boolean },
351
+ pid: number,
352
+ ): Promise<void> {
353
+ if (process.platform === "win32" && pid > 0) {
354
+ await new Promise<void>((resolve) => {
355
+ try {
356
+ // Absolute path avoids PATH-resolution: SystemRoot is set by Windows itself.
357
+ const taskkill = `${process.env.SystemRoot ?? "C:\\Windows"}\\System32\\taskkill.exe`;
358
+ const killer = nodeSpawn(taskkill, ["/F", "/T", "/PID", String(pid)], {
359
+ shell: false,
360
+ windowsHide: true,
361
+ });
362
+ killer.once("close", () => resolve());
363
+ killer.once("error", () => resolve());
364
+ } catch {
365
+ resolve();
366
+ }
367
+ });
368
+ return;
369
+ }
370
+
371
+ try {
372
+ proc.kill("SIGTERM");
373
+ // SIGTERM → 1.5s → SIGKILL escalation.
374
+ // SIGTERM alone can leave zombie processes if the server hangs.
375
+ await new Promise<void>((resolve) => setTimeout(resolve, 1500));
376
+ try {
377
+ if (!(proc as { killed?: boolean }).killed) {
378
+ proc.kill("SIGKILL");
379
+ }
380
+ } catch {
381
+ // best-effort
382
+ }
383
+ } catch {
384
+ // ignore
385
+ }
386
+ }
387
+
388
+ function mergeDiagnosticLists(
389
+ push: LSPDiagnostic[] | undefined,
390
+ pull: LSPDiagnostic[] | undefined,
391
+ ): LSPDiagnostic[] {
392
+ const merged: LSPDiagnostic[] = [];
393
+ const seen = new Set<string>();
394
+ for (const diagnostic of [...(push ?? []), ...(pull ?? [])]) {
395
+ const key = [
396
+ diagnostic.range.start.line,
397
+ diagnostic.range.start.character,
398
+ diagnostic.range.end.line,
399
+ diagnostic.range.end.character,
400
+ diagnostic.code ?? "",
401
+ diagnostic.source ?? "",
402
+ diagnostic.message,
403
+ ].join(":");
404
+ if (seen.has(key)) continue;
405
+ seen.add(key);
406
+ merged.push(diagnostic);
407
+ }
408
+ return merged;
409
+ }
410
+
411
+ function getMergedDiagnosticsForPath(
412
+ state: LSPClientState,
413
+ normalizedPath: string,
414
+ ): LSPDiagnostic[] {
415
+ const legacy = state as unknown as {
416
+ diagnostics?: Map<string, LSPDiagnostic[]>;
417
+ };
418
+ return mergeDiagnosticLists(
419
+ state.pushDiagnostics?.get(normalizedPath) ??
420
+ legacy.diagnostics?.get(normalizedPath),
421
+ state.documentPullDiagnostics?.get(normalizedPath),
422
+ );
423
+ }
424
+
425
+ function clearDiagnosticsForPath(
426
+ state: LSPClientState,
427
+ normalizedPath: string,
428
+ ): void {
429
+ const legacy = state as unknown as {
430
+ diagnostics?: Map<string, LSPDiagnostic[]>;
431
+ diagnosticTimestamps?: Map<string, number>;
432
+ };
433
+ state.pushDiagnostics?.delete(normalizedPath);
434
+ state.pushDiagnosticTimestamps?.delete(normalizedPath);
435
+ state.documentPullDiagnostics?.delete(normalizedPath);
436
+ state.documentPullDiagnosticTimestamps?.delete(normalizedPath);
437
+ legacy.diagnostics?.delete(normalizedPath);
438
+ legacy.diagnosticTimestamps?.delete(normalizedPath);
439
+ }
440
+
441
+ // Methods that can be registered dynamically and map to operationSupport keys
442
+ const DYNAMIC_OPERATION_METHOD_MAP: Record<string, keyof LSPOperationSupport> =
443
+ {
444
+ "textDocument/definition": "definition",
445
+ "textDocument/references": "references",
446
+ "textDocument/hover": "hover",
447
+ "textDocument/signatureHelp": "signatureHelp",
448
+ "textDocument/documentSymbol": "documentSymbol",
449
+ "workspace/symbol": "workspaceSymbol",
450
+ "textDocument/codeAction": "codeAction",
451
+ "textDocument/rename": "rename",
452
+ "textDocument/implementation": "implementation",
453
+ "textDocument/prepareCallHierarchy": "callHierarchy",
454
+ };
455
+
456
+ export function applyDynamicCapabilities(state: LSPClientState): void {
457
+ const registeredMethods = new Set(state.dynamicRegistrations.values());
458
+
459
+ const hasDynamicPull =
460
+ registeredMethods.has("textDocument/diagnostic") ||
461
+ registeredMethods.has("workspace/diagnostic");
462
+
463
+ if (hasDynamicPull) {
464
+ state.workspaceDiagnosticsSupport = {
465
+ advertised: true,
466
+ mode: "pull",
467
+ diagnosticProviderKind: "dynamic",
468
+ };
469
+ } else if (
470
+ state.staticDiagnosticsMode === "push-only" &&
471
+ state.workspaceDiagnosticsSupport.diagnosticProviderKind === "dynamic"
472
+ ) {
473
+ // Was only dynamically registered, now unregistered — revert to push-only
474
+ state.workspaceDiagnosticsSupport = {
475
+ advertised: false,
476
+ mode: "push-only",
477
+ diagnosticProviderKind: "none",
478
+ };
479
+ }
480
+
481
+ for (const [method, key] of Object.entries(DYNAMIC_OPERATION_METHOD_MAP)) {
482
+ if (registeredMethods.has(method)) {
483
+ state.operationSupport[key] = true;
484
+ }
485
+ }
486
+ }
487
+
488
+ function setupIncomingHandlers(
489
+ state: LSPClientState,
490
+ initialization: Record<string, unknown> | undefined,
491
+ ): void {
492
+ state.connection.onNotification(
493
+ "textDocument/publishDiagnostics",
494
+ (params: { uri: string; diagnostics?: LSPDiagnostic[] }) => {
495
+ const filePath = uriToPath(params.uri);
496
+ const normalizedPath = normalizeMapKey(filePath);
497
+ const newDiags: LSPDiagnostic[] = params.diagnostics || [];
498
+ const strategy = getStrategy(state.serverId);
499
+
500
+ // Seed on first push for servers whose first push is known complete.
501
+ // Bypasses the debounce timer entirely — resolves waiting promises immediately.
502
+ if (
503
+ strategy.seedFirstPush &&
504
+ !state.pushDiagnostics.has(normalizedPath)
505
+ ) {
506
+ state.pushDiagnostics.set(normalizedPath, newDiags);
507
+ state.pushDiagnosticTimestamps.set(normalizedPath, Date.now());
508
+ state.diagnosticEmitter.emit("diagnostics", normalizedPath);
509
+ return;
510
+ }
511
+
512
+ const existingTimer = state.pendingDiagnostics.get(normalizedPath);
513
+ if (existingTimer) clearTimeout(existingTimer);
514
+
515
+ const timer = setTimeout(() => {
516
+ state.pushDiagnostics.set(normalizedPath, newDiags);
517
+ state.pushDiagnosticTimestamps.set(normalizedPath, Date.now());
518
+ state.pendingDiagnostics.delete(normalizedPath);
519
+ state.diagnosticEmitter.emit("diagnostics", normalizedPath);
520
+ }, strategy.debounceMs);
521
+
522
+ state.pendingDiagnostics.set(normalizedPath, timer);
523
+ },
524
+ );
525
+
526
+ state.connection.onRequest("workspace/workspaceFolders", () => [
527
+ { name: "workspace", uri: pathToFileURL(state.root).href },
528
+ ]);
529
+ state.connection.onRequest(
530
+ "client/registerCapability",
531
+ async (params: {
532
+ registrations?: Array<{ id: string; method: string }>;
533
+ }) => {
534
+ for (const reg of params?.registrations ?? []) {
535
+ if (reg.id && reg.method) {
536
+ state.dynamicRegistrations.set(reg.id, reg.method);
537
+ }
538
+ }
539
+ applyDynamicCapabilities(state);
540
+ },
541
+ );
542
+ state.connection.onRequest(
543
+ "client/unregisterCapability",
544
+ async (params: { unregisterations?: Array<{ id: string }> }) => {
545
+ for (const unreg of params?.unregisterations ?? []) {
546
+ if (unreg.id) {
547
+ state.dynamicRegistrations.delete(unreg.id);
548
+ }
549
+ }
550
+ applyDynamicCapabilities(state);
551
+ },
552
+ );
553
+ state.connection.onRequest("workspace/configuration", async () => [
554
+ initialization ?? {},
555
+ ]);
556
+ state.connection.onRequest("window/workDoneProgress/create", async () => {});
557
+ }
558
+
559
+ function setupConnectionLifecycle(state: LSPClientState): void {
560
+ state.connection.onError(([error]: [Error, ...unknown[]]) => {
561
+ state.lastError = error instanceof Error ? error : new Error(String(error));
562
+ state.isConnected = false;
563
+ state.isDestroyed = true;
564
+ disposeClientConnection(state);
565
+ });
566
+
567
+ state.connection.onClose(() => {
568
+ state.isConnected = false;
569
+ state.isDestroyed = true;
570
+ disposeClientConnection(state);
571
+ });
572
+
573
+ state.lspProcess.process.on("exit", (code) => {
574
+ const wasConnected = state.isConnected;
575
+ state.isConnected = false;
576
+ state.isDestroyed = true;
577
+ disposeClientConnection(state);
578
+ if (wasConnected) {
579
+ logLatency({
580
+ type: "phase",
581
+ phase: "lsp_server_unexpected_exit",
582
+ filePath: state.root,
583
+ durationMs: 0,
584
+ metadata: {
585
+ serverId: state.serverId,
586
+ pid: state.lspProcess.pid,
587
+ exitCode: code ?? null,
588
+ },
589
+ });
590
+ }
591
+ });
592
+ }
593
+
594
+ async function clientRequestPullDiagnostics(
595
+ state: LSPClientState,
596
+ filePath: string,
597
+ ): Promise<number> {
598
+ if (!isClientAlive(state)) return 0;
599
+ const uri = pathToFileURL(filePath).href;
600
+ try {
601
+ const report = await safeSendRequest<{
602
+ kind?: string;
603
+ items?: LSPDiagnostic[];
604
+ relatedDocuments?: Record<string, { items?: LSPDiagnostic[] }>;
605
+ }>(state.connection, "textDocument/diagnostic", { textDocument: { uri } });
606
+
607
+ if (!report) return 0;
608
+
609
+ const normalizedPath = normalizeMapKey(filePath);
610
+ const primaryItems = report.items ?? [];
611
+ const now = Date.now();
612
+ state.documentPullDiagnostics.set(normalizedPath, primaryItems);
613
+ state.documentPullDiagnosticTimestamps.set(normalizedPath, now);
614
+ let totalCount = primaryItems.length;
615
+
616
+ if (report.relatedDocuments) {
617
+ for (const [relatedUri, related] of Object.entries(
618
+ report.relatedDocuments,
619
+ )) {
620
+ const relatedPath = uriToPath(relatedUri);
621
+ const relatedItems = related?.items ?? [];
622
+ state.documentPullDiagnostics.set(
623
+ normalizeMapKey(relatedPath),
624
+ relatedItems,
625
+ );
626
+ state.documentPullDiagnosticTimestamps.set(
627
+ normalizeMapKey(relatedPath),
628
+ now,
629
+ );
630
+ totalCount += relatedItems.length;
631
+ }
632
+ }
633
+
634
+ state.diagnosticEmitter.emit("diagnostics", normalizedPath);
635
+ return totalCount;
636
+ } catch {
637
+ return 0;
638
+ }
639
+ }
640
+
641
+ export async function clientWaitForDiagnostics(
642
+ state: LSPClientState,
643
+ filePath: string,
644
+ timeoutMs: number,
645
+ ): Promise<void> {
646
+ const normalizedPath = normalizeMapKey(filePath);
647
+
648
+ if (state.workspaceDiagnosticsSupport.mode === "pull") {
649
+ const firstPullCount = await clientRequestPullDiagnostics(state, filePath);
650
+ if (firstPullCount > 0) return;
651
+
652
+ const strategy = getStrategy(state.serverId);
653
+ const retryBudgetMs =
654
+ strategy.pullRetryBudgetMs > 0
655
+ ? Math.min(timeoutMs, strategy.pullRetryBudgetMs)
656
+ : 0;
657
+ const startedAt = Date.now();
658
+ let latestCount = firstPullCount;
659
+
660
+ while (latestCount === 0 && Date.now() - startedAt < retryBudgetMs) {
661
+ await new Promise((resolve) =>
662
+ setTimeout(resolve, PULL_DIAGNOSTICS_RETRY_INTERVAL_MS),
663
+ );
664
+ latestCount = await clientRequestPullDiagnostics(state, filePath);
665
+ }
666
+ if (latestCount > 0) return;
667
+ }
668
+
669
+ if (getMergedDiagnosticsForPath(state, normalizedPath).length > 0) return;
670
+
671
+ return new Promise<void>((resolve) => {
672
+ let debounceTimer: ReturnType<typeof setTimeout> | undefined;
673
+
674
+ const onDiagnostics = (fp: string) => {
675
+ if (normalizeMapKey(fp) !== normalizedPath) return;
676
+ if (debounceTimer) clearTimeout(debounceTimer);
677
+
678
+ // Adaptive debounce: use time since last push to compute remaining
679
+ // wait instead of always waiting the full debounce window.
680
+ const strategy = getStrategy(state.serverId);
681
+ const hit = state.pushDiagnosticTimestamps.get(normalizedPath);
682
+ const timeSincePush = hit ? Date.now() - hit : Infinity;
683
+ const remaining = Math.max(0, strategy.debounceMs - timeSincePush);
684
+
685
+ debounceTimer = setTimeout(() => {
686
+ state.diagnosticEmitter.off("diagnostics", onDiagnostics);
687
+ clearTimeout(timeout);
688
+ resolve();
689
+ }, remaining);
690
+ };
691
+
692
+ state.diagnosticEmitter.on("diagnostics", onDiagnostics);
693
+
694
+ const timeout = setTimeout(() => {
695
+ if (debounceTimer) clearTimeout(debounceTimer);
696
+ state.diagnosticEmitter.off("diagnostics", onDiagnostics);
697
+ resolve();
698
+ }, timeoutMs);
699
+ });
700
+ }
701
+
702
+ export async function handleNotifyOpen(
703
+ state: LSPClientState,
704
+ filePath: string,
705
+ content: string,
706
+ languageId: string,
707
+ preserveDiagnostics = false,
708
+ silent = false,
709
+ ): Promise<void> {
710
+ if (!isClientAlive(state)) return;
711
+ const uri = pathToFileURL(filePath).href;
712
+ const normalizedPath = normalizeMapKey(filePath);
713
+
714
+ if (
715
+ state.openDocuments.has(normalizedPath) ||
716
+ state.pendingOpens.has(normalizedPath)
717
+ ) {
718
+ const version = (state.documentVersions.get(normalizedPath) ?? 0) + 1;
719
+ state.documentVersions.set(normalizedPath, version);
720
+ // preserveDiagnostics: skip cache clear for format-only resyncs so
721
+ // waitForDiagnostics fast-paths instead of waiting up to 5s for TypeScript
722
+ // to re-publish what it already knows (formatting doesn't change semantics).
723
+ if (!preserveDiagnostics) {
724
+ clearDiagnosticsForPath(state, normalizedPath);
725
+ }
726
+ await safeSendNotification(state.connection, "textDocument/didChange", {
727
+ textDocument: { uri, version },
728
+ contentChanges: [{ text: content }],
729
+ });
730
+ return;
731
+ }
732
+
733
+ state.pendingOpens.add(normalizedPath);
734
+ state.documentVersions.set(normalizedPath, 0);
735
+ clearDiagnosticsForPath(state, normalizedPath); // always clear for initial open
736
+
737
+ // Send workspace notification first (like opencode does).
738
+ // Skipped in silent mode — cascade reads a file for diagnostics,
739
+ // not reporting a real filesystem change. Avoids N project-wide
740
+ // rechecks on push-diagnostics LSPs (TypeScript, Python) per CR-1.
741
+ if (!silent) {
742
+ await safeSendNotification(
743
+ state.connection,
744
+ "workspace/didChangeWatchedFiles",
745
+ { changes: [{ uri, type: existsSync(filePath) ? 2 : 1 }] },
746
+ );
747
+ }
748
+
749
+ if (!isClientAlive(state)) return;
750
+
751
+ await safeSendNotification(state.connection, "textDocument/didOpen", {
752
+ textDocument: { uri, languageId, version: 0, text: content },
753
+ });
754
+ state.pendingOpens.delete(normalizedPath);
755
+ state.openDocuments.add(normalizedPath);
756
+ }
757
+
758
+ export async function handleNotifyChange(
759
+ state: LSPClientState,
760
+ filePath: string,
761
+ content: string,
762
+ ): Promise<void> {
763
+ if (!isClientAlive(state)) return;
764
+ const uri = pathToFileURL(filePath).href;
765
+ const normalizedPath = normalizeMapKey(filePath);
766
+
767
+ if (!state.openDocuments.has(normalizedPath)) {
768
+ // Safety fallback: keep protocol ordering valid even if caller sends
769
+ // didChange before first didOpen for this document.
770
+ await safeSendNotification(state.connection, "textDocument/didOpen", {
771
+ textDocument: { uri, languageId: "plaintext", version: 0, text: content },
772
+ });
773
+ state.documentVersions.set(normalizedPath, 0);
774
+ state.openDocuments.add(normalizedPath);
775
+ return;
776
+ }
777
+
778
+ const version = (state.documentVersions.get(normalizedPath) ?? 0) + 1;
779
+ state.documentVersions.set(normalizedPath, version);
780
+ // Clear stale diagnostics before sending new content so waitForDiagnostics
781
+ // doesn't return immediately with the previous edit's results.
782
+ clearDiagnosticsForPath(state, normalizedPath);
783
+ await safeSendNotification(state.connection, "textDocument/didChange", {
784
+ textDocument: { uri, version },
785
+ contentChanges: [{ text: content }],
786
+ });
787
+ }
788
+
789
+ async function clientShutdown(state: LSPClientState): Promise<void> {
790
+ state.isConnected = false;
791
+ state.isDestroyed = true;
792
+ for (const timer of state.pendingDiagnostics.values()) {
793
+ clearTimeout(timer);
794
+ }
795
+ state.pendingDiagnostics.clear();
796
+ state.pendingOpens.clear();
797
+ state.openDocuments.clear();
798
+ state.diagnosticEmitter.removeAllListeners();
799
+ try {
800
+ await withTimeout(
801
+ safeSendRequest(state.connection, "shutdown", {}),
802
+ SHUTDOWN_REQUEST_TIMEOUT_MS,
803
+ );
804
+ } catch {
805
+ /* ignore — proceed to exit/kill so shutdown cannot hang the session */
806
+ }
807
+ try {
808
+ await safeSendNotification(state.connection, "exit", {});
809
+ } catch {
810
+ /* ignore */
811
+ }
812
+ disposeClientConnection(state);
813
+ const pid = state.lspProcess.pid;
814
+ // On Windows, killing the direct child first can orphan grandchildren before
815
+ // taskkill can traverse the tree. Kill the full tree first and wait briefly.
816
+ await killProcessTree(state.lspProcess.process, pid);
817
+ }
818
+
819
+ async function navRequest<T>(
820
+ state: LSPClientState,
821
+ method: string,
822
+ params: Record<string, unknown>,
823
+ ): Promise<T | null | undefined> {
824
+ if (!isClientAlive(state)) return null;
825
+ return withTimeout(
826
+ safeSendRequest<T>(state.connection, method, params),
827
+ NAV_REQUEST_TIMEOUT_MS,
828
+ ).catch((err: unknown) => {
829
+ if (err instanceof Error && err.message.startsWith("Timeout after")) {
830
+ return undefined;
831
+ }
832
+ throw err;
833
+ }) as Promise<T | undefined>;
834
+ }
835
+
836
+ // --- Client Factory ---
837
+
838
+ export async function createLSPClient(options: {
839
+ serverId: string;
840
+ process: LSPProcess;
841
+ root: string;
842
+ initialization?: Record<string, unknown>;
843
+ initializeTimeoutMs?: number;
844
+ }): Promise<LSPClientInfo> {
845
+ installCrashGuard();
846
+
847
+ const {
848
+ serverId,
849
+ process: lspProcess,
850
+ root,
851
+ initialization,
852
+ initializeTimeoutMs = INITIALIZE_TIMEOUT_MS,
853
+ } = options;
854
+
855
+ const startupState: {
856
+ exitCode: number | null;
857
+ exitSignal: NodeJS.Signals | null;
858
+ closeCode: number | null;
859
+ closeSignal: NodeJS.Signals | null;
860
+ stderr: string;
861
+ } = {
862
+ exitCode: null,
863
+ exitSignal: null,
864
+ closeCode: null,
865
+ closeSignal: null,
866
+ stderr: "",
867
+ };
868
+
869
+ // Persistent stderr ring buffer — captures last ~100 lines for diagnostics.
870
+ // Used in error messages to show what the server said before dying.
871
+ const stderrRing: string[] = [];
872
+ const MAX_STDERR_LINES = 100;
873
+
874
+ const onStderr = (chunk: Buffer | string): void => {
875
+ stderrRing.push(chunk.toString());
876
+ if (stderrRing.length > MAX_STDERR_LINES) stderrRing.shift();
877
+ // Also capture startup stderr for the initialized-failed error path
878
+ if (startupState.stderr.length < 4096) {
879
+ startupState.stderr += chunk.toString();
880
+ }
881
+ };
882
+
883
+ const recentStderr = (lines = 10): string =>
884
+ stderrRing.slice(-lines).join("").trim();
885
+
886
+ // Pre-request health check — returns error string if process is dead.
887
+ const checkProcessAlive = (): string | undefined => {
888
+ const exited = lspProcess.process.exitCode;
889
+ if (exited !== null) {
890
+ const tail = recentStderr(20);
891
+ return `LSP server ${serverId} exited with code ${exited}${tail ? `. stderr: ${tail}` : ""}`;
892
+ }
893
+ if ((lspProcess.process as { killed?: boolean }).killed) {
894
+ return `LSP server ${serverId} was killed`;
895
+ }
896
+ return undefined;
897
+ };
898
+
899
+ const onProcessExit = (
900
+ code: number | null,
901
+ signal: NodeJS.Signals | null,
902
+ ): void => {
903
+ startupState.exitCode = code;
904
+ startupState.exitSignal = signal;
905
+ };
906
+ const onProcessClose = (
907
+ code: number | null,
908
+ signal: NodeJS.Signals | null,
909
+ ): void => {
910
+ startupState.closeCode = code;
911
+ startupState.closeSignal = signal;
912
+ };
913
+
914
+ (lspProcess.stderr as NodeJS.ReadableStream).on("data", onStderr);
915
+ lspProcess.process.on("exit", onProcessExit);
916
+ lspProcess.process.on("close", onProcessClose);
917
+
918
+ // Attach persistent 'error' listeners to all three stdio streams.
919
+ //
920
+ // Why: when the LSP process exits, Node.js destroys its stdio streams and
921
+ // may emit 'error' (ERR_STREAM_DESTROYED / EPIPE / ECONNRESET) on them.
922
+ // Without a listener that becomes an uncaught exception.
923
+ //
924
+ // vscode-jsonrpc covers stdin/stdout during the connection lifetime but
925
+ // removes its listeners on dispose(). Our permanent listeners cover the gap.
926
+ const streamErrorHandler =
927
+ (_label: string) => (err: Error & { code?: string }) => {
928
+ if (
929
+ err.code === "ERR_STREAM_DESTROYED" ||
930
+ err.code === "ERR_STREAM_WRITE_AFTER_END" ||
931
+ err.code === "EPIPE" ||
932
+ err.code === "ECONNRESET"
933
+ )
934
+ return;
935
+ };
936
+ (lspProcess.stdin as NodeJS.WritableStream).on(
937
+ "error",
938
+ streamErrorHandler("stdin"),
939
+ );
940
+ (lspProcess.stdout as NodeJS.ReadableStream).on(
941
+ "error",
942
+ streamErrorHandler("stdout"),
943
+ );
944
+ (lspProcess.stderr as NodeJS.ReadableStream).on(
945
+ "error",
946
+ streamErrorHandler("stderr"),
947
+ );
948
+
949
+ const connection = createMessageConnection(
950
+ new StreamMessageReader(lspProcess.stdout),
951
+ new StreamMessageWriter(lspProcess.stdin),
952
+ );
953
+
954
+ // Local event emitter — signals waitForDiagnostics when new diagnostics arrive.
955
+ // Scoped to this client instance. setMaxListeners guards against Node.js warning
956
+ // for concurrent waitForDiagnostics calls.
957
+ const diagnosticEmitter = new EventEmitter();
958
+ diagnosticEmitter.setMaxListeners(50);
959
+
960
+ const state: LSPClientState = {
961
+ isConnected: true,
962
+ isDestroyed: false,
963
+ connectionDisposed: false,
964
+ lastError: undefined,
965
+ connection,
966
+ pushDiagnostics: new Map(),
967
+ pushDiagnosticTimestamps: new Map(),
968
+ documentPullDiagnostics: new Map(),
969
+ documentPullDiagnosticTimestamps: new Map(),
970
+ pendingDiagnostics: new Map(),
971
+ diagnosticEmitter,
972
+ documentVersions: new Map(),
973
+ openDocuments: new Set(),
974
+ pendingOpens: new Set(),
975
+ // these are filled in after initialize — cast to avoid two-phase init
976
+ workspaceDiagnosticsSupport:
977
+ undefined as unknown as LSPWorkspaceDiagnosticsSupport,
978
+ operationSupport: undefined as unknown as LSPOperationSupport,
979
+ staticDiagnosticsMode: "push-only",
980
+ dynamicRegistrations: new Map(),
981
+ serverId,
982
+ root,
983
+ lspProcess,
984
+ };
985
+
986
+ setupIncomingHandlers(state, initialization);
987
+ connection.listen();
988
+ setupConnectionLifecycle(state);
989
+
990
+ let initResult: Awaited<ReturnType<typeof safeSendRequest>>;
991
+ try {
992
+ initResult = await withTimeout(
993
+ safeSendRequest(connection, "initialize", {
994
+ processId: process.pid,
995
+ rootUri: pathToFileURL(root).href,
996
+ workspaceFolders: [
997
+ { name: "workspace", uri: pathToFileURL(root).href },
998
+ ],
999
+ capabilities: {
1000
+ window: { workDoneProgress: true },
1001
+ workspace: {
1002
+ workspaceFolders: true,
1003
+ configuration: true,
1004
+ didChangeWatchedFiles: { dynamicRegistration: true },
1005
+ },
1006
+ textDocument: {
1007
+ synchronization: { didOpen: true, didChange: true },
1008
+ publishDiagnostics: { versionSupport: true },
1009
+ },
1010
+ },
1011
+ initializationOptions: initialization,
1012
+ }),
1013
+ initializeTimeoutMs,
1014
+ );
1015
+ } catch (err) {
1016
+ // Hard-kill the hung process so it doesn't become a zombie.
1017
+ // SIGTERM alone is unreliable on Windows for cmd.exe/PowerShell trees.
1018
+ const pid = lspProcess.pid;
1019
+ void killProcessTree(lspProcess.process, pid);
1020
+ setTimeout(() => {
1021
+ if (!lspProcess.process.killed && process.platform !== "win32") {
1022
+ lspProcess.process.kill("SIGKILL");
1023
+ }
1024
+ }, 2000);
1025
+ throw err;
1026
+ } finally {
1027
+ (lspProcess.stderr as NodeJS.ReadableStream).off("data", onStderr);
1028
+ }
1029
+
1030
+ if (initResult === undefined) {
1031
+ const compactStderr = startupState.stderr
1032
+ .replace(/\s+/g, " ")
1033
+ .trim()
1034
+ .slice(0, 320);
1035
+ const reinstallHint =
1036
+ serverId === "cpp"
1037
+ ? "Install clangd (LLVM/clang-tools) and ensure clangd.exe is on PATH."
1038
+ : `Try reinstalling: npm install -g ${serverId}-language-server.`;
1039
+ const telemetry = [
1040
+ `pid=${lspProcess.pid}`,
1041
+ `exitCode=${startupState.exitCode ?? "none"}`,
1042
+ `exitSignal=${startupState.exitSignal ?? "none"}`,
1043
+ `closeCode=${startupState.closeCode ?? "none"}`,
1044
+ `closeSignal=${startupState.closeSignal ?? "none"}`,
1045
+ `root=${root}`,
1046
+ compactStderr ? `stderr=${compactStderr}` : "stderr=<empty>",
1047
+ ].join(" ");
1048
+ throw new Error(
1049
+ `[lsp] ${serverId} failed to initialize - stream may have been destroyed. ` +
1050
+ `The server binary may be missing or crashed immediately. ${reinstallHint} ` +
1051
+ `telemetry: ${telemetry}`,
1052
+ );
1053
+ }
1054
+
1055
+ state.workspaceDiagnosticsSupport =
1056
+ detectWorkspaceDiagnosticsSupport(initResult);
1057
+ state.operationSupport = detectOperationSupport(initResult);
1058
+ state.staticDiagnosticsMode = state.workspaceDiagnosticsSupport.mode;
1059
+
1060
+ await safeSendNotification(connection, "initialized", {});
1061
+ if (initialization) {
1062
+ await safeSendNotification(connection, "workspace/didChangeConfiguration", {
1063
+ settings: initialization,
1064
+ });
1065
+ }
1066
+
1067
+ return {
1068
+ serverId,
1069
+ root,
1070
+ connection,
1071
+ isAlive: () => isClientAlive(state),
1072
+
1073
+ /** True if the server process has exited or been killed. */
1074
+ processExited: () =>
1075
+ lspProcess.process.exitCode !== null ||
1076
+ (lspProcess.process as { killed?: boolean }).killed === true,
1077
+
1078
+ /** Last N lines of server stderr for diagnostics. */
1079
+ recentStderr: (lines?: number) => recentStderr(lines),
1080
+
1081
+ /** Pre-request health check — returns error string if dead. */
1082
+ checkAlive: () => checkProcessAlive(),
1083
+
1084
+ notify: {
1085
+ async open(filePath, content, languageId, preserveDiagnostics, silent) {
1086
+ return handleNotifyOpen(
1087
+ state,
1088
+ filePath,
1089
+ content,
1090
+ languageId,
1091
+ preserveDiagnostics,
1092
+ silent,
1093
+ );
1094
+ },
1095
+ async change(filePath, content) {
1096
+ return handleNotifyChange(state, filePath, content);
1097
+ },
1098
+ },
1099
+
1100
+ getDiagnostics(filePath) {
1101
+ return getMergedDiagnosticsForPath(state, normalizeMapKey(filePath));
1102
+ },
1103
+
1104
+ getAllDiagnostics() {
1105
+ const result = new Map<string, { diags: LSPDiagnostic[]; ts: number }>();
1106
+ const keys = new Set([
1107
+ ...state.pushDiagnostics.keys(),
1108
+ ...state.documentPullDiagnostics.keys(),
1109
+ ]);
1110
+ for (const key of keys) {
1111
+ result.set(key, {
1112
+ diags: getMergedDiagnosticsForPath(state, key),
1113
+ ts: Math.max(
1114
+ state.pushDiagnosticTimestamps.get(key) ?? 0,
1115
+ state.documentPullDiagnosticTimestamps.get(key) ?? 0,
1116
+ ),
1117
+ });
1118
+ }
1119
+ return result;
1120
+ },
1121
+
1122
+ pruneDiagnostics(predicate) {
1123
+ let removed = 0;
1124
+ const keys = new Set([
1125
+ ...state.pushDiagnostics.keys(),
1126
+ ...state.documentPullDiagnostics.keys(),
1127
+ ]);
1128
+ for (const key of keys) {
1129
+ const diags = getMergedDiagnosticsForPath(state, key);
1130
+ const ts = Math.max(
1131
+ state.pushDiagnosticTimestamps.get(key) ?? 0,
1132
+ state.documentPullDiagnosticTimestamps.get(key) ?? 0,
1133
+ );
1134
+ if (!predicate(key, ts, diags)) continue;
1135
+ clearDiagnosticsForPath(state, key);
1136
+ removed++;
1137
+ }
1138
+ return removed;
1139
+ },
1140
+
1141
+ getWorkspaceDiagnosticsSupport() {
1142
+ return state.workspaceDiagnosticsSupport;
1143
+ },
1144
+
1145
+ getOperationSupport() {
1146
+ return state.operationSupport;
1147
+ },
1148
+
1149
+ async waitForDiagnostics(
1150
+ filePath,
1151
+ timeoutMs = DIAGNOSTICS_WAIT_TIMEOUT_MS,
1152
+ ) {
1153
+ return clientWaitForDiagnostics(state, filePath, timeoutMs);
1154
+ },
1155
+
1156
+ async definition(filePath, line, character) {
1157
+ const result = await navRequest<LSPLocation | LSPLocation[]>(
1158
+ state,
1159
+ "textDocument/definition",
1160
+ {
1161
+ textDocument: { uri: pathToFileURL(filePath).href },
1162
+ position: { line, character },
1163
+ },
1164
+ );
1165
+ if (!result) return [];
1166
+ return Array.isArray(result) ? result : [result];
1167
+ },
1168
+
1169
+ async references(filePath, line, character, includeDeclaration = true) {
1170
+ const result = await navRequest<LSPLocation[]>(
1171
+ state,
1172
+ "textDocument/references",
1173
+ {
1174
+ textDocument: { uri: pathToFileURL(filePath).href },
1175
+ position: { line, character },
1176
+ context: { includeDeclaration },
1177
+ },
1178
+ );
1179
+ return result ?? [];
1180
+ },
1181
+
1182
+ async hover(filePath, line, character) {
1183
+ const result = await navRequest<LSPHover>(state, "textDocument/hover", {
1184
+ textDocument: { uri: pathToFileURL(filePath).href },
1185
+ position: { line, character },
1186
+ });
1187
+ return result ?? null;
1188
+ },
1189
+
1190
+ async signatureHelp(filePath, line, character) {
1191
+ const result = await navRequest<LSPSignatureHelp>(
1192
+ state,
1193
+ "textDocument/signatureHelp",
1194
+ {
1195
+ textDocument: { uri: pathToFileURL(filePath).href },
1196
+ position: { line, character },
1197
+ },
1198
+ );
1199
+ return result ?? null;
1200
+ },
1201
+
1202
+ async documentSymbol(filePath) {
1203
+ const result = await navRequest<LSPSymbol[]>(
1204
+ state,
1205
+ "textDocument/documentSymbol",
1206
+ { textDocument: { uri: pathToFileURL(filePath).href } },
1207
+ );
1208
+ return result ?? [];
1209
+ },
1210
+
1211
+ async workspaceSymbol(query) {
1212
+ if (!isClientAlive(state)) return [];
1213
+ const result = await safeSendRequest<LSPSymbol[]>(
1214
+ connection,
1215
+ "workspace/symbol",
1216
+ { query },
1217
+ );
1218
+ return result ?? [];
1219
+ },
1220
+
1221
+ async codeAction(filePath, line, character, endLine, endCharacter) {
1222
+ if (!isClientAlive(state)) return [];
1223
+ const uri = pathToFileURL(filePath).href;
1224
+ const result = await safeSendRequest<unknown[]>(
1225
+ connection,
1226
+ "textDocument/codeAction",
1227
+ {
1228
+ textDocument: { uri },
1229
+ range: {
1230
+ start: { line, character },
1231
+ end: { line: endLine, character: endCharacter },
1232
+ },
1233
+ context: {
1234
+ diagnostics: getMergedDiagnosticsForPath(
1235
+ state,
1236
+ normalizeMapKey(filePath),
1237
+ ),
1238
+ },
1239
+ },
1240
+ );
1241
+ if (!result || !Array.isArray(result)) return [];
1242
+ return result.filter(
1243
+ (item): item is LSPCodeAction =>
1244
+ typeof item === "object" && item !== null && "title" in item,
1245
+ );
1246
+ },
1247
+
1248
+ async rename(filePath, line, character, newName) {
1249
+ const result = await navRequest<LSPWorkspaceEdit>(
1250
+ state,
1251
+ "textDocument/rename",
1252
+ {
1253
+ textDocument: { uri: pathToFileURL(filePath).href },
1254
+ position: { line, character },
1255
+ newName,
1256
+ },
1257
+ );
1258
+ return result ?? null;
1259
+ },
1260
+
1261
+ async implementation(filePath, line, character) {
1262
+ const result = await navRequest<LSPLocation | LSPLocation[]>(
1263
+ state,
1264
+ "textDocument/implementation",
1265
+ {
1266
+ textDocument: { uri: pathToFileURL(filePath).href },
1267
+ position: { line, character },
1268
+ },
1269
+ );
1270
+ if (!result) return [];
1271
+ return Array.isArray(result) ? result : [result];
1272
+ },
1273
+
1274
+ async prepareCallHierarchy(filePath, line, character) {
1275
+ const result = await navRequest<
1276
+ LSPCallHierarchyItem | LSPCallHierarchyItem[]
1277
+ >(state, "textDocument/prepareCallHierarchy", {
1278
+ textDocument: { uri: pathToFileURL(filePath).href },
1279
+ position: { line, character },
1280
+ });
1281
+ if (!result) return [];
1282
+ return Array.isArray(result) ? result : [result];
1283
+ },
1284
+
1285
+ async incomingCalls(item) {
1286
+ const result = await navRequest<LSPCallHierarchyIncomingCall[]>(
1287
+ state,
1288
+ "callHierarchy/incomingCalls",
1289
+ { item },
1290
+ );
1291
+ return result ?? [];
1292
+ },
1293
+
1294
+ async outgoingCalls(item) {
1295
+ const result = await navRequest<LSPCallHierarchyOutgoingCall[]>(
1296
+ state,
1297
+ "callHierarchy/outgoingCalls",
1298
+ { item },
1299
+ );
1300
+ return result ?? [];
1301
+ },
1302
+
1303
+ async shutdown() {
1304
+ return clientShutdown(state);
1305
+ },
1306
+ };
1307
+ }
1308
+
1309
+ // Helper to safely send notifications - catches stream destruction
1310
+ async function safeSendNotification(
1311
+ connection: MessageConnection,
1312
+ method: string,
1313
+ params: unknown,
1314
+ ): Promise<void> {
1315
+ try {
1316
+ await connection.sendNotification(method as never, params as never);
1317
+ } catch (err) {
1318
+ if (isStreamError(err)) {
1319
+ // Silently ignore - stream was destroyed, connection error handlers will update state
1320
+ return;
1321
+ }
1322
+ throw err;
1323
+ }
1324
+ }
1325
+
1326
+ // Helper to safely send requests - catches stream destruction
1327
+ async function safeSendRequest<T>(
1328
+ connection: MessageConnection,
1329
+ method: string,
1330
+ params: unknown,
1331
+ ): Promise<T | undefined> {
1332
+ try {
1333
+ return (await connection.sendRequest(
1334
+ method as never,
1335
+ params as never,
1336
+ )) as T;
1337
+ } catch (err) {
1338
+ if (isStreamError(err)) {
1339
+ // Silently ignore - stream was destroyed
1340
+ return undefined;
1341
+ }
1342
+ throw err;
1343
+ }
1344
+ }
1345
+
1346
+ // Helper to detect stream destruction / connection disposal errors.
1347
+ // vscode-jsonrpc throws these when the LSP server process exits while
1348
+ // requests are still in flight:
1349
+ // "Connection is disposed."
1350
+ // "Pending response rejected since connection got disposed"
1351
+ // Neither phrase contains "stream", "destroyed", or "closed", which is
1352
+ // why we must also match "disposed" and "cancelled" here.
1353
+ function isStreamError(err: unknown): boolean {
1354
+ if (!(err instanceof Error)) return false;
1355
+ const msg = err.message.toLowerCase();
1356
+ return (
1357
+ msg.includes("stream") ||
1358
+ msg.includes("destroyed") ||
1359
+ msg.includes("closed") ||
1360
+ msg.includes("disposed") ||
1361
+ msg.includes("cancelled") ||
1362
+ (err as { code?: string }).code === "ERR_STREAM_DESTROYED" ||
1363
+ (err as { code?: string }).code === "ERR_STREAM_WRITE_AFTER_END" ||
1364
+ (err as { code?: string }).code === "EPIPE"
1365
+ );
1366
+ }
1367
+
1368
+ // Using shared path utilities from path-utils.ts
1369
+
1370
+ async function withTimeout<T>(
1371
+ promise: Promise<T>,
1372
+ timeoutMs: number,
1373
+ ): Promise<T> {
1374
+ let timeout: ReturnType<typeof setTimeout> | undefined;
1375
+ // Suppress unhandled rejection if `promise` rejects AFTER the timeout
1376
+ // wins the race — Promise.race settles on the first result but the
1377
+ // losing promises still run, and any later rejection would be uncaught.
1378
+ promise.catch(() => {});
1379
+ try {
1380
+ return await Promise.race([
1381
+ promise,
1382
+ new Promise<T>((_, reject) => {
1383
+ timeout = setTimeout(
1384
+ () => reject(new Error(`Timeout after ${timeoutMs}ms`)),
1385
+ timeoutMs,
1386
+ );
1387
+ }),
1388
+ ]);
1389
+ } finally {
1390
+ if (timeout) clearTimeout(timeout);
1391
+ }
1392
+ }
1393
+
1394
+ function positiveIntFromEnv(name: string, fallback: number): number {
1395
+ const raw = process.env[name];
1396
+ if (!raw) return fallback;
1397
+ const parsed = Number.parseInt(raw, 10);
1398
+ if (!Number.isFinite(parsed) || parsed <= 0) return fallback;
1399
+ return parsed;
1400
+ }
1401
+
1402
+ function detectWorkspaceDiagnosticsSupport(
1403
+ initResult: unknown,
1404
+ ): LSPWorkspaceDiagnosticsSupport {
1405
+ const capabilities =
1406
+ typeof initResult === "object" && initResult !== null
1407
+ ? (initResult as { capabilities?: Record<string, unknown> }).capabilities
1408
+ : undefined;
1409
+ const diagnosticProvider = capabilities?.diagnosticProvider;
1410
+ if (!diagnosticProvider) {
1411
+ return {
1412
+ advertised: false,
1413
+ mode: "push-only",
1414
+ diagnosticProviderKind: "none",
1415
+ };
1416
+ }
1417
+
1418
+ if (typeof diagnosticProvider === "boolean") {
1419
+ return {
1420
+ advertised: diagnosticProvider,
1421
+ mode: diagnosticProvider ? "pull" : "push-only",
1422
+ diagnosticProviderKind: "boolean",
1423
+ };
1424
+ }
1425
+
1426
+ if (typeof diagnosticProvider === "object") {
1427
+ return {
1428
+ advertised: true,
1429
+ mode: "pull",
1430
+ diagnosticProviderKind: "object",
1431
+ };
1432
+ }
1433
+
1434
+ return {
1435
+ advertised: false,
1436
+ mode: "push-only",
1437
+ diagnosticProviderKind: typeof diagnosticProvider,
1438
+ };
1439
+ }
1440
+
1441
+ function detectOperationSupport(initResult: unknown): LSPOperationSupport {
1442
+ const capabilities =
1443
+ typeof initResult === "object" && initResult !== null
1444
+ ? (initResult as { capabilities?: Record<string, unknown> }).capabilities
1445
+ : undefined;
1446
+
1447
+ const hasProvider = (key: string): boolean => {
1448
+ const value = capabilities?.[key];
1449
+ if (value === undefined || value === null) return false;
1450
+ if (typeof value === "boolean") return value;
1451
+ return true;
1452
+ };
1453
+
1454
+ return {
1455
+ definition: hasProvider("definitionProvider"),
1456
+ references: hasProvider("referencesProvider"),
1457
+ hover: hasProvider("hoverProvider"),
1458
+ signatureHelp: hasProvider("signatureHelpProvider"),
1459
+ documentSymbol: hasProvider("documentSymbolProvider"),
1460
+ workspaceSymbol: hasProvider("workspaceSymbolProvider"),
1461
+ codeAction: hasProvider("codeActionProvider"),
1462
+ rename: hasProvider("renameProvider"),
1463
+ implementation: hasProvider("implementationProvider"),
1464
+ callHierarchy: hasProvider("callHierarchyProvider"),
1465
+ };
1466
+ }