@herbertgao/pi-extensions 2026.8.3 → 2026.8.5

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 (408) hide show
  1. package/README.md +45 -4
  2. package/examples/pi-footer.json +275 -0
  3. package/node_modules/@herbertgao/pi-cc-extensions/README.en.md +10 -2
  4. package/node_modules/@herbertgao/pi-cc-extensions/README.md +10 -2
  5. package/node_modules/@herbertgao/pi-cc-extensions/extensions/config/config.ts +24 -0
  6. package/node_modules/@herbertgao/pi-cc-extensions/extensions/config/panel.ts +91 -1
  7. package/node_modules/@herbertgao/pi-cc-extensions/extensions/feature/compact-thinking.ts +7 -3
  8. package/node_modules/@herbertgao/pi-cc-extensions/extensions/index.ts +7 -6
  9. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/compact-mode.ts +21 -12
  10. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/default-mode.ts +4 -5
  11. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/markdown-enhance.ts +5 -8
  12. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/grouping.ts +22 -12
  13. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/result.ts +2 -4
  14. package/node_modules/@herbertgao/pi-cc-extensions/package.json +3 -3
  15. package/node_modules/@herbertgao/pi-subagents/CHANGELOG.md +8 -0
  16. package/node_modules/@herbertgao/pi-subagents/package.json +8 -4
  17. package/node_modules/@herbertgao/pi-subagents/src/agent-color.ts +72 -67
  18. package/node_modules/@herbertgao/pi-subagents/src/agent-file-toggle.ts +255 -0
  19. package/node_modules/@herbertgao/pi-subagents/src/agent-manager.ts +191 -3
  20. package/node_modules/@herbertgao/pi-subagents/src/agent-runner.ts +15 -2
  21. package/node_modules/@herbertgao/pi-subagents/src/index.ts +289 -113
  22. package/node_modules/@herbertgao/pi-subagents/src/output-file.ts +24 -1
  23. package/node_modules/@herbertgao/pi-subagents/src/ui/agent-widget.ts +24 -2
  24. package/node_modules/@herbertgao/pi-subagents/src/ui/schedule-menu.ts +9 -8
  25. package/node_modules/@herbertgao/pi-subagents/src/ui/select-item.ts +48 -0
  26. package/node_modules/@juicesharp/rpiv-ask-user-question/docs/keyboard.md +7 -0
  27. package/node_modules/@juicesharp/rpiv-ask-user-question/package.json +2 -2
  28. package/node_modules/@juicesharp/rpiv-ask-user-question/state/build-questionnaire.ts +7 -0
  29. package/node_modules/@juicesharp/rpiv-ask-user-question/state/key-router.ts +16 -5
  30. package/node_modules/@juicesharp/rpiv-ask-user-question/state/questionnaire-session.ts +4 -2
  31. package/node_modules/@narumitw/pi-btw/README.md +8 -7
  32. package/node_modules/@narumitw/pi-btw/package.json +2 -2
  33. package/node_modules/@narumitw/pi-btw/src/menu.ts +1 -0
  34. package/node_modules/pi-footer/CHANGELOG.md +145 -0
  35. package/node_modules/pi-footer/LICENSE +21 -0
  36. package/node_modules/pi-footer/README.md +354 -0
  37. package/node_modules/pi-footer/package.json +68 -0
  38. package/node_modules/pi-footer/src/cache.ts +106 -0
  39. package/node_modules/pi-footer/src/colors.ts +237 -0
  40. package/node_modules/pi-footer/src/config.ts +202 -0
  41. package/node_modules/pi-footer/src/event-widgets.ts +41 -0
  42. package/node_modules/pi-footer/src/extension-statuses.ts +102 -0
  43. package/node_modules/pi-footer/src/git.ts +135 -0
  44. package/node_modules/pi-footer/src/index.ts +268 -0
  45. package/node_modules/pi-footer/src/metrics.ts +141 -0
  46. package/node_modules/pi-footer/src/presets.ts +383 -0
  47. package/node_modules/pi-footer/src/render.ts +114 -0
  48. package/node_modules/pi-footer/src/separators.ts +52 -0
  49. package/node_modules/pi-footer/src/types.ts +122 -0
  50. package/node_modules/pi-footer/src/ui/color-level-confirm.ts +22 -0
  51. package/node_modules/pi-footer/src/ui/color-options.ts +195 -0
  52. package/node_modules/pi-footer/src/ui/config-lifecycle.ts +61 -0
  53. package/node_modules/pi-footer/src/ui/edit-colors.ts +13 -0
  54. package/node_modules/pi-footer/src/ui/events.ts +35 -0
  55. package/node_modules/pi-footer/src/ui/extension-status-picker.ts +60 -0
  56. package/node_modules/pi-footer/src/ui/extension-statuses.ts +73 -0
  57. package/node_modules/pi-footer/src/ui/fields.ts +252 -0
  58. package/node_modules/pi-footer/src/ui/global-menu.ts +126 -0
  59. package/node_modules/pi-footer/src/ui/helpers.ts +44 -0
  60. package/node_modules/pi-footer/src/ui/layout.ts +17 -0
  61. package/node_modules/pi-footer/src/ui/line-list.ts +19 -0
  62. package/node_modules/pi-footer/src/ui/model.ts +64 -0
  63. package/node_modules/pi-footer/src/ui/navigation.ts +29 -0
  64. package/node_modules/pi-footer/src/ui/option-edit.ts +93 -0
  65. package/node_modules/pi-footer/src/ui/overlay-render.ts +103 -0
  66. package/node_modules/pi-footer/src/ui/screen-context.ts +18 -0
  67. package/node_modules/pi-footer/src/ui/screen-controller.ts +27 -0
  68. package/node_modules/pi-footer/src/ui/screen-render.ts +32 -0
  69. package/node_modules/pi-footer/src/ui/screen-state.ts +20 -0
  70. package/node_modules/pi-footer/src/ui/screen.ts +243 -0
  71. package/node_modules/pi-footer/src/ui/screens/add-widget.ts +92 -0
  72. package/node_modules/pi-footer/src/ui/screens/confirm-exit.ts +52 -0
  73. package/node_modules/pi-footer/src/ui/screens/controller.ts +12 -0
  74. package/node_modules/pi-footer/src/ui/screens/edit-colors.ts +68 -0
  75. package/node_modules/pi-footer/src/ui/screens/edit-widget.ts +115 -0
  76. package/node_modules/pi-footer/src/ui/screens/extension-status-row.ts +61 -0
  77. package/node_modules/pi-footer/src/ui/screens/global.ts +75 -0
  78. package/node_modules/pi-footer/src/ui/screens/line-list.ts +108 -0
  79. package/node_modules/pi-footer/src/ui/screens/main.ts +99 -0
  80. package/node_modules/pi-footer/src/ui/screens/terminal.ts +121 -0
  81. package/node_modules/pi-footer/src/ui/screens/widget-list.ts +154 -0
  82. package/node_modules/pi-footer/src/ui/terminal-menu.ts +32 -0
  83. package/node_modules/pi-footer/src/ui/theme.ts +48 -0
  84. package/node_modules/pi-footer/src/ui/title-bar.ts +100 -0
  85. package/node_modules/pi-footer/src/ui/widget-actions.ts +76 -0
  86. package/node_modules/pi-footer/src/ui.ts +66 -0
  87. package/node_modules/pi-footer/src/widgets/context.ts +28 -0
  88. package/node_modules/pi-footer/src/widgets/core/active-tools.ts +17 -0
  89. package/node_modules/pi-footer/src/widgets/core/context-window.ts +24 -0
  90. package/node_modules/pi-footer/src/widgets/core/cwd-basename.ts +19 -0
  91. package/node_modules/pi-footer/src/widgets/core/cwd.ts +108 -0
  92. package/node_modules/pi-footer/src/widgets/core/event.ts +34 -0
  93. package/node_modules/pi-footer/src/widgets/core/external-status.ts +102 -0
  94. package/node_modules/pi-footer/src/widgets/core/model-provider.ts +20 -0
  95. package/node_modules/pi-footer/src/widgets/core/model.ts +35 -0
  96. package/node_modules/pi-footer/src/widgets/core/provider.ts +17 -0
  97. package/node_modules/pi-footer/src/widgets/core/session-name.ts +17 -0
  98. package/node_modules/pi-footer/src/widgets/core/text-verbosity.ts +17 -0
  99. package/node_modules/pi-footer/src/widgets/core/thinking-level.ts +17 -0
  100. package/node_modules/pi-footer/src/widgets/git/ahead-behind.ts +17 -0
  101. package/node_modules/pi-footer/src/widgets/git/branch.ts +81 -0
  102. package/node_modules/pi-footer/src/widgets/git/clean.ts +19 -0
  103. package/node_modules/pi-footer/src/widgets/git/deletions.ts +17 -0
  104. package/node_modules/pi-footer/src/widgets/git/diff.ts +38 -0
  105. package/node_modules/pi-footer/src/widgets/git/insertions.ts +17 -0
  106. package/node_modules/pi-footer/src/widgets/git/remote.ts +17 -0
  107. package/node_modules/pi-footer/src/widgets/git/root.ts +17 -0
  108. package/node_modules/pi-footer/src/widgets/git/sha.ts +17 -0
  109. package/node_modules/pi-footer/src/widgets/git/staged.ts +17 -0
  110. package/node_modules/pi-footer/src/widgets/git/status.ts +19 -0
  111. package/node_modules/pi-footer/src/widgets/git/unstaged.ts +17 -0
  112. package/node_modules/pi-footer/src/widgets/git/untracked.ts +17 -0
  113. package/node_modules/pi-footer/src/widgets/instance.ts +118 -0
  114. package/node_modules/pi-footer/src/widgets/layout/custom-text.ts +18 -0
  115. package/node_modules/pi-footer/src/widgets/layout/flex-separator.ts +32 -0
  116. package/node_modules/pi-footer/src/widgets/layout/separator.ts +57 -0
  117. package/node_modules/pi-footer/src/widgets/layout/spacer.ts +31 -0
  118. package/node_modules/pi-footer/src/widgets/options.ts +122 -0
  119. package/node_modules/pi-footer/src/widgets/project/runtime.ts +361 -0
  120. package/node_modules/pi-footer/src/widgets/registry.ts +223 -0
  121. package/node_modules/pi-footer/src/widgets/session/assistant-messages.ts +17 -0
  122. package/node_modules/pi-footer/src/widgets/session/compactions.ts +17 -0
  123. package/node_modules/pi-footer/src/widgets/session/elapsed.ts +18 -0
  124. package/node_modules/pi-footer/src/widgets/session/last-activity.ts +18 -0
  125. package/node_modules/pi-footer/src/widgets/session/messages.ts +19 -0
  126. package/node_modules/pi-footer/src/widgets/session/session-id.ts +17 -0
  127. package/node_modules/pi-footer/src/widgets/session/session-start.ts +18 -0
  128. package/node_modules/pi-footer/src/widgets/session/tool-results.ts +17 -0
  129. package/node_modules/pi-footer/src/widgets/session/total-messages.ts +19 -0
  130. package/node_modules/pi-footer/src/widgets/session/total-time.ts +18 -0
  131. package/node_modules/pi-footer/src/widgets/session/user-messages.ts +17 -0
  132. package/node_modules/pi-footer/src/widgets/store.ts +26 -0
  133. package/node_modules/pi-footer/src/widgets/tokens/cache-hit-rate.ts +55 -0
  134. package/node_modules/pi-footer/src/widgets/tokens/cache-read.ts +18 -0
  135. package/node_modules/pi-footer/src/widgets/tokens/cache-write.ts +18 -0
  136. package/node_modules/pi-footer/src/widgets/tokens/context-bar.ts +108 -0
  137. package/node_modules/pi-footer/src/widgets/tokens/context-length.ts +26 -0
  138. package/node_modules/pi-footer/src/widgets/tokens/context-remaining.ts +29 -0
  139. package/node_modules/pi-footer/src/widgets/tokens/context.ts +29 -0
  140. package/node_modules/pi-footer/src/widgets/tokens/cost.ts +51 -0
  141. package/node_modules/pi-footer/src/widgets/tokens/input-speed.ts +25 -0
  142. package/node_modules/pi-footer/src/widgets/tokens/input-tokens.ts +18 -0
  143. package/node_modules/pi-footer/src/widgets/tokens/output-speed.ts +25 -0
  144. package/node_modules/pi-footer/src/widgets/tokens/output-tokens.ts +18 -0
  145. package/node_modules/pi-footer/src/widgets/tokens/tokens.ts +20 -0
  146. package/node_modules/pi-footer/src/widgets/tokens/total-speed.ts +25 -0
  147. package/node_modules/pi-footer/src/widgets/tokens/total-tokens.ts +18 -0
  148. package/node_modules/pi-footer/src/widgets/types.ts +181 -0
  149. package/node_modules/pi-footer/src/widgets/utils/colors.ts +11 -0
  150. package/node_modules/pi-footer/src/widgets/utils/context.ts +112 -0
  151. package/node_modules/pi-footer/src/widgets/utils/session.ts +19 -0
  152. package/node_modules/pi-footer/src/widgets/utils/token-format.ts +78 -0
  153. package/node_modules/pi-lens/CHANGELOG.md +2440 -954
  154. package/node_modules/pi-lens/README.md +13 -0
  155. package/node_modules/pi-lens/dist/clients/actionable-warnings.js +73 -18
  156. package/node_modules/pi-lens/dist/clients/agent-behavior-client.js +21 -4
  157. package/node_modules/pi-lens/dist/clients/ast-grep-client.js +68 -16
  158. package/node_modules/pi-lens/dist/clients/ast-grep-types.js +0 -1
  159. package/node_modules/pi-lens/dist/clients/ast-grep-yaml-synth.js +37 -10
  160. package/node_modules/pi-lens/dist/clients/atomic-write-staging.js +63 -0
  161. package/node_modules/pi-lens/dist/clients/atomic-write.js +114 -14
  162. package/node_modules/pi-lens/dist/clients/bash-file-access.js +193 -23
  163. package/node_modules/pi-lens/dist/clients/biome-client.js +29 -25
  164. package/node_modules/pi-lens/dist/clients/bootstrap.js +11 -2
  165. package/node_modules/pi-lens/dist/clients/bounded-cache.js +34 -0
  166. package/node_modules/pi-lens/dist/clients/bounded-pid-file-lock.js +192 -0
  167. package/node_modules/pi-lens/dist/clients/bus-publish.js +10 -3
  168. package/node_modules/pi-lens/dist/clients/cache/rule-cache.js +47 -5
  169. package/node_modules/pi-lens/dist/clients/cache-manager.js +97 -9
  170. package/node_modules/pi-lens/dist/clients/cache-observability.js +301 -14
  171. package/node_modules/pi-lens/dist/clients/call-graph.js +449 -117
  172. package/node_modules/pi-lens/dist/clients/child-unref.js +85 -0
  173. package/node_modules/pi-lens/dist/clients/codebase-model.js +52 -23
  174. package/node_modules/pi-lens/dist/clients/collateral-test-role.js +40 -0
  175. package/node_modules/pi-lens/dist/clients/complexity-client.js +2 -1
  176. package/node_modules/pi-lens/dist/clients/console-guard-install.js +14 -0
  177. package/node_modules/pi-lens/dist/clients/cooperative-budget.js +42 -0
  178. package/node_modules/pi-lens/dist/clients/dead-code-client.js +2 -1
  179. package/node_modules/pi-lens/dist/clients/debug-handles.js +206 -0
  180. package/node_modules/pi-lens/dist/clients/debug-heap.js +167 -0
  181. package/node_modules/pi-lens/dist/clients/degradation-ledger.js +153 -0
  182. package/node_modules/pi-lens/dist/clients/dependency-checker.js +347 -75
  183. package/node_modules/pi-lens/dist/clients/diagnostic-dispositions.js +61 -11
  184. package/node_modules/pi-lens/dist/clients/diagnostics-publish.js +10 -3
  185. package/node_modules/pi-lens/dist/clients/dispatch/dispatcher.js +63 -20
  186. package/node_modules/pi-lens/dist/clients/dispatch/facts/function-facts.js +23 -7
  187. package/node_modules/pi-lens/dist/clients/dispatch/facts/import-facts.js +8 -18
  188. package/node_modules/pi-lens/dist/clients/dispatch/facts/tree-sitter-facts.js +8 -5
  189. package/node_modules/pi-lens/dist/clients/dispatch/indent-detect.js +35 -0
  190. package/node_modules/pi-lens/dist/clients/dispatch/inline-suppressions.js +17 -8
  191. package/node_modules/pi-lens/dist/clients/dispatch/integration.js +1085 -681
  192. package/node_modules/pi-lens/dist/clients/dispatch/lazy.js +14 -0
  193. package/node_modules/pi-lens/dist/clients/dispatch/plan.js +11 -1
  194. package/node_modules/pi-lens/dist/clients/dispatch/rule-id-normalize.js +50 -0
  195. package/node_modules/pi-lens/dist/clients/dispatch/rule-policy.js +135 -0
  196. package/node_modules/pi-lens/dist/clients/dispatch/runners/ast-grep-napi.js +23 -5
  197. package/node_modules/pi-lens/dist/clients/dispatch/runners/biome-check.js +5 -9
  198. package/node_modules/pi-lens/dist/clients/dispatch/runners/detekt.js +3 -2
  199. package/node_modules/pi-lens/dist/clients/dispatch/runners/hadolint.js +2 -1
  200. package/node_modules/pi-lens/dist/clients/dispatch/runners/helm-lint.js +154 -0
  201. package/node_modules/pi-lens/dist/clients/dispatch/runners/htmlhint.js +3 -2
  202. package/node_modules/pi-lens/dist/clients/dispatch/runners/index.js +5 -1
  203. package/node_modules/pi-lens/dist/clients/dispatch/runners/lsp.js +14 -6
  204. package/node_modules/pi-lens/dist/clients/dispatch/runners/markdownlint.js +4 -9
  205. package/node_modules/pi-lens/dist/clients/dispatch/runners/pyright.js +9 -5
  206. package/node_modules/pi-lens/dist/clients/dispatch/runners/ruff.js +4 -8
  207. package/node_modules/pi-lens/dist/clients/dispatch/runners/shellcheck.js +2 -3
  208. package/node_modules/pi-lens/dist/clients/dispatch/runners/shfmt.js +2 -3
  209. package/node_modules/pi-lens/dist/clients/dispatch/runners/spotbugs.js +2 -1
  210. package/node_modules/pi-lens/dist/clients/dispatch/runners/taplo.js +2 -1
  211. package/node_modules/pi-lens/dist/clients/dispatch/runners/terragrunt.js +163 -0
  212. package/node_modules/pi-lens/dist/clients/dispatch/runners/tflint.js +37 -5
  213. package/node_modules/pi-lens/dist/clients/dispatch/runners/tree-sitter.js +3 -1
  214. package/node_modules/pi-lens/dist/clients/dispatch/runners/trivy-config.js +13 -7
  215. package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/lazy-installer.js +3 -0
  216. package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/runner-helpers.js +277 -26
  217. package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/spawn-outcome.js +22 -0
  218. package/node_modules/pi-lens/dist/clients/dispatch/runners/yaml-rule-parser.js +5 -4
  219. package/node_modules/pi-lens/dist/clients/disposition-publish.js +10 -3
  220. package/node_modules/pi-lens/dist/clients/durable-store.js +97 -0
  221. package/node_modules/pi-lens/dist/clients/event-loop-monitor.js +94 -3
  222. package/node_modules/pi-lens/dist/clients/extension-log.js +139 -0
  223. package/node_modules/pi-lens/dist/clients/extension-mode.js +73 -0
  224. package/node_modules/pi-lens/dist/clients/file-kinds.js +57 -0
  225. package/node_modules/pi-lens/dist/clients/file-role.js +18 -3
  226. package/node_modules/pi-lens/dist/clients/file-utils.js +56 -34
  227. package/node_modules/pi-lens/dist/clients/format-events-publish.js +16 -3
  228. package/node_modules/pi-lens/dist/clients/format-service.js +24 -8
  229. package/node_modules/pi-lens/dist/clients/formatters-lazy.js +8 -0
  230. package/node_modules/pi-lens/dist/clients/formatters.js +358 -61
  231. package/node_modules/pi-lens/dist/clients/generated-artifacts.js +148 -15
  232. package/node_modules/pi-lens/dist/clients/git-guard.js +681 -11
  233. package/node_modules/pi-lens/dist/clients/git-tracked-ignore.js +4 -0
  234. package/node_modules/pi-lens/dist/clients/go-client.js +2 -1
  235. package/node_modules/pi-lens/dist/clients/govulncheck-client.js +8 -0
  236. package/node_modules/pi-lens/dist/clients/grammar-source.js +12 -4
  237. package/node_modules/pi-lens/dist/clients/gzip-stage-write.js +21 -3
  238. package/node_modules/pi-lens/dist/clients/host-ports.js +31 -0
  239. package/node_modules/pi-lens/dist/clients/installer/index.js +390 -99
  240. package/node_modules/pi-lens/dist/clients/instance-reaper.js +142 -172
  241. package/node_modules/pi-lens/dist/clients/jscpd-client.js +31 -54
  242. package/node_modules/pi-lens/dist/clients/knip-client.js +15 -36
  243. package/node_modules/pi-lens/dist/clients/language-policy.js +14 -2
  244. package/node_modules/pi-lens/dist/clients/language-profile.js +13 -7
  245. package/node_modules/pi-lens/dist/clients/latency-logger.js +23 -1
  246. package/node_modules/pi-lens/dist/clients/lens-config.js +12 -1
  247. package/node_modules/pi-lens/dist/clients/lens-engine.js +86 -17
  248. package/node_modules/pi-lens/dist/clients/lens-events.js +27 -4
  249. package/node_modules/pi-lens/dist/clients/lens-flag-registry.js +8 -0
  250. package/node_modules/pi-lens/dist/clients/lens-map.js +94 -19
  251. package/node_modules/pi-lens/dist/clients/live-bus-emitter.js +30 -0
  252. package/node_modules/pi-lens/dist/clients/log-cleanup.js +2 -1
  253. package/node_modules/pi-lens/dist/clients/lsp/cascade-tier.js +1 -1
  254. package/node_modules/pi-lens/dist/clients/lsp/client.js +493 -48
  255. package/node_modules/pi-lens/dist/clients/lsp/config.js +13 -2
  256. package/node_modules/pi-lens/dist/clients/lsp/diagnostic-binding.js +86 -0
  257. package/node_modules/pi-lens/dist/clients/lsp/edits.js +1012 -121
  258. package/node_modules/pi-lens/dist/clients/lsp/index.js +1807 -780
  259. package/node_modules/pi-lens/dist/clients/lsp/launch.js +32 -26
  260. package/node_modules/pi-lens/dist/clients/lsp/path-utils.js +1 -1
  261. package/node_modules/pi-lens/dist/clients/lsp/ruby-drive-dirs.js +83 -0
  262. package/node_modules/pi-lens/dist/clients/lsp/server.js +196 -32
  263. package/node_modules/pi-lens/dist/clients/lsp/workspace-diagnostics-cache.js +28 -4
  264. package/node_modules/pi-lens/dist/clients/lsp-lazy.js +8 -0
  265. package/node_modules/pi-lens/dist/clients/lsp-mutation.js +248 -0
  266. package/node_modules/pi-lens/dist/clients/mcp/analyze.js +123 -32
  267. package/node_modules/pi-lens/dist/clients/mcp/ipc.js +157 -24
  268. package/node_modules/pi-lens/dist/clients/mcp/session.js +216 -8
  269. package/node_modules/pi-lens/dist/clients/memory-sampler.js +115 -0
  270. package/node_modules/pi-lens/dist/clients/metrics-client.js +2 -1
  271. package/node_modules/pi-lens/dist/clients/metrics-history.js +2 -1
  272. package/node_modules/pi-lens/dist/clients/module-report-lsp.js +5 -0
  273. package/node_modules/pi-lens/dist/clients/module-report.js +168 -3
  274. package/node_modules/pi-lens/dist/clients/ndjson-logger.js +319 -118
  275. package/node_modules/pi-lens/dist/clients/opengrep-client.js +1 -1
  276. package/node_modules/pi-lens/dist/clients/partial-edit-apply.js +120 -5
  277. package/node_modules/pi-lens/dist/clients/path-utils.js +110 -9
  278. package/node_modules/pi-lens/dist/clients/pipeline.js +47 -13
  279. package/node_modules/pi-lens/dist/clients/print-mode.js +21 -0
  280. package/node_modules/pi-lens/dist/clients/project-changes.js +40 -0
  281. package/node_modules/pi-lens/dist/clients/project-diagnostics/cache.js +3 -2
  282. package/node_modules/pi-lens/dist/clients/project-diagnostics/runner-adapters/call-graph-impact.js +8 -0
  283. package/node_modules/pi-lens/dist/clients/project-diagnostics/scanner.js +21 -1
  284. package/node_modules/pi-lens/dist/clients/project-lens-config.js +86 -9
  285. package/node_modules/pi-lens/dist/clients/project-report.js +27 -13
  286. package/node_modules/pi-lens/dist/clients/project-snapshot.js +175 -20
  287. package/node_modules/pi-lens/dist/clients/project-trust.js +181 -0
  288. package/node_modules/pi-lens/dist/clients/quiet-window.js +18 -4
  289. package/node_modules/pi-lens/dist/clients/read-bridge.js +147 -0
  290. package/node_modules/pi-lens/dist/clients/read-guard-logger.js +159 -1
  291. package/node_modules/pi-lens/dist/clients/read-guard-tool-lines.js +150 -29
  292. package/node_modules/pi-lens/dist/clients/read-guard.js +185 -4
  293. package/node_modules/pi-lens/dist/clients/resource-sampler.js +70 -84
  294. package/node_modules/pi-lens/dist/clients/review-graph/builder.js +1122 -221
  295. package/node_modules/pi-lens/dist/clients/review-graph/git-identity.js +2 -1
  296. package/node_modules/pi-lens/dist/clients/review-graph/import-resolvers.js +21 -10
  297. package/node_modules/pi-lens/dist/clients/review-graph/symbol-id.js +46 -0
  298. package/node_modules/pi-lens/dist/clients/review-graph/tsconfig-paths.js +57 -6
  299. package/node_modules/pi-lens/dist/clients/review-graph/workspace-modules.js +51 -3
  300. package/node_modules/pi-lens/dist/clients/review-graph-logger.js +30 -1
  301. package/node_modules/pi-lens/dist/clients/ruff-client.js +27 -44
  302. package/node_modules/pi-lens/dist/clients/runtime-agent-end.js +107 -73
  303. package/node_modules/pi-lens/dist/clients/runtime-context.js +36 -12
  304. package/node_modules/pi-lens/dist/clients/runtime-coordinator.js +74 -6
  305. package/node_modules/pi-lens/dist/clients/runtime-session.js +405 -117
  306. package/node_modules/pi-lens/dist/clients/runtime-tool-call.js +71 -48
  307. package/node_modules/pi-lens/dist/clients/runtime-tool-result.js +218 -23
  308. package/node_modules/pi-lens/dist/clients/runtime-turn.js +237 -64
  309. package/node_modules/pi-lens/dist/clients/rust-client.js +2 -1
  310. package/node_modules/pi-lens/dist/clients/safe-spawn.js +777 -62
  311. package/node_modules/pi-lens/dist/clients/security-scan-client.js +2 -1
  312. package/node_modules/pi-lens/dist/clients/session-state-store.js +3 -2
  313. package/node_modules/pi-lens/dist/clients/sg-runner.js +284 -174
  314. package/node_modules/pi-lens/dist/clients/slow-fs.js +2 -1
  315. package/node_modules/pi-lens/dist/clients/smells-rollup.js +224 -0
  316. package/node_modules/pi-lens/dist/clients/source-filter.js +184 -20
  317. package/node_modules/pi-lens/dist/clients/source-walker.js +66 -10
  318. package/node_modules/pi-lens/dist/clients/startup-scan.js +15 -16
  319. package/node_modules/pi-lens/dist/clients/startup-timing.js +1 -1
  320. package/node_modules/pi-lens/dist/clients/test-runner-client.js +38 -8
  321. package/node_modules/pi-lens/dist/clients/tool-event.js +35 -0
  322. package/node_modules/pi-lens/dist/clients/tool-policy.js +388 -39
  323. package/node_modules/pi-lens/dist/clients/tool-render.js +155 -0
  324. package/node_modules/pi-lens/dist/clients/tree-sitter-cache.js +6 -1
  325. package/node_modules/pi-lens/dist/clients/tree-sitter-client.js +419 -50
  326. package/node_modules/pi-lens/dist/clients/tree-sitter-logger.js +21 -0
  327. package/node_modules/pi-lens/dist/clients/tree-sitter-query-loader.js +7 -1
  328. package/node_modules/pi-lens/dist/clients/tree-sitter-shared.js +7 -2
  329. package/node_modules/pi-lens/dist/clients/tree-sitter-symbol-extractor.js +44 -7
  330. package/node_modules/pi-lens/dist/clients/user-notify.js +51 -0
  331. package/node_modules/pi-lens/dist/clients/vanished-instance-marker.js +57 -0
  332. package/node_modules/pi-lens/dist/clients/warm-attach.js +33 -13
  333. package/node_modules/pi-lens/dist/clients/widget-state.js +300 -43
  334. package/node_modules/pi-lens/dist/clients/word-index.js +391 -64
  335. package/node_modules/pi-lens/dist/clients/workspace-topology.js +89 -6
  336. package/node_modules/pi-lens/dist/index.js +49361 -36888
  337. package/node_modules/pi-lens/dist/mcp/analyze-cli.js +182 -29
  338. package/node_modules/pi-lens/dist/mcp/server.js +211 -44
  339. package/node_modules/pi-lens/dist/tools/ast-grep-search.js +93 -27
  340. package/node_modules/pi-lens/dist/tools/lens-diagnostics.js +200 -36
  341. package/node_modules/pi-lens/dist/tools/lsp-diagnostics.js +153 -40
  342. package/node_modules/pi-lens/dist/tools/lsp-navigation.js +46 -3
  343. package/node_modules/pi-lens/dist/tools/module-report.js +9 -0
  344. package/node_modules/pi-lens/docs/agent-guide.md +2 -2
  345. package/node_modules/pi-lens/docs/agent-tools.md +21 -10
  346. package/node_modules/pi-lens/docs/analysisall.md +192 -0
  347. package/node_modules/pi-lens/docs/api-ports-inventory.md +165 -0
  348. package/node_modules/pi-lens/docs/ast-grep_rules_catalog.md +5 -2
  349. package/node_modules/pi-lens/docs/durable-store-audit-1202.md +32 -0
  350. package/node_modules/pi-lens/docs/environment-variables.md +44 -0
  351. package/node_modules/pi-lens/docs/fable.md +27 -0
  352. package/node_modules/pi-lens/docs/features.md +31 -2
  353. package/node_modules/pi-lens/docs/globalconfig.md +45 -1
  354. package/node_modules/pi-lens/docs/language-coverage.md +4 -3
  355. package/node_modules/pi-lens/docs/lsp-capability-matrix.md +2 -2
  356. package/node_modules/pi-lens/docs/mcp.md +37 -4
  357. package/node_modules/pi-lens/docs/servercapabilities.md +3 -3
  358. package/node_modules/pi-lens/docs/settings.md +12 -1
  359. package/node_modules/pi-lens/docs/tools.md +4 -1
  360. package/node_modules/pi-lens/docs/usage.md +18 -0
  361. package/node_modules/pi-lens/package.json +10 -10
  362. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-bare-except-test.yml +49 -0
  363. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-bare-host-path-in-win32-branch-test.yml +87 -0
  364. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-nested-links-test.yml +11 -1
  365. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-raw-json-store-write-test.yml +17 -0
  366. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-win32-isabsolute-for-qualification-test.yml +16 -0
  367. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-bare-except.yml +3 -15
  368. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-bare-host-path-in-win32-branch.yml +25 -0
  369. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-nested-links.yml +7 -0
  370. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-raw-json-store-write.yml +30 -0
  371. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-win32-isabsolute-for-qualification.yml +23 -0
  372. package/node_modules/pi-lens/rules/tree-sitter-queries/tsx/no-nested-links.yml +4 -6
  373. package/node_modules/pi-lens/rules/tree-sitter-queries/typescript/duplicate-function-arg.yml +2 -2
  374. package/node_modules/pi-lens/scripts/analyze-pi-lens-logs.mjs +55 -2
  375. package/node_modules/pi-mcp-adapter/CHANGELOG.md +25 -0
  376. package/node_modules/pi-mcp-adapter/README.md +34 -3
  377. package/node_modules/pi-mcp-adapter/agent-plugin-loader.ts +1 -1
  378. package/node_modules/pi-mcp-adapter/commands.ts +15 -7
  379. package/node_modules/pi-mcp-adapter/direct-tools.ts +54 -7
  380. package/node_modules/pi-mcp-adapter/host-html-template.ts +0 -8
  381. package/node_modules/pi-mcp-adapter/index.ts +8 -3
  382. package/node_modules/pi-mcp-adapter/init.ts +29 -5
  383. package/node_modules/pi-mcp-adapter/lifecycle.ts +0 -4
  384. package/node_modules/pi-mcp-adapter/mcp-auth-flow.ts +1 -2
  385. package/node_modules/pi-mcp-adapter/mcp-auth.ts +60 -2
  386. package/node_modules/pi-mcp-adapter/mcp-panel.ts +45 -9
  387. package/node_modules/pi-mcp-adapter/mcp-script-worker.mjs +22 -20
  388. package/node_modules/pi-mcp-adapter/metadata-cache.ts +27 -4
  389. package/node_modules/pi-mcp-adapter/npx-resolver.ts +81 -26
  390. package/node_modules/pi-mcp-adapter/package.json +3 -3
  391. package/node_modules/pi-mcp-adapter/proxy-modes.ts +108 -26
  392. package/node_modules/pi-mcp-adapter/search-ranking.ts +86 -7
  393. package/node_modules/pi-mcp-adapter/server-manager.ts +36 -7
  394. package/node_modules/pi-mcp-adapter/session-recovery.ts +8 -2
  395. package/node_modules/pi-mcp-adapter/tool-approval.ts +48 -7
  396. package/node_modules/pi-mcp-adapter/tool-metadata.ts +42 -4
  397. package/node_modules/pi-mcp-adapter/tool-result-renderer.ts +138 -12
  398. package/node_modules/pi-mcp-adapter/types.ts +93 -27
  399. package/node_modules/pi-mcp-adapter/ui-server.ts +20 -1
  400. package/package.json +17 -13
  401. package/node_modules/pi-lens/dist/clients/ast-grep-parser.js +0 -86
  402. package/node_modules/pi-lens/dist/clients/lsp/interactive-install.js +0 -367
  403. package/node_modules/pi-lens/dist/clients/lsp/lsp-index.js +0 -10
  404. package/node_modules/pi-lens/dist/clients/lsp/server-strategies.js +0 -5
  405. package/node_modules/pi-lens/dist/clients/project-metadata.js +0 -690
  406. package/node_modules/pi-lens/dist/clients/source-groups.js +0 -96
  407. package/node_modules/pi-lens/dist/clients/startup-marker.js +0 -6
  408. package/node_modules/pi-lens/dist/clients/types.js +0 -11
@@ -11,9 +11,10 @@
11
11
  */
12
12
  import * as fs from "node:fs";
13
13
  import { createFileTime } from "./file-time.js";
14
+ import { hashDiagnosticContent } from "./lsp/diagnostic-binding.js";
14
15
  import { normalizeFilePath } from "./path-utils.js";
15
16
  import { logReadGuardEvent } from "./read-guard-logger.js";
16
- const READ_GUARD_STATE_VERSION = 1;
17
+ export const READ_GUARD_STATE_VERSION = 1;
17
18
  // --- Constants ---
18
19
  const DEFAULT_CONFIG = {
19
20
  enabled: true,
@@ -28,6 +29,66 @@ const DEFAULT_CONFIG = {
28
29
  const OWN_EDIT_STALE_GRACE_MS = Math.max(0, Number.parseInt(process.env.PI_LENS_READ_GUARD_OWN_EDIT_GRACE_MS ?? "120000", 10) || 120000);
29
30
  /** Avoid hashing very large reads in the hot path. */
30
31
  const READ_HASH_MAX_LINES = Math.max(0, Number.parseInt(process.env.PI_LENS_READ_GUARD_HASH_MAX_LINES ?? "3000", 10) || 3000);
32
+ /**
33
+ * Content bindings are defense in depth on a read-adjacent hot path. Cap the
34
+ * synchronous disk read so bridge registration never hashes an unbounded file.
35
+ */
36
+ const READ_BINDING_MAX_BYTES = 4 * 1024 * 1024;
37
+ const READ_GUARD_MAX_FILES = 256;
38
+ // Unconsumed reads remain valid until edit or session end, but this high
39
+ // sanity cap prevents a read-only session from growing without bound.
40
+ const READ_GUARD_MAX_UNCONSUMED_FILES = 4096;
41
+ const READ_GUARD_IDLE_EVICT_MS_DEFAULT = 30 * 60_000;
42
+ export function captureReadContentBinding(filePath, offset, limit) {
43
+ try {
44
+ if (fs.statSync(filePath).size > READ_BINDING_MAX_BYTES)
45
+ return undefined;
46
+ const content = fs.readFileSync(filePath, "utf-8");
47
+ // Allocate at most the classification prefix first. For range bindings,
48
+ // splitting then stops at the requested range end rather than the EOF.
49
+ const lines = splitLinesThrough(content, READ_HASH_MAX_LINES + 1);
50
+ if (lines.length <= READ_HASH_MAX_LINES) {
51
+ return {
52
+ hash: hashDiagnosticContent(content),
53
+ fullFile: true,
54
+ offset: 1,
55
+ limit: lines.length,
56
+ };
57
+ }
58
+ const scopedOffset = Math.max(1, offset);
59
+ const scopedLimit = Math.min(Math.max(0, limit), READ_HASH_MAX_LINES);
60
+ if (scopedLimit === 0)
61
+ return undefined;
62
+ const rangeLines = splitLinesThrough(content, scopedOffset - 1 + scopedLimit);
63
+ const scopedContent = rangeLines
64
+ .slice(scopedOffset - 1, scopedOffset - 1 + scopedLimit)
65
+ .join("\n");
66
+ return {
67
+ hash: hashDiagnosticContent(scopedContent),
68
+ fullFile: false,
69
+ offset: scopedOffset,
70
+ limit: scopedLimit,
71
+ };
72
+ }
73
+ catch {
74
+ return undefined;
75
+ }
76
+ }
77
+ export function _currentContentMatchesBindingForTests(filePath, binding) {
78
+ try {
79
+ const content = fs.readFileSync(filePath, "utf-8");
80
+ const boundContent = binding.fullFile
81
+ ? content
82
+ : splitLines(content)
83
+ .slice(binding.offset - 1, binding.offset - 1 + binding.limit)
84
+ .join("\n");
85
+ return hashDiagnosticContent(boundContent) === binding.hash;
86
+ }
87
+ catch {
88
+ return false;
89
+ }
90
+ }
91
+ const currentContentMatchesBinding = _currentContentMatchesBindingForTests;
31
92
  // Adaptive relocation window (findRelocation). A globally-unique hash-sequence
32
93
  // match always wins; when the content is duplicated elsewhere, we fall back to
33
94
  // a match that is unique WITHIN this window of the original position. The window
@@ -40,6 +101,28 @@ const RELOCATION_WINDOW_MAX = Math.max(RELOCATION_WINDOW_MIN, Number.parseInt(pr
40
101
  function splitLines(text) {
41
102
  return text.split(/\r?\n/);
42
103
  }
104
+ /** `splitLines` semantics, but without scanning/allocating beyond `maxLines`. */
105
+ function splitLinesThrough(text, maxLines) {
106
+ if (maxLines <= 0)
107
+ return [];
108
+ const lines = [];
109
+ let start = 0;
110
+ while (lines.length < maxLines) {
111
+ const newline = text.indexOf("\n", start);
112
+ if (newline === -1) {
113
+ lines.push(text.slice(start));
114
+ break;
115
+ }
116
+ const end = newline > start && text[newline - 1] === "\r" ? newline - 1 : newline;
117
+ lines.push(text.slice(start, end));
118
+ start = newline + 1;
119
+ if (start === text.length && lines.length < maxLines) {
120
+ lines.push("");
121
+ break;
122
+ }
123
+ }
124
+ return lines;
125
+ }
43
126
  export function lineContentHash(line) {
44
127
  // FNV-1a over whitespace-stripped content. This treats no-op formatter/touch
45
128
  // changes as still-valid context while detecting semantic line changes.
@@ -118,6 +201,10 @@ export class ReadGuard {
118
201
  config;
119
202
  reads = new Map();
120
203
  edits = new Map();
204
+ fileLastUsed = new Map();
205
+ fileIdleTimers = new Map();
206
+ /** Reads remain behavior-gating until the corresponding edit is published. */
207
+ consumedReadFiles = new Set();
121
208
  fileTime;
122
209
  exemptions = new Set(); // One-time exemptions via /lens-allow-edit
123
210
  pendingCreations = new Map();
@@ -147,6 +234,66 @@ export class ReadGuard {
147
234
  key(filePath) {
148
235
  return normalizeFilePath(filePath);
149
236
  }
237
+ idleEvictMs() {
238
+ const value = Number.parseInt(process.env.PI_LENS_READ_GUARD_IDLE_EVICT_MS ?? "", 10);
239
+ return Number.isSafeInteger(value) && value > 0 ? value : READ_GUARD_IDLE_EVICT_MS_DEFAULT;
240
+ }
241
+ clearFileTimer(filePath) {
242
+ const timer = this.fileIdleTimers.get(filePath);
243
+ if (timer)
244
+ clearTimeout(timer);
245
+ this.fileIdleTimers.delete(filePath);
246
+ }
247
+ evictFile(filePath) {
248
+ this.clearFileTimer(filePath);
249
+ this.reads.delete(filePath);
250
+ this.edits.delete(filePath);
251
+ this.fileLastUsed.delete(filePath);
252
+ this.consumedReadFiles.delete(filePath);
253
+ this.writtenThisSession.delete(filePath);
254
+ }
255
+ touchFile(filePath) {
256
+ const now = Date.now();
257
+ this.fileLastUsed.set(filePath, now);
258
+ this.clearFileTimer(filePath);
259
+ // An outstanding read is enforcement state, not a rebuildable cache entry.
260
+ // It must survive idle time and file-cap pressure until the edit consumes it.
261
+ if (this.reads.has(filePath) && !this.consumedReadFiles.has(filePath))
262
+ return;
263
+ const stamp = now;
264
+ const timer = setTimeout(() => {
265
+ if (this.fileLastUsed.get(filePath) !== stamp)
266
+ return;
267
+ // Never turn an outstanding read into a zero-read block through idle
268
+ // eviction. Only consumed reads and rebuildable edit history may expire.
269
+ if (this.reads.has(filePath) && !this.consumedReadFiles.has(filePath))
270
+ return;
271
+ this.evictFile(filePath);
272
+ }, this.idleEvictMs());
273
+ timer.unref?.();
274
+ this.fileIdleTimers.set(filePath, timer);
275
+ }
276
+ enforceFileCap() {
277
+ while (this.reads.size > READ_GUARD_MAX_FILES) {
278
+ const victim = [...this.reads.keys()]
279
+ .filter((filePath) => this.consumedReadFiles.has(filePath))
280
+ .sort((a, b) => (this.fileLastUsed.get(a) ?? 0) - (this.fileLastUsed.get(b) ?? 0))[0];
281
+ if (!victim)
282
+ break;
283
+ this.evictFile(victim);
284
+ }
285
+ while (this.reads.size > READ_GUARD_MAX_UNCONSUMED_FILES) {
286
+ const victim = [...this.reads.keys()]
287
+ .filter((filePath) => !this.consumedReadFiles.has(filePath))
288
+ .sort((a, b) => (this.fileLastUsed.get(a) ?? 0) -
289
+ (this.fileLastUsed.get(b) ?? 0))[0];
290
+ if (!victim)
291
+ break;
292
+ // This is a normal read miss: a later edit must require a fresh read,
293
+ // never silently allow and never become a permanent hard-block.
294
+ this.evictFile(victim);
295
+ }
296
+ }
150
297
  // --- Public API ---
151
298
  /**
152
299
  * Record that a file was read.
@@ -161,8 +308,11 @@ export class ReadGuard {
161
308
  captureLineHashes(filePath, record.effectiveOffset, record.effectiveLimit),
162
309
  };
163
310
  const arr = this.reads.get(storedRecord.filePath) ?? [];
311
+ this.consumedReadFiles.delete(storedRecord.filePath);
164
312
  arr.push(storedRecord);
165
313
  this.reads.set(storedRecord.filePath, arr);
314
+ this.touchFile(storedRecord.filePath);
315
+ this.enforceFileCap();
166
316
  logReadGuardEvent({
167
317
  event: "read_recorded",
168
318
  sessionId: this.sessionId,
@@ -181,6 +331,9 @@ export class ReadGuard {
181
331
  writeIndex: storedRecord.writeIndex,
182
332
  readCountForFile: arr.length,
183
333
  hashLineCount: Object.keys(storedRecord.lineHashes ?? {}).length,
334
+ ...(storedRecord.source !== undefined && {
335
+ source: storedRecord.source,
336
+ }),
184
337
  },
185
338
  });
186
339
  // Also update FileTime stamp for this file
@@ -228,6 +381,8 @@ export class ReadGuard {
228
381
  // Canonicalize once: every map lookup below (and every private helper this
229
382
  // passes filePath to) must agree with how recordRead keyed the read.
230
383
  filePath = this.key(filePath);
384
+ if (this.reads.has(filePath) || this.edits.has(filePath))
385
+ this.touchFile(filePath);
231
386
  // Check exemptions
232
387
  if (this.exemptions.has(filePath)) {
233
388
  this.exemptions.delete(filePath); // One-time use
@@ -271,6 +426,19 @@ export class ReadGuard {
271
426
  });
272
427
  return verdict;
273
428
  }
429
+ const lastBoundRead = [...fileReads]
430
+ .reverse()
431
+ .find((read) => read.contentBinding !== undefined);
432
+ if (lastBoundRead?.contentBinding &&
433
+ !currentContentMatchesBinding(filePath, lastBoundRead.contentBinding)) {
434
+ const verdict = this.blockOrWarn("file-modified", `🔄 RETRYABLE — File modified since read\n\nYou last read \`${filePath}\` at ${new Date(lastBoundRead.timestamp).toISOString()}.\nThe file content no longer matches the bridge-recorded read.\n\nYour mental model is out of sync with the actual file content.\nTo proceed:\n 1. Re-read the file: \`read path="${filePath}"\``, undefined, effectiveMode);
435
+ this.recordVerdict(filePath, "edit", touchedLines, verdict, {
436
+ reasonKind: "file_modified",
437
+ lastReadTimestamp: lastBoundRead.timestamp,
438
+ contentBindingMismatch: true,
439
+ });
440
+ return verdict;
441
+ }
274
442
  // 2. FileTime check (actual staleness)
275
443
  let ignoredOwnEditStaleness = false;
276
444
  let ignoredHashStaleness = false;
@@ -315,7 +483,9 @@ export class ReadGuard {
315
483
  // If oldText was resolved (content-verified), the model demonstrably
316
484
  // knew the content it's replacing — line drift from prior edits in
317
485
  // the session is the likely cause. Downgrade to warn rather than block.
318
- const outOfRangeMode = options?.oldTextResolved ? "warn" : effectiveMode;
486
+ const outOfRangeMode = options?.oldTextResolved
487
+ ? "warn"
488
+ : effectiveMode;
319
489
  const verdict = this.blockOrWarn("out-of-range", `🔄 RETRYABLE — Edit outside read range\n\nYou read \`${filePath}\` lines ${lastRead.effectiveOffset}-${lastReadEnd}${lastRead.enclosingSymbol ? ` (${lastRead.enclosingSymbol.kind} \`${lastRead.enclosingSymbol.name}\`)` : ""}, but your edit touches lines ${editStart}-${editEnd}.\n\nRead the relevant section first, then retry the edit:\n \`read path="${filePath}" offset=${Math.max(1, editStart - 5)} limit=${Math.min(30, editEnd - editStart + 10)}\``, {
320
490
  editRange: range,
321
491
  readRanges: fileReads.map((r) => ({
@@ -435,6 +605,10 @@ export class ReadGuard {
435
605
  filePath = this.key(filePath);
436
606
  this.fileTime.read(filePath);
437
607
  this.writtenThisSession.add(filePath);
608
+ if (this.reads.has(filePath))
609
+ this.consumedReadFiles.add(filePath);
610
+ this.touchFile(filePath);
611
+ this.enforceFileCap();
438
612
  const creation = this.pendingCreations.get(filePath);
439
613
  if (creation) {
440
614
  this.pendingCreations.delete(filePath);
@@ -496,7 +670,10 @@ export class ReadGuard {
496
670
  * Get all read records for a file (for debugging).
497
671
  */
498
672
  getReadHistory(filePath) {
499
- return this.reads.get(this.key(filePath)) ?? [];
673
+ const key = this.key(filePath);
674
+ if (this.reads.has(key))
675
+ this.touchFile(key);
676
+ return this.reads.get(key) ?? [];
500
677
  }
501
678
  /**
502
679
  * Snapshot the read-set for persistence across a session resume (#1041).
@@ -579,7 +756,10 @@ export class ReadGuard {
579
756
  * Get all edit records for a file (for debugging).
580
757
  */
581
758
  getEditHistory(filePath) {
582
- return this.edits.get(this.key(filePath)) ?? [];
759
+ const key = this.key(filePath);
760
+ if (this.edits.has(key))
761
+ this.touchFile(key);
762
+ return this.edits.get(key) ?? [];
583
763
  }
584
764
  // --- Private helpers ---
585
765
  injectCreationRead(filePath, turnIndex, writeIndex) {
@@ -926,6 +1106,7 @@ export class ReadGuard {
926
1106
  return { action: "allow" };
927
1107
  }
928
1108
  recordEdit(filePath, tool, touchedLines, verdict) {
1109
+ this.touchFile(filePath);
929
1110
  const arr = this.edits.get(filePath) ?? [];
930
1111
  arr.push({
931
1112
  filePath,
@@ -43,9 +43,9 @@
43
43
  * process/pidusage involvement — mirrors the pure/impure split in
44
44
  * clients/instance-reaper.ts (`decideOrphanReaping` vs `sweepOrphans`).
45
45
  */
46
- import { spawn as nodeSpawn } from "node:child_process";
47
46
  import * as path from "node:path";
48
47
  import pidusage from "pidusage";
48
+ import { spawnCollectStdout } from "./child-unref.js";
49
49
  // Read the platform live (not a module-load const) so both the Windows and the
50
50
  // POSIX sampling paths are exercisable in unit tests regardless of the host OS.
51
51
  function runningOnWindows() {
@@ -106,32 +106,26 @@ async function findDescendantPidsWindows(rootPid) {
106
106
  const psScript = "Get-CimInstance Win32_Process " +
107
107
  '| Select-Object -Property ProcessId,ParentProcessId ' +
108
108
  '| ForEach-Object { "$($_.ProcessId),$($_.ParentProcessId)" }';
109
- const pairs = await new Promise((resolve) => {
110
- try {
111
- const powershell = path.join(process.env.SystemRoot ?? String.raw `C:\Windows`, "WindowsPowerShell", "v1.0", "powershell.exe");
112
- const child = nodeSpawn(powershell, ["-NoProfile", "-NonInteractive", "-Command", psScript], { shell: false, windowsHide: true, stdio: ["ignore", "pipe", "ignore"] });
113
- let out = "";
114
- child.stdout?.on("data", (chunk) => {
115
- out += chunk.toString();
116
- });
117
- child.once("error", () => resolve([]));
118
- child.once("close", () => {
119
- const result = [];
120
- for (const line of out.split(/\r?\n/)) {
121
- const [pidStr, ppidStr] = line.split(",");
122
- const pid = Number(pidStr);
123
- const ppid = Number(ppidStr);
124
- if (Number.isFinite(pid) && Number.isFinite(ppid)) {
125
- result.push([pid, ppid]);
126
- }
127
- }
128
- resolve(result);
129
- });
130
- }
131
- catch {
132
- resolve([]);
109
+ const powershell = path.join(process.env.SystemRoot ?? String.raw `C:\Windows`, "WindowsPowerShell", "v1.0", "powershell.exe");
110
+ // Fire-and-forget, per-poll-tick spawn (#1155): `spawnCollectStdout` unrefs
111
+ // the child AND its piped stdout so this one-shot CIM query can never keep
112
+ // a settled `pi --print` process alive past its own close — mirrors the
113
+ // reaper's identical spawn→collect plumbing (#1153/#1160). Sampling still
114
+ // works normally in an interactive/long-lived session: unref only means
115
+ // "don't hold the loop open FOR this alone," the collected stdout is still
116
+ // delivered whenever `close` fires. Resolves to `""` on any spawn/`error`
117
+ // failure, which the parse below turns into an empty pairs list (same
118
+ // result the old inline `resolve([])` error path produced).
119
+ const out = await spawnCollectStdout(powershell, ["-NoProfile", "-NonInteractive", "-Command", psScript], { shell: false, windowsHide: true, stdio: ["ignore", "pipe", "ignore"] });
120
+ const pairs = [];
121
+ for (const line of out.split(/\r?\n/)) {
122
+ const [pidStr, ppidStr] = line.split(",");
123
+ const pid = Number(pidStr);
124
+ const ppid = Number(ppidStr);
125
+ if (Number.isFinite(pid) && Number.isFinite(ppid)) {
126
+ pairs.push([pid, ppid]);
133
127
  }
134
- });
128
+ }
135
129
  return walkDescendantPids(rootPid, pairs);
136
130
  }
137
131
  const windowsCpuHistory = new Map();
@@ -168,67 +162,59 @@ async function sampleProcessesWindows(valid) {
168
162
  const psScript = `Get-CimInstance Win32_Process -Filter "${filter}" ` +
169
163
  "| Select-Object -Property ProcessId,WorkingSetSize,KernelModeTime,UserModeTime " +
170
164
  '| ForEach-Object { "$($_.ProcessId),$($_.WorkingSetSize),$($_.KernelModeTime),$($_.UserModeTime)" }';
171
- return await new Promise((resolve) => {
172
- try {
173
- const powershell = path.join(process.env.SystemRoot ?? String.raw `C:\Windows`, "WindowsPowerShell", "v1.0", "powershell.exe");
174
- const child = nodeSpawn(powershell, ["-NoProfile", "-NonInteractive", "-Command", psScript], { shell: false, windowsHide: true, stdio: ["ignore", "pipe", "ignore"] });
175
- let out = "";
176
- child.stdout?.on("data", (chunk) => {
177
- out += chunk.toString();
178
- });
179
- // A spawn error (ENOENT, spawn UNKNOWN surfaced async, etc.) loses this
180
- // tick's data for every pid but never rejects.
181
- child.once("error", () => resolve(result));
182
- child.once("close", () => {
183
- try {
184
- const now = Date.now();
185
- const seen = new Set();
186
- for (const line of out.split(/\r?\n/)) {
187
- const parts = line.split(",");
188
- if (parts.length < 4)
189
- continue;
190
- const pid = Number(parts[0]);
191
- const workingSet = Number(parts[1]);
192
- const kernel100ns = Number(parts[2]);
193
- const user100ns = Number(parts[3]);
194
- if (!Number.isFinite(pid) || pid <= 0)
195
- continue;
196
- if (!Number.isFinite(workingSet))
197
- continue;
198
- const cpuMs = Math.round(kernel100ns / 1e4) + Math.round(user100ns / 1e4);
199
- const prev = windowsCpuHistory.get(pid);
200
- let cpuPercent = 0;
201
- if (prev) {
202
- const wallMs = now - prev.ts;
203
- if (wallMs > 0) {
204
- cpuPercent = ((cpuMs - prev.cpuMs) / wallMs) * 100;
205
- if (!Number.isFinite(cpuPercent) || cpuPercent < 0)
206
- cpuPercent = 0;
207
- }
208
- }
209
- windowsCpuHistory.set(pid, { cpuMs, ts: now });
210
- seen.add(pid);
211
- result.set(pid, { rssBytes: workingSet, cpuPercent });
212
- }
213
- // Prune stale history so pids that have gone away don't accumulate.
214
- for (const [pid, entry] of windowsCpuHistory) {
215
- if (!seen.has(pid) && now - entry.ts > CPU_HISTORY_MAX_AGE_MS) {
216
- windowsCpuHistory.delete(pid);
217
- }
218
- }
219
- }
220
- catch {
221
- // Parsing must never throw into the resolve path; best-effort.
165
+ const powershell = path.join(process.env.SystemRoot ?? String.raw `C:\Windows`, "WindowsPowerShell", "v1.0", "powershell.exe");
166
+ // Fire-and-forget, per-poll-tick spawn (#1155): `spawnCollectStdout` unrefs
167
+ // the child AND its piped stdout so this one-shot CIM query can never keep
168
+ // a settled `pi --print` process alive past its own close — mirrors the
169
+ // reaper's identical spawn→collect plumbing (#1153/#1160). It also absorbs
170
+ // both failure modes this function used to guard inline — a synchronous
171
+ // `spawn` throw (the `spawn UNKNOWN` crash vector, #620) and an async
172
+ // `error` event — resolving to `""` either way, which the parse below
173
+ // turns into the same empty/partial `result` map the old inline handlers
174
+ // produced. Sampling still works normally in an interactive/long-lived
175
+ // session: unref only means "don't hold the loop open FOR this alone."
176
+ const out = await spawnCollectStdout(powershell, ["-NoProfile", "-NonInteractive", "-Command", psScript], { shell: false, windowsHide: true, stdio: ["ignore", "pipe", "ignore"] });
177
+ try {
178
+ const now = Date.now();
179
+ const seen = new Set();
180
+ for (const line of out.split(/\r?\n/)) {
181
+ const parts = line.split(",");
182
+ if (parts.length < 4)
183
+ continue;
184
+ const pid = Number(parts[0]);
185
+ const workingSet = Number(parts[1]);
186
+ const kernel100ns = Number(parts[2]);
187
+ const user100ns = Number(parts[3]);
188
+ if (!Number.isFinite(pid) || pid <= 0)
189
+ continue;
190
+ if (!Number.isFinite(workingSet))
191
+ continue;
192
+ const cpuMs = Math.round(kernel100ns / 1e4) + Math.round(user100ns / 1e4);
193
+ const prev = windowsCpuHistory.get(pid);
194
+ let cpuPercent = 0;
195
+ if (prev) {
196
+ const wallMs = now - prev.ts;
197
+ if (wallMs > 0) {
198
+ cpuPercent = ((cpuMs - prev.cpuMs) / wallMs) * 100;
199
+ if (!Number.isFinite(cpuPercent) || cpuPercent < 0)
200
+ cpuPercent = 0;
222
201
  }
223
- resolve(result);
224
- });
202
+ }
203
+ windowsCpuHistory.set(pid, { cpuMs, ts: now });
204
+ seen.add(pid);
205
+ result.set(pid, { rssBytes: workingSet, cpuPercent });
225
206
  }
226
- catch {
227
- // Synchronous spawn throw (the `spawn UNKNOWN` crash vector) — resolve
228
- // to whatever we have (empty), never propagate.
229
- resolve(result);
207
+ // Prune stale history so pids that have gone away don't accumulate.
208
+ for (const [pid, entry] of windowsCpuHistory) {
209
+ if (!seen.has(pid) && now - entry.ts > CPU_HISTORY_MAX_AGE_MS) {
210
+ windowsCpuHistory.delete(pid);
211
+ }
230
212
  }
231
- });
213
+ }
214
+ catch {
215
+ // Parsing must never throw into the caller; best-effort.
216
+ }
217
+ return result;
232
218
  }
233
219
  /**
234
220
  * Sample CPU%/RSS for a set of pids. Best-effort: a pid that can't be resolved