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,182 @@
1
+ /**
2
+ * Path utilities for pi-lens
3
+ *
4
+ * Handles cross-platform path normalization, particularly
5
+ * Windows case-insensitivity issues when using paths as Map keys.
6
+ *
7
+ * Approach (inspired by OpenCode's Filesystem.normalizePath):
8
+ * - On Windows: try realpathSync.native() for canonical casing
9
+ * - Falls back to lowercase for files that don't exist yet
10
+ * - On non-Windows: return path as-is (case-sensitive filesystem)
11
+ * - Always convert backslashes to forward slashes for Map key consistency
12
+ */
13
+
14
+ import { existsSync, realpathSync } from "node:fs";
15
+ import { dirname, win32 } from "node:path";
16
+ import { fileURLToPath, pathToFileURL } from "node:url";
17
+
18
+ /**
19
+ * Detect if a path is a Windows path (has drive letter or UNC prefix).
20
+ */
21
+ function isWindowsPath(filePath: string): boolean {
22
+ return /^[A-Za-z]:/.test(filePath) || filePath.startsWith("\\\\");
23
+ }
24
+
25
+ /**
26
+ * Normalize a file path for consistent Map key usage.
27
+ *
28
+ * On Windows:
29
+ * - If the file exists: uses realpathSync.native() to get the canonical
30
+ * filesystem path (actual casing, resolved symlinks)
31
+ * - If the file doesn't exist: resolves the path and lowercases
32
+ * (needed for new files where we haven't written yet)
33
+ *
34
+ * On non-Windows: returns path as-is (case-sensitive filesystem).
35
+ *
36
+ * Always converts backslashes to forward slashes for consistent Map keys.
37
+ */
38
+ export function normalizeFilePath(filePath: string): string {
39
+ // Convert backslashes to forward slashes first
40
+ const normalized = filePath.replace(/\\/g, "/");
41
+
42
+ if (process.platform !== "win32" && !isWindowsPath(normalized)) {
43
+ return normalized;
44
+ }
45
+
46
+ // Windows: try realpathSync.native() for canonical casing
47
+ // This resolves symlinks and returns the actual filesystem casing
48
+ try {
49
+ const canonical = realpathSync.native(filePath);
50
+ return canonical.replace(/\\/g, "/");
51
+ } catch {
52
+ // File doesn't exist yet (new file) — resolve path and lowercase
53
+ // We need to walk up the directory tree to find the nearest existing
54
+ // parent, resolve its casing, then append the non-existent parts
55
+ try {
56
+ return resolveNonExisting(filePath);
57
+ } catch {
58
+ // Last resort: just lowercase the resolved path
59
+ const resolved = win32.normalize(win32.resolve(filePath));
60
+ return resolved.replace(/\\/g, "/").toLowerCase();
61
+ }
62
+ }
63
+ }
64
+
65
+ /**
66
+ * Resolve a non-existing path by finding the nearest existing parent,
67
+ * getting its canonical casing, then appending the non-existent parts lowercased.
68
+ *
69
+ * Example: C:\Users\Foo\newdir\file.ts
70
+ * - C:\Users\Foo exists → realpathSync gives C:\Users\Foo
71
+ * - newdir\file.ts doesn't exist → lowercased
72
+ * - Result: C:/Users/Foo/newdir/file.ts
73
+ */
74
+ function resolveNonExisting(filePath: string): string {
75
+ const resolved = win32.resolve(filePath);
76
+ let current = resolved;
77
+ const nonExistentParts: string[] = [];
78
+
79
+ // Walk up until we find an existing directory
80
+ while (true) {
81
+ if (existsSync(current)) {
82
+ // Found existing ancestor — get its canonical casing
83
+ const canonical = realpathSync.native(current);
84
+ if (nonExistentParts.length === 0) {
85
+ return canonical.replace(/\\/g, "/");
86
+ }
87
+ // Append non-existent parts (lowercased for consistency)
88
+ const tail = nonExistentParts.reverse().join("/").toLowerCase();
89
+ const base = canonical.replace(/\\/g, "/");
90
+ return base.endsWith("/") ? base + tail : `${base}/${tail}`;
91
+ }
92
+
93
+ const parent = dirname(current);
94
+ if (parent === current) {
95
+ // Reached filesystem root without finding existing dir
96
+ // Fall back to full lowercase
97
+ throw new Error("No existing parent found");
98
+ }
99
+
100
+ nonExistentParts.push(win32.basename(current));
101
+ current = parent;
102
+ }
103
+ }
104
+
105
+ /**
106
+ * Convert a file:// URI to a normalized path.
107
+ * Handles URL decoding and Windows drive letter normalization.
108
+ */
109
+ export function uriToPath(uri: string): string {
110
+ try {
111
+ const filePath = fileURLToPath(uri);
112
+ return normalizeFilePath(filePath);
113
+ } catch {
114
+ // Not a valid file:// URI, treat as plain path
115
+ return normalizeFilePath(uri);
116
+ }
117
+ }
118
+
119
+ /**
120
+ * Convert a path to a file:// URI.
121
+ * Does NOT normalize the path - URIs preserve original casing.
122
+ */
123
+ export function pathToUri(filePath: string): string {
124
+ return pathToFileURL(filePath).href;
125
+ }
126
+
127
+ /**
128
+ * Normalize a Map key lookup for file paths.
129
+ * Use this when getting/setting values in Maps that use file paths as keys.
130
+ */
131
+ export function normalizeMapKey(filePath: string): string {
132
+ return normalizeFilePath(filePath);
133
+ }
134
+
135
+ /**
136
+ * Compare two file paths for equality, handling Windows case-insensitivity
137
+ * and mixed separators (backslash vs forward slash).
138
+ */
139
+ export function pathsEqual(a: string, b: string): boolean {
140
+ return normalizeFilePath(a) === normalizeFilePath(b);
141
+ }
142
+
143
+ /**
144
+ * Check if `child` is under `parent` directory.
145
+ * Separator-agnostic and case-insensitive on Windows.
146
+ */
147
+ export function isUnderDir(child: string, parent: string): boolean {
148
+ const normChild = normalizeFilePath(child);
149
+ const normParent = normalizeFilePath(parent);
150
+ // Ensure parent ends with / for prefix matching
151
+ const parentPrefix = normParent.endsWith("/") ? normParent : `${normParent}/`;
152
+ return normChild === normParent || normChild.startsWith(parentPrefix);
153
+ }
154
+
155
+ const VENDOR_DIR_NAMES = new Set([
156
+ "node_modules",
157
+ "vendor",
158
+ "vendors",
159
+ "third_party",
160
+ "third-party",
161
+ ]);
162
+
163
+ /**
164
+ * Returns true when a file should be treated as external/vendor and excluded
165
+ * from pipelines (LSP, diagnostics, complexity, etc.).
166
+ *
167
+ * Cases:
168
+ * 1. Outside the project root entirely (e.g. global npm packages, system files)
169
+ * 2. Inside the project but under a vendor directory (node_modules, vendor, third_party, etc.)
170
+ */
171
+ export function isExternalOrVendorFile(
172
+ filePath: string,
173
+ projectRoot: string,
174
+ ): boolean {
175
+ if (!isUnderDir(filePath, projectRoot)) return true;
176
+ const normalized = normalizeFilePath(filePath);
177
+ const rootNorm = normalizeFilePath(projectRoot);
178
+ const rel = normalized.startsWith(rootNorm + "/")
179
+ ? normalized.slice(rootNorm.length + 1)
180
+ : normalized;
181
+ return rel.split("/").some((seg) => VENDOR_DIR_NAMES.has(seg));
182
+ }
@@ -0,0 +1,360 @@
1
+ /**
2
+ * Post-write pipeline for harness lens.
3
+ *
4
+ * The ultimate-pi harness keeps lens focused on edit safety, formatting, LSP sync,
5
+ * and secret blocking.
6
+ */
7
+
8
+ import * as nodeFs from "node:fs";
9
+ import * as path from "node:path";
10
+ import { detectFileKind, getFileKindLabel } from "./file-kinds.js";
11
+ import type { FormatService } from "./format-service.js";
12
+ import { logLatency } from "./latency-logger.js";
13
+ import { emitLensAnalysisComplete } from "./lens-events.js";
14
+ import { getLSPService } from "./lsp/index.js";
15
+ import { RUNTIME_CONFIG } from "./runtime-config.js";
16
+ import { formatSecrets, scanForSecrets } from "./secrets-scanner.js";
17
+
18
+ const LSP_MAX_FILE_BYTES = RUNTIME_CONFIG.pipeline.lspMaxFileBytes;
19
+ const LSP_MAX_FILE_LINES = RUNTIME_CONFIG.pipeline.lspMaxFileLines;
20
+ const LSP_SPAWN_BUDGET_MS = RUNTIME_CONFIG.pipeline.lspSpawnBudgetMs;
21
+
22
+ export interface PipelineContext {
23
+ filePath: string;
24
+ cwd: string;
25
+ toolName: string;
26
+ modifiedRanges?: { start: number; end: number }[];
27
+ telemetry?: {
28
+ model: string;
29
+ sessionId: string;
30
+ turnIndex: number;
31
+ writeIndex: number;
32
+ };
33
+ getFlag: (name: string) => boolean | string | undefined;
34
+ dbg: (msg: string) => void;
35
+ }
36
+
37
+ export interface PipelineDeps {
38
+ getFormatService: () => FormatService;
39
+ }
40
+
41
+ export interface PipelineResult {
42
+ output: string;
43
+ hasBlockers: boolean;
44
+ isError: boolean;
45
+ fileModified: boolean;
46
+ changedFiles?: string[];
47
+ }
48
+
49
+ interface PhaseTracker {
50
+ start(name: string): void;
51
+ end(name: string, metadata?: Record<string, unknown>): void;
52
+ }
53
+
54
+ type SecretDiagnostic = {
55
+ id: string;
56
+ message: string;
57
+ filePath: string;
58
+ line: number;
59
+ column: number;
60
+ severity: "error";
61
+ semantic: "blocking";
62
+ tool: "secrets-scanner";
63
+ rule: "secrets";
64
+ defectClass: "secrets";
65
+ };
66
+
67
+ function createPhaseTracker(toolName: string, filePath: string): PhaseTracker {
68
+ const phases: Array<{ name: string; startTime: number; ended: boolean }> = [];
69
+ return {
70
+ start(name: string) {
71
+ phases.push({ name, startTime: Date.now(), ended: false });
72
+ },
73
+ end(name: string, metadata?: Record<string, unknown>) {
74
+ const phase = phases.find((item) => item.name === name && !item.ended);
75
+ if (!phase) return;
76
+ phase.ended = true;
77
+ logLatency({
78
+ type: "phase",
79
+ toolName,
80
+ filePath,
81
+ phase: name,
82
+ durationMs: Date.now() - phase.startTime,
83
+ metadata,
84
+ });
85
+ },
86
+ };
87
+ }
88
+
89
+ function exceedsLspSyncLimits(content: string): {
90
+ tooLarge: boolean;
91
+ reason: string;
92
+ } {
93
+ const sizeBytes = Buffer.byteLength(content, "utf-8");
94
+ if (sizeBytes > LSP_MAX_FILE_BYTES) {
95
+ return {
96
+ tooLarge: true,
97
+ reason: `${Math.round(sizeBytes / 1024)}KB exceeds ${Math.round(LSP_MAX_FILE_BYTES / 1024)}KB`,
98
+ };
99
+ }
100
+
101
+ const lineCount = content.split("\n").length;
102
+ if (lineCount > LSP_MAX_FILE_LINES) {
103
+ return {
104
+ tooLarge: true,
105
+ reason: `${lineCount} lines exceeds ${LSP_MAX_FILE_LINES}`,
106
+ };
107
+ }
108
+
109
+ return { tooLarge: false, reason: "" };
110
+ }
111
+
112
+ function displayPath(cwd: string, filePath: string): string {
113
+ const relative = path.relative(cwd, filePath);
114
+ return relative && !relative.startsWith("..") && !path.isAbsolute(relative)
115
+ ? relative.replace(/\\/g, "/")
116
+ : filePath.replace(/\\/g, "/");
117
+ }
118
+
119
+ function buildAllClearOutput(elapsed: number, filePath: string): string {
120
+ const kind = detectFileKind(filePath);
121
+ const langLabel = kind ? getFileKindLabel(kind) : path.extname(filePath);
122
+ const parts = kind
123
+ ? [`${langLabel} clean`, `${elapsed}ms`]
124
+ : [`${elapsed}ms`];
125
+ return `✓ ${parts.join(" · ")}`;
126
+ }
127
+
128
+ export interface FormatPhaseResult {
129
+ formatChanged: boolean;
130
+ formattersUsed: string[];
131
+ formatFailures: string[];
132
+ fileContent: string | undefined;
133
+ }
134
+
135
+ export async function runFormatPhase(
136
+ filePath: string,
137
+ getFormatService: () => FormatService,
138
+ dbg: PipelineContext["dbg"],
139
+ ): Promise<FormatPhaseResult> {
140
+ let formatChanged = false;
141
+ let formattersUsed: string[] = [];
142
+ const formatFailures: string[] = [];
143
+ let fileContent: string | undefined;
144
+
145
+ const formatService = getFormatService();
146
+ try {
147
+ formatService.recordRead(filePath);
148
+ const result = await formatService.formatFile(filePath);
149
+ formattersUsed = result.formatters.map((formatter) => formatter.name);
150
+ if (result.anyChanged) {
151
+ formatChanged = true;
152
+ dbg(
153
+ "autoformat: " +
154
+ result.formatters
155
+ .map(
156
+ (formatter) =>
157
+ `${formatter.name}(${formatter.changed ? "changed" : "unchanged"})`,
158
+ )
159
+ .join(", "),
160
+ );
161
+ }
162
+ if (!result.allSucceeded) {
163
+ const failures = result.formatters.filter(
164
+ (formatter) => !formatter.success,
165
+ );
166
+ formatFailures.push(
167
+ ...failures.map(
168
+ (formatter) =>
169
+ `${formatter.name}: ${formatter.error ?? "unknown error"}`,
170
+ ),
171
+ );
172
+ dbg(
173
+ "autoformat: " +
174
+ failures
175
+ .map(
176
+ (formatter) =>
177
+ `${formatter.name} failed: ${formatter.error ?? "unknown error"}`,
178
+ )
179
+ .join("; "),
180
+ );
181
+ }
182
+ } catch (err) {
183
+ const message = err instanceof Error ? err.message : String(err);
184
+ formatFailures.push(message);
185
+ dbg(`autoformat error: ${err}`);
186
+ }
187
+
188
+ try {
189
+ fileContent = nodeFs.readFileSync(filePath, "utf-8");
190
+ } catch {
191
+ fileContent = undefined;
192
+ }
193
+
194
+ return { formatChanged, formattersUsed, formatFailures, fileContent };
195
+ }
196
+
197
+ export async function resyncLspFile(
198
+ filePath: string,
199
+ fileContent: string,
200
+ needsContentRefresh: boolean,
201
+ lspSyncCompleted: boolean,
202
+ getFlag: PipelineContext["getFlag"],
203
+ dbg: PipelineContext["dbg"],
204
+ formatChanged = false,
205
+ ): Promise<void> {
206
+ if (getFlag("no-lsp")) return;
207
+ if (!needsContentRefresh && lspSyncCompleted) return;
208
+ if (exceedsLspSyncLimits(fileContent).tooLarge) return;
209
+
210
+ try {
211
+ const lspService = getLSPService();
212
+ if (!lspService.supportsLSP(filePath)) return;
213
+ await lspService.openFile(filePath, fileContent, {
214
+ preserveDiagnostics: formatChanged,
215
+ spawnBudgetMs: LSP_SPAWN_BUDGET_MS,
216
+ });
217
+ } catch (err) {
218
+ dbg(`LSP resync error: ${err}`);
219
+ }
220
+ }
221
+
222
+ export async function runPipeline(
223
+ ctx: PipelineContext,
224
+ deps: PipelineDeps,
225
+ ): Promise<PipelineResult> {
226
+ const { filePath, cwd, toolName, getFlag, dbg } = ctx;
227
+ const { getFormatService } = deps;
228
+ const phase = createPhaseTracker(toolName, filePath);
229
+ const pipelineStart = Date.now();
230
+ phase.start("total");
231
+
232
+ phase.start("read_file");
233
+ let fileContent: string | undefined;
234
+ try {
235
+ fileContent = nodeFs.readFileSync(filePath, "utf-8");
236
+ } catch {
237
+ fileContent = undefined;
238
+ }
239
+ phase.end("read_file");
240
+
241
+ if (fileContent) {
242
+ const secretFindings = scanForSecrets(fileContent, filePath);
243
+ if (secretFindings.length > 0) {
244
+ const durationMs = Date.now() - pipelineStart;
245
+ logLatency({
246
+ type: "tool_result",
247
+ toolName,
248
+ filePath,
249
+ durationMs,
250
+ result: "blocked_secrets",
251
+ metadata: { secretsFound: secretFindings.length },
252
+ });
253
+ const secretDiagnostics: SecretDiagnostic[] = secretFindings.map(
254
+ (finding) => ({
255
+ id: `secrets:${finding.line}`,
256
+ message: finding.message,
257
+ filePath,
258
+ line: finding.line,
259
+ column: 1,
260
+ severity: "error",
261
+ semantic: "blocking",
262
+ tool: "secrets-scanner",
263
+ rule: "secrets",
264
+ defectClass: "secrets",
265
+ }),
266
+ );
267
+ emitLensAnalysisComplete({
268
+ cwd,
269
+ filePath,
270
+ toolName,
271
+ model: ctx.telemetry?.model ?? "unknown",
272
+ sessionId: ctx.telemetry?.sessionId ?? "unknown",
273
+ turnIndex: ctx.telemetry?.turnIndex ?? 0,
274
+ writeIndex: ctx.telemetry?.writeIndex ?? 0,
275
+ diagnostics: secretDiagnostics,
276
+ blockers: secretDiagnostics,
277
+ warnings: [],
278
+ fixed: [],
279
+ resolvedCount: 0,
280
+ hasBlockers: true,
281
+ fileModified: false,
282
+ changedFiles: [],
283
+ durationMs,
284
+ });
285
+ return {
286
+ output: `\n\n${formatSecrets(secretFindings, filePath)}`,
287
+ hasBlockers: true,
288
+ isError: true,
289
+ fileModified: false,
290
+ changedFiles: [],
291
+ };
292
+ }
293
+ }
294
+
295
+ phase.start("format");
296
+ let formatChanged = false;
297
+ let formatFailures: string[] = [];
298
+ const changedFiles = new Set<string>();
299
+ const autoformatDisabled = !!getFlag("no-autoformat");
300
+ const immediateFormat = !!getFlag("immediate-format");
301
+ const formatDeferred =
302
+ !autoformatDisabled && !immediateFormat && !!fileContent;
303
+ if (!autoformatDisabled && immediateFormat && fileContent) {
304
+ const formatResult = await runFormatPhase(filePath, getFormatService, dbg);
305
+ formatChanged = formatResult.formatChanged;
306
+ formatFailures = formatResult.formatFailures;
307
+ fileContent = formatResult.fileContent;
308
+ if (formatChanged) changedFiles.add(path.resolve(filePath));
309
+ } else if (formatDeferred) {
310
+ dbg(`autoformat: deferred until agent_end for ${filePath}`);
311
+ }
312
+ phase.end("format", { formatChanged, deferred: formatDeferred });
313
+
314
+ phase.start("lsp_sync");
315
+ let lspSyncCompleted = false;
316
+ if (fileContent) {
317
+ await resyncLspFile(
318
+ filePath,
319
+ fileContent,
320
+ true,
321
+ false,
322
+ getFlag,
323
+ dbg,
324
+ formatChanged,
325
+ );
326
+ lspSyncCompleted = true;
327
+ }
328
+ phase.end("lsp_sync", { completed: lspSyncCompleted, finalContent: true });
329
+
330
+ let output = "";
331
+ if (formatFailures.length > 0) {
332
+ const details = formatFailures.slice(0, 3).join("; ");
333
+ const suffix =
334
+ formatFailures.length > 3
335
+ ? `; ... and ${formatFailures.length - 3} more`
336
+ : "";
337
+ output += `\n\n⚠️ Auto-format failed: ${details}${suffix}`;
338
+ }
339
+ if (formatChanged) {
340
+ const changedList = [...changedFiles].map((changedFile) =>
341
+ displayPath(cwd, changedFile),
342
+ );
343
+ const fileList = changedList.length
344
+ ? `\nModified files:\n${changedList.map((file) => ` - ${file}`).join("\n")}`
345
+ : "";
346
+ output += `\n\n⚠️ **File was modified by auto-format. You MUST re-read modified file(s) before making any further edits — the content on disk has changed.**${fileList}`;
347
+ }
348
+
349
+ const elapsed = Date.now() - pipelineStart;
350
+ if (!output) output = buildAllClearOutput(elapsed, filePath);
351
+ phase.end("total", { hasOutput: !!output });
352
+
353
+ return {
354
+ output,
355
+ hasBlockers: false,
356
+ isError: false,
357
+ fileModified: formatChanged,
358
+ changedFiles: [...changedFiles],
359
+ };
360
+ }
@@ -0,0 +1,117 @@
1
+ import * as fs from "node:fs";
2
+ import * as path from "node:path";
3
+ import { detectFileKind, type FileKind } from "./file-kinds.js";
4
+ import { isPathIgnoredByProject } from "./file-utils.js";
5
+
6
+ export interface ProjectProfile {
7
+ present: Partial<Record<FileKind, boolean>>;
8
+ counts: Partial<Record<FileKind, number>>;
9
+ detectedKinds: FileKind[];
10
+ }
11
+
12
+ const MARKERS: Partial<Record<FileKind, string[]>> = {
13
+ go: ["go.mod"],
14
+ python: ["pyproject.toml", "requirements.txt", "setup.py"],
15
+ rust: ["Cargo.toml"],
16
+ jsts: ["package.json", "tsconfig.json"],
17
+ java: ["pom.xml", "build.gradle", "build.gradle.kts"],
18
+ ruby: ["Gemfile"],
19
+ php: ["composer.json"],
20
+ };
21
+
22
+ const LSP_DEFAULTS: Partial<Record<FileKind, string>> = {
23
+ jsts: "typescript-language-server",
24
+ python: "pyright",
25
+ rust: "rust-analyzer",
26
+ java: "jdtls",
27
+ ruby: "solargraph",
28
+ php: "intelephense",
29
+ };
30
+
31
+ const MAX_WALK_FILES = 4000;
32
+ const MAX_LSP_PREINSTALL = 3;
33
+
34
+ function countKind(
35
+ kind: FileKind,
36
+ counts: Partial<Record<FileKind, number>>,
37
+ ): void {
38
+ counts[kind] = (counts[kind] ?? 0) + 1;
39
+ }
40
+
41
+ function walkProject(root: string): Partial<Record<FileKind, number>> {
42
+ const counts: Partial<Record<FileKind, number>> = {};
43
+ let scanned = 0;
44
+ const queue = [root];
45
+
46
+ while (queue.length > 0 && scanned < MAX_WALK_FILES) {
47
+ const dir = queue.pop();
48
+ if (!dir) break;
49
+ let entries: fs.Dirent[];
50
+ try {
51
+ entries = fs.readdirSync(dir, { withFileTypes: true });
52
+ } catch {
53
+ continue;
54
+ }
55
+ for (const entry of entries) {
56
+ if (scanned >= MAX_WALK_FILES) break;
57
+ const full = path.join(dir, entry.name);
58
+ if (entry.isDirectory()) {
59
+ if (entry.name === "node_modules" || entry.name === ".git") continue;
60
+ if (isPathIgnoredByProject(full, root, true)) continue;
61
+ queue.push(full);
62
+ continue;
63
+ }
64
+ if (!entry.isFile()) continue;
65
+ if (isPathIgnoredByProject(full, root, false)) continue;
66
+ scanned += 1;
67
+ const kind = detectFileKind(full);
68
+ if (kind) countKind(kind, counts);
69
+ }
70
+ }
71
+
72
+ return counts;
73
+ }
74
+
75
+ function markerPresent(root: string, kind: FileKind): boolean {
76
+ for (const marker of MARKERS[kind] ?? []) {
77
+ if (fs.existsSync(path.join(root, marker))) return true;
78
+ }
79
+ return false;
80
+ }
81
+
82
+ export function detectProjectProfile(root: string): ProjectProfile {
83
+ const resolved = path.resolve(root);
84
+ const counts = walkProject(resolved);
85
+ const present: Partial<Record<FileKind, boolean>> = {};
86
+
87
+ for (const [kind, count] of Object.entries(counts)) {
88
+ if ((count ?? 0) > 0) present[kind as FileKind] = true;
89
+ }
90
+
91
+ for (const kind of Object.keys(MARKERS) as FileKind[]) {
92
+ if (markerPresent(resolved, kind)) present[kind] = true;
93
+ }
94
+
95
+ const detectedKinds = (Object.keys(present) as FileKind[])
96
+ .filter((kind) => present[kind])
97
+ .sort((a, b) => (counts[b] ?? 0) - (counts[a] ?? 0));
98
+
99
+ return { present, counts, detectedKinds };
100
+ }
101
+
102
+ export function lspPreinstallTools(profile: ProjectProfile): string[] {
103
+ const tools: string[] = [];
104
+ for (const kind of profile.detectedKinds) {
105
+ const tool = LSP_DEFAULTS[kind];
106
+ if (tool && !tools.includes(tool)) tools.push(tool);
107
+ if (tools.length >= MAX_LSP_PREINSTALL) break;
108
+ }
109
+ return tools;
110
+ }
111
+
112
+ export function resolveProjectRootForFile(
113
+ filePath: string,
114
+ fallbackRoot: string,
115
+ ): string {
116
+ return path.resolve(fallbackRoot);
117
+ }