@herbertgao/pi-extensions 2026.9.6 → 2026.9.7

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 (240) hide show
  1. package/README.md +3 -8
  2. package/THIRD_PARTY_NOTICES.md +0 -4
  3. package/node_modules/@narumitw/pi-btw/README.md +2 -0
  4. package/node_modules/@narumitw/pi-btw/dist/index.ts +284 -365
  5. package/node_modules/@narumitw/pi-btw/dist/index.ts.map +3 -3
  6. package/node_modules/@narumitw/pi-btw/package.json +52 -53
  7. package/node_modules/@narumitw/pi-btw/src/bring-to-main.ts +494 -547
  8. package/node_modules/@narumitw/pi-btw/src/btw.ts +814 -856
  9. package/node_modules/@narumitw/pi-btw/src/fullscreen-ui.ts +673 -674
  10. package/node_modules/@narumitw/pi-btw/src/keybindings.ts +228 -270
  11. package/node_modules/@narumitw/pi-btw/src/main-tree-picker.ts +309 -318
  12. package/node_modules/@narumitw/pi-btw/src/menu.ts +416 -454
  13. package/node_modules/@narumitw/pi-btw/src/settings.ts +203 -219
  14. package/node_modules/@narumitw/pi-btw/src/side-thread.ts +173 -201
  15. package/node_modules/@narumitw/pi-btw/src/text.ts +21 -23
  16. package/node_modules/@narumitw/pi-btw/src/transcript-markdown.ts +65 -0
  17. package/node_modules/@narumitw/pi-btw/src/transcript-pager.ts +611 -662
  18. package/node_modules/pi-lens/CHANGELOG.md +239 -0
  19. package/node_modules/pi-lens/config/dependency-cruiser-eager-allowlist.json +3 -0
  20. package/node_modules/pi-lens/dist/clients/actionable-warnings.js +30 -4
  21. package/node_modules/pi-lens/dist/clients/agent-behavior-client.js +2 -0
  22. package/node_modules/pi-lens/dist/clients/bash-file-access.js +26 -0
  23. package/node_modules/pi-lens/dist/clients/biome-client.js +1 -1
  24. package/node_modules/pi-lens/dist/clients/blocker-freshness.js +403 -35
  25. package/node_modules/pi-lens/dist/clients/bootstrap.js +6 -1
  26. package/node_modules/pi-lens/dist/clients/cascade-format.js +200 -4
  27. package/node_modules/pi-lens/dist/clients/code-quality-warnings.js +10 -2
  28. package/node_modules/pi-lens/dist/clients/degradation-ledger.js +16 -1
  29. package/node_modules/pi-lens/dist/clients/diagnostic-dispositions.js +12 -1
  30. package/node_modules/pi-lens/dist/clients/dispatch/ast-grep-catalog.js +65 -0
  31. package/node_modules/pi-lens/dist/clients/dispatch/auxiliary-lsp.js +28 -0
  32. package/node_modules/pi-lens/dist/clients/dispatch/dispatcher.js +9 -2
  33. package/node_modules/pi-lens/dist/clients/dispatch/finding-policy.js +222 -0
  34. package/node_modules/pi-lens/dist/clients/dispatch/indent-detect.js +389 -13
  35. package/node_modules/pi-lens/dist/clients/dispatch/integration.js +175 -25
  36. package/node_modules/pi-lens/dist/clients/dispatch/rule-ignores.js +62 -0
  37. package/node_modules/pi-lens/dist/clients/dispatch/runners/ast-grep-napi.js +21 -41
  38. package/node_modules/pi-lens/dist/clients/dispatch/runners/biome-check.js +6 -1
  39. package/node_modules/pi-lens/dist/clients/dispatch/runners/rust-clippy.js +7 -7
  40. package/node_modules/pi-lens/dist/clients/dispatch/runners/shfmt.js +2 -12
  41. package/node_modules/pi-lens/dist/clients/dispatch/runners/tree-sitter.js +2 -21
  42. package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/runner-helpers.js +1 -1
  43. package/node_modules/pi-lens/dist/clients/dispatch/types.js +4 -1
  44. package/node_modules/pi-lens/dist/clients/disposition-publish.js +1 -0
  45. package/node_modules/pi-lens/dist/clients/effective-config.js +1 -1
  46. package/node_modules/pi-lens/dist/clients/file-kinds.js +26 -1
  47. package/node_modules/pi-lens/dist/clients/file-role.js +21 -21
  48. package/node_modules/pi-lens/dist/clients/file-utils.js +156 -10
  49. package/node_modules/pi-lens/dist/clients/finding-delivery-gate.js +56 -9
  50. package/node_modules/pi-lens/dist/clients/format-service.js +39 -21
  51. package/node_modules/pi-lens/dist/clients/formatters.js +60 -13
  52. package/node_modules/pi-lens/dist/clients/generation-guard.js +2 -2
  53. package/node_modules/pi-lens/dist/clients/git-guard.js +11 -3
  54. package/node_modules/pi-lens/dist/clients/indent-retarget.js +78 -8
  55. package/node_modules/pi-lens/dist/clients/installer/index.js +333 -114
  56. package/node_modules/pi-lens/dist/clients/instance-reaper-state.js +7 -0
  57. package/node_modules/pi-lens/dist/clients/instance-reaper.js +20 -10
  58. package/node_modules/pi-lens/dist/clients/language-profile.js +23 -1
  59. package/node_modules/pi-lens/dist/clients/language-registry.js +5 -1
  60. package/node_modules/pi-lens/dist/clients/latency-logger.js +4 -1
  61. package/node_modules/pi-lens/dist/clients/lens-engine.js +12 -3
  62. package/node_modules/pi-lens/dist/clients/lens-events.js +2 -1
  63. package/node_modules/pi-lens/dist/clients/lens-flag-registry.js +79 -0
  64. package/node_modules/pi-lens/dist/clients/live-bus-emitter.js +2 -1
  65. package/node_modules/pi-lens/dist/clients/lsp/client.js +25 -21
  66. package/node_modules/pi-lens/dist/clients/lsp/config.js +2 -2
  67. package/node_modules/pi-lens/dist/clients/lsp/index.js +7 -0
  68. package/node_modules/pi-lens/dist/clients/lsp/launch.js +16 -4
  69. package/node_modules/pi-lens/dist/clients/lsp/server.js +19 -2
  70. package/node_modules/pi-lens/dist/clients/lsp/workspace-diagnostics-cache.js +2 -1
  71. package/node_modules/pi-lens/dist/clients/lsp-mutation.js +5 -3
  72. package/node_modules/pi-lens/dist/clients/map-with-concurrency.js +12 -8
  73. package/node_modules/pi-lens/dist/clients/mcp/session.js +36 -4
  74. package/node_modules/pi-lens/dist/clients/ndjson-logger.js +24 -7
  75. package/node_modules/pi-lens/dist/clients/observed-mutation.js +40 -2
  76. package/node_modules/pi-lens/dist/clients/opaque-mutation-scan.js +23 -3
  77. package/node_modules/pi-lens/dist/clients/opengrep-client.js +110 -15
  78. package/node_modules/pi-lens/dist/clients/path-utils.js +123 -3
  79. package/node_modules/pi-lens/dist/clients/performance-report.js +5 -1
  80. package/node_modules/pi-lens/dist/clients/php-cs-fixer-config.js +1 -1
  81. package/node_modules/pi-lens/dist/clients/pipeline.js +74 -5
  82. package/node_modules/pi-lens/dist/clients/project-diagnostics/cache.js +37 -5
  83. package/node_modules/pi-lens/dist/clients/project-diagnostics/fresh-fetch.js +145 -18
  84. package/node_modules/pi-lens/dist/clients/project-diagnostics/runner-adapters/dead-code.js +4 -1
  85. package/node_modules/pi-lens/dist/clients/project-diagnostics/scanner.js +59 -3
  86. package/node_modules/pi-lens/dist/clients/project-lens-config.js +45 -2
  87. package/node_modules/pi-lens/dist/clients/read-guard.js +70 -11
  88. package/node_modules/pi-lens/dist/clients/resource-sampler.js +147 -9
  89. package/node_modules/pi-lens/dist/clients/ruff-client.js +1 -1
  90. package/node_modules/pi-lens/dist/clients/runtime-agent-end.js +33 -5
  91. package/node_modules/pi-lens/dist/clients/runtime-context.js +6 -2
  92. package/node_modules/pi-lens/dist/clients/runtime-coordinator.js +77 -7
  93. package/node_modules/pi-lens/dist/clients/runtime-session.js +21 -2
  94. package/node_modules/pi-lens/dist/clients/runtime-tool-call.js +43 -12
  95. package/node_modules/pi-lens/dist/clients/runtime-tool-result.js +330 -85
  96. package/node_modules/pi-lens/dist/clients/runtime-turn.js +147 -17
  97. package/node_modules/pi-lens/dist/clients/safe-spawn.js +258 -5
  98. package/node_modules/pi-lens/dist/clients/scratch-tree-policy.js +6 -0
  99. package/node_modules/pi-lens/dist/clients/session-event-guard.js +50 -5
  100. package/node_modules/pi-lens/dist/clients/sg-runner.js +23 -11
  101. package/node_modules/pi-lens/dist/clients/sgconfig.js +79 -2
  102. package/node_modules/pi-lens/dist/clients/test-runner-client.js +6 -41
  103. package/node_modules/pi-lens/dist/clients/test-runner-delivery.js +43 -0
  104. package/node_modules/pi-lens/dist/clients/tool-agreement.js +423 -0
  105. package/node_modules/pi-lens/dist/clients/tool-config.js +50 -0
  106. package/node_modules/pi-lens/dist/clients/tool-cwd.js +47 -76
  107. package/node_modules/pi-lens/dist/clients/tool-policy.js +163 -7
  108. package/node_modules/pi-lens/dist/clients/tree-sitter-client.js +120 -0
  109. package/node_modules/pi-lens/dist/clients/tree-sitter-query-loader.js +127 -189
  110. package/node_modules/pi-lens/dist/clients/widget-state.js +98 -0
  111. package/node_modules/pi-lens/dist/clients/word-index.js +5 -2
  112. package/node_modules/pi-lens/dist/clients/write-ordering-guard.js +7 -0
  113. package/node_modules/pi-lens/dist/index.js +27416 -24153
  114. package/node_modules/pi-lens/dist/mcp/server.js +5 -2
  115. package/node_modules/pi-lens/dist/tools/lens-diagnostic-mark.js +7 -1
  116. package/node_modules/pi-lens/dist/tools/lens-diagnostics.js +168 -53
  117. package/node_modules/pi-lens/dist/tools/lsp-diagnostics.js +244 -72
  118. package/node_modules/pi-lens/docs/audit1.md +5 -5
  119. package/node_modules/pi-lens/docs/dependencies.md +11 -0
  120. package/node_modules/pi-lens/docs/dispositions.md +52 -0
  121. package/node_modules/pi-lens/docs/environment-variables.md +2 -2
  122. package/node_modules/pi-lens/docs/features.md +4 -4
  123. package/node_modules/pi-lens/docs/globalconfig.md +3 -1
  124. package/node_modules/pi-lens/docs/language-coverage.md +1 -0
  125. package/node_modules/pi-lens/docs/pi-lens-fixer.md +53 -94
  126. package/node_modules/pi-lens/docs/pi-lens-investigator.md +43 -38
  127. package/node_modules/pi-lens/docs/pi-lens-retro.md +97 -0
  128. package/node_modules/pi-lens/docs/pi-lens-reviewer.md +53 -66
  129. package/node_modules/pi-lens/docs/release-qa-baseline.md +22 -0
  130. package/node_modules/pi-lens/docs/settings.md +7 -4
  131. package/node_modules/pi-lens/docs/tree-sitter_rules_catalog.md +1 -1
  132. package/node_modules/pi-lens/docs/usage.md +2 -0
  133. package/node_modules/pi-lens/package.json +1 -1
  134. package/node_modules/pi-lens/rules/tree-sitter-queries/typescript/sql-injection.yml +24 -12
  135. package/node_modules/pi-lens/scripts/analyze-pi-lens-logs.mjs +40 -0
  136. package/node_modules/pi-lens/skills/pi-lens-ast-grep/SKILL.md +15 -0
  137. package/node_modules/pi-lens/skills/pi-lens-lsp-navigation/SKILL.md +7 -0
  138. package/package.json +5 -11
  139. package/node_modules/@luxusai/pi-hindsight/CHANGELOG.md +0 -376
  140. package/node_modules/@luxusai/pi-hindsight/README.md +0 -92
  141. package/node_modules/@luxusai/pi-hindsight/docs/adr/001-memory-lifecycle-and-scope.md +0 -56
  142. package/node_modules/@luxusai/pi-hindsight/docs/adr/002-explicit-routing-strategy-seam.md +0 -169
  143. package/node_modules/@luxusai/pi-hindsight/docs/adr/003-tui-memory-mode-vocabulary.md +0 -107
  144. package/node_modules/@luxusai/pi-hindsight/docs/adr/004-lifeos-dual-bank-design.md +0 -89
  145. package/node_modules/@luxusai/pi-hindsight/docs/adr/005-domain-banks-and-agent-first-surface.md +0 -189
  146. package/node_modules/@luxusai/pi-hindsight/docs/assets/logos/pi-hindsight-logo-dark.webp +0 -0
  147. package/node_modules/@luxusai/pi-hindsight/docs/assets/logos/pi-hindsight-logo-dark@2x.webp +0 -0
  148. package/node_modules/@luxusai/pi-hindsight/docs/coding-memory-evaluation.md +0 -54
  149. package/node_modules/@luxusai/pi-hindsight/docs/compatibility.md +0 -75
  150. package/node_modules/@luxusai/pi-hindsight/docs/hindsight-core-functions.md +0 -300
  151. package/node_modules/@luxusai/pi-hindsight/docs/mission-and-mental-model-quality.md +0 -158
  152. package/node_modules/@luxusai/pi-hindsight/docs/next-opt-out-design.md +0 -164
  153. package/node_modules/@luxusai/pi-hindsight/docs/risky-memory-modes.md +0 -139
  154. package/node_modules/@luxusai/pi-hindsight/docs/starter-mental-model-suggestions.md +0 -74
  155. package/node_modules/@luxusai/pi-hindsight/docs/surface-reference.md +0 -261
  156. package/node_modules/@luxusai/pi-hindsight/extensions/banks/bank-operations.ts +0 -151
  157. package/node_modules/@luxusai/pi-hindsight/extensions/banks/bank-selection.ts +0 -18
  158. package/node_modules/@luxusai/pi-hindsight/extensions/banks/bank-settings-presenter.ts +0 -46
  159. package/node_modules/@luxusai/pi-hindsight/extensions/banks/bank-templates.ts +0 -383
  160. package/node_modules/@luxusai/pi-hindsight/extensions/banks/banking.ts +0 -240
  161. package/node_modules/@luxusai/pi-hindsight/extensions/banks/knowledge-page-seed.ts +0 -176
  162. package/node_modules/@luxusai/pi-hindsight/extensions/banks/retain-strategies.ts +0 -178
  163. package/node_modules/@luxusai/pi-hindsight/extensions/client/client-retry.ts +0 -41
  164. package/node_modules/@luxusai/pi-hindsight/extensions/client/client.ts +0 -386
  165. package/node_modules/@luxusai/pi-hindsight/extensions/client/fetch-compat.ts +0 -39
  166. package/node_modules/@luxusai/pi-hindsight/extensions/client/timeout.ts +0 -34
  167. package/node_modules/@luxusai/pi-hindsight/extensions/config/config-defaults.ts +0 -140
  168. package/node_modules/@luxusai/pi-hindsight/extensions/config/config-editing-model.ts +0 -61
  169. package/node_modules/@luxusai/pi-hindsight/extensions/config/config-editing-registry.ts +0 -925
  170. package/node_modules/@luxusai/pi-hindsight/extensions/config/config-field-paths.ts +0 -247
  171. package/node_modules/@luxusai/pi-hindsight/extensions/config/config-normalize.ts +0 -547
  172. package/node_modules/@luxusai/pi-hindsight/extensions/config/config-writer.ts +0 -480
  173. package/node_modules/@luxusai/pi-hindsight/extensions/config/config.ts +0 -182
  174. package/node_modules/@luxusai/pi-hindsight/extensions/config/setup-gate.ts +0 -86
  175. package/node_modules/@luxusai/pi-hindsight/extensions/imports/import-execute.ts +0 -1010
  176. package/node_modules/@luxusai/pi-hindsight/extensions/imports/import-parse.ts +0 -175
  177. package/node_modules/@luxusai/pi-hindsight/extensions/imports/import-plan.ts +0 -425
  178. package/node_modules/@luxusai/pi-hindsight/extensions/imports/import-presentation.ts +0 -210
  179. package/node_modules/@luxusai/pi-hindsight/extensions/imports/import-sessions.ts +0 -817
  180. package/node_modules/@luxusai/pi-hindsight/extensions/index.ts +0 -25
  181. package/node_modules/@luxusai/pi-hindsight/extensions/lifecycle/git-seed.ts +0 -319
  182. package/node_modules/@luxusai/pi-hindsight/extensions/lifecycle/memory-lifecycle-recall.ts +0 -196
  183. package/node_modules/@luxusai/pi-hindsight/extensions/lifecycle/memory-lifecycle-retain.ts +0 -189
  184. package/node_modules/@luxusai/pi-hindsight/extensions/lifecycle/memory-lifecycle-runtime.ts +0 -65
  185. package/node_modules/@luxusai/pi-hindsight/extensions/lifecycle/memory-lifecycle.ts +0 -260
  186. package/node_modules/@luxusai/pi-hindsight/extensions/lifecycle/mental-models.ts +0 -244
  187. package/node_modules/@luxusai/pi-hindsight/extensions/lifecycle/observation-scopes.ts +0 -59
  188. package/node_modules/@luxusai/pi-hindsight/extensions/lifecycle/recall-cleanup.ts +0 -82
  189. package/node_modules/@luxusai/pi-hindsight/extensions/lifecycle/recall-visibility.ts +0 -45
  190. package/node_modules/@luxusai/pi-hindsight/extensions/lifecycle/recall.ts +0 -331
  191. package/node_modules/@luxusai/pi-hindsight/extensions/lifecycle/retain-cursor.ts +0 -354
  192. package/node_modules/@luxusai/pi-hindsight/extensions/lifecycle/retain-job-builder.ts +0 -66
  193. package/node_modules/@luxusai/pi-hindsight/extensions/lifecycle/retain-receipts.ts +0 -132
  194. package/node_modules/@luxusai/pi-hindsight/extensions/lifecycle/retain.ts +0 -228
  195. package/node_modules/@luxusai/pi-hindsight/extensions/operations/memory-bank-template-operations.ts +0 -74
  196. package/node_modules/@luxusai/pi-hindsight/extensions/operations/memory-config-operations.ts +0 -16
  197. package/node_modules/@luxusai/pi-hindsight/extensions/operations/memory-control-operations.ts +0 -648
  198. package/node_modules/@luxusai/pi-hindsight/extensions/operations/memory-diagnostics-operations.ts +0 -105
  199. package/node_modules/@luxusai/pi-hindsight/extensions/operations/memory-identity.ts +0 -68
  200. package/node_modules/@luxusai/pi-hindsight/extensions/operations/memory-operation-service.ts +0 -71
  201. package/node_modules/@luxusai/pi-hindsight/extensions/operations/memory-operation-types.ts +0 -13
  202. package/node_modules/@luxusai/pi-hindsight/extensions/operations/memory-recall-operations.ts +0 -162
  203. package/node_modules/@luxusai/pi-hindsight/extensions/operations/memory-retain-operations.ts +0 -131
  204. package/node_modules/@luxusai/pi-hindsight/extensions/operations/memory-scope.ts +0 -104
  205. package/node_modules/@luxusai/pi-hindsight/extensions/operations/memory-session-operations.ts +0 -37
  206. package/node_modules/@luxusai/pi-hindsight/extensions/operations/operation-catalog.ts +0 -893
  207. package/node_modules/@luxusai/pi-hindsight/extensions/operations/reflect-presenter.ts +0 -35
  208. package/node_modules/@luxusai/pi-hindsight/extensions/operations/scope-migrate.ts +0 -177
  209. package/node_modules/@luxusai/pi-hindsight/extensions/operations/tools.ts +0 -7
  210. package/node_modules/@luxusai/pi-hindsight/extensions/queue/flush-presenter.ts +0 -21
  211. package/node_modules/@luxusai/pi-hindsight/extensions/queue/jsonl-queue-store.ts +0 -112
  212. package/node_modules/@luxusai/pi-hindsight/extensions/queue/queue-delivery.ts +0 -111
  213. package/node_modules/@luxusai/pi-hindsight/extensions/queue/queue-lock.ts +0 -214
  214. package/node_modules/@luxusai/pi-hindsight/extensions/queue/queue-operations.ts +0 -70
  215. package/node_modules/@luxusai/pi-hindsight/extensions/queue/queue.ts +0 -392
  216. package/node_modules/@luxusai/pi-hindsight/extensions/tui/bank-template-presentation.ts +0 -45
  217. package/node_modules/@luxusai/pi-hindsight/extensions/tui/commands.ts +0 -9
  218. package/node_modules/@luxusai/pi-hindsight/extensions/tui/guided-setup.ts +0 -860
  219. package/node_modules/@luxusai/pi-hindsight/extensions/tui/prefill-input.ts +0 -105
  220. package/node_modules/@luxusai/pi-hindsight/extensions/tui/setup-flow.ts +0 -189
  221. package/node_modules/@luxusai/pi-hindsight/extensions/tui/setup-server-probe.ts +0 -299
  222. package/node_modules/@luxusai/pi-hindsight/extensions/tui/setup-tui-actions.ts +0 -201
  223. package/node_modules/@luxusai/pi-hindsight/extensions/tui/setup-tui-facts.ts +0 -47
  224. package/node_modules/@luxusai/pi-hindsight/extensions/tui/setup-tui-render.ts +0 -239
  225. package/node_modules/@luxusai/pi-hindsight/extensions/tui/setup-tui-types.ts +0 -50
  226. package/node_modules/@luxusai/pi-hindsight/extensions/tui/setup-tui.ts +0 -259
  227. package/node_modules/@luxusai/pi-hindsight/extensions/tui/tool-presenters.ts +0 -77
  228. package/node_modules/@luxusai/pi-hindsight/extensions/types.ts +0 -534
  229. package/node_modules/@luxusai/pi-hindsight/extensions/utils/diagnostics.ts +0 -319
  230. package/node_modules/@luxusai/pi-hindsight/extensions/utils/messages.ts +0 -325
  231. package/node_modules/@luxusai/pi-hindsight/extensions/utils/sanitize.ts +0 -42
  232. package/node_modules/@luxusai/pi-hindsight/extensions/utils/session-memory-meta.ts +0 -244
  233. package/node_modules/@luxusai/pi-hindsight/extensions/utils/session-operations.ts +0 -56
  234. package/node_modules/@luxusai/pi-hindsight/extensions/utils/session.ts +0 -50
  235. package/node_modules/@luxusai/pi-hindsight/extensions/utils/status-fields.ts +0 -167
  236. package/node_modules/@luxusai/pi-hindsight/extensions/utils/status-health.ts +0 -207
  237. package/node_modules/@luxusai/pi-hindsight/extensions/utils/status.ts +0 -120
  238. package/node_modules/@luxusai/pi-hindsight/extensions/version.ts +0 -8
  239. package/node_modules/@luxusai/pi-hindsight/package.json +0 -118
  240. package/node_modules/@luxusai/pi-hindsight/skills/hindsight-memory-doctor/SKILL.md +0 -75
@@ -70,6 +70,7 @@ import { acquireQuarantinePidFileLock } from "./bounded-pid-file-lock.js";
70
70
  import { unrefChildAndPipes, } from "./child-unref.js";
71
71
  import { incrementDegradationCount, recordDegradationOnce, } from "./degradation-ledger.js";
72
72
  import { getGlobalPiLensDir } from "./file-utils.js";
73
+ import { resolveBackstopStateDir } from "./instance-reaper-state.js";
73
74
  import { isInstanceRegistryEnabled, pruneDeadInstances as pruneDeadRegistryInstances, readInstanceRegistry, } from "./instance-registry.js";
74
75
  import { logLatency } from "./latency-logger.js";
75
76
  import { queryProcessTable, windowsExe } from "./process-snapshot.js";
@@ -717,7 +718,7 @@ function matchesManagedBinary(command) {
717
718
  export async function sweepUntrackedOrphans(options = {}) {
718
719
  const startedAt = Date.now();
719
720
  if (!isInstanceRegistryEnabled()) {
720
- return logBackstopOutcome("disabled", startedAt, {});
721
+ return logBackstopOutcome("disabled", startedAt, {}, options.onComplete);
721
722
  }
722
723
  const cooldownMs = Math.max(0, options.cooldownMs ?? BACKSTOP_COOLDOWN_MS);
723
724
  let release = null;
@@ -735,13 +736,13 @@ export async function sweepUntrackedOrphans(options = {}) {
735
736
  // Another process holds the sweep right now. Not an error, and not a
736
737
  // cooldown either — a distinct record, because "somebody else is
737
738
  // sweeping" and "we swept recently" have different causes.
738
- return logBackstopOutcome("concurrent", startedAt, {});
739
+ return logBackstopOutcome("concurrent", startedAt, {}, options.onComplete);
739
740
  }
740
741
  const throttled = await isWithinCooldown(Date.now(), cooldownMs, options);
741
742
  if (throttled !== undefined) {
742
743
  return logBackstopOutcome("cooldown", startedAt, {
743
744
  sinceLastMs: throttled,
744
- });
745
+ }, options.onComplete);
745
746
  }
746
747
  // Claim the cooldown slot BEFORE the scan, not after: a sweep that dies
747
748
  // mid-scan must not re-run at full cost on every retry.
@@ -766,7 +767,7 @@ export async function sweepUntrackedOrphans(options = {}) {
766
767
  scanStatus: scan.status,
767
768
  scannerKill: scan.timeoutKill,
768
769
  scanned: scan.processes.length,
769
- });
770
+ }, options.onComplete);
770
771
  }
771
772
  const partition = partitionBackstopCandidates(scan.processes, registry, realIsPidAlive, { graceMs: options.graceMs });
772
773
  for (const proc of partition.unknownAge) {
@@ -811,7 +812,7 @@ export async function sweepUntrackedOrphans(options = {}) {
811
812
  killedProcesses: killed.slice(0, BACKSTOP_IDENTITY_LOG_LIMIT),
812
813
  unverifiedProcesses: unverified.slice(0, BACKSTOP_IDENTITY_LOG_LIMIT),
813
814
  graceRetryInMs: retryAt,
814
- });
815
+ }, options.onComplete);
815
816
  }
816
817
  catch (error) {
817
818
  // The sweep must never throw out of session_start — but "threw" is now
@@ -821,7 +822,7 @@ export async function sweepUntrackedOrphans(options = {}) {
821
822
  subject: "sweep",
822
823
  reason: `sweep threw: ${error instanceof Error ? error.message : String(error)}`,
823
824
  });
824
- return logBackstopOutcome("error", startedAt, { threw: true });
825
+ return logBackstopOutcome("error", startedAt, { threw: true }, options.onComplete);
825
826
  }
826
827
  finally {
827
828
  if (release) {
@@ -909,13 +910,14 @@ function scheduleGraceRetryIfNeeded(partition, options) {
909
910
  force: true,
910
911
  allowGraceRetry: false,
911
912
  });
913
+ options.onGraceRetryScheduled?.(delayMs);
912
914
  return delayMs;
913
915
  }
914
916
  /** Serialize the sweep across processes. Returns null when another process
915
917
  * holds it — the caller skips rather than waits (`waitMs: 0`). */
916
918
  async function acquireBackstopLock() {
917
919
  try {
918
- return await acquireQuarantinePidFileLock(path.join(getGlobalPiLensDir(), "orphan-backstop.lock"), {
920
+ return await acquireQuarantinePidFileLock(path.join(resolveBackstopStateDir(getGlobalPiLensDir()), "orphan-backstop.lock"), {
919
921
  waitMs: 0,
920
922
  retryMs: 50,
921
923
  staleMs: BACKSTOP_LOCK_STALE_MS,
@@ -952,7 +954,7 @@ function describeManagedProcess(proc) {
952
954
  const matched = MANAGED_BINARY_NAMES.find((name) => lower.includes(name.toLowerCase()));
953
955
  return `${matched ?? "unknown"}#${proc.pid}`;
954
956
  }
955
- function logBackstopOutcome(outcome, startedAt, metadata) {
957
+ function logBackstopOutcome(outcome, startedAt, metadata, onComplete) {
956
958
  try {
957
959
  logLatency({
958
960
  type: "phase",
@@ -965,13 +967,19 @@ function logBackstopOutcome(outcome, startedAt, metadata) {
965
967
  catch {
966
968
  // best-effort logging only
967
969
  }
970
+ try {
971
+ onComplete?.(outcome);
972
+ }
973
+ catch {
974
+ // Test observers must not alter the best-effort sweep outcome.
975
+ }
968
976
  return outcome;
969
977
  }
970
978
  /** Machine-wide cooldown stamp. A file, not a module-level flag: the sweep is
971
979
  * machine-scoped (every pi-lens process shares one OS process table), and
972
980
  * process-lifetime state cannot express "30 minutes have passed". */
973
981
  function backstopStampPath() {
974
- return path.join(getGlobalPiLensDir(), "orphan-backstop.json");
982
+ return path.join(resolveBackstopStateDir(getGlobalPiLensDir()), "orphan-backstop.json");
975
983
  }
976
984
  async function readBackstopStamp() {
977
985
  try {
@@ -990,7 +998,9 @@ async function readBackstopStamp() {
990
998
  }
991
999
  async function writeBackstopStamp(at) {
992
1000
  try {
993
- await fs.promises.mkdir(getGlobalPiLensDir(), { recursive: true });
1001
+ await fs.promises.mkdir(resolveBackstopStateDir(getGlobalPiLensDir()), {
1002
+ recursive: true,
1003
+ });
994
1004
  await writeFileAtomicAsync(backstopStampPath(), JSON.stringify({ lastSweepAt: at }));
995
1005
  }
996
1006
  catch {
@@ -52,6 +52,8 @@ const PROJECT_MARKERS_BY_KIND = {
52
52
  };
53
53
  const ROOT_MARKERS_BY_KIND = {
54
54
  jsts: [
55
+ "biome.json",
56
+ "biome.jsonc",
55
57
  "package.json",
56
58
  "tsconfig.json",
57
59
  "jsconfig.json",
@@ -100,6 +102,26 @@ const ROOT_MARKERS_BY_KIND = {
100
102
  csharp: DOTNET_CSHARP_ROOT_MARKERS,
101
103
  fsharp: DOTNET_FSHARP_ROOT_MARKERS,
102
104
  };
105
+ // Tool-owned configuration belongs beside the shared language vocabulary, but
106
+ // not in ROOT_MARKERS_BY_KIND: these files are discovered by the tool, not by
107
+ // the language root detector. Keeping them here lets runner cwd resolution
108
+ // consume one canonical vocabulary without restoring a second seam-local map.
109
+ const TOOL_MARKERS_BY_RUNNER = {
110
+ ruff: ["ruff.toml", ".ruff.toml"],
111
+ oxlint: [".oxlintrc.json", "oxlint.config.js"],
112
+ "spellcheck/typos": ["_typos.toml", "typos.toml"],
113
+ yamllint: ["yamllint.yaml", "yamllint.yml"],
114
+ prettier: [".prettierignore"],
115
+ };
116
+ /** Return the one shared marker vocabulary used to anchor this file kind. */
117
+ export function rootMarkersForFile(filePath, runner) {
118
+ const kind = detectFileKind(path.resolve(filePath));
119
+ const languageMarkers = kind ? (ROOT_MARKERS_BY_KIND[kind] ?? []) : [];
120
+ const toolMarkers = runner ? (TOOL_MARKERS_BY_RUNNER[runner] ?? []) : [];
121
+ return toolMarkers.length
122
+ ? [...new Set([...languageMarkers, ...toolMarkers])]
123
+ : languageMarkers;
124
+ }
103
125
  function hasProjectMarker(projectRoot, marker) {
104
126
  if (!marker.includes("*"))
105
127
  return fs.existsSync(path.join(projectRoot, marker));
@@ -192,7 +214,7 @@ export function resolveLanguageRootForFile(filePath, workspaceRoot) {
192
214
  const kind = detectFileKind(absoluteFilePath);
193
215
  if (!kind)
194
216
  return path.resolve(workspaceRoot);
195
- const markers = ROOT_MARKERS_BY_KIND[kind];
217
+ const markers = rootMarkersForFile(absoluteFilePath);
196
218
  if (!markers || markers.length === 0) {
197
219
  return path.resolve(workspaceRoot);
198
220
  }
@@ -147,7 +147,9 @@ export const LANGUAGES = [
147
147
  id: "cpp",
148
148
  kind: "cxx",
149
149
  // The clang extension table (file-kinds.ts KIND_EXTENSIONS.cxx) minus the
150
- // C header/source pair, which the `c` entry owns.
150
+ // C header/source pair, which the `c` entry owns. `.cuh` deliberately
151
+ // diverges from clang's table: CUDA/HIP projects use it for C++ headers,
152
+ // so it must reach the same cpp grammar and clangd path as `.cu`/`.hip`.
151
153
  extensions: [
152
154
  ".c++",
153
155
  ".c++m",
@@ -158,6 +160,7 @@ export const LANGUAGES = [
158
160
  ".cpp",
159
161
  ".cppm",
160
162
  ".cu",
163
+ ".cuh",
161
164
  ".cxx",
162
165
  ".cxxm",
163
166
  ".hh",
@@ -182,6 +185,7 @@ export const LANGUAGES = [
182
185
  ".cc",
183
186
  ".cpp",
184
187
  ".cu",
188
+ ".cuh",
185
189
  ".cxx",
186
190
  ".hh",
187
191
  ".hip",
@@ -682,7 +682,10 @@ export function logLatency(entry) {
682
682
  if (entry.type === "phase" &&
683
683
  entry.phase &&
684
684
  !LAST_PHASE_EXCLUDED.has(entry.phase)) {
685
- recentPhases = [{ phase: entry.phase, ts }, ...recentPhases].slice(0, RECENT_PHASE_CAP);
685
+ recentPhases = [
686
+ { phase: entry.phase, ts, metadata: entry.metadata },
687
+ ...recentPhases,
688
+ ].slice(0, RECENT_PHASE_CAP);
686
689
  }
687
690
  if (isTestMode()) {
688
691
  return;
@@ -25,6 +25,7 @@ import { acquireWarmWordIndex } from "./mcp/analyze.js";
25
25
  import { normalizeMapKey } from "./path-utils.js";
26
26
  import { scanProjectDiagnostics } from "./project-diagnostics/scanner.js";
27
27
  import { loadProjectSnapshotWithoutWordIndex } from "./project-snapshot.js";
28
+ import { resolveLensToolName } from "./tool-config.js";
28
29
  import { getTreeSitterRuntimeStatus, } from "./tree-sitter-shared.js";
29
30
  import { centralityFromReverseDeps, getWordIndexBuildStatus, searchWordIndex, triggerBackgroundWordIndexBuild, } from "./word-index.js";
30
31
  // --- Facades (re-exported so adapters import only this module) ---------------
@@ -116,7 +117,7 @@ export function scanTruncationNotice(snapshot) {
116
117
  *
117
118
  * Returns `undefined` when neither counter is set (nothing to append).
118
119
  */
119
- export function generatedSkipNotice(snapshot) {
120
+ export function generatedSkipNotice(snapshot, host = "pi") {
120
121
  const files = snapshot.generatedNameOnlySkips ?? 0;
121
122
  const dirs = snapshot.generatedDirSkips ?? 0;
122
123
  if (files === 0 && dirs === 0)
@@ -126,11 +127,19 @@ export function generatedSkipNotice(snapshot) {
126
127
  parts.push(`${files} file(s)`);
127
128
  if (dirs > 0)
128
129
  parts.push(`${dirs} director${dirs === 1 ? "y" : "ies"}`);
130
+ // #2535 F1: name the tools THIS host can call. pi has no project-scan
131
+ // surface, so it names only its diagnostics tool; MCP names both. A
132
+ // known-but-unmapped host resolves to undefined and is left out, never
133
+ // printed as a name the host cannot resolve.
134
+ const targets = [
135
+ resolveLensToolName("project_scan", host),
136
+ resolveLensToolName("lens_diagnostics", host),
137
+ ].filter((tool) => tool !== undefined);
138
+ const scanRef = targets.length > 0 ? ` to ${targets.join(" / ")}` : "";
129
139
  return (`ℹ ${parts.join(" and ")} excluded by generated-name heuristics with no ` +
130
140
  "confirming evidence — a real hand-written file/directory whose name " +
131
141
  "looks generated (e.g. `gen.ts`, `generated/`) can be excluded too; " +
132
- "pass includeGenerated: true to pilens_project_scan / lens_diagnostics " +
133
- "to scan without this filter if you suspect a false skip.");
142
+ `pass includeGenerated: true${scanRef} to scan without this filter if you suspect a false skip.`);
134
143
  }
135
144
  /** Process-wide tree-sitter health for host status surfaces. */
136
145
  export function treeSitterRuntimeStatus() {
@@ -50,7 +50,8 @@ export function _resetForTests() {
50
50
  function emitLensEvent(eventName, payload) {
51
51
  setImmediate(() => {
52
52
  const rawCwd = payload.cwd ?? process.cwd();
53
- // M1: normalizeFilePath is a sync realpathSync.native on Windows build
53
+ // M1: normalizeFilePath is a sync realpathSync.native on Windows, and on
54
+ // POSIX too since #3098 — build
54
55
  // the resolveLiveBusEmitter log entry lazily so the ready-path (the
55
56
  // common case) never pays it. Only the stale-session branch needs it.
56
57
  const resolution = resolveLiveBusEmitter(liveEmitter, () => ({
@@ -173,6 +173,22 @@ export const LENS_FLAGS = [
173
173
  default: false,
174
174
  scope: "global",
175
175
  },
176
+ {
177
+ name: "lens-compact-lsp-status",
178
+ description: "Opt-in (#3099): collapse the footer LSP status to one state glyph per group (LSP ✓ green, LSP ✗ red, dim LSP ✗ when nothing is warm) instead of listing the active server names. Default off. Also via ui.compactLspStatus=true in ~/.pi-lens/config.json.",
179
+ configKey: "ui.compactLspStatus",
180
+ negated: false,
181
+ default: false,
182
+ scope: "global",
183
+ },
184
+ {
185
+ name: "lens-hide-lsp-status",
186
+ description: "Opt-in (#3099): publish no pi-lens-lsp footer status at all, so a host that renders extension statuses stops showing the key. Outranks lens-compact-lsp-status when both are set. Default off. Also via ui.hideLspStatus=true in ~/.pi-lens/config.json.",
187
+ configKey: "ui.hideLspStatus",
188
+ negated: false,
189
+ default: false,
190
+ scope: "global",
191
+ },
176
192
  {
177
193
  name: "no-lazy-tools",
178
194
  description: "Keep all pi-lens tools active to avoid tool-list cache changes. Also via tools.lazy=false in ~/.pi-lens/config.json.",
@@ -235,6 +251,40 @@ export const GLOBAL_NON_FLAG_CONFIG_SECTIONS = [
235
251
  "startup",
236
252
  "$schema",
237
253
  ];
254
+ /**
255
+ * Dotted GLOBAL-only config keys that are NOT `LENS_FLAGS` entries — no CLI
256
+ * flag, not a boolean — but still sit inside a section a project
257
+ * `.pi-lens.json` is otherwise recognized to touch (#3131). The project
258
+ * loader's mixed-scope sub-key scan (`project-lens-config.ts`) already derives
259
+ * its FLAG population from `LENS_FLAGS`; that derivation structurally cannot
260
+ * see a key like `actionableWarnings.autoFix.maxFixes` — a numeric cap with no
261
+ * `--flag` counterpart, documented global-only (`docs/settings.md`), read only
262
+ * through `getGlobalActionableWarningMaxFixes()` (`clients/lens-config.ts`) —
263
+ * because it was never a `LensFlagSpec` to begin with. `actionableWarnings` is
264
+ * itself recognized at project scope only because its SIBLING
265
+ * `actionableWarnings.autoFix.enabled` is a `scope: "project"` flag, so before
266
+ * this registry existed the loader parsed `maxFixes` away with no signal at
267
+ * all — the same silent-drop shape #2426 review round 2 (F3) fixed for
268
+ * `lsp.enabled` and #3112 fixed for `tools.lazy`.
269
+ *
270
+ * This is the single source that population is derived from: the mixed-scope
271
+ * scan reads `LENS_FLAGS` for flag keys and this list for everything else, so
272
+ * a future non-flag global-only key needs one line HERE, never a second
273
+ * hand-typed list inside the loader.
274
+ *
275
+ * A key belongs here ONLY when all three hold: (a) it is global-only — read
276
+ * solely off `~/.pi-lens/config.json`, never honored from a project document;
277
+ * (b) it is not a `LENS_FLAGS` entry; (c) its SECTION is recognized at project
278
+ * scope for some OTHER reason. A key whose whole section is unrecognized at
279
+ * project scope needs no entry — the top-level unknown-key scan already
280
+ * reports the section. Swept at #3131: every other non-flag dotted key either
281
+ * loader parses (`dispatch.runnerTimeoutFloorMs`, `widget.visible`,
282
+ * `startup.mode`, `startup.scans.enabled`) fails (a) or (c) — see that PR's
283
+ * body for the per-member table — so this is currently the only member.
284
+ */
285
+ export const GLOBAL_ONLY_NON_FLAG_KEYS = [
286
+ "actionableWarnings.autoFix.maxFixes",
287
+ ];
238
288
  /**
239
289
  * Recognized TOP-LEVEL sections of a project `.pi-lens.json` that are NOT
240
290
  * derived from the flag registry — the project loader's own hand-parsed
@@ -256,6 +306,19 @@ export const PROJECT_NON_FLAG_CONFIG_SECTIONS = [
256
306
  "trivy",
257
307
  "helm",
258
308
  "startup",
309
+ // `tools` is MIXED-SCOPE, and belongs here for the project-scoped half
310
+ // (#3112). `tools.<name>.enabled` is not a registry flag at all: it is
311
+ // hand-parsed by `readToolConfig` against `TOOL_REGISTRY` in BOTH loaders,
312
+ // and `resolveLensToolEnabled` reads the project document's value ahead of
313
+ // the global one — which is what `docs/settings.md` and
314
+ // `docs/globalconfig.md` document. Deriving the project-accepted sections
315
+ // from the flag registry alone therefore put the whole section in
316
+ // `globalScopeOnlyKeys` (its only registry flag, `tools.lazy`, IS global)
317
+ // and told every user of a documented per-tool override that it was being
318
+ // ignored. The global-only half is not lost: `tools.lazy` is still reported
319
+ // at project scope by the project loader's mixed-scope sub-key scan, which
320
+ // derives WHICH sub-keys those are from this same registry.
321
+ "tools",
259
322
  ];
260
323
  /**
261
324
  * The four LSP settings a PROJECT config file may carry — at its root during
@@ -343,6 +406,22 @@ function resolveFlagConfigPath(raw, configKey, warnInvalid) {
343
406
  return undefined;
344
407
  return { source: finalSource, segments };
345
408
  }
409
+ /**
410
+ * Whether a parsed config document actually SETS the dotted `configKey` —
411
+ * presence, not validity (#3112). The project loader's mixed-scope scan must
412
+ * report `tools: { lazy: "yes" }` exactly as it reports `tools: { lazy: false }`:
413
+ * the user wrote a global-only setting in a project file either way, and
414
+ * {@link readFlagConfigValue} would return `undefined` for the malformed one and
415
+ * silently skip it. Shares {@link resolveFlagConfigPath} with every other reader
416
+ * so "which segments does this key name" is answered in exactly one place.
417
+ */
418
+ export function hasFlagConfigPath(raw, configKey) {
419
+ const resolved = resolveFlagConfigPath(raw, configKey);
420
+ if (!resolved)
421
+ return false;
422
+ const leaf = resolved.segments.at(-1);
423
+ return leaf !== undefined && leaf in resolved.source;
424
+ }
346
425
  /**
347
426
  * Read the boolean at a spec's dotted `configKey` out of an already-parsed
348
427
  * config object. Returns undefined when any segment is missing or the leaf is
@@ -54,7 +54,8 @@ export function createLiveBusEmitter() {
54
54
  *
55
55
  * `entry` is a THUNK, not a value (M1, #1415 review): building the log entry
56
56
  * (every producer's version normalizes `cwd` via `normalizeFilePath`, a sync
57
- * `realpathSync.native` call on Windows) is real per-publish cost that used
57
+ * `realpathSync.native` call on Windows and, since #3098, on POSIX too) is
58
+ * real per-publish cost that used
58
59
  * to be paid on EVERY call regardless of outcome, even though it is only
59
60
  * consumed on the `stale-session` branch. Invoking the thunk only there means
60
61
  * the common `ready` path pays nothing for it.
@@ -15,6 +15,7 @@ import { pathToFileURL } from "node:url";
15
15
  import { emitBounded } from "../bounded-telemetry.js";
16
16
  import { withTimeout } from "../deadline-utils.js";
17
17
  import { minimatch } from "../deps/minimatch.js";
18
+ import { mapWithConcurrency } from "../map-with-concurrency.js";
18
19
  import { incrementDegradationCount, recordDegradationOnce, } from "../degradation-ledger.js";
19
20
  // vscode-jsonrpc v9 ships an `exports` map exposing the Node entry as the
20
21
  // `./node` subpath (no `.js`); the old `/node.js` file path no longer resolves.
@@ -25,7 +26,7 @@ import { workspaceDiagnosticsCacheSessionStart } from "./workspace-diagnostics-s
25
26
  import { logLatency } from "../latency-logger.js";
26
27
  import { newLspMutationCorrelationId, } from "../lsp-mutation.js";
27
28
  import { getProcessSingleton } from "../process-singletons.js";
28
- import { getAmbientAbortSignal } from "../safe-spawn.js";
29
+ import { getAmbientAbortSignal, isOwnLiveChild, releaseOwnChildPid, } from "../safe-spawn.js";
29
30
  import { raceToCompletion } from "./aggregation.js";
30
31
  import { hashDiagnosticContent, } from "./diagnostic-binding.js";
31
32
  import { applyWorkspaceEdit, normalizeWorkspaceEditToUtf16 } from "./edits.js";
@@ -216,25 +217,6 @@ const WORKSPACE_PULL_SCOPE = "*workspace*";
216
217
  // load. Small and fixed — this is a background improvement after the
217
218
  // protocol reply, never something worth tuning per project.
218
219
  const REFRESH_REPULL_CONCURRENCY = 4;
219
- /** Run `mapper` over `items` with at most `concurrency` in flight at once.
220
- * Same shape as `dependency-checker.ts`'s helper of the same name — a
221
- * worker-pool pattern repeated per-file by design in this codebase rather
222
- * than shared, so each caller can keep it un-exported and file-local. */
223
- async function mapWithConcurrency(items, concurrency, mapper) {
224
- if (items.length === 0)
225
- return;
226
- let nextIndex = 0;
227
- const workerCount = Math.max(1, Math.min(concurrency, items.length));
228
- const worker = async () => {
229
- while (true) {
230
- const index = nextIndex++;
231
- if (index >= items.length)
232
- return;
233
- await mapper(items[index]);
234
- }
235
- };
236
- await Promise.all(Array.from({ length: workerCount }, () => worker()));
237
- }
238
220
  // Anti-deadlock backstop for workspace/executeCommand. Deliberately generous
239
221
  // (30s): the command is mutating and legitimately long-running (a real server
240
222
  // refactor / organize-imports), so this must not truncate valid work — it only
@@ -383,6 +365,9 @@ export async function killProcessTree(proc, pid, options = {}) {
383
365
  // the handle-based proc.kill() below is moot, so return early.
384
366
  if ((proc.exitCode != null || proc.signalCode != null) &&
385
367
  !options.processExiting) {
368
+ // #3091 F1-r2b: this pid will never be signalled again, so retire the
369
+ // ownership hold taken at spawn rather than leaving it to age out.
370
+ releaseOwnChildPid(pid);
386
371
  proc.unref?.();
387
372
  return;
388
373
  }
@@ -458,7 +443,20 @@ export async function killProcessTree(proc, pid, options = {}) {
458
443
  return;
459
444
  }
460
445
  const killPosixProcessGroup = (signal) => {
461
- if (pid <= 0)
446
+ // #2042: one ownership predicate for every kill-by-raw-pid, replacing
447
+ // the `pid <= 0` sign check that let a test double's invented pid
448
+ // through. A pid we do not own falls back to `killDirectChild` below,
449
+ // which signals through the retained handle and can only ever reach
450
+ // our own child.
451
+ //
452
+ // #3091 F1: `proc` is deliberately NOT passed. The handle arm means
453
+ // "already exited ⇒ refuse", and this group kill must still fire when
454
+ // the direct child is dead — the early return at :1269 is skipped under
455
+ // `options.processExiting`, and a POSIX group outlives its leader, so
456
+ // the group signal is the only thing that reaps surviving grandchildren
457
+ // at host exit (#2026). Ownership here comes from the kernel and, once
458
+ // the leader is gone, from the verdict recorded while it was alive.
459
+ if (!isOwnLiveChild(pid, "lsp-stop-posix-group"))
462
460
  return false;
463
461
  try {
464
462
  process.kill(-pid, signal);
@@ -516,6 +514,11 @@ export async function killProcessTree(proc, pid, options = {}) {
516
514
  killDirectChild("SIGKILL");
517
515
  }
518
516
  }
517
+ // AFTER the escalation, never before it: this tick issues the
518
+ // last signal this pid can receive, and retiring the ownership
519
+ // hold first would make that very SIGKILL the thing that gets
520
+ // refused (#3091 F1-r2b).
521
+ releaseOwnChildPid(pid);
519
522
  }, 1500);
520
523
  timer.unref?.();
521
524
  proc.unref?.();
@@ -553,6 +556,7 @@ export async function killProcessTree(proc, pid, options = {}) {
553
556
  killDirectChild("SIGKILL");
554
557
  }
555
558
  }
559
+ releaseOwnChildPid(pid);
556
560
  }
557
561
  catch {
558
562
  // ignore
@@ -344,8 +344,8 @@ export function createCustomServer(config, id) {
344
344
  ? async (file) => resolveToolCwd("lsp", id, file, {
345
345
  cwd: process.cwd(),
346
346
  ...(config.rootMarkers ? { rootMarkers: config.rootMarkers } : {}),
347
- })
348
- : async (file) => resolveToolCwd("lsp", id, file, { cwd: process.cwd() }),
347
+ }).cwd
348
+ : async (file) => resolveToolCwd("lsp", id, file, { cwd: process.cwd() }).cwd,
349
349
  async spawn(root) {
350
350
  const proc = await launchLSP(config.command, config.args ?? ["--stdio"], {
351
351
  cwd: root,
@@ -7390,9 +7390,16 @@ export class LSPService {
7390
7390
  // duplicating what the per-edit path already suppresses. `content`
7391
7391
  // was already read above for this file, so `detectFileRole` gets the
7392
7392
  // higher-accuracy content-aware classification at no extra cost.
7393
+ // #3041: `filePath`/`scanRoot` additionally apply each rule's own
7394
+ // `ignores` carve-out (#965). ast-grep's LSP does NOT apply it to the
7395
+ // per-document diagnostics it publishes (only its own `scan` walk
7396
+ // does), so without this the sweep re-surfaces exactly what the NAPI
7397
+ // runner skips on the same file.
7393
7398
  const filteredDiagnostics = diagnostics
7394
7399
  ? applyAuxiliarySuppressions(diagnostics, content, {
7395
7400
  fileRole: detectFileRole(filePath, content),
7401
+ filePath,
7402
+ scanRoot: root,
7396
7403
  })
7397
7404
  : diagnostics;
7398
7405
  results.push({
@@ -16,7 +16,7 @@ import { getGlobalPiLensLogDir } from "../probe-home-state.js";
16
16
  import { isFullyQualified } from "../path-utils.js";
17
17
  import { findGlobalBinary } from "../package-manager.js";
18
18
  import { redactSecrets } from "../redact/secrets.js";
19
- import { classifySpawnFailure, SpawnFailureError } from "../safe-spawn.js";
19
+ import { classifySpawnFailure, holdOwnChildPid, isOwnLiveChild, SpawnFailureError, } from "../safe-spawn.js";
20
20
  import { getRubyVersionDirNamesAsync } from "./ruby-drive-dirs.js";
21
21
  const isWindows = process.platform === "win32";
22
22
  /**
@@ -327,6 +327,14 @@ function trySpawn(command, args, cwd, env, needsShell) {
327
327
  throw new Error(`LSP server ${command} exited immediately (code: ${proc.exitCode}). ` +
328
328
  `The binary may be missing or corrupted.`);
329
329
  }
330
+ // #3091 F1-r2: take ownership HERE, while the child is provably alive. LSP
331
+ // servers are the one long-lived child pi-lens spawns outside
332
+ // `safeSpawnAsync`, so without this their first offer of verification is
333
+ // `stopLSP`'s group kill — by which time, on the `processExiting` path, the
334
+ // leader can already be dead, `/proc` is gone, and the group signal that
335
+ // reaps surviving grandchildren (#2026) is refused. The verdict is released
336
+ // by `killProcessTree` when the shutdown ladder is done with the pid.
337
+ holdOwnChildPid(proc.pid, "lsp-spawn");
330
338
  return proc;
331
339
  }
332
340
  /**
@@ -654,7 +662,7 @@ export async function stopLSP(handle) {
654
662
  handle.process.once("exit", done);
655
663
  handle.process.once("error", done);
656
664
  const killWindowsTree = () => {
657
- if (!isWindows || handle.pid <= 0)
665
+ if (!isWindows)
658
666
  return false;
659
667
  // If our child has already exited, its PID is dead and the OS may have
660
668
  // RECYCLED it to an unrelated process. `taskkill /F /T` on a recycled PID
@@ -665,8 +673,12 @@ export async function stopLSP(handle) {
665
673
  // tree-kill a PID we no longer own — fall back to handle.process.kill(),
666
674
  // which on Windows signals via the retained process HANDLE (not the raw
667
675
  // PID), so it's a safe no-op on an already-exited child.
668
- if (handle.process.exitCode !== null ||
669
- handle.process.signalCode !== null)
676
+ //
677
+ // #2042 folded the sign check and this exited check into the one
678
+ // ownership predicate every kill-by-raw-pid now shares; the handle
679
+ // arm below IS this paragraph, and on Windows it is the only
680
+ // ownership evidence available (there is no /proc to read).
681
+ if (!isOwnLiveChild(handle.pid, "lsp-stop-windows-tree", handle.process))
670
682
  return false;
671
683
  try {
672
684
  // Absolute path avoids PATH-resolution substitution on Windows.
@@ -15,6 +15,7 @@ import { augmentPythonEnvironment, detectPythonEnvironment, detectPythonVenv, py
15
15
  export { detectPythonVenv };
16
16
  import { STAGE_TMP_PATTERN } from "../atomic-write-staging.js";
17
17
  import { DOTNET_CSHARP_ROOT_MARKERS, DOTNET_FSHARP_ROOT_MARKERS, KIND_EXTENSIONS, } from "../file-kinds.js";
18
+ import { extensionsForLanguage } from "../language-registry.js";
18
19
  import { CARGO_WORKSPACE_MEMBER_DIALECT, direntsHaveMarkerGlobMatch, isAtOrAboveHomeDir, isFullyQualified, isWindowsPath, matchesWorkspaceMemberPattern, pathsEqual, } from "../path-utils.js";
19
20
  import { ensureTool, findManagedToolBinary, getToolEnvironment, getToolPath, } from "../installer/index.js";
20
21
  import * as installer from "../installer/index.js";
@@ -71,14 +72,14 @@ export async function resolveLspServerCwd(server, filePath, sessionCwd, onRootFa
71
72
  return resolveToolCwd("lsp", server.id, filePath, {
72
73
  cwd: path.dirname(path.resolve(filePath)),
73
74
  rootMarkers,
74
- });
75
+ }).cwd;
75
76
  }
76
77
  const boundedServerRoot = enforceLspRootCeiling(serverRoot, sessionCwd, filePath);
77
78
  return resolveToolCwd("lsp", server.id, filePath, {
78
79
  cwd: sessionCwd,
79
80
  rootMarkers,
80
81
  serverRoot: boundedServerRoot,
81
- });
82
+ }).cwd;
82
83
  }
83
84
  const FIXTURE_ROOT_SEGMENTS = new Set(["__fixtures__", "testdata"]);
84
85
  const FALLBACK_PROJECT_MARKERS = [
@@ -2493,6 +2494,21 @@ export const GleamServer = {
2493
2494
  }, options?.allowInstall);
2494
2495
  },
2495
2496
  };
2497
+ export const TinymistServer = {
2498
+ id: "tinymist",
2499
+ name: "Tinymist",
2500
+ extensions: extensionsForLanguage("typst"),
2501
+ root: RootWithFallback(createRootDetector(["typst.toml", ".git"])),
2502
+ availabilityKey: "tinymist",
2503
+ async spawn(root, options) {
2504
+ return resolveAndLaunch({
2505
+ candidates: ["tinymist"],
2506
+ args: ["lsp"],
2507
+ cwd: root,
2508
+ managedToolId: "tinymist",
2509
+ }, options?.allowInstall);
2510
+ },
2511
+ };
2496
2512
  export const MarksmanServer = {
2497
2513
  id: "marksman",
2498
2514
  name: "Marksman",
@@ -3146,6 +3162,7 @@ export const LSP_SERVERS = [
3146
3162
  ElixirServer,
3147
3163
  ElixirExpertServer,
3148
3164
  GleamServer,
3165
+ TinymistServer,
3149
3166
  MarksmanServer,
3150
3167
  OCamlServer,
3151
3168
  ClojureServer,
@@ -150,7 +150,8 @@ export function clearWorkspaceDiagnosticsCacheAtAndAbove(cwd, ceiling = process.
150
150
  // "recompute", the same outcome a real refresh produces. The pre-#1754 map
151
151
  // was unbounded; the bound is new and costs at most one extra sweep for a
152
152
  // cwd that fell off the end.
153
- // - `normalizeMapKey` runs `realpathSync.native` on Windows. A handle re-runs
153
+ // - `normalizeMapKey` runs `realpathSync.native` on Windows and, since #3098,
154
+ // on POSIX as well. A handle re-runs
154
155
  // it only after the map has been invalidated, so an uninterrupted sweep pays
155
156
  // that syscall once at context creation rather than once per `lookup()`, and
156
157
  // an interrupted one pays it per check to stay correct if the cwd's realpath
@@ -85,9 +85,11 @@ function uniqueDetails(files, fileDetails) {
85
85
  const byPath = new Map();
86
86
  // #2016: `files` and `fileDetails` name the same paths, so without this the
87
87
  // map-build loop and the lookup below each pay `realpathSync.native` for the
88
- // same path (~200 microseconds per call on Windows; POSIX short-circuits, so
89
- // CI cannot see it). The memo lives for one call, so it has no staleness
90
- // window at all and needs no freshness design.
88
+ // same path (~200 microseconds per call on Windows; since #3098 POSIX pays it
89
+ // too, measured at 1.8 microseconds for an existing path still under any CI
90
+ // timing gate, so the waste stays invisible either way). The memo lives for
91
+ // one call, so it has no staleness window at all and needs no freshness
92
+ // design.
91
93
  const keyMemo = new Map();
92
94
  const keyFor = (filePath) => {
93
95
  let key = keyMemo.get(filePath);