pi-lens 4.1.5 → 4.2.0

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 (219) hide show
  1. package/CHANGELOG.md +415 -0
  2. package/README.md +13 -8
  3. package/config/dependency-cruiser-eager-allowlist.json +6 -1
  4. package/dist/clients/actionable-warnings.js +30 -4
  5. package/dist/clients/agent-behavior-client.js +2 -0
  6. package/dist/clients/analysed-root.js +1 -0
  7. package/dist/clients/ast-grep-tool-logger.js +1 -1
  8. package/dist/clients/bash-file-access.js +26 -0
  9. package/dist/clients/biome-client.js +13 -2
  10. package/dist/clients/blocker-freshness.js +403 -35
  11. package/dist/clients/bootstrap.js +6 -1
  12. package/dist/clients/bounded-telemetry.js +2 -0
  13. package/dist/clients/cache-observability.js +122 -1
  14. package/dist/clients/cascade-format.js +200 -4
  15. package/dist/clients/code-quality-warnings.js +10 -2
  16. package/dist/clients/complexity-client.js +5 -0
  17. package/dist/clients/config-diagnostic-codes.js +2 -0
  18. package/dist/clients/config-resolve.js +6 -3
  19. package/dist/clients/config-schema.js +17 -0
  20. package/dist/clients/config-warn.js +2 -2
  21. package/dist/clients/dead-code-client.js +8 -1
  22. package/dist/clients/degradation-ledger.js +16 -1
  23. package/dist/clients/dependency-checker.js +5 -1
  24. package/dist/clients/diagnostic-dispositions.js +12 -1
  25. package/dist/clients/dispatch/ast-grep-catalog.js +65 -0
  26. package/dist/clients/dispatch/auxiliary-lsp.js +28 -0
  27. package/dist/clients/dispatch/dispatcher.js +47 -17
  28. package/dist/clients/dispatch/facts/function-facts.js +1 -1
  29. package/dist/clients/dispatch/facts/import-facts.js +5 -1
  30. package/dist/clients/dispatch/finding-policy.js +222 -0
  31. package/dist/clients/dispatch/indent-detect.js +389 -13
  32. package/dist/clients/dispatch/integration.js +175 -25
  33. package/dist/clients/dispatch/rule-ignores.js +62 -0
  34. package/dist/clients/dispatch/runners/actionlint.js +2 -1
  35. package/dist/clients/dispatch/runners/ast-grep-napi.js +21 -41
  36. package/dist/clients/dispatch/runners/biome-check.js +8 -4
  37. package/dist/clients/dispatch/runners/cpp-check.js +4 -3
  38. package/dist/clients/dispatch/runners/credo.js +2 -1
  39. package/dist/clients/dispatch/runners/cue-vet.js +2 -1
  40. package/dist/clients/dispatch/runners/dart-analyze.js +2 -1
  41. package/dist/clients/dispatch/runners/detekt.js +2 -1
  42. package/dist/clients/dispatch/runners/dotnet-build.js +2 -1
  43. package/dist/clients/dispatch/runners/elixir-check.js +2 -1
  44. package/dist/clients/dispatch/runners/eslint.js +2 -1
  45. package/dist/clients/dispatch/runners/fish-indent.js +2 -1
  46. package/dist/clients/dispatch/runners/gleam-check.js +2 -1
  47. package/dist/clients/dispatch/runners/go-vet.js +2 -1
  48. package/dist/clients/dispatch/runners/golangci-lint.js +2 -1
  49. package/dist/clients/dispatch/runners/hadolint.js +2 -1
  50. package/dist/clients/dispatch/runners/helm-lint.js +3 -1
  51. package/dist/clients/dispatch/runners/helm-render.js +2 -1
  52. package/dist/clients/dispatch/runners/htmlhint.js +2 -1
  53. package/dist/clients/dispatch/runners/javac.js +2 -1
  54. package/dist/clients/dispatch/runners/ktlint.js +2 -1
  55. package/dist/clients/dispatch/runners/lsp.js +19 -1
  56. package/dist/clients/dispatch/runners/markdownlint.js +3 -2
  57. package/dist/clients/dispatch/runners/mypy.js +2 -1
  58. package/dist/clients/dispatch/runners/oxlint.js +2 -1
  59. package/dist/clients/dispatch/runners/php-lint.js +2 -1
  60. package/dist/clients/dispatch/runners/phpstan.js +2 -1
  61. package/dist/clients/dispatch/runners/prisma-validate.js +2 -1
  62. package/dist/clients/dispatch/runners/psscriptanalyzer.js +12 -12
  63. package/dist/clients/dispatch/runners/pyright.js +2 -1
  64. package/dist/clients/dispatch/runners/rubocop.js +2 -1
  65. package/dist/clients/dispatch/runners/ruff.js +2 -2
  66. package/dist/clients/dispatch/runners/rust-clippy.js +12 -11
  67. package/dist/clients/dispatch/runners/shellcheck.js +2 -1
  68. package/dist/clients/dispatch/runners/shfmt.js +4 -13
  69. package/dist/clients/dispatch/runners/spellcheck.js +2 -2
  70. package/dist/clients/dispatch/runners/spotbugs.js +2 -1
  71. package/dist/clients/dispatch/runners/sqlfluff.js +3 -2
  72. package/dist/clients/dispatch/runners/stylelint.js +3 -2
  73. package/dist/clients/dispatch/runners/swiftlint.js +2 -1
  74. package/dist/clients/dispatch/runners/taplo.js +2 -1
  75. package/dist/clients/dispatch/runners/terragrunt.js +2 -1
  76. package/dist/clients/dispatch/runners/tflint.js +2 -1
  77. package/dist/clients/dispatch/runners/tree-sitter.js +2 -21
  78. package/dist/clients/dispatch/runners/trivy-config.js +2 -1
  79. package/dist/clients/dispatch/runners/utils/availability-policy.js +5 -0
  80. package/dist/clients/dispatch/runners/utils/candidate-probe.js +2 -2
  81. package/dist/clients/dispatch/runners/utils/runner-helpers.js +8 -2
  82. package/dist/clients/dispatch/runners/vale.js +2 -1
  83. package/dist/clients/dispatch/runners/yamllint.js +4 -3
  84. package/dist/clients/dispatch/runners/zig-check.js +2 -1
  85. package/dist/clients/dispatch/types.js +4 -1
  86. package/dist/clients/disposition-publish.js +1 -0
  87. package/dist/clients/effective-config.js +12 -2
  88. package/dist/clients/extension-log.js +2 -0
  89. package/dist/clients/file-kinds.js +26 -1
  90. package/dist/clients/file-role.js +23 -6
  91. package/dist/clients/file-utils.js +156 -10
  92. package/dist/clients/finding-delivery-gate.js +60 -9
  93. package/dist/clients/format-service.js +39 -21
  94. package/dist/clients/formatters.js +113 -200
  95. package/dist/clients/generation-guard.js +6 -2
  96. package/dist/clients/git-guard.js +11 -3
  97. package/dist/clients/gitleaks-client.js +42 -6
  98. package/dist/clients/govulncheck-client.js +48 -7
  99. package/dist/clients/indent-retarget.js +78 -8
  100. package/dist/clients/installer/index.js +524 -161
  101. package/dist/clients/instance-reaper-state.js +7 -0
  102. package/dist/clients/instance-reaper.js +20 -10
  103. package/dist/clients/jscpd-client.js +9 -1
  104. package/dist/clients/knip-client.js +9 -2
  105. package/dist/clients/language-profile.js +35 -3
  106. package/dist/clients/language-registry.js +5 -1
  107. package/dist/clients/latency-logger.js +6 -1
  108. package/dist/clients/lens-config.js +4 -0
  109. package/dist/clients/lens-engine.js +12 -3
  110. package/dist/clients/lens-events.js +2 -1
  111. package/dist/clients/lens-flag-registry.js +79 -0
  112. package/dist/clients/live-bus-emitter.js +2 -1
  113. package/dist/clients/lsp/client.js +25 -21
  114. package/dist/clients/lsp/config.js +36 -7
  115. package/dist/clients/lsp/diagnostic-binding.js +24 -1
  116. package/dist/clients/lsp/document-drift.js +78 -0
  117. package/dist/clients/lsp/index.js +288 -45
  118. package/dist/clients/lsp/launch.js +16 -4
  119. package/dist/clients/lsp/server.js +79 -15
  120. package/dist/clients/lsp/workspace-diagnostics-cache.js +39 -5
  121. package/dist/clients/lsp-mutation.js +5 -3
  122. package/dist/clients/map-with-concurrency.js +12 -8
  123. package/dist/clients/mcp/analyze.js +3 -0
  124. package/dist/clients/mcp/session.js +38 -4
  125. package/dist/clients/ndjson-logger.js +24 -7
  126. package/dist/clients/observed-mutation.js +40 -2
  127. package/dist/clients/opaque-mutation-scan.js +23 -3
  128. package/dist/clients/opengrep-client.js +111 -14
  129. package/dist/clients/package-manager.js +2 -1
  130. package/dist/clients/path-utils.js +123 -3
  131. package/dist/clients/performance-report.js +5 -1
  132. package/dist/clients/php-cs-fixer-config.js +8 -3
  133. package/dist/clients/pipeline.js +77 -7
  134. package/dist/clients/project-diagnostics/cache.js +37 -5
  135. package/dist/clients/project-diagnostics/fresh-fetch.js +173 -18
  136. package/dist/clients/project-diagnostics/runner-adapters/dead-code.js +4 -1
  137. package/dist/clients/project-diagnostics/runner-adapters/gitleaks.js +10 -9
  138. package/dist/clients/project-diagnostics/scanner.js +59 -3
  139. package/dist/clients/project-lens-config.js +48 -2
  140. package/dist/clients/read-guard-logger.js +2 -0
  141. package/dist/clients/read-guard.js +70 -11
  142. package/dist/clients/resource-sampler.js +147 -9
  143. package/dist/clients/review-graph-logger.js +2 -0
  144. package/dist/clients/ruff-client.js +18 -9
  145. package/dist/clients/runtime-agent-end.js +42 -5
  146. package/dist/clients/runtime-context.js +6 -2
  147. package/dist/clients/runtime-coordinator.js +82 -7
  148. package/dist/clients/runtime-session.js +33 -5
  149. package/dist/clients/runtime-tool-call.js +43 -12
  150. package/dist/clients/runtime-tool-result.js +334 -86
  151. package/dist/clients/runtime-turn.js +195 -18
  152. package/dist/clients/safe-spawn.js +258 -5
  153. package/dist/clients/scratch-tree-policy.js +6 -0
  154. package/dist/clients/security-scan-client.js +2 -2
  155. package/dist/clients/session-event-guard.js +119 -6
  156. package/dist/clients/sg-runner.js +25 -12
  157. package/dist/clients/sgconfig.js +79 -2
  158. package/dist/clients/situational-tool-telemetry.js +108 -0
  159. package/dist/clients/test-runner-client.js +258 -78
  160. package/dist/clients/test-runner-delivery.js +43 -0
  161. package/dist/clients/tool-agreement.js +423 -0
  162. package/dist/clients/tool-config.js +249 -0
  163. package/dist/clients/tool-cwd.js +270 -0
  164. package/dist/clients/tool-policy.js +163 -7
  165. package/dist/clients/tool-probe.js +53 -0
  166. package/dist/clients/tool-set-policy.js +42 -0
  167. package/dist/clients/tree-sitter-client.js +120 -0
  168. package/dist/clients/tree-sitter-query-loader.js +127 -189
  169. package/dist/clients/trivy-client.js +9 -1
  170. package/dist/clients/turn-context.js +52 -0
  171. package/dist/clients/widget-state.js +102 -3
  172. package/dist/clients/word-index.js +5 -2
  173. package/dist/clients/write-ordering-guard.js +7 -0
  174. package/dist/index.js +26968 -21741
  175. package/dist/mcp/analyze-cli.js +3 -2
  176. package/dist/mcp/server.js +140 -161
  177. package/dist/tools/activate-tools.js +10 -7
  178. package/dist/tools/ast-grep-outline.js +2 -13
  179. package/dist/tools/ast-grep-replace.js +8 -13
  180. package/dist/tools/ast-grep-search.js +83 -39
  181. package/dist/tools/effective-config.js +4 -3
  182. package/dist/tools/lens-diagnostic-mark.js +8 -11
  183. package/dist/tools/lens-diagnostics.js +516 -133
  184. package/dist/tools/lsp-diagnostics.js +265 -182
  185. package/dist/tools/lsp-navigation.js +20 -44
  186. package/dist/tools/module-report.js +5 -9
  187. package/dist/tools/project-report.js +9 -17
  188. package/dist/tools/render-compact.js +280 -1
  189. package/dist/tools/symbol-search.js +2 -2
  190. package/docs/agent-guide.md +3 -4
  191. package/docs/agent-tools.md +46 -22
  192. package/docs/audit1.md +5 -5
  193. package/docs/configuration.md +9 -0
  194. package/docs/dependencies.md +11 -0
  195. package/docs/dispositions.md +52 -0
  196. package/docs/environment-variables.md +2 -7
  197. package/docs/features.md +7 -7
  198. package/docs/globalconfig.md +14 -2
  199. package/docs/language-coverage.md +1 -0
  200. package/docs/mcp.md +13 -1
  201. package/docs/pi-lens-fixer.md +48 -18
  202. package/docs/pi-lens-investigator.md +38 -18
  203. package/docs/pi-lens-monitor.md +88 -0
  204. package/docs/pi-lens-retro.md +97 -0
  205. package/docs/pi-lens-reviewer.md +53 -31
  206. package/docs/pi-lens-subagent.md +12 -4
  207. package/docs/public-api-stability.md +1 -0
  208. package/docs/real-harness.md +46 -0
  209. package/docs/release-qa-baseline.md +27 -1
  210. package/docs/servercapabilities.md +1 -6
  211. package/docs/settings.md +44 -6
  212. package/docs/tools_improvement2.md +4 -4
  213. package/docs/usage.md +15 -3
  214. package/package.json +10 -1
  215. package/rules/tree-sitter-queries/typescript/sql-injection.yml +23 -7
  216. package/scripts/analyze-pi-lens-logs.mjs +40 -0
  217. package/skills/pi-lens-ast-grep/SKILL.md +23 -6
  218. package/skills/pi-lens-lsp-navigation/SKILL.md +26 -9
  219. package/dist/tools/ast-dump.js +0 -103
@@ -0,0 +1,270 @@
1
+ import * as os from "node:os";
2
+ import { existsSync, readdirSync } from "node:fs";
3
+ import * as path from "node:path";
4
+ import { logExtension } from "./extension-log.js";
5
+ import { isRealGitMarker, isAtOrAboveHomeDir, isUnderDir, isWindowsPath, nameMatchesMarkerGlob, normalizeEphemeralMapKey, } from "./path-utils.js";
6
+ import { getDegradationLedgerGeneration, recordDegradationOnce, } from "./degradation-ledger.js";
7
+ import { createGenerationMap, createGenerationSource, } from "./generation-guard.js";
8
+ import { rootMarkersForFile } from "./language-profile.js";
9
+ export const FORMATTER_MARKERS = {
10
+ biome: ["biome.json", "biome.jsonc", "package.json", ".gitignore"],
11
+ prettier: [
12
+ ".prettierrc",
13
+ ".prettierrc.json",
14
+ ".prettierrc.yaml",
15
+ ".prettierrc.yml",
16
+ ".prettierrc.js",
17
+ ".prettierrc.cjs",
18
+ ".prettierrc.mjs",
19
+ "prettier.config.js",
20
+ "prettier.config.cjs",
21
+ "prettier.config.mjs",
22
+ ".prettierignore",
23
+ "package.json",
24
+ ".gitignore",
25
+ ],
26
+ ruff: ["pyproject.toml", "ruff.toml", ".ruff.toml", ".gitignore"],
27
+ black: ["pyproject.toml", "black.toml", ".black", ".gitignore"],
28
+ "php-cs-fixer": [".php-cs-fixer.php", ".php-cs-fixer.dist.php", ".gitignore"],
29
+ sqlfluff: [".sqlfluff", "pyproject.toml", "setup.cfg", ".gitignore"],
30
+ oxfmt: [
31
+ "oxfmt.toml",
32
+ ".oxfmtrc.json",
33
+ "vite-plus.json",
34
+ "package.json",
35
+ ".gitignore",
36
+ ],
37
+ rustfmt: ["rustfmt.toml", ".rustfmt.toml", "Cargo.toml", ".gitignore"],
38
+ rubocop: [".rubocop.yml", ".rubocop.yaml", ".gitignore"],
39
+ standardrb: [".standard.yml", ".standard.yaml", ".gitignore"],
40
+ "clang-format": [".clang-format", "_clang-format", ".gitignore"],
41
+ stylua: ["stylua.toml", ".stylua.toml", ".gitignore"],
42
+ ocamlformat: [".ocamlformat", ".gitignore"],
43
+ "google-java-format": [".google-java-format", ".editorconfig", ".gitignore"],
44
+ cljfmt: [".cljfmt.edn", "cljfmt.edn", ".cljfmt", ".gitignore"],
45
+ "cmake-format": [
46
+ ".cmake-format",
47
+ ".cmake-format.yaml",
48
+ ".cmake-format.yml",
49
+ ".cmake-format.json",
50
+ ".cmake-format.py",
51
+ "cmake-format.yaml",
52
+ "cmake-format.yml",
53
+ ".editorconfig",
54
+ ".gitignore",
55
+ ],
56
+ "psscriptanalyzer-format": [
57
+ "PSScriptAnalyzerSettings.psd1",
58
+ "ScriptAnalyzerSettings.psd1",
59
+ ".gitignore",
60
+ ],
61
+ csharpier: [
62
+ ".csharpierrc",
63
+ ".csharpierrc.json",
64
+ ".csharpierrc.yaml",
65
+ ".csharpierrc.yml",
66
+ ".gitignore",
67
+ ],
68
+ ormolu: [".ormolu", ".gitignore"],
69
+ taplo: ["taplo.toml", ".taplo.toml", ".gitignore"],
70
+ terraform: [".terraform.lock.hcl", ".gitignore"],
71
+ swiftformat: [".swiftformat", ".gitignore"],
72
+ fantomas: [".fantomasignore", ".editorconfig", ".gitignore"],
73
+ mix: [".formatter.exs", ".gitignore"],
74
+ shfmt: [".editorconfig", ".gitignore"],
75
+ ktlint: [".editorconfig", ".gitignore"],
76
+ ktfmt: [
77
+ ".editorconfig",
78
+ ".ktfmt",
79
+ ".ktfmt.kts",
80
+ "build.gradle",
81
+ "build.gradle.kts",
82
+ "settings.gradle",
83
+ "settings.gradle.kts",
84
+ ".gitignore",
85
+ ],
86
+ typstyle: [".gitignore"],
87
+ };
88
+ const toolCwdGeneration = createGenerationSource("tool-cwd");
89
+ const logged = createGenerationMap("tool-cwd-resolution-log");
90
+ /** Pure key seam for resolution-log and runner-advisory identity. */
91
+ export function _toolCwdEphemeralKey(parts) {
92
+ return parts.map(normalizeEphemeralMapKey).join("\0");
93
+ }
94
+ function syncGeneration() {
95
+ if (toolCwdGeneration.current() === getDegradationLedgerGeneration())
96
+ return;
97
+ logged.clear();
98
+ while (toolCwdGeneration.current() < getDegradationLedgerGeneration()) {
99
+ toolCwdGeneration.bump();
100
+ }
101
+ }
102
+ function walkMarkerRoot(startDir, markers, homeDir) {
103
+ let current = path.resolve(startDir);
104
+ for (let depth = 0; depth < 64; depth++) {
105
+ if (isAtOrAboveHomeDir(current, homeDir))
106
+ break;
107
+ for (const marker of markers) {
108
+ const slash = marker.replace(/\\/g, "/").lastIndexOf("/");
109
+ const parent = slash >= 0 ? marker.slice(0, slash) : "";
110
+ const basename = slash >= 0 ? marker.slice(slash + 1) : marker;
111
+ const target = parent
112
+ ? path.join(current, ...parent.split("/").filter(Boolean))
113
+ : current;
114
+ let found = false;
115
+ if (basename.includes("*")) {
116
+ try {
117
+ found = readdirSync(target, { withFileTypes: true }).some((entry) => (entry.isFile() || entry.isSymbolicLink()) &&
118
+ nameMatchesMarkerGlob(entry.name, basename));
119
+ }
120
+ catch {
121
+ found = false;
122
+ }
123
+ }
124
+ else {
125
+ found = existsSync(path.join(target, basename));
126
+ }
127
+ if (found)
128
+ return { root: current, marker };
129
+ }
130
+ const parentDir = path.dirname(current);
131
+ if (parentDir === current)
132
+ break;
133
+ current = parentDir;
134
+ }
135
+ return { root: null };
136
+ }
137
+ function findMarkerRoot(startDir, markers, homeDir) {
138
+ syncGeneration();
139
+ return walkMarkerRoot(startDir, markers, homeDir);
140
+ }
141
+ function markersFor(kind, tool, file, ctx) {
142
+ if (kind === "lsp")
143
+ return ctx.rootMarkers ?? [];
144
+ // #2965: runner fallback uses the language table that also anchors the
145
+ // dispatch context. A caller-owned table remains an explicit override.
146
+ if (kind === "runner")
147
+ return ctx.rootMarkers ?? rootMarkersForFile(file, tool);
148
+ return FORMATTER_MARKERS[tool] ?? [".gitignore"];
149
+ }
150
+ function emitResolution(kind, tool, cwd, reason) {
151
+ syncGeneration();
152
+ const key = _toolCwdEphemeralKey([kind, tool, cwd, reason]);
153
+ if (logged.current(key) !== 0)
154
+ return;
155
+ logged.bump(key);
156
+ logExtension({
157
+ subsystem: "tool-cwd",
158
+ level: "debug",
159
+ message: `cwd ${kind} ${tool} cwd=${cwd} reason=${reason}`,
160
+ });
161
+ }
162
+ /**
163
+ * Emit a constant runner advisory once per (tool, resolved root) per session
164
+ * (#2811): the yamllint/stylelint/sqlfluff "no config detected" lines and
165
+ * markdownlint's spawn-timeout cooldown notice logged once PER FILE at error
166
+ * level — 17 lines in one session for one tool. The throttle shares the
167
+ * resolution-log map and its ledger-generation reset, so `session_start`
168
+ * re-arms it like every once-latch in this module. The message rides
169
+ * `ctx.log` at debug level so the dispatch seam stays the one sink for
170
+ * runner lines, with its own filePath/kind metadata.
171
+ */
172
+ export function logRunnerAdvisoryOnce(ctx, tool, root, message) {
173
+ syncGeneration();
174
+ const key = _toolCwdEphemeralKey(["runner-advisory", tool, root]);
175
+ if (logged.current(key) !== 0)
176
+ return;
177
+ logged.bump(key);
178
+ ctx.log(message, "debug");
179
+ }
180
+ /** Resolve every child process cwd/root through one bounded, synchronous seam. */
181
+ export function resolveToolCwd(kind, tool, file, ctx) {
182
+ const dispatchRoot = path.resolve(ctx.cwd ?? process.cwd());
183
+ const absoluteFile = path.resolve(file);
184
+ const fileDir = path.dirname(absoluteFile);
185
+ const homeDir = ctx.homeDir ?? os.homedir();
186
+ const insideDispatch = isUnderDir(absoluteFile, dispatchRoot);
187
+ if (ctx.serverRoot) {
188
+ const rootPath = isWindowsPath(ctx.serverRoot) ? path.win32 : path;
189
+ const serverRoot = rootPath.resolve(ctx.serverRoot);
190
+ if (!ctx.suppressTelemetry)
191
+ emitResolution(kind, tool, serverRoot, "server-root");
192
+ return { cwd: serverRoot };
193
+ }
194
+ const markers = markersFor(kind, tool, absoluteFile, ctx);
195
+ const markerResult = markers.length
196
+ ? findMarkerRoot(fileDir, markers, ctx.allowHomeMarker ? path.parse(homeDir).root : homeDir)
197
+ : { root: null };
198
+ const markerRoot = markerResult.root;
199
+ if (markerRoot &&
200
+ (insideDispatch ? isUnderDir(markerRoot, dispatchRoot) : true)) {
201
+ const finalReason = `marker:${markerResult.marker ?? markers[0]}`;
202
+ if (!ctx.suppressTelemetry)
203
+ emitResolution(kind, tool, markerRoot, finalReason);
204
+ return {
205
+ cwd: markerRoot,
206
+ ...(markerResult.marker !== undefined
207
+ ? { marker: markerResult.marker }
208
+ : {}),
209
+ };
210
+ }
211
+ const memo = ctx.toolCwdMemo;
212
+ if (memo && memo.gitRoot === undefined) {
213
+ const gitResult = findMarkerRoot(fileDir, [".git"], homeDir);
214
+ memo.gitRoot =
215
+ gitResult.root && isRealGitMarker(path.join(gitResult.root, ".git"))
216
+ ? gitResult.root
217
+ : null;
218
+ }
219
+ const gitRoot = memo && memo.gitRoot !== undefined
220
+ ? memo.gitRoot
221
+ : (() => {
222
+ const gitResult = findMarkerRoot(fileDir, [".git"], homeDir);
223
+ return gitResult.root &&
224
+ isRealGitMarker(path.join(gitResult.root, ".git"))
225
+ ? gitResult.root
226
+ : null;
227
+ })();
228
+ if (gitRoot && (insideDispatch ? isUnderDir(gitRoot, dispatchRoot) : true)) {
229
+ if (!ctx.suppressTelemetry)
230
+ emitResolution(kind, tool, gitRoot, "git-root");
231
+ return { cwd: gitRoot, marker: ".git" };
232
+ }
233
+ if (insideDispatch) {
234
+ if (kind === "formatter") {
235
+ if (!ctx.suppressTelemetry)
236
+ emitResolution(kind, tool, fileDir, "file-dir-fallback");
237
+ if (!ctx.suppressTelemetry && !isUnderDir(fileDir, homeDir)) {
238
+ recordDegradationOnce({
239
+ kind: "tool-cwd-resolution",
240
+ subject: tool,
241
+ reason: `${kind}:home-cap:${absoluteFile}`,
242
+ });
243
+ }
244
+ return { cwd: fileDir };
245
+ }
246
+ if (!ctx.suppressTelemetry)
247
+ emitResolution(kind, tool, dispatchRoot, "dispatch-root");
248
+ return { cwd: dispatchRoot };
249
+ }
250
+ const reason = isUnderDir(fileDir, homeDir)
251
+ ? "file-dir-fallback"
252
+ : "home-cap";
253
+ const fallback = isUnderDir(fileDir, homeDir) ? fileDir : homeDir;
254
+ if (!ctx.suppressTelemetry)
255
+ recordDegradationOnce({
256
+ kind: "tool-cwd-resolution",
257
+ subject: tool,
258
+ reason: `${kind}:${reason}:${absoluteFile}`,
259
+ });
260
+ if (!ctx.suppressTelemetry)
261
+ emitResolution(kind, tool, fallback, reason);
262
+ return { cwd: fallback };
263
+ }
264
+ /** Runner-shaped adapter kept at the same seam for every runner consumer. */
265
+ export function resolveRunnerCwd(ctx, tool) {
266
+ return resolveToolCwd("runner", tool, ctx.filePath, ctx).cwd;
267
+ }
268
+ export function resolveRunnerCwdWithReason(ctx, tool) {
269
+ return resolveToolCwd("runner", tool, ctx.filePath, ctx);
270
+ }
@@ -1,6 +1,7 @@
1
1
  import * as fs from "node:fs";
2
2
  import * as path from "node:path";
3
3
  import { TERRAGRUNT_FILENAMES } from "./file-kinds.js";
4
+ import { recordDegradationOnce } from "./degradation-ledger.js";
4
5
  import { logLatency } from "./latency-logger.js";
5
6
  import { resolvePackagePath } from "./package-root.js";
6
7
  import { findNearestContaining, normalizeMapKey, walkUpDirs, } from "./path-utils.js";
@@ -371,6 +372,24 @@ const FORMATTER_POLICY_BY_EXTENSION = new Map([
371
372
  gate: "smart-default",
372
373
  },
373
374
  ],
375
+ [
376
+ ".typ",
377
+ {
378
+ formatterNames: ["typstyle"],
379
+ defaultFormatter: "typstyle",
380
+ defaultWhenUnconfigured: true,
381
+ gate: "smart-default",
382
+ },
383
+ ],
384
+ [
385
+ ".typc",
386
+ {
387
+ formatterNames: ["typstyle"],
388
+ defaultFormatter: "typstyle",
389
+ defaultWhenUnconfigured: true,
390
+ gate: "smart-default",
391
+ },
392
+ ],
374
393
  [
375
394
  ".c",
376
395
  {
@@ -738,6 +757,7 @@ const AUTO_INSTALLABLE_DEFAULT_FORMATTERS = new Map([
738
757
  ["shfmt", "shfmt"],
739
758
  ["taplo", "taplo"],
740
759
  ["ktlint", "ktlint"],
760
+ ["typstyle", "typstyle"],
741
761
  ]);
742
762
  // `gate: "smart-default"` so a matched file is formatted only when nothing else
743
763
  // claims it. Misdetection edge: `root.hcl` is filename-detected as terragrunt, so
@@ -1633,17 +1653,25 @@ export function getAutofixPolicyForFile(filePath, context = {}) {
1633
1653
  export function getPreferredAutofixTools(filePath, context) {
1634
1654
  return getAutofixPolicyForFile(filePath, context)?.preferredTools ?? [];
1635
1655
  }
1636
- const ESLINT_CONFIGS = [
1656
+ // Flat-config filenames resolve like ESLint itself: upward from cwd without
1657
+ // stopping at an intermediate package.json (#3017, shape 39). Legacy
1658
+ // `.eslintrc.*` names and the `package.json#eslintConfig` field keep the
1659
+ // historic per-package boundary below.
1660
+ const ESLINT_FLAT_CONFIGS = [
1661
+ "eslint.config.js",
1662
+ "eslint.config.mjs",
1663
+ "eslint.config.cjs",
1664
+ "eslint.config.ts",
1665
+ "eslint.config.mts",
1666
+ "eslint.config.cts",
1667
+ ];
1668
+ const ESLINT_LEGACY_CONFIGS = [
1637
1669
  ".eslintrc",
1638
1670
  ".eslintrc.js",
1639
1671
  ".eslintrc.cjs",
1640
1672
  ".eslintrc.json",
1641
1673
  ".eslintrc.yaml",
1642
1674
  ".eslintrc.yml",
1643
- "eslint.config.js",
1644
- "eslint.config.mjs",
1645
- "eslint.config.cjs",
1646
- "eslint.config.ts",
1647
1675
  ];
1648
1676
  function* walkUpDirsUntilPackageJson(cwd) {
1649
1677
  for (const dir of walkUpDirs(cwd)) {
@@ -1692,8 +1720,19 @@ export function hasNearestPackageJsonField(cwd, fieldName) {
1692
1720
  return false;
1693
1721
  }
1694
1722
  export function hasEslintConfig(cwd) {
1723
+ // Flat configs mirror ESLint's own discovery: every ancestor dir up to
1724
+ // the filesystem root, ignoring intermediate package.json boundaries, so
1725
+ // a monorepo root config stays visible from inside a nested package.
1726
+ for (const dir of walkUpDirs(cwd)) {
1727
+ for (const cfg of ESLINT_FLAT_CONFIGS) {
1728
+ if (fs.existsSync(path.join(dir, cfg)))
1729
+ return true;
1730
+ }
1731
+ }
1732
+ // Legacy `.eslintrc.*` and `package.json#eslintConfig` keep per-package
1733
+ // semantics: the walk stops at the nearest package.json.
1695
1734
  for (const dir of walkUpDirsUntilPackageJson(cwd)) {
1696
- for (const cfg of ESLINT_CONFIGS) {
1735
+ for (const cfg of ESLINT_LEGACY_CONFIGS) {
1697
1736
  if (fs.existsSync(path.join(dir, cfg)))
1698
1737
  return true;
1699
1738
  }
@@ -2231,6 +2270,16 @@ const KOTLIN_GRADLE_FILES = [
2231
2270
  "settings.gradle.kts",
2232
2271
  "settings.gradle",
2233
2272
  ];
2273
+ const GRADLE_KTLINT_PLUGIN_PATTERN = /(?:id\s*\(\s*|id\s+|apply\s+plugin\s*:\s*)["']org\.jlleitschuh\.gradle\.ktlint["']/g;
2274
+ const GRADLE_BUILD_LOGIC_DIRS = ["buildSrc", "build-logic"];
2275
+ const GRADLE_BUILD_LOGIC_EXTENSIONS = [".gradle", ".gradle.kts", ".kt"];
2276
+ const GRADLE_INCLUDE_BUILD_PATTERN = /\bincludeBuild\s*\(\s*["']([^"']+)["']/g;
2277
+ /**
2278
+ * Bound the synchronous ownership probe so a large convention tree cannot
2279
+ * stall every autofix. Exceeding it is an unknown ownership result, not proof
2280
+ * of no owner, and callers must decline the write (#3004).
2281
+ */
2282
+ export const GRADLE_BUILD_LOGIC_SCAN_MAX_ENTRIES = 10_000;
2234
2283
  const spotlessKotlinConfigCache = new Map();
2235
2284
  let spotlessGradleReadCount = 0;
2236
2285
  /** Test-only observability for asserting the hot-path I/O bound. */
@@ -2429,6 +2478,110 @@ export function getSpotlessKotlinFormatter(cwd) {
2429
2478
  export function hasKtlintConfig(cwd) {
2430
2479
  return getSpotlessKotlinFormatter(cwd) === "ktlint";
2431
2480
  }
2481
+ /**
2482
+ * Whether Gradle build logic applies the ktlint Gradle plugin. The plugin
2483
+ * establishes project ownership of ktlint, but its plugin version is not a
2484
+ * ktlint CLI version and must never be used as one (#3000).
2485
+ */
2486
+ export function hasGradleKtlintPlugin(cwd) {
2487
+ const files = new Set();
2488
+ const scan = { entries: 0, exceeded: false, indeterminate: false };
2489
+ for (const dir of walkUpDirs(cwd)) {
2490
+ for (const gradle of KOTLIN_GRADLE_FILES) {
2491
+ const filePath = path.join(dir, gradle);
2492
+ if (fs.existsSync(filePath))
2493
+ files.add(filePath);
2494
+ }
2495
+ for (const buildLogicDir of GRADLE_BUILD_LOGIC_DIRS) {
2496
+ if (!addGradleBuildLogicFiles(path.join(dir, buildLogicDir), files, scan)) {
2497
+ scan.exceeded = true;
2498
+ }
2499
+ }
2500
+ for (const settings of ["settings.gradle.kts", "settings.gradle"]) {
2501
+ const settingsPath = path.join(dir, settings);
2502
+ if (!fs.existsSync(settingsPath))
2503
+ continue;
2504
+ try {
2505
+ const raw = fs.readFileSync(settingsPath, "utf-8");
2506
+ const stripped = stripGradleCommentsAndStrings(raw);
2507
+ GRADLE_INCLUDE_BUILD_PATTERN.lastIndex = 0;
2508
+ let match;
2509
+ while ((match = GRADLE_INCLUDE_BUILD_PATTERN.exec(raw)) !== null) {
2510
+ const code = stripped.slice(match.index, match.index + match[0].length);
2511
+ const includedBuild = match[1];
2512
+ if (includedBuild && /^\s*includeBuild\s*\(\s*/.test(code)) {
2513
+ if (!addGradleBuildLogicFiles(path.resolve(dir, includedBuild), files, scan)) {
2514
+ scan.exceeded = true;
2515
+ }
2516
+ }
2517
+ }
2518
+ }
2519
+ catch {
2520
+ scan.indeterminate = true;
2521
+ }
2522
+ }
2523
+ }
2524
+ if (scan.exceeded) {
2525
+ recordDegradationOnce({
2526
+ kind: "gradle-ktlint-scan-budget-exceeded",
2527
+ subject: "ktlint:gradle-build-logic",
2528
+ reason: `Gradle build-logic ownership scan exceeded its ${GRADLE_BUILD_LOGIC_SCAN_MAX_ENTRIES}-entry budget; ` +
2529
+ "ownership cannot be established, so ktlint autofix is declined",
2530
+ });
2531
+ return { kind: "indeterminate" };
2532
+ }
2533
+ if (scan.indeterminate)
2534
+ return { kind: "indeterminate" };
2535
+ for (const filePath of files) {
2536
+ try {
2537
+ const raw = fs.readFileSync(filePath, "utf-8");
2538
+ const stripped = stripGradleCommentsAndStrings(raw);
2539
+ GRADLE_KTLINT_PLUGIN_PATTERN.lastIndex = 0;
2540
+ let match;
2541
+ while ((match = GRADLE_KTLINT_PLUGIN_PATTERN.exec(raw)) !== null) {
2542
+ const code = stripped.slice(match.index, match.index + match[0].length);
2543
+ if (!/^\s*$/.test(code))
2544
+ return { kind: "owned" };
2545
+ }
2546
+ }
2547
+ catch {
2548
+ return { kind: "indeterminate" };
2549
+ }
2550
+ }
2551
+ return { kind: "not-owned" };
2552
+ }
2553
+ function addGradleBuildLogicFiles(root, files, scan) {
2554
+ if (!fs.existsSync(root))
2555
+ return true;
2556
+ const pending = [root];
2557
+ while (pending.length > 0) {
2558
+ const dir = pending.pop();
2559
+ if (!dir)
2560
+ continue;
2561
+ let entries;
2562
+ try {
2563
+ entries = fs.readdirSync(dir, { withFileTypes: true });
2564
+ }
2565
+ catch {
2566
+ scan.indeterminate = true;
2567
+ continue;
2568
+ }
2569
+ for (const entry of entries) {
2570
+ scan.entries += 1;
2571
+ if (scan.entries > GRADLE_BUILD_LOGIC_SCAN_MAX_ENTRIES)
2572
+ return false;
2573
+ const entryPath = path.join(dir, entry.name);
2574
+ if (entry.isDirectory()) {
2575
+ pending.push(entryPath);
2576
+ }
2577
+ else if (entry.isFile() &&
2578
+ GRADLE_BUILD_LOGIC_EXTENSIONS.some((extension) => entry.name.endsWith(extension))) {
2579
+ files.add(entryPath);
2580
+ }
2581
+ }
2582
+ }
2583
+ return true;
2584
+ }
2432
2585
  export function hasKtfmtConfig(cwd) {
2433
2586
  const spotlessFormatter = getSpotlessKotlinFormatter(cwd);
2434
2587
  if (spotlessFormatter)
@@ -2557,7 +2710,10 @@ const OXLINT_CONFIGS = [
2557
2710
  "oxlint.config.mts",
2558
2711
  ];
2559
2712
  export function hasOxlintConfig(cwd) {
2560
- for (const dir of walkUpDirsUntilPackageJson(cwd)) {
2713
+ // Oxlint auto-discovers the nearest config by file location, so the walk
2714
+ // ignores intermediate package.json boundaries just like the flat
2715
+ // ESLint branch above (#3017).
2716
+ for (const dir of walkUpDirs(cwd)) {
2561
2717
  for (const cfg of OXLINT_CONFIGS) {
2562
2718
  if (fs.existsSync(path.join(dir, cfg)))
2563
2719
  return true;
@@ -0,0 +1,53 @@
1
+ /**
2
+ * The tool-probe spawn seam (#2894).
3
+ *
4
+ * A sibling of `tool-cwd.ts`: that module owns where a child that READS A
5
+ * PROJECT starts, this one owns the fact that a child which reads no project
6
+ * starts nowhere in particular.
7
+ *
8
+ * It is a module of its own rather than another export on `safe-spawn.ts`
9
+ * deliberately. Every availability test in this repo fakes the process
10
+ * boundary with `vi.mock("clients/safe-spawn.js", … { safeSpawnAsync })`, and
11
+ * a same-module helper would call the module's own unmocked binding — the
12
+ * probe would spawn for real while the test believed it had the boundary. One
13
+ * module hop keeps every existing double production-faithful.
14
+ */
15
+ import { safeSpawnAsync } from "./safe-spawn.js";
16
+ /**
17
+ * Run a tool's own presence/version invocation — `<tool> --version`, `cl`,
18
+ * `go version`, `which <pm>`, `Get-Module -ListAvailable …` — and hand back
19
+ * the raw spawn result for the caller's availability policy to classify.
20
+ *
21
+ * ## The contract: a probe never gets a `cwd`
22
+ *
23
+ * A probe asks whether a binary exists and what it calls itself. The answer
24
+ * does not depend on the directory the child starts in, so this seam passes
25
+ * NONE — the child inherits the host's directory and nothing it reports is
26
+ * read out of a project. That is why the sweep in
27
+ * `tests/clients/dispatch/runners/runner-spawn-cwd-sweep.test.ts` admits ONE
28
+ * cwd-less spawn here instead of the 23 it admitted before: every one of those
29
+ * sites re-decided the same thing in its own words, and 23 sentences a
30
+ * reviewer has to re-read one at a time is how an admission table stops being
31
+ * auditable (#2872 round 3 shipped 127 rows carrying two canned sentences,
32
+ * four of them false where they were read).
33
+ *
34
+ * `cwd` is STRIPPED rather than merely left out of {@link ProbeSpawnOptions}:
35
+ * the type stops an object literal from carrying one, and the destructure
36
+ * below removes one that arrived inside an already-typed options object a
37
+ * caller widened or spread. Both halves are guarded — deleting the
38
+ * destructure changes this call's text, which retires the sweep's admission
39
+ * row, and `tests/clients/probe-and-root-spawn-cwd.test.ts` reds on the
40
+ * smuggled cwd reaching the child. A probe that genuinely needs to
41
+ * run somewhere — `mix credo --version` needs a mix project, `cargo clippy
42
+ * --version` needs a package, `eslint --version` resolves a project-local
43
+ * binary — must call `safeSpawnAsync` directly and be admitted by that sweep
44
+ * on its own reason.
45
+ */
46
+ export async function probeToolAsync(command, args, options) {
47
+ // Destructured away rather than overwritten with `undefined`: the key is
48
+ // then genuinely ABSENT from what reaches the spawn, which is both what the
49
+ // contract says and what `exactOptionalPropertyTypes` wants (a present
50
+ // `cwd: undefined` is a strictness spike the `tests/config` ratchet counts).
51
+ const { cwd: _strippedCwd, ...rest } = (options ?? {});
52
+ return safeSpawnAsync(command, [...args], { ...rest });
53
+ }
@@ -1,4 +1,46 @@
1
1
  import { logLatency } from "./latency-logger.js";
2
+ import { BoundedFifoMap } from "./bounded-cache.js";
3
+ import { recordDegradationOnce } from "./degradation-ledger.js";
4
+ // pi re-runs the extension factory for session rebuilds, but imports this
5
+ // module once per process. Keep conversation activation memory here, keyed by
6
+ // pi's session file rather than by a factory closure or process-wide session.
7
+ export const REMEMBERED_LAZY_TOOLS_MAX_SESSIONS = 128;
8
+ const rememberedLazyToolsBySessionFile = new BoundedFifoMap(REMEMBERED_LAZY_TOOLS_MAX_SESSIONS);
9
+ export function rememberLazyTools(sessionFile, names) {
10
+ if (!sessionFile) {
11
+ recordDegradationOnce({
12
+ kind: "tool-set-session-file-unavailable",
13
+ subject: "activation",
14
+ reason: "session-file identity unavailable; activation memory is inert",
15
+ });
16
+ return;
17
+ }
18
+ const remembered = rememberedLazyToolsBySessionFile.get(sessionFile) ?? new Set();
19
+ for (const name of names)
20
+ remembered.add(name);
21
+ rememberedLazyToolsBySessionFile.set(sessionFile, remembered);
22
+ }
23
+ export function getRememberedLazyTools(sessionFile) {
24
+ return sessionFile
25
+ ? (rememberedLazyToolsBySessionFile.get(sessionFile) ?? new Set())
26
+ : new Set();
27
+ }
28
+ export function clearRememberedLazyTools(sessionFile) {
29
+ if (sessionFile)
30
+ rememberedLazyToolsBySessionFile.delete(sessionFile);
31
+ }
32
+ export function inheritRememberedLazyTools(parentSessionFile, childSessionFile) {
33
+ if (!parentSessionFile ||
34
+ !childSessionFile ||
35
+ parentSessionFile === childSessionFile)
36
+ return;
37
+ const remembered = rememberedLazyToolsBySessionFile.get(parentSessionFile);
38
+ if (remembered)
39
+ rememberedLazyToolsBySessionFile.set(childSessionFile, new Set(remembered));
40
+ }
41
+ export function resetRememberedLazyToolsForTests() {
42
+ rememberedLazyToolsBySessionFile.clear();
43
+ }
2
44
  /**
3
45
  * Whether the host will send this model deferred (searchable) tool
4
46
  * definitions rather than the full inline list.