@herbertgao/pi-extensions 2026.9.15 → 2026.9.17
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.
- package/README.md +5 -0
- package/THIRD_PARTY_NOTICES.md +26 -0
- package/node_modules/@herbertgao/pi-cc-extensions/package.json +1 -1
- package/node_modules/@herbertgao/sol-pi/package.json +1 -1
- package/node_modules/@herbertgao/sol-pi/src/sol-pi/extensions/online-context-compact/extension.ts +11 -53
- package/node_modules/@narumitw/pi-btw/dist/index.ts +12 -6
- package/node_modules/@narumitw/pi-btw/dist/index.ts.map +3 -3
- package/node_modules/@narumitw/pi-btw/package.json +4 -4
- package/node_modules/@narumitw/pi-btw/src/main-tree-picker.ts +2 -0
- package/node_modules/@narumitw/pi-btw/src/side-thread.ts +16 -4
- package/node_modules/pi-lens/CHANGELOG.md +498 -0
- package/node_modules/pi-lens/README.md +45 -2
- package/node_modules/pi-lens/dist/clients/actionable-warnings.js +23 -7
- package/node_modules/pi-lens/dist/clients/advisory-provenance.js +187 -51
- package/node_modules/pi-lens/dist/clients/cache-manager.js +64 -5
- package/node_modules/pi-lens/dist/clients/child-unref.js +17 -8
- package/node_modules/pi-lens/dist/clients/config-diagnostic-codes.js +2 -0
- package/node_modules/pi-lens/dist/clients/config-locations.js +237 -9
- package/node_modules/pi-lens/dist/clients/config-resolve.js +7 -1
- package/node_modules/pi-lens/dist/clients/degradation-ledger.js +33 -3
- package/node_modules/pi-lens/dist/clients/dependency-checker.js +87 -64
- package/node_modules/pi-lens/dist/clients/diagnostic-dispositions.js +39 -3
- package/node_modules/pi-lens/dist/clients/dispatch/finding-policy.js +118 -1
- package/node_modules/pi-lens/dist/clients/dispatch/indent-detect.js +8 -2
- package/node_modules/pi-lens/dist/clients/dispatch/inline-suppressions.js +23 -4
- package/node_modules/pi-lens/dist/clients/dispatch/rule-ignores.js +14 -7
- package/node_modules/pi-lens/dist/clients/dispatch/runners/actionlint.js +26 -28
- package/node_modules/pi-lens/dist/clients/dispatch/runners/biome-check.js +38 -25
- package/node_modules/pi-lens/dist/clients/dispatch/runners/cpp-check.js +32 -49
- package/node_modules/pi-lens/dist/clients/dispatch/runners/credo.js +16 -14
- package/node_modules/pi-lens/dist/clients/dispatch/runners/cue-vet.js +25 -7
- package/node_modules/pi-lens/dist/clients/dispatch/runners/dart-analyze.js +28 -47
- package/node_modules/pi-lens/dist/clients/dispatch/runners/detekt.js +11 -4
- package/node_modules/pi-lens/dist/clients/dispatch/runners/dotnet-build.js +27 -22
- package/node_modules/pi-lens/dist/clients/dispatch/runners/elixir-check.js +31 -46
- package/node_modules/pi-lens/dist/clients/dispatch/runners/eslint.js +29 -46
- package/node_modules/pi-lens/dist/clients/dispatch/runners/gleam-check.js +91 -11
- package/node_modules/pi-lens/dist/clients/dispatch/runners/go-vet.js +16 -2
- package/node_modules/pi-lens/dist/clients/dispatch/runners/golangci-lint.js +38 -23
- package/node_modules/pi-lens/dist/clients/dispatch/runners/hadolint.js +27 -15
- package/node_modules/pi-lens/dist/clients/dispatch/runners/htmlhint.js +17 -8
- package/node_modules/pi-lens/dist/clients/dispatch/runners/javac.js +31 -21
- package/node_modules/pi-lens/dist/clients/dispatch/runners/ktlint.js +34 -60
- package/node_modules/pi-lens/dist/clients/dispatch/runners/lsp.js +6 -5
- package/node_modules/pi-lens/dist/clients/dispatch/runners/markdownlint.js +12 -5
- package/node_modules/pi-lens/dist/clients/dispatch/runners/mypy.js +2 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/oxlint.js +22 -6
- package/node_modules/pi-lens/dist/clients/dispatch/runners/php-lint.js +39 -16
- package/node_modules/pi-lens/dist/clients/dispatch/runners/phpstan.js +1 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/rubocop.js +27 -18
- package/node_modules/pi-lens/dist/clients/dispatch/runners/ruff.js +18 -19
- package/node_modules/pi-lens/dist/clients/dispatch/runners/rust-clippy.js +6 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/shellcheck.js +6 -2
- package/node_modules/pi-lens/dist/clients/dispatch/runners/spellcheck.js +10 -3
- package/node_modules/pi-lens/dist/clients/dispatch/runners/sqlfluff.js +11 -2
- package/node_modules/pi-lens/dist/clients/dispatch/runners/stylelint.js +10 -2
- package/node_modules/pi-lens/dist/clients/dispatch/runners/swiftlint.js +11 -2
- package/node_modules/pi-lens/dist/clients/dispatch/runners/taplo.js +12 -6
- package/node_modules/pi-lens/dist/clients/dispatch/runners/tflint.js +30 -15
- package/node_modules/pi-lens/dist/clients/dispatch/runners/tree-sitter.js +23 -0
- package/node_modules/pi-lens/dist/clients/dispatch/runners/trivy-config.js +8 -2
- package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/diagnostic-parsers.js +11 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/vale.js +15 -3
- package/node_modules/pi-lens/dist/clients/dispatch/runners/yamllint.js +12 -2
- package/node_modules/pi-lens/dist/clients/dispatch/runners/zig-check.js +27 -39
- package/node_modules/pi-lens/dist/clients/dispatch/suppress-writer.js +13 -3
- package/node_modules/pi-lens/dist/clients/effective-config.js +3 -1
- package/node_modules/pi-lens/dist/clients/finding-delivery-gate.js +42 -11
- package/node_modules/pi-lens/dist/clients/formatters.js +49 -11
- package/node_modules/pi-lens/dist/clients/git-guard.js +247 -34
- package/node_modules/pi-lens/dist/clients/gradle-ktfmt-style.js +121 -25
- package/node_modules/pi-lens/dist/clients/inline-blocker-dispositions.js +186 -0
- package/node_modules/pi-lens/dist/clients/install-diagnostics.js +19 -6
- package/node_modules/pi-lens/dist/clients/installer/index.js +281 -32
- package/node_modules/pi-lens/dist/clients/jscpd-client.js +8 -0
- package/node_modules/pi-lens/dist/clients/latency-logger.js +8 -0
- package/node_modules/pi-lens/dist/clients/lens-config.js +66 -8
- package/node_modules/pi-lens/dist/clients/lsp/client.js +139 -11
- package/node_modules/pi-lens/dist/clients/lsp/config.js +9 -7
- package/node_modules/pi-lens/dist/clients/lsp/content-limits.js +41 -0
- package/node_modules/pi-lens/dist/clients/lsp/index.js +1 -1
- package/node_modules/pi-lens/dist/clients/lsp/launch.js +23 -2
- package/node_modules/pi-lens/dist/clients/lsp/server.js +106 -25
- package/node_modules/pi-lens/dist/clients/lsp/sync-kind.js +14 -0
- package/node_modules/pi-lens/dist/clients/lsp/wait-policy/strategies.js +105 -16
- package/node_modules/pi-lens/dist/clients/mcp/ipc.js +232 -44
- package/node_modules/pi-lens/dist/clients/mcp/review.js +53 -7
- package/node_modules/pi-lens/dist/clients/opengrep-client.js +11 -6
- package/node_modules/pi-lens/dist/clients/pipeline.js +121 -66
- package/node_modules/pi-lens/dist/clients/project-diagnostics/fresh-fetch.js +21 -3
- package/node_modules/pi-lens/dist/clients/project-lens-config.js +11 -3
- package/node_modules/pi-lens/dist/clients/review-graph/builder.js +34 -26
- package/node_modules/pi-lens/dist/clients/review-graph/git-identity.js +16 -10
- package/node_modules/pi-lens/dist/clients/ruff-client.js +31 -4
- package/node_modules/pi-lens/dist/clients/runtime-coordinator.js +38 -4
- package/node_modules/pi-lens/dist/clients/runtime-tool-call.js +16 -5
- package/node_modules/pi-lens/dist/clients/runtime-tool-result.js +27 -12
- package/node_modules/pi-lens/dist/clients/runtime-turn.js +447 -280
- package/node_modules/pi-lens/dist/clients/safe-spawn.js +256 -65
- package/node_modules/pi-lens/dist/clients/spawn-output-cap.js +84 -1
- package/node_modules/pi-lens/dist/clients/tree-sitter-client.js +119 -58
- package/node_modules/pi-lens/dist/clients/turn-end/lane.js +51 -0
- package/node_modules/pi-lens/dist/clients/turn-end/lanes/govulncheck.js +112 -0
- package/node_modules/pi-lens/dist/clients/turn-end/lanes/secrets.js +204 -0
- package/node_modules/pi-lens/dist/clients/warm-attach.js +59 -0
- package/node_modules/pi-lens/dist/clients/widget-state.js +145 -14
- package/node_modules/pi-lens/dist/clients/workspace-topology.js +57 -3
- package/node_modules/pi-lens/dist/clients/zizmor-config.js +26 -13
- package/node_modules/pi-lens/dist/index.js +5695 -3733
- package/node_modules/pi-lens/dist/mcp/analyze-cli.js +4 -0
- package/node_modules/pi-lens/dist/mcp/server.js +33 -21
- package/node_modules/pi-lens/dist/scripts/lib/web-tree-sitter-dir.mjs +123 -0
- package/node_modules/pi-lens/dist/tools/lens-diagnostics.js +43 -16
- package/node_modules/pi-lens/dist/tools/lsp-diagnostics.js +118 -7
- package/node_modules/pi-lens/docs/adr/0001-stale-advisory-live-arm.md +29 -0
- package/node_modules/pi-lens/docs/adr/0002-workspace-hash-rendezvous.md +30 -0
- package/node_modules/pi-lens/docs/adr/0003-git-guard-latch-writer.md +203 -0
- package/node_modules/pi-lens/docs/adr/0004-disposition-policy-seam.md +38 -0
- package/node_modules/pi-lens/docs/adr/0005-tool-availability-enforcement-seam.md +29 -0
- package/node_modules/pi-lens/docs/adr/0006-derived-state-benchmark-first.md +31 -0
- package/node_modules/pi-lens/docs/adr/0007-end-to-end-witness-per-seam-slice.md +38 -0
- package/node_modules/pi-lens/docs/adr/0008-turn-end-lane-interface.md +165 -0
- package/node_modules/pi-lens/docs/adr/0009-reported-path-attribution.md +114 -0
- package/node_modules/pi-lens/docs/adr/README.md +26 -0
- package/node_modules/pi-lens/docs/agent-tools.md +18 -1
- package/node_modules/pi-lens/docs/ast-grep_rules_catalog.md +4 -3
- package/node_modules/pi-lens/docs/configuration.md +55 -1
- package/node_modules/pi-lens/docs/engineering-principles.md +202 -0
- package/node_modules/pi-lens/docs/environment-variables.md +61 -4
- package/node_modules/pi-lens/docs/features.md +2 -2
- package/node_modules/pi-lens/docs/globalconfig.md +6 -1
- package/node_modules/pi-lens/docs/language-coverage.md +17 -0
- package/node_modules/pi-lens/docs/lsp-capability-matrix.md +98 -31
- package/node_modules/pi-lens/docs/pi-lens-fixer.md +57 -3
- package/node_modules/pi-lens/docs/pi-lens-investigator.md +9 -0
- package/node_modules/pi-lens/docs/pi-lens-reviewer.md +16 -0
- package/node_modules/pi-lens/docs/pi-lens-subagent.md +9 -0
- package/node_modules/pi-lens/docs/public-api-stability.md +1 -0
- package/node_modules/pi-lens/docs/release-qa-baseline.md +2 -0
- package/node_modules/pi-lens/docs/servercapabilities.md +21 -11
- package/node_modules/pi-lens/docs/settings.md +12 -3
- package/node_modules/pi-lens/docs/subagent-compat.md +1 -1
- package/node_modules/pi-lens/package.json +5 -3
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-assert-tuple-test.yml +13 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-boolean-in-except-test.yml +26 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-http-headers-bracket-access-test.yml +10 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/unchecked-numeric-parse-python-test.yml +18 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/unchecked-throwing-call-python-test.yml +4 -3
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/go-no-fmt-println.yml +2 -2
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-assert-tuple.yml +23 -13
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-boolean-in-except.yml +7 -3
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-console-except-error-js.yml +2 -2
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-console-except-error.yml +2 -2
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-http-headers-bracket-access.yml +12 -7
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/unchecked-numeric-parse-python.yml +44 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/unchecked-throwing-call-python.yml +5 -3
- package/node_modules/pi-lens/rules/tree-sitter-queries/typescript/console-statement.yml +2 -2
- package/node_modules/pi-lens/scripts/install-selftest.mjs +21 -17
- package/node_modules/pi-lens/scripts/lib/web-tree-sitter-dir.mjs +123 -0
- package/node_modules/pi-mcp-adapter/CHANGELOG.md +29 -0
- package/node_modules/pi-mcp-adapter/OAUTH.md +369 -0
- package/node_modules/pi-mcp-adapter/README.md +12 -5
- package/node_modules/pi-mcp-adapter/config.ts +31 -8
- package/node_modules/pi-mcp-adapter/direct-tool-surface.ts +1 -1
- package/node_modules/pi-mcp-adapter/dist/config.js +33 -8
- package/node_modules/pi-mcp-adapter/dist/config.js.map +1 -1
- package/node_modules/pi-mcp-adapter/dist/json-schema-validator.js +15 -2
- package/node_modules/pi-mcp-adapter/dist/json-schema-validator.js.map +1 -1
- package/node_modules/pi-mcp-adapter/dist/mcp-auth.d.ts +2 -0
- package/node_modules/pi-mcp-adapter/dist/mcp-auth.js +27 -2
- package/node_modules/pi-mcp-adapter/dist/mcp-auth.js.map +1 -1
- package/node_modules/pi-mcp-adapter/dist/metadata-cache.js +1 -1
- package/node_modules/pi-mcp-adapter/dist/metadata-cache.js.map +1 -1
- package/node_modules/pi-mcp-adapter/dist/server-manager.js +6 -4
- package/node_modules/pi-mcp-adapter/dist/server-manager.js.map +1 -1
- package/node_modules/pi-mcp-adapter/dist/types.d.ts +2 -2
- package/node_modules/pi-mcp-adapter/dist/utils.d.ts +2 -0
- package/node_modules/pi-mcp-adapter/dist/utils.js +10 -3
- package/node_modules/pi-mcp-adapter/dist/utils.js.map +1 -1
- package/node_modules/pi-mcp-adapter/index.ts +37 -20
- package/node_modules/pi-mcp-adapter/init.ts +14 -15
- package/node_modules/pi-mcp-adapter/json-schema-validator.ts +16 -2
- package/node_modules/pi-mcp-adapter/mcp-auth.ts +27 -2
- package/node_modules/pi-mcp-adapter/metadata-cache.ts +1 -1
- package/node_modules/pi-mcp-adapter/package.json +3 -2
- package/node_modules/pi-mcp-adapter/proxy-modes.ts +1 -1
- package/node_modules/pi-mcp-adapter/server-manager.ts +6 -3
- package/node_modules/pi-mcp-adapter/tool-result-renderer.ts +99 -6
- package/node_modules/pi-mcp-adapter/types.ts +2 -2
- package/node_modules/pi-mcp-adapter/ui-session.ts +60 -27
- package/node_modules/pi-mcp-adapter/utils.ts +11 -3
- package/node_modules/pi-next-cue/.github/workflows/publish.yml +20 -0
- package/node_modules/pi-next-cue/LICENSE +21 -0
- package/node_modules/pi-next-cue/README.md +81 -0
- package/node_modules/pi-next-cue/index.ts +358 -0
- package/node_modules/pi-next-cue/package.json +28 -0
- package/package.json +9 -6
|
@@ -16,7 +16,7 @@ export { detectPythonVenv };
|
|
|
16
16
|
import { STAGE_TMP_PATTERN } from "../atomic-write-staging.js";
|
|
17
17
|
import { DOTNET_CSHARP_ROOT_MARKERS, DOTNET_FSHARP_ROOT_MARKERS, KIND_EXTENSIONS, } from "../file-kinds.js";
|
|
18
18
|
import { extensionsForLanguage } from "../language-registry.js";
|
|
19
|
-
import { CARGO_WORKSPACE_MEMBER_DIALECT, direntsHaveMarkerGlobMatch, isAtOrAboveHomeDir, isFullyQualified, isWindowsPath, matchesWorkspaceMemberPattern, pathsEqual, } from "../path-utils.js";
|
|
19
|
+
import { CARGO_WORKSPACE_MEMBER_DIALECT, direntsHaveMarkerGlobMatch, isAtOrAboveHomeDir, isFullyQualified, isWindowsPath, matchesWorkspaceMemberPattern, normalizeEphemeralMapKey, pathsEqual, toPosix, } from "../path-utils.js";
|
|
20
20
|
import { ensureTool, findManagedToolBinary, getToolEnvironment, getToolPath, } from "../installer/index.js";
|
|
21
21
|
import * as installer from "../installer/index.js";
|
|
22
22
|
import { classifyProbeFailure, describeInstallAttempt, logAvailabilityDecision, } from "../dispatch/runners/utils/availability-policy.js";
|
|
@@ -28,6 +28,10 @@ import { findLocalSgconfig, resolveBaselineSgconfig } from "../sgconfig.js";
|
|
|
28
28
|
import { findLocalTyposConfig } from "../typos-config.js";
|
|
29
29
|
import { resolvePackagePath } from "../package-root.js";
|
|
30
30
|
import { resolveToolCwd } from "../tool-cwd.js";
|
|
31
|
+
import { dirMtimeRecordsAsync, dirMtimesStillFreshAsync, unknownDirMtimeRecords, } from "../workspace-topology.js";
|
|
32
|
+
import { bounded } from "../deadline-utils.js";
|
|
33
|
+
import { FRESHNESS_CADENCE_MS } from "../freshness-cadence.js";
|
|
34
|
+
import { HOOK_WALL_BUDGET_MS } from "../hook-budgets.js";
|
|
31
35
|
import { recordDegradationOnce } from "../degradation-ledger.js";
|
|
32
36
|
import { hasCargoWorkspaceTable, readCargoWorkspaceExclude, readCargoWorkspaceMembers, readTextFileOrUndefined, } from "../cargo-manifest.js";
|
|
33
37
|
import { resolveAstGrepNativeExe } from "./wait-policy/index.js";
|
|
@@ -871,30 +875,33 @@ function nodeBinLocalCandidates(root, baseName) {
|
|
|
871
875
|
function nodeBinCandidates(root, baseName) {
|
|
872
876
|
return [...nodeBinLocalCandidates(root, baseName), baseName];
|
|
873
877
|
}
|
|
874
|
-
function normalizeSlashKey(value) {
|
|
875
|
-
const normalized = path.resolve(value).replace(/\\/g, "/");
|
|
876
|
-
return process.platform === "win32" ? normalized.toLowerCase() : normalized;
|
|
877
|
-
}
|
|
878
878
|
function piAgentExtensionsRootKey(file) {
|
|
879
|
-
|
|
879
|
+
// #1193 P3: `normalizeEphemeralMapKey` IS this module's former
|
|
880
|
+
// `normalizeSlashKey` — slash-fold, then lowercase only on win32 — and these
|
|
881
|
+
// directory keys are exactly what it is scoped for: process-local, same-run,
|
|
882
|
+
// derived from this process's own `path.resolve`, never persisted and never
|
|
883
|
+
// compared against an externally supplied spelling.
|
|
884
|
+
const dirKey = normalizeEphemeralMapKey(path.dirname(path.resolve(file)));
|
|
880
885
|
const marker = "/.pi/agent/extensions";
|
|
881
886
|
const index = dirKey.indexOf(marker);
|
|
882
887
|
if (index === -1)
|
|
883
888
|
return undefined;
|
|
884
889
|
return dirKey.slice(0, index + marker.length);
|
|
885
890
|
}
|
|
886
|
-
function normalizeRootKey(root) {
|
|
887
|
-
return process.platform === "win32"
|
|
888
|
-
? path.resolve(root).toLowerCase()
|
|
889
|
-
: path.resolve(root);
|
|
890
|
-
}
|
|
891
891
|
function IgnoreHomeRoot(primary) {
|
|
892
|
-
|
|
892
|
+
// #1193 P3: the former `normalizeRootKey` folded case on win32 but left the
|
|
893
|
+
// separator alone; both sides of the comparison below go through the same
|
|
894
|
+
// derivation, so adding the seam's slash fold cannot change the verdict for
|
|
895
|
+
// any spelling the old copy already agreed on, and it makes a mixed-separator
|
|
896
|
+
// root agree with the home directory it names.
|
|
897
|
+
const homeKey = normalizeEphemeralMapKey(path.resolve(os.homedir()));
|
|
893
898
|
return withRootMarkers(async (file) => {
|
|
894
899
|
const root = await primary(file);
|
|
895
900
|
if (!root)
|
|
896
901
|
return undefined;
|
|
897
|
-
return
|
|
902
|
+
return normalizeEphemeralMapKey(path.resolve(root)) === homeKey
|
|
903
|
+
? undefined
|
|
904
|
+
: root;
|
|
898
905
|
}, primary.rootMarkers ?? []);
|
|
899
906
|
}
|
|
900
907
|
function rubyBinCandidates(baseName) {
|
|
@@ -991,6 +998,22 @@ function isPermissionFsError(err) {
|
|
|
991
998
|
const code = err?.code;
|
|
992
999
|
return code === "EACCES" || code === "EPERM";
|
|
993
1000
|
}
|
|
1001
|
+
/**
|
|
1002
|
+
* The directory `markerExists(dir, pattern)` reads to answer for `pattern`:
|
|
1003
|
+
* `dir` itself for a plain basename, and the named subdirectory for a pattern
|
|
1004
|
+
* that carries a path segment (`"prisma/schema.prisma"`). `NearestRoot`'s
|
|
1005
|
+
* freshness records are keyed on these directories, so the derivation lives
|
|
1006
|
+
* HERE, next to the probe it describes — a second copy could drift and silently
|
|
1007
|
+
* stop covering a pattern shape (#3412).
|
|
1008
|
+
*/
|
|
1009
|
+
function markerProbeDir(dir, pattern) {
|
|
1010
|
+
const normalized = toPosix(pattern);
|
|
1011
|
+
const slash = normalized.lastIndexOf("/");
|
|
1012
|
+
const parentPattern = slash >= 0 ? normalized.slice(0, slash) : "";
|
|
1013
|
+
return parentPattern
|
|
1014
|
+
? path.join(dir, ...parentPattern.split("/").filter(Boolean))
|
|
1015
|
+
: dir;
|
|
1016
|
+
}
|
|
994
1017
|
async function markerExists(dir, pattern) {
|
|
995
1018
|
if (!pattern.includes("*")) {
|
|
996
1019
|
try {
|
|
@@ -1006,13 +1029,10 @@ async function markerExists(dir, pattern) {
|
|
|
1006
1029
|
}
|
|
1007
1030
|
const normalized = pattern.replace(/\\/g, "/");
|
|
1008
1031
|
const slash = normalized.lastIndexOf("/");
|
|
1009
|
-
const parentPattern = slash >= 0 ? normalized.slice(0, slash) : "";
|
|
1010
1032
|
const basenamePattern = slash >= 0 ? normalized.slice(slash + 1) : normalized;
|
|
1011
1033
|
if (!basenamePattern)
|
|
1012
1034
|
return false;
|
|
1013
|
-
const targetDir =
|
|
1014
|
-
? path.join(dir, ...parentPattern.split("/").filter(Boolean))
|
|
1015
|
-
: dir;
|
|
1035
|
+
const targetDir = markerProbeDir(dir, normalized);
|
|
1016
1036
|
try {
|
|
1017
1037
|
const entries = await readdir(targetDir, { withFileTypes: true });
|
|
1018
1038
|
// Match files/symlinks only — a directory named like the marker (e.g. a
|
|
@@ -1028,8 +1048,22 @@ async function markerExists(dir, pattern) {
|
|
|
1028
1048
|
return false;
|
|
1029
1049
|
}
|
|
1030
1050
|
}
|
|
1031
|
-
|
|
1032
|
-
|
|
1051
|
+
/**
|
|
1052
|
+
* One bound for every directory-mtime read this detector awaits. `signal` is
|
|
1053
|
+
* `undefined` on purpose: `RootFunction` takes a file path and nothing else, so
|
|
1054
|
+
* no hook signal reaches this seam until #2523 AC4 threads one — the wall clock
|
|
1055
|
+
* is the live half, and the registry entry says so. The budget is the edit
|
|
1056
|
+
* `tool_result` one because that is the hook the per-file touch path runs under
|
|
1057
|
+
* and the only hook the contract lets block the host; a tighter number would
|
|
1058
|
+
* abandon a revalidation that is strictly CHEAPER than the walk it falls back
|
|
1059
|
+
* to, making the hook slower rather than safer.
|
|
1060
|
+
*/
|
|
1061
|
+
const rootMtimeBound = (label) => ({
|
|
1062
|
+
ms: HOOK_WALL_BUDGET_MS.tool_result_edit,
|
|
1063
|
+
signal: undefined,
|
|
1064
|
+
hook: "tool_result_edit",
|
|
1065
|
+
label,
|
|
1066
|
+
});
|
|
1033
1067
|
/**
|
|
1034
1068
|
* Walk up the directory tree looking for project root markers.
|
|
1035
1069
|
*
|
|
@@ -1055,21 +1089,55 @@ export function NearestRoot(includePatterns, excludePatterns, stopDir) {
|
|
|
1055
1089
|
// present transition must work without a process restart. The uncached
|
|
1056
1090
|
// re-walk cost for configless repos is a known trade-off; bounding the
|
|
1057
1091
|
// walk with stopDir for in-cwd files is the tracked optimization (#1412).
|
|
1092
|
+
//
|
|
1093
|
+
// A HIT is revalidated instead of trusted for the session: it carries the
|
|
1094
|
+
// mtime of every directory the walk probed, and is served only while all of
|
|
1095
|
+
// them are unchanged. Without that, a nearer marker scaffolded below an
|
|
1096
|
+
// already-resolved root (`swift package init` in a subdirectory) — and a
|
|
1097
|
+
// marker REMOVED at the resolved root — stayed invisible for the whole
|
|
1098
|
+
// session, the other half of the very transition the paragraph above
|
|
1099
|
+
// handles (#3412; same shape as #2922 at the dispatch marker seam).
|
|
1058
1100
|
const inFlight = new Map();
|
|
1101
|
+
// The patterns whose probe reads a subdirectory of the walked directory
|
|
1102
|
+
// instead of the walked directory itself: a pattern carrying a path segment
|
|
1103
|
+
// (`prisma/schema.prisma`) is answered from a subdirectory, so creating that
|
|
1104
|
+
// marker bumps the SUBDIRECTORY's mtime and the walked directory's own only
|
|
1105
|
+
// when the subdirectory had to be created too. Derived once from this
|
|
1106
|
+
// detector's pattern table, so a new pattern shape is covered on arrival
|
|
1107
|
+
// without a second list to maintain.
|
|
1108
|
+
const subdirPatterns = [];
|
|
1109
|
+
for (const pattern of [...includePatterns, ...(excludePatterns ?? [])]) {
|
|
1110
|
+
if (markerProbeDir("", pattern))
|
|
1111
|
+
subdirPatterns.push(pattern);
|
|
1112
|
+
}
|
|
1059
1113
|
return withRootMarkers(async (file) => {
|
|
1060
1114
|
// Cache key is the resolved directory — all files in the same dir share a root.
|
|
1061
1115
|
const startDir = path.resolve(path.dirname(file));
|
|
1062
1116
|
const dirKey = normalizeMapKey(startDir);
|
|
1063
|
-
// Fast path: already resolved for this directory
|
|
1117
|
+
// Fast path: already resolved for this directory, nothing the walk looked at
|
|
1118
|
+
// has changed since, and the entry is younger than the shared re-check
|
|
1119
|
+
// cadence. One stat per probed directory replaces the whole walk (markers x
|
|
1120
|
+
// directories, plus the hit directory's `isExcludedLspRoot` git-boundary
|
|
1121
|
+
// walk), so the memo still pays for itself; the cadence is what bounds the
|
|
1122
|
+
// axes a directory mtime cannot see — a same-tick create, and a `.gitignore`
|
|
1123
|
+
// above the hit changing the exclusion verdict — to one window instead of
|
|
1124
|
+
// the session (#3412 review round 1). A fired bound resolves `undefined`,
|
|
1125
|
+
// which is NOT freshness: it falls through to the walk.
|
|
1064
1126
|
const cached = cache.get(dirKey);
|
|
1065
|
-
if (cached !== undefined
|
|
1066
|
-
|
|
1127
|
+
if (cached !== undefined &&
|
|
1128
|
+
Date.now() - cached.walkedAt < FRESHNESS_CADENCE_MS &&
|
|
1129
|
+
(await bounded(dirMtimesStillFreshAsync(cached.probedDirs), rootMtimeBound("lsp-root-memo-freshness"))) === true)
|
|
1130
|
+
return cached.root;
|
|
1067
1131
|
// In-flight deduplication: if N parallel pipelines edit files in the same
|
|
1068
1132
|
// directory simultaneously, only one stat-walk runs; the rest await the same
|
|
1069
1133
|
// promise. This is the main fix for parallel-turn LSP timeout spikes.
|
|
1070
1134
|
const flying = inFlight.get(dirKey);
|
|
1071
1135
|
if (flying)
|
|
1072
1136
|
return flying;
|
|
1137
|
+
const probedDirs = [];
|
|
1138
|
+
// Stamped before the walk so the window also covers the walk's own duration
|
|
1139
|
+
// — the conservative direction, by the walk's runtime (~0.2 ms measured).
|
|
1140
|
+
const walkedAt = Date.now();
|
|
1073
1141
|
const promise = (async () => {
|
|
1074
1142
|
let currentDir = startDir;
|
|
1075
1143
|
const fsRoot = path.parse(currentDir).root;
|
|
@@ -1080,6 +1148,19 @@ export function NearestRoot(includePatterns, excludePatterns, stopDir) {
|
|
|
1080
1148
|
currentDir !== stop) {
|
|
1081
1149
|
break;
|
|
1082
1150
|
}
|
|
1151
|
+
// Record every directory this step is about to read, BEFORE reading
|
|
1152
|
+
// it. Ordering is the invariant: a marker created between the stat
|
|
1153
|
+
// and the probe leaves the record STALE (the next call re-walks and
|
|
1154
|
+
// finds it), whereas recording afterwards would store the mtime that
|
|
1155
|
+
// already includes the creation and hide it for the session. Only
|
|
1156
|
+
// directories up to the hit are recorded — a marker created above a
|
|
1157
|
+
// hit cannot change which root is nearest.
|
|
1158
|
+
const stepDirs = [
|
|
1159
|
+
currentDir,
|
|
1160
|
+
...subdirPatterns.map((pattern) => markerProbeDir(currentDir, pattern)),
|
|
1161
|
+
];
|
|
1162
|
+
const stepRecords = await bounded(dirMtimeRecordsAsync(stepDirs), rootMtimeBound("lsp-root-walk-dir-mtimes"));
|
|
1163
|
+
probedDirs.push(...(stepRecords ?? unknownDirMtimeRecords(stepDirs)));
|
|
1083
1164
|
// Check exclude patterns — skip this dir (but keep walking up)
|
|
1084
1165
|
if (excludePatterns) {
|
|
1085
1166
|
let excluded = false;
|
|
@@ -1114,7 +1195,7 @@ export function NearestRoot(includePatterns, excludePatterns, stopDir) {
|
|
|
1114
1195
|
try {
|
|
1115
1196
|
const result = await promise;
|
|
1116
1197
|
if (result !== undefined)
|
|
1117
|
-
cache.set(dirKey, result);
|
|
1198
|
+
cache.set(dirKey, { root: result, probedDirs, walkedAt });
|
|
1118
1199
|
return result;
|
|
1119
1200
|
}
|
|
1120
1201
|
finally {
|
|
@@ -1595,7 +1676,7 @@ async function findExtensionBoundedRoot(file, extensionRootKey) {
|
|
|
1595
1676
|
}
|
|
1596
1677
|
// Stop at or beyond the extensions root — never walk into the
|
|
1597
1678
|
// pi-agent-wide scope.
|
|
1598
|
-
const currentKey =
|
|
1679
|
+
const currentKey = normalizeEphemeralMapKey(path.resolve(currentDir));
|
|
1599
1680
|
if (currentKey === extensionRootKey)
|
|
1600
1681
|
return undefined;
|
|
1601
1682
|
const parent = path.dirname(currentDir);
|
|
@@ -2623,7 +2704,7 @@ export const FishServer = {
|
|
|
2623
2704
|
spawn(root, options) {
|
|
2624
2705
|
return resolveAndLaunch({
|
|
2625
2706
|
candidates: nodeBinCandidates(root, "fish-lsp"),
|
|
2626
|
-
args: ["start"],
|
|
2707
|
+
args: ["start", "--stdio"],
|
|
2627
2708
|
cwd: root,
|
|
2628
2709
|
managedToolId: "fish-lsp",
|
|
2629
2710
|
}, options?.allowInstall);
|
|
@@ -40,3 +40,17 @@ export function negotiateSyncKind(serverCapabilities) {
|
|
|
40
40
|
}
|
|
41
41
|
return TEXT_DOCUMENT_SYNC_KIND_FULL;
|
|
42
42
|
}
|
|
43
|
+
export function negotiateSaveOptions(serverCapabilities) {
|
|
44
|
+
const sync = serverCapabilities?.textDocumentSync;
|
|
45
|
+
if (!sync || typeof sync !== "object")
|
|
46
|
+
return undefined;
|
|
47
|
+
const save = sync.save;
|
|
48
|
+
if (save === true)
|
|
49
|
+
return { includeText: false };
|
|
50
|
+
if (save && typeof save === "object") {
|
|
51
|
+
return {
|
|
52
|
+
includeText: save.includeText === true,
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
return undefined;
|
|
56
|
+
}
|
|
@@ -220,8 +220,8 @@ export const SERVER_DIAGNOSTIC_STRATEGIES = {
|
|
|
220
220
|
// once the index has already had a full aggregateWaitMs window.
|
|
221
221
|
workspaceIndexing: true,
|
|
222
222
|
workspaceIndexingWarmWaitMs: 250,
|
|
223
|
-
// #799: marksman is push-only and publishes NOTHING on
|
|
224
|
-
// there is no pull fallback and no sync-confirm protocol (unlike
|
|
223
|
+
// #799 / run 35914033696: marksman is push-only and publishes NOTHING on
|
|
224
|
+
// a clean transition — there is no pull fallback and no sync-confirm protocol (unlike
|
|
225
225
|
// typescript's tsserver commands), so a clean markdown file's touch
|
|
226
226
|
// waits its full budget with zero signal either way. Marking it
|
|
227
227
|
// `silentOnClean` lets the generic push-only clean-confirm gate
|
|
@@ -243,20 +243,9 @@ export const SERVER_DIAGNOSTIC_STRATEGIES = {
|
|
|
243
243
|
// diagnostics this strategy governs are a REAL but PARTIAL signal, not
|
|
244
244
|
// full CUE validation.
|
|
245
245
|
//
|
|
246
|
-
//
|
|
247
|
-
//
|
|
248
|
-
//
|
|
249
|
-
// "diagnostics-wait"` after the full budget, never an affirmative empty
|
|
250
|
-
// array). This reproduces the #1520 review's original "reports
|
|
251
|
-
// conclusively clean while still waiting" concern for the clean-cold-open
|
|
252
|
-
// case specifically. It is NOT silent on every transition, though: once a
|
|
253
|
-
// document is open and a `didChange` lands, the server DOES publish —
|
|
254
|
-
// both the error (edit clean→broken: confirmed, ~930ms warm) and an
|
|
255
|
-
// explicit empty array clearing it (edit broken→clean: confirmed,
|
|
256
|
-
// ~320ms warm, `contentHash`-bound). That edit-triggered empty publish is
|
|
257
|
-
// exactly what the shared push-only clean-confirm gate needs
|
|
258
|
-
// `silentOnClean` for — it only ever governs the "no publish, notify
|
|
259
|
-
// succeeded" case, never a case that already got a real publish.
|
|
246
|
+
// The repaired clean-signal probe measured cue as publishing a versioned
|
|
247
|
+
// clean-transition set (run 35914033696), so this marker stays absent:
|
|
248
|
+
// the cascade must retain its normal early-publish path.
|
|
260
249
|
//
|
|
261
250
|
// seedFirstPush: true — the one cold-open case that DOES publish (a
|
|
262
251
|
// file that is already broken) sends the complete single diagnostic on
|
|
@@ -267,6 +256,96 @@ export const SERVER_DIAGNOSTIC_STRATEGIES = {
|
|
|
267
256
|
// (~930ms, syntax reparse) with margin; a cold spawn's first push (~2.1s
|
|
268
257
|
// end-to-end including process start) is bounded separately by
|
|
269
258
|
// maxClientWaitMs, not this per-diagnostics budget.
|
|
259
|
+
// intelephense (php, #3310). Measured directly against the real v1.18.5
|
|
260
|
+
// binary over a raw JSON-RPC session answering `workspace/configuration`
|
|
261
|
+
// the way pi-lens does (2026-09-23, linux):
|
|
262
|
+
//
|
|
263
|
+
// +284ms initialize response — NO diagnosticProvider (push-only)
|
|
264
|
+
// +295ms publishDiagnostics diags=0 version=undefined <- pre-index
|
|
265
|
+
// +301ms indexingStarted
|
|
266
|
+
// +689ms indexingEnded
|
|
267
|
+
// +696ms publishDiagnostics diags=2 version=undefined <- the answer
|
|
268
|
+
//
|
|
269
|
+
// so `emptyFirstPublish: "indexing"`: the first publish is an artifact of
|
|
270
|
+
// the cold index, not an answer. The same session on a genuinely CLEAN
|
|
271
|
+
// file publishes `[]` at +304ms and `[]` AGAIN at +663ms (right after
|
|
272
|
+
// `indexingEnded`), which is what lets the held publish be released by
|
|
273
|
+
// the server's own second publish instead of by a timer — and a WARM
|
|
274
|
+
// touch (a second file, index already built) publishes the real set
|
|
275
|
+
// FIRST at +5ms, so the hold costs nothing once the session is warm.
|
|
276
|
+
//
|
|
277
|
+
// seedFirstPush: false — the measurement above is the direct refutation
|
|
278
|
+
// of "the first push is complete" for this server.
|
|
279
|
+
// pullRetryBudgetMs: 0 — no pull provider is advertised at all.
|
|
280
|
+
// aggregateWaitMs: 8000 — the budget must cover the cold index window,
|
|
281
|
+
// which is what the second (real) publish waits on: ~0.4s for a 139-file
|
|
282
|
+
// index on the dev box, ~5s on the nightly ubuntu runner (#3217's
|
|
283
|
+
// transcript). Per-edit callers cap this as a CEILING (#242), so an
|
|
284
|
+
// interactive edit still waits only its own cap; the uncapped paths (the
|
|
285
|
+
// tool-smoke LSP gate, which itself caps at 8000ms) get the full budget,
|
|
286
|
+
// and the cold index is paid once per session, not once per touch.
|
|
287
|
+
php: {
|
|
288
|
+
seedFirstPush: false,
|
|
289
|
+
pullRetryBudgetMs: 0,
|
|
290
|
+
debounceMs: 150,
|
|
291
|
+
aggregateWaitMs: 8000,
|
|
292
|
+
expectSemanticSecondPush: false,
|
|
293
|
+
emptyFirstPublish: "indexing",
|
|
294
|
+
},
|
|
295
|
+
// Svelte's pull diagnostics settle after the default 1500ms budget on a
|
|
296
|
+
// cold server. The tool-smoke gate caps waits at 8000ms, so give this
|
|
297
|
+
// server enough aggregate budget to return its seeded findings (#3311).
|
|
298
|
+
svelte: {
|
|
299
|
+
seedFirstPush: false,
|
|
300
|
+
pullRetryBudgetMs: 0,
|
|
301
|
+
debounceMs: 150,
|
|
302
|
+
aggregateWaitMs: 4000,
|
|
303
|
+
expectSemanticSecondPush: false,
|
|
304
|
+
},
|
|
305
|
+
// csharp-ls loads (design-time-builds) the restored project AFTER
|
|
306
|
+
// `initialize` returns, so the publish for a seeded CS0029 lands inside
|
|
307
|
+
// `waitForDiagnostics`, not inside the client wait. MEASURED A/B over two
|
|
308
|
+
// nightly Tool-smoke runs of the same fixture with `dotnet restore` already
|
|
309
|
+
// done — the ONLY difference between them was this budget:
|
|
310
|
+
// * `aggregateWaitMs` 1500 (the default): run 36054901266 →
|
|
311
|
+
// `[csharp] touched=0`, 4.05s after `Restored …/toolsmoke.csproj`.
|
|
312
|
+
// A real compiler error read as clean.
|
|
313
|
+
// * 8000: run 36058292424 → `[csharp] touched=1`, 6.49s after the same
|
|
314
|
+
// line; gate run 36059988117 → `lsp_diagnostics returned 1 primary
|
|
315
|
+
// finding`, 4.86s after it.
|
|
316
|
+
// Both windows INCLUDE workspace bootstrap + spawn + initialize, so they
|
|
317
|
+
// bound the wait from above: the required budget is in (1500, 4860] ms.
|
|
318
|
+
// 6000 covers the measured gate window with margin and stays under the
|
|
319
|
+
// tool-smoke gate's own 8000ms ceiling (that relation is pinned by
|
|
320
|
+
// tests/config/lsp-gate-population.test.ts), so the gate can still witness
|
|
321
|
+
// this budget. CONFIRMED at 6000 by run 36064829436: `✓ csharp csharp-ls 1
|
|
322
|
+
// lsp_diagnostics returned 1 primary finding`, 4.78s after `Restored
|
|
323
|
+
// …csproj`, census unchanged at gated 31 / handshake-only 8 / unavailable 6
|
|
324
|
+
// — so 6000 is measured as sufficient, not merely inferred from the 8000
|
|
325
|
+
// runs above. It is deliberately NOT the ceiling: an `lsp_diagnostics` call
|
|
326
|
+
// that passes no `waitMs` pays this budget in full on a file the server never
|
|
327
|
+
// publishes for (`tools/lsp-diagnostics.ts` leaves `maxClientWaitMs`
|
|
328
|
+
// undefined → `perServerTimeout` has no caller cap), so every 1000ms here
|
|
329
|
+
// is 1000ms of turn latency on the no-publication path (#3402 review r2).
|
|
330
|
+
// Every other field is DEFAULT_STRATEGY's value on purpose: csharp-ls is
|
|
331
|
+
// `mode=pull`/tier-1 authoritative-clean (#3311 investigator table), and
|
|
332
|
+
// nothing has measured its pull retry, so this entry moves the one field
|
|
333
|
+
// that was measured and no other (pinned in
|
|
334
|
+
// tests/clients/lsp/server-strategies.test.ts).
|
|
335
|
+
// fsharp/expert/vue deliberately have NO entry: the same two runs show
|
|
336
|
+
// `touched=0` at BOTH 1500 and 8000, so the budget is not what stops them
|
|
337
|
+
// publishing and an 8000 entry would buy nothing while costing every
|
|
338
|
+
// uncapped production call 6.5 extra seconds. Their fixture rows carry the
|
|
339
|
+
// observed-behavior exemption instead (`scripts/smoke-tools.mjs`), and a
|
|
340
|
+
// probe that wants a long window sets `PI_LENS_LSP_DIAGNOSTICS_MAX_WAIT_MS`
|
|
341
|
+
// — a flat harness override that needs no production budget.
|
|
342
|
+
csharp: {
|
|
343
|
+
seedFirstPush: false,
|
|
344
|
+
pullRetryBudgetMs: 250,
|
|
345
|
+
debounceMs: 150,
|
|
346
|
+
aggregateWaitMs: 6000,
|
|
347
|
+
expectSemanticSecondPush: false,
|
|
348
|
+
},
|
|
270
349
|
cue: {
|
|
271
350
|
seedFirstPush: true,
|
|
272
351
|
pullRetryBudgetMs: 0,
|
|
@@ -274,6 +353,16 @@ export const SERVER_DIAGNOSTIC_STRATEGIES = {
|
|
|
274
353
|
aggregateWaitMs: 2000,
|
|
275
354
|
expectSemanticSecondPush: false,
|
|
276
355
|
reopenOnResync: false,
|
|
356
|
+
},
|
|
357
|
+
// lua-language-server is push-only and was silent on clean transitions in
|
|
358
|
+
// the repaired probe (run 35914033696). Keep the marker on the measured
|
|
359
|
+
// server id so the cascade can skip its in-lane wait safely.
|
|
360
|
+
lua: {
|
|
361
|
+
seedFirstPush: true,
|
|
362
|
+
pullRetryBudgetMs: 0,
|
|
363
|
+
debounceMs: 150,
|
|
364
|
+
aggregateWaitMs: 2000,
|
|
365
|
+
expectSemanticSecondPush: false,
|
|
277
366
|
silentOnClean: true,
|
|
278
367
|
},
|
|
279
368
|
};
|