@herbertgao/pi-extensions 2026.9.2 → 2026.9.4
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 +4 -2
- package/THIRD_PARTY_NOTICES.md +49 -0
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/compact-mode.ts +3 -2
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/default-mode.ts +16 -11
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/diff/diff-renderer.ts +20 -6
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/grouping.ts +12 -7
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/result.ts +98 -0
- package/node_modules/@herbertgao/pi-cc-extensions/package.json +3 -3
- package/node_modules/@herbertgao/sol-pi/LICENSE +19 -0
- package/node_modules/@herbertgao/sol-pi/README.md +159 -0
- package/node_modules/@herbertgao/sol-pi/SECURITY.md +26 -0
- package/node_modules/@herbertgao/sol-pi/THIRD_PARTY_NOTICES.md +19 -0
- package/node_modules/@herbertgao/sol-pi/agents-install.md +150 -0
- package/node_modules/@herbertgao/sol-pi/assets/sol-pi-hero.png +0 -0
- package/node_modules/@herbertgao/sol-pi/docs/compatibility.md +69 -0
- package/node_modules/@herbertgao/sol-pi/docs/configuration.md +75 -0
- package/node_modules/@herbertgao/sol-pi/package.json +76 -0
- package/node_modules/@herbertgao/sol-pi/scripts/check-pi-compat.mjs +32 -0
- package/node_modules/@herbertgao/sol-pi/scripts/check-sol-pi-config.mjs +120 -0
- package/node_modules/@herbertgao/sol-pi/sol-pi.example.json +10 -0
- package/node_modules/@herbertgao/sol-pi/src/sol-pi/config.ts +135 -0
- package/node_modules/@herbertgao/sol-pi/src/sol-pi/extensions/action-fusion/file-queue.ts +74 -0
- package/node_modules/@herbertgao/sol-pi/src/sol-pi/extensions/action-fusion/index.ts +185 -0
- package/node_modules/@herbertgao/sol-pi/src/sol-pi/extensions/action-fusion/then-run.ts +128 -0
- package/node_modules/@herbertgao/sol-pi/src/sol-pi/extensions/evidence-preserving-reducer/archive.ts +53 -0
- package/node_modules/@herbertgao/sol-pi/src/sol-pi/extensions/evidence-preserving-reducer/candidate.ts +101 -0
- package/node_modules/@herbertgao/sol-pi/src/sol-pi/extensions/evidence-preserving-reducer/config.ts +71 -0
- package/node_modules/@herbertgao/sol-pi/src/sol-pi/extensions/evidence-preserving-reducer/index.ts +220 -0
- package/node_modules/@herbertgao/sol-pi/src/sol-pi/extensions/evidence-preserving-reducer/journal.ts +25 -0
- package/node_modules/@herbertgao/sol-pi/src/sol-pi/extensions/evidence-preserving-reducer/provider.ts +164 -0
- package/node_modules/@herbertgao/sol-pi/src/sol-pi/extensions/evidence-preserving-reducer/receipt.ts +177 -0
- package/node_modules/@herbertgao/sol-pi/src/sol-pi/extensions/observation-pack/index.ts +227 -0
- package/node_modules/@herbertgao/sol-pi/src/sol-pi/extensions/observation-pack/ledger.ts +20 -0
- package/node_modules/@herbertgao/sol-pi/src/sol-pi/extensions/observation-pack/observation.ts +252 -0
- package/node_modules/@herbertgao/sol-pi/src/sol-pi/extensions/online-context-compact/economics.ts +237 -0
- package/node_modules/@herbertgao/sol-pi/src/sol-pi/extensions/online-context-compact/extension.ts +455 -0
- package/node_modules/@herbertgao/sol-pi/src/sol-pi/extensions/online-context-compact/index.ts +49 -0
- package/node_modules/@herbertgao/sol-pi/src/sol-pi/extensions/online-context-compact/plan.ts +79 -0
- package/node_modules/@herbertgao/sol-pi/src/sol-pi/extensions/online-context-compact/state.ts +208 -0
- package/node_modules/@herbertgao/sol-pi/src/sol-pi/extensions/online-context-compact/tools.ts +100 -0
- package/node_modules/@herbertgao/sol-pi/src/sol-pi/index.ts +42 -0
- package/node_modules/@herbertgao/sol-pi/src/sol-pi/runtime-paths.ts +17 -0
- package/node_modules/@herbertgao/sol-pi/src/sol-pi/tui.ts +71 -0
- package/node_modules/@narumitw/pi-btw/dist/index.ts +12 -2
- package/node_modules/@narumitw/pi-btw/dist/index.ts.map +2 -2
- package/node_modules/@narumitw/pi-btw/package.json +1 -1
- package/node_modules/@narumitw/pi-btw/src/btw.ts +13 -2
- package/node_modules/pi-antigravity/LICENSE +21 -0
- package/node_modules/pi-antigravity/README.md +194 -0
- package/node_modules/pi-antigravity/package.json +67 -0
- package/node_modules/pi-antigravity/src/auth/index.ts +14 -0
- package/node_modules/pi-antigravity/src/auth/oauth.ts +442 -0
- package/node_modules/pi-antigravity/src/client/client.ts +561 -0
- package/node_modules/pi-antigravity/src/client/index.ts +1 -0
- package/node_modules/pi-antigravity/src/diagnostics/diagnostics.ts +96 -0
- package/node_modules/pi-antigravity/src/diagnostics/index.ts +1 -0
- package/node_modules/pi-antigravity/src/image/image.ts +336 -0
- package/node_modules/pi-antigravity/src/image/index.ts +1 -0
- package/node_modules/pi-antigravity/src/index.ts +280 -0
- package/node_modules/pi-antigravity/src/models/discovery.ts +154 -0
- package/node_modules/pi-antigravity/src/models/grouping.ts +424 -0
- package/node_modules/pi-antigravity/src/models/index.ts +3 -0
- package/node_modules/pi-antigravity/src/models/models.ts +500 -0
- package/node_modules/pi-antigravity/src/stream/index.ts +1 -0
- package/node_modules/pi-antigravity/src/stream/stream.ts +1460 -0
- package/node_modules/pi-antigravity/src/types/enums.ts +42 -0
- package/node_modules/pi-antigravity/src/types/index.ts +2 -0
- package/node_modules/pi-antigravity/src/types/types.ts +292 -0
- package/node_modules/pi-antigravity/src/usage/index.ts +1 -0
- package/node_modules/pi-antigravity/src/usage/usage.ts +371 -0
- package/node_modules/pi-antigravity/src/utils/http.ts +91 -0
- package/node_modules/pi-antigravity/src/utils/index.ts +3 -0
- package/node_modules/pi-antigravity/src/utils/security.ts +73 -0
- package/node_modules/pi-antigravity/src/utils/util.ts +132 -0
- package/node_modules/pi-antigravity/tsconfig.json +21 -0
- package/node_modules/pi-lens/CHANGELOG.md +176 -0
- package/node_modules/pi-lens/README.md +13 -8
- package/node_modules/pi-lens/config/dependency-cruiser-eager-allowlist.json +3 -1
- package/node_modules/pi-lens/dist/clients/analysed-root.js +1 -0
- package/node_modules/pi-lens/dist/clients/ast-grep-tool-logger.js +1 -1
- package/node_modules/pi-lens/dist/clients/biome-client.js +13 -2
- package/node_modules/pi-lens/dist/clients/bounded-telemetry.js +2 -0
- package/node_modules/pi-lens/dist/clients/cache-observability.js +122 -1
- package/node_modules/pi-lens/dist/clients/complexity-client.js +5 -0
- package/node_modules/pi-lens/dist/clients/config-diagnostic-codes.js +2 -0
- package/node_modules/pi-lens/dist/clients/config-resolve.js +6 -3
- package/node_modules/pi-lens/dist/clients/config-schema.js +17 -0
- package/node_modules/pi-lens/dist/clients/config-warn.js +2 -2
- package/node_modules/pi-lens/dist/clients/dead-code-client.js +8 -1
- package/node_modules/pi-lens/dist/clients/dependency-checker.js +5 -1
- package/node_modules/pi-lens/dist/clients/dispatch/dispatcher.js +38 -15
- package/node_modules/pi-lens/dist/clients/dispatch/facts/function-facts.js +1 -1
- package/node_modules/pi-lens/dist/clients/dispatch/facts/import-facts.js +5 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/actionlint.js +2 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/biome-check.js +2 -3
- package/node_modules/pi-lens/dist/clients/dispatch/runners/cpp-check.js +4 -3
- package/node_modules/pi-lens/dist/clients/dispatch/runners/credo.js +2 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/cue-vet.js +2 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/dart-analyze.js +2 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/detekt.js +2 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/dotnet-build.js +2 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/elixir-check.js +2 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/eslint.js +2 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/fish-indent.js +2 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/gleam-check.js +2 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/go-vet.js +2 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/golangci-lint.js +2 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/hadolint.js +2 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/helm-lint.js +3 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/helm-render.js +2 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/htmlhint.js +2 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/javac.js +2 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/ktlint.js +2 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/lsp.js +19 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/markdownlint.js +3 -2
- package/node_modules/pi-lens/dist/clients/dispatch/runners/mypy.js +2 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/oxlint.js +2 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/php-lint.js +2 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/phpstan.js +2 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/prisma-validate.js +2 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/psscriptanalyzer.js +12 -12
- package/node_modules/pi-lens/dist/clients/dispatch/runners/pyright.js +2 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/rubocop.js +2 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/ruff.js +2 -2
- package/node_modules/pi-lens/dist/clients/dispatch/runners/rust-clippy.js +12 -11
- package/node_modules/pi-lens/dist/clients/dispatch/runners/shellcheck.js +2 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/shfmt.js +2 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/spellcheck.js +2 -2
- package/node_modules/pi-lens/dist/clients/dispatch/runners/spotbugs.js +2 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/sqlfluff.js +3 -2
- package/node_modules/pi-lens/dist/clients/dispatch/runners/stylelint.js +3 -2
- package/node_modules/pi-lens/dist/clients/dispatch/runners/swiftlint.js +2 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/taplo.js +2 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/terragrunt.js +2 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/tflint.js +2 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/trivy-config.js +2 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/availability-policy.js +5 -0
- package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/candidate-probe.js +2 -2
- package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/runner-helpers.js +8 -2
- package/node_modules/pi-lens/dist/clients/dispatch/runners/vale.js +2 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/yamllint.js +4 -3
- package/node_modules/pi-lens/dist/clients/dispatch/runners/zig-check.js +2 -1
- package/node_modules/pi-lens/dist/clients/effective-config.js +12 -2
- package/node_modules/pi-lens/dist/clients/extension-log.js +2 -0
- package/node_modules/pi-lens/dist/clients/file-role.js +18 -1
- package/node_modules/pi-lens/dist/clients/finding-delivery-gate.js +4 -0
- package/node_modules/pi-lens/dist/clients/formatters.js +54 -188
- package/node_modules/pi-lens/dist/clients/generation-guard.js +4 -0
- package/node_modules/pi-lens/dist/clients/gitleaks-client.js +42 -6
- package/node_modules/pi-lens/dist/clients/govulncheck-client.js +48 -7
- package/node_modules/pi-lens/dist/clients/installer/index.js +196 -52
- package/node_modules/pi-lens/dist/clients/jscpd-client.js +9 -1
- package/node_modules/pi-lens/dist/clients/knip-client.js +9 -2
- package/node_modules/pi-lens/dist/clients/language-profile.js +12 -2
- package/node_modules/pi-lens/dist/clients/latency-logger.js +2 -0
- package/node_modules/pi-lens/dist/clients/lens-config.js +4 -0
- package/node_modules/pi-lens/dist/clients/lsp/config.js +36 -7
- package/node_modules/pi-lens/dist/clients/lsp/diagnostic-binding.js +24 -1
- package/node_modules/pi-lens/dist/clients/lsp/document-drift.js +78 -0
- package/node_modules/pi-lens/dist/clients/lsp/index.js +281 -45
- package/node_modules/pi-lens/dist/clients/lsp/server.js +62 -15
- package/node_modules/pi-lens/dist/clients/lsp/workspace-diagnostics-cache.js +37 -4
- package/node_modules/pi-lens/dist/clients/mcp/analyze.js +3 -0
- package/node_modules/pi-lens/dist/clients/mcp/session.js +2 -0
- package/node_modules/pi-lens/dist/clients/opengrep-client.js +2 -0
- package/node_modules/pi-lens/dist/clients/package-manager.js +2 -1
- package/node_modules/pi-lens/dist/clients/php-cs-fixer-config.js +8 -3
- package/node_modules/pi-lens/dist/clients/pipeline.js +3 -2
- package/node_modules/pi-lens/dist/clients/project-diagnostics/fresh-fetch.js +38 -10
- package/node_modules/pi-lens/dist/clients/project-diagnostics/runner-adapters/gitleaks.js +10 -9
- package/node_modules/pi-lens/dist/clients/project-lens-config.js +3 -0
- package/node_modules/pi-lens/dist/clients/read-guard-logger.js +2 -0
- package/node_modules/pi-lens/dist/clients/review-graph-logger.js +2 -0
- package/node_modules/pi-lens/dist/clients/ruff-client.js +18 -9
- package/node_modules/pi-lens/dist/clients/runtime-agent-end.js +9 -0
- package/node_modules/pi-lens/dist/clients/runtime-coordinator.js +5 -0
- package/node_modules/pi-lens/dist/clients/runtime-session.js +13 -4
- package/node_modules/pi-lens/dist/clients/runtime-tool-result.js +4 -1
- package/node_modules/pi-lens/dist/clients/runtime-turn.js +48 -1
- package/node_modules/pi-lens/dist/clients/security-scan-client.js +2 -2
- package/node_modules/pi-lens/dist/clients/session-event-guard.js +69 -1
- package/node_modules/pi-lens/dist/clients/sg-runner.js +2 -1
- package/node_modules/pi-lens/dist/clients/situational-tool-telemetry.js +108 -0
- package/node_modules/pi-lens/dist/clients/test-runner-client.js +291 -76
- package/node_modules/pi-lens/dist/clients/tool-config.js +199 -0
- package/node_modules/pi-lens/dist/clients/tool-cwd.js +299 -0
- package/node_modules/pi-lens/dist/clients/tool-probe.js +53 -0
- package/node_modules/pi-lens/dist/clients/tool-set-policy.js +42 -0
- package/node_modules/pi-lens/dist/clients/trivy-client.js +9 -1
- package/node_modules/pi-lens/dist/clients/turn-context.js +52 -0
- package/node_modules/pi-lens/dist/clients/widget-state.js +4 -3
- package/node_modules/pi-lens/dist/index.js +21969 -20005
- package/node_modules/pi-lens/dist/mcp/analyze-cli.js +3 -2
- package/node_modules/pi-lens/dist/mcp/server.js +135 -159
- package/node_modules/pi-lens/dist/tools/activate-tools.js +10 -7
- package/node_modules/pi-lens/dist/tools/ast-grep-outline.js +2 -13
- package/node_modules/pi-lens/dist/tools/ast-grep-replace.js +8 -13
- package/node_modules/pi-lens/dist/tools/ast-grep-search.js +83 -39
- package/node_modules/pi-lens/dist/tools/effective-config.js +4 -3
- package/node_modules/pi-lens/dist/tools/lens-diagnostic-mark.js +1 -10
- package/node_modules/pi-lens/dist/tools/lens-diagnostics.js +353 -85
- package/node_modules/pi-lens/dist/tools/lsp-diagnostics.js +21 -110
- package/node_modules/pi-lens/dist/tools/lsp-navigation.js +20 -44
- package/node_modules/pi-lens/dist/tools/module-report.js +5 -9
- package/node_modules/pi-lens/dist/tools/project-report.js +9 -17
- package/node_modules/pi-lens/dist/tools/render-compact.js +280 -1
- package/node_modules/pi-lens/dist/tools/symbol-search.js +2 -2
- package/node_modules/pi-lens/docs/agent-guide.md +3 -4
- package/node_modules/pi-lens/docs/agent-tools.md +46 -22
- package/node_modules/pi-lens/docs/configuration.md +9 -0
- package/node_modules/pi-lens/docs/environment-variables.md +0 -5
- package/node_modules/pi-lens/docs/features.md +5 -5
- package/node_modules/pi-lens/docs/globalconfig.md +11 -1
- package/node_modules/pi-lens/docs/mcp.md +13 -1
- package/node_modules/pi-lens/docs/pi-lens-fixer.md +71 -0
- package/node_modules/pi-lens/docs/pi-lens-investigator.md +15 -0
- package/node_modules/pi-lens/docs/pi-lens-monitor.md +88 -0
- package/node_modules/pi-lens/docs/pi-lens-reviewer.md +35 -0
- package/node_modules/pi-lens/docs/pi-lens-subagent.md +12 -4
- package/node_modules/pi-lens/docs/public-api-stability.md +1 -0
- package/node_modules/pi-lens/docs/real-harness.md +46 -0
- package/node_modules/pi-lens/docs/release-qa-baseline.md +5 -1
- package/node_modules/pi-lens/docs/servercapabilities.md +1 -6
- package/node_modules/pi-lens/docs/settings.md +37 -2
- package/node_modules/pi-lens/docs/tools_improvement2.md +4 -4
- package/node_modules/pi-lens/docs/tree-sitter_rules_catalog.md +1 -1
- package/node_modules/pi-lens/docs/usage.md +13 -3
- package/node_modules/pi-lens/package.json +10 -1
- package/node_modules/pi-lens/rules/tree-sitter-queries/typescript/sql-injection.yml +7 -3
- package/node_modules/pi-lens/skills/pi-lens-ast-grep/SKILL.md +8 -6
- package/node_modules/pi-lens/skills/pi-lens-lsp-navigation/SKILL.md +19 -9
- package/node_modules/pi-web-access/CHANGELOG.md +29 -0
- package/node_modules/pi-web-access/README.md +33 -22
- package/node_modules/pi-web-access/abortable.ts +17 -0
- package/node_modules/pi-web-access/crawl4ai.ts +204 -0
- package/node_modules/pi-web-access/credential-source.ts +1 -0
- package/node_modules/pi-web-access/curator-page.ts +20 -2
- package/node_modules/pi-web-access/curator-run.ts +44 -0
- package/node_modules/pi-web-access/curator-server.ts +3 -1
- package/node_modules/pi-web-access/duckduckgo.ts +1 -1
- package/node_modules/pi-web-access/extract.ts +57 -12
- package/node_modules/pi-web-access/fetch-params.ts +1 -1
- package/node_modules/pi-web-access/gemini-search.ts +9 -5
- package/node_modules/pi-web-access/index.ts +25 -18
- package/node_modules/pi-web-access/openai-search.ts +35 -13
- package/node_modules/pi-web-access/package.json +2 -2
- package/node_modules/pi-web-access/page-query.ts +5 -2
- package/node_modules/pi-web-access/query-rewrite.ts +5 -2
- package/node_modules/pi-web-access/serpapi.ts +220 -0
- package/node_modules/pi-web-access/ssrf-protection.ts +5 -1
- package/node_modules/pi-web-access/storage.ts +10 -0
- package/node_modules/pi-web-access/summary-review.ts +34 -21
- package/node_modules/pi-web-access/utils.ts +6 -4
- package/package.json +11 -5
- package/node_modules/pi-lens/dist/tools/ast-dump.js +0 -103
|
@@ -13,6 +13,7 @@ import * as path from "node:path";
|
|
|
13
13
|
import { createAvailabilityChecker, resolveAvailableOrInstall, } from "./dispatch/runners/utils/runner-helpers.js";
|
|
14
14
|
import { isFileKind } from "./file-kinds.js";
|
|
15
15
|
import { safeSpawnAsync } from "./safe-spawn.js";
|
|
16
|
+
import { resolveToolCwd } from "./tool-cwd.js";
|
|
16
17
|
import { ruffConfigArgs } from "./tool-policy.js";
|
|
17
18
|
// --- Client ---
|
|
18
19
|
const ruffAvailability = createAvailabilityChecker("ruff", ".exe");
|
|
@@ -44,8 +45,11 @@ export class RuffClient {
|
|
|
44
45
|
}
|
|
45
46
|
/**
|
|
46
47
|
* Async auto-fix variant for pipeline use (non-blocking spawn).
|
|
47
|
-
* `cwd` is the dispatch language root
|
|
48
|
-
*
|
|
48
|
+
* `cwd` is the dispatch language root. It is the seam's starting point,
|
|
49
|
+
* not the answer: `resolveToolCwd` walks from the file to the nearest
|
|
50
|
+
* `pyproject.toml`/`ruff.toml`/`.ruff.toml` at or above it, capped inside
|
|
51
|
+
* `cwd` (and at `$HOME` when there is none), so a file in a nested package
|
|
52
|
+
* gets that package's config rather than the workspace-root one.
|
|
49
53
|
*/
|
|
50
54
|
async fixFileAsync(filePath, cwd) {
|
|
51
55
|
if (!(await this.ensureAvailable())) {
|
|
@@ -67,15 +71,20 @@ export class RuffClient {
|
|
|
67
71
|
}
|
|
68
72
|
try {
|
|
69
73
|
const before = await fs.promises.readFile(absolutePath, "utf-8");
|
|
74
|
+
// #2894: the root ruff resolves its config from now comes from the
|
|
75
|
+
// shared seam, not a local `cwd ?? path.dirname(file)`. `tool-policy.ts`
|
|
76
|
+
// requires the lint runner (`ruff.ts`, on `resolveRunnerCwd`) and this
|
|
77
|
+
// autofix path to consume the SAME policy; deriving the root two
|
|
78
|
+
// different ways is how they drift, and the hand-rolled form ignored a
|
|
79
|
+
// nested `pyproject.toml` whenever the caller passed a workspace root.
|
|
80
|
+
const ruffCwd = resolveToolCwd("runner", "ruff", absolutePath, {
|
|
81
|
+
...(cwd !== undefined && { cwd }),
|
|
82
|
+
});
|
|
70
83
|
// Shared config-args seam (#1247): the lint runner consumes the same
|
|
71
84
|
// builder, so `check --fix` can never drift to ruff's default rule
|
|
72
85
|
// set when the project lacks its own config and the package-owned
|
|
73
86
|
// core.toml fallback applies.
|
|
74
|
-
const configArgs = ruffConfigArgs(
|
|
75
|
-
const spawnOpts = {
|
|
76
|
-
timeout: 10000,
|
|
77
|
-
cwd: cwd ?? path.dirname(absolutePath),
|
|
78
|
-
};
|
|
87
|
+
const configArgs = ruffConfigArgs(ruffCwd);
|
|
79
88
|
const pre = await safeSpawnAsync(this.ruffCommand, [
|
|
80
89
|
"check",
|
|
81
90
|
"--output-format",
|
|
@@ -84,12 +93,12 @@ export class RuffClient {
|
|
|
84
93
|
"py310",
|
|
85
94
|
...configArgs,
|
|
86
95
|
absolutePath,
|
|
87
|
-
],
|
|
96
|
+
], { timeout: 10000, cwd: ruffCwd });
|
|
88
97
|
const beforeDiags = pre.stdout?.trim()
|
|
89
98
|
? this.parseOutput(pre.stdout, absolutePath)
|
|
90
99
|
: [];
|
|
91
100
|
const fixableCount = beforeDiags.filter((d) => d.fixable).length;
|
|
92
|
-
const fix = await safeSpawnAsync(this.ruffCommand, ["check", "--fix", ...configArgs, absolutePath], { timeout: 15000, cwd:
|
|
101
|
+
const fix = await safeSpawnAsync(this.ruffCommand, ["check", "--fix", ...configArgs, absolutePath], { timeout: 15000, cwd: ruffCwd });
|
|
93
102
|
if (fix.error) {
|
|
94
103
|
return {
|
|
95
104
|
success: false,
|
|
@@ -39,6 +39,7 @@ export async function handleAgentEnd({ ctxCwd, getFlag, getFlagSource, notify, d
|
|
|
39
39
|
// to fall back to "claim as orphaned" (see claimDeferredFormatFiles).
|
|
40
40
|
const { claimed, staleClaimed, deferredToOwner, droppedOrphans } = runtime.claimDeferredMutations(currentSessionId, Date.now(), staleAfterMs, ctxCwd ?? runtime.projectRoot);
|
|
41
41
|
const records = [...claimed, ...staleClaimed];
|
|
42
|
+
const scheduledTurnId = records[0]?.queuedTurnId;
|
|
42
43
|
const requeuedKinds = new Set();
|
|
43
44
|
// S2d (gap 4, #1432 review): the aggregate `agent_end_deferred_mutation_drain`
|
|
44
45
|
// row only carried a coalesced `requeuedKinds` set with no reason, per-call
|
|
@@ -65,6 +66,7 @@ export async function handleAgentEnd({ ctxCwd, getFlag, getFlagSource, notify, d
|
|
|
65
66
|
toolName: "agent_end",
|
|
66
67
|
filePath: ctxCwd ?? runtime.projectRoot,
|
|
67
68
|
phase: "agent_end_deferred_mutation_requeue",
|
|
69
|
+
turnId: pending[0]?.queuedTurnId,
|
|
68
70
|
durationMs: 0,
|
|
69
71
|
metadata: {
|
|
70
72
|
reason,
|
|
@@ -170,6 +172,7 @@ export async function handleAgentEnd({ ctxCwd, getFlag, getFlagSource, notify, d
|
|
|
170
172
|
toolName: "agent_end",
|
|
171
173
|
filePath: ctxCwd ?? runtime.projectRoot,
|
|
172
174
|
phase: "agent_end_deferred_format_stale_claim",
|
|
175
|
+
turnId: scheduledTurnId,
|
|
173
176
|
durationMs: 0,
|
|
174
177
|
metadata: {
|
|
175
178
|
fileCount: staleClaimed.length,
|
|
@@ -307,6 +310,7 @@ export async function handleAgentEnd({ ctxCwd, getFlag, getFlagSource, notify, d
|
|
|
307
310
|
toolName: "agent_end",
|
|
308
311
|
filePath: ctxCwd ?? runtime.projectRoot,
|
|
309
312
|
phase: "agent_end_deferred_format_start",
|
|
313
|
+
turnId: scheduledTurnId,
|
|
310
314
|
durationMs: 0,
|
|
311
315
|
metadata: {
|
|
312
316
|
fileCount: records.length,
|
|
@@ -491,6 +495,7 @@ export async function handleAgentEnd({ ctxCwd, getFlag, getFlagSource, notify, d
|
|
|
491
495
|
toolName: "agent_end",
|
|
492
496
|
filePath,
|
|
493
497
|
phase: "deferred_format_file",
|
|
498
|
+
turnId: record.queuedTurnId,
|
|
494
499
|
durationMs: Date.now() - fileStart,
|
|
495
500
|
metadata: {
|
|
496
501
|
changed: result.formatChanged,
|
|
@@ -635,6 +640,7 @@ export async function handleAgentEnd({ ctxCwd, getFlag, getFlagSource, notify, d
|
|
|
635
640
|
toolName: "agent_end",
|
|
636
641
|
filePath: ctxCwd ?? runtime.projectRoot,
|
|
637
642
|
phase: "actionable_warnings_autofix",
|
|
643
|
+
turnId: scheduledTurnId,
|
|
638
644
|
durationMs: Date.now() - fixStart,
|
|
639
645
|
metadata: {
|
|
640
646
|
considered: fixSummary.considered,
|
|
@@ -657,6 +663,7 @@ export async function handleAgentEnd({ ctxCwd, getFlag, getFlagSource, notify, d
|
|
|
657
663
|
toolName: "agent_end",
|
|
658
664
|
filePath: ctxCwd ?? runtime.projectRoot,
|
|
659
665
|
phase: "agent_end_deferred_mutation_drain",
|
|
666
|
+
turnId: scheduledTurnId,
|
|
660
667
|
durationMs: Date.now() - startedAt,
|
|
661
668
|
metadata: {
|
|
662
669
|
autofixRecords: autofixRecords.length,
|
|
@@ -671,6 +678,7 @@ export async function handleAgentEnd({ ctxCwd, getFlag, getFlagSource, notify, d
|
|
|
671
678
|
filePath: ctxCwd ?? runtime.projectRoot,
|
|
672
679
|
durationMs: Date.now() - startedAt,
|
|
673
680
|
result: "deferred_format_complete",
|
|
681
|
+
turnId: scheduledTurnId,
|
|
674
682
|
metadata: {
|
|
675
683
|
queued: summary.queued,
|
|
676
684
|
formatted: summary.formatted,
|
|
@@ -685,6 +693,7 @@ export async function handleAgentEnd({ ctxCwd, getFlag, getFlagSource, notify, d
|
|
|
685
693
|
toolName: "agent_end",
|
|
686
694
|
filePath: ctxCwd ?? runtime.projectRoot,
|
|
687
695
|
phase: "agent_end_deferred_format_done",
|
|
696
|
+
turnId: scheduledTurnId,
|
|
688
697
|
durationMs: Date.now() - startedAt,
|
|
689
698
|
metadata: {
|
|
690
699
|
formatted: summary.formatted,
|
|
@@ -11,6 +11,7 @@ import { ReadGuard } from "./read-guard.js";
|
|
|
11
11
|
import { RUNTIME_CONFIG } from "./runtime-config.js";
|
|
12
12
|
import { TurnSummaryCollector } from "./turn-summary.js";
|
|
13
13
|
import { deriveProviderFromModelId } from "./model-provider.js";
|
|
14
|
+
import { beginTurnContext, setTurnContextSession } from "./turn-context.js";
|
|
14
15
|
/**
|
|
15
16
|
* Bounded receipt ring capacity (defect shape 9: bound the axis that grows —
|
|
16
17
|
* per-mutation receipts are unbounded across a long session). Oldest receipts
|
|
@@ -259,6 +260,7 @@ export class RuntimeCoordinator {
|
|
|
259
260
|
// by resetForSession().
|
|
260
261
|
this._turnStartProjectSeq = this._projectSeq;
|
|
261
262
|
this._turnIndex += 1;
|
|
263
|
+
beginTurnContext(this._telemetrySessionId);
|
|
262
264
|
this._writeIndex = 0;
|
|
263
265
|
this._reportedThisTurn.clear();
|
|
264
266
|
this._writtenThisTurn.clear();
|
|
@@ -362,6 +364,7 @@ export class RuntimeCoordinator {
|
|
|
362
364
|
setTelemetryIdentity(identity) {
|
|
363
365
|
if (identity.sessionId && identity.sessionId.trim()) {
|
|
364
366
|
this._telemetrySessionId = identity.sessionId.trim();
|
|
367
|
+
setTurnContextSession(this._telemetrySessionId);
|
|
365
368
|
}
|
|
366
369
|
const model = identity.model?.trim();
|
|
367
370
|
const provider = identity.provider?.trim();
|
|
@@ -1029,6 +1032,7 @@ export class RuntimeCoordinator {
|
|
|
1029
1032
|
existing.toolNames.add(toolName);
|
|
1030
1033
|
existing.kinds.add(kind);
|
|
1031
1034
|
existing.queuedTurnIndex = this._turnIndex;
|
|
1035
|
+
existing.queuedTurnId = `${this._telemetrySessionId}:${this._turnIndex}`;
|
|
1032
1036
|
existing.ownerSessionId = ownerSessionId;
|
|
1033
1037
|
existing.originCwd = resolvedOriginCwd;
|
|
1034
1038
|
return addedKind;
|
|
@@ -1042,6 +1046,7 @@ export class RuntimeCoordinator {
|
|
|
1042
1046
|
toolNames: new Set([toolName]),
|
|
1043
1047
|
kinds: new Set([kind]),
|
|
1044
1048
|
queuedTurnIndex: this._turnIndex,
|
|
1049
|
+
queuedTurnId: `${this._telemetrySessionId}:${this._turnIndex}`,
|
|
1045
1050
|
ownerSessionId,
|
|
1046
1051
|
originCwd: resolvedOriginCwd,
|
|
1047
1052
|
});
|
|
@@ -42,6 +42,7 @@ import { resetSafeSpawnWindowsCommandCache } from "./safe-spawn.js";
|
|
|
42
42
|
import { resetAnalyzerBootstrapSessionState, } from "./bootstrap.js";
|
|
43
43
|
import { getSlowFsVerdict, isSlowFs, slowFsDegradationNotice, } from "./slow-fs.js";
|
|
44
44
|
import { countRecentSmells, formatSmellsSessionStartLine, resetSmellsSessionState, } from "./smells-rollup.js";
|
|
45
|
+
import { resetSituationalToolTelemetry } from "./situational-tool-telemetry.js";
|
|
45
46
|
import { findNearestProjectRoot, getStartupScanMaxEntries, isStartupScanVerdictFresh, resolveStartupScanContext, } from "./startup-scan.js";
|
|
46
47
|
import { getSubagentIdentity, isSubagentSession, subagentLightModeNotice, } from "./subagent-mode.js";
|
|
47
48
|
import { TrivyClient } from "./trivy-client.js";
|
|
@@ -1361,17 +1362,16 @@ function scheduleDeferredToolProbesWithClients(deps, languageProfile, startupDef
|
|
|
1361
1362
|
/**
|
|
1362
1363
|
* Session-start orientation prepended as a context message (gated by the
|
|
1363
1364
|
* context-injection toggle). Deliberately lean: it names the high-value tools
|
|
1364
|
-
* and the
|
|
1365
|
+
* and the distinction between cached reporting and active verification
|
|
1365
1366
|
* — per-tool argument detail lives in each tool's own registered description, so
|
|
1366
1367
|
* re-documenting it here would just pay the tokens twice every session.
|
|
1367
1368
|
*/
|
|
1368
1369
|
export const SESSION_START_GUIDANCE = [
|
|
1369
1370
|
"📌 pi-lens active — automated checks run on every edit/write; blocking errors (including pre-existing) show inline and must be fixed.\n" +
|
|
1370
1371
|
"Key tools (see each tool's own description for args):\n" +
|
|
1371
|
-
"• lens_diagnostics — session
|
|
1372
|
+
"• lens_diagnostics — source=session reads cache; empty cache ≠ clean; use source=lsp scope=paths for changed files with absent or stale findings.\n" +
|
|
1372
1373
|
"• symbol_search → module_report → read_symbol/read_enclosing — ranked identifier search, then navigable outline/callback handles + exact body reads; cheaper than reading a whole file before editing.\n" +
|
|
1373
|
-
"•
|
|
1374
|
-
"• Situational (activate via pi_lens_activate_tools): lsp_navigation, ast_grep_search, ast_grep_replace, ast_grep_dump.",
|
|
1374
|
+
"• Situational (activate via pi_lens_activate_tools): lsp_navigation, ast_grep_search, ast_grep_replace. Use ast_grep_search with dump=true to inspect AST nodes.",
|
|
1375
1375
|
];
|
|
1376
1376
|
export async function handleSessionStart(deps) {
|
|
1377
1377
|
resetDegradationLedger();
|
|
@@ -1814,6 +1814,15 @@ export async function handleSessionStart(deps) {
|
|
|
1814
1814
|
// #1123 item 3: a fresh session can re-report smells that a prior session
|
|
1815
1815
|
// already surfaced once (see `checkSmellsAndNoteOnce`'s once-per-session gate).
|
|
1816
1816
|
resetSmellsSessionState();
|
|
1817
|
+
// #2800 item 8: the situational-tool dead-weight observation (which tools
|
|
1818
|
+
// this session never activated or called) is session-scoped, so its sets
|
|
1819
|
+
// and once-latch re-arm here beside the other registered resets. Both hosts
|
|
1820
|
+
// open the session's row through startSituationalToolTelemetrySession()
|
|
1821
|
+
// BEFORE this handler runs, and a repeated MCP session_start refresh
|
|
1822
|
+
// legitimately re-runs this handler — so the reset only acts when no
|
|
1823
|
+
// telemetry session is open; clearing a live session here would wipe the
|
|
1824
|
+
// calls recorded before the refresh.
|
|
1825
|
+
resetSituationalToolTelemetry();
|
|
1817
1826
|
// #1782: re-arm the workspace-diagnostics cache session clock. Entries
|
|
1818
1827
|
// written before this instant assert findings from a session that is over,
|
|
1819
1828
|
// so they must revalidate before they can be served as current again.
|
|
@@ -21,7 +21,7 @@ import { replayThroughMutationBridge, storedLineHashesFor, } from "./observed-mu
|
|
|
21
21
|
import { getAmbientAbortSignal } from "./safe-spawn.js";
|
|
22
22
|
import { resolveToolCallCorrelationId } from "./tool-event.js";
|
|
23
23
|
import { boundedIndexesForCount, createReadGuardEditBatchSummary, getReadGuardCorrelationId, logReadGuardEvent, } from "./read-guard-logger.js";
|
|
24
|
-
import { notifyExternalFileChange } from "./lsp/index.js";
|
|
24
|
+
import { notifyExternalFileChange, resyncGitChangedFiles, } from "./lsp/index.js";
|
|
25
25
|
import { runPipeline } from "./pipeline.js";
|
|
26
26
|
import { renderPostAutofixNotice, } from "./post-autofix-notice.js";
|
|
27
27
|
import { syncGitGuardRecord } from "./git-guard.js";
|
|
@@ -897,6 +897,9 @@ export async function handleToolResult(deps) {
|
|
|
897
897
|
durationMs: Date.now() - started,
|
|
898
898
|
result: `changed:${opaquePaths.length}`,
|
|
899
899
|
});
|
|
900
|
+
void resyncGitChangedFiles(opaquePaths).catch((err) => {
|
|
901
|
+
dbg(`git-change LSP resync failed: ${err}`);
|
|
902
|
+
});
|
|
900
903
|
}
|
|
901
904
|
}
|
|
902
905
|
// wp iterates opaquePaths VERBATIM (already normalizeMapKey keys), so the
|
|
@@ -13,6 +13,7 @@ import { getKnipIgnorePatterns } from "./file-utils.js";
|
|
|
13
13
|
import { formatCacheAgeLabel } from "./finding-delivery-gate.js";
|
|
14
14
|
import { getFullScanWallClockMs, isWorkspaceSweepActive, runWhenWorkspaceSweepIdle, SWEEP_IDLE_SAFETY_MARGIN_MS, } from "./lsp/workspace-sweep-hold.js";
|
|
15
15
|
import { isTestRoleCollateral } from "./collateral-test-role.js";
|
|
16
|
+
import { classifyAndFilterFindings, } from "./gitleaks-client.js";
|
|
16
17
|
import { dedupeSecretFindings, fromAstGrepWarnings, fromGitleaks, fromTrivySecrets, isSecretWarning, secretLocationKey, } from "./secret-findings.js";
|
|
17
18
|
import { deadCodeIssueKey, deadCodeIssues, formatDeadCodeDelta, stableFindingKey, } from "./dead-code-client.js";
|
|
18
19
|
import { logDeadCodeScan } from "./dead-code-logger.js";
|
|
@@ -49,6 +50,8 @@ import { emitBounded } from "./bounded-telemetry.js";
|
|
|
49
50
|
import { degradeDemotedFindingBody, formatDeliveryCapNote, formatRetirementNote, } from "./demoted-finding-render.js";
|
|
50
51
|
import { STALE_LINE_MARKER } from "./stale-marker.js";
|
|
51
52
|
import { getActiveSessionId } from "./session-lifecycle.js";
|
|
53
|
+
import { bounded } from "./deadline-utils.js";
|
|
54
|
+
import { HOOK_WALL_BUDGET_MS } from "./hook-budgets.js";
|
|
52
55
|
import { drainRenderedDependencyDriftFilePaths, getWidgetBlockingFilesForSweep, incrementWidgetDependencyDriftDelivery, markWidgetFileBlockersStale, recordRunner, retireWidgetDependencyDriftBlockers, } from "./widget-state.js";
|
|
53
56
|
/** Maximum detailed notify-stall coverage-gap rows emitted in one turn. */
|
|
54
57
|
const LATE_AUX_COVERAGE_GAP_DETAIL_CAP_PER_TURN = 20;
|
|
@@ -1483,6 +1486,26 @@ export async function handleTurnEnd(deps) {
|
|
|
1483
1486
|
// provenance) — a blocker, since credentials need rotation before merge.
|
|
1484
1487
|
const gitleaksData = cacheManager.readCache("gitleaks", cwd)?.data;
|
|
1485
1488
|
const trivySecretsData = trivyCacheEntry?.data;
|
|
1489
|
+
// Gitleaks deliberately scans gitignored local files and nested repositories
|
|
1490
|
+
// so an explicit security audit can still inspect them. The adapter is the
|
|
1491
|
+
// source of truth for whether a finding belongs in a blocking delivery lane;
|
|
1492
|
+
// filter here before freshness handling so demoted findings cannot leak into
|
|
1493
|
+
// either the blocker or stale-secret turn context.
|
|
1494
|
+
const boundedClassification = await bounded(classifyAndFilterFindings(gitleaksData?.findings ?? [], cwd), {
|
|
1495
|
+
ms: HOOK_WALL_BUDGET_MS.turn_end,
|
|
1496
|
+
signal: deps.signal,
|
|
1497
|
+
hook: "turn_end",
|
|
1498
|
+
label: "classifyAndFilterFindings",
|
|
1499
|
+
});
|
|
1500
|
+
const classifiedGitleaksFindings = boundedClassification ?? gitleaksData?.findings ?? [];
|
|
1501
|
+
if (boundedClassification === undefined) {
|
|
1502
|
+
recordDegradationOnce({
|
|
1503
|
+
kind: "gitleaks_classification_timeout",
|
|
1504
|
+
subject: cwd,
|
|
1505
|
+
reason: "gitleaks classification exceeded the turn_end budget; retained raw findings to fail open",
|
|
1506
|
+
});
|
|
1507
|
+
}
|
|
1508
|
+
const blockingGitleaksFindings = classifiedGitleaksFindings.filter((finding) => gitleaksFindingToProjectDiagnostic(cwd, finding).semantic === "blocking");
|
|
1486
1509
|
// #1461 slice 1 (#1460): the gitleaks cache is TTL-only, so a finding for a
|
|
1487
1510
|
// file deleted after the scan is still served as a 🔴 blocker for the rest
|
|
1488
1511
|
// of the 30-minute window — the live case, and 119 of 126 findings in
|
|
@@ -1497,7 +1520,7 @@ export async function handleTurnEnd(deps) {
|
|
|
1497
1520
|
// would let any edit — malicious or accidental — mute a real secret.
|
|
1498
1521
|
const gitleaksGate = gateFindingsByPathFreshness({
|
|
1499
1522
|
store: "gitleaks",
|
|
1500
|
-
findings:
|
|
1523
|
+
findings: blockingGitleaksFindings,
|
|
1501
1524
|
cwd,
|
|
1502
1525
|
scannedAt: gitleaksData?.scannedAt,
|
|
1503
1526
|
citedPath: (finding) => finding.file,
|
|
@@ -2882,6 +2905,7 @@ export async function handleTurnEnd(deps) {
|
|
|
2882
2905
|
let lateAuxCoverageGapDropCount = 0;
|
|
2883
2906
|
const lateAuxStuckPairs = [];
|
|
2884
2907
|
if (drainedPairs.length > 0) {
|
|
2908
|
+
const lateObserverDeadline = Date.now() + HOOK_WALL_BUDGET_MS.turn_end;
|
|
2885
2909
|
const byFile = new Map();
|
|
2886
2910
|
for (const pair of drainedPairs) {
|
|
2887
2911
|
const list = byFile.get(pair.filePath);
|
|
@@ -2993,6 +3017,19 @@ export async function handleTurnEnd(deps) {
|
|
|
2993
3017
|
}
|
|
2994
3018
|
continue;
|
|
2995
3019
|
}
|
|
3020
|
+
// A demoted auxiliary still answers through this late path. Feed the
|
|
3021
|
+
// publication-minus-mark interval into the re-promotion streak. This is
|
|
3022
|
+
// delivery latency observed by the drain, not the scanner's total scan
|
|
3023
|
+
// latency. Cache priming is
|
|
3024
|
+
// below the freshness gate so a changed file cannot resurrect stale data.
|
|
3025
|
+
if (typeof service.observeLateAuxiliaryAnswer === "function") {
|
|
3026
|
+
await bounded(service.observeLateAuxiliaryAnswer(lateAuxPath, pair.serverId, cachedEntry.publishedAt - pair.markedAtMs), {
|
|
3027
|
+
ms: Math.max(1, lateObserverDeadline - Date.now()),
|
|
3028
|
+
signal: deps.signal /* late observer */,
|
|
3029
|
+
hook: "turn_end",
|
|
3030
|
+
label: "observeLateAuxiliaryAnswer",
|
|
3031
|
+
});
|
|
3032
|
+
}
|
|
2996
3033
|
if (rawDiags.length === 0) {
|
|
2997
3034
|
lateAuxCleanConfirmed += 1;
|
|
2998
3035
|
continue;
|
|
@@ -3045,6 +3082,16 @@ export async function handleTurnEnd(deps) {
|
|
|
3045
3082
|
}
|
|
3046
3083
|
continue;
|
|
3047
3084
|
}
|
|
3085
|
+
// #2810 round 4: this drain does NOT write the hash-bound
|
|
3086
|
+
// last-known record. The prime it used to call could only fire when
|
|
3087
|
+
// a record already existed at the pair's hash — which requires a
|
|
3088
|
+
// FULLY covered touch of those exact bytes, the one case where the
|
|
3089
|
+
// scanner's findings are already in the record — so it was a no-op
|
|
3090
|
+
// in the demoted steady state it was added for, and a #570/#1470
|
|
3091
|
+
// hazard everywhere else (an auxiliary-only array replacing the
|
|
3092
|
+
// merged one). Late findings reach the agent as the gated advisory
|
|
3093
|
+
// below; the turn-end hash-guarded fast path stays cold for a file
|
|
3094
|
+
// whose touch was partial, which is exactly what #1470 requires.
|
|
3048
3095
|
const lines = gate.live.map((f) => ` ${displayLateAuxPath}:${f.line}:${f.column} [${f.rule}] ${f.message}`);
|
|
3049
3096
|
lateAuxDelivered += gate.live.length;
|
|
3050
3097
|
lateAuxAnswered += 1;
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
*/
|
|
15
15
|
import * as path from "node:path";
|
|
16
16
|
import { createSubsystemLogger } from "./extension-log.js";
|
|
17
|
-
import {
|
|
17
|
+
import { probeToolAsync } from "./tool-probe.js";
|
|
18
18
|
import { classifyProbeFailure, createAvailabilityLatch, describeInstallAttempt, describeProbeEvidence, logAvailabilityDecision, startHostStallSampler, } from "./dispatch/runners/utils/availability-policy.js";
|
|
19
19
|
import { createAvailabilityProbeFlight } from "./availability-probe-flight.js";
|
|
20
20
|
import { createSingleFlight } from "./single-flight.js";
|
|
@@ -132,7 +132,7 @@ export class SecurityScanClient {
|
|
|
132
132
|
const { findManagedToolBinary } = await import("./installer/index.js");
|
|
133
133
|
const managed = await findManagedToolBinary(this.toolName);
|
|
134
134
|
return {
|
|
135
|
-
probe: await
|
|
135
|
+
probe: await probeToolAsync(managed ?? this.toolName, versionArgs, {
|
|
136
136
|
timeout: 5000,
|
|
137
137
|
}),
|
|
138
138
|
binaryPath: managed ?? null,
|
|
@@ -64,7 +64,22 @@
|
|
|
64
64
|
* wrapper narrows nothing else: a handler bug must stay as loud as it was.
|
|
65
65
|
*/
|
|
66
66
|
import { emitBounded } from "./bounded-telemetry.js";
|
|
67
|
+
import { recordDegradationOnce } from "./degradation-ledger.js";
|
|
67
68
|
import { probeCtxActive } from "./session-lifecycle.js";
|
|
69
|
+
import { runWithTurnContext } from "./turn-context.js";
|
|
70
|
+
function stableSessionId(ctx) {
|
|
71
|
+
try {
|
|
72
|
+
return ctx?.sessionManager?.getSessionId?.();
|
|
73
|
+
}
|
|
74
|
+
catch {
|
|
75
|
+
recordDegradationOnce({
|
|
76
|
+
kind: "turn-context-identity-fallback",
|
|
77
|
+
subject: "session-event-guard",
|
|
78
|
+
reason: "stable session identity resolution failed; using detached turn context",
|
|
79
|
+
});
|
|
80
|
+
return undefined;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
68
83
|
/**
|
|
69
84
|
* The pi SDK invalidates a captured `pi`/command ctx after a session
|
|
70
85
|
* replacement or reload; every later `pi.*` call then throws with this
|
|
@@ -99,6 +114,59 @@ function recordStaleSkip(eventName, detectedAt) {
|
|
|
99
114
|
reason: `${eventName} skipped: extension ctx is stale (${detectedAt})`,
|
|
100
115
|
});
|
|
101
116
|
}
|
|
117
|
+
/**
|
|
118
|
+
* The one policy for "a pi hook handler threw and production swallows it"
|
|
119
|
+
* (#2884).
|
|
120
|
+
*
|
|
121
|
+
* Nine catch sites in `index.ts` absorb a crashed handler so a pi-lens bug
|
|
122
|
+
* can never take down the host's session. Each of them used to write only
|
|
123
|
+
* `dbg(...)`, and `dbg` writes nothing under vitest — so a crashed handler was
|
|
124
|
+
* indistinguishable from a completed one. #2859 is what that costs: fourteen
|
|
125
|
+
* `session_start` awaits in `tests/index-integration.test.ts` rejected into
|
|
126
|
+
* one of those catches, every assertion after them was vacuous, and the file
|
|
127
|
+
* stayed green. #2866 closed the hole for `session_start` with an inline
|
|
128
|
+
* `if (process.env.VITEST) throw`; this function is that guard folded into one
|
|
129
|
+
* place so the remaining eight cannot drift from it.
|
|
130
|
+
*
|
|
131
|
+
* Two things happen on every crash, in this order:
|
|
132
|
+
*
|
|
133
|
+
* 1. **The production record.** One bounded `hook-handler-crash` row per
|
|
134
|
+
* handler per session (`recordDegradationOnce`), so a handler that crashes
|
|
135
|
+
* on every turn leaves one durable row and an exact ledger tally instead of
|
|
136
|
+
* a silent no-op. It is written BEFORE the rethrow, so a test can assert the
|
|
137
|
+
* production observability the runner path would otherwise hide.
|
|
138
|
+
* 2. **The runner rethrow.** Under vitest the crash is rethrown, so the test
|
|
139
|
+
* whose `await` caused it fails with the real error instead of resolving.
|
|
140
|
+
* In production nothing is rethrown and the caller's swallow stands.
|
|
141
|
+
*
|
|
142
|
+
* Deliberately `process.env.VITEST` and not `isTestMode()` (kept from #2859):
|
|
143
|
+
* the question is whether a TEST is awaiting this handler, and the #2815 R7
|
|
144
|
+
* case runs under vitest with `PI_LENS_TEST_MODE=0`.
|
|
145
|
+
*
|
|
146
|
+
* The stale-ctx class is NOT this function's business. Callers that classify
|
|
147
|
+
* it (`session_start`, `agent_end`, `turn_end`, the `agent_settled` drain)
|
|
148
|
+
* rethrow `isStaleExtensionCtxError` themselves BEFORE calling in, so a benign
|
|
149
|
+
* session swap keeps its own single record and never lands here as a crash.
|
|
150
|
+
* The quiet-window site is intentionally the exception to the test-runner
|
|
151
|
+
* rethrow: it is fire-and-forget, so a rethrow would be an unhandled rejection
|
|
152
|
+
* that can terminate the pi host before its caller can observe the failure.
|
|
153
|
+
*/
|
|
154
|
+
export function surfaceHandlerCrash(handler, err, options = {}) {
|
|
155
|
+
try {
|
|
156
|
+
options.dbg?.(`${handler} crashed: ${err}`);
|
|
157
|
+
options.dbg?.(`${handler} crash stack: ${err?.stack}`);
|
|
158
|
+
}
|
|
159
|
+
catch {
|
|
160
|
+
// A debug sink must never decide whether a crash is recorded.
|
|
161
|
+
}
|
|
162
|
+
recordDegradationOnce({
|
|
163
|
+
kind: "hook-handler-crash",
|
|
164
|
+
subject: handler,
|
|
165
|
+
reason: `${handler} handler crashed and was swallowed: ${String(err)}`,
|
|
166
|
+
});
|
|
167
|
+
if (process.env.VITEST && options.rethrow !== false)
|
|
168
|
+
throw err;
|
|
169
|
+
}
|
|
102
170
|
function isThenable(value) {
|
|
103
171
|
return (typeof value?.then === "function");
|
|
104
172
|
}
|
|
@@ -131,7 +199,7 @@ function guardSessionEvent(eventName, handler, onStaleResult, options) {
|
|
|
131
199
|
// below.
|
|
132
200
|
return skip(event, "pre-dispatch");
|
|
133
201
|
try {
|
|
134
|
-
const result = handler(event, ctx);
|
|
202
|
+
const result = runWithTurnContext(stableSessionId(ctx), () => handler(event, ctx));
|
|
135
203
|
if (isThenable(result)) {
|
|
136
204
|
// Recover the rejection in place. The host awaits the same promise
|
|
137
205
|
// it would have awaited anyway; it just resolves instead.
|
|
@@ -12,6 +12,7 @@ import { getSgCommand, resolveManagedToolClient, } from "./dispatch/runners/util
|
|
|
12
12
|
import { getProjectIgnoreGlobs } from "./file-utils.js";
|
|
13
13
|
import { findGlobalBinary } from "./package-manager.js";
|
|
14
14
|
import { safeSpawnAsync } from "./safe-spawn.js";
|
|
15
|
+
import { probeToolAsync } from "./tool-probe.js";
|
|
15
16
|
import { truncatedByOutputCap } from "./spawn-output-cap.js";
|
|
16
17
|
import { createSingleFlight } from "./single-flight.js";
|
|
17
18
|
import { classifyProbeFailure, describeInstallAttempt, createAvailabilityLatch, logAvailabilityDecision, startHostStallSampler, } from "./dispatch/runners/utils/availability-policy.js";
|
|
@@ -459,7 +460,7 @@ export class SgRunner {
|
|
|
459
460
|
let result;
|
|
460
461
|
let hostStallMs;
|
|
461
462
|
try {
|
|
462
|
-
result = await
|
|
463
|
+
result = await probeToolAsync(cmd, [...argsPrefix, "--version"], {
|
|
463
464
|
timeout: PROBE_TIMEOUT_MS,
|
|
464
465
|
});
|
|
465
466
|
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { logExtension } from "./extension-log.js";
|
|
2
|
+
import { TOOL_REGISTRY } from "./tool-config.js";
|
|
3
|
+
const situationalTools = TOOL_REGISTRY.filter((tool) => "situational" in tool && tool.situational === true).map((tool) => tool.name);
|
|
4
|
+
const situationalToolSet = new Set(situationalTools);
|
|
5
|
+
const activated = new Set();
|
|
6
|
+
const called = new Set();
|
|
7
|
+
let sessionStarted = false;
|
|
8
|
+
let emitted = false;
|
|
9
|
+
// MCP-only connection-terminal latch: once the connection's row is recorded,
|
|
10
|
+
// repeated initialize/tool-call starts must not reopen the session.
|
|
11
|
+
let connectionEnded = false;
|
|
12
|
+
// Which host owns the open session; only MCP may arm connectionEnded. Pi state
|
|
13
|
+
// belongs to the conversation and survives in-process session rebuilds.
|
|
14
|
+
let sessionHost = "mcp";
|
|
15
|
+
function observe(set, name) {
|
|
16
|
+
if (situationalToolSet.has(name)) {
|
|
17
|
+
set.add(name);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
function clearObservations() {
|
|
21
|
+
activated.clear();
|
|
22
|
+
called.clear();
|
|
23
|
+
}
|
|
24
|
+
export function observeSituationalToolActivation(names) {
|
|
25
|
+
for (const name of names)
|
|
26
|
+
observe(activated, name);
|
|
27
|
+
}
|
|
28
|
+
export function observeSituationalToolCall(name) {
|
|
29
|
+
observe(called, name);
|
|
30
|
+
}
|
|
31
|
+
export function resetSituationalToolTelemetry() {
|
|
32
|
+
// A live telemetry session owns its observation sets: both hosts open the
|
|
33
|
+
// session (which resets the sets) BEFORE handleSessionStart runs, and a
|
|
34
|
+
// repeated MCP session_start refresh legitimately re-runs that handler —
|
|
35
|
+
// clearing here would wipe the calls recorded before the refresh. This
|
|
36
|
+
// registered reset therefore only acts when no session is open.
|
|
37
|
+
if (sessionStarted)
|
|
38
|
+
return;
|
|
39
|
+
clearObservations();
|
|
40
|
+
emitted = false;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Open the telemetry session for one host.
|
|
44
|
+
*
|
|
45
|
+
* Pi owns one observation set per session file. A shutdown with a
|
|
46
|
+
* `targetSessionFile` emits the conversation that is ending before the host
|
|
47
|
+
* opens the replacement. Reload keeps the same file, so its factory rebuild
|
|
48
|
+
* does not split the row. A process restart opens an empty set and recovers nothing:
|
|
49
|
+
* pi-lens's own activation memory (`rememberedLazyTools` in `index.ts`) is
|
|
50
|
+
* empty in a new process, so the restore deactivates every situational tool,
|
|
51
|
+
* and the host's restored active set is evidence of REGISTRATION, not of
|
|
52
|
+
* model activation (#2866 review F1). MCP remains connection-scoped.
|
|
53
|
+
*/
|
|
54
|
+
export function startSituationalToolTelemetrySession(host) {
|
|
55
|
+
if (host === "mcp") {
|
|
56
|
+
if (connectionEnded)
|
|
57
|
+
return;
|
|
58
|
+
if (sessionStarted)
|
|
59
|
+
return;
|
|
60
|
+
sessionHost = "mcp";
|
|
61
|
+
clearObservations();
|
|
62
|
+
emitted = false;
|
|
63
|
+
sessionStarted = true;
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
if (sessionStarted) {
|
|
67
|
+
sessionHost = "pi";
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
sessionHost = "pi";
|
|
71
|
+
clearObservations();
|
|
72
|
+
emitted = false;
|
|
73
|
+
sessionStarted = true;
|
|
74
|
+
}
|
|
75
|
+
/** Emit the one session-end row and make repeated shutdown calls harmless. */
|
|
76
|
+
export function endSituationalToolTelemetry() {
|
|
77
|
+
if (!sessionStarted)
|
|
78
|
+
return;
|
|
79
|
+
emitSituationalDeadWeight();
|
|
80
|
+
if (sessionHost === "mcp")
|
|
81
|
+
connectionEnded = true;
|
|
82
|
+
clearObservations();
|
|
83
|
+
emitted = false;
|
|
84
|
+
sessionStarted = false;
|
|
85
|
+
}
|
|
86
|
+
export function emitSituationalDeadWeight() {
|
|
87
|
+
if (emitted)
|
|
88
|
+
return;
|
|
89
|
+
emitted = true;
|
|
90
|
+
const used = new Set([...activated, ...called]);
|
|
91
|
+
logExtension({
|
|
92
|
+
subsystem: "tools",
|
|
93
|
+
level: "debug",
|
|
94
|
+
message: "situational tool dead weight",
|
|
95
|
+
metadata: {
|
|
96
|
+
tools: situationalTools.filter((name) => !used.has(name)),
|
|
97
|
+
},
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
/** Test-only view of the module's state, for the #1635 session-state registry probe. */
|
|
101
|
+
export function _getSituationalToolTelemetryStateForTests() {
|
|
102
|
+
return {
|
|
103
|
+
activated: activated.size,
|
|
104
|
+
called: called.size,
|
|
105
|
+
emitted,
|
|
106
|
+
sessionStarted,
|
|
107
|
+
};
|
|
108
|
+
}
|