@herbertgao/pi-extensions 2026.8.14 → 2026.8.15

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 (231) hide show
  1. package/README.md +16 -13
  2. package/THIRD_PARTY_NOTICES.md +0 -1
  3. package/node_modules/@czottmann/pi-automode/CHANGELOG.md +8 -1
  4. package/node_modules/@czottmann/pi-automode/README.md +4 -0
  5. package/node_modules/@czottmann/pi-automode/extensions/auto-mode/classifier.ts +71 -9
  6. package/node_modules/@czottmann/pi-automode/package.json +1 -1
  7. package/node_modules/@herbertgao/pi-bark/LICENSE +21 -0
  8. package/node_modules/@herbertgao/pi-bark/README.md +44 -0
  9. package/node_modules/@herbertgao/pi-bark/assets/pi-icon.png +0 -0
  10. package/node_modules/@herbertgao/pi-bark/package.json +53 -0
  11. package/node_modules/@herbertgao/pi-bark/src/index.ts +178 -0
  12. package/node_modules/@juicesharp/rpiv-ask-user-question/package.json +2 -2
  13. package/node_modules/@narumitw/pi-btw/README.md +125 -88
  14. package/node_modules/@narumitw/pi-btw/dist/index.ts +140 -45
  15. package/node_modules/@narumitw/pi-btw/dist/index.ts.map +2 -2
  16. package/node_modules/@narumitw/pi-btw/package.json +5 -8
  17. package/node_modules/@narumitw/pi-btw/src/btw.ts +31 -26
  18. package/node_modules/@narumitw/pi-btw/src/fullscreen-ui.ts +106 -17
  19. package/node_modules/@narumitw/pi-btw/src/menu.ts +34 -2
  20. package/node_modules/@narumitw/pi-btw/src/settings.ts +16 -0
  21. package/node_modules/@tifan/pi-handoff/README.md +36 -13
  22. package/node_modules/@tifan/pi-handoff/package.json +2 -2
  23. package/node_modules/@tifan/pi-handoff/src/index.ts +160 -4
  24. package/node_modules/@tifan/pi-recap/README.md +1 -1
  25. package/node_modules/@tifan/pi-recap/package.json +2 -2
  26. package/node_modules/@tifan/pi-recap/src/index.ts +15 -0
  27. package/node_modules/@tifan/pi-rename/README.md +42 -21
  28. package/node_modules/@tifan/pi-rename/package.json +1 -1
  29. package/node_modules/@tifan/pi-rename/src/index.ts +62 -11
  30. package/node_modules/@tifan/pi-rename/src/language.ts +18 -0
  31. package/node_modules/@tifan/pi-rename/src/models.ts +69 -13
  32. package/node_modules/@tifan/pi-rename/src/naming.ts +36 -7
  33. package/node_modules/@tifan/pi-rename/src/sanitize.ts +22 -14
  34. package/node_modules/pi-lens/CHANGELOG.md +320 -0
  35. package/node_modules/pi-lens/config/dependency-cruiser-eager-allowlist.json +79 -0
  36. package/node_modules/pi-lens/dist/clients/actionable-warnings-logger.js +16 -1
  37. package/node_modules/pi-lens/dist/clients/ast-grep-client.js +13 -6
  38. package/node_modules/pi-lens/dist/clients/availability-probe-flight.js +12 -0
  39. package/node_modules/pi-lens/dist/clients/biome-client.js +7 -0
  40. package/node_modules/pi-lens/dist/clients/blocker-freshness.js +20 -0
  41. package/node_modules/pi-lens/dist/clients/bounded-telemetry.js +56 -19
  42. package/node_modules/pi-lens/dist/clients/build-identity.js +95 -0
  43. package/node_modules/pi-lens/dist/clients/bus-publish.js +3 -0
  44. package/node_modules/pi-lens/dist/clients/cache/rule-cache.js +2 -1
  45. package/node_modules/pi-lens/dist/clients/cache-observability.js +2 -1
  46. package/node_modules/pi-lens/dist/clients/cascade-logger.js +15 -1
  47. package/node_modules/pi-lens/dist/clients/dead-code-client.js +14 -11
  48. package/node_modules/pi-lens/dist/clients/degradation-ledger.js +56 -4
  49. package/node_modules/pi-lens/dist/clients/demoted-finding-render.js +14 -0
  50. package/node_modules/pi-lens/dist/clients/dependency-checker.js +20 -3
  51. package/node_modules/pi-lens/dist/clients/deps/minimatch.js +1 -1
  52. package/node_modules/pi-lens/dist/clients/dispatch/collect-later-tier.js +39 -0
  53. package/node_modules/pi-lens/dist/clients/dispatch/dispatcher.js +150 -9
  54. package/node_modules/pi-lens/dist/clients/dispatch/fact-store.js +231 -9
  55. package/node_modules/pi-lens/dist/clients/dispatch/integration.js +205 -100
  56. package/node_modules/pi-lens/dist/clients/dispatch/pending-runner-findings.js +87 -0
  57. package/node_modules/pi-lens/dist/clients/dispatch/plan.js +21 -5
  58. package/node_modules/pi-lens/dist/clients/dispatch/runners/ast-grep-napi.js +174 -55
  59. package/node_modules/pi-lens/dist/clients/dispatch/runners/eslint.js +5 -1
  60. package/node_modules/pi-lens/dist/clients/dispatch/runners/helm-lint.js +16 -3
  61. package/node_modules/pi-lens/dist/clients/dispatch/runners/helm-render.js +54 -9
  62. package/node_modules/pi-lens/dist/clients/dispatch/runners/index.js +1 -3
  63. package/node_modules/pi-lens/dist/clients/dispatch/runners/lsp.js +37 -1
  64. package/node_modules/pi-lens/dist/clients/dispatch/runners/markdownlint.js +4 -1
  65. package/node_modules/pi-lens/dist/clients/dispatch/runners/oxlint.js +11 -4
  66. package/node_modules/pi-lens/dist/clients/dispatch/runners/psscriptanalyzer.js +4 -0
  67. package/node_modules/pi-lens/dist/clients/dispatch/runners/rust-clippy.js +8 -3
  68. package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/runner-helpers.js +76 -25
  69. package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/spawn-outcome.js +4 -0
  70. package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/tool-failure.js +19 -7
  71. package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/toolchain-availability.js +7 -13
  72. package/node_modules/pi-lens/dist/clients/dispatch/runners/yaml-rule-parser.js +51 -17
  73. package/node_modules/pi-lens/dist/clients/event-loop-monitor.js +66 -1
  74. package/node_modules/pi-lens/dist/clients/file-utils.js +157 -7
  75. package/node_modules/pi-lens/dist/clients/finding-delivery-gate.js +5 -0
  76. package/node_modules/pi-lens/dist/clients/formatters.js +28 -3
  77. package/node_modules/pi-lens/dist/clients/git-guard.js +197 -47
  78. package/node_modules/pi-lens/dist/clients/git-tracked-ignore.js +32 -1
  79. package/node_modules/pi-lens/dist/clients/govulncheck-client.js +5 -1
  80. package/node_modules/pi-lens/dist/clients/gzip-stage-write.js +7 -0
  81. package/node_modules/pi-lens/dist/clients/installer/index.js +131 -34
  82. package/node_modules/pi-lens/dist/clients/installer/managed-tool-refresh.js +3 -1
  83. package/node_modules/pi-lens/dist/clients/instance-reaper.js +4 -13
  84. package/node_modules/pi-lens/dist/clients/instance-registry-lock.js +145 -0
  85. package/node_modules/pi-lens/dist/clients/instance-registry.js +432 -122
  86. package/node_modules/pi-lens/dist/clients/jscpd-client.js +6 -1
  87. package/node_modules/pi-lens/dist/clients/language-profile.js +4 -2
  88. package/node_modules/pi-lens/dist/clients/latency-logger.js +19 -1
  89. package/node_modules/pi-lens/dist/clients/lens-flag-registry.js +8 -0
  90. package/node_modules/pi-lens/dist/clients/lens-map.js +3 -1
  91. package/node_modules/pi-lens/dist/clients/lsp/cascade-tier.js +4 -0
  92. package/node_modules/pi-lens/dist/clients/lsp/client.js +516 -31
  93. package/node_modules/pi-lens/dist/clients/lsp/config.js +25 -2
  94. package/node_modules/pi-lens/dist/clients/lsp/diagnostic-binding.js +9 -4
  95. package/node_modules/pi-lens/dist/clients/lsp/index.js +509 -182
  96. package/node_modules/pi-lens/dist/clients/lsp/jvm-runtime.js +5 -0
  97. package/node_modules/pi-lens/dist/clients/lsp/launch.js +36 -8
  98. package/node_modules/pi-lens/dist/clients/lsp/pending-aux-coverage.js +116 -4
  99. package/node_modules/pi-lens/dist/clients/lsp/server.js +142 -13
  100. package/node_modules/pi-lens/dist/clients/lsp/session-roots.js +99 -0
  101. package/node_modules/pi-lens/dist/clients/lsp/workspace-diagnostics-cache.js +16 -6
  102. package/node_modules/pi-lens/dist/clients/lsp/workspace-sweep-hold.js +32 -23
  103. package/node_modules/pi-lens/dist/clients/lsp-mutation.js +16 -2
  104. package/node_modules/pi-lens/dist/clients/mcp/analyze.js +12 -6
  105. package/node_modules/pi-lens/dist/clients/mcp/session.js +14 -1
  106. package/node_modules/pi-lens/dist/clients/memory-sampler.js +23 -1
  107. package/node_modules/pi-lens/dist/clients/message-end-attribution.js +30 -0
  108. package/node_modules/pi-lens/dist/clients/metrics-history.js +21 -4
  109. package/node_modules/pi-lens/dist/clients/opaque-mutation-scan.js +232 -18
  110. package/node_modules/pi-lens/dist/clients/package-manager.js +6 -18
  111. package/node_modules/pi-lens/dist/clients/path-keyed-map.js +14 -1
  112. package/node_modules/pi-lens/dist/clients/path-utils.js +32 -0
  113. package/node_modules/pi-lens/dist/clients/pipeline.js +0 -8
  114. package/node_modules/pi-lens/dist/clients/process-singletons.js +210 -0
  115. package/node_modules/pi-lens/dist/clients/project-diagnostics/runner-adapters/madge.js +2 -1
  116. package/node_modules/pi-lens/dist/clients/project-diagnostics/scanner.js +10 -4
  117. package/node_modules/pi-lens/dist/clients/project-snapshot.js +33 -11
  118. package/node_modules/pi-lens/dist/clients/read-guard-logger.js +24 -2
  119. package/node_modules/pi-lens/dist/clients/read-guard.js +74 -7
  120. package/node_modules/pi-lens/dist/clients/recent-touches.js +17 -5
  121. package/node_modules/pi-lens/dist/clients/review-graph/builder.js +639 -62
  122. package/node_modules/pi-lens/dist/clients/review-graph/tsconfig-paths.js +5 -1
  123. package/node_modules/pi-lens/dist/clients/review-graph-logger.js +21 -1
  124. package/node_modules/pi-lens/dist/clients/runtime-coordinator.js +68 -0
  125. package/node_modules/pi-lens/dist/clients/runtime-session.js +30 -12
  126. package/node_modules/pi-lens/dist/clients/runtime-tool-call.js +15 -0
  127. package/node_modules/pi-lens/dist/clients/runtime-tool-result.js +106 -7
  128. package/node_modules/pi-lens/dist/clients/runtime-turn.js +242 -27
  129. package/node_modules/pi-lens/dist/clients/safe-spawn.js +158 -18
  130. package/node_modules/pi-lens/dist/clients/security-scan-client.js +48 -17
  131. package/node_modules/pi-lens/dist/clients/session-lifecycle.js +238 -42
  132. package/node_modules/pi-lens/dist/clients/sg-runner.js +27 -15
  133. package/node_modules/pi-lens/dist/clients/shared-checkout-guard.js +338 -0
  134. package/node_modules/pi-lens/dist/clients/spawn-output-cap.js +37 -0
  135. package/node_modules/pi-lens/dist/clients/spawn-timeout-cooldown.js +15 -1
  136. package/node_modules/pi-lens/dist/clients/startup-scan.js +6 -7
  137. package/node_modules/pi-lens/dist/clients/startup-timing.js +69 -4
  138. package/node_modules/pi-lens/dist/clients/string-utils.js +20 -0
  139. package/node_modules/pi-lens/dist/clients/test-runner-client.js +419 -88
  140. package/node_modules/pi-lens/dist/clients/tool-policy.js +1 -1
  141. package/node_modules/pi-lens/dist/clients/tree-sitter-client.js +11 -5
  142. package/node_modules/pi-lens/dist/clients/tree-sitter-logger.js +17 -1
  143. package/node_modules/pi-lens/dist/clients/warm-attach.js +8 -10
  144. package/node_modules/pi-lens/dist/clients/word-index-logger.js +13 -1
  145. package/node_modules/pi-lens/dist/clients/word-index-store.js +448 -0
  146. package/node_modules/pi-lens/dist/clients/word-index.js +582 -104
  147. package/node_modules/pi-lens/dist/clients/workspace-topology.js +17 -0
  148. package/node_modules/pi-lens/dist/clients/zizmor-config.js +26 -6
  149. package/node_modules/pi-lens/dist/index.js +11243 -6651
  150. package/node_modules/pi-lens/dist/mcp/build-staleness.js +8 -7
  151. package/node_modules/pi-lens/dist/mcp/cli.js +3 -1
  152. package/node_modules/pi-lens/dist/mcp/server.js +6 -1
  153. package/node_modules/pi-lens/dist/tools/lens-diagnostics.js +4 -0
  154. package/node_modules/pi-lens/dist/tools/lsp-diagnostics.js +14 -5
  155. package/node_modules/pi-lens/dist/tools/lsp-navigation.js +3 -1
  156. package/node_modules/pi-lens/docs/agent-guide.md +4 -2
  157. package/node_modules/pi-lens/docs/ast-grep_rules_catalog.md +41 -10
  158. package/node_modules/pi-lens/docs/astplayground.md +26 -17
  159. package/node_modules/pi-lens/package.json +5 -1
  160. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/c-no-malloc-free-test.yml +20 -0
  161. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/cpp-no-malloc-free-test.yml +25 -0
  162. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/cpp-no-printf-test.yml +19 -0
  163. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/go-no-fmt-println-test.yml +19 -0
  164. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/go-no-panic-in-lib-test.yml +17 -0
  165. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/go-no-underscore-func-name-test.yml +18 -0
  166. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/go-prefer-errors-is-test.yml +15 -0
  167. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/go-prefer-string-builder-test.yml +21 -0
  168. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/hyphenated-svg-attribute-test.yml +11 -0
  169. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/kotlin-no-lateinit-test.yml +17 -0
  170. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/kotlin-no-nullable-boolean-test.yml +15 -0
  171. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/kotlin-no-println-test.yml +19 -0
  172. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/kotlin-no-unnecessary-let-test.yml +7 -0
  173. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/kotlin-no-unsafe-cast-test.yml +7 -0
  174. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/kotlin-no-var-test.yml +7 -0
  175. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/kotlin-prefer-data-class-test.yml +25 -0
  176. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/kotlin-prefer-is-empty-test.yml +11 -0
  177. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-important-test.yml +21 -0
  178. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-raw-types-test.yml +30 -0
  179. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-string-concat-in-loop-test.yml +100 -0
  180. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-system-out-println-test.yml +7 -0
  181. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/prefer-string-is-empty-test.yml +7 -0
  182. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/python-optional-type-test.yml +9 -0
  183. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/reducible-list-comprehension-test.yml +11 -0
  184. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/ruby-symbol-to-proc-candidate-test.yml +10 -0
  185. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/rust-no-chars-enumerate-test.yml +7 -0
  186. package/node_modules/pi-lens/rules/ast-grep-rules/rules/c-no-malloc-free.yml +39 -0
  187. package/node_modules/pi-lens/rules/ast-grep-rules/rules/cpp-no-malloc-free.yml +45 -0
  188. package/node_modules/pi-lens/rules/ast-grep-rules/rules/cpp-no-printf.yml +41 -0
  189. package/node_modules/pi-lens/rules/ast-grep-rules/rules/go-no-fmt-println.yml +43 -0
  190. package/node_modules/pi-lens/rules/ast-grep-rules/rules/go-no-panic-in-lib.yml +41 -0
  191. package/node_modules/pi-lens/rules/ast-grep-rules/rules/go-no-underscore-func-name.yml +35 -0
  192. package/node_modules/pi-lens/rules/ast-grep-rules/rules/go-prefer-errors-is.yml +40 -0
  193. package/node_modules/pi-lens/rules/ast-grep-rules/rules/go-prefer-string-builder.yml +43 -0
  194. package/node_modules/pi-lens/rules/ast-grep-rules/rules/hyphenated-svg-attribute.yml +32 -0
  195. package/node_modules/pi-lens/rules/ast-grep-rules/rules/kotlin-no-lateinit.yml +37 -0
  196. package/node_modules/pi-lens/rules/ast-grep-rules/rules/kotlin-no-nullable-boolean.yml +33 -0
  197. package/node_modules/pi-lens/rules/ast-grep-rules/rules/kotlin-no-println.yml +42 -0
  198. package/node_modules/pi-lens/rules/ast-grep-rules/rules/kotlin-no-unnecessary-let.yml +28 -0
  199. package/node_modules/pi-lens/rules/ast-grep-rules/rules/kotlin-no-unsafe-cast.yml +28 -0
  200. package/node_modules/pi-lens/rules/ast-grep-rules/rules/kotlin-no-var.yml +35 -0
  201. package/node_modules/pi-lens/rules/ast-grep-rules/rules/kotlin-prefer-data-class.yml +66 -0
  202. package/node_modules/pi-lens/rules/ast-grep-rules/rules/kotlin-prefer-is-empty.yml +35 -0
  203. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-important.yml +21 -0
  204. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-raw-types.yml +33 -0
  205. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-string-concat-in-loop.yml +90 -0
  206. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-system-out-println.yml +16 -0
  207. package/node_modules/pi-lens/rules/ast-grep-rules/rules/prefer-string-is-empty.yml +11 -0
  208. package/node_modules/pi-lens/rules/ast-grep-rules/rules/python-optional-type.yml +22 -0
  209. package/node_modules/pi-lens/rules/ast-grep-rules/rules/reducible-list-comprehension.yml +29 -0
  210. package/node_modules/pi-lens/rules/ast-grep-rules/rules/ruby-symbol-to-proc-candidate.yml +25 -0
  211. package/node_modules/pi-lens/rules/ast-grep-rules/rules/rust-no-chars-enumerate.yml +19 -0
  212. package/node_modules/pi-mcp-adapter/CHANGELOG.md +15 -0
  213. package/node_modules/pi-mcp-adapter/README.md +2 -2
  214. package/node_modules/pi-mcp-adapter/commands.ts +1 -1
  215. package/node_modules/pi-mcp-adapter/dist/types.d.ts +1 -1
  216. package/node_modules/pi-mcp-adapter/mcp-auth-flow.ts +105 -32
  217. package/node_modules/pi-mcp-adapter/package.json +1 -1
  218. package/node_modules/pi-mcp-adapter/proxy-modes.ts +22 -12
  219. package/node_modules/pi-mcp-adapter/server-manager.ts +6 -0
  220. package/node_modules/pi-mcp-adapter/types.ts +1 -1
  221. package/node_modules/pi-web-access/CHANGELOG.md +20 -0
  222. package/node_modules/pi-web-access/README.md +15 -2
  223. package/node_modules/pi-web-access/extract.ts +57 -12
  224. package/node_modules/pi-web-access/github-extract.ts +47 -3
  225. package/node_modules/pi-web-access/index.ts +1 -1
  226. package/node_modules/pi-web-access/package.json +1 -1
  227. package/node_modules/pi-web-access/page-query.ts +61 -8
  228. package/package.json +16 -16
  229. package/node_modules/@tifan/pi-titlebar-spinner/README.md +0 -24
  230. package/node_modules/@tifan/pi-titlebar-spinner/package.json +0 -39
  231. package/node_modules/@tifan/pi-titlebar-spinner/src/index.ts +0 -49
@@ -3,6 +3,12 @@ import { logExtension } from "./extension-log.js";
3
3
  import { LEDGER_FIELD_MAX, truncateForLedger } from "./ledger-bounds.js";
4
4
  import { logLatency } from "./latency-logger.js";
5
5
  import { getSinkWriteFailures, resetSinkWriteFailures, } from "./ndjson-logger.js";
6
+ // #2146: pulled at READ time, never pushed. `process-singletons.ts` is a
7
+ // dependency leaf on purpose — it cannot import this module without closing a
8
+ // no-client-cycles cycle through instance-registry/instance-reaper — so the
9
+ // ledger reaches IN for its reset log, the same inversion `getSinkWriteFailures`
10
+ // above uses.
11
+ import { getProcessSingletonResets, PROCESS_SINGLETON_RESET_KIND, } from "./process-singletons.js";
6
12
  // Re-exported so existing importers keep one name for the ledger's bound.
7
13
  export { LEDGER_FIELD_MAX, truncateForLedger };
8
14
  const ENTRIES_PER_KIND = 20;
@@ -59,7 +65,7 @@ export function recordDegradationOnce(record) {
59
65
  return;
60
66
  onceKeys.add(key);
61
67
  if (recordDegradation({ kind, subject, reason: record.reason })) {
62
- logDurableDegradation(kind, subject, 1);
68
+ logDurableDegradation(kind, subject, 1, record.metadata);
63
69
  }
64
70
  }
65
71
  catch (error) {
@@ -108,7 +114,7 @@ export function incrementDegradationCount(record) {
108
114
  // Durable rows use the summary's admission and emit the first event and
109
115
  // power-of-two milestones only, keeping the sink bounded.
110
116
  if (admitted && isPowerOfTwo(count)) {
111
- logDurableDegradation(kind, subject, count);
117
+ logDurableDegradation(kind, subject, count, record.metadata);
112
118
  }
113
119
  return count === 1;
114
120
  }
@@ -124,15 +130,34 @@ export function incrementDegradationCount(record) {
124
130
  * redaction, and write queue. The subject and kind were already bounded by the
125
131
  * ledger before reaching this seam.
126
132
  */
127
- function logDurableDegradation(kind, subject, count) {
133
+ function logDurableDegradation(kind, subject, count, metadata) {
134
+ const boundedMetadata = boundLedgerMetadata(metadata);
128
135
  logLatency({
129
136
  type: "phase",
130
137
  phase: "degradation_ledger",
131
138
  filePath: subject,
132
139
  durationMs: 0,
133
- metadata: { kind, subject, count, ledgerGeneration },
140
+ metadata: {
141
+ ...boundedMetadata,
142
+ kind,
143
+ subject,
144
+ count,
145
+ ledgerGeneration,
146
+ },
134
147
  });
135
148
  }
149
+ const MAX_METADATA_KEYS = 8;
150
+ function boundLedgerMetadata(metadata) {
151
+ if (!metadata)
152
+ return {};
153
+ const entries = Object.entries(metadata);
154
+ const kept = entries.slice(0, MAX_METADATA_KEYS);
155
+ const bounded = Object.fromEntries(kept.map(([key, value]) => [key, truncateForLedger(value)]));
156
+ const dropped = entries.length - kept.length;
157
+ if (dropped > 0)
158
+ bounded.metadataDropped = dropped;
159
+ return bounded;
160
+ }
136
161
  function isPowerOfTwo(value) {
137
162
  return value > 0 && (value & (value - 1)) === 0;
138
163
  }
@@ -165,6 +190,21 @@ export function getDegradationSummary() {
165
190
  })),
166
191
  });
167
192
  }
193
+ // #2146, same read-time fold: process-singleton resets live in the leaf
194
+ // module's own bounded log. One entry per family, so this group's count is
195
+ // the number of families this build could not adopt, never an event tally.
196
+ const singletonResets = getProcessSingletonResets();
197
+ if (singletonResets.length > 0) {
198
+ summary.push({
199
+ kind: PROCESS_SINGLETON_RESET_KIND,
200
+ count: singletonResets.length,
201
+ droppedCount: 0,
202
+ latestReasons: singletonResets.map((reset) => ({
203
+ subject: truncateForLedger(reset.family),
204
+ reason: truncateForLedger(reset.reason),
205
+ })),
206
+ });
207
+ }
168
208
  return summary;
169
209
  }
170
210
  function isRenderableSummary(value) {
@@ -218,6 +258,18 @@ export function resetDegradationLedger() {
218
258
  // process-lifetime latch too — it re-arms alongside the rest of the
219
259
  // ledger rather than surviving past the session that observed it.
220
260
  resetSinkWriteFailures();
261
+ // #2146 review F3: the OTHER pulled source, `getProcessSingletonResets()`,
262
+ // deliberately does NOT re-arm here, and the difference from its neighbour
263
+ // above is the point. A sink write failure recurs — new writes fail, so
264
+ // clearing the tally costs nothing and a later session re-observes the
265
+ // problem. A process-singleton reset happens once, at module-evaluation
266
+ // time, and cannot recur: after it, the container holds only compatible
267
+ // cells. Clearing it would show the fact in the first session's
268
+ // `pilens_health` and hide it from every session after, which is exactly
269
+ // when someone reads that line. The row is bounded independently of the
270
+ // session (one entry per family, capped at 16), so leaving it costs a fixed
271
+ // handful of lines and keeps a process-scope fact visible for the process's
272
+ // life. Deliberate exception to catalog shape 17, not an oversight.
221
273
  }
222
274
  export const DEGRADATION_ENTRIES_PER_KIND = ENTRIES_PER_KIND;
223
275
  export const DEGRADATION_MAX_DISTINCT_KINDS = MAX_DISTINCT_KINDS;
@@ -153,3 +153,17 @@ export function formatRetirementNote(deadLines) {
153
153
  return (`Retired after this delivery${cited}: the file no longer has these lines, ` +
154
154
  "so this finding cannot be re-confirmed and will not be served again.");
155
155
  }
156
+ /**
157
+ * The one-line note a surface appends when it RETIRES a dependency-drift
158
+ * demotion at its delivery cap (#1950). Deliberately worded differently from
159
+ * {@link formatRetirementNote}: that one means the finding is provably
160
+ * unrecoverable (the cited lines are gone), while THIS retirement is a
161
+ * bounded-noise cap on a record that is still recoverable — a fresh dispatch
162
+ * of the file could still confirm or clear it. Saying "cannot be re-confirmed"
163
+ * here would be false.
164
+ */
165
+ export function formatDeliveryCapNote(deliveryCount) {
166
+ return (`Not shown again after ${deliveryCount} deliveries with no re-run: ` +
167
+ "this finding may still be accurate, but re-serving it further adds no " +
168
+ "new information. A fresh dispatch of this file will re-confirm or clear it.");
169
+ }
@@ -14,6 +14,7 @@ import * as path from "node:path";
14
14
  import { findNodeToolBinary } from "./package-manager.js";
15
15
  import { isFullyQualified } from "./path-utils.js";
16
16
  import { safeSpawnAsync } from "./safe-spawn.js";
17
+ import { compareOrdinal } from "./string-utils.js";
17
18
  import { createAvailabilityChecker, discoverManagedTool, getManagedToolEnvironment, resolveAvailableOrInstall, } from "./dispatch/runners/utils/runner-helpers.js";
18
19
  import { createAvailabilityLatch, } from "./dispatch/runners/utils/availability-policy.js";
19
20
  /**
@@ -501,8 +502,14 @@ export class DependencyChecker {
501
502
  const existing = this.checkInFlight.get(key);
502
503
  if (existing)
503
504
  return existing;
505
+ // Identity-guarded release (#1968's pattern): delete only if THIS run is
506
+ // still the registered one. A bare delete-by-key lets a late-settling run
507
+ // evict a live successor a second writer registered under the same key
508
+ // mid-flight, after which the next caller starts a duplicate check.
504
509
  const promise = this.runCheckFile(normalized, projectRoot, gen).finally(() => {
505
- this.checkInFlight.delete(key);
510
+ if (this.checkInFlight.get(key) === promise) {
511
+ this.checkInFlight.delete(key);
512
+ }
506
513
  });
507
514
  this.checkInFlight.set(key, promise);
508
515
  return promise;
@@ -781,8 +788,14 @@ export class DependencyChecker {
781
788
  // does, and the two overlap (runtime-session, fresh-fetch), so it takes a
782
789
  // generation too — claimed before the spawn, as late as a scan can.
783
790
  const gen = ++this.opGeneration;
791
+ // Identity-guarded release (#1968's pattern): delete only if THIS run is
792
+ // still the registered one. A bare delete-by-key lets a late-settling run
793
+ // evict a live successor a second writer registered under the same key
794
+ // mid-flight, after which the next caller starts a duplicate scan.
784
795
  const promise = this.runScanProject(projectRoot, gen).finally(() => {
785
- this.scanInFlight.delete(projectRoot);
796
+ if (this.scanInFlight.get(projectRoot) === promise) {
797
+ this.scanInFlight.delete(projectRoot);
798
+ }
786
799
  });
787
800
  this.scanInFlight.set(projectRoot, promise);
788
801
  return promise;
@@ -830,7 +843,11 @@ export class DependencyChecker {
830
843
  const seen = new Set();
831
844
  let output = `[Circular Deps] ${circular.length} cycle(s) found:\n`;
832
845
  for (const dep of circular) {
833
- const cycleKey = dep.path.sort((a, b) => a.localeCompare(b)).join("→");
846
+ // Copy before sorting: `dep.path` is rendered verbatim below (and by
847
+ // other CircularDep consumers, e.g. madge.ts's diagnostic renderer),
848
+ // so the dedupe key must not reorder — let alone mutate in place —
849
+ // the path a user reads.
850
+ const cycleKey = [...dep.path].sort(compareOrdinal).join("→");
834
851
  if (seen.has(cycleKey))
835
852
  continue;
836
853
  seen.add(cycleKey);
@@ -1,4 +1,4 @@
1
1
  /**
2
2
  * Centralized accessor for `minimatch`. See ./typescript.ts for the rationale.
3
3
  */
4
- export { minimatch } from "minimatch";
4
+ export { minimatch, Minimatch } from "minimatch";
@@ -0,0 +1,39 @@
1
+ /** Observed-latency classification shared by dispatch and LSP tier seams. */
2
+ import { normalizeMapKey } from "../path-utils.js";
3
+ /** A runner gets this much time before the next run moves off the write path. */
4
+ export const COLLECT_LATER_THRESHOLD_MS = 5_000;
5
+ const slowRunners = new Map();
6
+ function key(projectRoot, runnerId) {
7
+ return `${normalizeMapKey(projectRoot)}\u0000${runnerId}`;
8
+ }
9
+ /** Read the current observed tier. Missing observations stay inline. */
10
+ export function classifyObservedRunner(projectRoot, runnerId) {
11
+ return slowRunners.has(key(projectRoot, runnerId))
12
+ ? "collect-later"
13
+ : "inline";
14
+ }
15
+ /** Record one completed latency observation and return the resulting tier. */
16
+ export function observeRunnerLatency(options) {
17
+ const normalizedProjectRoot = normalizeMapKey(options.projectRoot);
18
+ const runnerKey = key(normalizedProjectRoot, options.runnerId);
19
+ if (options.timedOut === true ||
20
+ options.durationMs > COLLECT_LATER_THRESHOLD_MS) {
21
+ slowRunners.set(runnerKey, {
22
+ projectRoot: normalizedProjectRoot,
23
+ runnerId: options.runnerId,
24
+ });
25
+ return "collect-later";
26
+ }
27
+ // A fast completed run is affirmative recovery, not merely absence of a
28
+ // failure. This deliberately prevents a process-lifetime latch.
29
+ slowRunners.delete(runnerKey);
30
+ return "inline";
31
+ }
32
+ /** Session boundary reset. */
33
+ export function resetObservedRunnerLatency() {
34
+ slowRunners.clear();
35
+ }
36
+ /** Test-only inspection without exposing mutable state. */
37
+ export function observedRunnerTierForTests(projectRoot, runnerId) {
38
+ return classifyObservedRunner(projectRoot, runnerId);
39
+ }
@@ -17,6 +17,7 @@ import { logExtension } from "../extension-log.js";
17
17
  import * as fs from "node:fs";
18
18
  import * as path from "node:path";
19
19
  import { recordRunner } from "../widget-state.js";
20
+ import { incrementDegradationCount } from "../degradation-ledger.js";
20
21
  import { detectFileKind } from "../file-kinds.js";
21
22
  import { detectFileRole } from "../file-role.js";
22
23
  import { isTestFile } from "../file-utils.js";
@@ -24,19 +25,24 @@ import { getPrimaryDispatchGroup } from "../language-policy.js";
24
25
  import { resolveLanguageRootForFile } from "../language-profile.js";
25
26
  import { logLatency, phaseFinished, phaseStarted } from "../latency-logger.js";
26
27
  import { isSpawnableCommand } from "../installer/index.js";
27
- import { normalizeMapKey } from "../path-utils.js";
28
+ import { normalizeEphemeralMapKey, normalizeMapKey } from "../path-utils.js";
28
29
  import { loadPiLensProjectConfig } from "../project-lens-config.js";
29
30
  import { RUNTIME_CONFIG, getRunnerTimeoutFloorMs } from "../runtime-config.js";
30
31
  import { safeSpawnAsync } from "../safe-spawn.js";
31
32
  import { classifyDiagnostic } from "./diagnostic-taxonomy.js";
32
33
  import { classifyProbeFailure, logAvailabilityDecision, startHostStallSampler, transientRetryDelayMs, } from "./runners/utils/availability-policy.js";
34
+ import { recordAvailabilityProbeOverrun, getDispatchAvailabilityGeneration, } from "./runners/utils/runner-helpers.js";
35
+ import { createAvailabilityProbeFlight } from "../availability-probe-flight.js";
33
36
  import { applyDispositions } from "../diagnostic-dispositions.js";
34
37
  import { applyInlineSuppressions } from "./inline-suppressions.js";
35
38
  import { getToolPlan } from "./plan.js";
36
39
  import { resolveRunnerPath } from "./runner-context.js";
40
+ import { classifyObservedRunner, COLLECT_LATER_THRESHOLD_MS, observeRunnerLatency, } from "./collect-later-tier.js";
41
+ import { deferRunnerFindings } from "./pending-runner-findings.js";
37
42
  import { applyRulePolicy, rulePolicyMapFromConfig } from "./rule-policy.js";
38
43
  import { getToolProfile } from "./tool-profile.js";
39
44
  import { isRunnerSkipReason } from "./types.js";
45
+ const dispatcherProbeFlights = createAvailabilityProbeFlight({ generation: () => getDispatchAvailabilityGeneration() });
40
46
  import { formatDiagnostics } from "./utils/format-utils.js";
41
47
  // --- Runner Registry ---
42
48
  export class RunnerRegistry {
@@ -134,15 +140,19 @@ export async function checkToolAvailability(command, facts) {
134
140
  const startedAt = Date.now();
135
141
  let result;
136
142
  let hostStallMs;
143
+ let probeJoined = false;
137
144
  try {
138
- result = await safeSpawnAsync(command, ["--version"], {
145
+ const shared = dispatcherProbeFlights.run(`dispatcher:${key}`, () => safeSpawnAsync(command, ["--version"], {
139
146
  timeout: TOOL_PROBE_TIMEOUT_MS,
140
- });
147
+ }));
148
+ probeJoined = shared.joined;
149
+ result = await shared.promise;
141
150
  }
142
151
  finally {
143
152
  hostStallMs = sampler.stop();
144
153
  }
145
154
  const elapsedMs = Date.now() - startedAt;
155
+ recordAvailabilityProbeOverrun(command, key, elapsedMs, TOOL_PROBE_TIMEOUT_MS, result.failure);
146
156
  if (result.status === 0) {
147
157
  facts.setSessionFact(key, true);
148
158
  // The tool answered: retire the cooldown facts rather than leaving a
@@ -156,6 +166,7 @@ export async function checkToolAvailability(command, facts) {
156
166
  cause: "ok",
157
167
  elapsedMs,
158
168
  latched: true,
169
+ classifiedBy: probeJoined ? "joined" : "probe",
159
170
  hostStallMs,
160
171
  budgetMs: TOOL_PROBE_TIMEOUT_MS,
161
172
  });
@@ -188,7 +199,7 @@ export async function checkToolAvailability(command, facts) {
188
199
  hostStallMs,
189
200
  ...(retryAfterMs !== undefined && { retryAfterMs }),
190
201
  budgetMs: TOOL_PROBE_TIMEOUT_MS,
191
- classifiedBy: "probe",
202
+ classifiedBy: probeJoined ? "joined" : "probe",
192
203
  evidence,
193
204
  });
194
205
  return false;
@@ -419,13 +430,19 @@ function buildCoverageNotice(ctx, runnerLatencies) {
419
430
  // The marker describes this exact silent-scanner set. A scanner can
420
431
  // recover while another goes dark on the same file, so the set belongs
421
432
  // in the session dedupe identity rather than only kind and path.
433
+ // #2016: these are SCANNER IDS, not filesystem paths. `normalizeMapKey`
434
+ // would realpath each one; on Windows that fails, falls through to
435
+ // `resolveNonExisting`, and resolves the id against the CURRENT process
436
+ // cwd, so the dedupe key differed by platform and by cwd (the #2219
437
+ // non-path-sentinel class). The cheap syntactic fold is what this
438
+ // session-scoped dedupe key actually needs.
422
439
  const silentScannerSet = [...new Set(unconfirmedServerIds)]
423
- .map(normalizeMapKey)
440
+ .map(normalizeEphemeralMapKey)
424
441
  // Code-unit comparator: the sorted set is a dedupe KEY, so ordering
425
442
  // must be deterministic across locales — localeCompare is not.
426
443
  .sort((a, b) => Number(a > b) - Number(a < b))
427
444
  .join(",");
428
- const onceKey = `${ctx.kind}:${normalizeMapKey(ctx.filePath)}:${silentScannerSet}`;
445
+ const onceKey = `${ctx.kind}:${ctx.filePath}:${silentScannerSet}`;
429
446
  if (coverageNoticeSeen.has(onceKey))
430
447
  return undefined;
431
448
  coverageNoticeSeen.add(onceKey);
@@ -467,7 +484,7 @@ function buildCoverageNotice(ctx, runnerLatencies) {
467
484
  (r.status === "succeeded" || r.status === "failed"));
468
485
  if (anyLinterHasCoverage)
469
486
  return undefined;
470
- const onceKey = `${ctx.kind}:${normalizeMapKey(ctx.filePath)}`;
487
+ const onceKey = `${ctx.kind}:${ctx.filePath}`;
471
488
  if (coverageNoticeSeen.has(onceKey))
472
489
  return undefined;
473
490
  coverageNoticeSeen.add(onceKey);
@@ -603,10 +620,123 @@ async function runGroup(ctx, group, registry, onRunnerResult) {
603
620
  });
604
621
  continue;
605
622
  }
623
+ const projectRoot = ctx.projectRoot ?? ctx.cwd;
624
+ // Only post-write dispatches participate. Project scans and direct API
625
+ // callers must retain their existing synchronous semantics.
626
+ const observedTier = ctx.writeIndex === undefined
627
+ ? "inline"
628
+ : classifyObservedRunner(projectRoot, runner.id);
629
+ if (observedTier === "collect-later") {
630
+ const markedAtMs = Date.now();
631
+ const deferred = runRunner(ctx, runner, semantic).then((result) => {
632
+ const durationMs = Date.now() - markedAtMs;
633
+ const tier = observeRunnerLatency({
634
+ projectRoot,
635
+ runnerId: runner.id,
636
+ durationMs,
637
+ timedOut: result.failureKind === "timeout",
638
+ });
639
+ if (tier !== observedTier) {
640
+ logLatency({
641
+ type: "phase",
642
+ filePath: ctx.filePath,
643
+ phase: "runner_collect_later_tier_flip",
644
+ durationMs: 0,
645
+ metadata: {
646
+ runnerId: runner.id,
647
+ from: observedTier,
648
+ to: tier,
649
+ projectRoot,
650
+ },
651
+ });
652
+ }
653
+ if (tier === "collect-later") {
654
+ incrementDegradationCount({
655
+ kind: "runner-collect-later",
656
+ subject: `${projectRoot}:${runner.id}`,
657
+ reason: `observed ${durationMs}ms, threshold ${COLLECT_LATER_THRESHOLD_MS}ms`,
658
+ });
659
+ }
660
+ logLatency({
661
+ type: "runner",
662
+ filePath: ctx.filePath,
663
+ runnerId: runner.id,
664
+ startedAt: new Date(markedAtMs).toISOString(),
665
+ durationMs,
666
+ status: result.status,
667
+ diagnosticCount: result.diagnostics.length,
668
+ semantic: result.semantic ?? semantic,
669
+ metadata: { tier: "collect-later", delivered: "turn_end" },
670
+ });
671
+ return result;
672
+ });
673
+ deferRunnerFindings({
674
+ filePath: ctx.filePath,
675
+ cwd: ctx.cwd,
676
+ projectRoot,
677
+ runnerId: runner.id,
678
+ markedAtMs,
679
+ writeIndex: ctx.writeIndex,
680
+ promise: deferred,
681
+ });
682
+ // A deferred runner is still an observed runner. Keep it visible in
683
+ // both the edit latency report and the widget until its turn-end result
684
+ // replaces this pending state (#2122 F1).
685
+ latencies.push({
686
+ runnerId: runner.id,
687
+ startTime: runnerStart,
688
+ endTime: runnerStart,
689
+ durationMs: 0,
690
+ status: "pending",
691
+ diagnosticCount: 0,
692
+ semantic: semantic,
693
+ });
694
+ recordRunner(ctx.filePath, runner.id, "pending", 0, 0, ctx.writeIndex);
695
+ logLatency({
696
+ type: "runner",
697
+ filePath: ctx.filePath,
698
+ runnerId: runner.id,
699
+ durationMs: 0,
700
+ status: "pending",
701
+ diagnosticCount: 0,
702
+ semantic,
703
+ metadata: { tier: "collect-later", delivered: "turn_end" },
704
+ });
705
+ continue;
706
+ }
606
707
  const result = await runRunner(ctx, runner, semantic);
607
708
  onRunnerResult?.(runnerId, result);
608
709
  const runnerEnd = Date.now();
609
710
  const duration = runnerEnd - runnerStart;
711
+ const tier = ctx.writeIndex === undefined
712
+ ? "inline"
713
+ : observeRunnerLatency({
714
+ projectRoot,
715
+ runnerId: runner.id,
716
+ durationMs: duration,
717
+ timedOut: result.failureKind === "timeout",
718
+ });
719
+ if (tier !== observedTier) {
720
+ logLatency({
721
+ type: "phase",
722
+ filePath: ctx.filePath,
723
+ phase: "runner_collect_later_tier_flip",
724
+ durationMs: 0,
725
+ metadata: {
726
+ runnerId: runner.id,
727
+ from: observedTier,
728
+ to: tier,
729
+ projectRoot,
730
+ },
731
+ });
732
+ }
733
+ if (tier === "collect-later") {
734
+ incrementDegradationCount({
735
+ kind: "runner-collect-later",
736
+ subject: `${projectRoot}:${runner.id}`,
737
+ reason: `observed ${duration}ms, threshold ${COLLECT_LATER_THRESHOLD_MS}ms`,
738
+ });
739
+ }
610
740
  // Runner definitions are a typed API, but embedders/plugins can still
611
741
  // return untyped objects at runtime. Admit only the closed taxonomy and
612
742
  // only on actual skips so free text cannot enter durable latency metadata.
@@ -707,8 +837,13 @@ export async function dispatchForFile(ctx, groups, registry, onRunnerResult) {
707
837
  const groupResults = await Promise.all(groups.map((group) => runGroup(ctx, group, registry, onRunnerResult)));
708
838
  // Count baseline warnings before filtering (for delta count display)
709
839
  const relativeKey = path.relative(ctx.cwd, ctx.filePath).replace(/\\/g, "/");
710
- const baselineAbsKey = `session.baseline.${normalizeMapKey(ctx.filePath)}`;
711
- const baselineRelKey = `session.baseline.${normalizeMapKey(relativeKey)}`;
840
+ const baselineAbsKey = `session.baseline.${ctx.filePath}`;
841
+ // #2016: `relativeKey` is relative to `ctx.cwd`. `normalizeMapKey` resolved
842
+ // it against the process cwd instead, so on Windows this "relative" key
843
+ // became an absolute path anchored on the wrong root while POSIX left it
844
+ // relative. Both the read here and the write below use this const, so the
845
+ // key stays self-consistent within a session.
846
+ const baselineRelKey = `session.baseline.${normalizeEphemeralMapKey(relativeKey)}`;
712
847
  const previousBaseline = ctx.deltaMode
713
848
  ? (ctx.facts.getSessionFact(baselineAbsKey) ??
714
849
  ctx.facts.getSessionFact(baselineRelKey))
@@ -819,6 +954,12 @@ export async function dispatchForFile(ctx, groups, registry, onRunnerResult) {
819
954
  output += formatDiagnostics([coverageNotice], "warning", 1);
820
955
  warnings.push(coverageNotice);
821
956
  }
957
+ const pendingRunners = runnerLatencies
958
+ .filter((runner) => runner.status === "pending")
959
+ .map((runner) => runner.runnerId);
960
+ if (pendingRunners.length > 0) {
961
+ output += `\n⏳ Pending runners (reported at turn end): ${pendingRunners.join(", ")}\n`;
962
+ }
822
963
  // Generate and store latency report
823
964
  const overallEnd = Date.now();
824
965
  const latencyReport = {