@herbertgao/pi-extensions 2026.9.6 → 2026.9.8

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 (304) hide show
  1. package/README.md +3 -8
  2. package/THIRD_PARTY_NOTICES.md +0 -4
  3. package/node_modules/@narumitw/pi-btw/README.md +2 -0
  4. package/node_modules/@narumitw/pi-btw/dist/index.ts +284 -365
  5. package/node_modules/@narumitw/pi-btw/dist/index.ts.map +3 -3
  6. package/node_modules/@narumitw/pi-btw/package.json +52 -53
  7. package/node_modules/@narumitw/pi-btw/src/bring-to-main.ts +494 -547
  8. package/node_modules/@narumitw/pi-btw/src/btw.ts +814 -856
  9. package/node_modules/@narumitw/pi-btw/src/fullscreen-ui.ts +673 -674
  10. package/node_modules/@narumitw/pi-btw/src/keybindings.ts +228 -270
  11. package/node_modules/@narumitw/pi-btw/src/main-tree-picker.ts +309 -318
  12. package/node_modules/@narumitw/pi-btw/src/menu.ts +416 -454
  13. package/node_modules/@narumitw/pi-btw/src/settings.ts +203 -219
  14. package/node_modules/@narumitw/pi-btw/src/side-thread.ts +173 -201
  15. package/node_modules/@narumitw/pi-btw/src/text.ts +21 -23
  16. package/node_modules/@narumitw/pi-btw/src/transcript-markdown.ts +65 -0
  17. package/node_modules/@narumitw/pi-btw/src/transcript-pager.ts +611 -662
  18. package/node_modules/grok-mermaid/CHANGELOG.md +46 -0
  19. package/node_modules/grok-mermaid/LICENSE +205 -0
  20. package/node_modules/grok-mermaid/README.md +191 -0
  21. package/node_modules/grok-mermaid/dist/ansi.d.ts +16 -0
  22. package/node_modules/grok-mermaid/dist/ansi.d.ts.map +1 -0
  23. package/node_modules/grok-mermaid/dist/ansi.js +23 -0
  24. package/node_modules/grok-mermaid/dist/ansi.js.map +1 -0
  25. package/node_modules/grok-mermaid/dist/canvas.d.ts +87 -0
  26. package/node_modules/grok-mermaid/dist/canvas.d.ts.map +1 -0
  27. package/node_modules/grok-mermaid/dist/canvas.js +366 -0
  28. package/node_modules/grok-mermaid/dist/canvas.js.map +1 -0
  29. package/node_modules/grok-mermaid/dist/graph.d.ts +74 -0
  30. package/node_modules/grok-mermaid/dist/graph.d.ts.map +1 -0
  31. package/node_modules/grok-mermaid/dist/graph.js +91 -0
  32. package/node_modules/grok-mermaid/dist/graph.js.map +1 -0
  33. package/node_modules/grok-mermaid/dist/index.d.ts +32 -0
  34. package/node_modules/grok-mermaid/dist/index.d.ts.map +1 -0
  35. package/node_modules/grok-mermaid/dist/index.js +100 -0
  36. package/node_modules/grok-mermaid/dist/index.js.map +1 -0
  37. package/node_modules/grok-mermaid/dist/labels.d.ts +62 -0
  38. package/node_modules/grok-mermaid/dist/labels.d.ts.map +1 -0
  39. package/node_modules/grok-mermaid/dist/labels.js +324 -0
  40. package/node_modules/grok-mermaid/dist/labels.js.map +1 -0
  41. package/node_modules/grok-mermaid/dist/layout-seq.d.ts +12 -0
  42. package/node_modules/grok-mermaid/dist/layout-seq.d.ts.map +1 -0
  43. package/node_modules/grok-mermaid/dist/layout-seq.js +194 -0
  44. package/node_modules/grok-mermaid/dist/layout-seq.js.map +1 -0
  45. package/node_modules/grok-mermaid/dist/layout.d.ts +87 -0
  46. package/node_modules/grok-mermaid/dist/layout.d.ts.map +1 -0
  47. package/node_modules/grok-mermaid/dist/layout.js +881 -0
  48. package/node_modules/grok-mermaid/dist/layout.js.map +1 -0
  49. package/node_modules/grok-mermaid/dist/parse.d.ts +83 -0
  50. package/node_modules/grok-mermaid/dist/parse.d.ts.map +1 -0
  51. package/node_modules/grok-mermaid/dist/parse.js +1151 -0
  52. package/node_modules/grok-mermaid/dist/parse.js.map +1 -0
  53. package/node_modules/grok-mermaid/dist/source-box.d.ts +18 -0
  54. package/node_modules/grok-mermaid/dist/source-box.d.ts.map +1 -0
  55. package/node_modules/grok-mermaid/dist/source-box.js +78 -0
  56. package/node_modules/grok-mermaid/dist/source-box.js.map +1 -0
  57. package/node_modules/grok-mermaid/dist/types.d.ts +42 -0
  58. package/node_modules/grok-mermaid/dist/types.d.ts.map +1 -0
  59. package/node_modules/grok-mermaid/dist/types.js +1 -0
  60. package/node_modules/grok-mermaid/dist/types.js.map +1 -0
  61. package/node_modules/grok-mermaid/dist/width-data.d.ts +2 -0
  62. package/node_modules/grok-mermaid/dist/width-data.d.ts.map +1 -0
  63. package/node_modules/grok-mermaid/dist/width-data.js +994 -0
  64. package/node_modules/grok-mermaid/dist/width-data.js.map +1 -0
  65. package/node_modules/grok-mermaid/dist/width.d.ts +18 -0
  66. package/node_modules/grok-mermaid/dist/width.d.ts.map +1 -0
  67. package/node_modules/grok-mermaid/dist/width.js +76 -0
  68. package/node_modules/grok-mermaid/dist/width.js.map +1 -0
  69. package/node_modules/grok-mermaid/package.json +49 -0
  70. package/node_modules/grok-mermaid/src/ansi.ts +34 -0
  71. package/node_modules/grok-mermaid/src/canvas.ts +373 -0
  72. package/node_modules/grok-mermaid/src/graph.ts +142 -0
  73. package/node_modules/grok-mermaid/src/index.ts +104 -0
  74. package/node_modules/grok-mermaid/src/labels.ts +326 -0
  75. package/node_modules/grok-mermaid/src/layout-seq.ts +203 -0
  76. package/node_modules/grok-mermaid/src/layout.ts +1015 -0
  77. package/node_modules/grok-mermaid/src/parse.ts +1189 -0
  78. package/node_modules/grok-mermaid/src/source-box.ts +89 -0
  79. package/node_modules/grok-mermaid/src/types.ts +43 -0
  80. package/node_modules/grok-mermaid/src/width-data.ts +993 -0
  81. package/node_modules/grok-mermaid/src/width.ts +74 -0
  82. package/node_modules/pi-lens/CHANGELOG.md +239 -0
  83. package/node_modules/pi-lens/config/dependency-cruiser-eager-allowlist.json +3 -0
  84. package/node_modules/pi-lens/dist/clients/actionable-warnings.js +30 -4
  85. package/node_modules/pi-lens/dist/clients/agent-behavior-client.js +2 -0
  86. package/node_modules/pi-lens/dist/clients/bash-file-access.js +26 -0
  87. package/node_modules/pi-lens/dist/clients/biome-client.js +1 -1
  88. package/node_modules/pi-lens/dist/clients/blocker-freshness.js +403 -35
  89. package/node_modules/pi-lens/dist/clients/bootstrap.js +6 -1
  90. package/node_modules/pi-lens/dist/clients/cascade-format.js +200 -4
  91. package/node_modules/pi-lens/dist/clients/code-quality-warnings.js +10 -2
  92. package/node_modules/pi-lens/dist/clients/degradation-ledger.js +16 -1
  93. package/node_modules/pi-lens/dist/clients/diagnostic-dispositions.js +12 -1
  94. package/node_modules/pi-lens/dist/clients/dispatch/ast-grep-catalog.js +65 -0
  95. package/node_modules/pi-lens/dist/clients/dispatch/auxiliary-lsp.js +28 -0
  96. package/node_modules/pi-lens/dist/clients/dispatch/dispatcher.js +9 -2
  97. package/node_modules/pi-lens/dist/clients/dispatch/finding-policy.js +222 -0
  98. package/node_modules/pi-lens/dist/clients/dispatch/indent-detect.js +389 -13
  99. package/node_modules/pi-lens/dist/clients/dispatch/integration.js +175 -25
  100. package/node_modules/pi-lens/dist/clients/dispatch/rule-ignores.js +62 -0
  101. package/node_modules/pi-lens/dist/clients/dispatch/runners/ast-grep-napi.js +21 -41
  102. package/node_modules/pi-lens/dist/clients/dispatch/runners/biome-check.js +6 -1
  103. package/node_modules/pi-lens/dist/clients/dispatch/runners/rust-clippy.js +7 -7
  104. package/node_modules/pi-lens/dist/clients/dispatch/runners/shfmt.js +2 -12
  105. package/node_modules/pi-lens/dist/clients/dispatch/runners/tree-sitter.js +2 -21
  106. package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/runner-helpers.js +1 -1
  107. package/node_modules/pi-lens/dist/clients/dispatch/types.js +4 -1
  108. package/node_modules/pi-lens/dist/clients/disposition-publish.js +1 -0
  109. package/node_modules/pi-lens/dist/clients/effective-config.js +1 -1
  110. package/node_modules/pi-lens/dist/clients/file-kinds.js +26 -1
  111. package/node_modules/pi-lens/dist/clients/file-role.js +21 -21
  112. package/node_modules/pi-lens/dist/clients/file-utils.js +156 -10
  113. package/node_modules/pi-lens/dist/clients/finding-delivery-gate.js +56 -9
  114. package/node_modules/pi-lens/dist/clients/format-service.js +39 -21
  115. package/node_modules/pi-lens/dist/clients/formatters.js +60 -13
  116. package/node_modules/pi-lens/dist/clients/generation-guard.js +2 -2
  117. package/node_modules/pi-lens/dist/clients/git-guard.js +11 -3
  118. package/node_modules/pi-lens/dist/clients/indent-retarget.js +78 -8
  119. package/node_modules/pi-lens/dist/clients/installer/index.js +333 -114
  120. package/node_modules/pi-lens/dist/clients/instance-reaper-state.js +7 -0
  121. package/node_modules/pi-lens/dist/clients/instance-reaper.js +20 -10
  122. package/node_modules/pi-lens/dist/clients/language-profile.js +23 -1
  123. package/node_modules/pi-lens/dist/clients/language-registry.js +5 -1
  124. package/node_modules/pi-lens/dist/clients/latency-logger.js +4 -1
  125. package/node_modules/pi-lens/dist/clients/lens-engine.js +12 -3
  126. package/node_modules/pi-lens/dist/clients/lens-events.js +2 -1
  127. package/node_modules/pi-lens/dist/clients/lens-flag-registry.js +79 -0
  128. package/node_modules/pi-lens/dist/clients/live-bus-emitter.js +2 -1
  129. package/node_modules/pi-lens/dist/clients/lsp/client.js +25 -21
  130. package/node_modules/pi-lens/dist/clients/lsp/config.js +2 -2
  131. package/node_modules/pi-lens/dist/clients/lsp/index.js +7 -0
  132. package/node_modules/pi-lens/dist/clients/lsp/launch.js +16 -4
  133. package/node_modules/pi-lens/dist/clients/lsp/server.js +19 -2
  134. package/node_modules/pi-lens/dist/clients/lsp/workspace-diagnostics-cache.js +2 -1
  135. package/node_modules/pi-lens/dist/clients/lsp-mutation.js +5 -3
  136. package/node_modules/pi-lens/dist/clients/map-with-concurrency.js +12 -8
  137. package/node_modules/pi-lens/dist/clients/mcp/session.js +36 -4
  138. package/node_modules/pi-lens/dist/clients/ndjson-logger.js +24 -7
  139. package/node_modules/pi-lens/dist/clients/observed-mutation.js +40 -2
  140. package/node_modules/pi-lens/dist/clients/opaque-mutation-scan.js +23 -3
  141. package/node_modules/pi-lens/dist/clients/opengrep-client.js +110 -15
  142. package/node_modules/pi-lens/dist/clients/path-utils.js +123 -3
  143. package/node_modules/pi-lens/dist/clients/performance-report.js +5 -1
  144. package/node_modules/pi-lens/dist/clients/php-cs-fixer-config.js +1 -1
  145. package/node_modules/pi-lens/dist/clients/pipeline.js +74 -5
  146. package/node_modules/pi-lens/dist/clients/project-diagnostics/cache.js +37 -5
  147. package/node_modules/pi-lens/dist/clients/project-diagnostics/fresh-fetch.js +145 -18
  148. package/node_modules/pi-lens/dist/clients/project-diagnostics/runner-adapters/dead-code.js +4 -1
  149. package/node_modules/pi-lens/dist/clients/project-diagnostics/scanner.js +59 -3
  150. package/node_modules/pi-lens/dist/clients/project-lens-config.js +45 -2
  151. package/node_modules/pi-lens/dist/clients/read-guard.js +70 -11
  152. package/node_modules/pi-lens/dist/clients/resource-sampler.js +147 -9
  153. package/node_modules/pi-lens/dist/clients/ruff-client.js +1 -1
  154. package/node_modules/pi-lens/dist/clients/runtime-agent-end.js +33 -5
  155. package/node_modules/pi-lens/dist/clients/runtime-context.js +6 -2
  156. package/node_modules/pi-lens/dist/clients/runtime-coordinator.js +77 -7
  157. package/node_modules/pi-lens/dist/clients/runtime-session.js +21 -2
  158. package/node_modules/pi-lens/dist/clients/runtime-tool-call.js +43 -12
  159. package/node_modules/pi-lens/dist/clients/runtime-tool-result.js +330 -85
  160. package/node_modules/pi-lens/dist/clients/runtime-turn.js +147 -17
  161. package/node_modules/pi-lens/dist/clients/safe-spawn.js +258 -5
  162. package/node_modules/pi-lens/dist/clients/scratch-tree-policy.js +6 -0
  163. package/node_modules/pi-lens/dist/clients/session-event-guard.js +50 -5
  164. package/node_modules/pi-lens/dist/clients/sg-runner.js +23 -11
  165. package/node_modules/pi-lens/dist/clients/sgconfig.js +79 -2
  166. package/node_modules/pi-lens/dist/clients/test-runner-client.js +6 -41
  167. package/node_modules/pi-lens/dist/clients/test-runner-delivery.js +43 -0
  168. package/node_modules/pi-lens/dist/clients/tool-agreement.js +423 -0
  169. package/node_modules/pi-lens/dist/clients/tool-config.js +50 -0
  170. package/node_modules/pi-lens/dist/clients/tool-cwd.js +47 -76
  171. package/node_modules/pi-lens/dist/clients/tool-policy.js +163 -7
  172. package/node_modules/pi-lens/dist/clients/tree-sitter-client.js +120 -0
  173. package/node_modules/pi-lens/dist/clients/tree-sitter-query-loader.js +127 -189
  174. package/node_modules/pi-lens/dist/clients/widget-state.js +98 -0
  175. package/node_modules/pi-lens/dist/clients/word-index.js +5 -2
  176. package/node_modules/pi-lens/dist/clients/write-ordering-guard.js +7 -0
  177. package/node_modules/pi-lens/dist/index.js +27416 -24153
  178. package/node_modules/pi-lens/dist/mcp/server.js +5 -2
  179. package/node_modules/pi-lens/dist/tools/lens-diagnostic-mark.js +7 -1
  180. package/node_modules/pi-lens/dist/tools/lens-diagnostics.js +168 -53
  181. package/node_modules/pi-lens/dist/tools/lsp-diagnostics.js +244 -72
  182. package/node_modules/pi-lens/docs/audit1.md +5 -5
  183. package/node_modules/pi-lens/docs/dependencies.md +11 -0
  184. package/node_modules/pi-lens/docs/dispositions.md +52 -0
  185. package/node_modules/pi-lens/docs/environment-variables.md +2 -2
  186. package/node_modules/pi-lens/docs/features.md +4 -4
  187. package/node_modules/pi-lens/docs/globalconfig.md +3 -1
  188. package/node_modules/pi-lens/docs/language-coverage.md +1 -0
  189. package/node_modules/pi-lens/docs/pi-lens-fixer.md +53 -94
  190. package/node_modules/pi-lens/docs/pi-lens-investigator.md +43 -38
  191. package/node_modules/pi-lens/docs/pi-lens-retro.md +97 -0
  192. package/node_modules/pi-lens/docs/pi-lens-reviewer.md +53 -66
  193. package/node_modules/pi-lens/docs/release-qa-baseline.md +22 -0
  194. package/node_modules/pi-lens/docs/settings.md +7 -4
  195. package/node_modules/pi-lens/docs/tree-sitter_rules_catalog.md +1 -1
  196. package/node_modules/pi-lens/docs/usage.md +2 -0
  197. package/node_modules/pi-lens/package.json +1 -1
  198. package/node_modules/pi-lens/rules/tree-sitter-queries/typescript/sql-injection.yml +24 -12
  199. package/node_modules/pi-lens/scripts/analyze-pi-lens-logs.mjs +40 -0
  200. package/node_modules/pi-lens/skills/pi-lens-ast-grep/SKILL.md +15 -0
  201. package/node_modules/pi-lens/skills/pi-lens-lsp-navigation/SKILL.md +7 -0
  202. package/package.json +6 -11
  203. package/node_modules/@luxusai/pi-hindsight/CHANGELOG.md +0 -376
  204. package/node_modules/@luxusai/pi-hindsight/README.md +0 -92
  205. package/node_modules/@luxusai/pi-hindsight/docs/adr/001-memory-lifecycle-and-scope.md +0 -56
  206. package/node_modules/@luxusai/pi-hindsight/docs/adr/002-explicit-routing-strategy-seam.md +0 -169
  207. package/node_modules/@luxusai/pi-hindsight/docs/adr/003-tui-memory-mode-vocabulary.md +0 -107
  208. package/node_modules/@luxusai/pi-hindsight/docs/adr/004-lifeos-dual-bank-design.md +0 -89
  209. package/node_modules/@luxusai/pi-hindsight/docs/adr/005-domain-banks-and-agent-first-surface.md +0 -189
  210. package/node_modules/@luxusai/pi-hindsight/docs/assets/logos/pi-hindsight-logo-dark.webp +0 -0
  211. package/node_modules/@luxusai/pi-hindsight/docs/assets/logos/pi-hindsight-logo-dark@2x.webp +0 -0
  212. package/node_modules/@luxusai/pi-hindsight/docs/coding-memory-evaluation.md +0 -54
  213. package/node_modules/@luxusai/pi-hindsight/docs/compatibility.md +0 -75
  214. package/node_modules/@luxusai/pi-hindsight/docs/hindsight-core-functions.md +0 -300
  215. package/node_modules/@luxusai/pi-hindsight/docs/mission-and-mental-model-quality.md +0 -158
  216. package/node_modules/@luxusai/pi-hindsight/docs/next-opt-out-design.md +0 -164
  217. package/node_modules/@luxusai/pi-hindsight/docs/risky-memory-modes.md +0 -139
  218. package/node_modules/@luxusai/pi-hindsight/docs/starter-mental-model-suggestions.md +0 -74
  219. package/node_modules/@luxusai/pi-hindsight/docs/surface-reference.md +0 -261
  220. package/node_modules/@luxusai/pi-hindsight/extensions/banks/bank-operations.ts +0 -151
  221. package/node_modules/@luxusai/pi-hindsight/extensions/banks/bank-selection.ts +0 -18
  222. package/node_modules/@luxusai/pi-hindsight/extensions/banks/bank-settings-presenter.ts +0 -46
  223. package/node_modules/@luxusai/pi-hindsight/extensions/banks/bank-templates.ts +0 -383
  224. package/node_modules/@luxusai/pi-hindsight/extensions/banks/banking.ts +0 -240
  225. package/node_modules/@luxusai/pi-hindsight/extensions/banks/knowledge-page-seed.ts +0 -176
  226. package/node_modules/@luxusai/pi-hindsight/extensions/banks/retain-strategies.ts +0 -178
  227. package/node_modules/@luxusai/pi-hindsight/extensions/client/client-retry.ts +0 -41
  228. package/node_modules/@luxusai/pi-hindsight/extensions/client/client.ts +0 -386
  229. package/node_modules/@luxusai/pi-hindsight/extensions/client/fetch-compat.ts +0 -39
  230. package/node_modules/@luxusai/pi-hindsight/extensions/client/timeout.ts +0 -34
  231. package/node_modules/@luxusai/pi-hindsight/extensions/config/config-defaults.ts +0 -140
  232. package/node_modules/@luxusai/pi-hindsight/extensions/config/config-editing-model.ts +0 -61
  233. package/node_modules/@luxusai/pi-hindsight/extensions/config/config-editing-registry.ts +0 -925
  234. package/node_modules/@luxusai/pi-hindsight/extensions/config/config-field-paths.ts +0 -247
  235. package/node_modules/@luxusai/pi-hindsight/extensions/config/config-normalize.ts +0 -547
  236. package/node_modules/@luxusai/pi-hindsight/extensions/config/config-writer.ts +0 -480
  237. package/node_modules/@luxusai/pi-hindsight/extensions/config/config.ts +0 -182
  238. package/node_modules/@luxusai/pi-hindsight/extensions/config/setup-gate.ts +0 -86
  239. package/node_modules/@luxusai/pi-hindsight/extensions/imports/import-execute.ts +0 -1010
  240. package/node_modules/@luxusai/pi-hindsight/extensions/imports/import-parse.ts +0 -175
  241. package/node_modules/@luxusai/pi-hindsight/extensions/imports/import-plan.ts +0 -425
  242. package/node_modules/@luxusai/pi-hindsight/extensions/imports/import-presentation.ts +0 -210
  243. package/node_modules/@luxusai/pi-hindsight/extensions/imports/import-sessions.ts +0 -817
  244. package/node_modules/@luxusai/pi-hindsight/extensions/index.ts +0 -25
  245. package/node_modules/@luxusai/pi-hindsight/extensions/lifecycle/git-seed.ts +0 -319
  246. package/node_modules/@luxusai/pi-hindsight/extensions/lifecycle/memory-lifecycle-recall.ts +0 -196
  247. package/node_modules/@luxusai/pi-hindsight/extensions/lifecycle/memory-lifecycle-retain.ts +0 -189
  248. package/node_modules/@luxusai/pi-hindsight/extensions/lifecycle/memory-lifecycle-runtime.ts +0 -65
  249. package/node_modules/@luxusai/pi-hindsight/extensions/lifecycle/memory-lifecycle.ts +0 -260
  250. package/node_modules/@luxusai/pi-hindsight/extensions/lifecycle/mental-models.ts +0 -244
  251. package/node_modules/@luxusai/pi-hindsight/extensions/lifecycle/observation-scopes.ts +0 -59
  252. package/node_modules/@luxusai/pi-hindsight/extensions/lifecycle/recall-cleanup.ts +0 -82
  253. package/node_modules/@luxusai/pi-hindsight/extensions/lifecycle/recall-visibility.ts +0 -45
  254. package/node_modules/@luxusai/pi-hindsight/extensions/lifecycle/recall.ts +0 -331
  255. package/node_modules/@luxusai/pi-hindsight/extensions/lifecycle/retain-cursor.ts +0 -354
  256. package/node_modules/@luxusai/pi-hindsight/extensions/lifecycle/retain-job-builder.ts +0 -66
  257. package/node_modules/@luxusai/pi-hindsight/extensions/lifecycle/retain-receipts.ts +0 -132
  258. package/node_modules/@luxusai/pi-hindsight/extensions/lifecycle/retain.ts +0 -228
  259. package/node_modules/@luxusai/pi-hindsight/extensions/operations/memory-bank-template-operations.ts +0 -74
  260. package/node_modules/@luxusai/pi-hindsight/extensions/operations/memory-config-operations.ts +0 -16
  261. package/node_modules/@luxusai/pi-hindsight/extensions/operations/memory-control-operations.ts +0 -648
  262. package/node_modules/@luxusai/pi-hindsight/extensions/operations/memory-diagnostics-operations.ts +0 -105
  263. package/node_modules/@luxusai/pi-hindsight/extensions/operations/memory-identity.ts +0 -68
  264. package/node_modules/@luxusai/pi-hindsight/extensions/operations/memory-operation-service.ts +0 -71
  265. package/node_modules/@luxusai/pi-hindsight/extensions/operations/memory-operation-types.ts +0 -13
  266. package/node_modules/@luxusai/pi-hindsight/extensions/operations/memory-recall-operations.ts +0 -162
  267. package/node_modules/@luxusai/pi-hindsight/extensions/operations/memory-retain-operations.ts +0 -131
  268. package/node_modules/@luxusai/pi-hindsight/extensions/operations/memory-scope.ts +0 -104
  269. package/node_modules/@luxusai/pi-hindsight/extensions/operations/memory-session-operations.ts +0 -37
  270. package/node_modules/@luxusai/pi-hindsight/extensions/operations/operation-catalog.ts +0 -893
  271. package/node_modules/@luxusai/pi-hindsight/extensions/operations/reflect-presenter.ts +0 -35
  272. package/node_modules/@luxusai/pi-hindsight/extensions/operations/scope-migrate.ts +0 -177
  273. package/node_modules/@luxusai/pi-hindsight/extensions/operations/tools.ts +0 -7
  274. package/node_modules/@luxusai/pi-hindsight/extensions/queue/flush-presenter.ts +0 -21
  275. package/node_modules/@luxusai/pi-hindsight/extensions/queue/jsonl-queue-store.ts +0 -112
  276. package/node_modules/@luxusai/pi-hindsight/extensions/queue/queue-delivery.ts +0 -111
  277. package/node_modules/@luxusai/pi-hindsight/extensions/queue/queue-lock.ts +0 -214
  278. package/node_modules/@luxusai/pi-hindsight/extensions/queue/queue-operations.ts +0 -70
  279. package/node_modules/@luxusai/pi-hindsight/extensions/queue/queue.ts +0 -392
  280. package/node_modules/@luxusai/pi-hindsight/extensions/tui/bank-template-presentation.ts +0 -45
  281. package/node_modules/@luxusai/pi-hindsight/extensions/tui/commands.ts +0 -9
  282. package/node_modules/@luxusai/pi-hindsight/extensions/tui/guided-setup.ts +0 -860
  283. package/node_modules/@luxusai/pi-hindsight/extensions/tui/prefill-input.ts +0 -105
  284. package/node_modules/@luxusai/pi-hindsight/extensions/tui/setup-flow.ts +0 -189
  285. package/node_modules/@luxusai/pi-hindsight/extensions/tui/setup-server-probe.ts +0 -299
  286. package/node_modules/@luxusai/pi-hindsight/extensions/tui/setup-tui-actions.ts +0 -201
  287. package/node_modules/@luxusai/pi-hindsight/extensions/tui/setup-tui-facts.ts +0 -47
  288. package/node_modules/@luxusai/pi-hindsight/extensions/tui/setup-tui-render.ts +0 -239
  289. package/node_modules/@luxusai/pi-hindsight/extensions/tui/setup-tui-types.ts +0 -50
  290. package/node_modules/@luxusai/pi-hindsight/extensions/tui/setup-tui.ts +0 -259
  291. package/node_modules/@luxusai/pi-hindsight/extensions/tui/tool-presenters.ts +0 -77
  292. package/node_modules/@luxusai/pi-hindsight/extensions/types.ts +0 -534
  293. package/node_modules/@luxusai/pi-hindsight/extensions/utils/diagnostics.ts +0 -319
  294. package/node_modules/@luxusai/pi-hindsight/extensions/utils/messages.ts +0 -325
  295. package/node_modules/@luxusai/pi-hindsight/extensions/utils/sanitize.ts +0 -42
  296. package/node_modules/@luxusai/pi-hindsight/extensions/utils/session-memory-meta.ts +0 -244
  297. package/node_modules/@luxusai/pi-hindsight/extensions/utils/session-operations.ts +0 -56
  298. package/node_modules/@luxusai/pi-hindsight/extensions/utils/session.ts +0 -50
  299. package/node_modules/@luxusai/pi-hindsight/extensions/utils/status-fields.ts +0 -167
  300. package/node_modules/@luxusai/pi-hindsight/extensions/utils/status-health.ts +0 -207
  301. package/node_modules/@luxusai/pi-hindsight/extensions/utils/status.ts +0 -120
  302. package/node_modules/@luxusai/pi-hindsight/extensions/version.ts +0 -8
  303. package/node_modules/@luxusai/pi-hindsight/package.json +0 -118
  304. package/node_modules/@luxusai/pi-hindsight/skills/hindsight-memory-doctor/SKILL.md +0 -75
@@ -31,11 +31,66 @@
31
31
  * all runners, so an eslint, biome-check, or ast-grep security-rule (hardcoded
32
32
  * secret, CVE) finding can land here too. Only a language-server verdict is
33
33
  * actually invalidated by an IMPORT changing; an ast-grep secret match doesn't stop
34
- * being true because a file it imports was edited. The sweep therefore demotes an
35
- * entry only when its recorded `sources` (`InlineBlockerRecord.sources`) are ALL
36
- * `"lsp"` — a mixed or non-LSP-sourced blocker is left fully authoritative, the same
37
- * fail-closed shape `retireInlineBlockerOnConfirmedClean` already uses for
38
- * provenance it can't yet reason about.
34
+ * being true because a file it imports was edited.
35
+ *
36
+ * Self-drift axis (#1561 remainder). That reasoning covers the import axis and
37
+ * nothing else, but the original gate discarded BOTH axes for a non-LSP record:
38
+ * entries whose `sources` were not all `"lsp"` skipped the check entirely. A
39
+ * tree-sitter verdict about F's own syntax is emphatically invalidated by F's own
40
+ * bytes changing, and no other path could clear it —
41
+ * `retireInlineBlockerOnConfirmedClean` requires `coveredSources` to be a superset
42
+ * of the record's sources, `coveredSourcesForCheck` builds that set from the LSP
43
+ * server registry, and no registered server has id `"tree-sitter"`, so the retire
44
+ * could never fire however many clean checks ran. Live shape: a
45
+ * `ts-incomplete-assertion` blocker re-served every turn for the rest of a session
46
+ * against a file already proven clean by grep, LSP, and a passing test.
47
+ *
48
+ * So the sweep now runs over every entry that HAS recorded provenance, and picks
49
+ * the axis from the sources: all-`"lsp"` gets the own-file-plus-forward-imports
50
+ * walk, anything else (tree-sitter, ast-grep, mixed, or `"unknown"`) gets an
51
+ * own-file-only check. A record with NO recorded sources stays fail-closed and
52
+ * untouched, matching the test `retireInlineBlockerOnConfirmedClean` applies before
53
+ * it will retire.
54
+ *
55
+ * The self axis is CONTENT-CONFIRMED and RE-ARMING, and those two properties are
56
+ * why it carries its own `"self-drift"` reason rather than reusing
57
+ * `"dependency-drift"` (#2982 review).
58
+ *
59
+ * Content-confirmed: mtime moving is not evidence a byte moved. A `touch`, a
60
+ * `git checkout` restoring identical bytes, or a no-op formatter pass all move
61
+ * it. #2449 round 2 F7 settled this for `observed-mutation.ts` ("mtime-only
62
+ * drift has to be confirmed against content before anything is replayed"), and
63
+ * it binds harder here, where an unconfirmed demotion walks a finding out of the
64
+ * authoritative channel. `detectSelfDrift` applies that rule at both tiers:
65
+ * `size` first, then a sha256 of the bytes when size cannot separate a
66
+ * one-character edit from a `touch`. A same-LENGTH change is the common shape,
67
+ * not an exotic one (a renamed identifier of equal length, a flipped comparison,
68
+ * a changed digit), so a size-only tier would leave a genuinely changed record
69
+ * authoritative, which is #2982 arriving from the other side. The baseline is
70
+ * carried by `PipelineResult` beside the inline-blocker evidence and stamped
71
+ * synchronously by `recordInlineBlockers`. Every bound that expires, and every missing baseline, yields
72
+ * `unverifiable`, which changes no state in either direction and is counted so
73
+ * it is visible rather than silent.
74
+ *
75
+ * Re-arming: `setInlineBlockerSelfDriftStale` re-derives the verdict every turn
76
+ * and un-demotes a record whose bytes come back, exactly as
77
+ * `setInlineBlockerPastEofStale` does for a transient shrink-then-restore. That
78
+ * is what lets this axis stay OUT of the #1950 delivery cap. The cap retires a
79
+ * record permanently after `DEPENDENCY_DRIFT_MAX_DELIVERIES` stale deliveries,
80
+ * which suits recoverable LSP dependency drift; a self-drift record can carry
81
+ * ast-grep or tree-sitter security provenance, and retiring one because an
82
+ * advisory was shown three times would walk a hardcoded secret out of turn-end
83
+ * rendering with no dispatch to re-raise it. A record that heals on its own
84
+ * needs no bounded-noise retirement.
85
+ *
86
+ * Defect shape 24 is satisfied by composition, not by reusing a string: the new
87
+ * reason has per-writer semantics (re-arm, no cap), and like every sibling gate
88
+ * it never touches a demotion another gate made and never heals one it did not
89
+ * make. The commit gate is unaffected either way. `updateGitGuardStatus` counts
90
+ * `getInlineBlockersSnapshot().length` with no `stale` filter, so a demoted
91
+ * record still gates a commit; `tests/clients/blocker-freshness.test.ts` pins
92
+ * that, because it is what makes widening this sweep to security provenance
93
+ * safe at all.
39
94
  *
40
95
  * Resolution boundary (#1631 review F8): `extractForwardImportPaths` parses static
41
96
  * import/require syntax via tree-sitter. It does not, and cannot, resolve a
@@ -76,13 +131,29 @@
76
131
  * with this module is the one number both caps retire against,
77
132
  * `DEPENDENCY_DRIFT_MAX_DELIVERIES`, rather than inventing a second one.
78
133
  */
134
+ import { createHash } from "node:crypto";
79
135
  import * as fs from "node:fs";
136
+ import { bounded } from "./deadline-utils.js";
137
+ import { recordDegradationOnce } from "./degradation-ledger.js";
138
+ import { HOOK_WALL_BUDGET_MS } from "./hook-budgets.js";
80
139
  import { normalizeEphemeralMapKey } from "./path-utils.js";
140
+ import { toRunnerDisplayPath } from "./dispatch/runner-context.js";
81
141
  import { resolveImportToFiles } from "./review-graph/import-resolvers.js";
82
142
  import { getSharedTreeSitterClient, resolveTreeSitterLanguage, } from "./tree-sitter-shared.js";
83
143
  import { TreeSitterSymbolExtractor } from "./tree-sitter-symbol-extractor.js";
84
144
  /** See the module doc's "Delivery cap (#1950)" section above. */
85
145
  export const DEPENDENCY_DRIFT_MAX_DELIVERIES = 3;
146
+ /**
147
+ * #2982: total bytes the self-drift axis may hash in ONE sweep.
148
+ *
149
+ * Each read is individually bounded by `bounded()`, but a turn with many
150
+ * non-LSP blockers would still issue many whole-file reads on a hook path, which
151
+ * is defect shape 9 (bounded on one axis, growing on another). Once a sweep has
152
+ * spent this budget the remaining same-size records are reported `unverifiable`
153
+ * and left exactly as they are, the same fail-closed direction as a read that
154
+ * times out.
155
+ */
156
+ const SELF_DRIFT_HASH_BUDGET_BYTES = 8 * 1024 * 1024;
86
157
  /**
87
158
  * Upper bound on the number of forward imports stat-checked for a single blocker.
88
159
  * Blocking entries are rare, but one file can import many modules; the sweep must
@@ -219,6 +290,95 @@ async function statMtimeMs(filePath) {
219
290
  return undefined;
220
291
  }
221
292
  }
293
+ /**
294
+ * Confirm a self-axis record against content, not mtime alone.
295
+ *
296
+ * mtime moving is not evidence that content changed: a `touch`, a `git
297
+ * checkout` restoring identical bytes, a no-op formatter pass, or a `chmod`
298
+ * all move it while every byte stays put. #2449 round 2 F7 settled this for
299
+ * `observed-mutation.ts` ("a `touch` bumps it without a byte moving, so
300
+ * mtime-only drift has to be confirmed against content before anything is
301
+ * replayed"), and the same reasoning governs here, where an unconfirmed
302
+ * demotion would walk a finding out of the authoritative channel.
303
+ *
304
+ * Three gates, in cost order. `size` decides most cases from the stat already
305
+ * taken, and it runs BEFORE the mtime gate: an out-of-band write (a formatter,
306
+ * a checkout) can land at-or-before the `recordedAtMs` baseline, so mtime alone
307
+ * is blind to the own-file drift this axis exists to catch — the size gate fires
308
+ * regardless of the mtime relationship. When the size matches, the mtime gate is
309
+ * a fast path that skips the expensive hash tier for non-LSP records (size same
310
+ * AND mtime never moved → the file almost certainly did not change, so we do
311
+ * not read and hash every unchanged file on the hook path). All-LSP records
312
+ * with an available hash baseline force the hash tier after the size check, so
313
+ * a same-size rewrite at-or-before the baseline cannot remain authoritative.
314
+ * When hashing is not forced and mtime moved, the hash separates a one-character
315
+ * edit from a `touch`, reading the bytes and comparing against the baseline
316
+ * `setInlineBlockerContentBaseline` attached off the dispatch path.
317
+ *
318
+ * Both tiers fail toward `"unverifiable"`, never toward `"drift"`: a bound that
319
+ * expires, a baseline that never landed, or a file past the per-sweep hash
320
+ * budget all leave the record exactly as it is. Demoting on a verdict the tier
321
+ * could not actually reach is the failure this function exists to prevent, and
322
+ * `bounded()` returning `undefined` on timeout makes that an easy mistake to
323
+ * write, so every bound's result is checked explicitly.
324
+ */
325
+ async function detectSelfDrift(args) {
326
+ const boundOptions = (label) => ({
327
+ ms: HOOK_WALL_BUDGET_MS.turn_end,
328
+ signal: args.signal,
329
+ hook: "turn_end",
330
+ label,
331
+ });
332
+ // EVERY bound that expires yields `undefined`, and every one of those maps to
333
+ // `unverifiable`, never to drift. A timed-out stat compared against the
334
+ // recorded size would read as a size change and demote the record, which is
335
+ // the exact failure this tier exists to prevent.
336
+ const stat = await bounded(fs.promises.stat(args.filePath), boundOptions("selfDriftStat"));
337
+ if (stat === undefined)
338
+ return { verdict: "unverifiable", unverifiableReason: "stat-unavailable" };
339
+ // Size gate FIRST, before the mtime gate. mtime is a blind signal for the
340
+ // out-of-band-rewrite case: an external write (a formatter, a checkout) can
341
+ // land at-or-before the `recordedAtMs` baseline, so `freshnessFromMtime`
342
+ // reports the own file unchanged even though its bytes differ. Size is cheap
343
+ // content confirmation from the same stat, and it fires regardless of the
344
+ // mtime relationship — the all-LSP own-file case the mtime gate alone misses.
345
+ if (args.recordedSize === undefined)
346
+ return { verdict: "unverifiable", unverifiableReason: "missing-baseline" };
347
+ if (stat.size !== args.recordedSize)
348
+ return { verdict: "drift" };
349
+ // Size matches. Non-LSP records retain the mtime fast path. All-LSP records
350
+ // with a hash baseline force content confirmation because a same-size rewrite
351
+ // can land at-or-before the baseline.
352
+ const freshness = freshnessFromMtime({
353
+ mtimeMs: stat.mtimeMs,
354
+ referenceMs: args.recordedAtMs,
355
+ });
356
+ if (!args.forceContent && freshness.verdict !== "stale")
357
+ return { verdict: "unchanged" };
358
+ // Same length AND mtime moved. Only the hash can separate a one-character
359
+ // edit from a `touch`, and a same-length edit is the common shape, not an
360
+ // exotic one. Forced all-LSP confirmation reaches this tier even when mtime
361
+ // did not move.
362
+ if (args.recordedHash === undefined)
363
+ return { verdict: "unverifiable", unverifiableReason: "missing-baseline" };
364
+ // Defect shape 9: each read is individually bounded, but N blockers in one
365
+ // turn is an unbounded AGGREGATE read on a hook path. The per-sweep byte
366
+ // budget bounds the other axis; past it the record is unverifiable, not
367
+ // drifted.
368
+ if (stat.size > args.budget.bytesLeft) {
369
+ args.budget.exhausted = true;
370
+ return {
371
+ verdict: "unverifiable",
372
+ unverifiableReason: "hash-budget-exhausted",
373
+ };
374
+ }
375
+ const content = await bounded(fs.promises.readFile(args.filePath), boundOptions("selfDriftHash"));
376
+ if (content === undefined)
377
+ return { verdict: "unverifiable", unverifiableReason: "hash-unavailable" };
378
+ args.budget.bytesLeft -= content.byteLength;
379
+ const hash = createHash("sha256").update(content).digest("hex");
380
+ return { verdict: hash === args.recordedHash ? "unchanged" : "drift" };
381
+ }
222
382
  /**
223
383
  * Resolve `filePath`'s forward imports and stat each, returning the imports that
224
384
  * exist together with their mtime. Shared by the inline-blocker sweep and the
@@ -268,14 +428,18 @@ export async function collectForwardImportMtimes(cwd, filePath, resolveForwardIm
268
428
  // re-export preserves every existing importer's path.
269
429
  export { MTIME_DRIFT_TOLERANCE_MS } from "./freshness.js";
270
430
  import { freshnessFromMtime } from "./freshness.js";
271
- async function detectDrift(cwd, filePath, recordedAtMs, resolveForwardImports, turnIndex) {
431
+ async function detectDrift(cwd, filePath, recordedAtMs, resolveForwardImports, turnIndex,
432
+ /** Skip the own-file mtime tier after content confirms it is unchanged. */
433
+ skipOwnFile = false) {
272
434
  const drifted = [];
273
- const ownFreshness = freshnessFromMtime({
274
- mtimeMs: await statMtimeMs(filePath),
275
- referenceMs: recordedAtMs,
276
- });
277
- if (ownFreshness.verdict === "stale")
278
- drifted.push(filePath);
435
+ if (!skipOwnFile) {
436
+ const ownFreshness = freshnessFromMtime({
437
+ mtimeMs: await statMtimeMs(filePath),
438
+ referenceMs: recordedAtMs,
439
+ });
440
+ if (ownFreshness.verdict === "stale")
441
+ drifted.push(filePath);
442
+ }
279
443
  const { mtimes, truncated } = await collectForwardImportMtimes(cwd, filePath, resolveForwardImports, turnIndex);
280
444
  for (const { path: depPath, mtimeMs } of mtimes) {
281
445
  const verdict = freshnessFromMtime({
@@ -290,19 +454,53 @@ async function detectDrift(cwd, filePath, recordedAtMs, resolveForwardImports, t
290
454
  /**
291
455
  * Whether a population entry is eligible for the drift check at all — the same
292
456
  * three gates the main sweep loop applies (not already stale, has a timestamp
293
- * baseline, all-`"lsp"` sources). Factored out so the #1790 review F5 dedup
457
+ * baseline, HAS recorded provenance). Factored out so the #1790 review F5 dedup
294
458
  * decision below (chain vs. separate row) asks the IDENTICAL question the main
295
459
  * loop will ask, rather than a second hand-written approximation of it that
296
460
  * could drift from the real gates.
461
+ *
462
+ * The third gate reads "has provenance", not "is all-`lsp`": a non-LSP record is
463
+ * now eligible for the self-drift axis (own file only, no import walk). Widening
464
+ * it here and in the loop together is the point of the shared predicate — the
465
+ * loop still decides WHICH axis each eligible entry gets.
297
466
  */
298
467
  function isEligibleForDriftCheck(entry) {
299
468
  if (entry.stale)
300
469
  return false;
301
470
  if (entry.recordedAtMs === undefined)
302
471
  return false;
303
- return (entry.sources !== undefined &&
304
- entry.sources.length > 0 &&
305
- entry.sources.every((source) => source === "lsp"));
472
+ return entry.sources !== undefined && entry.sources.length > 0;
473
+ }
474
+ /**
475
+ * All recorded sources are `"lsp"` — the record gets the full own-file PLUS
476
+ * forward-import walk. Anything else (tree-sitter, ast-grep, mixed, `"unknown"`)
477
+ * gets the self-only axis. One spelling, shared by the sweep loop and the
478
+ * chain decision below so the two can never disagree about which axis an entry
479
+ * is on.
480
+ */
481
+ function isAllLspSourced(sources) {
482
+ return (sources !== undefined &&
483
+ sources.length > 0 &&
484
+ sources.every((source) => source === "lsp"));
485
+ }
486
+ /**
487
+ * Whether a duplicated widget row may be CHAINED onto this inline entry's single
488
+ * drift check instead of being given its own population row (#1790 review F5).
489
+ *
490
+ * Strictly stronger than {@link isEligibleForDriftCheck} since the self-drift
491
+ * axis landed, and deliberately so. A widget row from
492
+ * `getWidgetBlockingFilesForSweep` is pure-LSP by construction, so the check that
493
+ * speaks for it must walk forward imports. An inline entry on the self-only axis
494
+ * never consults imports, so chaining a widget row onto it would silently drop
495
+ * that row's import axis — the #1790 ghost, in a new disguise: the inline entry
496
+ * reports `kept`, the widget row is never independently checked, and its
497
+ * `isBlocking` stays true for a dependency that drifted.
498
+ *
499
+ * Such an inline entry is still drift-checked on its own axis. It just cannot
500
+ * answer another store's question.
501
+ */
502
+ function canSubsumeLspWidgetRow(entry) {
503
+ return isEligibleForDriftCheck(entry) && isAllLspSourced(entry.sources);
306
504
  }
307
505
  /**
308
506
  * Freshness sweep over every blocking row the widget currently serves — the cached
@@ -347,6 +545,9 @@ export async function sweepInlineBlockerFreshness(runtime, cwd, options) {
347
545
  revalidated: 0,
348
546
  alreadyStale: 0,
349
547
  truncatedImports: 0,
548
+ selfHealed: 0,
549
+ selfUnverifiable: 0,
550
+ hashBudgetExhausted: 0,
350
551
  };
351
552
  const resolveForwardImports = options?.resolveForwardImports ?? extractForwardImportPaths;
352
553
  let turnIndex;
@@ -368,7 +569,11 @@ export async function sweepInlineBlockerFreshness(runtime, cwd, options) {
368
569
  stale: entry.stale ?? false,
369
570
  recordedAtMs: entry.recordedAtMs,
370
571
  sources: entry.sources,
572
+ staleReason: entry.staleReason,
573
+ recordedSize: entry.recordedSize,
574
+ recordedHash: entry.recordedHash,
371
575
  demote: () => runtime.markInlineBlockerStale(entry.filePath, "dependency-drift"),
576
+ setSelfDrift: (isSelfDrift) => runtime.setInlineBlockerSelfDriftStale(entry.filePath, isSelfDrift),
372
577
  }));
373
578
  // #1790 review F2: dedup key is `normalizeEphemeralMapKey`, not
374
579
  // `normalizeMapKey` — the latter realpaths a live file and walks up the
@@ -390,16 +595,22 @@ export async function sweepInlineBlockerFreshness(runtime, cwd, options) {
390
595
  // drop, not a merge. The main loop below short-circuits BEFORE ever
391
596
  // calling `demote()` for an already-stale entry (the one-way
392
597
  // dependency-drift latch — a forever-ghost once it fires once), an
393
- // unstamped legacy record, or a non-LSP/mixed-sources entry (e.g. an
394
- // unrelated ast-grep finding on the same file). Any of those swallows a
395
- // chained widget demote even though the widget row is pure-LSP by
396
- // construction (`getWidgetBlockingFilesForSweep` only emits LSP-sourced
397
- // rows) and carries its OWN baseline. Eligibility belongs to the STORE
398
- // the row came from, not the file path two stores happen to share — so
399
- // only chain when the inline entry would itself reach `demote()`;
400
- // otherwise give the widget row its own population entry so its own
401
- // gates and its own drift check decide its own fate.
402
- if (inlineEntry && isEligibleForDriftCheck(inlineEntry)) {
598
+ // unstamped legacy record, or a record with no provenance at all. Any of
599
+ // those swallows a chained widget demote even though the widget row is
600
+ // pure-LSP by construction (`getWidgetBlockingFilesForSweep` only emits
601
+ // LSP-sourced rows) and carries its OWN baseline. Eligibility belongs to
602
+ // the STORE the row came from, not the file path two stores happen to
603
+ // share — so only chain when the inline entry would itself reach
604
+ // `demote()`; otherwise give the widget row its own population entry so
605
+ // its own gates and its own drift check decide its own fate.
606
+ //
607
+ // Since the self-drift axis landed, "would reach `demote()`" is no longer
608
+ // enough: a non-LSP inline entry IS drift-checked now, but only against
609
+ // its own file. It never consults imports, so it cannot speak for a
610
+ // pure-LSP widget row whose dependency drifted. `canSubsumeLspWidgetRow`
611
+ // carries that stronger test; the unrelated-ast-grep-finding case in the
612
+ // paragraph above is now excluded by the axis, not by ineligibility.
613
+ if (inlineEntry && canSubsumeLspWidgetRow(inlineEntry)) {
403
614
  // #1790 review F1: a duplicated path is counted and drift-checked ONCE
404
615
  // (via the inline entry above), but BOTH stores must record the
405
616
  // verdict — `markInlineBlockerStale` only ever touches
@@ -423,12 +634,31 @@ export async function sweepInlineBlockerFreshness(runtime, cwd, options) {
423
634
  recordedAtMs: extra.recordedAtMs,
424
635
  sources: ["lsp"],
425
636
  demote: extra.demote,
637
+ // A widget row is pure-LSP by construction, so it takes the import
638
+ // axis and never reaches the self axis. No `setSelfDrift` (#2982):
639
+ // the widget store has no re-arming self-drift setter, and an entry
640
+ // without one is not eligible for that axis.
641
+ staleReason: undefined,
642
+ recordedSize: undefined,
643
+ recordedHash: undefined,
426
644
  });
427
645
  }
428
646
  counts.total = population.length;
647
+ // One budget for the whole sweep (defect shape 9), consumed by the self
648
+ // axis's hash tier as it goes.
649
+ const hashBudget = {
650
+ bytesLeft: SELF_DRIFT_HASH_BUDGET_BYTES,
651
+ exhausted: false,
652
+ };
429
653
  for (const entry of population) {
430
654
  try {
431
- if (entry.stale) {
655
+ // #2982: a latched demotion belongs to the gate that made it and this
656
+ // loop leaves it alone. Its OWN self-drift demotion is re-armable, so
657
+ // it is re-derived every turn instead: bytes that come back must
658
+ // un-demote the record, which is what lets the self axis skip the
659
+ // #1950 delivery cap rather than retire a security finding for good.
660
+ const selfDriftDemoted = entry.stale && entry.staleReason === "self-drift";
661
+ if (entry.stale && !selfDriftDemoted) {
432
662
  counts.alreadyStale += 1;
433
663
  continue;
434
664
  }
@@ -439,17 +669,154 @@ export async function sweepInlineBlockerFreshness(runtime, cwd, options) {
439
669
  counts.kept += 1;
440
670
  continue;
441
671
  }
442
- // #1631 review F4: fail-closed on non-LSP (or unknown) provenance. Import
443
- // drift only invalidates a language-server verdict; a mixed or non-`"lsp"`
444
- // `sources` list is kept at full authority rather than demoted.
445
- const isLspSourced = entry.sources !== undefined &&
446
- entry.sources.length > 0 &&
447
- entry.sources.every((source) => source === "lsp");
448
- if (!isLspSourced) {
672
+ // #1631 review F4 kept every non-`"lsp"` record at full authority. Its
673
+ // stated reason is about the IMPORT axis only — "an ast-grep secret match
674
+ // doesn't stop being true because a file it imports was edited" — but the
675
+ // gate discarded the self axis with it. A record with NO provenance at all
676
+ // stays fail-closed here, the same test
677
+ // `retireInlineBlockerOnConfirmedClean` applies before it will retire.
678
+ const recordedSources = entry.sources !== undefined && entry.sources.length > 0
679
+ ? entry.sources
680
+ : undefined;
681
+ if (recordedSources === undefined) {
449
682
  counts.kept += 1;
450
683
  continue;
451
684
  }
452
- const { drifted, truncated } = await detectDrift(cwd, entry.filePath, entry.recordedAtMs, resolveForwardImports, turnIndex);
685
+ // #2982 remainder: the record's OWN file is checked via the content-
686
+ // confirmed self-drift axis (size → hash, re-arming) for records that
687
+ // have a re-arming setter — shared by the non-LSP self axis below and
688
+ // the all-LSP axis. The mtime-only own-file check in `detectDrift` is
689
+ // blind to the out-of-band-rewrite case (an external write can land at-
690
+ // or-before the `recordedAtMs` baseline), so the content axis owns the
691
+ // own-file verdict when it can decide. One bounded() call site for both
692
+ // axes — the #2523 registry requires a single occurrence, so the two
693
+ // branches must not each wrap the identical call. Demotion, not deletion
694
+ // (#1419): a demoted entry is re-served in the advisory channel marked
695
+ // `[stale — re-run to confirm]` and retires through the existing #1950
696
+ // delivery cap.
697
+ const setSelfDrift = entry.setSelfDrift;
698
+ const isLspSourced = isAllLspSourced(recordedSources);
699
+ let ownVerdict;
700
+ if (setSelfDrift) {
701
+ // The outer bound too: an expired one yields `undefined`, and that
702
+ // means "could not decide", never "changed".
703
+ const hashBudgetWasExhausted = hashBudget.exhausted;
704
+ const selfDrift = (await bounded(detectSelfDrift({
705
+ filePath: entry.filePath,
706
+ recordedAtMs: entry.recordedAtMs,
707
+ recordedSize: entry.recordedSize,
708
+ recordedHash: entry.recordedHash,
709
+ forceContent: isLspSourced &&
710
+ entry.recordedSize !== undefined &&
711
+ entry.recordedHash !== undefined,
712
+ signal: options?.signal,
713
+ budget: hashBudget,
714
+ }), {
715
+ ms: HOOK_WALL_BUDGET_MS.turn_end,
716
+ signal: options?.signal,
717
+ hook: "turn_end",
718
+ label: "detectSelfDrift",
719
+ })) ?? {
720
+ verdict: "unverifiable",
721
+ unverifiableReason: "bound-expired",
722
+ };
723
+ ownVerdict = selfDrift.verdict;
724
+ if (ownVerdict === "unverifiable") {
725
+ // Decide nothing. Leave the record in whatever state it holds.
726
+ counts.selfUnverifiable += 1;
727
+ const reason = selfDrift.unverifiableReason ??
728
+ (hashBudgetWasExhausted || hashBudget.exhausted
729
+ ? "hash-budget-exhausted"
730
+ : "hash-unavailable");
731
+ recordDegradationOnce({
732
+ kind: "self-drift-unverifiable",
733
+ subject: `inline-blocker:${toRunnerDisplayPath(cwd, entry.filePath)}#${reason}`,
734
+ reason: `the self-drift check was unverifiable: ${reason}`,
735
+ });
736
+ if (reason === "hash-budget-exhausted") {
737
+ recordDegradationOnce({
738
+ kind: "self-drift-hash-budget-exhausted",
739
+ subject: `inline-blocker:${toRunnerDisplayPath(cwd, entry.filePath)}`,
740
+ reason: "the aggregate self-drift hash budget was exhausted",
741
+ });
742
+ }
743
+ }
744
+ }
745
+ if (!isLspSourced) {
746
+ // The self axis. Content-confirmed, re-arming, and outside the
747
+ // #1950 delivery cap. A store with no re-arming setter is not
748
+ // eligible for it and stays authoritative (fail-closed).
749
+ if (!setSelfDrift || ownVerdict === undefined) {
750
+ counts.kept += 1;
751
+ continue;
752
+ }
753
+ if (ownVerdict === "unverifiable") {
754
+ if (selfDriftDemoted)
755
+ counts.alreadyStale += 1;
756
+ else
757
+ counts.kept += 1;
758
+ continue;
759
+ }
760
+ const shouldDemote = ownVerdict === "drift";
761
+ const transitioned = setSelfDrift(shouldDemote);
762
+ if (shouldDemote) {
763
+ if (transitioned)
764
+ counts.revalidated += 1;
765
+ else
766
+ counts.alreadyStale += 1;
767
+ }
768
+ else if (transitioned) {
769
+ counts.selfHealed += 1;
770
+ }
771
+ else {
772
+ counts.kept += 1;
773
+ }
774
+ continue;
775
+ }
776
+ // The LSP axis (#1618). The own file is checked by the content axis
777
+ // above when a baseline was captured; without one it falls back to the
778
+ // mtime check (the pre-#2982 behavior, fail-open). The forward-import
779
+ // walk stays on the one-way dependency-drift axis.
780
+ let contentDecidedOwnFile = false;
781
+ if (setSelfDrift !== undefined &&
782
+ ownVerdict !== undefined &&
783
+ entry.recordedSize !== undefined) {
784
+ if (ownVerdict === "drift") {
785
+ const transitioned = setSelfDrift(true);
786
+ if (transitioned)
787
+ counts.revalidated += 1;
788
+ else
789
+ counts.alreadyStale += 1;
790
+ // Own file drifted; the record is out of the authoritative
791
+ // channel, so the import walk is moot this turn.
792
+ continue;
793
+ }
794
+ if (ownVerdict === "unchanged") {
795
+ // Content confirms the own file is unchanged: skip the mtime check
796
+ // below (a `touch` must not demote a still-valid record), and
797
+ // un-demote a record self-drifted last turn.
798
+ contentDecidedOwnFile = true;
799
+ const transitioned = setSelfDrift(false);
800
+ if (transitioned)
801
+ counts.selfHealed += 1;
802
+ }
803
+ else {
804
+ // "unverifiable": the content axis could not decide (a stat that
805
+ // failed, or a size that matched but whose hash never landed). Fall
806
+ // back to the mtime check below.
807
+ if (selfDriftDemoted) {
808
+ counts.alreadyStale += 1;
809
+ continue;
810
+ }
811
+ }
812
+ }
813
+ // Forward-import walk (one-way dependency-drift). Skip the own file ONLY
814
+ // when the content axis decided it "unchanged" — the one case where the
815
+ // mtime check would wrongly demote on a `touch`. A widget row (no setter)
816
+ // and an inline entry without a baseline never reach the content axis, so
817
+ // they still get the mtime own-file check (fail-open).
818
+ const skipOwnFile = contentDecidedOwnFile;
819
+ const { drifted, truncated } = await detectDrift(cwd, entry.filePath, entry.recordedAtMs, resolveForwardImports, turnIndex, skipOwnFile);
453
820
  if (truncated)
454
821
  counts.truncatedImports += 1;
455
822
  if (drifted.length > 0) {
@@ -465,5 +832,6 @@ export async function sweepInlineBlockerFreshness(runtime, cwd, options) {
465
832
  counts.kept += 1;
466
833
  }
467
834
  }
835
+ counts.hashBudgetExhausted = hashBudget.exhausted ? 1 : 0;
468
836
  return counts;
469
837
  }
@@ -41,6 +41,7 @@ import { logLatency } from "./latency-logger.js";
41
41
  import { emitBounded } from "./bounded-telemetry.js";
42
42
  import { recordDegradationOnce } from "./degradation-ledger.js";
43
43
  import { createSingleFlight } from "./single-flight.js";
44
+ import { agentBehaviorClient } from "./agent-behavior-client.js";
44
45
  /**
45
46
  * The clients of a COMPLETED load, or `null` while none has completed.
46
47
  *
@@ -56,6 +57,10 @@ import { createSingleFlight } from "./single-flight.js";
56
57
  * by the single-flight primitive's identity-guarded release).
57
58
  */
58
59
  let residentClients = null;
60
+ /** Behaviour history remains available before analyzer bootstrap completes. */
61
+ export function getAgentBehaviorClient() {
62
+ return agentBehaviorClient;
63
+ }
59
64
  /**
60
65
  * Set by {@link markAnalyzerBootstrapShutdown} when the PRIMARY session tears
61
66
  * down. It refuses NEW loads only — a demand that already joined the in-flight
@@ -233,7 +238,7 @@ async function buildBootstrapClients() {
233
238
  load("opengrep", async () => new (await import("./opengrep-client.js")).OpengrepClient()),
234
239
  // The process-wide singleton, for the same reason as "go" above.
235
240
  load("rust", async () => (await import("./rust-client.js")).rustClient),
236
- load("agent-behavior", async () => new (await import("./agent-behavior-client.js")).AgentBehaviorClient()),
241
+ load("agent-behavior", async () => (await import("./agent-behavior-client.js")).agentBehaviorClient),
237
242
  loadList("dead-code", async () => (await import("./dead-code-client.js")).getDeadCodeClients()),
238
243
  ]);
239
244
  if (failures.length > 0)