ultimate-pi 0.18.0 → 0.19.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (314) hide show
  1. package/.agents/skills/harness-debate-plan/SKILL.md +1 -1
  2. package/.agents/skills/harness-decisions/SKILL.md +2 -3
  3. package/.agents/skills/harness-governor/SKILL.md +6 -5
  4. package/.agents/skills/harness-orchestration/SKILL.md +4 -4
  5. package/.agents/skills/harness-review/SKILL.md +7 -7
  6. package/.agents/skills/harness-sentrux-setup/SKILL.md +4 -3
  7. package/.agents/skills/harness-steer/SKILL.md +1 -1
  8. package/.agents/skills/sentrux/SKILL.md +9 -9
  9. package/.pi/PACKAGING.md +4 -4
  10. package/.pi/SYSTEM.md +54 -120
  11. package/.pi/agents/harness/incident-recorder.md +0 -1
  12. package/.pi/agents/harness/planning/decompose.md +1 -3
  13. package/.pi/agents/harness/planning/execution-plan-author.md +0 -2
  14. package/.pi/agents/harness/planning/hypothesis-validator.md +0 -2
  15. package/.pi/agents/harness/planning/hypothesis.md +0 -2
  16. package/.pi/agents/harness/planning/implementation-researcher.md +0 -2
  17. package/.pi/agents/harness/planning/plan-adversary.md +0 -2
  18. package/.pi/agents/harness/planning/plan-evaluator.md +1 -3
  19. package/.pi/agents/harness/planning/planning-context.md +0 -2
  20. package/.pi/agents/harness/planning/review-integrator.md +0 -2
  21. package/.pi/agents/harness/planning/sprint-contract-auditor.md +0 -2
  22. package/.pi/agents/harness/planning/stack-researcher.md +0 -2
  23. package/.pi/agents/harness/{adversary.md → reviewing/adversary.md} +0 -2
  24. package/.pi/agents/harness/{evaluator.md → reviewing/evaluator.md} +0 -2
  25. package/.pi/agents/harness/{tie-breaker.md → reviewing/tie-breaker.md} +0 -2
  26. package/.pi/agents/harness/{executor.md → running/executor.md} +0 -2
  27. package/.pi/agents/harness/sentrux-bootstrap.md +0 -1
  28. package/.pi/agents/harness/sentrux-steward.md +0 -2
  29. package/.pi/agents/harness/trace-librarian.md +0 -1
  30. package/.pi/extensions/00-harness-project-control.ts +133 -0
  31. package/.pi/extensions/00-posthog-network-bootstrap.ts +1 -1
  32. package/.pi/extensions/agt-kill-switch.ts +57 -0
  33. package/.pi/extensions/agt-prompt-guard.ts +32 -0
  34. package/.pi/extensions/budget-guard.ts +2 -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 +7 -5
  39. package/.pi/extensions/harness-ask-user.ts +8 -8
  40. package/.pi/extensions/harness-debate-tools.ts +27 -43
  41. package/.pi/extensions/harness-lens.ts +94 -0
  42. package/.pi/extensions/harness-live-widget.ts +33 -2
  43. package/.pi/extensions/harness-plan-approval.ts +12 -12
  44. package/.pi/extensions/harness-run-context.ts +1214 -852
  45. package/.pi/extensions/harness-subagent-governance.ts +8 -0
  46. package/.pi/extensions/harness-subagent-submit.ts +36 -164
  47. package/.pi/extensions/harness-subagents.ts +4 -4
  48. package/.pi/extensions/harness-telemetry.ts +3 -1
  49. package/.pi/extensions/harness-web-tools.ts +3 -3
  50. package/.pi/extensions/observation-bus.ts +2 -0
  51. package/.pi/extensions/policy-gate.ts +27 -5
  52. package/.pi/extensions/review-integrity.ts +91 -10
  53. package/.pi/extensions/sentrux-rules-sync.ts +3 -1
  54. package/.pi/extensions/subagent-governance.ts +92 -0
  55. package/.pi/extensions/test-diff-integrity.ts +1 -0
  56. package/.pi/extensions/trace-recorder.ts +3 -1
  57. package/.pi/extensions/{ultimate-pi-vcc.ts → vcc-compaction.ts} +1 -1
  58. package/.pi/harness/README.md +6 -2
  59. package/.pi/harness/agents.manifest.json +38 -49
  60. package/.pi/harness/agents.policy.yaml +275 -0
  61. package/.pi/harness/corpus/graphify-kb-updater.config.json +55 -0
  62. package/.pi/harness/docs/adrs/0006-sentrux-dual-layer.md +2 -1
  63. package/.pi/harness/docs/adrs/0030-inhouse-vcc-compaction.md +1 -1
  64. package/.pi/harness/docs/adrs/0035-plan-phase-review-gate.md +1 -1
  65. package/.pi/harness/docs/adrs/0044-harness-steer-loop.md +3 -2
  66. package/.pi/harness/docs/adrs/0045-harness-lens-minimal-contract.md +49 -0
  67. package/.pi/harness/docs/adrs/0045-phase-scoped-agent-directories.md +33 -0
  68. package/.pi/harness/docs/adrs/0046-agt-policy-engine.md +51 -0
  69. package/.pi/harness/docs/adrs/0047-agt-layered-security.md +39 -0
  70. package/.pi/harness/docs/adrs/0048-tool-call-hook-order.md +25 -0
  71. package/.pi/harness/docs/adrs/0049-agents-policy-manifest.md +36 -0
  72. package/.pi/harness/docs/adrs/README.md +6 -0
  73. package/.pi/harness/docs/graphify-kb-updater-runbook.md +11 -5
  74. package/.pi/harness/docs/practice-map.md +2 -2
  75. package/.pi/harness/evolution/README.md +1 -2
  76. package/.pi/harness/examples/agents.policy.project.yaml +19 -0
  77. package/.pi/harness/examples/policies/custom-deny-bash.yaml +9 -0
  78. package/.pi/harness/policies/bash-denylists.yaml +5 -0
  79. package/.pi/harness/policies/defaults.yaml +51 -0
  80. package/.pi/harness/policies/orchestrator.yaml +18 -0
  81. package/.pi/harness/policies/phases.yaml +10 -0
  82. package/.pi/harness/policies/roles.yaml +5 -0
  83. package/.pi/harness/policies/web-guard.yaml +5 -0
  84. package/.pi/harness/policies/workflow-sequences.yaml +9 -0
  85. package/.pi/harness/sentrux/architecture.manifest.json +26 -4
  86. package/.pi/harness/specs/harness-spawn-context.schema.json +1 -1
  87. package/.pi/harness/specs/observation.schema.json +2 -1
  88. package/.pi/lib/agents-policy.d.mts +70 -0
  89. package/.pi/lib/agents-policy.mjs +325 -0
  90. package/.pi/lib/agents-policy.ts +19 -0
  91. package/.pi/lib/agt/audit-run-sink.ts +52 -0
  92. package/.pi/lib/agt/build-evaluation-context.ts +285 -0
  93. package/.pi/lib/agt/config.ts +28 -0
  94. package/.pi/lib/agt/delegation.ts +69 -0
  95. package/.pi/lib/agt/evaluate-policy.ts +56 -0
  96. package/.pi/lib/agt/identity-registry.ts +41 -0
  97. package/.pi/lib/agt/index.ts +55 -0
  98. package/.pi/lib/agt/kill-switch-state.ts +11 -0
  99. package/.pi/lib/agt/legacy-evaluate.ts +101 -0
  100. package/.pi/lib/agt/policy-engine.ts +154 -0
  101. package/.pi/lib/agt/rings.ts +21 -0
  102. package/.pi/lib/agt/sre-hooks.ts +45 -0
  103. package/.pi/lib/agt/trust-run-store.ts +26 -0
  104. package/.pi/lib/agt/workflow-history.ts +29 -0
  105. package/.pi/lib/agt-governance-active.ts +14 -0
  106. package/.pi/lib/agt-tool-guard.ts +78 -0
  107. package/.pi/lib/ask-user/dialog.ts +314 -0
  108. package/.pi/{extensions/lib → lib}/debate-bus-core.ts +10 -10
  109. package/.pi/{extensions/lib → lib}/debate-bus-state.ts +1 -1
  110. package/.pi/{extensions/lib → lib}/extension-load-guard.ts +21 -0
  111. package/.pi/lib/harness-agt-tool-guard.ts +5 -0
  112. package/.pi/{extensions/lib → lib}/harness-artifact-gate.ts +6 -16
  113. package/.pi/lib/harness-debate-core-deps.ts +14 -0
  114. package/.pi/lib/harness-debate-workflow-deps.ts +43 -0
  115. package/.pi/lib/harness-lens/.gitattributes +1 -0
  116. package/.pi/lib/harness-lens/clients/edit-autopatch.ts +88 -0
  117. package/.pi/lib/harness-lens/clients/file-kinds.ts +380 -0
  118. package/.pi/lib/harness-lens/clients/file-time.ts +215 -0
  119. package/.pi/lib/harness-lens/clients/file-utils.ts +484 -0
  120. package/.pi/lib/harness-lens/clients/format-service.ts +276 -0
  121. package/.pi/lib/harness-lens/clients/formatters.ts +1000 -0
  122. package/.pi/lib/harness-lens/clients/git-guard.ts +31 -0
  123. package/.pi/lib/harness-lens/clients/indent-retarget.ts +90 -0
  124. package/.pi/lib/harness-lens/clients/installer/index.ts +2368 -0
  125. package/.pi/lib/harness-lens/clients/latency-logger.ts +80 -0
  126. package/.pi/lib/harness-lens/clients/lens-config.ts +43 -0
  127. package/.pi/lib/harness-lens/clients/lens-events.ts +164 -0
  128. package/.pi/lib/harness-lens/clients/lsp/aggregation.ts +91 -0
  129. package/.pi/lib/harness-lens/clients/lsp/client.ts +1466 -0
  130. package/.pi/lib/harness-lens/clients/lsp/config.ts +216 -0
  131. package/.pi/lib/harness-lens/clients/lsp/edits.ts +297 -0
  132. package/.pi/lib/harness-lens/clients/lsp/index.ts +1355 -0
  133. package/.pi/lib/harness-lens/clients/lsp/interactive-install.ts +424 -0
  134. package/.pi/lib/harness-lens/clients/lsp/language.ts +223 -0
  135. package/.pi/lib/harness-lens/clients/lsp/launch.ts +939 -0
  136. package/.pi/lib/harness-lens/clients/lsp/lsp-index.ts +11 -0
  137. package/.pi/lib/harness-lens/clients/lsp/path-utils.ts +12 -0
  138. package/.pi/lib/harness-lens/clients/lsp/server-strategies.ts +81 -0
  139. package/.pi/lib/harness-lens/clients/lsp/server.ts +1971 -0
  140. package/.pi/lib/harness-lens/clients/path-utils.ts +182 -0
  141. package/.pi/lib/harness-lens/clients/pipeline.ts +360 -0
  142. package/.pi/lib/harness-lens/clients/project-profile.ts +117 -0
  143. package/.pi/lib/harness-lens/clients/runtime-agent-end.ts +112 -0
  144. package/.pi/lib/harness-lens/clients/runtime-config.ts +33 -0
  145. package/.pi/lib/harness-lens/clients/runtime-coordinator.ts +186 -0
  146. package/.pi/lib/harness-lens/clients/runtime-tool-result.ts +171 -0
  147. package/.pi/lib/harness-lens/clients/safe-spawn.ts +339 -0
  148. package/.pi/lib/harness-lens/clients/secrets-scanner.ts +214 -0
  149. package/.pi/lib/harness-lens/clients/tool-policy.ts +2072 -0
  150. package/.pi/lib/harness-lens/clients/types.ts +59 -0
  151. package/.pi/lib/harness-lens/clients/widget-state.ts +283 -0
  152. package/.pi/lib/harness-lens/index.ts +532 -0
  153. package/.pi/lib/harness-lens/tools/lsp-diagnostics.ts +706 -0
  154. package/.pi/lib/harness-lens/tools/lsp-navigation.ts +1246 -0
  155. package/.pi/{extensions/lib → lib}/harness-posthog.ts +3 -0
  156. package/.pi/lib/harness-project-config.ts +91 -0
  157. package/.pi/lib/harness-run-context-responses.ts +9 -0
  158. package/.pi/lib/harness-run-context.ts +1 -3
  159. package/.pi/{extensions/lib/spawn-policy.ts → lib/harness-spawn-policy.ts} +4 -3
  160. package/.pi/{extensions/lib → lib}/harness-spawn-topology.ts +5 -28
  161. package/.pi/lib/harness-subagent-auth.ts +51 -0
  162. package/.pi/{extensions/lib → lib}/harness-subagent-precheck.ts +13 -10
  163. package/.pi/{extensions/lib → lib}/harness-subagent-submit-pipeline.ts +3 -3
  164. package/.pi/lib/harness-subagent-submit-register.ts +163 -0
  165. package/.pi/{extensions/lib → lib}/harness-subagent-submit-registry.ts +1 -55
  166. package/.pi/{extensions/lib → lib}/harness-subagents-bridge.ts +53 -14
  167. package/.pi/{extensions/lib → lib}/harness-subprocess-bootstrap.ts +1 -1
  168. package/.pi/lib/harness-ui-state.ts +27 -12
  169. package/.pi/{extensions/lib → lib}/plan-approval/create-plan.ts +2 -2
  170. package/.pi/{extensions/lib → lib}/plan-approval/format-plan.ts +2 -2
  171. package/.pi/{extensions/lib → lib}/plan-approval/plan-review.ts +162 -201
  172. package/.pi/{extensions/lib → lib}/plan-approval/render.ts +1 -1
  173. package/.pi/{extensions/lib → lib}/plan-approval/resolve-disk.ts +2 -2
  174. package/.pi/{extensions/lib → lib}/plan-approval/types.ts +1 -1
  175. package/.pi/{extensions/lib → lib}/plan-approval/validate.ts +3 -3
  176. package/.pi/{extensions/lib → lib}/plan-approval-readiness.ts +3 -52
  177. package/.pi/{extensions/lib → lib}/plan-debate-envelope.ts +1 -1
  178. package/.pi/{extensions/lib → lib}/plan-debate-gate.ts +1 -1
  179. package/.pi/{extensions/lib → lib}/plan-debate-lane.ts +1 -4
  180. package/.pi/{extensions/lib → lib}/plan-messenger.ts +1 -1
  181. package/.pi/prompts/harness-auto.md +2 -2
  182. package/.pi/prompts/harness-plan.md +4 -6
  183. package/.pi/prompts/harness-review.md +9 -9
  184. package/.pi/prompts/harness-run.md +7 -7
  185. package/.pi/prompts/harness-setup.md +42 -68
  186. package/.pi/prompts/harness-steer.md +2 -2
  187. package/.pi/scripts/README.md +3 -5
  188. package/.pi/scripts/generate-agents-policy-yaml.mjs +148 -0
  189. package/.pi/scripts/graphify-kb-updater.mjs +48 -8
  190. package/.pi/scripts/harness-agents-manifest.mjs +61 -4
  191. package/.pi/scripts/harness-agt-doctor.ts +36 -0
  192. package/.pi/scripts/harness-cli-verify.sh +9 -2
  193. package/.pi/scripts/harness-project-toggle.mjs +129 -0
  194. package/.pi/scripts/harness-sentrux-cli.mjs +142 -0
  195. package/.pi/scripts/harness-verify.mjs +113 -39
  196. package/.pi/scripts/harness-web-policy-guard.mjs +2 -2
  197. package/.pi/scripts/validate-plan-dag.mjs +65 -74
  198. package/.pi/scripts/vendor-pi-vcc-settings.stub.ts +2 -2
  199. package/.pi/scripts/vendor-sync-pi-vcc.sh +1 -1
  200. package/.pi/skills/architecture/broker-domain/SKILL.md +65 -0
  201. package/.pi/skills/architecture/cqrs/SKILL.md +63 -0
  202. package/.pi/skills/architecture/event-driven/SKILL.md +60 -0
  203. package/.pi/skills/architecture/hexagonal-ports-adapters/SKILL.md +66 -0
  204. package/.pi/skills/architecture/layered/SKILL.md +68 -0
  205. package/.pi/skills/architecture/microkernel/SKILL.md +62 -0
  206. package/.pi/skills/architecture/microservices/SKILL.md +64 -0
  207. package/.pi/skills/architecture/modular-monolith/SKILL.md +65 -0
  208. package/.pi/skills/architecture/orchestration-driven-soa/SKILL.md +61 -0
  209. package/.pi/skills/architecture/pipeline/SKILL.md +63 -0
  210. package/.pi/skills/architecture/service-based/SKILL.md +64 -0
  211. package/.pi/skills/architecture/service-mesh/SKILL.md +60 -0
  212. package/.pi/skills/architecture/space-based/SKILL.md +60 -0
  213. package/.pi/skills/ast-grep/SKILL.md +40 -321
  214. package/.pi/skills/delivery/debugging-discipline/SKILL.md +36 -0
  215. package/.pi/skills/delivery/documentation-update/SKILL.md +33 -0
  216. package/.pi/skills/delivery/requirements-to-implementation/SKILL.md +34 -0
  217. package/.pi/skills/delivery/risk-based-verification/SKILL.md +43 -0
  218. package/.pi/skills/delivery/tradeoff-analysis/SKILL.md +34 -0
  219. package/.pi/skills/engineering/api-contract-design/SKILL.md +38 -0
  220. package/.pi/skills/engineering/cohesion-coupling/SKILL.md +43 -0
  221. package/.pi/skills/engineering/complexity-control/SKILL.md +31 -0
  222. package/.pi/skills/engineering/defensive-programming/SKILL.md +38 -0
  223. package/.pi/skills/engineering/dependency-management/SKILL.md +29 -0
  224. package/.pi/skills/engineering/domain-modeling/SKILL.md +32 -0
  225. package/.pi/skills/engineering/error-handling/SKILL.md +37 -0
  226. package/.pi/skills/engineering/legacy-code-seams/SKILL.md +35 -0
  227. package/.pi/skills/engineering/naming-and-intent/SKILL.md +29 -0
  228. package/.pi/skills/engineering/refactoring-safe-evolution/SKILL.md +35 -0
  229. package/.pi/skills/engineering/routine-function-design/SKILL.md +34 -0
  230. package/.pi/skills/engineering/small-change-discipline/SKILL.md +35 -0
  231. package/.pi/skills/lsp-navigation/SKILL.md +89 -0
  232. package/.pi/skills/quality/code-review-self-check/SKILL.md +35 -0
  233. package/.pi/skills/quality/privacy-data-handling/SKILL.md +26 -0
  234. package/.pi/skills/quality/security-review/SKILL.md +34 -0
  235. package/.pi/skills/quality/test-strategy/SKILL.md +33 -0
  236. package/.pi/skills/quality/testability-design/SKILL.md +33 -0
  237. package/.pi/skills/systems/concurrency-safety/SKILL.md +32 -0
  238. package/.pi/skills/systems/data-modeling-migrations/SKILL.md +31 -0
  239. package/.pi/skills/systems/observability-instrumentation/SKILL.md +32 -0
  240. package/.pi/skills/systems/performance-measurement/SKILL.md +35 -0
  241. package/.pi/skills/systems/reliability-design/SKILL.md +32 -0
  242. package/.sentrux/rules.toml +20 -4
  243. package/AGENTS.md +5 -0
  244. package/CHANGELOG.md +26 -0
  245. package/README.md +85 -58
  246. package/THIRD_PARTY_NOTICES.md +12 -21
  247. package/package.json +15 -7
  248. package/vendor/pi-subagents/src/agents.ts +45 -1
  249. package/vendor/pi-subagents/src/subagents.ts +866 -811
  250. package/vendor/pi-vcc/src/core/brief.ts +68 -99
  251. package/vendor/pi-vcc/src/core/settings.ts +2 -2
  252. package/.agents/skills/caveman/SKILL.md +0 -67
  253. package/.pi/agents/harness/meta-optimizer.md +0 -36
  254. package/.pi/agents/harness/planning/scout-graphify.md +0 -39
  255. package/.pi/agents/harness/planning/scout-semantic.md +0 -41
  256. package/.pi/agents/harness/planning/scout-structure.md +0 -37
  257. package/.pi/extensions/lib/ask-user/dialog.ts +0 -260
  258. package/.pi/extensions/lib/harness-subagent-auth.ts +0 -209
  259. package/.pi/extensions/lib/harness-subagent-policy.ts +0 -236
  260. package/.pi/extensions/pi-model-router-harness.ts +0 -42
  261. package/.pi/harness/evolution/meta-optimizer.mjs +0 -99
  262. package/.pi/harness/specs/router-tuning-proposal.schema.json +0 -114
  263. package/.pi/model-router.example.json +0 -36
  264. package/.pi/prompts/harness-critic.md +0 -10
  265. package/.pi/prompts/harness-eval.md +0 -10
  266. package/.pi/prompts/harness-router-tune.md +0 -52
  267. package/.pi/scripts/harness-generate-model-router.mjs +0 -327
  268. package/.pi/scripts/harness-model-router-routing.test.mjs +0 -97
  269. package/.pi/scripts/harness-sync-model-router.mjs +0 -97
  270. package/.pi/scripts/vendor-sync-pi-model-router.sh +0 -47
  271. package/vendor/pi-model-router/.prettierignore +0 -4
  272. package/vendor/pi-model-router/.prettierrc +0 -5
  273. package/vendor/pi-model-router/AGENTS.md +0 -39
  274. package/vendor/pi-model-router/LICENSE +0 -21
  275. package/vendor/pi-model-router/README.md +0 -99
  276. package/vendor/pi-model-router/UPSTREAM_PIN.md +0 -10
  277. package/vendor/pi-model-router/docs/ARCHITECTURE.md +0 -54
  278. package/vendor/pi-model-router/extensions/commands.ts +0 -720
  279. package/vendor/pi-model-router/extensions/config.ts +0 -348
  280. package/vendor/pi-model-router/extensions/constants.ts +0 -1
  281. package/vendor/pi-model-router/extensions/index.ts +0 -478
  282. package/vendor/pi-model-router/extensions/provider.ts +0 -580
  283. package/vendor/pi-model-router/extensions/routing.ts +0 -564
  284. package/vendor/pi-model-router/extensions/state.ts +0 -52
  285. package/vendor/pi-model-router/extensions/types.ts +0 -95
  286. package/vendor/pi-model-router/extensions/ui.ts +0 -144
  287. package/vendor/pi-model-router/model-router.example.json +0 -48
  288. package/vendor/pi-model-router/package.json +0 -48
  289. package/vendor/pi-model-router/tsconfig.json +0 -16
  290. /package/.pi/{prompts → harness/docs}/planning-rubrics.md +0 -0
  291. /package/.pi/{extensions/lib → lib}/ask-user/fallback.ts +0 -0
  292. /package/.pi/{extensions/lib → lib}/ask-user/render.ts +0 -0
  293. /package/.pi/{extensions/lib → lib}/ask-user/schema.ts +0 -0
  294. /package/.pi/{extensions/lib → lib}/ask-user/types.ts +0 -0
  295. /package/.pi/{extensions/lib → lib}/ask-user/validate-core.mjs +0 -0
  296. /package/.pi/{extensions/lib → lib}/ask-user/validate.ts +0 -0
  297. /package/.pi/{extensions/lib → lib}/harness-cocoindex-refresh.ts +0 -0
  298. /package/.pi/{extensions/lib → lib}/harness-paths.ts +0 -0
  299. /package/.pi/{extensions/lib → lib}/harness-spawn-budget.ts +0 -0
  300. /package/.pi/{extensions/lib → lib}/harness-vcc-settings.ts +0 -0
  301. /package/.pi/{extensions/lib → lib}/harness-web/run-cli.ts +0 -0
  302. /package/.pi/{extensions/lib → lib}/plan-approval/dialog.ts +0 -0
  303. /package/.pi/{extensions/lib → lib}/plan-approval/schema.ts +0 -0
  304. /package/.pi/{extensions/lib → lib}/plan-debate-eligibility.ts +0 -0
  305. /package/.pi/{extensions/lib → lib}/plan-debate-focus.ts +0 -0
  306. /package/.pi/{extensions/lib → lib}/plan-debate-id.ts +0 -0
  307. /package/.pi/{extensions/lib → lib}/plan-debate-lanes.ts +0 -0
  308. /package/.pi/{extensions/lib → lib}/plan-debate-round-status.ts +0 -0
  309. /package/.pi/{extensions/lib → lib}/plan-debate-write-guard.ts +0 -0
  310. /package/.pi/{extensions/lib → lib}/plan-review-gate.ts +0 -0
  311. /package/.pi/{extensions/lib → lib}/plan-review-integrator-rules.ts +0 -0
  312. /package/.pi/{extensions/lib → lib}/plan-scope-guard.ts +0 -0
  313. /package/.pi/{extensions/lib → lib}/posthog-client.ts +0 -0
  314. /package/.pi/{extensions/lib → lib}/posthog-node.d.ts +0 -0
@@ -0,0 +1,1246 @@
1
+ /**
2
+ * lsp_navigation tool definition
3
+ *
4
+ * Extracted from index.ts for maintainability.
5
+ */
6
+
7
+ import * as nodeFs from "node:fs";
8
+ import * as path from "node:path";
9
+ import { pathToFileURL } from "node:url";
10
+ import { Type } from "typebox";
11
+ import { logLatency } from "../clients/latency-logger.js";
12
+ import type { LSPCallHierarchyItem } from "../clients/lsp/client.js";
13
+ import {
14
+ applyWorkspaceEdit,
15
+ summarizeWorkspaceEdit,
16
+ } from "../clients/lsp/edits.js";
17
+ import { getLSPService } from "../clients/lsp/index.js";
18
+
19
+ const VALID_OPERATIONS = [
20
+ "definition",
21
+ "references",
22
+ "hover",
23
+ "signatureHelp",
24
+ "documentSymbol",
25
+ "findSymbol",
26
+ "workspaceSymbol",
27
+ "codeAction",
28
+ "rename",
29
+ "implementation",
30
+ "prepareCallHierarchy",
31
+ "incomingCalls",
32
+ "outgoingCalls",
33
+ "workspaceDiagnostics",
34
+ ] as const;
35
+
36
+ type LspNavigationOperation = (typeof VALID_OPERATIONS)[number];
37
+
38
+ function normalizeOperation(value: unknown): string {
39
+ if (typeof value !== "string") return "";
40
+ return value.trim().replace(/^["']+|["']+$/g, "");
41
+ }
42
+
43
+ function isValidOperation(value: string): value is LspNavigationOperation {
44
+ return (VALID_OPERATIONS as readonly string[]).includes(value);
45
+ }
46
+
47
+ function operationSupportStatus(
48
+ operation: LspNavigationOperation,
49
+ support: import("../clients/lsp/client.js").LSPOperationSupport | null,
50
+ ): boolean | null {
51
+ if (!support) return null;
52
+ if (operation === "definition") return support.definition;
53
+ if (operation === "references") return support.references;
54
+ if (operation === "hover") return support.hover;
55
+ if (operation === "signatureHelp") return support.signatureHelp;
56
+ if (operation === "documentSymbol" || operation === "findSymbol")
57
+ return support.documentSymbol;
58
+ if (operation === "workspaceSymbol") return support.workspaceSymbol;
59
+ if (operation === "codeAction") return support.codeAction;
60
+ if (operation === "rename") return support.rename;
61
+ if (operation === "implementation") return support.implementation;
62
+ if (
63
+ operation === "prepareCallHierarchy" ||
64
+ operation === "incomingCalls" ||
65
+ operation === "outgoingCalls"
66
+ )
67
+ return support.callHierarchy;
68
+ return null;
69
+ }
70
+
71
+ function emptyReasonForOperation(operation: LspNavigationOperation): string {
72
+ if (operation === "signatureHelp")
73
+ return "position-sensitive-or-no-signature";
74
+ if (operation === "codeAction") return "no-applicable-actions";
75
+ if (operation === "rename") return "no-rename-edits-or-symbol-not-renamable";
76
+ if (operation === "findSymbol") return "no-matching-symbols";
77
+ if (operation === "workspaceSymbol")
78
+ return "no-matching-symbols-or-server-index-unavailable";
79
+ if (operation === "incomingCalls" || operation === "outgoingCalls")
80
+ return "no-call-hierarchy-results";
81
+ return "no-results";
82
+ }
83
+
84
+ function tokenAtPosition(
85
+ content: string,
86
+ line1: number,
87
+ char1: number,
88
+ ): string | undefined {
89
+ const lines = content.split(/\r?\n/);
90
+ const line = lines[line1 - 1];
91
+ if (!line) return undefined;
92
+ const chars = [...line];
93
+ const idx = Math.max(0, Math.min(chars.length - 1, char1 - 1));
94
+ const isWord = (ch: string | undefined) => !!ch && /[A-Za-z0-9_?!]/.test(ch);
95
+
96
+ let left = idx;
97
+ let right = idx;
98
+ if (!isWord(chars[idx]) && isWord(chars[idx + 1])) {
99
+ left = idx + 1;
100
+ right = idx + 1;
101
+ }
102
+ while (left > 0 && isWord(chars[left - 1])) left -= 1;
103
+ while (right < chars.length - 1 && isWord(chars[right + 1])) right += 1;
104
+ const token = chars
105
+ .slice(left, right + 1)
106
+ .join("")
107
+ .trim();
108
+ return token.length > 0 ? token : undefined;
109
+ }
110
+
111
+ type SymbolNode = {
112
+ name?: string;
113
+ kind?: number;
114
+ detail?: string;
115
+ location?: { uri: string; range: Record<string, unknown> };
116
+ range?: Record<string, unknown>;
117
+ selectionRange?: Record<string, unknown>;
118
+ children?: SymbolNode[];
119
+ };
120
+
121
+ type SymbolMatch = {
122
+ name: string;
123
+ kind: string;
124
+ kindCode?: number;
125
+ detail?: string;
126
+ line?: number;
127
+ character?: number;
128
+ depth: number;
129
+ location?: { uri: string; range: Record<string, unknown> };
130
+ range?: Record<string, unknown>;
131
+ };
132
+
133
+ const SYMBOL_KIND_LABELS: Record<number, string> = {
134
+ 2: "module",
135
+ 3: "namespace",
136
+ 4: "package",
137
+ 5: "class",
138
+ 6: "method",
139
+ 7: "property",
140
+ 8: "field",
141
+ 9: "constructor",
142
+ 10: "enum",
143
+ 11: "interface",
144
+ 12: "function",
145
+ 13: "variable",
146
+ 14: "constant",
147
+ 15: "string",
148
+ 16: "number",
149
+ 17: "boolean",
150
+ 18: "array",
151
+ 19: "object",
152
+ 20: "key",
153
+ 21: "null",
154
+ 22: "enumMember",
155
+ 23: "struct",
156
+ 24: "event",
157
+ 25: "operator",
158
+ 26: "typeParameter",
159
+ };
160
+
161
+ function symbolKindLabel(kind: number | undefined): string {
162
+ return kind == null ? "symbol" : (SYMBOL_KIND_LABELS[kind] ?? "symbol");
163
+ }
164
+
165
+ function rangeStart(range: Record<string, unknown> | undefined): {
166
+ line?: number;
167
+ character?: number;
168
+ } {
169
+ const start = range?.start as
170
+ | { line?: unknown; character?: unknown }
171
+ | undefined;
172
+ return {
173
+ line: typeof start?.line === "number" ? start.line + 1 : undefined,
174
+ character:
175
+ typeof start?.character === "number" ? start.character + 1 : undefined,
176
+ };
177
+ }
178
+
179
+ function findSymbolMatches(
180
+ symbols: SymbolNode[],
181
+ query: string,
182
+ options: {
183
+ maxResults: number;
184
+ topLevelOnly: boolean;
185
+ exactMatch: boolean;
186
+ kinds: Set<string>;
187
+ },
188
+ ): SymbolMatch[] {
189
+ const normalizedQuery = query.trim().toLowerCase();
190
+ if (!normalizedQuery) return [];
191
+ const matches: SymbolMatch[] = [];
192
+
193
+ const matchesText = (symbol: SymbolNode): boolean => {
194
+ const values = [symbol.name, symbol.detail]
195
+ .filter((value): value is string => Boolean(value))
196
+ .map((value) => value.trim().toLowerCase());
197
+ return options.exactMatch
198
+ ? values.some((value) => value === normalizedQuery)
199
+ : values.some((value) => value.includes(normalizedQuery));
200
+ };
201
+
202
+ const matchesKind = (symbol: SymbolNode): boolean => {
203
+ if (options.kinds.size === 0) return true;
204
+ return options.kinds.has(symbolKindLabel(symbol.kind).toLowerCase());
205
+ };
206
+
207
+ const visit = (entries: SymbolNode[], depth: number): void => {
208
+ for (const symbol of entries) {
209
+ if (symbol.name && matchesText(symbol) && matchesKind(symbol)) {
210
+ const preferredRange = symbol.selectionRange ?? symbol.range;
211
+ const start = rangeStart(preferredRange);
212
+ matches.push({
213
+ name: symbol.name,
214
+ kind: symbolKindLabel(symbol.kind),
215
+ kindCode: symbol.kind,
216
+ detail: symbol.detail,
217
+ line: start.line,
218
+ character: start.character,
219
+ depth,
220
+ location: symbol.location,
221
+ range: preferredRange,
222
+ });
223
+ if (matches.length >= options.maxResults) return;
224
+ }
225
+ if (!options.topLevelOnly && symbol.children?.length) {
226
+ visit(symbol.children, depth + 1);
227
+ if (matches.length >= options.maxResults) return;
228
+ }
229
+ }
230
+ };
231
+
232
+ visit(symbols, 1);
233
+ return matches;
234
+ }
235
+
236
+ function flattenSymbols(symbols: SymbolNode[]): SymbolNode[] {
237
+ const all: SymbolNode[] = [];
238
+ for (const symbol of symbols) {
239
+ all.push(symbol);
240
+ if (symbol.children && symbol.children.length > 0) {
241
+ all.push(...flattenSymbols(symbol.children));
242
+ }
243
+ }
244
+ return all;
245
+ }
246
+
247
+ function pickLocalSymbolLocation(
248
+ symbols: SymbolNode[],
249
+ token: string,
250
+ filePath: string,
251
+ ): Array<{ uri: string; range: Record<string, unknown> }> {
252
+ const flat = flattenSymbols(symbols).filter(
253
+ (symbol) => symbol.name === token,
254
+ );
255
+ if (flat.length === 0) return [];
256
+ const uri = pathToFileURL(filePath).href;
257
+ return flat
258
+ .map((symbol) => {
259
+ if (symbol.location?.uri && symbol.location.range) {
260
+ return { uri: symbol.location.uri, range: symbol.location.range };
261
+ }
262
+ if (symbol.range) {
263
+ return { uri, range: symbol.range };
264
+ }
265
+ return undefined;
266
+ })
267
+ .filter((entry): entry is { uri: string; range: Record<string, unknown> } =>
268
+ Boolean(entry),
269
+ );
270
+ }
271
+
272
+ function classifyCodeActions(actions: Array<{ kind?: string }> | undefined): {
273
+ quickfix: number;
274
+ refactor: number;
275
+ other: number;
276
+ } {
277
+ if (!actions || actions.length === 0)
278
+ return { quickfix: 0, refactor: 0, other: 0 };
279
+ let quickfix = 0;
280
+ let refactor = 0;
281
+ let other = 0;
282
+ for (const action of actions) {
283
+ const kind = action.kind ?? "";
284
+ if (kind.startsWith("quickfix")) quickfix += 1;
285
+ else if (kind.startsWith("refactor")) refactor += 1;
286
+ else other += 1;
287
+ }
288
+ return { quickfix, refactor, other };
289
+ }
290
+
291
+ async function openFileBestEffort(
292
+ lspService: ReturnType<typeof getLSPService>,
293
+ filePath: string,
294
+ waitForDiagnostics = false,
295
+ ): Promise<void> {
296
+ let fileContent: string | undefined;
297
+ try {
298
+ fileContent = nodeFs.readFileSync(filePath, "utf-8");
299
+ } catch {
300
+ return;
301
+ }
302
+ if (!fileContent) return;
303
+ try {
304
+ if (typeof lspService.touchFile === "function") {
305
+ await lspService.touchFile(filePath, fileContent, {
306
+ diagnostics: waitForDiagnostics ? "document" : "none",
307
+ source: "lsp_navigation",
308
+ clientScope: waitForDiagnostics ? "all" : "primary",
309
+ });
310
+ } else {
311
+ await lspService.openFile(filePath, fileContent);
312
+ }
313
+ } catch {
314
+ /* LSP server may not be ready yet — proceed anyway */
315
+ }
316
+ }
317
+
318
+ async function handleWorkspaceDiagnosticsOperation(args: {
319
+ operation: LspNavigationOperation;
320
+ lspService: ReturnType<typeof getLSPService>;
321
+ rawPath?: string;
322
+ filePath: string;
323
+ filePathIsDirectory: boolean;
324
+ setDiagnosticsMode: (mode: "pull" | "push-only" | "unknown") => void;
325
+ finalize: (
326
+ payload: {
327
+ content: Array<{ type: "text"; text: string }>;
328
+ isError?: boolean;
329
+ details?: Record<string, unknown>;
330
+ },
331
+ meta: {
332
+ operation: string;
333
+ filePath: string;
334
+ failureKind: string;
335
+ resultCount: number;
336
+ },
337
+ ) => any;
338
+ }): Promise<any> {
339
+ const {
340
+ operation,
341
+ lspService,
342
+ rawPath,
343
+ filePath,
344
+ filePathIsDirectory,
345
+ setDiagnosticsMode,
346
+ finalize,
347
+ } = args;
348
+ const wsDiagSupport = await lspService.getWorkspaceDiagnosticsSupport(
349
+ rawPath ? filePath : undefined,
350
+ );
351
+ const diagnosticsMode = wsDiagSupport?.mode ?? "unknown";
352
+ setDiagnosticsMode(diagnosticsMode);
353
+
354
+ if (rawPath && !filePathIsDirectory) {
355
+ const hasLSP = lspService.supportsLSP(filePath);
356
+ if (!hasLSP) {
357
+ return finalize(
358
+ {
359
+ content: [
360
+ {
361
+ type: "text",
362
+ text: `No LSP server available for ${path.basename(filePath)}. Check that the language server is installed.`,
363
+ },
364
+ ],
365
+ isError: true,
366
+ },
367
+ { operation, filePath, failureKind: "no_server", resultCount: 0 },
368
+ );
369
+ }
370
+ await openFileBestEffort(lspService, filePath, true);
371
+ const diagnostics = await lspService.getDiagnostics(filePath);
372
+ const result = [{ filePath, diagnostics, count: diagnostics.length }];
373
+ const noteMap: Record<string, string> = {
374
+ pull: "Note: filePath mode requests pull diagnostics for this file and returns the aggregated result.",
375
+ "push-only":
376
+ "Note: server is push-only; result depends on published diagnostics for this file.",
377
+ };
378
+ const note =
379
+ noteMap[diagnosticsMode] ??
380
+ "Note: workspace diagnostics mode unknown (no active capability snapshot).";
381
+ const resultCount = diagnostics.length;
382
+ return finalize(
383
+ {
384
+ content: [
385
+ { type: "text", text: `${note}\n${JSON.stringify(result, null, 2)}` },
386
+ ],
387
+ details: {
388
+ operation,
389
+ resultCount,
390
+ diagnosticsMode,
391
+ coverage: "requested-file",
392
+ },
393
+ },
394
+ {
395
+ operation,
396
+ filePath,
397
+ failureKind: resultCount === 0 ? "empty_result" : "success",
398
+ resultCount,
399
+ },
400
+ );
401
+ }
402
+
403
+ const allDiagnostics = await lspService.getAllDiagnostics();
404
+ const result = Array.from(allDiagnostics.entries()).map(
405
+ ([trackedFile, { diags }]) => ({
406
+ filePath: trackedFile,
407
+ diagnostics: diags,
408
+ count: diags.length,
409
+ }),
410
+ );
411
+ const noteMap2: Record<string, string> = {
412
+ "push-only":
413
+ "Note: push-only tracked diagnostics snapshot (not full workspace pull diagnostics).",
414
+ pull: "Note: tracked diagnostics snapshot from active clients. Provide filePath to force file-level diagnostics collection.",
415
+ };
416
+ const note =
417
+ noteMap2[diagnosticsMode] ??
418
+ "Note: workspace diagnostics mode unknown (no active capability snapshot).";
419
+ return finalize(
420
+ {
421
+ content: [
422
+ { type: "text", text: `${note}\n${JSON.stringify(result, null, 2)}` },
423
+ ],
424
+ details: {
425
+ operation,
426
+ resultCount: result.length,
427
+ diagnosticsMode,
428
+ coverage: "tracked-open-files",
429
+ },
430
+ },
431
+ {
432
+ operation,
433
+ filePath: rawPath ? filePath : "(workspace)",
434
+ failureKind:
435
+ diagnosticsMode === "push-only" ? "tracked_snapshot" : "success",
436
+ resultCount: result.length,
437
+ },
438
+ );
439
+ }
440
+
441
+ async function runLspOperation(args: {
442
+ operation: LspNavigationOperation;
443
+ lspService: ReturnType<typeof getLSPService>;
444
+ filePath: string;
445
+ rawPath?: string;
446
+ lspLine: number;
447
+ lspChar: number;
448
+ lspEndLine: number;
449
+ lspEndChar: number;
450
+ query?: string;
451
+ maxResults?: number;
452
+ topLevelOnly?: boolean;
453
+ exactMatch?: boolean;
454
+ kinds?: string[];
455
+ newName?: string;
456
+ apply?: boolean;
457
+ ctxCwd: string;
458
+ params: Record<string, unknown>;
459
+ setSupported: (value: boolean | null) => void;
460
+ }): Promise<unknown> {
461
+ const {
462
+ operation,
463
+ lspService,
464
+ filePath,
465
+ rawPath,
466
+ lspLine,
467
+ lspChar,
468
+ lspEndLine,
469
+ lspEndChar,
470
+ query,
471
+ maxResults,
472
+ topLevelOnly,
473
+ exactMatch,
474
+ kinds,
475
+ newName,
476
+ apply,
477
+ ctxCwd,
478
+ params,
479
+ setSupported,
480
+ } = args;
481
+
482
+ switch (operation) {
483
+ case "definition":
484
+ return lspService.definition(filePath, lspLine, lspChar);
485
+ case "references":
486
+ return lspService.references(filePath, lspLine, lspChar);
487
+ case "hover":
488
+ return lspService.hover(filePath, lspLine, lspChar);
489
+ case "signatureHelp":
490
+ return lspService.signatureHelp(filePath, lspLine, lspChar);
491
+ case "documentSymbol":
492
+ return lspService.documentSymbol(filePath);
493
+ case "findSymbol": {
494
+ if (!query || query.trim().length === 0) {
495
+ throw new Error("__BADINPUT__ query parameter required for findSymbol");
496
+ }
497
+ const symbols = (await lspService.documentSymbol(
498
+ filePath,
499
+ )) as SymbolNode[];
500
+ return findSymbolMatches(symbols, query, {
501
+ maxResults: Math.max(1, Math.min(100, maxResults ?? 20)),
502
+ topLevelOnly: topLevelOnly ?? false,
503
+ exactMatch: exactMatch ?? false,
504
+ kinds: new Set(
505
+ (kinds ?? [])
506
+ .map((kind) => kind.trim().toLowerCase())
507
+ .filter(Boolean),
508
+ ),
509
+ });
510
+ }
511
+ case "workspaceSymbol": {
512
+ const support = await lspService.getOperationSupport(
513
+ rawPath ? filePath : undefined,
514
+ );
515
+ setSupported(operationSupportStatus(operation, support));
516
+ if (operationSupportStatus(operation, support) === false) {
517
+ throw new Error(
518
+ "__UNSUPPORTED__ Active LSP server does not advertise support for workspaceSymbol",
519
+ );
520
+ }
521
+ if (!query || query.trim().length === 0) {
522
+ throw new Error(
523
+ "__BADINPUT__ query parameter required for workspaceSymbol",
524
+ );
525
+ }
526
+ if (rawPath) await openFileBestEffort(lspService, filePath);
527
+ try {
528
+ const raw = await lspService.workspaceSymbol(
529
+ query ?? "",
530
+ rawPath ? filePath : undefined,
531
+ );
532
+ const NAVIGABLE_KINDS = new Set([5, 6, 8, 11, 12, 13, 22, 23]);
533
+ const filtered = (Array.isArray(raw) ? raw : [raw]).filter(
534
+ (s) =>
535
+ typeof s === "object" &&
536
+ s !== null &&
537
+ (!s.kind || NAVIGABLE_KINDS.has(s.kind)),
538
+ );
539
+ return filtered.slice(0, 15);
540
+ } catch (err) {
541
+ const msg = err instanceof Error ? err.message : String(err);
542
+ if (rawPath && /No Project/i.test(msg)) {
543
+ await openFileBestEffort(lspService, filePath);
544
+ await new Promise((resolve) => setTimeout(resolve, 120));
545
+ return lspService.workspaceSymbol(query ?? "", filePath);
546
+ }
547
+ throw err;
548
+ }
549
+ }
550
+ case "codeAction":
551
+ return lspService.codeAction(
552
+ filePath,
553
+ lspLine,
554
+ lspChar,
555
+ lspEndLine,
556
+ lspEndChar,
557
+ );
558
+ case "rename": {
559
+ if (!newName || newName.trim().length === 0) {
560
+ throw new Error("__BADINPUT__ newName parameter required for rename");
561
+ }
562
+ const edit = await lspService.rename(filePath, lspLine, lspChar, newName);
563
+ if (!edit) return null;
564
+ if (!apply) {
565
+ return {
566
+ applied: false,
567
+ summary: summarizeWorkspaceEdit(edit, ctxCwd),
568
+ edit,
569
+ };
570
+ }
571
+ const applied = await applyWorkspaceEdit(edit, ctxCwd);
572
+ for (const touchedFile of applied.files) {
573
+ try {
574
+ await openFileBestEffort(lspService, touchedFile, false);
575
+ } catch {
576
+ // Best-effort LSP resync only; disk edit already succeeded.
577
+ }
578
+ }
579
+ return { applied: true, ...applied };
580
+ }
581
+ case "implementation":
582
+ return lspService.implementation(filePath, lspLine, lspChar);
583
+ case "prepareCallHierarchy":
584
+ return lspService.prepareCallHierarchy(filePath, lspLine, lspChar);
585
+ case "incomingCalls": {
586
+ const callItem = (params as { callHierarchyItem?: LSPCallHierarchyItem })
587
+ .callHierarchyItem;
588
+ if (!callItem) {
589
+ throw new Error(
590
+ "__BADINPUT__ callHierarchyItem parameter required for incomingCalls",
591
+ );
592
+ }
593
+ return lspService.incomingCalls(callItem);
594
+ }
595
+ case "outgoingCalls": {
596
+ const callItem = (params as { callHierarchyItem?: LSPCallHierarchyItem })
597
+ .callHierarchyItem;
598
+ if (!callItem) {
599
+ throw new Error(
600
+ "__BADINPUT__ callHierarchyItem parameter required for outgoingCalls",
601
+ );
602
+ }
603
+ return lspService.outgoingCalls(callItem);
604
+ }
605
+ default:
606
+ return [];
607
+ }
608
+ }
609
+
610
+ async function executeOperationWithFallback(args: {
611
+ runOperation: () => Promise<unknown>;
612
+ operation: LspNavigationOperation;
613
+ needsFilePath: boolean;
614
+ filePath: string;
615
+ line?: number;
616
+ character?: number;
617
+ lspService: ReturnType<typeof getLSPService>;
618
+ }): Promise<{ result: unknown; usedDocumentSymbolFallback: boolean }> {
619
+ let result = await args.runOperation();
620
+ const isEmptyInitial =
621
+ !result || (Array.isArray(result) && result.length === 0);
622
+ const shouldRetryOnEmpty =
623
+ isEmptyInitial &&
624
+ args.needsFilePath &&
625
+ [
626
+ "definition",
627
+ "references",
628
+ "hover",
629
+ "signatureHelp",
630
+ "codeAction",
631
+ "rename",
632
+ "implementation",
633
+ ].includes(args.operation);
634
+ if (shouldRetryOnEmpty) {
635
+ await openFileBestEffort(args.lspService, args.filePath, true);
636
+ result = await args.runOperation();
637
+ }
638
+
639
+ let usedDocumentSymbolFallback = false;
640
+ const stillEmpty = !result || (Array.isArray(result) && result.length === 0);
641
+ if (stillEmpty && args.needsFilePath && args.operation === "definition") {
642
+ const content = nodeFs.readFileSync(args.filePath, "utf-8");
643
+ const token =
644
+ args.line && args.character
645
+ ? tokenAtPosition(content, args.line, args.character)
646
+ : undefined;
647
+ if (token) {
648
+ const docSymbols = (await args.lspService.documentSymbol(
649
+ args.filePath,
650
+ )) as SymbolNode[];
651
+ const locations = pickLocalSymbolLocation(
652
+ docSymbols,
653
+ token,
654
+ args.filePath,
655
+ );
656
+ if (locations.length > 0) {
657
+ result = locations;
658
+ usedDocumentSymbolFallback = true;
659
+ }
660
+ }
661
+ }
662
+ return { result, usedDocumentSymbolFallback };
663
+ }
664
+
665
+ function buildNavigationOutput(args: {
666
+ result: unknown;
667
+ operation: LspNavigationOperation;
668
+ filePath: string;
669
+ rawPath?: string;
670
+ line?: number;
671
+ character?: number;
672
+ usedDocumentSymbolFallback: boolean;
673
+ }): {
674
+ output: string;
675
+ actionStats: { quickfix: number; refactor: number; other: number } | null;
676
+ isEmpty: boolean;
677
+ resultCount: number;
678
+ } {
679
+ const {
680
+ result,
681
+ operation,
682
+ filePath,
683
+ rawPath,
684
+ line,
685
+ character,
686
+ usedDocumentSymbolFallback,
687
+ } = args;
688
+ const isEmpty = !result || (Array.isArray(result) && result.length === 0);
689
+ const fileCtx = filePath ? " at " + path.basename(filePath) : "";
690
+ const lineCtx = line ? ":" + line + ":" + character : "";
691
+ let output = isEmpty
692
+ ? "No results for " + operation + fileCtx + lineCtx
693
+ : JSON.stringify(result, null, 2);
694
+ if (isEmpty && operation === "workspaceSymbol" && !rawPath) {
695
+ output +=
696
+ "\nHint: provide filePath to scope workspaceSymbol to the active language server/root.";
697
+ }
698
+ if (usedDocumentSymbolFallback) {
699
+ output +=
700
+ "\nNote: served from documentSymbol fallback due to empty primary result.";
701
+ }
702
+ if (
703
+ operation === "references" &&
704
+ Array.isArray(result) &&
705
+ result.length <= 2
706
+ ) {
707
+ output +=
708
+ "\nHint: references from usage sites can be partial; retry from the symbol definition for broader cross-file results.";
709
+ }
710
+ const actionStats =
711
+ operation === "codeAction" && Array.isArray(result)
712
+ ? classifyCodeActions(result as Array<{ kind?: string }>)
713
+ : null;
714
+ if (
715
+ operation === "codeAction" &&
716
+ actionStats?.quickfix === 0 &&
717
+ actionStats.refactor > 0
718
+ ) {
719
+ output +=
720
+ "\nNote: no diagnostic quick fixes returned; refactor-only actions available.";
721
+ }
722
+ const resultCount = Array.isArray(result) ? result.length : result ? 1 : 0;
723
+ return { output, actionStats, isEmpty, resultCount };
724
+ }
725
+
726
+ export function createLspNavigationTool(
727
+ getFlag: (name: string) => boolean | string | undefined,
728
+ ) {
729
+ return {
730
+ name: "lsp_navigation" as const,
731
+ label: "LSP Navigate",
732
+ description:
733
+ "Navigate code using LSP (Language Server Protocol). LSP is enabled by default; disable with --no-lsp.\n" +
734
+ "Operations:\n" +
735
+ "- definition: Jump to where a symbol is defined\n" +
736
+ "- references: Find all usages of a symbol\n" +
737
+ "- hover: Get type/doc info at a position\n" +
738
+ "- signatureHelp: Show callable signatures at cursor\n" +
739
+ "- documentSymbol: List all symbols (functions/classes/vars) in a file\n" +
740
+ "- findSymbol: Search document symbols in a file by name/detail with optional kind/top-level/exact filters\n" +
741
+ "- workspaceSymbol: Search symbols across the whole project (best with filePath context)\n" +
742
+ "- codeAction: Find available quick fixes/refactors at a range\n" +
743
+ "- rename: Compute or apply workspace edits for renaming a symbol\n" +
744
+ "- implementation: Jump to interface implementations\n" +
745
+ "- prepareCallHierarchy: Get callable item at position (for incoming/outgoing)\n" +
746
+ "- incomingCalls: Find all functions/methods that CALL this function\n" +
747
+ "- outgoingCalls: Find all functions/methods CALLED by this function\n" +
748
+ "- workspaceDiagnostics: List all diagnostics tracked by active LSP clients\n\n" +
749
+ "Line and character are 1-based (as shown in editors).",
750
+ promptSnippet:
751
+ "Use lsp_navigation to find definitions, references, and hover info via LSP",
752
+ parameters: Type.Object({
753
+ operation: Type.String({
754
+ description:
755
+ "LSP operation to perform. Valid values: " +
756
+ VALID_OPERATIONS.join(", "),
757
+ }),
758
+ filePath: Type.Optional(
759
+ Type.String({
760
+ description:
761
+ "Absolute or relative file path. Required for file-scoped operations; optional for workspaceSymbol/workspaceDiagnostics.",
762
+ }),
763
+ ),
764
+ line: Type.Optional(
765
+ Type.Number({
766
+ description:
767
+ "Line number (1-based). Required for definition/references/hover/implementation",
768
+ }),
769
+ ),
770
+ character: Type.Optional(
771
+ Type.Number({
772
+ description:
773
+ "Character offset (1-based). Required for definition/references/hover/implementation",
774
+ }),
775
+ ),
776
+ endLine: Type.Optional(
777
+ Type.Number({
778
+ description:
779
+ "End line (1-based). Optional; used by codeAction range.",
780
+ }),
781
+ ),
782
+ endCharacter: Type.Optional(
783
+ Type.Number({
784
+ description:
785
+ "End character (1-based). Optional; used by codeAction range.",
786
+ }),
787
+ ),
788
+ newName: Type.Optional(
789
+ Type.String({
790
+ description: "Required for rename operation.",
791
+ }),
792
+ ),
793
+ apply: Type.Optional(
794
+ Type.Boolean({
795
+ description:
796
+ "rename only: apply the returned workspace edit to disk (default: false; preview only).",
797
+ }),
798
+ ),
799
+ query: Type.Optional(
800
+ Type.String({
801
+ description:
802
+ "Symbol name to search. Used by workspaceSymbol and findSymbol.",
803
+ }),
804
+ ),
805
+ kinds: Type.Optional(
806
+ Type.Array(Type.String(), {
807
+ description:
808
+ "findSymbol only: restrict matches to symbol kind labels such as function, class, method, variable, interface.",
809
+ }),
810
+ ),
811
+ exactMatch: Type.Optional(
812
+ Type.Boolean({
813
+ description:
814
+ "findSymbol only: match whole symbol names/details exactly instead of substring matching.",
815
+ }),
816
+ ),
817
+ topLevelOnly: Type.Optional(
818
+ Type.Boolean({
819
+ description: "findSymbol only: do not search nested child symbols.",
820
+ }),
821
+ ),
822
+ maxResults: Type.Optional(
823
+ Type.Number({
824
+ description:
825
+ "findSymbol only: maximum matches to return. Default 20.",
826
+ }),
827
+ ),
828
+ callHierarchyItem: Type.Optional(
829
+ Type.Object(
830
+ {
831
+ name: Type.String(),
832
+ kind: Type.Number(),
833
+ uri: Type.String(),
834
+ range: Type.Object({
835
+ start: Type.Object({
836
+ line: Type.Number(),
837
+ character: Type.Number(),
838
+ }),
839
+ end: Type.Object({
840
+ line: Type.Number(),
841
+ character: Type.Number(),
842
+ }),
843
+ }),
844
+ selectionRange: Type.Object({
845
+ start: Type.Object({
846
+ line: Type.Number(),
847
+ character: Type.Number(),
848
+ }),
849
+ end: Type.Object({
850
+ line: Type.Number(),
851
+ character: Type.Number(),
852
+ }),
853
+ }),
854
+ },
855
+ {
856
+ description:
857
+ "Call hierarchy item. Required for incomingCalls/outgoingCalls",
858
+ },
859
+ ),
860
+ ),
861
+ }),
862
+ async execute(
863
+ _toolCallId: string,
864
+ params: Record<string, unknown>,
865
+ _signal: AbortSignal,
866
+ _onUpdate: unknown,
867
+ ctx: { cwd?: string },
868
+ ) {
869
+ const startedAt = Date.now();
870
+ let supported: boolean | null = null;
871
+ let diagnosticsMode: "pull" | "push-only" | "unknown" = "unknown";
872
+
873
+ const finalize = (
874
+ payload: {
875
+ content: Array<{ type: "text"; text: string }>;
876
+ isError?: boolean;
877
+ details?: Record<string, unknown>;
878
+ },
879
+ meta: {
880
+ operation: string;
881
+ filePath: string;
882
+ failureKind: string;
883
+ resultCount: number;
884
+ },
885
+ ): typeof payload & {
886
+ details: typeof payload.details & {
887
+ failureKind: string;
888
+ };
889
+ } => {
890
+ const normalizedFilePath = meta.filePath.replace(/\\/g, "/");
891
+ logLatency({
892
+ type: "phase",
893
+ phase: "lsp_navigation_result",
894
+ filePath: normalizedFilePath,
895
+ durationMs: Date.now() - startedAt,
896
+ metadata: {
897
+ operation: meta.operation,
898
+ failureKind: meta.failureKind,
899
+ resultCount: meta.resultCount,
900
+ supported,
901
+ diagnosticsMode,
902
+ },
903
+ });
904
+
905
+ return {
906
+ ...payload,
907
+ details: {
908
+ ...(payload.details ?? {}),
909
+ failureKind: meta.failureKind,
910
+ },
911
+ };
912
+ };
913
+
914
+ if (getFlag("no-lsp")) {
915
+ return finalize(
916
+ {
917
+ content: [
918
+ {
919
+ type: "text" as const,
920
+ text: "lsp_navigation requires LSP to be enabled. Remove --no-lsp to use LSP navigation.",
921
+ },
922
+ ],
923
+ isError: true,
924
+ },
925
+ {
926
+ operation: "precheck",
927
+ filePath: "(workspace)",
928
+ failureKind: "lsp_disabled",
929
+ resultCount: 0,
930
+ },
931
+ );
932
+ }
933
+
934
+ const {
935
+ operation: rawOperation,
936
+ filePath: rawPath,
937
+ line,
938
+ character,
939
+ endLine,
940
+ endCharacter,
941
+ newName,
942
+ apply,
943
+ query,
944
+ kinds,
945
+ exactMatch,
946
+ topLevelOnly,
947
+ maxResults,
948
+ } = params as {
949
+ operation: string;
950
+ filePath?: string;
951
+ line?: number;
952
+ character?: number;
953
+ endLine?: number;
954
+ endCharacter?: number;
955
+ newName?: string;
956
+ apply?: boolean;
957
+ query?: string;
958
+ kinds?: string[];
959
+ exactMatch?: boolean;
960
+ topLevelOnly?: boolean;
961
+ maxResults?: number;
962
+ };
963
+ const normalizedOperation = normalizeOperation(rawOperation);
964
+ if (!isValidOperation(normalizedOperation)) {
965
+ return finalize(
966
+ {
967
+ content: [
968
+ {
969
+ type: "text" as const,
970
+ text:
971
+ `Unknown lsp_navigation operation "${normalizedOperation || String(rawOperation ?? "") || ""}". ` +
972
+ `Valid operations: ${VALID_OPERATIONS.join(", ")}`,
973
+ },
974
+ ],
975
+ isError: true,
976
+ details: {
977
+ rawOperation,
978
+ normalizedOperation,
979
+ validOperations: VALID_OPERATIONS,
980
+ },
981
+ },
982
+ {
983
+ operation: normalizedOperation || "invalid",
984
+ filePath: "(workspace)",
985
+ failureKind: "invalid_operation",
986
+ resultCount: 0,
987
+ },
988
+ );
989
+ }
990
+ const operation = normalizedOperation;
991
+
992
+ const isCallHierarchyTraversal =
993
+ operation === "incomingCalls" || operation === "outgoingCalls";
994
+ const needsFilePath =
995
+ operation !== "workspaceDiagnostics" &&
996
+ operation !== "workspaceSymbol" &&
997
+ !isCallHierarchyTraversal;
998
+ if (needsFilePath && (!rawPath || rawPath.trim().length === 0)) {
999
+ return finalize(
1000
+ {
1001
+ content: [
1002
+ {
1003
+ type: "text" as const,
1004
+ text: `filePath is required for ${operation}`,
1005
+ },
1006
+ ],
1007
+ isError: true,
1008
+ },
1009
+ {
1010
+ operation,
1011
+ filePath: "(workspace)",
1012
+ failureKind: "missing_file_path",
1013
+ resultCount: 0,
1014
+ },
1015
+ );
1016
+ }
1017
+
1018
+ const filePath = rawPath
1019
+ ? path.isAbsolute(rawPath)
1020
+ ? rawPath
1021
+ : path.resolve(ctx.cwd || ".", rawPath)
1022
+ : "";
1023
+
1024
+ let filePathIsDirectory = false;
1025
+ if (filePath) {
1026
+ try {
1027
+ filePathIsDirectory = nodeFs.statSync(filePath).isDirectory();
1028
+ } catch {
1029
+ // non-existent path — existing error paths handle this
1030
+ }
1031
+ }
1032
+
1033
+ const lspService = getLSPService();
1034
+ if (operation === "workspaceDiagnostics") {
1035
+ return handleWorkspaceDiagnosticsOperation({
1036
+ operation,
1037
+ lspService,
1038
+ rawPath,
1039
+ filePath,
1040
+ filePathIsDirectory,
1041
+ setDiagnosticsMode: (mode) => {
1042
+ diagnosticsMode = mode;
1043
+ },
1044
+ finalize,
1045
+ });
1046
+ }
1047
+
1048
+ if (needsFilePath && filePathIsDirectory) {
1049
+ return finalize(
1050
+ {
1051
+ content: [
1052
+ {
1053
+ type: "text" as const,
1054
+ text: `filePath must be a source file, got directory: ${filePath}. Pass a source file path, or omit filePath for workspace-level operations.`,
1055
+ },
1056
+ ],
1057
+ isError: true,
1058
+ },
1059
+ {
1060
+ operation,
1061
+ filePath,
1062
+ failureKind: "filepath_is_directory",
1063
+ resultCount: 0,
1064
+ },
1065
+ );
1066
+ }
1067
+
1068
+ const hasLSP = filePath ? lspService.supportsLSP(filePath) : false;
1069
+ if (needsFilePath && !hasLSP) {
1070
+ return finalize(
1071
+ {
1072
+ content: [
1073
+ {
1074
+ type: "text" as const,
1075
+ text: `No LSP server available for ${path.basename(filePath)}. Check that the language server is installed.`,
1076
+ },
1077
+ ],
1078
+ isError: true,
1079
+ },
1080
+ {
1081
+ operation,
1082
+ filePath,
1083
+ failureKind: "no_server",
1084
+ resultCount: 0,
1085
+ },
1086
+ );
1087
+ }
1088
+
1089
+ if (needsFilePath) {
1090
+ const support = await lspService.getOperationSupport(filePath);
1091
+ supported = operationSupportStatus(operation, support);
1092
+ if (supported === false) {
1093
+ return finalize(
1094
+ {
1095
+ content: [
1096
+ {
1097
+ type: "text" as const,
1098
+ text: `LSP server for ${path.basename(filePath)} does not advertise support for ${operation}`,
1099
+ },
1100
+ ],
1101
+ isError: true,
1102
+ details: {
1103
+ operation,
1104
+ supported: false,
1105
+ emptyReason: "unsupported",
1106
+ },
1107
+ },
1108
+ { operation, filePath, failureKind: "unsupported", resultCount: 0 },
1109
+ );
1110
+ }
1111
+
1112
+ await openFileBestEffort(lspService, filePath);
1113
+ }
1114
+
1115
+ // Convert 1-based editor coords to 0-based LSP coords
1116
+ const lspLine = (line ?? 1) - 1;
1117
+ const lspChar = (character ?? 1) - 1;
1118
+ const lspEndLine = (endLine ?? line ?? 1) - 1;
1119
+ const lspEndChar = (endCharacter ?? character ?? 1) - 1;
1120
+
1121
+ const runOperation = async (): Promise<unknown> =>
1122
+ runLspOperation({
1123
+ operation,
1124
+ lspService,
1125
+ filePath,
1126
+ rawPath,
1127
+ lspLine,
1128
+ lspChar,
1129
+ lspEndLine,
1130
+ lspEndChar,
1131
+ query,
1132
+ maxResults,
1133
+ topLevelOnly,
1134
+ exactMatch,
1135
+ kinds,
1136
+ newName,
1137
+ apply,
1138
+ ctxCwd: ctx.cwd || ".",
1139
+ params,
1140
+ setSupported: (value) => {
1141
+ supported = value;
1142
+ },
1143
+ });
1144
+
1145
+ let result: unknown;
1146
+ let usedDocumentSymbolFallback = false;
1147
+ try {
1148
+ const resolved = await executeOperationWithFallback({
1149
+ runOperation,
1150
+ operation,
1151
+ needsFilePath,
1152
+ filePath,
1153
+ line,
1154
+ character,
1155
+ lspService,
1156
+ });
1157
+ result = resolved.result;
1158
+ usedDocumentSymbolFallback = resolved.usedDocumentSymbolFallback;
1159
+ } catch (err) {
1160
+ const msg = err instanceof Error ? err.message : String(err);
1161
+ if (msg.startsWith("__UNSUPPORTED__ ")) {
1162
+ return finalize(
1163
+ {
1164
+ content: [
1165
+ {
1166
+ type: "text" as const,
1167
+ text: msg.replace("__UNSUPPORTED__ ", ""),
1168
+ },
1169
+ ],
1170
+ isError: true,
1171
+ details: {
1172
+ operation,
1173
+ supported: false,
1174
+ emptyReason: "unsupported",
1175
+ },
1176
+ },
1177
+ { operation, filePath, failureKind: "unsupported", resultCount: 0 },
1178
+ );
1179
+ }
1180
+ if (msg.startsWith("__BADINPUT__ ")) {
1181
+ return finalize(
1182
+ {
1183
+ content: [
1184
+ {
1185
+ type: "text" as const,
1186
+ text: msg.replace("__BADINPUT__ ", ""),
1187
+ },
1188
+ ],
1189
+ isError: true,
1190
+ details: {},
1191
+ },
1192
+ { operation, filePath, failureKind: "bad_input", resultCount: 0 },
1193
+ );
1194
+ }
1195
+ return finalize(
1196
+ {
1197
+ content: [
1198
+ {
1199
+ type: "text" as const,
1200
+ text: `LSP error: ${err instanceof Error ? err.message : String(err)}`,
1201
+ },
1202
+ ],
1203
+ isError: true,
1204
+ details: {},
1205
+ },
1206
+ { operation, filePath, failureKind: "lsp_error", resultCount: 0 },
1207
+ );
1208
+ }
1209
+
1210
+ const rendered = buildNavigationOutput({
1211
+ result,
1212
+ operation,
1213
+ filePath,
1214
+ rawPath,
1215
+ line,
1216
+ character,
1217
+ usedDocumentSymbolFallback,
1218
+ });
1219
+ const { output, actionStats, isEmpty, resultCount } = rendered;
1220
+ return finalize(
1221
+ {
1222
+ content: [{ type: "text" as const, text: output }],
1223
+ details: {
1224
+ operation,
1225
+ supported,
1226
+ emptyReason: isEmpty
1227
+ ? emptyReasonForOperation(operation)
1228
+ : undefined,
1229
+ codeActionKinds: actionStats ?? undefined,
1230
+ resultCount,
1231
+ },
1232
+ },
1233
+ {
1234
+ operation,
1235
+ filePath: rawPath ? filePath : "(workspace)",
1236
+ failureKind: isEmpty
1237
+ ? "empty_result"
1238
+ : usedDocumentSymbolFallback
1239
+ ? "fallback_success"
1240
+ : "success",
1241
+ resultCount,
1242
+ },
1243
+ );
1244
+ },
1245
+ };
1246
+ }