@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
@@ -302,7 +302,7 @@ function matchesGitignorePattern(pattern, relativePath, isDirectory, matchExpand
302
302
  return candidates.some((value) => matchExpanded(value, expanded));
303
303
  });
304
304
  }
305
- export function readGitignorePatterns(rootDir, layer = "gitignore") {
305
+ function readGitignorePatterns(rootDir, layer = "gitignore") {
306
306
  const gitignorePath = path.join(rootDir, ".gitignore");
307
307
  try {
308
308
  return parseGitignoreContent(fs.readFileSync(gitignorePath, "utf-8"), layer);
@@ -762,7 +762,11 @@ export function readGitignoreDirs(rootDir) {
762
762
  .map((entry) => entry.pattern);
763
763
  }
764
764
  function globToRegExp(glob) {
765
+ // Directory names use the same `*`-only dialect as read-guard exemptions.
766
+ // Collapse adjacent stars before compiling to avoid nullable-group
767
+ // backtracking on a non-matching name (#2622).
765
768
  const escaped = glob
769
+ .replace(/\*+/g, "*")
766
770
  .replace(/[.+^${}()|[\]\\]/g, "\\$&")
767
771
  .replace(/\*/g, ".*")
768
772
  .replace(/\?/g, ".");
@@ -202,4 +202,3 @@ export function clearFormatServiceAndFileState() {
202
202
  resetFormatService();
203
203
  }
204
204
  // Re-export for convenience
205
- export { clearAllSessions } from "./file-time.js";
@@ -11,10 +11,11 @@
11
11
  import { logExtension } from "./extension-log.js";
12
12
  import { AsyncLocalStorage } from "node:async_hooks";
13
13
  import * as fs from "node:fs/promises";
14
+ import * as os from "node:os";
14
15
  import * as path from "node:path";
15
16
  import { BoundedLruCache } from "./bounded-cache.js";
16
17
  import { createGenerationSource } from "./generation-guard.js";
17
- import { normalizeMapKey } from "./path-utils.js";
18
+ import { findNearestMarkerRoot, isRealGitMarker, normalizeMapKey, } from "./path-utils.js";
18
19
  import { resolveCargoPackageEdition } from "./cargo-manifest.js";
19
20
  import { resolveKtfmtGradleStyle } from "./gradle-ktfmt-style.js";
20
21
  import { resolvePhpCsFixerConfig } from "./php-cs-fixer-config.js";
@@ -602,6 +603,110 @@ async function indentationArgs(filePath, tool, cwd) {
602
603
  String(indentation.width),
603
604
  ];
604
605
  }
606
+ const FORMATTER_MARKERS_BY_NAME = new Map([
607
+ ["biome", ["biome.json", "biome.jsonc", "package.json"]],
608
+ [
609
+ "prettier",
610
+ [
611
+ ".prettierrc",
612
+ ".prettierrc.json",
613
+ ".prettierrc.yaml",
614
+ ".prettierrc.yml",
615
+ ".prettierrc.js",
616
+ ".prettierrc.cjs",
617
+ ".prettierrc.mjs",
618
+ "prettier.config.js",
619
+ "prettier.config.cjs",
620
+ "prettier.config.mjs",
621
+ ".prettierignore",
622
+ "package.json",
623
+ ],
624
+ ],
625
+ [
626
+ "oxfmt",
627
+ ["oxfmt.toml", ".oxfmtrc.json", "vite-plus.json", "package.json"],
628
+ ],
629
+ ["ruff", ["pyproject.toml", "ruff.toml", ".ruff.toml"]],
630
+ ["black", ["pyproject.toml", "black.toml", ".black"]],
631
+ ["sqlfluff", [".sqlfluff", "pyproject.toml", "setup.cfg"]],
632
+ ["rustfmt", ["rustfmt.toml", ".rustfmt.toml", "Cargo.toml"]],
633
+ ["rubocop", [".rubocop.yml", ".rubocop.yaml"]],
634
+ ["standardrb", [".standard.yml", ".standard.yaml"]],
635
+ ["clang-format", [".clang-format", "_clang-format"]],
636
+ ["php-cs-fixer", [".php-cs-fixer.php", ".php-cs-fixer.dist.php"]],
637
+ ["stylua", ["stylua.toml", ".stylua.toml"]],
638
+ ["ocamlformat", [".ocamlformat"]],
639
+ ["google-java-format", [".google-java-format", ".editorconfig"]],
640
+ ["cljfmt", [".cljfmt.edn", "cljfmt.edn", ".cljfmt"]],
641
+ [
642
+ "cmake-format",
643
+ [
644
+ ".cmake-format",
645
+ ".cmake-format.yaml",
646
+ ".cmake-format.yml",
647
+ ".cmake-format.json",
648
+ ".cmake-format.py",
649
+ "cmake-format.yaml",
650
+ "cmake-format.yml",
651
+ ".editorconfig",
652
+ ],
653
+ ],
654
+ [
655
+ "psscriptanalyzer-format",
656
+ ["PSScriptAnalyzerSettings.psd1", "ScriptAnalyzerSettings.psd1"],
657
+ ],
658
+ [
659
+ "csharpier",
660
+ [
661
+ ".csharpierrc",
662
+ ".csharpierrc.json",
663
+ ".csharpierrc.yaml",
664
+ ".csharpierrc.yml",
665
+ ],
666
+ ],
667
+ ["ormolu", [".ormolu"]],
668
+ ["taplo", ["taplo.toml", ".taplo.toml"]],
669
+ ["terraform", [".terraform.lock.hcl"]],
670
+ ["swiftformat", [".swiftformat"]],
671
+ ["fantomas", [".fantomasignore", ".editorconfig"]],
672
+ ["mix", [".formatter.exs"]],
673
+ ["shfmt", [".editorconfig"]],
674
+ ["ktlint", [".editorconfig"]],
675
+ [
676
+ "ktfmt",
677
+ [
678
+ ".editorconfig",
679
+ ".ktfmt",
680
+ ".ktfmt.kts",
681
+ "build.gradle",
682
+ "build.gradle.kts",
683
+ "settings.gradle",
684
+ "settings.gradle.kts",
685
+ ],
686
+ ],
687
+ ]);
688
+ /**
689
+ * Resolve the cwd for the formatter child, capped at the user's home
690
+ * directory. Files outside a project retain the historical file-directory
691
+ * cwd. The optional homeDir is only for the ceiling regression test.
692
+ */
693
+ export function resolveFormatterCwd(absolutePath, formatterName, homeDir = os.homedir()) {
694
+ const fileDir = path.dirname(path.resolve(absolutePath));
695
+ const effectiveHome = homeDir || undefined;
696
+ const markers = formatterName
697
+ ? [...(FORMATTER_MARKERS_BY_NAME.get(formatterName) ?? []), ".gitignore"]
698
+ : [".gitignore"];
699
+ const root = findNearestMarkerRoot(fileDir, markers, {
700
+ homeDir: effectiveHome,
701
+ });
702
+ if (root)
703
+ return root;
704
+ const gitRoot = findNearestMarkerRoot(fileDir, [".git"], {
705
+ homeDir: effectiveHome,
706
+ markerPredicate: isRealGitMarker,
707
+ });
708
+ return gitRoot ?? fileDir;
709
+ }
605
710
  /**
606
711
  * Every biome invocation must carry this. Biome exits 1 with "No files were
607
712
  * processed in the specified paths" when the path is ignored by the repo's own
@@ -1438,7 +1543,7 @@ const detectionCache = new BoundedLruCache(32);
1438
1543
  // The signature is immutable for a cache generation. This memo is separate
1439
1544
  // from detectionCache because a cwd can have several extension entries, and a
1440
1545
  // warm lookup must not repeat the ancestor walk or stat matched configs.
1441
- const formatterSignatureFlights = new Map();
1546
+ const formatterSignatureFlights = new BoundedLruCache(32);
1442
1547
  const formatterCacheGeneration = createGenerationSource("formatter-cache");
1443
1548
  // These are the formatter configuration files consulted by the policy helpers
1444
1549
  // above. Their metadata is captured by the cold detection signature. The
@@ -1810,6 +1915,7 @@ export function _getFormatterResetStateForTests() {
1810
1915
  whichTransientCommands,
1811
1916
  cooldownRecordedForRetryAtMs,
1812
1917
  detectionCache,
1918
+ formatterSignatureFlights,
1813
1919
  };
1814
1920
  }
1815
1921
  export function clearFormatterRuntimeState() {
@@ -1887,6 +1993,7 @@ export async function formatFile(filePath, formatter) {
1887
1993
  try {
1888
1994
  const absolutePath = path.resolve(filePath);
1889
1995
  const cwd = path.dirname(absolutePath);
1996
+ const formatterCwd = resolveFormatterCwd(absolutePath, formatter.name);
1890
1997
  const contentBefore = await fs.readFile(absolutePath, "utf-8");
1891
1998
  // Resolve command: prefer local (venv/vendor/node_modules) over global.
1892
1999
  // The shared seam must honor SKIP_FORMATTING before selecting the static
@@ -1912,7 +2019,7 @@ export async function formatFile(filePath, formatter) {
1912
2019
  // Run formatter without blocking the event loop.
1913
2020
  const result = await safeSpawnAsync(cmd[0], cmd.slice(1), {
1914
2021
  timeout: 15000,
1915
- cwd,
2022
+ cwd: formatterCwd,
1916
2023
  });
1917
2024
  // A resolver that could NOT prove absence (it never probed PATH — e.g.
1918
2025
  // black/sqlfluff, which only look in a venv) legitimately falls back to the
@@ -205,7 +205,7 @@ function hasGeneratedArtifactContent(content) {
205
205
  * file is not machine-emitted evidence), and an empty or whitespace-only
206
206
  * file yields a zero mean and stays source.
207
207
  */
208
- export const MACHINE_EMITTED_LINE_SHAPE_MIN_CONTENT = 2048;
208
+ const MACHINE_EMITTED_LINE_SHAPE_MIN_CONTENT = 2048;
209
209
  export const MACHINE_EMITTED_LINE_SHAPE_MEAN_THRESHOLD = 2500;
210
210
  export function classifyMachineEmittedLineShape(content) {
211
211
  const contentLength = content.length;
@@ -325,8 +325,7 @@ function analyzeFileHeader(filePath, maxBytes) {
325
325
  * function and short-circuit on a hit, so by the time this function runs for
326
326
  * those callers evidence (a) has already come back negative. Non-walk
327
327
  * callers with no upstream sibling probe (`file-role.ts`'s content-based
328
- * check, `project-scan-policy.ts`'s single-path `shouldSkipProjectPath`)
329
- * evaluate evidence (b) only — a documented, narrower guarantee than the
328
+ * check) evaluate evidence (b) only — a documented, narrower guarantee than the
330
329
  * walk path's.
331
330
  *
332
331
  * Tradeoff (documented per the issue): when NEITHER piece of evidence
@@ -387,7 +386,7 @@ export function classifyGeneratedOrArtifactDetailed(filePath, options = {}) {
387
386
  : { verdict: "clean" };
388
387
  }
389
388
  /** Verdict-only convenience wrapper over {@link classifyGeneratedOrArtifactDetailed}. */
390
- export function classifyGeneratedOrArtifact(filePath, options = {}) {
389
+ function classifyGeneratedOrArtifact(filePath, options = {}) {
391
390
  return classifyGeneratedOrArtifactDetailed(filePath, options).verdict;
392
391
  }
393
392
  export function isGeneratedOrArtifact(filePath, options = {}) {
@@ -141,6 +141,11 @@ function canonicalizeGuardCommand(command) {
141
141
  let pendingSpace = false;
142
142
  quote = undefined;
143
143
  for (const ch of result) {
144
+ if (!quote && ch === "\n") {
145
+ collapsed += ch;
146
+ pendingSpace = false;
147
+ continue;
148
+ }
144
149
  if (!quote && /\s/.test(ch)) {
145
150
  pendingSpace = collapsed.length > 0;
146
151
  continue;
@@ -18,7 +18,7 @@ import { getPackageRoot } from "./package-root.js";
18
18
  /** tree-sitter-wasms release the grammars are pulled from. */
19
19
  export const TREE_SITTER_WASMS_VERSION = "0.1.13";
20
20
  /** unpkg mirror of the tree-sitter-wasms artifacts. */
21
- export const GRAMMAR_CDN_BASE = `https://unpkg.com/tree-sitter-wasms@${TREE_SITTER_WASMS_VERSION}/out`;
21
+ const GRAMMAR_CDN_BASE = `https://unpkg.com/tree-sitter-wasms@${TREE_SITTER_WASMS_VERSION}/out`;
22
22
  export const GRAMMAR_SOURCE_OVERRIDES = {
23
23
  "tree-sitter-lua.wasm": {
24
24
  package: "@tree-sitter-grammars/tree-sitter-lua",
@@ -145,7 +145,7 @@ export const BLOCKED_GRAMMARS = {
145
145
  },
146
146
  };
147
147
  /** Runtime signals for the currently-running process. */
148
- export function currentGrammarRuntime() {
148
+ function currentGrammarRuntime() {
149
149
  const m = /^v?(\d+)/.exec(process.versions?.node ?? "");
150
150
  return {
151
151
  nodeMajor: m ? Number(m[1]) : 0,
@@ -173,7 +173,7 @@ export function grammarBlockReason(filename, rt = currentGrammarRuntime()) {
173
173
  * both the download path (validate before writing) and the on-disk path
174
174
  * (`fileHasWasmMagic`, for a file poisoned before this shipped) key off it.
175
175
  */
176
- export const WASM_MAGIC = [0x00, 0x61, 0x73, 0x6d];
176
+ const WASM_MAGIC = [0x00, 0x61, 0x73, 0x6d];
177
177
  /** Do `bytes` start with the wasm magic number? */
178
178
  export function hasWasmMagic(bytes) {
179
179
  if (bytes.length < WASM_MAGIC.length)
@@ -548,10 +548,27 @@ export const TOOLS = [
548
548
  binaryName: "docker-langserver",
549
549
  },
550
550
  {
551
+ // #2722: intelephense has no CLI — its entry calls createConnection()
552
+ // unconditionally — and Node prints the offending source line before the
553
+ // error. The bundle is one ~4 MB minified line, so the transport-required
554
+ // marker #208 rescues on lands at byte 4,154,741 of a 4,423,356-byte
555
+ // stderr while Node truncates piped stderr at 1 MiB on exit. Measured on
556
+ // intelephense@1.18.5, linux, Node v22.22.1:
557
+ // $ intelephense --version 2>&1 | wc -c -> 1048576
558
+ // $ intelephense --version 2>&1 | grep -c "Connection input stream is not set" -> 0
559
+ // $ intelephense --version 2>err.txt; wc -c < err.txt -> 4423356
560
+ // Same run over the alternatives, each with stdin closed the way
561
+ // verifyToolBinary closes it (`input: ""`):
562
+ // --help, -v, --socket=0 -> identical dump (exit 1, 4423356 bytes,
563
+ // marker at 4154741, 1048576 through a pipe, marker absent)
564
+ // --stdio -> exit 1 with ZERO bytes on either stream,
565
+ // so it carries no marker to rescue on either.
566
+ // No checkArgs value produces a verdict. Verified spawn-free instead.
551
567
  id: "intelephense",
552
568
  name: "Intelephense",
553
569
  checkCommand: "intelephense",
554
570
  checkArgs: ["--version"],
571
+ verification: "package-entry",
555
572
  installStrategy: "npm",
556
573
  packageName: "intelephense",
557
574
  binaryName: "intelephense",
@@ -1851,6 +1868,111 @@ function extractVersionToken(output) {
1851
1868
  * maven/archive) never populate or read this map.
1852
1869
  */
1853
1870
  const lastManagedInstallVersion = new Map();
1871
+ /**
1872
+ * The package whose entry module verifies `tool`'s managed install spawn-free,
1873
+ * or undefined when the tool is verified by spawning `checkArgs` (#2722).
1874
+ * Registry-declared per tool — never inferred from the strategy: a spawn probe
1875
+ * is the stronger check everywhere it can actually return a verdict.
1876
+ */
1877
+ export function packageEntryVerification(tool) {
1878
+ return tool.verification === "package-entry" ? tool.packageName : undefined;
1879
+ }
1880
+ /**
1881
+ * Spawn-free verification of a managed npm install (#2722).
1882
+ *
1883
+ * For an stdio LSP server with no CLI surface, every `checkArgs` value is a
1884
+ * throw, and #208's transport-required rescue is output-ORDER dependent: Node
1885
+ * prints the offending source line ahead of the error, so a server whose bundle
1886
+ * is megabytes of minified JS pushes the marker past both our retained window
1887
+ * and Node's own 1 MiB pipe truncation. The bytes never leave the child, so no
1888
+ * spawn can answer "is this install intact".
1889
+ *
1890
+ * The evidence used instead is the same class the `archive` strategy already
1891
+ * accepts through `treeMarker`: the installed tree is inspected on disk. The
1892
+ * install verifies when the package directory beside the `.bin` shim carries a
1893
+ * readable `package.json` with a `version`, and the entry module that
1894
+ * package.json itself names — `bin[<shim name>]` or a bare `bin` string —
1895
+ * exists as a non-empty file. `main` is deliberately NOT a fallback: npm
1896
+ * creates the `node_modules/.bin/<shim>` this function is given only from a
1897
+ * `bin` field, so a manifest without one cannot be the manifest behind this
1898
+ * shim. That is exactly what `verifyToolBinary` was documented to catch here —
1899
+ * broken symlinks and partial installs — and it is strictly more than the
1900
+ * `--version` spawn can report for this class.
1901
+ */
1902
+ export async function verifyNpmPackageEntry(binPath, packageName) {
1903
+ // `<...>/node_modules/.bin/<shim>` → `<...>/node_modules`.
1904
+ const nodeModulesDir = path.dirname(path.dirname(binPath));
1905
+ const pinned = parsePinnedVersion(packageName);
1906
+ const bareName = pinned
1907
+ ? packageName.slice(0, -(pinned.length + 1))
1908
+ : packageName;
1909
+ const packageDir = path.join(nodeModulesDir, ...bareName.split("/"));
1910
+ const shimName = path
1911
+ .basename(binPath)
1912
+ .replace(/\.(cmd|exe|ps1|bat)$/i, "")
1913
+ .toLowerCase();
1914
+ const fail = (reason) => {
1915
+ logSessionStart(`auto-install verify: failed for ${binPath} (check=package-entry, kind=${reason})`);
1916
+ return false;
1917
+ };
1918
+ // R2-F1: the shim ITSELF, before anything else. Every path below is DERIVED
1919
+ // from `binPath`, so without this a partial install — npm never wrote the
1920
+ // `.bin` entry, or wrote one that is empty, a directory, or a symlink
1921
+ // dangling at a deleted target — verified `true` on the strength of a
1922
+ // package tree that nothing can execute, and `installNpmTool` then recorded
1923
+ // the install as SUCCEEDED. `classifyInstallOutcome` grades any non-"failed"
1924
+ // outcome as `⚠ unavailable (succeeded)`, so that answer re-hid exactly the
1925
+ // nightly row #2722 exists to expose. `statSync` FOLLOWS the link, which is
1926
+ // what makes missing and dangling one branch.
1927
+ try {
1928
+ const shim = statSync(binPath);
1929
+ if (!shim.isFile() || shim.size === 0)
1930
+ return fail("shim-not-a-file");
1931
+ }
1932
+ catch {
1933
+ return fail("shim-missing");
1934
+ }
1935
+ let manifest;
1936
+ try {
1937
+ manifest = JSON.parse(await fs.readFile(path.join(packageDir, "package.json"), "utf8"));
1938
+ }
1939
+ catch {
1940
+ return fail("package-json-unreadable");
1941
+ }
1942
+ if (typeof manifest.version !== "string" || !manifest.version) {
1943
+ return fail("package-json-no-version");
1944
+ }
1945
+ const bin = manifest.bin;
1946
+ const entry = typeof bin === "string"
1947
+ ? bin
1948
+ : bin && typeof bin === "object"
1949
+ ? Object.entries(bin).find(
1950
+ // Case-folded on purpose: on a case-insensitive filesystem the
1951
+ // shim on disk can differ in case from the manifest key npm
1952
+ // wrote it from, and `path.basename` reads the disk name.
1953
+ ([name]) => name.toLowerCase() === shimName)?.[1]
1954
+ : undefined;
1955
+ if (typeof entry !== "string" || !entry) {
1956
+ return fail("package-json-no-entry");
1957
+ }
1958
+ try {
1959
+ const stat = statSync(path.join(packageDir, entry));
1960
+ if (!stat.isFile() || stat.size === 0)
1961
+ return fail("entry-not-a-file");
1962
+ }
1963
+ catch {
1964
+ return fail("entry-missing");
1965
+ }
1966
+ // R2-F3 (catalog shape 31): "did the new verifier run at all, and on what"
1967
+ // has to be answerable from sessionstart.log, not only from a debug build —
1968
+ // the failure branches above already log there. Measured volume on a real
1969
+ // intelephense install, scratch PI_LENS_HOME: TWO rows on the session that
1970
+ // installs (installNpmTool's verify, then the post-install resolution), and
1971
+ // ZERO on every later session — the probe cache answers before any verify
1972
+ // runs (`auto-install ensure intelephense: probe cache hit`).
1973
+ logSessionStart(`auto-install verify: succeeded for ${binPath} (check=package-entry, version=${manifest.version}, entry=${entry})`);
1974
+ return true;
1975
+ }
1854
1976
  /**
1855
1977
  * Verify a tool binary actually works by running its configured checkArgs.
1856
1978
  * This catches broken symlinks, partial installs, and corrupted binaries.
@@ -1878,7 +2000,26 @@ onTransient,
1878
2000
  * sensitive caller on the dispatch hot path passes a shorter one and treats
1879
2001
  * the expiry as transient rather than as a verdict (#1657).
1880
2002
  */
1881
- timeoutMs = 10000, verificationArgs = ["--version"]) {
2003
+ timeoutMs = 10000, verificationArgs = ["--version"],
2004
+ /**
2005
+ * Package name from {@link packageEntryVerification} — present only for a
2006
+ * registry entry declaring `verification: "package-entry"`. When present the
2007
+ * spawn is skipped entirely and {@link verifyNpmPackageEntry} answers (#2722).
2008
+ */
2009
+ packageEntryOf,
2010
+ /**
2011
+ * Called when a `false` verdict is INCONCLUSIVE rather than a verdict: the
2012
+ * transport-required matcher was armed, never matched, and the child's
2013
+ * output was cut off before the tail arrived, so the #208 rescue could not
2014
+ * be evaluated at all (#2722). Distinct from `onTransient` on purpose — the
2015
+ * prober DID run to completion and re-probing reproduces it exactly, so this
2016
+ * is a durable "cannot be verified", not a stall (catalog shape 13). A
2017
+ * caller must not treat it as "broken" and delete the installation.
2018
+ */
2019
+ onInconclusive) {
2020
+ if (packageEntryOf !== undefined) {
2021
+ return verifyNpmPackageEntry(binPath, packageEntryOf);
2022
+ }
1882
2023
  // #2015: safeSpawnAsync instead of raw spawn. Raw spawn's timeout
1883
2024
  // SIGTERMed only cmd.exe on Windows (.cmd shims run shell:true), orphaning
1884
2025
  // the grandchild node process - which kept scanning, held handles against
@@ -1937,6 +2078,31 @@ timeoutMs = 10000, verificationArgs = ["--version"]) {
1937
2078
  // verdict from the binary (#1569 transient semantics).
1938
2079
  if (result.signal !== undefined || result.spawnFailure)
1939
2080
  onTransient?.();
2081
+ if (result.outputTruncated &&
2082
+ result.signal === undefined &&
2083
+ !result.spawnFailure) {
2084
+ // #2722: the transport-required matcher above is armed on EVERY probe,
2085
+ // and it never matched — but the output we kept is a prefix, so the
2086
+ // marker may simply sit past it (intelephense emits ~4 MB of bundle
2087
+ // ahead of its own diagnostic). "Unmatched within a truncated prefix"
2088
+ // is not "this binary is broken"; callers must not delete the install
2089
+ // on it. Recorded as its own kind so a monitor can tell an unreadable
2090
+ // probe apart from a rejected binary.
2091
+ //
2092
+ // R2-F4: gated on the probe having actually FINISHED. A verbose child
2093
+ // that is SIGTERMed at the timeout also arrives here truncated, and
2094
+ // that is the #1569 transient class one line above, not this one —
2095
+ // ungated, the two overlapped and `installNpmTool` (which tests
2096
+ // inconclusive first) replaced the #2015 transient message with this
2097
+ // one. The doc comment's "the prober DID run to completion" is now
2098
+ // true rather than aspirational.
2099
+ onInconclusive?.();
2100
+ recordDegradationOnce({
2101
+ kind: "installer-verification-inconclusive",
2102
+ subject: binPath,
2103
+ reason: `transport-required marker unresolved in truncated output (${verificationArgs.join(" ")})`,
2104
+ });
2105
+ }
1940
2106
  const kind = result.spawnFailure?.kind ??
1941
2107
  (result.signal
1942
2108
  ? `killed-signal=${result.signal}`
@@ -2056,7 +2222,7 @@ export async function getAllToolStatuses() {
2056
2222
  const localPath = installerPlatform() === "win32" ? `${localBase}.cmd` : localBase;
2057
2223
  try {
2058
2224
  await fs.access(localPath);
2059
- if (await verifyToolBinary(localPath, undefined, undefined, getToolVerificationTimeout(tool), tool.checkArgs)) {
2225
+ if (await verifyToolBinary(localPath, undefined, undefined, getToolVerificationTimeout(tool), tool.checkArgs, packageEntryVerification(tool))) {
2060
2226
  status.installed = true;
2061
2227
  status.source = "pi-lens-auto";
2062
2228
  status.path = localPath;
@@ -2225,7 +2391,7 @@ async function getToolPathResolved(toolId, onTransient) {
2225
2391
  const cmdPath = `${localBase}.cmd`;
2226
2392
  try {
2227
2393
  await fs.access(cmdPath);
2228
- if (await verifyToolBinary(cmdPath, recordVersion, onTransient, getToolVerificationTimeout(tool), tool.checkArgs)) {
2394
+ if (await verifyToolBinary(cmdPath, recordVersion, onTransient, getToolVerificationTimeout(tool), tool.checkArgs, packageEntryVerification(tool))) {
2229
2395
  return cmdPath;
2230
2396
  }
2231
2397
  logSessionStart(`auto-install verify: ${cmdPath} exists but is broken, will reinstall`);
@@ -2238,7 +2404,7 @@ async function getToolPathResolved(toolId, onTransient) {
2238
2404
  const exePath = `${localBase}.exe`;
2239
2405
  try {
2240
2406
  await fs.access(exePath);
2241
- if (await verifyToolBinary(exePath, recordVersion, onTransient, getToolVerificationTimeout(tool), tool.checkArgs)) {
2407
+ if (await verifyToolBinary(exePath, recordVersion, onTransient, getToolVerificationTimeout(tool), tool.checkArgs, packageEntryVerification(tool))) {
2242
2408
  return exePath;
2243
2409
  }
2244
2410
  logSessionStart(`auto-install verify: ${exePath} exists but is broken, will reinstall`);
@@ -2250,7 +2416,7 @@ async function getToolPathResolved(toolId, onTransient) {
2250
2416
  if (installerPlatform() !== "win32") {
2251
2417
  try {
2252
2418
  await fs.access(localBase);
2253
- if (await verifyToolBinary(localBase, recordVersion, onTransient, getToolVerificationTimeout(tool), tool.checkArgs)) {
2419
+ if (await verifyToolBinary(localBase, recordVersion, onTransient, getToolVerificationTimeout(tool), tool.checkArgs, packageEntryVerification(tool))) {
2254
2420
  return localBase;
2255
2421
  }
2256
2422
  logSessionStart(`auto-install verify: ${localBase} exists but is broken, will reinstall`);
@@ -2873,6 +3039,7 @@ export function npmToolNeedsPostinstall(packageName) {
2873
3039
  * that wanders off it.
2874
3040
  */
2875
3041
  export function getRefreshableManagedNpmTools() {
3042
+ const seenPackages = new Set();
2876
3043
  return getRefreshableManagedTools()
2877
3044
  .filter((candidate) => candidate.strategy === "npm")
2878
3045
  .map((candidate) => ({
@@ -2881,7 +3048,13 @@ export function getRefreshableManagedNpmTools() {
2881
3048
  // have both, so these are total.
2882
3049
  packageName: candidate.packageName,
2883
3050
  binaryName: candidate.binaryName,
2884
- }));
3051
+ }))
3052
+ .filter((candidate) => {
3053
+ if (seenPackages.has(candidate.packageName))
3054
+ return false;
3055
+ seenPackages.add(candidate.packageName);
3056
+ return true;
3057
+ });
2885
3058
  }
2886
3059
  /** The Maven GAV a `maven` entry pins, as a stable identity string. */
2887
3060
  function mavenCoordinate(spec) {
@@ -2928,6 +3101,7 @@ export function getRefreshableManagedTools() {
2928
3101
  packageName: tool.packageName,
2929
3102
  binaryName: tool.binaryName,
2930
3103
  verificationTimeoutMs: tool.verificationTimeoutMs,
3104
+ packageEntryOf: packageEntryVerification(tool),
2931
3105
  });
2932
3106
  break;
2933
3107
  }
@@ -3757,7 +3931,9 @@ function recordPackageManagerInstallException(toolId, strategyLabel, packageName
3757
3931
  installFailureReasons.set(toolId, message);
3758
3932
  return undefined;
3759
3933
  }
3760
- async function installNpmTool(toolId, packageName, binaryName, verificationArgs = ["--version"], verificationTimeoutMs = 10_000) {
3934
+ async function installNpmTool(toolId, packageName, binaryName, verificationArgs = ["--version"], verificationTimeoutMs = 10_000,
3935
+ /** See {@link packageEntryVerification} — spawn-free verification (#2722). */
3936
+ packageEntryOf) {
3761
3937
  try {
3762
3938
  // Ensure tools directory exists
3763
3939
  await fs.mkdir(TOOLS_DIR, { recursive: true });
@@ -3837,11 +4013,15 @@ async function installNpmTool(toolId, packageName, binaryName, verificationArgs
3837
4013
  debugLog(`Verifying ${binaryName}...`);
3838
4014
  let isValid = false;
3839
4015
  let lastAttemptTransient = false;
4016
+ let lastAttemptInconclusive = false;
3840
4017
  for (let attempt = 1; attempt <= 3; attempt++) {
3841
4018
  lastAttemptTransient = false;
4019
+ lastAttemptInconclusive = false;
3842
4020
  isValid = await verifyToolBinary(binPath, undefined, () => {
3843
4021
  lastAttemptTransient = true;
3844
- }, verificationTimeoutMs, verificationArgs);
4022
+ }, verificationTimeoutMs, verificationArgs, packageEntryOf, () => {
4023
+ lastAttemptInconclusive = true;
4024
+ });
3845
4025
  if (isValid)
3846
4026
  break;
3847
4027
  if (attempt < 3) {
@@ -3849,6 +4029,37 @@ async function installNpmTool(toolId, packageName, binaryName, verificationArgs
3849
4029
  await new Promise((r) => setTimeout(r, 1000 * attempt));
3850
4030
  }
3851
4031
  }
4032
+ if (!isValid && lastAttemptInconclusive) {
4033
+ // #2722: the probe ran to completion but could not decide — the #208
4034
+ // transport-required matcher was armed, never matched, and the kept
4035
+ // output is a truncated prefix, so the marker may sit past it (the
4036
+ // class intelephense fell into: ~4 MB of bundle ahead of the marker,
4037
+ // unreachable through any pipe). Same rule as the transient branch
4038
+ // below (#2015): a non-verdict must not delete a freshly installed
4039
+ // package. Deliberately NOT folded into `lastAttemptTransient` —
4040
+ // re-probing reproduces this exactly, so it is durable, and a
4041
+ // transient verdict would keep re-arming the reinstall path
4042
+ // (catalog shape 13).
4043
+ // R2-F2: a non-verdict is a reason to keep ONLY while the tree on disk
4044
+ // still looks like a complete install. With intelephense verified
4045
+ // spawn-free, no registry entry reaches this branch healthy (measured:
4046
+ // no other npm-strategy LSP server emits more than 1,527 bytes), so
4047
+ // its live population is BROKEN servers that spew past the retained
4048
+ // window and die — and for those the delete was the only repair that
4049
+ // existed. Measured on npm 9.2.0 against a real managed prefix: a
4050
+ // package file corrupted IN PLACE is not repaired by re-installing
4051
+ // (`up to date`, the zeroed file stays zeroed), while a package
4052
+ // directory, a `.bin` shim or a nested dependency that is GONE is
4053
+ // reinstalled. So the same on-disk evidence `verification:
4054
+ // "package-entry"` uses decides keep-vs-delete here — strictly as a
4055
+ // gate, never as a verdict: the install is still recorded as failed,
4056
+ // so the tool still reads `✗` and nothing is re-hidden.
4057
+ if (await verifyNpmPackageEntry(binPath, packageName)) {
4058
+ logSessionStart(`auto-install ${packageName}: verification inconclusive (output truncated before the transport-required marker) but the installed tree is intact; keeping installation for re-probe`);
4059
+ return undefined;
4060
+ }
4061
+ logSessionStart(`auto-install ${packageName}: verification inconclusive AND the installed tree is incomplete; cleaning up so the next install can repair it`);
4062
+ }
3852
4063
  if (!isValid && lastAttemptTransient) {
3853
4064
  // #2015: a killed/spawn-failed prober is NOT a verdict about the
3854
4065
  // binary (#1569 semantics). Cold-start verifies on fresh installs
@@ -4132,7 +4343,7 @@ export async function installTool(toolId) {
4132
4343
  case "npm": {
4133
4344
  if (!tool.packageName || !tool.binaryName)
4134
4345
  return false;
4135
- const npmPath = await installNpmTool(tool.id, tool.packageName, tool.binaryName, tool.checkArgs, getToolVerificationTimeout(tool));
4346
+ const npmPath = await installNpmTool(tool.id, tool.packageName, tool.binaryName, tool.checkArgs, getToolVerificationTimeout(tool), packageEntryVerification(tool));
4136
4347
  if (npmPath !== undefined) {
4137
4348
  // #1746 review F4: an install just resolved this package's range
4138
4349
  // against the registry, so record it as freshly checked. Otherwise a