@herbertgao/pi-extensions 2026.9.5 → 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 (276) hide show
  1. package/README.md +5 -22
  2. package/THIRD_PARTY_NOTICES.md +0 -4
  3. package/node_modules/@herbertgao/sol-pi/docs/compatibility.md +1 -1
  4. package/node_modules/@herbertgao/sol-pi/package.json +1 -1
  5. package/node_modules/@herbertgao/sol-pi/src/sol-pi/extensions/action-fusion/index.ts +0 -5
  6. package/node_modules/@herbertgao/sol-pi/src/sol-pi/extensions/online-context-compact/state.ts +3 -1
  7. package/node_modules/@narumitw/pi-btw/README.md +2 -0
  8. package/node_modules/@narumitw/pi-btw/dist/index.ts +284 -365
  9. package/node_modules/@narumitw/pi-btw/dist/index.ts.map +3 -3
  10. package/node_modules/@narumitw/pi-btw/package.json +52 -53
  11. package/node_modules/@narumitw/pi-btw/src/bring-to-main.ts +494 -547
  12. package/node_modules/@narumitw/pi-btw/src/btw.ts +814 -856
  13. package/node_modules/@narumitw/pi-btw/src/fullscreen-ui.ts +673 -674
  14. package/node_modules/@narumitw/pi-btw/src/keybindings.ts +228 -270
  15. package/node_modules/@narumitw/pi-btw/src/main-tree-picker.ts +309 -318
  16. package/node_modules/@narumitw/pi-btw/src/menu.ts +416 -454
  17. package/node_modules/@narumitw/pi-btw/src/settings.ts +203 -219
  18. package/node_modules/@narumitw/pi-btw/src/side-thread.ts +173 -201
  19. package/node_modules/@narumitw/pi-btw/src/text.ts +21 -23
  20. package/node_modules/@narumitw/pi-btw/src/transcript-markdown.ts +65 -0
  21. package/node_modules/@narumitw/pi-btw/src/transcript-pager.ts +611 -662
  22. package/node_modules/pi-lens/CHANGELOG.md +239 -0
  23. package/node_modules/pi-lens/config/dependency-cruiser-eager-allowlist.json +3 -0
  24. package/node_modules/pi-lens/dist/clients/actionable-warnings.js +30 -4
  25. package/node_modules/pi-lens/dist/clients/agent-behavior-client.js +2 -0
  26. package/node_modules/pi-lens/dist/clients/bash-file-access.js +26 -0
  27. package/node_modules/pi-lens/dist/clients/biome-client.js +1 -1
  28. package/node_modules/pi-lens/dist/clients/blocker-freshness.js +403 -35
  29. package/node_modules/pi-lens/dist/clients/bootstrap.js +6 -1
  30. package/node_modules/pi-lens/dist/clients/cascade-format.js +200 -4
  31. package/node_modules/pi-lens/dist/clients/code-quality-warnings.js +10 -2
  32. package/node_modules/pi-lens/dist/clients/degradation-ledger.js +16 -1
  33. package/node_modules/pi-lens/dist/clients/diagnostic-dispositions.js +12 -1
  34. package/node_modules/pi-lens/dist/clients/dispatch/ast-grep-catalog.js +65 -0
  35. package/node_modules/pi-lens/dist/clients/dispatch/auxiliary-lsp.js +28 -0
  36. package/node_modules/pi-lens/dist/clients/dispatch/dispatcher.js +9 -2
  37. package/node_modules/pi-lens/dist/clients/dispatch/finding-policy.js +222 -0
  38. package/node_modules/pi-lens/dist/clients/dispatch/indent-detect.js +389 -13
  39. package/node_modules/pi-lens/dist/clients/dispatch/integration.js +175 -25
  40. package/node_modules/pi-lens/dist/clients/dispatch/rule-ignores.js +62 -0
  41. package/node_modules/pi-lens/dist/clients/dispatch/runners/ast-grep-napi.js +21 -41
  42. package/node_modules/pi-lens/dist/clients/dispatch/runners/biome-check.js +6 -1
  43. package/node_modules/pi-lens/dist/clients/dispatch/runners/rust-clippy.js +7 -7
  44. package/node_modules/pi-lens/dist/clients/dispatch/runners/shfmt.js +2 -12
  45. package/node_modules/pi-lens/dist/clients/dispatch/runners/tree-sitter.js +2 -21
  46. package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/runner-helpers.js +1 -1
  47. package/node_modules/pi-lens/dist/clients/dispatch/types.js +4 -1
  48. package/node_modules/pi-lens/dist/clients/disposition-publish.js +1 -0
  49. package/node_modules/pi-lens/dist/clients/effective-config.js +1 -1
  50. package/node_modules/pi-lens/dist/clients/file-kinds.js +26 -1
  51. package/node_modules/pi-lens/dist/clients/file-role.js +21 -21
  52. package/node_modules/pi-lens/dist/clients/file-utils.js +156 -10
  53. package/node_modules/pi-lens/dist/clients/finding-delivery-gate.js +56 -9
  54. package/node_modules/pi-lens/dist/clients/format-service.js +39 -21
  55. package/node_modules/pi-lens/dist/clients/formatters.js +60 -13
  56. package/node_modules/pi-lens/dist/clients/generation-guard.js +2 -2
  57. package/node_modules/pi-lens/dist/clients/git-guard.js +11 -3
  58. package/node_modules/pi-lens/dist/clients/indent-retarget.js +78 -8
  59. package/node_modules/pi-lens/dist/clients/installer/index.js +333 -114
  60. package/node_modules/pi-lens/dist/clients/instance-reaper-state.js +7 -0
  61. package/node_modules/pi-lens/dist/clients/instance-reaper.js +20 -10
  62. package/node_modules/pi-lens/dist/clients/language-profile.js +23 -1
  63. package/node_modules/pi-lens/dist/clients/language-registry.js +5 -1
  64. package/node_modules/pi-lens/dist/clients/latency-logger.js +4 -1
  65. package/node_modules/pi-lens/dist/clients/lens-engine.js +12 -3
  66. package/node_modules/pi-lens/dist/clients/lens-events.js +2 -1
  67. package/node_modules/pi-lens/dist/clients/lens-flag-registry.js +79 -0
  68. package/node_modules/pi-lens/dist/clients/live-bus-emitter.js +2 -1
  69. package/node_modules/pi-lens/dist/clients/lsp/client.js +25 -21
  70. package/node_modules/pi-lens/dist/clients/lsp/config.js +2 -2
  71. package/node_modules/pi-lens/dist/clients/lsp/index.js +7 -0
  72. package/node_modules/pi-lens/dist/clients/lsp/launch.js +16 -4
  73. package/node_modules/pi-lens/dist/clients/lsp/server.js +19 -2
  74. package/node_modules/pi-lens/dist/clients/lsp/workspace-diagnostics-cache.js +2 -1
  75. package/node_modules/pi-lens/dist/clients/lsp-mutation.js +5 -3
  76. package/node_modules/pi-lens/dist/clients/map-with-concurrency.js +12 -8
  77. package/node_modules/pi-lens/dist/clients/mcp/session.js +36 -4
  78. package/node_modules/pi-lens/dist/clients/ndjson-logger.js +24 -7
  79. package/node_modules/pi-lens/dist/clients/observed-mutation.js +40 -2
  80. package/node_modules/pi-lens/dist/clients/opaque-mutation-scan.js +23 -3
  81. package/node_modules/pi-lens/dist/clients/opengrep-client.js +110 -15
  82. package/node_modules/pi-lens/dist/clients/path-utils.js +123 -3
  83. package/node_modules/pi-lens/dist/clients/performance-report.js +5 -1
  84. package/node_modules/pi-lens/dist/clients/php-cs-fixer-config.js +1 -1
  85. package/node_modules/pi-lens/dist/clients/pipeline.js +74 -5
  86. package/node_modules/pi-lens/dist/clients/project-diagnostics/cache.js +37 -5
  87. package/node_modules/pi-lens/dist/clients/project-diagnostics/fresh-fetch.js +145 -18
  88. package/node_modules/pi-lens/dist/clients/project-diagnostics/runner-adapters/dead-code.js +4 -1
  89. package/node_modules/pi-lens/dist/clients/project-diagnostics/scanner.js +59 -3
  90. package/node_modules/pi-lens/dist/clients/project-lens-config.js +45 -2
  91. package/node_modules/pi-lens/dist/clients/read-guard.js +70 -11
  92. package/node_modules/pi-lens/dist/clients/resource-sampler.js +147 -9
  93. package/node_modules/pi-lens/dist/clients/ruff-client.js +1 -1
  94. package/node_modules/pi-lens/dist/clients/runtime-agent-end.js +33 -5
  95. package/node_modules/pi-lens/dist/clients/runtime-context.js +6 -2
  96. package/node_modules/pi-lens/dist/clients/runtime-coordinator.js +77 -7
  97. package/node_modules/pi-lens/dist/clients/runtime-session.js +21 -2
  98. package/node_modules/pi-lens/dist/clients/runtime-tool-call.js +43 -12
  99. package/node_modules/pi-lens/dist/clients/runtime-tool-result.js +330 -85
  100. package/node_modules/pi-lens/dist/clients/runtime-turn.js +147 -17
  101. package/node_modules/pi-lens/dist/clients/safe-spawn.js +258 -5
  102. package/node_modules/pi-lens/dist/clients/scratch-tree-policy.js +6 -0
  103. package/node_modules/pi-lens/dist/clients/session-event-guard.js +50 -5
  104. package/node_modules/pi-lens/dist/clients/sg-runner.js +23 -11
  105. package/node_modules/pi-lens/dist/clients/sgconfig.js +79 -2
  106. package/node_modules/pi-lens/dist/clients/test-runner-client.js +6 -41
  107. package/node_modules/pi-lens/dist/clients/test-runner-delivery.js +43 -0
  108. package/node_modules/pi-lens/dist/clients/tool-agreement.js +423 -0
  109. package/node_modules/pi-lens/dist/clients/tool-config.js +50 -0
  110. package/node_modules/pi-lens/dist/clients/tool-cwd.js +47 -76
  111. package/node_modules/pi-lens/dist/clients/tool-policy.js +163 -7
  112. package/node_modules/pi-lens/dist/clients/tree-sitter-client.js +120 -0
  113. package/node_modules/pi-lens/dist/clients/tree-sitter-query-loader.js +127 -189
  114. package/node_modules/pi-lens/dist/clients/widget-state.js +98 -0
  115. package/node_modules/pi-lens/dist/clients/word-index.js +5 -2
  116. package/node_modules/pi-lens/dist/clients/write-ordering-guard.js +7 -0
  117. package/node_modules/pi-lens/dist/index.js +27416 -24153
  118. package/node_modules/pi-lens/dist/mcp/server.js +5 -2
  119. package/node_modules/pi-lens/dist/tools/lens-diagnostic-mark.js +7 -1
  120. package/node_modules/pi-lens/dist/tools/lens-diagnostics.js +168 -53
  121. package/node_modules/pi-lens/dist/tools/lsp-diagnostics.js +244 -72
  122. package/node_modules/pi-lens/docs/audit1.md +5 -5
  123. package/node_modules/pi-lens/docs/dependencies.md +11 -0
  124. package/node_modules/pi-lens/docs/dispositions.md +52 -0
  125. package/node_modules/pi-lens/docs/environment-variables.md +2 -2
  126. package/node_modules/pi-lens/docs/features.md +4 -4
  127. package/node_modules/pi-lens/docs/globalconfig.md +3 -1
  128. package/node_modules/pi-lens/docs/language-coverage.md +1 -0
  129. package/node_modules/pi-lens/docs/pi-lens-fixer.md +53 -94
  130. package/node_modules/pi-lens/docs/pi-lens-investigator.md +43 -38
  131. package/node_modules/pi-lens/docs/pi-lens-retro.md +97 -0
  132. package/node_modules/pi-lens/docs/pi-lens-reviewer.md +53 -66
  133. package/node_modules/pi-lens/docs/release-qa-baseline.md +22 -0
  134. package/node_modules/pi-lens/docs/settings.md +7 -4
  135. package/node_modules/pi-lens/docs/tree-sitter_rules_catalog.md +1 -1
  136. package/node_modules/pi-lens/docs/usage.md +2 -0
  137. package/node_modules/pi-lens/package.json +1 -1
  138. package/node_modules/pi-lens/rules/tree-sitter-queries/typescript/sql-injection.yml +24 -12
  139. package/node_modules/pi-lens/scripts/analyze-pi-lens-logs.mjs +40 -0
  140. package/node_modules/pi-lens/skills/pi-lens-ast-grep/SKILL.md +15 -0
  141. package/node_modules/pi-lens/skills/pi-lens-lsp-navigation/SKILL.md +7 -0
  142. package/package.json +6 -17
  143. package/examples/pi-footer.json +0 -275
  144. package/node_modules/@czottmann/pi-automode/CHANGELOG.md +0 -70
  145. package/node_modules/@czottmann/pi-automode/LICENSE.md +0 -21
  146. package/node_modules/@czottmann/pi-automode/README.md +0 -215
  147. package/node_modules/@czottmann/pi-automode/docs/GLOSSARY.md +0 -39
  148. package/node_modules/@czottmann/pi-automode/docs/adr/ADR-001-permission-precedence-and-trust-boundaries.md +0 -46
  149. package/node_modules/@czottmann/pi-automode/docs/adr/ADR-002-global-config-in-extension-data-directory.md +0 -60
  150. package/node_modules/@czottmann/pi-automode/docs/adr/INDEX.md +0 -6
  151. package/node_modules/@czottmann/pi-automode/docs/automode-classifier-flow.md +0 -428
  152. package/node_modules/@czottmann/pi-automode/docs/configuration.md +0 -171
  153. package/node_modules/@czottmann/pi-automode/docs/defaults.md +0 -178
  154. package/node_modules/@czottmann/pi-automode/docs/diagnostics.md +0 -90
  155. package/node_modules/@czottmann/pi-automode/docs/observability-logging.md +0 -160
  156. package/node_modules/@czottmann/pi-automode/examples/automode.local.json +0 -44
  157. package/node_modules/@czottmann/pi-automode/extensions/auto-mode/bash.ts +0 -692
  158. package/node_modules/@czottmann/pi-automode/extensions/auto-mode/classifier.ts +0 -852
  159. package/node_modules/@czottmann/pi-automode/extensions/auto-mode/config.ts +0 -904
  160. package/node_modules/@czottmann/pi-automode/extensions/auto-mode/constants.ts +0 -228
  161. package/node_modules/@czottmann/pi-automode/extensions/auto-mode/extension.ts +0 -954
  162. package/node_modules/@czottmann/pi-automode/extensions/auto-mode/hard-deny.ts +0 -429
  163. package/node_modules/@czottmann/pi-automode/extensions/auto-mode/log.ts +0 -173
  164. package/node_modules/@czottmann/pi-automode/extensions/auto-mode/model-selector.ts +0 -113
  165. package/node_modules/@czottmann/pi-automode/extensions/auto-mode/model.ts +0 -13
  166. package/node_modules/@czottmann/pi-automode/extensions/auto-mode/paths.ts +0 -297
  167. package/node_modules/@czottmann/pi-automode/extensions/auto-mode/permissions.ts +0 -623
  168. package/node_modules/@czottmann/pi-automode/extensions/auto-mode/state.ts +0 -102
  169. package/node_modules/@czottmann/pi-automode/extensions/auto-mode/transcript.ts +0 -236
  170. package/node_modules/@czottmann/pi-automode/extensions/auto-mode/types.ts +0 -205
  171. package/node_modules/@czottmann/pi-automode/extensions/auto-mode/utils.ts +0 -54
  172. package/node_modules/@czottmann/pi-automode/extensions/auto-mode.ts +0 -26
  173. package/node_modules/@czottmann/pi-automode/package.json +0 -61
  174. package/node_modules/@czottmann/pi-automode/skills/automode-diagnostics/SKILL.md +0 -63
  175. package/node_modules/@luxusai/pi-hindsight/CHANGELOG.md +0 -376
  176. package/node_modules/@luxusai/pi-hindsight/README.md +0 -92
  177. package/node_modules/@luxusai/pi-hindsight/docs/adr/001-memory-lifecycle-and-scope.md +0 -56
  178. package/node_modules/@luxusai/pi-hindsight/docs/adr/002-explicit-routing-strategy-seam.md +0 -169
  179. package/node_modules/@luxusai/pi-hindsight/docs/adr/003-tui-memory-mode-vocabulary.md +0 -107
  180. package/node_modules/@luxusai/pi-hindsight/docs/adr/004-lifeos-dual-bank-design.md +0 -89
  181. package/node_modules/@luxusai/pi-hindsight/docs/adr/005-domain-banks-and-agent-first-surface.md +0 -189
  182. package/node_modules/@luxusai/pi-hindsight/docs/assets/logos/pi-hindsight-logo-dark.webp +0 -0
  183. package/node_modules/@luxusai/pi-hindsight/docs/assets/logos/pi-hindsight-logo-dark@2x.webp +0 -0
  184. package/node_modules/@luxusai/pi-hindsight/docs/coding-memory-evaluation.md +0 -54
  185. package/node_modules/@luxusai/pi-hindsight/docs/compatibility.md +0 -75
  186. package/node_modules/@luxusai/pi-hindsight/docs/hindsight-core-functions.md +0 -300
  187. package/node_modules/@luxusai/pi-hindsight/docs/mission-and-mental-model-quality.md +0 -158
  188. package/node_modules/@luxusai/pi-hindsight/docs/next-opt-out-design.md +0 -164
  189. package/node_modules/@luxusai/pi-hindsight/docs/risky-memory-modes.md +0 -139
  190. package/node_modules/@luxusai/pi-hindsight/docs/starter-mental-model-suggestions.md +0 -74
  191. package/node_modules/@luxusai/pi-hindsight/docs/surface-reference.md +0 -261
  192. package/node_modules/@luxusai/pi-hindsight/extensions/banks/bank-operations.ts +0 -151
  193. package/node_modules/@luxusai/pi-hindsight/extensions/banks/bank-selection.ts +0 -18
  194. package/node_modules/@luxusai/pi-hindsight/extensions/banks/bank-settings-presenter.ts +0 -46
  195. package/node_modules/@luxusai/pi-hindsight/extensions/banks/bank-templates.ts +0 -383
  196. package/node_modules/@luxusai/pi-hindsight/extensions/banks/banking.ts +0 -240
  197. package/node_modules/@luxusai/pi-hindsight/extensions/banks/knowledge-page-seed.ts +0 -176
  198. package/node_modules/@luxusai/pi-hindsight/extensions/banks/retain-strategies.ts +0 -178
  199. package/node_modules/@luxusai/pi-hindsight/extensions/client/client-retry.ts +0 -41
  200. package/node_modules/@luxusai/pi-hindsight/extensions/client/client.ts +0 -386
  201. package/node_modules/@luxusai/pi-hindsight/extensions/client/fetch-compat.ts +0 -39
  202. package/node_modules/@luxusai/pi-hindsight/extensions/client/timeout.ts +0 -34
  203. package/node_modules/@luxusai/pi-hindsight/extensions/config/config-defaults.ts +0 -140
  204. package/node_modules/@luxusai/pi-hindsight/extensions/config/config-editing-model.ts +0 -61
  205. package/node_modules/@luxusai/pi-hindsight/extensions/config/config-editing-registry.ts +0 -925
  206. package/node_modules/@luxusai/pi-hindsight/extensions/config/config-field-paths.ts +0 -247
  207. package/node_modules/@luxusai/pi-hindsight/extensions/config/config-normalize.ts +0 -547
  208. package/node_modules/@luxusai/pi-hindsight/extensions/config/config-writer.ts +0 -480
  209. package/node_modules/@luxusai/pi-hindsight/extensions/config/config.ts +0 -182
  210. package/node_modules/@luxusai/pi-hindsight/extensions/config/setup-gate.ts +0 -86
  211. package/node_modules/@luxusai/pi-hindsight/extensions/imports/import-execute.ts +0 -1010
  212. package/node_modules/@luxusai/pi-hindsight/extensions/imports/import-parse.ts +0 -175
  213. package/node_modules/@luxusai/pi-hindsight/extensions/imports/import-plan.ts +0 -425
  214. package/node_modules/@luxusai/pi-hindsight/extensions/imports/import-presentation.ts +0 -210
  215. package/node_modules/@luxusai/pi-hindsight/extensions/imports/import-sessions.ts +0 -817
  216. package/node_modules/@luxusai/pi-hindsight/extensions/index.ts +0 -25
  217. package/node_modules/@luxusai/pi-hindsight/extensions/lifecycle/git-seed.ts +0 -319
  218. package/node_modules/@luxusai/pi-hindsight/extensions/lifecycle/memory-lifecycle-recall.ts +0 -196
  219. package/node_modules/@luxusai/pi-hindsight/extensions/lifecycle/memory-lifecycle-retain.ts +0 -189
  220. package/node_modules/@luxusai/pi-hindsight/extensions/lifecycle/memory-lifecycle-runtime.ts +0 -65
  221. package/node_modules/@luxusai/pi-hindsight/extensions/lifecycle/memory-lifecycle.ts +0 -260
  222. package/node_modules/@luxusai/pi-hindsight/extensions/lifecycle/mental-models.ts +0 -244
  223. package/node_modules/@luxusai/pi-hindsight/extensions/lifecycle/observation-scopes.ts +0 -59
  224. package/node_modules/@luxusai/pi-hindsight/extensions/lifecycle/recall-cleanup.ts +0 -82
  225. package/node_modules/@luxusai/pi-hindsight/extensions/lifecycle/recall-visibility.ts +0 -45
  226. package/node_modules/@luxusai/pi-hindsight/extensions/lifecycle/recall.ts +0 -331
  227. package/node_modules/@luxusai/pi-hindsight/extensions/lifecycle/retain-cursor.ts +0 -354
  228. package/node_modules/@luxusai/pi-hindsight/extensions/lifecycle/retain-job-builder.ts +0 -66
  229. package/node_modules/@luxusai/pi-hindsight/extensions/lifecycle/retain-receipts.ts +0 -132
  230. package/node_modules/@luxusai/pi-hindsight/extensions/lifecycle/retain.ts +0 -228
  231. package/node_modules/@luxusai/pi-hindsight/extensions/operations/memory-bank-template-operations.ts +0 -74
  232. package/node_modules/@luxusai/pi-hindsight/extensions/operations/memory-config-operations.ts +0 -16
  233. package/node_modules/@luxusai/pi-hindsight/extensions/operations/memory-control-operations.ts +0 -648
  234. package/node_modules/@luxusai/pi-hindsight/extensions/operations/memory-diagnostics-operations.ts +0 -105
  235. package/node_modules/@luxusai/pi-hindsight/extensions/operations/memory-identity.ts +0 -68
  236. package/node_modules/@luxusai/pi-hindsight/extensions/operations/memory-operation-service.ts +0 -71
  237. package/node_modules/@luxusai/pi-hindsight/extensions/operations/memory-operation-types.ts +0 -13
  238. package/node_modules/@luxusai/pi-hindsight/extensions/operations/memory-recall-operations.ts +0 -162
  239. package/node_modules/@luxusai/pi-hindsight/extensions/operations/memory-retain-operations.ts +0 -131
  240. package/node_modules/@luxusai/pi-hindsight/extensions/operations/memory-scope.ts +0 -104
  241. package/node_modules/@luxusai/pi-hindsight/extensions/operations/memory-session-operations.ts +0 -37
  242. package/node_modules/@luxusai/pi-hindsight/extensions/operations/operation-catalog.ts +0 -893
  243. package/node_modules/@luxusai/pi-hindsight/extensions/operations/reflect-presenter.ts +0 -35
  244. package/node_modules/@luxusai/pi-hindsight/extensions/operations/scope-migrate.ts +0 -177
  245. package/node_modules/@luxusai/pi-hindsight/extensions/operations/tools.ts +0 -7
  246. package/node_modules/@luxusai/pi-hindsight/extensions/queue/flush-presenter.ts +0 -21
  247. package/node_modules/@luxusai/pi-hindsight/extensions/queue/jsonl-queue-store.ts +0 -112
  248. package/node_modules/@luxusai/pi-hindsight/extensions/queue/queue-delivery.ts +0 -111
  249. package/node_modules/@luxusai/pi-hindsight/extensions/queue/queue-lock.ts +0 -214
  250. package/node_modules/@luxusai/pi-hindsight/extensions/queue/queue-operations.ts +0 -70
  251. package/node_modules/@luxusai/pi-hindsight/extensions/queue/queue.ts +0 -392
  252. package/node_modules/@luxusai/pi-hindsight/extensions/tui/bank-template-presentation.ts +0 -45
  253. package/node_modules/@luxusai/pi-hindsight/extensions/tui/commands.ts +0 -9
  254. package/node_modules/@luxusai/pi-hindsight/extensions/tui/guided-setup.ts +0 -860
  255. package/node_modules/@luxusai/pi-hindsight/extensions/tui/prefill-input.ts +0 -105
  256. package/node_modules/@luxusai/pi-hindsight/extensions/tui/setup-flow.ts +0 -189
  257. package/node_modules/@luxusai/pi-hindsight/extensions/tui/setup-server-probe.ts +0 -299
  258. package/node_modules/@luxusai/pi-hindsight/extensions/tui/setup-tui-actions.ts +0 -201
  259. package/node_modules/@luxusai/pi-hindsight/extensions/tui/setup-tui-facts.ts +0 -47
  260. package/node_modules/@luxusai/pi-hindsight/extensions/tui/setup-tui-render.ts +0 -239
  261. package/node_modules/@luxusai/pi-hindsight/extensions/tui/setup-tui-types.ts +0 -50
  262. package/node_modules/@luxusai/pi-hindsight/extensions/tui/setup-tui.ts +0 -259
  263. package/node_modules/@luxusai/pi-hindsight/extensions/tui/tool-presenters.ts +0 -77
  264. package/node_modules/@luxusai/pi-hindsight/extensions/types.ts +0 -534
  265. package/node_modules/@luxusai/pi-hindsight/extensions/utils/diagnostics.ts +0 -319
  266. package/node_modules/@luxusai/pi-hindsight/extensions/utils/messages.ts +0 -325
  267. package/node_modules/@luxusai/pi-hindsight/extensions/utils/sanitize.ts +0 -42
  268. package/node_modules/@luxusai/pi-hindsight/extensions/utils/session-memory-meta.ts +0 -244
  269. package/node_modules/@luxusai/pi-hindsight/extensions/utils/session-operations.ts +0 -56
  270. package/node_modules/@luxusai/pi-hindsight/extensions/utils/session.ts +0 -50
  271. package/node_modules/@luxusai/pi-hindsight/extensions/utils/status-fields.ts +0 -167
  272. package/node_modules/@luxusai/pi-hindsight/extensions/utils/status-health.ts +0 -207
  273. package/node_modules/@luxusai/pi-hindsight/extensions/utils/status.ts +0 -120
  274. package/node_modules/@luxusai/pi-hindsight/extensions/version.ts +0 -8
  275. package/node_modules/@luxusai/pi-hindsight/package.json +0 -118
  276. package/node_modules/@luxusai/pi-hindsight/skills/hindsight-memory-doctor/SKILL.md +0 -75
@@ -39,6 +39,9 @@ import { sweepInlineBlockerPastEof } from "./blocker-past-eof.js";
39
39
  import { getLSPService } from "./lsp/index.js";
40
40
  import { drainPendingAuxCapEvictedCount, drainPendingAuxiliaryCoverage, isPendingAuxiliaryPastRearmTtl, rearmPendingAuxiliaryCoverage, MAX_LATE_AUX_REARMS, pendingAuxiliaryCoverageSize, } from "./lsp/pending-aux-coverage.js";
41
41
  import { convertLspDiagnostics } from "./dispatch/utils/lsp-diagnostics.js";
42
+ import { retagAuxiliaryDiagnostics } from "./dispatch/auxiliary-lsp.js";
43
+ import { applyFindingPolicy, loadProjectRulePolicyMap, renderedRuleIdentities, } from "./dispatch/finding-policy.js";
44
+ import { detectFileRole } from "./file-role.js";
42
45
  import { drainPendingRunnerFindings, dropStaleRunnerFindings, pendingRunnerFindingsSize, } from "./dispatch/pending-runner-findings.js";
43
46
  // #1631 review V2: moved to its own leaf module so a low-level store
44
47
  // (widget-state.ts) can use the marker without importing this orchestrator —
@@ -472,7 +475,15 @@ function capTurnEndMessage(content) {
472
475
  return out;
473
476
  }
474
477
  export async function handleTurnEnd(deps) {
475
- const { ctxCwd, getFlag, dbg, runtime, cacheManager, knipClient, deadCodeClients, depChecker, testRunnerClient, sessionId, owner, resetLSPService, resetFormatService, } = deps;
478
+ const { ctxCwd, getFlag, dbg, runtime, cacheManager, knipClient, deadCodeClients, depChecker, testRunnerClient, sessionId, host = "pi", owner, resetLSPService, resetFormatService, } = deps;
479
+ const turnIndexAtDispatch = runtime.turnIndex;
480
+ const clearOwnedTurnState = () => {
481
+ if (runtime.turnIndex !== turnIndexAtDispatch) {
482
+ dbg(`turn_end: retaining newer turn state (dispatch=${turnIndexAtDispatch}, current=${runtime.turnIndex})`);
483
+ return;
484
+ }
485
+ cacheManager.clearTurnState(cwd, currentOwner);
486
+ };
476
487
  // #449 slice 1: piggyback the instance-registry heartbeat on this existing
477
488
  // per-turn touchpoint rather than adding a new timer/interval. Cheap (reads
478
489
  // process.memoryUsage().rss, one read-modify-write of instances.json) and
@@ -527,7 +538,7 @@ export async function handleTurnEnd(deps) {
527
538
  if (access === "available" &&
528
539
  (turnState.files || turnState.owner || turnState.sessionId)) {
529
540
  dbg("turn_end: evicting stale turn-state owner");
530
- cacheManager.clearTurnState(cwd, currentOwner);
541
+ clearOwnedTurnState();
531
542
  turnState = cacheManager.readTurnState(cwd);
532
543
  }
533
544
  const files = Object.keys(turnState.files);
@@ -640,7 +651,7 @@ export async function handleTurnEnd(deps) {
640
651
  dbg(`turn_end: ${files.length} file(s) modified, cycles: ${turnState.turnCycles}/${turnState.maxCycles}, access: ${access}, owner: ${gateOwnerLabel}`);
641
652
  if (cacheManager.isMaxCyclesExceeded(cwd)) {
642
653
  dbg("turn_end: max cycles exceeded, clearing state and forcing through");
643
- cacheManager.clearTurnState(cwd, currentOwner);
654
+ clearOwnedTurnState();
644
655
  runtime.fixedThisTurn.clear();
645
656
  resetFormatService();
646
657
  return;
@@ -691,6 +702,9 @@ export async function handleTurnEnd(deps) {
691
702
  // map — see blocker-freshness.ts's `WidgetSweepBlockerEntry` doc for why this
692
703
  // is injected here rather than imported by blocker-freshness.ts itself.
693
704
  const blockerFreshness = await sweepInlineBlockerFreshness(runtime, cwd, {
705
+ // #2982: the hook's own signal, so the self axis's filesystem work is
706
+ // bounded by the same abort everything else in this handler honours.
707
+ ...(deps.signal === undefined ? {} : { signal: deps.signal }),
694
708
  additionalEntries: getWidgetBlockingFilesForSweep().map((row) => ({
695
709
  filePath: row.filePath,
696
710
  recordedAtMs: row.recordedAtMs,
@@ -709,6 +723,9 @@ export async function handleTurnEnd(deps) {
709
723
  revalidated: blockerFreshness.revalidated,
710
724
  alreadyStale: blockerFreshness.alreadyStale,
711
725
  truncatedImports: blockerFreshness.truncatedImports,
726
+ selfHealed: blockerFreshness.selfHealed,
727
+ selfUnverifiable: blockerFreshness.selfUnverifiable,
728
+ hashBudgetExhausted: blockerFreshness.hashBudgetExhausted,
712
729
  },
713
730
  });
714
731
  // Re-surface inline blockers from this turn that the agent didn't fix.
@@ -1960,6 +1977,7 @@ export async function handleTurnEnd(deps) {
1960
1977
  heldDeferred.push({
1961
1978
  testFile,
1962
1979
  runner: carried.runner,
1980
+ sourceFile: carried.sourceFile ?? testFile,
1963
1981
  attempts,
1964
1982
  sessionId: turnSessionId,
1965
1983
  });
@@ -1970,6 +1988,7 @@ export async function handleTurnEnd(deps) {
1970
1988
  redispatchedDeferred++;
1971
1989
  targets.push({
1972
1990
  testFile,
1991
+ sourceFile: carried.sourceFile ?? testFile,
1973
1992
  runner: carried.runner,
1974
1993
  config,
1975
1994
  strategy: "deferred",
@@ -2063,7 +2082,7 @@ export async function handleTurnEnd(deps) {
2063
2082
  overCapTargets++;
2064
2083
  continue;
2065
2084
  }
2066
- targets.push(target);
2085
+ targets.push({ ...target, sourceFile: abs });
2067
2086
  dbg(`turn_end: ${display} → test ${target.runner} ${path.relative(cwd, target.testFile)} (${target.strategy}${isNeighbor ? ", cascade-neighbor" : ""})`);
2068
2087
  }
2069
2088
  else if (!target) {
@@ -2091,6 +2110,9 @@ export async function handleTurnEnd(deps) {
2091
2110
  dbg(`turn_end: test target count capped at ${TEST_RUNNER_MAX_TARGETS}, ${overCapTargets} skipped`);
2092
2111
  }
2093
2112
  if (targets.length > 0) {
2113
+ for (const target of targets) {
2114
+ target.fileSeqAtRun = runtime.getFileSeq(target.sourceFile);
2115
+ }
2094
2116
  dbg(`turn_end: firing ${targets.length} test target(s) async (non-blocking, max ${TEST_RUNNER_BATCH_CONCURRENCY} concurrent)`);
2095
2117
  const firedAtTurn = runtime.turnIndex;
2096
2118
  const firedSessionId = turnSessionId;
@@ -2137,8 +2159,32 @@ export async function handleTurnEnd(deps) {
2137
2159
  }),
2138
2160
  })
2139
2161
  .then(({ results, deferred, stopReason }) => {
2162
+ const settledResults = results;
2163
+ const verdicts = settledResults.flatMap((result) => {
2164
+ if (result.status === "rejected")
2165
+ return [];
2166
+ const target = targets.find((candidate) => candidate.testFile === result.value.file);
2167
+ return target
2168
+ ? [
2169
+ {
2170
+ file: result.value.file,
2171
+ sourceFile: target.sourceFile,
2172
+ fileSeq: target.fileSeqAtRun === undefined
2173
+ ? {
2174
+ state: "unknown",
2175
+ reason: "sequence-unavailable",
2176
+ }
2177
+ : {
2178
+ state: "known",
2179
+ value: target.fileSeqAtRun,
2180
+ },
2181
+ },
2182
+ ]
2183
+ : [];
2184
+ });
2140
2185
  const deferredTargets = deferred.map((t) => ({
2141
2186
  testFile: t.testFile,
2187
+ sourceFile: t.sourceFile,
2142
2188
  runner: t.runner,
2143
2189
  // One more cut batch for this target. Read back by the
2144
2190
  // selection loop above, which retires it at
@@ -2342,6 +2388,7 @@ export async function handleTurnEnd(deps) {
2342
2388
  content,
2343
2389
  stale,
2344
2390
  results: resultValues,
2391
+ verdicts,
2345
2392
  testRunGeneration,
2346
2393
  launchedFrom,
2347
2394
  publishedAgainst,
@@ -2439,6 +2486,7 @@ export async function handleTurnEnd(deps) {
2439
2486
  content: deferralNote,
2440
2487
  stale,
2441
2488
  results: resultValues,
2489
+ verdicts,
2442
2490
  testRunGeneration,
2443
2491
  launchedFrom,
2444
2492
  publishedAgainst,
@@ -2718,7 +2766,7 @@ export async function handleTurnEnd(deps) {
2718
2766
  // #2521: `cwd` is what resolves the store location the advisory
2719
2767
  // names -- it is the SAME cwd `publishActionableWarningsReport`
2720
2768
  // just wrote through, so the two cannot disagree.
2721
- const advisory = formatActionableWarningsAdvisory(publishResult.report, cwd);
2769
+ const advisory = formatActionableWarningsAdvisory(publishResult.report, cwd, host);
2722
2770
  // @delivery-surface: runtime-turn:actionable-warnings-advisory
2723
2771
  if (advisory)
2724
2772
  advisoryParts.push(advisory);
@@ -2774,7 +2822,7 @@ export async function handleTurnEnd(deps) {
2774
2822
  });
2775
2823
  writeCodeQualityWarningsReport(cacheManager, cwd, qualityReport);
2776
2824
  appendCodeQualityWarningsHistory(cwd, qualityReport);
2777
- const advisory = formatCodeQualityWarningsAdvisory(qualityReport, cwd);
2825
+ const advisory = formatCodeQualityWarningsAdvisory(qualityReport, cwd, host);
2778
2826
  // @delivery-surface: runtime-turn:code-quality-warnings-advisory
2779
2827
  if (advisory)
2780
2828
  advisoryParts.push(advisory);
@@ -2900,6 +2948,13 @@ export async function handleTurnEnd(deps) {
2900
2948
  let lateAuxCeilingExhausted = 0;
2901
2949
  let lateAuxAnswered = 0;
2902
2950
  let lateAuxNotifyStallDemoted = 0;
2951
+ // #3102: dropped by the shared finding-policy stack (inline `pi-lens-ignore`
2952
+ // / stored disposition / `.pi-lens.json` rule policy) and, separately, by the
2953
+ // auxiliary profile's OWN native suppression inside `retagAuxiliaryDiagnostics`.
2954
+ // Both are reported in the one bounded per-turn record below — a drop is
2955
+ // never silent (shape 10).
2956
+ let lateAuxDispositionSuppressed = 0;
2957
+ let lateAuxAuxSuppressed = 0;
2903
2958
  const lateAuxCoverageGapPairs = [];
2904
2959
  let lateAuxCoverageGapDetailCount = 0;
2905
2960
  let lateAuxCoverageGapDropCount = 0;
@@ -2947,6 +3002,32 @@ export async function handleTurnEnd(deps) {
2947
3002
  continue;
2948
3003
  }
2949
3004
  const displayLateAuxPath = toRunnerDisplayPath(cwd, lateAuxPath);
3005
+ // #3102: the file's CURRENT bytes, for the two content-bound halves
3006
+ // of the policy stack (inline `pi-lens-ignore` and the STRICT
3007
+ // `false-positive` anchor) and for the auxiliary profile's own
3008
+ // native suppression. Read at most ONCE per file per drain, lazily:
3009
+ // a turn that drains nothing, finds no live client, re-arms, or
3010
+ // confirms clean pays no I/O at all, and a file with several pending
3011
+ // servers pays one read for all of them. Measured cost on the one
3012
+ // file that does publish: 0.10-0.20 ms typical, 4.0 ms worst case
3013
+ // (a 180 KB file, 5 findings, a populated disposition store) against
3014
+ // the 3000 ms `HOOK_WALL_BUDGET_MS.turn_end`. A read failure yields
3015
+ // `undefined`: the content-free half still applies and nothing is
3016
+ // hidden on an I/O error (shape 48).
3017
+ let lateAuxContentRead = false;
3018
+ let lateAuxContentValue;
3019
+ const readLateAuxContent = () => {
3020
+ if (!lateAuxContentRead) {
3021
+ lateAuxContentRead = true;
3022
+ try {
3023
+ lateAuxContentValue = fs.readFileSync(lateAuxPath, "utf-8");
3024
+ }
3025
+ catch {
3026
+ lateAuxContentValue = undefined;
3027
+ }
3028
+ }
3029
+ return lateAuxContentValue;
3030
+ };
2950
3031
  for (const pair of pairs) {
2951
3032
  const cachedEntry = cached.get(pair.serverId);
2952
3033
  if (cachedEntry === undefined) {
@@ -3034,14 +3115,29 @@ export async function handleTurnEnd(deps) {
3034
3115
  lateAuxCleanConfirmed += 1;
3035
3116
  continue;
3036
3117
  }
3037
- const converted = convertLspDiagnostics(rawDiags, lateAuxPath, {
3038
- tool: "lsp",
3039
- });
3040
- if (converted.length === 0) {
3118
+ // `convertLspDiagnostics` drops entries with no start line, which
3119
+ // would break the 1:1 index alignment `retagAuxiliaryDiagnostics`
3120
+ // needs. Partition first so `converted[i]` IS `anchored[i]` —
3121
+ // the same pre-partition `applyLspFindingPolicy` does.
3122
+ const anchored = rawDiags.filter((d) => d.range?.start?.line !== undefined);
3123
+ if (anchored.length === 0) {
3041
3124
  lateAuxMissing += rawDiags.length;
3042
3125
  lateAuxAnswered += 1;
3043
3126
  continue;
3044
3127
  }
3128
+ const lateAuxContent = readLateAuxContent();
3129
+ const converted = convertLspDiagnostics(anchored, lateAuxPath);
3130
+ // #3046/#3047: the auxiliary's REAL tool id (and its own native
3131
+ // inline suppression — opengrep's `# nosemgrep`, ast-grep's
3132
+ // test-file gate), from the ONE shared derivation every other
3133
+ // surface anchors a mark against. These diagnostics come straight
3134
+ // off the aux client's cache, so nothing upstream applied it.
3135
+ const retained = retagAuxiliaryDiagnostics(converted, anchored, lateAuxContent ?? "", { cwd, fileRole: detectFileRole(lateAuxPath, lateAuxContent) });
3136
+ lateAuxAuxSuppressed += converted.length - retained.length;
3137
+ if (retained.length === 0) {
3138
+ lateAuxAnswered += 1;
3139
+ continue;
3140
+ }
3045
3141
  // Freshness kernel (#1634 gated surface): stat the cited file
3046
3142
  // against the mark timestamp. Missing → drop (no remediation for
3047
3143
  // a deleted file); mtime drifted past the mark → drop too, NOT
@@ -3053,14 +3149,14 @@ export async function handleTurnEnd(deps) {
3053
3149
  // never silent (shape 10).
3054
3150
  const gate = gateFindingsByPathFreshness({
3055
3151
  store: "late-auxiliary-findings",
3056
- findings: converted,
3152
+ findings: retained,
3057
3153
  cwd,
3058
3154
  scannedAt: pair.markedAtMs,
3059
3155
  citedPath: () => lateAuxPath,
3060
3156
  });
3061
3157
  lateAuxStale += gate.stale.length;
3062
3158
  lateAuxMissing +=
3063
- converted.length - gate.live.length - gate.stale.length;
3159
+ retained.length - gate.live.length - gate.stale.length;
3064
3160
  if (gate.live.length === 0) {
3065
3161
  if (gate.stale.length > 0) {
3066
3162
  // Stale findings mean the scan predates the last edit. Re-arm
@@ -3092,11 +3188,43 @@ export async function handleTurnEnd(deps) {
3092
3188
  // merged one). Late findings reach the agent as the gated advisory
3093
3189
  // below; the turn-end hash-guarded fast path stays cold for a file
3094
3190
  // whose touch was partial, which is exactly what #1470 requires.
3095
- const lines = gate.live.map((f) => ` ${displayLateAuxPath}:${f.line}:${f.column} [${f.rule}] ${f.message}`);
3096
- lateAuxDelivered += gate.live.length;
3191
+ // #3102: the survivors are what the agent READS, so they take the
3192
+ // same `clients/dispatch/finding-policy.ts` stack — inline
3193
+ // `pi-lens-ignore` → stored dispositions → `.pi-lens.json` rule
3194
+ // policy — the per-edit dispatcher, `mode=full` and the
3195
+ // `source=lsp` probe lane apply. Without it a finding the agent
3196
+ // marked `false-positive` re-reported on every turn that drained
3197
+ // a late pair. Runs AFTER the freshness gate, like the #1625
3198
+ // govulncheck/secrets filters: the anchor is derived from the
3199
+ // post-gate identity, never the raw pre-gate set.
3200
+ const { kept: lateAuxKept } = applyFindingPolicy(gate.live, {
3201
+ cwd,
3202
+ filePath: lateAuxPath,
3203
+ content: lateAuxContent ?? "",
3204
+ // mtime-cached, so a drain of several files costs one stat.
3205
+ policyMap: loadProjectRulePolicyMap(cwd),
3206
+ identities: renderedRuleIdentities,
3207
+ });
3208
+ const lateAuxSuppressedHere = gate.live.length - lateAuxKept.length;
3209
+ lateAuxDispositionSuppressed += lateAuxSuppressedHere;
3210
+ if (lateAuxKept.length === 0) {
3211
+ // Every late finding was suppressed. This is a PUSH surface:
3212
+ // silence after a mark is the mark working, not a clean
3213
+ // verdict, so the count rides the bounded per-turn record below
3214
+ // instead of re-announcing the suppression every single turn.
3215
+ lateAuxAnswered += 1;
3216
+ continue;
3217
+ }
3218
+ const lines = lateAuxKept.map((f) => ` ${displayLateAuxPath}:${f.line}:${f.column} [${f.rule}] ${f.message}`);
3219
+ lateAuxDelivered += lateAuxKept.length;
3097
3220
  lateAuxAnswered += 1;
3221
+ // #1616 suppressed-bucket rule: a delivery that still has
3222
+ // something to say states what it dropped, once per delivery.
3223
+ const lateAuxSuppressedNote = lateAuxSuppressedHere > 0
3224
+ ? `; suppressed by disposition: ${lateAuxSuppressedHere} finding(s)`
3225
+ : "";
3098
3226
  // @delivery-surface: runtime-turn:late-auxiliary-findings
3099
- advisoryParts.push(`🕐 Late auxiliary diagnostics (${pair.serverId} answered after its grace window):\n${lines.join("\n")}`);
3227
+ advisoryParts.push(`🕐 Late auxiliary diagnostics (${pair.serverId} answered after its grace window${lateAuxSuppressedNote}):\n${lines.join("\n")}`);
3100
3228
  }
3101
3229
  }
3102
3230
  }
@@ -3150,6 +3278,8 @@ export async function handleTurnEnd(deps) {
3150
3278
  expired: lateAuxExpired,
3151
3279
  ceilingExhausted: lateAuxCeilingExhausted,
3152
3280
  answered: lateAuxAnswered,
3281
+ dispositionSuppressed: lateAuxDispositionSuppressed,
3282
+ auxSuppressed: lateAuxAuxSuppressed,
3153
3283
  notifyStallDemoted: lateAuxNotifyStallDemoted,
3154
3284
  coverageGapReRaised: lateAuxCoverageGapPairs.length,
3155
3285
  coverageGapReRaisedDetailed: lateAuxCoverageGapDetailCount,
@@ -3202,7 +3332,7 @@ export async function handleTurnEnd(deps) {
3202
3332
  });
3203
3333
  }
3204
3334
  }
3205
- cacheManager.clearTurnState(cwd, currentOwner);
3335
+ clearOwnedTurnState();
3206
3336
  runtime.fixedThisTurn.clear();
3207
3337
  resetFormatService();
3208
3338
  return;
@@ -3289,7 +3419,7 @@ export async function handleTurnEnd(deps) {
3289
3419
  });
3290
3420
  }
3291
3421
  if (blockerParts.length === 0) {
3292
- cacheManager.clearTurnState(cwd, currentOwner);
3422
+ clearOwnedTurnState();
3293
3423
  // `staleSecretParts` counts here too (#1622 review M2): clearing the
3294
3424
  // findings record while a stale secret is still unverified would drop the
3295
3425
  // only surviving trace of it.
@@ -16,9 +16,9 @@
16
16
  import { spawn, spawnSync, } from "node:child_process";
17
17
  import * as fs from "node:fs";
18
18
  import * as path from "node:path";
19
- import { BoundedFifoMap } from "./bounded-cache.js";
19
+ import { BoundedFifoMap, BoundedSet } from "./bounded-cache.js";
20
20
  import { logLatency } from "./latency-logger.js";
21
- import { recordDegradation } from "./degradation-ledger.js";
21
+ import { incrementDegradationCount, recordDegradation, recordDegradationOnce, } from "./degradation-ledger.js";
22
22
  import { logExtension } from "./extension-log.js";
23
23
  import { isFullyQualifiedWin32 } from "./path-utils.js";
24
24
  import { startSpawnUsageSampler } from "./resource-sampler.js";
@@ -212,6 +212,228 @@ const lifetimeState = processWithLifetimeState[lifetimeStateKey] ??
212
212
  pids: new Set(),
213
213
  installed: false,
214
214
  });
215
+ /**
216
+ * THE ownership predicate for every kill-by-raw-pid in this repo (#2042).
217
+ *
218
+ * A pid is signalable only when it is a LIVE DIRECT CHILD of this process.
219
+ * `tests/clients/lsp/launch.test.ts` mocked `node:child_process` with a fake
220
+ * child carrying the literal pid `2468`; `safeSpawnAsync` registered it in
221
+ * `lifetimeState.pids`, the fake never emitted `close` so nothing removed it,
222
+ * and at fork teardown `installLifetimeCleanup()` fired
223
+ * `process.kill(-2468, "SIGKILL")`. On ~10 % of GitHub runners pid 2468 was
224
+ * one of the job's own long-lived processes — that is the whole of #2042:
225
+ * exit 137, no failing assertion, no kernel record, five weeks of "infra
226
+ * kill" reruns. A `pid <= 0` guard would not have stopped it; 2468 is a
227
+ * perfectly plausible pid. The defect is OWNERSHIP, not sign.
228
+ *
229
+ * Three guards fold into this one (net-count rule): `lsp/client.ts`'s
230
+ * `pid <= 0`, `lsp/launch.ts`'s `handle.pid <= 0` plus its already-exited
231
+ * recycled-pid check, and `killTree`'s `child.pid > 0` below. It closes
232
+ * fabricated, stale and recycled pids together.
233
+ *
234
+ * NOT folded: `instance-reaper.ts#killPidTree`. Its pids come from OTHER
235
+ * pi-lens instances' registries and from an OS scan of managed binaries with
236
+ * confirmed-dead parents — deliberately NOT our children. Its `!isFinite ||
237
+ * pid <= 0` guard is well-formedness on a different axis, and applying this
238
+ * predicate there would turn the orphan reaper into a no-op.
239
+ *
240
+ * Platform behaviour, stated rather than implied:
241
+ * - Linux (the authoritative lane, and the one that lost five weeks):
242
+ * `/proc/<pid>/status` is the kernel's own answer. Missing entry = the pid
243
+ * does not exist, so there is nothing of ours to kill; `PPid` other than
244
+ * ours = someone else's process, which is recorded and never signalled.
245
+ * - Windows and macOS have no `/proc` (and neither does a Linux host with
246
+ * `/proc` unmounted — probed once, not assumed from `process.platform`),
247
+ * so ownership is unverifiable from a raw pid and the predicate keeps
248
+ * today's best-effort behaviour. The `handle` arm applies THERE and only
249
+ * there: it is the only ownership evidence Windows has, and it is what
250
+ * `killWindowsTree` already used. It must not gate the POSIX group kill,
251
+ * which legitimately runs after its leader has died (#3091 F1).
252
+ */
253
+ /**
254
+ * Whether `/proc/<pid>/status` can actually be read here, probed ONCE against
255
+ * this process's own entry (#3091 F4). `process.platform === "linux"` is not
256
+ * the same question: a container or a hardened host can run Linux with no
257
+ * `/proc` mounted, and there the per-pid read fails exactly the way a dead pid
258
+ * does. Without this probe that silently refused every registration, so
259
+ * `lifetimeState.pids` stayed empty and the host-exit tree kill stopped working
260
+ * with no record anywhere.
261
+ */
262
+ const PROC_PPID_READABLE = (() => {
263
+ if (process.platform !== "linux")
264
+ return false;
265
+ try {
266
+ return /^PPid:\s*\d+$/m.test(fs.readFileSync("/proc/self/status", "utf8"));
267
+ }
268
+ catch {
269
+ return false;
270
+ }
271
+ })();
272
+ /**
273
+ * Pids this process has PROVEN, from the kernel, to be its own live children.
274
+ * FIFO-bounded (`BoundedFifoMap`, not a wholesale clear): a process that spawns
275
+ * more than the cap drops its OLDEST verdicts, where a wholesale clear would
276
+ * drop the one it is about to need.
277
+ */
278
+ export const VERIFIED_OWN_PID_CAP = 512;
279
+ const verifiedOwnPids = new BoundedFifoMap(VERIFIED_OWN_PID_CAP);
280
+ /**
281
+ * Pids whose OWNERSHIP IS HELD for the lifetime of a long-lived resource —
282
+ * today, the LSP children `clients/lsp/launch.ts` spawns with `nodeSpawn`
283
+ * (#3091 F1-r2b).
284
+ *
285
+ * These deliberately do NOT live in `verifiedOwnPids`. Age is the wrong
286
+ * retirement axis for a resource-scoped verdict: an LSP leader is verified once
287
+ * at spawn and then sits untouched for the whole session, so in a FIFO that
288
+ * every `safeSpawnAsync` writes to it is the OLDEST entry and therefore the
289
+ * FIRST evicted — measured, 520 later verdicts were enough — and its group kill
290
+ * at host exit is refused precisely because the server was long-lived enough to
291
+ * matter. Retirement here is by RESOURCE STATE instead: `releaseOwnChildPid`
292
+ * when the shutdown ladder is done with the pid, plus the confirmed-dead sweep
293
+ * below for the paths that never reach a ladder (a server that crashes, or a
294
+ * host killed mid-session).
295
+ *
296
+ * The bound that remains, stated rather than implied: at most
297
+ * `HELD_OWN_PID_SWEEP_AT` entries — the confirmed-dead sweep below runs before
298
+ * every insert, and `BoundedSet`'s own FIFO overflow is the last resort for the
299
+ * case where every held group really is still alive. A crash-looping server cannot grow it without bound
300
+ * because each sweep drops every pid whose process GROUP no longer exists,
301
+ * which is exactly the condition under which the entry can never be needed
302
+ * again.
303
+ */
304
+ const HELD_OWN_PID_SWEEP_AT = 256;
305
+ const heldOwnPids = new BoundedSet(HELD_OWN_PID_SWEEP_AT);
306
+ /** A process group that no longer exists can never need another signal. */
307
+ function groupIsGone(pid) {
308
+ try {
309
+ process.kill(-pid, 0);
310
+ return false;
311
+ }
312
+ catch (error) {
313
+ return errorCode(error) === "ESRCH";
314
+ }
315
+ }
316
+ function sweepHeldOwnPids() {
317
+ if (heldOwnPids.size < HELD_OWN_PID_SWEEP_AT)
318
+ return;
319
+ // Deleting the current entry mid-iteration is well-defined for a Set, so
320
+ // no snapshot copy is needed.
321
+ for (const pid of heldOwnPids) {
322
+ if (groupIsGone(pid))
323
+ heldOwnPids.delete(pid);
324
+ }
325
+ // No hand-rolled "drop the oldest" block (#2442 / #3091 round 4): the
326
+ // last-resort eviction, for the case where every held group is still alive
327
+ // at the ceiling, is `BoundedSet.add`'s own FIFO overflow.
328
+ }
329
+ /**
330
+ * Verify a pid at ADMISSION and hold the verdict for the resource's lifetime.
331
+ *
332
+ * This is AGENTS.md shape 50's own rule, which #3091 round 1 broke at the site
333
+ * it was fixing: `isOwnLiveChild`'s three call sites were the `safeSpawnAsync`
334
+ * registration, the Windows tree kill, and the SHUTDOWN group kill — so for an
335
+ * LSP child, spawned with `nodeSpawn`, the first offer of verification was also
336
+ * the last, and on the `processExiting` path the leader could already be dead
337
+ * by then (`/proc` gone, nothing on record, signal refused, grandchildren
338
+ * orphaned).
339
+ */
340
+ export function holdOwnChildPid(pid, site) {
341
+ const owned = isOwnLiveChild(pid, site);
342
+ if (!owned || !PROC_PPID_READABLE || typeof pid !== "number")
343
+ return owned;
344
+ // Exactly one home: the verification above filed the verdict in the
345
+ // age-bounded FIFO, and leaving a copy there would make BOTH the held set
346
+ // and its release mutation-inert — the FIFO copy would answer for them.
347
+ verifiedOwnPids.delete(pid);
348
+ sweepHeldOwnPids();
349
+ heldOwnPids.add(pid);
350
+ return true;
351
+ }
352
+ /**
353
+ * Retire a held pid once nothing will signal it again — the end of the
354
+ * shutdown ladder. Idempotent.
355
+ */
356
+ export function releaseOwnChildPid(pid) {
357
+ if (typeof pid === "number")
358
+ heldOwnPids.delete(pid);
359
+ }
360
+ /** Once per session: a Linux host whose `/proc` cannot answer the question. */
361
+ function recordProcUnavailable(site) {
362
+ if (process.platform !== "linux")
363
+ return;
364
+ recordDegradationOnce({
365
+ kind: "kill-ownership-unverifiable",
366
+ subject: site,
367
+ reason: "/proc/self/status is unreadable on this Linux host; kill-by-pid ownership falls back to best-effort",
368
+ });
369
+ }
370
+ export function isOwnLiveChild(pid, site, handle) {
371
+ if (typeof pid !== "number" || !Number.isInteger(pid) || pid <= 0)
372
+ return false;
373
+ if (!PROC_PPID_READABLE) {
374
+ // No `/proc` to consult. A handle that has already reported exit is then
375
+ // the ONLY ownership evidence available, and it is exactly what
376
+ // `killWindowsTree` used before this predicate existed: a dead pid may
377
+ // have been recycled, and `taskkill /F /T` on a recycled pid destroys an
378
+ // unrelated tree. It overrides the memo below deliberately.
379
+ if (handle && (handle.exitCode != null || handle.signalCode != null))
380
+ return false;
381
+ recordProcUnavailable(site);
382
+ return true;
383
+ }
384
+ let status;
385
+ try {
386
+ status = fs.readFileSync(`/proc/${pid}/status`, "utf8");
387
+ }
388
+ catch {
389
+ // The pid does not exist. Not a degradation — a child that exited before
390
+ // the teardown signal is the ordinary case — but not automatically a
391
+ // refusal either: a POSIX process GROUP outlives its leader, and #2026's
392
+ // whole point is that the escalation must still reach a SIGTERM-hardy
393
+ // grandchild after the direct child has died. Ownership verified while
394
+ // the leader was alive is what answers that; a pid this process never
395
+ // owned is still refused.
396
+ return heldOwnPids.has(pid) || verifiedOwnPids.has(pid);
397
+ }
398
+ const parent = /^PPid:\s*(\d+)$/m.exec(status);
399
+ if (!parent)
400
+ return true;
401
+ if (Number(parent[1]) === process.pid) {
402
+ // Memoized only on a KERNEL-verified verdict, and only here: a pid we
403
+ // never proved was ours can never enter the memo, and a pid that is
404
+ // alive under a different parent is refused above regardless of what the
405
+ // memo holds — so a recycled pid cannot be signalled on the strength of
406
+ // its previous owner.
407
+ //
408
+ // Never for a pid already HELD, though: the shutdown ladder verifies a
409
+ // still-live leader on its way to the group signal, and filing that
410
+ // verdict in the FIFO too would put a held pid back in both stores —
411
+ // which outlives `releaseOwnChildPid` (the FIFO copy would keep
412
+ // answering for a pid this process has explicitly stopped claiming) and
413
+ // makes the held store's own tests unable to fail.
414
+ if (!heldOwnPids.has(pid))
415
+ verifiedOwnPids.set(pid, true);
416
+ return true;
417
+ }
418
+ // Alive AND someone else's: the dangerous case, and the only one worth a
419
+ // record. Subject is the SITE (a fixed, tiny set), never the pid, so the
420
+ // ledger stays bounded however often it fires.
421
+ incrementDegradationCount({
422
+ kind: "kill-foreign-pid-refused",
423
+ subject: site,
424
+ reason: `pid ${pid} has parent ${parent[1]}, not this process — signal refused`,
425
+ });
426
+ return false;
427
+ }
428
+ /**
429
+ * Kill one registered pid's tree at host exit. Deliberately NOT re-checked
430
+ * against `isOwnLiveChild` here: `lifetimeState.pids` has exactly one writer
431
+ * (the registration below), that writer is now gated, and a second gate on
432
+ * the same feeder would make BOTH mutation-inert — remove either one and the
433
+ * other still blocks the kill, so neither would have a test that reds. The
434
+ * gate lives where the pid enters, which is also where ownership is a fact
435
+ * rather than a stale reading (#2042).
436
+ */
215
437
  function killPidTreeSync(pid) {
216
438
  if (process.platform === "win32") {
217
439
  try {
@@ -836,6 +1058,17 @@ export function resetUtf8ConsoleCodePageStateForTests() {
836
1058
  // caller's overall budget unboundedly.
837
1059
  const EXIT_PIPE_IDLE_GRACE_MS = 100;
838
1060
  const EXIT_PIPE_IDLE_MAX_WAIT_MS = 2000;
1061
+ /**
1062
+ * #2968: how long past its own deadline this spawn's CPU/RSS sampler may keep
1063
+ * polling. Everything this function does to end a child after the deadline
1064
+ * fits inside it: `killTree`'s SIGTERM plus its 1000ms SIGKILL escalation (or
1065
+ * Windows' `taskkill /F /T`), then at most EXIT_PIPE_IDLE_MAX_WAIT_MS of
1066
+ * pipe-idle wait. A child still alive past deadline + this grace has outlived
1067
+ * every teardown available here, so polling it further only buys the Windows
1068
+ * process pile-up the report measured — and `stop()` still returns whatever
1069
+ * the sampler gathered before the cap.
1070
+ */
1071
+ const SPAWN_SAMPLER_TEARDOWN_GRACE_MS = 5_000;
839
1072
  // ============================================================================
840
1073
  // ASYNC VERSION (Recommended - Non-blocking)
841
1074
  // ============================================================================
@@ -1225,7 +1458,14 @@ export async function safeSpawnAsync(command, args, options) {
1225
1458
  child.stdin?.on("error", () => { });
1226
1459
  child.stdin?.end(options.input);
1227
1460
  }
1228
- if (child.pid && (posixProcessGroup || options?.lifetimeCoupled)) {
1461
+ // #2042: ownership is a SPAWN-TIME fact — a real child is alive with
1462
+ // `PPid == ours` the instant `spawn` returns, while a test double's
1463
+ // invented pid never is. Resolve it once, here, and reuse the verdict:
1464
+ // the group escalation below must still fire after the direct child
1465
+ // dies (#2027: a SIGTERM-hardy grandchild keeps the group alive), so it
1466
+ // cannot re-read `/proc` at kill time.
1467
+ const ownsChildPid = isOwnLiveChild(child.pid, "safe-spawn-register");
1468
+ if (ownsChildPid && (posixProcessGroup || options?.lifetimeCoupled)) {
1229
1469
  // #2026: POSIX registers unconditionally - detached children no
1230
1470
  // longer receive terminal signals directly, so the lifetime
1231
1471
  // cleanup's signal forwarding IS their die-with-host path.
@@ -1240,9 +1480,22 @@ export async function safeSpawnAsync(command, args, options) {
1240
1480
  // best-effort/never-throws by design, but this call site wraps it anyway
1241
1481
  // (belt and suspenders: the sampling seam must never be the reason a real
1242
1482
  // spawn fails) with a no-op fallback sampler.
1483
+ //
1484
+ // #2968: the sampler's polling lifetime is bounded by THIS spawn's own
1485
+ // deadline plus the teardown grace, not by the child's willingness to
1486
+ // exit. `finalize`/the `error` handler still stop it on every settle
1487
+ // path, but none of those paths exist for a child that never settles —
1488
+ // a hung `.cmd` shim whose tree survives `taskkill /F /T` kept polling
1489
+ // for 5-6 hours in the report.
1243
1490
  let usageSampler;
1244
1491
  try {
1245
- usageSampler = startSpawnUsageSampler(child.pid);
1492
+ usageSampler = startSpawnUsageSampler(
1493
+ // The poll CADENCE is the sampler's own policy (it owns the backoff
1494
+ // too); the DEADLINE is this function's. Passing `undefined` rather
1495
+ // than importing the cadence constant keeps this module's view of
1496
+ // the sampler at one export, which is also what every existing
1497
+ // `vi.mock` of it provides.
1498
+ child.pid, undefined, timeout + SPAWN_SAMPLER_TEARDOWN_GRACE_MS);
1246
1499
  }
1247
1500
  catch {
1248
1501
  usageSampler = { stop: () => null };
@@ -1272,7 +1525,7 @@ export async function safeSpawnAsync(command, args, options) {
1272
1525
  child.kill("SIGKILL");
1273
1526
  }
1274
1527
  }
1275
- else if (posixProcessGroup && child.pid && child.pid > 0) {
1528
+ else if (posixProcessGroup && ownsChildPid) {
1276
1529
  // #2026: signal the whole process group. Grandchildren spawned
1277
1530
  // by the tool share its group, so one signal reaches the whole
1278
1531
  // tree; a negative-pid ESRCH means it already exited.
@@ -91,6 +91,12 @@ export function getScratchTreeFnmatchPatterns() {
91
91
  export const SECRETS_LANE_SCRATCH_DIR_NAMES = [
92
92
  // pi-ecosystem / coding-agent data + cache directories.
93
93
  ".pi",
94
+ // The same agent's rebranded config dir (#3112) — pi's config-dir name is
95
+ // `pkg.piConfig?.configDir || ".pi"`, so `.omp/agent/sessions` is the very
96
+ // history `.pi/agent/sessions` holds. Without it the secrets lane READS a
97
+ // project-local session history and can report a key a user pasted into a
98
+ // transcript as a blocking first-party leak.
99
+ ".omp",
94
100
  ".pi-lens",
95
101
  ".claude",
96
102
  ".codex",