@herbertgao/pi-extensions 2026.9.1 → 2026.9.2

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 (166) hide show
  1. package/node_modules/@narumitw/pi-btw/README.md +57 -141
  2. package/node_modules/@narumitw/pi-btw/dist/index.ts +466 -41
  3. package/node_modules/@narumitw/pi-btw/dist/index.ts.map +4 -4
  4. package/node_modules/@narumitw/pi-btw/docs/workflows.md +74 -0
  5. package/node_modules/@narumitw/pi-btw/package.json +2 -1
  6. package/node_modules/@narumitw/pi-btw/src/btw.ts +15 -1
  7. package/node_modules/@narumitw/pi-btw/src/fullscreen-ui.ts +32 -2
  8. package/node_modules/@narumitw/pi-btw/src/keybindings.ts +330 -0
  9. package/node_modules/@narumitw/pi-btw/src/menu.ts +136 -19
  10. package/node_modules/@narumitw/pi-btw/src/settings.ts +32 -0
  11. package/node_modules/@narumitw/pi-btw/src/side-thread.ts +57 -5
  12. package/node_modules/@narumitw/pi-btw/src/transcript-pager.ts +52 -30
  13. package/node_modules/pi-lens/CHANGELOG.md +84 -0
  14. package/node_modules/pi-lens/dist/clients/actionable-warnings-logger.js +0 -7
  15. package/node_modules/pi-lens/dist/clients/advisory-provenance.js +1 -1
  16. package/node_modules/pi-lens/dist/clients/ast-grep-client.js +1 -1
  17. package/node_modules/pi-lens/dist/clients/ast-grep-tool-logger.js +0 -7
  18. package/node_modules/pi-lens/dist/clients/ast-grep-types.js +2 -0
  19. package/node_modules/pi-lens/dist/clients/bash-file-access.js +133 -1
  20. package/node_modules/pi-lens/dist/clients/blocker-freshness.js +0 -6
  21. package/node_modules/pi-lens/dist/clients/bootstrap.js +4 -4
  22. package/node_modules/pi-lens/dist/clients/bounded-pid-file-lock.js +1 -1
  23. package/node_modules/pi-lens/dist/clients/bounded-telemetry.js +0 -5
  24. package/node_modules/pi-lens/dist/clients/bus-events-logger.js +1 -9
  25. package/node_modules/pi-lens/dist/clients/cache-observability.js +1 -6
  26. package/node_modules/pi-lens/dist/clients/cargo-manifest.js +2 -2
  27. package/node_modules/pi-lens/dist/clients/cascade-logger.js +0 -7
  28. package/node_modules/pi-lens/dist/clients/child-unref.js +1 -19
  29. package/node_modules/pi-lens/dist/clients/config-core/index.js +11 -8
  30. package/node_modules/pi-lens/dist/clients/config-core/process-spec.js +1 -1
  31. package/node_modules/pi-lens/dist/clients/config-core/provenance.js +2 -9
  32. package/node_modules/pi-lens/dist/clients/config-core/records.js +1 -1
  33. package/node_modules/pi-lens/dist/clients/config-core/schema.js +3 -5
  34. package/node_modules/pi-lens/dist/clients/config-diagnostic-codes.js +1 -1
  35. package/node_modules/pi-lens/dist/clients/config-resolve.js +4 -4
  36. package/node_modules/pi-lens/dist/clients/config-schema.js +39 -2
  37. package/node_modules/pi-lens/dist/clients/dead-code-logger.js +0 -4
  38. package/node_modules/pi-lens/dist/clients/diagnostic-dispositions.js +1 -1
  39. package/node_modules/pi-lens/dist/clients/diagnostic-logger.js +1 -1
  40. package/node_modules/pi-lens/dist/clients/dispatch/collect-later-tier.js +0 -4
  41. package/node_modules/pi-lens/dist/clients/dispatch/dispatcher.js +0 -26
  42. package/node_modules/pi-lens/dist/clients/dispatch/integration.js +18 -1
  43. package/node_modules/pi-lens/dist/clients/dispatch/lazy.js +0 -6
  44. package/node_modules/pi-lens/dist/clients/dispatch/pending-runner-findings.js +1 -1
  45. package/node_modules/pi-lens/dist/clients/dispatch/plan.js +0 -3
  46. package/node_modules/pi-lens/dist/clients/dispatch/rules/high-complexity.js +2 -2
  47. package/node_modules/pi-lens/dist/clients/dispatch/rules/high-fan-out.js +1 -1
  48. package/node_modules/pi-lens/dist/clients/dispatch/runners/ast-grep-napi.js +3 -1
  49. package/node_modules/pi-lens/dist/clients/dispatch/runners/cpp-check.js +4 -0
  50. package/node_modules/pi-lens/dist/clients/dispatch/runners/helm-render.js +1 -1
  51. package/node_modules/pi-lens/dist/clients/dispatch/runners/oxlint.js +1 -0
  52. package/node_modules/pi-lens/dist/clients/dispatch/runners/psscriptanalyzer.js +6 -2
  53. package/node_modules/pi-lens/dist/clients/dispatch/runners/ruff.js +1 -1
  54. package/node_modules/pi-lens/dist/clients/dispatch/runners/shellcheck.js +1 -1
  55. package/node_modules/pi-lens/dist/clients/dispatch/runners/spellcheck.js +4 -2
  56. package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/diagnostic-parsers.js +1 -64
  57. package/node_modules/pi-lens/dist/clients/dispatch/runners/utils.js +0 -21
  58. package/node_modules/pi-lens/dist/clients/dispatch/runners/yaml-rule-parser.js +2 -20
  59. package/node_modules/pi-lens/dist/clients/dispatch/runners/yamllint.js +1 -0
  60. package/node_modules/pi-lens/dist/clients/dispatch/utils/format-utils.js +2 -2
  61. package/node_modules/pi-lens/dist/clients/disposition-logger.js +0 -7
  62. package/node_modules/pi-lens/dist/clients/event-loop-monitor.js +2 -2
  63. package/node_modules/pi-lens/dist/clients/file-kinds.js +1 -38
  64. package/node_modules/pi-lens/dist/clients/file-time.js +1 -1
  65. package/node_modules/pi-lens/dist/clients/file-utils.js +5 -1
  66. package/node_modules/pi-lens/dist/clients/format-service.js +0 -1
  67. package/node_modules/pi-lens/dist/clients/formatters.js +110 -3
  68. package/node_modules/pi-lens/dist/clients/generated-artifacts.js +3 -4
  69. package/node_modules/pi-lens/dist/clients/git-guard.js +5 -0
  70. package/node_modules/pi-lens/dist/clients/grammar-source.js +3 -3
  71. package/node_modules/pi-lens/dist/clients/installer/index.js +220 -9
  72. package/node_modules/pi-lens/dist/clients/installer/managed-tool-refresh.js +84 -18
  73. package/node_modules/pi-lens/dist/clients/instance-reaper.js +6 -6
  74. package/node_modules/pi-lens/dist/clients/language-profile.js +0 -9
  75. package/node_modules/pi-lens/dist/clients/language-registry.js +5 -1
  76. package/node_modules/pi-lens/dist/clients/latency-logger.js +5 -7
  77. package/node_modules/pi-lens/dist/clients/ledger-bounds.js +21 -1
  78. package/node_modules/pi-lens/dist/clients/lens-config.js +19 -0
  79. package/node_modules/pi-lens/dist/clients/lens-engine.js +3 -4
  80. package/node_modules/pi-lens/dist/clients/lens-events.js +1 -1
  81. package/node_modules/pi-lens/dist/clients/lens-flag-registry.js +18 -0
  82. package/node_modules/pi-lens/dist/clients/lens-map.js +1 -1
  83. package/node_modules/pi-lens/dist/clients/log-cleanup.js +0 -36
  84. package/node_modules/pi-lens/dist/clients/lsp/client.js +4 -3
  85. package/node_modules/pi-lens/dist/clients/lsp/config.js +1 -0
  86. package/node_modules/pi-lens/dist/clients/lsp/document-drift.js +1 -1
  87. package/node_modules/pi-lens/dist/clients/lsp/edits.js +3 -3
  88. package/node_modules/pi-lens/dist/clients/lsp/index.js +216 -25
  89. package/node_modules/pi-lens/dist/clients/lsp/inferred-project.js +1 -1
  90. package/node_modules/pi-lens/dist/clients/lsp/language.js +0 -8
  91. package/node_modules/pi-lens/dist/clients/lsp/path-utils.js +1 -1
  92. package/node_modules/pi-lens/dist/clients/lsp/pending-aux-coverage.js +0 -2
  93. package/node_modules/pi-lens/dist/clients/lsp/sync-kind.js +0 -1
  94. package/node_modules/pi-lens/dist/clients/lsp/tsserver-sync.js +5 -5
  95. package/node_modules/pi-lens/dist/clients/lsp/wait-policy/classification.js +6 -0
  96. package/node_modules/pi-lens/dist/clients/lsp/wait-policy/strategies.js +2 -1
  97. package/node_modules/pi-lens/dist/clients/lsp-document-symbols.js +1 -1
  98. package/node_modules/pi-lens/dist/clients/metrics-history.js +28 -107
  99. package/node_modules/pi-lens/dist/clients/middle-man-analysis.js +1 -1
  100. package/node_modules/pi-lens/dist/clients/module-report.js +1 -1
  101. package/node_modules/pi-lens/dist/clients/mutating-tool.js +3 -3
  102. package/node_modules/pi-lens/dist/clients/mutation-attribution.js +3 -3
  103. package/node_modules/pi-lens/dist/clients/ndjson-logger.js +0 -4
  104. package/node_modules/pi-lens/dist/clients/observed-mutation.js +9 -9
  105. package/node_modules/pi-lens/dist/clients/opaque-mutation-scan.js +2 -6
  106. package/node_modules/pi-lens/dist/clients/opengrep-config.js +1 -1
  107. package/node_modules/pi-lens/dist/clients/path-utils.js +23 -2
  108. package/node_modules/pi-lens/dist/clients/performance-report.js +2 -2
  109. package/node_modules/pi-lens/dist/clients/php-cs-fixer-config.js +1 -1
  110. package/node_modules/pi-lens/dist/clients/pipeline.js +2 -2
  111. package/node_modules/pi-lens/dist/clients/project-changes.js +1 -1
  112. package/node_modules/pi-lens/dist/clients/project-diagnostics/extractors.js +1 -1
  113. package/node_modules/pi-lens/dist/clients/project-diagnostics/runner-adapters/jscpd.js +1 -1
  114. package/node_modules/pi-lens/dist/clients/project-diagnostics/runner-adapters/knip.js +1 -1
  115. package/node_modules/pi-lens/dist/clients/project-diagnostics/runner-adapters/madge.js +1 -1
  116. package/node_modules/pi-lens/dist/clients/project-diagnostics/runner-adapters/opengrep.js +1 -1
  117. package/node_modules/pi-lens/dist/clients/project-diagnostics/runner-adapters/runner-findings.js +1 -1
  118. package/node_modules/pi-lens/dist/clients/project-lens-config.js +46 -1
  119. package/node_modules/pi-lens/dist/clients/project-scan-policy.js +3 -51
  120. package/node_modules/pi-lens/dist/clients/python-provenance.js +1 -1
  121. package/node_modules/pi-lens/dist/clients/read-guard-logger.js +1 -1
  122. package/node_modules/pi-lens/dist/clients/read-guard.js +5 -2
  123. package/node_modules/pi-lens/dist/clients/recent-touches.js +2 -2
  124. package/node_modules/pi-lens/dist/clients/review-graph/builder.js +3 -5
  125. package/node_modules/pi-lens/dist/clients/review-graph/import-resolvers.js +1 -1
  126. package/node_modules/pi-lens/dist/clients/runtime-agent-end.js +1 -1
  127. package/node_modules/pi-lens/dist/clients/runtime-session.js +45 -10
  128. package/node_modules/pi-lens/dist/clients/runtime-tool-call.js +11 -1
  129. package/node_modules/pi-lens/dist/clients/sanitize.js +0 -11
  130. package/node_modules/pi-lens/dist/clients/scan-utils.js +1 -56
  131. package/node_modules/pi-lens/dist/clients/session-summary.js +0 -24
  132. package/node_modules/pi-lens/dist/clients/sgconfig.js +1 -5
  133. package/node_modules/pi-lens/dist/clients/shared-checkout-guard.js +1 -1
  134. package/node_modules/pi-lens/dist/clients/smells-rollup.js +2 -2
  135. package/node_modules/pi-lens/dist/clients/startup-scan.js +2 -10
  136. package/node_modules/pi-lens/dist/clients/test-runner-delivery.js +77 -72
  137. package/node_modules/pi-lens/dist/clients/tool-policy.js +1 -1
  138. package/node_modules/pi-lens/dist/clients/tree-sitter-client.js +1 -52
  139. package/node_modules/pi-lens/dist/clients/tui-fit.js +0 -4
  140. package/node_modules/pi-lens/dist/clients/user-notify.js +0 -4
  141. package/node_modules/pi-lens/dist/clients/word-index-logger.js +0 -7
  142. package/node_modules/pi-lens/dist/clients/word-index-store.js +3 -7
  143. package/node_modules/pi-lens/dist/clients/word-index.js +9 -16
  144. package/node_modules/pi-lens/dist/clients/zizmor-config.js +1 -1
  145. package/node_modules/pi-lens/dist/index.js +1062 -538
  146. package/node_modules/pi-lens/dist/mcp/build-staleness.js +1 -1
  147. package/node_modules/pi-lens/dist/scripts/lib/process-scan.mjs +1 -1
  148. package/node_modules/pi-lens/dist/scripts/lib/skills-predicate.mjs +1 -1
  149. package/node_modules/pi-lens/dist/tools/lens-diagnostics.js +1 -1
  150. package/node_modules/pi-lens/dist/tools/lsp-diagnostics.js +37 -6
  151. package/node_modules/pi-lens/dist/tools/lsp-structured-output.js +3 -3
  152. package/node_modules/pi-lens/dist/tools/render-compact.js +1 -1
  153. package/node_modules/pi-lens/dist/tools/symbol-search.js +1 -2
  154. package/node_modules/pi-lens/docs/globalconfig.md +20 -0
  155. package/node_modules/pi-lens/docs/lsp-capability-matrix.md +9 -1
  156. package/node_modules/pi-lens/docs/pi-lens-reviewer.md +6 -0
  157. package/node_modules/pi-lens/docs/pi-lens-subagent.md +12 -1
  158. package/node_modules/pi-lens/docs/release-qa-baseline.md +12 -4
  159. package/node_modules/pi-lens/docs/servercapabilities.md +7 -3
  160. package/node_modules/pi-lens/docs/word-index.md +1 -1
  161. package/node_modules/pi-lens/package.json +6 -15
  162. package/node_modules/pi-lens/rules/typos/_typos.toml +4 -2
  163. package/node_modules/pi-lens/scripts/lib/skills-predicate.mjs +1 -1
  164. package/node_modules/pi-lens/scripts/lib/warm-loader-cache.mjs +1 -1
  165. package/node_modules/pi-lens/scripts/warm-loader-cache.mjs +4 -2
  166. package/package.json +3 -3
@@ -45,6 +45,8 @@
45
45
  */
46
46
  import * as fs from "node:fs/promises";
47
47
  import * as path from "node:path";
48
+ import { HOOK_WALL_BUDGET_MS } from "../hook-budgets.js";
49
+ import { bounded } from "../deadline-utils.js";
48
50
  import { recordDegradationOnce } from "../degradation-ledger.js";
49
51
  import { commitDurableStoreAsync } from "../durable-store.js";
50
52
  import { pmBinary, resolveNodePackageManager, updateArgs, } from "../package-manager.js";
@@ -263,9 +265,28 @@ async function installedRefreshCandidates() {
263
265
  verificationTimeoutMs: tool.verificationTimeoutMs ?? 10_000,
264
266
  ...(tool.packageName !== undefined && { packageName: tool.packageName }),
265
267
  ...(tool.binaryName !== undefined && { binaryName: tool.binaryName }),
268
+ ...(tool.packageEntryOf !== undefined && {
269
+ packageEntryOf: tool.packageEntryOf,
270
+ }),
266
271
  });
267
272
  }
268
- return candidates;
273
+ const byPackage = new Map();
274
+ const distinct = [];
275
+ for (const candidate of candidates) {
276
+ if (candidate.strategy !== "npm" || !candidate.packageName) {
277
+ distinct.push(candidate);
278
+ continue;
279
+ }
280
+ const existing = byPackage.get(candidate.packageName);
281
+ if (existing) {
282
+ existing.coveredToolIds?.push(candidate.toolId);
283
+ continue;
284
+ }
285
+ const representative = { ...candidate, coveredToolIds: [candidate.toolId] };
286
+ byPackage.set(candidate.packageName, representative);
287
+ distinct.push(representative);
288
+ }
289
+ return distinct;
269
290
  }
270
291
  /**
271
292
  * Which candidates are due, oldest stamp first. A tool with no stamp sorts
@@ -276,15 +297,17 @@ export function selectStaleTools(candidates, state, now) {
276
297
  const interval = refreshIntervalMs();
277
298
  const retry = retryIntervalMs();
278
299
  const due = candidates.filter((candidate) => {
279
- const entry = state.tools[candidate.toolId];
280
- if (!entry)
281
- return true;
282
- const cooldown = entry.failed ? retry : interval;
283
- return now - entry.checkedAt >= cooldown;
300
+ return (candidate.coveredToolIds ?? [candidate.toolId]).some((toolId) => {
301
+ const entry = state.tools[toolId];
302
+ if (!entry)
303
+ return true;
304
+ const cooldown = entry.failed ? retry : interval;
305
+ return now - entry.checkedAt >= cooldown;
306
+ });
284
307
  });
285
308
  return due.sort((a, b) => {
286
- const aAt = state.tools[a.toolId]?.checkedAt ?? 0;
287
- const bAt = state.tools[b.toolId]?.checkedAt ?? 0;
309
+ const aAt = Math.min(...(a.coveredToolIds ?? [a.toolId]).map((toolId) => state.tools[toolId]?.checkedAt ?? 0));
310
+ const bAt = Math.min(...(b.coveredToolIds ?? [b.toolId]).map((toolId) => state.tools[toolId]?.checkedAt ?? 0));
288
311
  return aAt !== bAt ? aAt - bAt : a.toolId.localeCompare(b.toolId);
289
312
  });
290
313
  }
@@ -346,6 +369,7 @@ async function refreshNonNpmOne(candidate, entry, now) {
346
369
  changed: false,
347
370
  verified: false,
348
371
  ok: false,
372
+ attempted: false,
349
373
  };
350
374
  }
351
375
  // A real failure may have left a stale cached path or probe entry behind
@@ -383,6 +407,7 @@ async function refreshNonNpmOne(candidate, entry, now) {
383
407
  // touched the installed copy, so there is nothing new to check.
384
408
  verified: false,
385
409
  ok: false,
410
+ attempted: true,
386
411
  };
387
412
  }
388
413
  const currentVersion = attempt.version;
@@ -412,6 +437,7 @@ async function refreshNonNpmOne(candidate, entry, now) {
412
437
  // probe for pip/gem) — `ok: true` here already means "and it runs".
413
438
  verified: true,
414
439
  ok: true,
440
+ attempted: true,
415
441
  };
416
442
  }
417
443
  async function refreshNpmOne(candidate, now) {
@@ -435,6 +461,7 @@ async function refreshNpmOne(candidate, now) {
435
461
  changed: false,
436
462
  verified: false,
437
463
  ok: false,
464
+ attempted: false,
438
465
  };
439
466
  }
440
467
  try {
@@ -468,6 +495,7 @@ async function performNpmRefresh(candidate, packageName, now) {
468
495
  const elapsedMs = Date.now() - startedAt;
469
496
  if (spawned.status !== 0 || spawned.error) {
470
497
  const reason = (spawned.error?.message ?? spawned.stderr ?? "").slice(0, 200);
498
+ const coveredIds = candidate.coveredToolIds?.join(",") ?? candidate.toolId;
471
499
  // "Keeping <old version>" would be an unchecked assertion (#1746 review
472
500
  // F2). The spawn budget kills the package manager where it stands, which
473
501
  // can be mid-write: the tree may hold a new version, a half-written one,
@@ -479,9 +507,9 @@ async function performNpmRefresh(candidate, packageName, now) {
479
507
  recordDegradationOnce({
480
508
  kind: "managed-tool-refresh",
481
509
  subject: candidate.toolId,
482
- reason: `${pm} update failed: ${reason || "non-zero exit"}`,
510
+ reason: `package ${packageName}, covered ids ${coveredIds}; ${pm} update failed: ${reason || "non-zero exit"}`,
483
511
  });
484
- logSessionStart(`managed-tool-refresh ${candidate.toolId}: ${pm} update failed after ${elapsedMs}ms — on disk now ${onDisk ?? "unreadable"} (was ${previousVersion ?? "unknown"}); resolution cache cleared for re-probe (${reason || "non-zero exit"})`);
512
+ logSessionStart(`managed-tool-refresh ${candidate.toolId}: ${pm} update failed after ${elapsedMs}ms — package ${packageName}, covered ids ${coveredIds}; on disk now ${onDisk ?? "unreadable"} (was ${previousVersion ?? "unknown"}); resolution cache cleared for re-probe (${reason || "non-zero exit"})`);
485
513
  await writeRefreshStamp(candidate.toolId, {
486
514
  checkedAt: now,
487
515
  failed: true,
@@ -496,6 +524,7 @@ async function performNpmRefresh(candidate, packageName, now) {
496
524
  changed: onDisk !== undefined && onDisk !== previousVersion,
497
525
  verified: false,
498
526
  ok: false,
527
+ attempted: true,
499
528
  };
500
529
  }
501
530
  const currentVersion = await readInstalledVersion(packageName);
@@ -515,12 +544,17 @@ async function performNpmRefresh(candidate, packageName, now) {
515
544
  // drops npm entries that do not.
516
545
  const binaryName = candidate.binaryName;
517
546
  const binPath = resolveManagedNpmBinPath(binaryName);
518
- const verified = await verifyToolBinary(binPath, undefined, undefined, candidate.verificationTimeoutMs, candidate.checkArgs);
547
+ const verified = await verifyToolBinary(binPath, undefined, undefined, candidate.verificationTimeoutMs, candidate.checkArgs,
548
+ // #2722: the same verification the install used. Without this the daily
549
+ // refresh would demand a `--version` verdict from a server that can never
550
+ // give one — a ~4 MB doomed spawn that stamps the refresh failed forever.
551
+ candidate.packageEntryOf);
519
552
  if (!verified) {
553
+ const coveredIds = candidate.coveredToolIds?.join(",") ?? candidate.toolId;
520
554
  recordDegradationOnce({
521
555
  kind: "managed-tool-refresh",
522
556
  subject: candidate.toolId,
523
- reason: `binary failed verification after ${pm} update (${previousVersion ?? "unknown"} → ${currentVersion ?? "unknown"})`,
557
+ reason: `package ${packageName}, covered ids ${coveredIds}; binary failed verification after ${pm} update (${previousVersion ?? "unknown"} → ${currentVersion ?? "unknown"})`,
524
558
  });
525
559
  // Deliberately NOT removed. `installNpmTool` deletes a freshly installed
526
560
  // package that fails verification because nothing was working before it;
@@ -528,7 +562,7 @@ async function performNpmRefresh(candidate, packageName, now) {
528
562
  // offline for certain rather than probably. The cache invalidation above is
529
563
  // what lets `ensureTool`'s own probe-and-repair path see the breakage and
530
564
  // reinstall. Stamped as failed so the shorter retry cooldown applies.
531
- logSessionStart(`managed-tool-refresh ${candidate.toolId}: ${pm} update ran but ${binPath} failed verification — resolution cache cleared for re-probe (${elapsedMs}ms)`);
565
+ logSessionStart(`managed-tool-refresh ${candidate.toolId}: ${pm} update ran but ${binPath} failed verification — package ${packageName}, covered ids ${coveredIds}; resolution cache cleared for re-probe (${elapsedMs}ms)`);
532
566
  await writeRefreshStamp(candidate.toolId, {
533
567
  checkedAt: now,
534
568
  failed: true,
@@ -543,6 +577,7 @@ async function performNpmRefresh(candidate, packageName, now) {
543
577
  changed,
544
578
  verified: false,
545
579
  ok: false,
580
+ attempted: true,
546
581
  };
547
582
  }
548
583
  await writeRefreshStamp(candidate.toolId, {
@@ -552,8 +587,8 @@ async function performNpmRefresh(candidate, packageName, now) {
552
587
  // The observability row #1730 asks for: a wrong verdict from a managed tool
553
588
  // can be traced to the version that produced it, and to the moment it moved.
554
589
  logSessionStart(changed
555
- ? `managed-tool-refresh ${candidate.toolId}: ${previousVersion ?? "unknown"} → ${currentVersion} via ${pm} update, verified (${elapsedMs}ms)`
556
- : `managed-tool-refresh ${candidate.toolId}: unchanged at ${currentVersion ?? previousVersion ?? "unknown"} (${elapsedMs}ms)`);
590
+ ? `managed-tool-refresh ${candidate.toolId}: ${previousVersion ?? "unknown"} → ${currentVersion} via ${pm} update, verified; package ${packageName}, covered ids ${candidate.coveredToolIds?.join(",") ?? candidate.toolId} (${elapsedMs}ms)`
591
+ : `managed-tool-refresh ${candidate.toolId}: unchanged at ${currentVersion ?? previousVersion ?? "unknown"}; package ${packageName}, covered ids ${candidate.coveredToolIds?.join(",") ?? candidate.toolId} (${elapsedMs}ms)`);
557
592
  return {
558
593
  toolId: candidate.toolId,
559
594
  strategy: "npm",
@@ -563,6 +598,7 @@ async function performNpmRefresh(candidate, packageName, now) {
563
598
  changed,
564
599
  verified: true,
565
600
  ok: true,
601
+ attempted: true,
566
602
  };
567
603
  }
568
604
  /**
@@ -682,13 +718,22 @@ async function executeManagedToolRefresh(now, tap = noopTap) {
682
718
  // `/new` calls walked the entire 22-tool stale list. A local allowance
683
719
  // cannot be re-armed by anyone: a mid-run reset restores the SESSION's
684
720
  // right to start a new run, which is correct, without extending this one.
685
- const want = Math.min(maxPerSession(), stale.length);
721
+ const stalePackages = new Set();
722
+ const distinctStale = stale.filter((candidate) => {
723
+ if (candidate.strategy !== "npm" || !candidate.packageName)
724
+ return true;
725
+ if (stalePackages.has(candidate.packageName))
726
+ return false;
727
+ stalePackages.add(candidate.packageName);
728
+ return true;
729
+ });
730
+ const want = Math.min(maxPerSession(), distinctStale.length);
686
731
  while (held < want && reserveManagedToolRefreshSlot(maxPerSession())) {
687
732
  held += 1;
688
733
  }
689
734
  let allowance = held;
690
735
  const refreshed = [];
691
- for (const candidate of stale) {
736
+ for (const candidate of distinctStale) {
692
737
  // Once a spawn happens the slot is spent for good — a failing tool must
693
738
  // not hand its slot to the next candidate and turn a budget of one into
694
739
  // 22 spawns.
@@ -697,7 +742,28 @@ async function executeManagedToolRefresh(now, tap = noopTap) {
697
742
  allowance -= 1;
698
743
  held -= 1;
699
744
  try {
700
- refreshed.push(await refreshOne(candidate, read.state.tools[candidate.toolId], now));
745
+ const result = await refreshOne(candidate, read.state.tools[candidate.toolId], now);
746
+ refreshed.push(result);
747
+ if (result.attempted && candidate.coveredToolIds) {
748
+ for (const toolId of candidate.coveredToolIds) {
749
+ if (toolId === candidate.toolId)
750
+ continue;
751
+ await bounded(writeRefreshStamp(toolId, {
752
+ checkedAt: now,
753
+ ...(result.currentVersion !== undefined && {
754
+ version: result.currentVersion,
755
+ }),
756
+ ...(result.ok ? {} : { failed: true }),
757
+ }), {
758
+ ms: HOOK_WALL_BUDGET_MS.session_start,
759
+ // The unref'd timer runs after session_start returns, so no
760
+ // live turn signal belongs to this background refresh.
761
+ signal: undefined,
762
+ hook: "session_start",
763
+ label: "writeRefreshStamp:covered-tool",
764
+ });
765
+ }
766
+ }
701
767
  await tap();
702
768
  }
703
769
  catch (err) {
@@ -370,8 +370,8 @@ export function buildIdentityMatcher(cmdlines) {
370
370
  /** Post-kill liveness poll budget. `taskkill /F /T` returns before the kernel
371
371
  * has finished tearing the tree down, so a single immediate check would
372
372
  * report false `alive`s. */
373
- export const KILL_VERIFY_ATTEMPTS = 5;
374
- export const KILL_VERIFY_INTERVAL_MS = 100;
373
+ const KILL_VERIFY_ATTEMPTS = 5;
374
+ const KILL_VERIFY_INTERVAL_MS = 100;
375
375
  /** `setTimeout` as an awaitable, with the timer unref'd so a settled one-shot
376
376
  * `pi --print` never waits on best-effort kill verification. */
377
377
  function sleepUnref(ms) {
@@ -459,7 +459,7 @@ async function verifyPidGone(pid, options = {}) {
459
459
  * (script path), not the process image name, exactly like the existing
460
460
  * marker-search's WQL LIKE query below.
461
461
  */
462
- export const MANAGED_BINARIES = [
462
+ const MANAGED_BINARIES = [
463
463
  { name: "ast-grep", launcher: "native" },
464
464
  { name: "opengrep-core", launcher: "native" },
465
465
  { name: "opengrep", launcher: "native" },
@@ -844,11 +844,11 @@ function classifyKillOutcome(killed, unverified) {
844
844
  * not time-critical, so paying its cost once per half hour instead of once
845
845
  * per session_start removes essentially all of its aggregate cost.
846
846
  */
847
- export const BACKSTOP_COOLDOWN_MS = 30 * 60 * 1000;
847
+ const BACKSTOP_COOLDOWN_MS = 30 * 60 * 1000;
848
848
  /** How long after session_start the deferred sweep fires. Chosen to clear the
849
849
  * warmup window it was measured starving: `warmup_total` median 3288ms,
850
850
  * worst on record 5480ms (#1857). */
851
- export const BACKSTOP_START_DELAY_MS = 30_000;
851
+ const BACKSTOP_START_DELAY_MS = 30_000;
852
852
  /** Cap on process identities embedded in one latency record. */
853
853
  const BACKSTOP_IDENTITY_LOG_LIMIT = 10;
854
854
  /**
@@ -856,7 +856,7 @@ const BACKSTOP_IDENTITY_LOG_LIMIT = 10;
856
856
  * a candidate that was 1ms too fresh is comfortably past the grace by the
857
857
  * time the retry looks at it again.
858
858
  */
859
- export const BACKSTOP_GRACE_RETRY_MARGIN_MS = 30_000;
859
+ const BACKSTOP_GRACE_RETRY_MARGIN_MS = 30_000;
860
860
  /** How long the sweep lock may be held before another process reclaims it.
861
861
  * Well above the scan timeout plus the kill budget, and irrelevant when the
862
862
  * holder dies — the lock reclaims immediately on a dead owner pid
@@ -160,15 +160,6 @@ function computeProjectLanguageProfile(projectRoot, sourceFiles) {
160
160
  detectedKinds,
161
161
  };
162
162
  }
163
- export function hasLanguage(profile, kind) {
164
- return !!profile.present[kind];
165
- }
166
- export function hasAnyLanguage(profile, kinds) {
167
- return kinds.some((kind) => hasLanguage(profile, kind));
168
- }
169
- export function isLanguageConfigured(profile, kind) {
170
- return !!profile.configured[kind];
171
- }
172
163
  export function getDefaultStartupTools(profile) {
173
164
  const tools = new Set();
174
165
  for (const kind of Object.keys(LANGUAGE_POLICY)) {
@@ -121,7 +121,11 @@ export const PINNED_LANGUAGE_IDS = [
121
121
  * literal message below and `true` stops being assignable to it, so
122
122
  * `npm run build` fails at THIS line with the reason spelled out.
123
123
  */
124
- export const LANGUAGE_ID_PIN_IS_EXHAUSTIVE = true;
124
+ const LANGUAGE_ID_PIN_IS_EXHAUSTIVE = true;
125
+ // Not exported: its only job is to fail `npm run build`. The `void` reference
126
+ // is what keeps oxlint's no-unused-vars (CI-gating via lint:js) and knip
127
+ // quiet without giving it a consumer; tsc's noUnusedLocals does not flag it.
128
+ void LANGUAGE_ID_PIN_IS_EXHAUSTIVE;
125
129
  /** The registry. Sorted by id; one entry per language, one owner per extension. */
126
130
  export const LANGUAGES = [
127
131
  { id: "ada", extensions: [".adb", ".ads"] },
@@ -155,8 +155,6 @@ export function _recentPhasesStorageLengthForTest() {
155
155
  export function _setRecentPhasesForTest(entries) {
156
156
  recentPhases = entries;
157
157
  }
158
- /** Bound on the closed-bracket ring below — same size discipline as `recentPhases`. */
159
- export const CLOSED_BRACKET_CAP = RECENT_PHASE_CAP;
160
158
  /**
161
159
  * Phases currently executing, keyed by their own start token (#1723 review
162
160
  * round: this replaces an earlier single-slot design that broke two ways.
@@ -180,7 +178,7 @@ export const CLOSED_BRACKET_CAP = RECENT_PHASE_CAP;
180
178
  */
181
179
  const liveBrackets = new Map();
182
180
  /**
183
- * Recently CLOSED brackets, newest first, bounded to `CLOSED_BRACKET_CAP`
181
+ * Recently CLOSED brackets, newest first, bounded to `RECENT_PHASE_CAP`
184
182
  * (#1723 review round, F3 — the decisive finding). `phaseFinished` runs
185
183
  * inside a `finally`, which resumes as a MICROTASK, while the host schedules
186
184
  * `turn_end` as a MACROTASK — and microtasks always fully drain before the
@@ -210,7 +208,7 @@ export function phaseStarted(phase) {
210
208
  }
211
209
  /**
212
210
  * Close a bracket: removes it from `liveBrackets` (one `Map.delete`, O(1))
213
- * and records it on the closed-bracket ring, bounded to `CLOSED_BRACKET_CAP`
211
+ * and records it on the closed-bracket ring, bounded to `RECENT_PHASE_CAP`
214
212
  * with the oldest entry dropped first — see the `closedBrackets` doc comment
215
213
  * above for why a closed history is load-bearing, not just nice-to-have
216
214
  * (#1723 review F3). `Map.delete` reports whether it actually removed
@@ -246,7 +244,7 @@ export function phaseFinished(token) {
246
244
  closedAt: new Date().toISOString(),
247
245
  },
248
246
  ...closedBrackets,
249
- ].slice(0, CLOSED_BRACKET_CAP);
247
+ ].slice(0, RECENT_PHASE_CAP);
250
248
  }
251
249
  /**
252
250
  * The OLDEST phase still executing, if any — the longest-running open
@@ -267,7 +265,7 @@ export function getCurrentPhase() {
267
265
  * A candidate bracket is ignored outright if its OWN lifetime (`elapsedMs`)
268
266
  * is under this fraction of the window's length (#1723 review round 3, N4).
269
267
  * Overlap alone is not enough: the bounded closed-bracket ring can churn a
270
- * real culprit out (busy siblings filling `CLOSED_BRACKET_CAP`), leaving only
268
+ * real culprit out (busy siblings filling `RECENT_PHASE_CAP`), leaving only
271
269
  * a 1ms bracket that happens to have SOME positive overlap with an 18-second
272
270
  * window — reporting it would be a CONFIDENT WRONG ANSWER, worse than no
273
271
  * answer. 5%: a genuine cause's own duration should be a meaningful fraction
@@ -489,7 +487,7 @@ export function getPhaseForWindow(windowStartMs, windowEndMs) {
489
487
  /**
490
488
  * Test-only: the closed-bracket ring's actual storage length, mirroring
491
489
  * `_recentPhasesStorageLengthForTest` above — pins that `phaseFinished`'s
492
- * `.slice(0, CLOSED_BRACKET_CAP)` guard is intact independent of any
490
+ * `.slice(0, RECENT_PHASE_CAP)` guard is intact independent of any
493
491
  * read-side behavior (#1723 review: "ring unbounded" mutation).
494
492
  */
495
493
  export function _closedBracketsStorageLengthForTest() {
@@ -23,7 +23,27 @@ export const LEDGER_FIELD_MAX = 200;
23
23
  */
24
24
  export const DEGRADATION_ENTRIES_PER_KIND = 20;
25
25
  export function normalizeForLedger(value) {
26
- return String(value ?? "unknown");
26
+ if (value === null || value === undefined)
27
+ return "unknown";
28
+ // A plain object or array has no `toString` of its own, so `String()`
29
+ // writes "[object Object]" — a ledger row that names nothing. Serialise
30
+ // those; everything else (Error, Date, URL, primitives, class instances)
31
+ // keeps its `String()` form. An object carrying its OWN `toString` goes
32
+ // through `String()` too, so a throwing one still raises into the
33
+ // ledger's corrupted-input failsafe (#2703 r1 F1) instead of being
34
+ // admitted as "{}". Circular input degrades to a fixed marker.
35
+ if (typeof value === "object" && !Object.hasOwn(value, "toString")) {
36
+ const proto = Object.getPrototypeOf(value);
37
+ if (proto === Object.prototype || proto === null || Array.isArray(value)) {
38
+ try {
39
+ return JSON.stringify(value) ?? "unknown";
40
+ }
41
+ catch {
42
+ return "[unserializable object]";
43
+ }
44
+ }
45
+ }
46
+ return String(value);
27
47
  }
28
48
  /** Bound a value to `LEDGER_FIELD_MAX`, marking the elision. */
29
49
  export function truncateForLedger(value) {
@@ -133,6 +133,25 @@ export function loadPiLensGlobalConfig(configPath = getPiLensGlobalConfigPath())
133
133
  : undefined;
134
134
  if (ignore && ignore.length > 0)
135
135
  config.ignore = ignore;
136
+ const startup = asConfigObject(raw.startup);
137
+ if (startup) {
138
+ const startupConfig = {};
139
+ const mode = startup.mode;
140
+ const scans = asConfigObject(startup.scans);
141
+ if (mode === "quick" || mode === "full" || mode === "minimal")
142
+ startupConfig.mode = mode;
143
+ else if ("mode" in startup)
144
+ warnInvalid('startup.mode must be "quick", "full", or "minimal"');
145
+ if (scans) {
146
+ if (typeof scans.enabled === "boolean") {
147
+ startupConfig.scans = { enabled: scans.enabled };
148
+ }
149
+ else if ("enabled" in scans)
150
+ warnInvalid("startup.scans.enabled must be a boolean");
151
+ }
152
+ if (startupConfig.mode !== undefined || startupConfig.scans !== undefined)
153
+ config.startup = startupConfig;
154
+ }
136
155
  const dispatch = asConfigObject(raw.dispatch);
137
156
  if (dispatch) {
138
157
  const floor = dispatch.runnerTimeoutFloorMs;
@@ -28,14 +28,13 @@ import { loadProjectSnapshotWithoutWordIndex } from "./project-snapshot.js";
28
28
  import { getTreeSitterRuntimeStatus, } from "./tree-sitter-shared.js";
29
29
  import { centralityFromReverseDeps, getWordIndexBuildStatus, searchWordIndex, triggerBackgroundWordIndexBuild, } from "./word-index.js";
30
30
  // --- Facades (re-exported so adapters import only this module) ---------------
31
- export { analyzeFile, } from "./mcp/analyze.js";
31
+ export { analyzeFile } from "./mcp/analyze.js";
32
32
  export { createMcpHost } from "./mcp/host-shim.js";
33
- export { createWarmIpcLineReader, createWarmIpcRequestQueue, ipcPathForCwd, readTurnEndStatus, requestWarmAnalyze, WARM_TURN_END_SCHEMA_VERSION, } from "./mcp/ipc.js";
34
- export { analyzeFileFresh, canRebuildPiLens, REBUILD_UNAVAILABLE_MESSAGE, resolveRebuildScript, runRebuild, summarizeScan, } from "./mcp/review.js";
33
+ export { createWarmIpcLineReader, createWarmIpcRequestQueue, ipcPathForCwd, readTurnEndStatus, WARM_TURN_END_SCHEMA_VERSION, } from "./mcp/ipc.js";
34
+ export { analyzeFileFresh, canRebuildPiLens, resolveRebuildScript, runRebuild, summarizeScan, } from "./mcp/review.js";
35
35
  export { acknowledgeTurnEnd, runSessionStart, runTurnEnd, runTurnEndForIpc, } from "./mcp/session.js";
36
36
  export { moduleReport, readEnclosing, readSymbol, renderCompactModuleReport, } from "./module-report.js";
37
37
  export { projectReport, renderCompactProjectReport, } from "./project-report.js";
38
- export { createDefaultHostPorts, } from "./host-ports.js";
39
38
  // Effective-config introspection (#2427). A facade rather than a wrapper for
40
39
  // the same reason the others are: the computation reaches into the config
41
40
  // resolution, the LSP registry and the dispatch plan at once, and putting that
@@ -2,7 +2,7 @@ import { logExtension } from "./extension-log.js";
2
2
  import { logBusEvent } from "./bus-events-logger.js";
3
3
  import { normalizeFilePath } from "./path-utils.js";
4
4
  import { createLiveBusEmitter, recordStaleBusFailure, resolveLiveBusEmitter, } from "./live-bus-emitter.js";
5
- export const LENS_EVENT_VERSION = 1;
5
+ const LENS_EVENT_VERSION = 1;
6
6
  export const LENS_EVENT_NAMES = {
7
7
  analysisComplete: "pi-lens/analysis-complete",
8
8
  findings: "pi-lens/findings",
@@ -189,6 +189,22 @@ export const LENS_FLAGS = [
189
189
  default: false,
190
190
  scope: "global",
191
191
  },
192
+ ...[
193
+ "knip",
194
+ "jscpd",
195
+ "madge",
196
+ "gitleaks",
197
+ "govulncheck",
198
+ "deadCode",
199
+ "complexity",
200
+ ].map((analyzer) => ({
201
+ name: `no-${analyzer.replace(/[A-Z]/g, (letter) => `-${letter.toLowerCase()}`)}`,
202
+ description: `Disable the ${analyzer} session-start analyzer. Also via ${analyzer}.enabled=false in config.`,
203
+ configKey: `${analyzer}.enabled`,
204
+ negated: true,
205
+ default: false,
206
+ scope: "project",
207
+ })),
192
208
  ];
193
209
  const byName = new Map(LENS_FLAGS.map((spec) => [spec.name, spec]));
194
210
  export function getLensFlagSpec(name) {
@@ -216,6 +232,7 @@ export const GLOBAL_NON_FLAG_CONFIG_SECTIONS = [
216
232
  "ignore",
217
233
  "dispatch",
218
234
  "widget",
235
+ "startup",
219
236
  "$schema",
220
237
  ];
221
238
  /**
@@ -238,6 +255,7 @@ export const PROJECT_NON_FLAG_CONFIG_SECTIONS = [
238
255
  "reviewGraph",
239
256
  "trivy",
240
257
  "helm",
258
+ "startup",
241
259
  ];
242
260
  /**
243
261
  * The four LSP settings a PROJECT config file may carry — at its root during
@@ -24,7 +24,7 @@ import { normalizeMapKey, toProjectRelativePath } from "./path-utils.js";
24
24
  import { canonicalSourceTwin } from "./source-filter.js";
25
25
  import { buildOrUpdateGraph } from "./review-graph/service.js";
26
26
  import { detectFileRole } from "./file-role.js";
27
- export const DEFAULT_MAX_MAP_NODES = 500;
27
+ const DEFAULT_MAX_MAP_NODES = 500;
28
28
  /**
29
29
  * Aggregates a symbol-level {@link ReviewGraph} into file-level nodes/edges.
30
30
  * Pure and side-effect-free — no graph build here, no filesystem access — so
@@ -220,39 +220,3 @@ export function runLogCleanup(dbg) {
220
220
  }
221
221
  return results;
222
222
  }
223
- /**
224
- * Get current log storage summary
225
- */
226
- export function getLogStorageSummary() {
227
- const files = [];
228
- let totalMB = 0;
229
- // Main logs
230
- for (const name of getManagedLogFiles()) {
231
- const filePath = path.join(LOG_DIR, name);
232
- if (fs.existsSync(filePath)) {
233
- const sizeMB = getFileSizeMB(filePath);
234
- const ageDays = getFileAgeDays(filePath);
235
- files.push({ name, sizeMB, ageDays });
236
- totalMB += sizeMB;
237
- }
238
- }
239
- // Daily logs
240
- try {
241
- if (fs.existsSync(LOGS_SUBDIR)) {
242
- const dailyFiles = fs.readdirSync(LOGS_SUBDIR);
243
- for (const name of dailyFiles) {
244
- if (!name.endsWith(".jsonl"))
245
- continue;
246
- const filePath = path.join(LOGS_SUBDIR, name);
247
- const sizeMB = getFileSizeMB(filePath);
248
- const ageDays = getFileAgeDays(filePath);
249
- files.push({ name: `logs/${name}`, sizeMB, ageDays });
250
- totalMB += sizeMB;
251
- }
252
- }
253
- }
254
- catch {
255
- // Ignore
256
- }
257
- return { totalMB, files };
258
- }
@@ -1173,7 +1173,7 @@ export function resolveConfigurationSection(initialization, section) {
1173
1173
  // Exported (only) so tests can invoke the publishDiagnostics notification
1174
1174
  // handler directly against a mock LSPClientState/connection without spawning
1175
1175
  // a real language server. Not part of the public client API surface.
1176
- export function setupIncomingHandlers(state, initialization) {
1176
+ export function setupIncomingHandlers(state, initialization, onDiagnosticsPublished) {
1177
1177
  state.connection.onNotification("textDocument/publishDiagnostics", (params) => {
1178
1178
  // #1639: the settle operation's own clock for THIS publish. A
1179
1179
  // "quiet-window" settle's durationMs below measures from here (when
@@ -1188,6 +1188,7 @@ export function setupIncomingHandlers(state, initialization) {
1188
1188
  // that is no longer open on this client.
1189
1189
  if (state.closedDocuments?.has(normalizedPath))
1190
1190
  return;
1191
+ onDiagnosticsPublished?.(state.serverId);
1191
1192
  const newDiags = normalizeLspDiagnostics(params.diagnostics || []);
1192
1193
  const docVersion = params.version;
1193
1194
  if (PUB_DEBUG) {
@@ -3498,7 +3499,7 @@ async function resolveCodeActionBestEffort(state, action) {
3498
3499
  // --- Client Factory ---
3499
3500
  export async function createLSPClient(options) {
3500
3501
  installCrashGuard();
3501
- const { serverId, process: lspProcess, root, sessionCwd, initialization, initializeTimeoutMs = INITIALIZE_TIMEOUT_MS, launchVariant, } = options;
3502
+ const { serverId, process: lspProcess, root, sessionCwd, initialization, initializeTimeoutMs = INITIALIZE_TIMEOUT_MS, launchVariant, onDiagnosticsPublished, } = options;
3502
3503
  // #449/#472: register this LSP child in the cross-process instance registry
3503
3504
  // as soon as we have a live pid — BEFORE `initialize` completes, not after.
3504
3505
  // Registering early means a child that dies/hangs during initialize (the
@@ -3671,7 +3672,7 @@ export async function createLSPClient(options) {
3671
3672
  return;
3672
3673
  void safeSendNotification(state.connection, "workspace/didChangeWatchedFiles", { changes });
3673
3674
  });
3674
- setupIncomingHandlers(state, initialization);
3675
+ setupIncomingHandlers(state, initialization, onDiagnosticsPublished);
3675
3676
  connection.listen();
3676
3677
  setupConnectionLifecycle(state, recentStderr);
3677
3678
  let initResult;
@@ -336,6 +336,7 @@ export function createCustomServer(config, id) {
336
336
  return {
337
337
  id,
338
338
  name: config.name,
339
+ custom: true,
339
340
  extensions: config.extensions,
340
341
  root: config.rootMarkers
341
342
  ? createRootDetector(config.rootMarkers)
@@ -88,7 +88,7 @@ const SKIPPED = {
88
88
  tracked: 0,
89
89
  };
90
90
  /** Documents stat'd per pass. Bounds the per-pass cost on a large workspace. */
91
- export const DRIFT_CHECK_BATCH = 64;
91
+ const DRIFT_CHECK_BATCH = 64;
92
92
  /** Resyncs issued per pass. Paces the heal so a bulk edit cannot burst. */
93
93
  export const DRIFT_RESYNC_BATCH = 4;
94
94
  /** Tracked-document ceiling. A workspace sweep can open far more than this. */
@@ -131,7 +131,7 @@ function parseWorkspaceEdit(edit) {
131
131
  function formatRange(range) {
132
132
  return `${range.start.line + 1}:${range.start.character + 1}-${range.end.line + 1}:${range.end.character + 1}`;
133
133
  }
134
- export function rangesOverlap(a, b) {
134
+ function rangesOverlap(a, b) {
135
135
  return (comparePosition(a.start, b.end) < 0 && comparePosition(b.start, a.end) < 0);
136
136
  }
137
137
  function positionsEqual(a, b) {
@@ -351,7 +351,7 @@ export async function normalizeWorkspaceEditToUtf16(edit, positionEncoding, cwd)
351
351
  ...(edit.documentChanges !== undefined ? { documentChanges } : {}),
352
352
  };
353
353
  }
354
- export function flattenWorkspaceTextEdits(edit) {
354
+ function flattenWorkspaceTextEdits(edit) {
355
355
  parseWorkspaceEdit(edit);
356
356
  const buckets = new Map();
357
357
  const push = (uri, edits) => {
@@ -786,7 +786,7 @@ async function createWorkspaceUriConfiner(cwd) {
786
786
  filePath = uriToPath(uri);
787
787
  }
788
788
  catch (err) {
789
- throw new Error(`invalid workspace edit URI ${uri}: ${err instanceof Error ? err.message : String(err)}`);
789
+ throw new Error(`invalid workspace edit URI ${uri}: ${err instanceof Error ? err.message : String(err)}`, { cause: err });
790
790
  }
791
791
  if (!path.isAbsolute(filePath))
792
792
  throw new Error(`workspace edit path escapes workspace: ${uri}`);