pi-lens 4.1.0 → 4.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +185 -0
- package/README.md +106 -0
- package/dist/clients/actionable-warnings-logger.js +2 -1
- package/dist/clients/actionable-warnings.js +14 -5
- package/dist/clients/advisory-provenance.js +48 -18
- package/dist/clients/ast-grep-client.js +1 -3
- package/dist/clients/atomic-write-staging.js +16 -4
- package/dist/clients/bash-file-access.js +99 -6
- package/dist/clients/biome-client.js +1 -3
- package/dist/clients/blocker-freshness.js +21 -8
- package/dist/clients/blocker-past-eof.js +16 -6
- package/dist/clients/bounded-cache.js +24 -8
- package/dist/clients/bounded-pid-file-lock.js +4 -3
- package/dist/clients/bounded-telemetry.js +14 -0
- package/dist/clients/cache-manager.js +6 -7
- package/dist/clients/cache-observability.js +782 -110
- package/dist/clients/call-graph.js +238 -63
- package/dist/clients/codebase-model.js +13 -5
- package/dist/clients/complexity-client.js +1 -3
- package/dist/clients/dead-code-client.js +19 -8
- package/dist/clients/dead-code-logger.js +1 -2
- package/dist/clients/deadline-utils.js +3 -1
- package/dist/clients/debug-heap.js +8 -1
- package/dist/clients/degradation-ledger.js +22 -1
- package/dist/clients/demoted-finding-render.js +155 -0
- package/dist/clients/dependency-checker.js +11 -7
- package/dist/clients/deps/pi-tui.js +8 -2
- package/dist/clients/deps/typebox.js +8 -3
- package/dist/clients/diagnostic-dispositions.js +39 -2
- package/dist/clients/diagnostic-line-freshness.js +5 -1
- package/dist/clients/dispatch/auxiliary-lsp.js +2 -3
- package/dist/clients/dispatch/dispatcher.js +13 -1
- package/dist/clients/dispatch/facts/function-facts.js +3 -1
- package/dist/clients/dispatch/facts/import-facts.js +2 -1
- package/dist/clients/dispatch/facts/tree-sitter-facts.js +1 -1
- package/dist/clients/dispatch/integration.js +9 -4
- package/dist/clients/dispatch/rules/error-swallowing.js +3 -1
- package/dist/clients/dispatch/rules/pass-through-wrappers.js +6 -2
- package/dist/clients/dispatch/rules/unsafe-boundary.js +31 -7
- package/dist/clients/dispatch/runners/actionlint.js +5 -2
- package/dist/clients/dispatch/runners/biome-check.js +10 -14
- package/dist/clients/dispatch/runners/cpp-check.js +5 -1
- package/dist/clients/dispatch/runners/detekt.js +9 -6
- package/dist/clients/dispatch/runners/dotnet-build.js +1 -1
- package/dist/clients/dispatch/runners/elixir-check.js +3 -1
- package/dist/clients/dispatch/runners/eslint.js +25 -6
- package/dist/clients/dispatch/runners/fish-indent.js +1 -1
- package/dist/clients/dispatch/runners/gleam-check.js +1 -1
- package/dist/clients/dispatch/runners/golangci-lint.js +3 -1
- package/dist/clients/dispatch/runners/hadolint.js +22 -15
- package/dist/clients/dispatch/runners/helm-lint.js +4 -1
- package/dist/clients/dispatch/runners/htmlhint.js +33 -16
- package/dist/clients/dispatch/runners/javac.js +1 -1
- package/dist/clients/dispatch/runners/ktlint.js +1 -1
- package/dist/clients/dispatch/runners/lsp.js +1 -1
- package/dist/clients/dispatch/runners/markdownlint.js +34 -12
- package/dist/clients/dispatch/runners/mypy.js +15 -17
- package/dist/clients/dispatch/runners/oxlint.js +231 -15
- package/dist/clients/dispatch/runners/php-lint.js +1 -1
- package/dist/clients/dispatch/runners/phpstan.js +52 -15
- package/dist/clients/dispatch/runners/psscriptanalyzer.js +10 -6
- package/dist/clients/dispatch/runners/pyright.js +23 -14
- package/dist/clients/dispatch/runners/rust-clippy.js +1 -1
- package/dist/clients/dispatch/runners/shellcheck.js +7 -9
- package/dist/clients/dispatch/runners/shfmt.js +5 -2
- package/dist/clients/dispatch/runners/spellcheck.js +10 -15
- package/dist/clients/dispatch/runners/sqlfluff.js +25 -17
- package/dist/clients/dispatch/runners/stylelint.js +40 -20
- package/dist/clients/dispatch/runners/swiftlint.js +8 -8
- package/dist/clients/dispatch/runners/taplo.js +110 -23
- package/dist/clients/dispatch/runners/terragrunt.js +13 -8
- package/dist/clients/dispatch/runners/tflint.js +25 -15
- package/dist/clients/dispatch/runners/tree-sitter.js +17 -3
- package/dist/clients/dispatch/runners/utils/candidate-probe.js +7 -1
- package/dist/clients/dispatch/runners/utils/runner-helpers.js +24 -1
- package/dist/clients/dispatch/runners/utils/tool-failure.js +152 -1
- package/dist/clients/dispatch/runners/vale.js +16 -14
- package/dist/clients/dispatch/runners/yaml-rule-parser.js +1 -3
- package/dist/clients/dispatch/runners/yamllint.js +13 -16
- package/dist/clients/dispatch/runners/zig-check.js +1 -1
- package/dist/clients/dispatch/suppress-writer.js +2 -1
- package/dist/clients/dispatch/types.js +6 -1
- package/dist/clients/dispatch/utils/lsp-diagnostics.js +6 -2
- package/dist/clients/extension-log.js +5 -3
- package/dist/clients/feature-hints.js +8 -1
- package/dist/clients/file-kinds.js +34 -129
- package/dist/clients/file-utils.js +17 -1
- package/dist/clients/finding-delivery-gate.js +79 -2
- package/dist/clients/formatters.js +144 -27
- package/dist/clients/freshness.js +38 -0
- package/dist/clients/git-guard.js +43 -13
- package/dist/clients/go-client.js +1 -3
- package/dist/clients/grammar-source.js +1 -1
- package/dist/clients/gzip-stage-write.js +30 -4
- package/dist/clients/host-edit-normalize.js +1 -3
- package/dist/clients/install-diagnostics.js +4 -2
- package/dist/clients/installer/index.js +86 -77
- package/dist/clients/instance-reaper.js +5 -2
- package/dist/clients/instance-registry.js +4 -2
- package/dist/clients/jscpd-client.js +11 -3
- package/dist/clients/knip-client.js +29 -15
- package/dist/clients/language-profile.js +11 -3
- package/dist/clients/latency-logger.js +13 -2
- package/dist/clients/lens-config.js +1 -1
- package/dist/clients/lens-map.js +8 -3
- package/dist/clients/live-bus-emitter.js +1 -1
- package/dist/clients/lsp/cascade-tier.js +27 -2
- package/dist/clients/lsp/client.js +144 -5
- package/dist/clients/lsp/config.js +4 -6
- package/dist/clients/lsp/edits.js +119 -32
- package/dist/clients/lsp/index.js +245 -41
- package/dist/clients/lsp/inferred-project.js +2 -1
- package/dist/clients/lsp/jvm-runtime.js +4 -1
- package/dist/clients/lsp/launch.js +11 -3
- package/dist/clients/lsp/pending-aux-coverage.js +152 -0
- package/dist/clients/lsp/server.js +19 -7
- package/dist/clients/lsp/tsserver-sync.js +10 -2
- package/dist/clients/lsp/workspace-diagnostics-cache.js +9 -4
- package/dist/clients/lsp-document-symbols.js +1 -3
- package/dist/clients/lsp-mutation.js +10 -20
- package/dist/clients/mcp/host-shim.js +1 -1
- package/dist/clients/memory-sampler.js +73 -7
- package/dist/clients/metrics-client.js +1 -3
- package/dist/clients/middle-man-analysis.js +8 -3
- package/dist/clients/module-report-lsp.js +7 -1
- package/dist/clients/module-report.js +28 -18
- package/dist/clients/ndjson-logger.js +117 -28
- package/dist/clients/opaque-mutation-scan.js +220 -0
- package/dist/clients/package-manager.js +6 -1
- package/dist/clients/pipeline.js +57 -9
- package/dist/clients/project-conventions.js +2 -8
- package/dist/clients/project-diagnostics/cache.js +10 -7
- package/dist/clients/project-diagnostics/runner-adapters/call-graph-impact.js +2 -1
- package/dist/clients/project-diagnostics/runner-adapters/gitleaks.js +3 -1
- package/dist/clients/project-diagnostics/runner-adapters/runner-findings.js +13 -9
- package/dist/clients/project-diagnostics/runner-adapters/trivy.js +3 -1
- package/dist/clients/project-diagnostics/scanner.js +22 -10
- package/dist/clients/project-lens-config.js +12 -5
- package/dist/clients/project-report.js +62 -27
- package/dist/clients/project-snapshot-fingerprint.js +48 -0
- package/dist/clients/project-snapshot-persist-worker.js +15 -1
- package/dist/clients/project-snapshot.js +472 -66
- package/dist/clients/read-expansion.js +12 -2
- package/dist/clients/read-guard-logger.js +12 -6
- package/dist/clients/read-guard-tool-lines.js +5 -2
- package/dist/clients/read-guard.js +88 -19
- package/dist/clients/resource-sampler.js +1 -1
- package/dist/clients/reverse-deps.js +6 -5
- package/dist/clients/review-graph/builder.js +236 -48
- package/dist/clients/review-graph/git-identity.js +8 -2
- package/dist/clients/review-graph/import-resolvers.js +10 -1
- package/dist/clients/review-graph/query.js +2 -1
- package/dist/clients/review-graph/revision-drift.js +21 -0
- package/dist/clients/review-graph/tsconfig-paths.js +4 -2
- package/dist/clients/review-graph/workspace-modules.js +8 -5
- package/dist/clients/ruff-client.js +6 -5
- package/dist/clients/runtime-agent-end.js +63 -31
- package/dist/clients/runtime-context.js +12 -4
- package/dist/clients/runtime-coordinator.js +127 -4
- package/dist/clients/runtime-session.js +55 -0
- package/dist/clients/runtime-tool-call.js +59 -3
- package/dist/clients/runtime-tool-result.js +148 -34
- package/dist/clients/runtime-turn.js +194 -8
- package/dist/clients/rust-client.js +1 -3
- package/dist/clients/safe-spawn.js +150 -11
- package/dist/clients/security-scan-client.js +1 -3
- package/dist/clients/session-event-guard.js +184 -0
- package/dist/clients/session-summary.js +3 -1
- package/dist/clients/sg-runner.js +1 -3
- package/dist/clients/source-filter.js +16 -8
- package/dist/clients/source-walker.js +1 -1
- package/dist/clients/spawn-timeout-cooldown.js +96 -0
- package/dist/clients/startup-scan.js +9 -3
- package/dist/clients/tool-definition.js +7 -0
- package/dist/clients/tool-policy.js +26 -11
- package/dist/clients/tool-render.js +2 -1
- package/dist/clients/tree-sitter-client.js +10 -4
- package/dist/clients/tree-sitter-logger.js +2 -1
- package/dist/clients/tree-sitter-symbol-extractor.js +12 -5
- package/dist/clients/warm-attach.js +12 -5
- package/dist/clients/widget-state.js +27 -11
- package/dist/clients/word-index.js +3 -1
- package/dist/clients/workspace-topology.js +8 -2
- package/dist/clients/zizmor-config.js +9 -1
- package/dist/i18n.js +13 -5
- package/dist/index.js +15811 -12810
- package/dist/mcp/server.js +3 -1
- package/dist/tools/ast-grep-replace.js +68 -12
- package/dist/tools/lens-diagnostic-mark.js +21 -7
- package/dist/tools/lens-diagnostics.js +174 -38
- package/dist/tools/lsp-diagnostics.js +24 -7
- package/dist/tools/lsp-navigation.js +3 -1
- package/dist/tools/module-report.js +1 -1
- package/dist/tools/project-report.js +1 -1
- package/dist/tools/symbol-search.js +5 -2
- package/docs/durable-store-audit-1202.md +1 -1
- package/docs/servercapabilities.md +1 -1
- package/package.json +160 -143
- package/rules/typos/_typos.toml +2 -2
- package/scripts/analyze-pi-lens-logs.mjs +31 -13
- package/scripts/download-grammars.js +9 -3
- package/scripts/install-selftest.mjs +99 -11
- package/scripts/lib/host-provided-deps.mjs +75 -0
- package/scripts/lib/warm-loader-cache.mjs +285 -0
- package/scripts/rpc-load-check.mjs +30 -9
- package/scripts/warm-loader-cache.mjs +233 -0
package/CHANGELOG.md
CHANGED
|
@@ -16,6 +16,191 @@ All notable changes to pi-lens will be documented in this file.
|
|
|
16
16
|
|
|
17
17
|
### Security
|
|
18
18
|
|
|
19
|
+
## [4.1.2] - 2026-08-24
|
|
20
|
+
|
|
21
|
+
### Added
|
|
22
|
+
|
|
23
|
+
- **Prompt-cache misses now say why, and mixed injections split by source (refs #1071)** — a `cache_usage` record carries `interTurnGapMs` and a `cacheMissCause` verdict of `ttl-expired`, `prefix-broke`, `partial-eviction`, or `unknown`, so the dominant cache cost is readable from `latency.log` instead of reconstructed by hand-joining timestamps. The gap is measured to request time, so a slow response is not mistaken for idle time; `gapBasis` names which endpoint produced the value. A `cache_context` record splits a mixed injection payload by contributing source, with per-source message count, characters, bytes, and an estimated token count. Token figures use a documented four-chars-per-token estimate and are never presented as provider-measured. The TTL threshold defaults to 60s and is overridable via `PI_LENS_PROVIDER_CACHE_TTL_MS`.
|
|
24
|
+
|
|
25
|
+
- **Quick-mode session_start now logs which steps it skipped (closes #1911)** — quick mode silently skipped slow tool probes, language profiling, preinstall, startup scans, and the error-debt baseline, with no record either way. `session_start` now emits one bounded `session_start_skipped_steps` latency record naming the skipped step set, so a reader can tell "quick mode correctly skipped these" from "the probes silently never ran".
|
|
26
|
+
|
|
27
|
+
- **Runner parsers are now held to real binary output (refs #1937)** — a parser written from documentation can be wrong in a way no hand-authored test sees, because the test asserts the same imagined shape. Captured real-bytes fixtures now live in `tests/fixtures/runner-output/`, each with a machine-generated provenance header naming the tool, version, and exact argv; a replay suite feeds those bytes through the runner, fails when the parser finds nothing, and fails when the runner's invocation drifts from the argv the fixture recorded. The sweep found four live instances: taplo spawned a `--output=json` flag taplo rejects, stylelint read stdout while stylelint 16+ reports on stderr, phpstan read the error COUNT as if it were the error array and discarded its file-independent findings entirely, and sqlfluff inserted `--dialect` between `--format` and its value so no unconfigured SQL project was ever linted. A scheduled `parser-smoke` lane now runs the tier-1 tools' real binaries over a planted violation, with a pass floor so a run that installed nothing cannot report green.
|
|
28
|
+
|
|
29
|
+
- **A runner that reads nothing out of a failing tool's output now leaves a record (closes #1948)** — when a dispatch runner's tool exits nonzero, prints output, and the runner's parser extracts zero diagnostics, the degradation ledger gets a bounded `runner-parsed-nothing` row naming the tool, the exit status, the output length, and the first output line. Until now that case was byte-for-byte identical to a clean file in the worklog, which is how five parser bugs (vale, taplo, stylelint, phpstan, sqlfluff) reported clean files for months. A genuinely clean run records nothing, so the ledger does not fill up with one row per save. The gate lives in one shared helper, `parseToolRun`, that fourteen runners now share, and a sweep test keeps a new runner from silently opting out through any of the older spawn-outcome primitives.
|
|
30
|
+
|
|
31
|
+
- **Mechanical lint batch: actionlint, PR-title lint, markdownlint, OSV scan (refs #1844)** — a new `lint.yml` workflow dogfoods actionlint against every workflow file, validates that every PR title carries a conventional prefix and an issue reference **in the title itself** (`scripts/check-pr-title.mjs` — a reference living only in the PR body no longer counts, since it never reaches the merge-commit subject), and lints Markdown docs with `markdownlint-cli2` under a repo-tuned config. A separate `osv-scan.yml` runs an advisory weekly `osv-scanner` sweep plus a scan on lockfile-touching PRs, writing results to the job summary.
|
|
32
|
+
|
|
33
|
+
- **Vale prose lint with a vendored Google style base (refs #1844)** — `.vale.ini` at the repo root scopes Vale to `docs/`, `AGENTS.md`, `CLAUDE.md`, `README.md`, `CONTRIBUTING.md`, `.claude/agents/*.md`, and `.changelog/*.md`, using the Google style package vendored under `.vale/styles/Google/` (no `vale sync` at CI time) plus a `pi-lens/` style with three house rules: no `please` in instructions, an em-dash-density check, and a long-sentence check. A new advisory `vale` job in `lint.yml` runs it on every PR. Because the in-product `vale` runner (`clients/dispatch/runners/vale.ts`) activates on `.vale.ini` presence, dogfood sessions now surface Vale findings on doc edits too.
|
|
34
|
+
|
|
35
|
+
- **Husky-managed local git hooks (closes #1804)** — `npm install` now wires
|
|
36
|
+
a pre-commit hook (changelog fragment validation + `npm run lint`) and a
|
|
37
|
+
pre-push hook (build + a capped, full-path-resolved targeted `vitest`
|
|
38
|
+
selection for changed files, never the full suite; degrades to build-only
|
|
39
|
+
past 25 matched test files or a 2-minute shared test-lock wait). Both are
|
|
40
|
+
skippable with `PI_LENS_SKIP_HOOKS` set to any non-empty value, which
|
|
41
|
+
agents and CI should set; humans leave hooks on. Hook install itself is
|
|
42
|
+
skipped for CI and production/consumer installs, and never fails
|
|
43
|
+
`npm install` on error.
|
|
44
|
+
|
|
45
|
+
- **latency.log now says whether the LSP pool reused a client or spawned one (closes #1934)** — `lsp_client_selected` fired 5601 times in a 20.8h dogfood window carrying only `{serverId, candidateCount}`, so nothing in the log said whether the pool served a warm client or paid for a language-server spawn. This is the most expensive cache in pi-lens: a miss costs a process. The only estimate was a cross-record inference against `lsp_launch_candidate_success`, which counts something else, so a regression that halved pool reuse was invisible. The record now carries an `outcome` field with one of three values on the same record and the same denominator. `warm-reuse` means the pool served an already-connected client. `cold-spawn` means this selection waited on a spawn, whether it started that spawn or joined one already in flight, so a session-start burst where many files wait on the same spawn reads as several cold spawns and deflates the apparent reuse rate for that window. `spawn-failure` means a spawn ran and failed. Reuse rate is `warm-reuse / (warm-reuse + cold-spawn)`. A spawn failure is deliberately distinct from a clean decline: no root, a breaker already open, host trust refused, or a binary absent while installs are disabled. Declines emit no outcome record and keep their existing ones, so `lsp_client_unavailable` and the `outcome` field can both describe the same event and are counted separately. `getWarmClientForFile`, the warm-only lookup the cascade quiet window and read expansion call per file, previously returned `undefined` and emitted nothing; it now emits a bounded `lsp_warm_client_missing` record naming the candidate server and root, rising-edge per candidate set with the exact count in the degradation ledger.
|
|
46
|
+
|
|
47
|
+
- **Dogfood sessions on pi-lens's own repo now activate the oxlint, spellcheck, yamllint, and taplo runner lanes (refs #1844)** — four new root config files (`.oxlintrc.json`, `typos.toml`, `.yamllint`, `taplo.toml`) opt pi-lens's own repository into linters it already ships for every other project. `.oxlintrc.json` pins `oxlint` as a new exact-version devDependency (1.79.0, matching the runner's tested fixture), makes oxlint this repo's sole preferred JS/TS lint runner (dropping the `biome-check-json` fallback — that IS the dogfood intent), and excludes the fixture directories that carry no config override of their own; oxlint's nested-config discovery still finds the two fixture directories that deliberately escalate severity for the runner's own smoke tests, so those stay as they are. The oxlint runner also now tells a config-excluded file (`number_of_files: 0`) apart from a genuinely clean one, reporting `skipped` instead of a false `succeeded`. `typos.toml` allowlists evidenced false positives (a routine "mis-" prefix, HashiCorp's brand abbreviation, and similar); its presence enables typos-lsp's blocking opt-in, but findings stay advisory in practice since typos-lsp's own default diagnostic severity is warning. `.yamllint` scopes yamllint to hand-authored YAML (GitHub Actions workflows, top-level config), downgrades line-length to a warning rather than raising its threshold, and defuses the `on:` truthy false positive. `taplo.toml` activates both the TOML linter and the `taplo fmt` autofix formatter, and this PR runs that formatter once so activation doesn't start the lane red. Markdown linting was already live via the pre-existing `.markdownlint-cli2.jsonc` (since #1917) — confirmed with a real `markdownlint-cli2` run, no new config needed.
|
|
48
|
+
|
|
49
|
+
- **Memory-sample observability upgrades (refs #1999)** — The periodic
|
|
50
|
+
`memory_sample` latency.log line gains three fields. `peakWorkingSetBytes`
|
|
51
|
+
records the OS high-water working set from `process.resourceUsage().maxRSS`,
|
|
52
|
+
so an idle-moment rss sample can be told apart from true growth (on Windows
|
|
53
|
+
libuv backs both counters with one `GetProcessMemoryInfo()` call: rss reads
|
|
54
|
+
current `WorkingSetSize`, maxRSS reads `PeakWorkingSetSize`). A rising-edge
|
|
55
|
+
cadence tightens sampling from every 10 turns to every turn while heapUsed
|
|
56
|
+
grows more than 20% between samples, reverting once growth stabilizes; the
|
|
57
|
+
state resets at each primary session start. Each sample now also carries
|
|
58
|
+
session age, session start time, and turn count, so growth-vs-age curves are
|
|
59
|
+
plottable from logs alone. The `/lens-health` memory line shows peak WS when
|
|
60
|
+
known.
|
|
61
|
+
|
|
62
|
+
- **Opaque-write recovery for bash commands (refs #2000 phase 2)** — commands the path extractor does not recognize (python/node/perl/PowerShell internal writes) now get a bounded pre/post stat diff of the project source universe. Recovered files are attributed to the read guard as agent-authored and dispatched through the mutation seam, with explicit coverage-unknown telemetry instead of silent gaps.
|
|
63
|
+
|
|
64
|
+
- **Collect-later delivery for slow auxiliary LSP servers (closes #2001, refs #2002)** —
|
|
65
|
+
When an auxiliary scanner such as opengrep misses its aux-grace window,
|
|
66
|
+
pi-lens now marks the file and server pair in a bounded pending store
|
|
67
|
+
instead of dropping the scanner's eventual findings. The next turn end
|
|
68
|
+
probes the auxiliary's client cache through a read-only seam,
|
|
69
|
+
freshness-gates the result against the mark timestamp, and delivers
|
|
70
|
+
survivors as a `Late auxiliary diagnostics` advisory. A cited file that was
|
|
71
|
+
edited or deleted since the mark drops its findings, and both drop arms are
|
|
72
|
+
counted in the new `late_auxiliary_findings` latency record.
|
|
73
|
+
|
|
74
|
+
- **Freshness kernel: one comparator, one verdict type for staleness gates (closes #1739)** — `clients/freshness.ts` owns the mtime-vs-reference comparison and shared drift tolerance that six stores had independently reimplemented (three copies carried the identical #1710/#1711 tolerance defect). `freshnessFromMtime` returns an explicit verdict (`fresh` / `stale: modified-after-reference` / `indeterminate: no-mtime-evidence`) so each caller keeps its own no-evidence policy while sharing the comparison. A registered-or-fail sweep fails on any new out-of-kernel mtime-vs-reference comparison in `clients/`.
|
|
75
|
+
|
|
76
|
+
- **Shared fault-injection test kit (closes #1838)** — `tests/support/fault-injection.ts` promotes the bespoke fault probes reviewers kept rebuilding by hand into four one-call primitives: `spawnWedgedChild` (a real child whose stdin pipe is genuinely full — the #1811 fixture generalized, with its fail-fast-instead-of-hang trap asserted against), `delayInside` (deterministic completion delay inside any mocked async seam), `fireResetAt` (fire a lifecycle hook from inside a seam's implementation at a chosen call — the #1746-R2-F1 shape), and `starveBudget` plus `gatedPromise` (the tiny-budget starvation repro). Every primitive carries its own fidelity test in `fault-injection.test.ts`, so a neutered primitive goes red in CI instead of silently weakening every consumer.
|
|
77
|
+
|
|
78
|
+
- **Adversarial garbage battery for every CLI lint runner (closes #1839)** — Twenty runners now meet a 12-case battery of hostile outputs (truncated JSON, usage prose on the findings stream, unknown severities, hostile numbers) under format-blind invariants: never crash, never emit malformed diagnostics, never report clean on a nonzero exit carrying bytes. The first pass found 79 violations of that last invariant across 14 runners — all fixed by consolidating their identical tails onto one shared `finishParsedRun` seam. Also fixes htmlhint's `--rules` flag being fed JSON (the tool wants a ruleid list), which left zero rules enabled so every file read clean.
|
|
79
|
+
|
|
80
|
+
### Changed
|
|
81
|
+
|
|
82
|
+
- **pi-lens formats its own TypeScript with oxfmt (refs #1844)** — The
|
|
83
|
+
repository now carries an `.oxfmtrc.json` and an `oxfmt` devDependency, and
|
|
84
|
+
an advisory `oxfmt --check` job runs in CI. Because `hasOxfmtConfig` gates
|
|
85
|
+
the oxfmt formatter on exactly that config file, a pi-lens session opened on
|
|
86
|
+
this repository now dispatches oxfmt as the format runner for edited
|
|
87
|
+
TypeScript and JavaScript files.
|
|
88
|
+
|
|
89
|
+
- **Opaque-write recovery goes git-first (refs #2000)** — inside a git worktree the pre side records only a timestamp and `git status --porcelain` plus an mtime window answers what changed, with no file-universe cap: recovery now works on any repo size, including large monorepos where the stat-walk previously degraded every command to coverage-unknown. The bounded stat-diff path remains for small non-git trees.
|
|
90
|
+
|
|
91
|
+
- **ONE mutation seam: `RuntimeCoordinator.recordProjectMutation` (refs #2000 phase 1)** — the triplicated bump+change-log pairing (runtime-tool-result, runtime-agent-end, lsp-mutation) is consolidated onto one seam that bumps the seq store, appends a bounded attributed receipt ring (`getMutationsSince`, cap 512 with a surfaced dropped-count), and appends the durable change-log entry. Consumers derive touched-files answers from one store instead of three hand-copied pairings; phase 2's opaque-write recovery feeds the same seam.
|
|
92
|
+
|
|
93
|
+
### Fixed
|
|
94
|
+
|
|
95
|
+
- **Search credit ignores pipe truncation (closes #1908)** — `grep -C2 pattern
|
|
96
|
+
file | head -1` no longer credits context lines the pipe cut off. The
|
|
97
|
+
read-guard now detects a truncating pipe tail (`head`, `tail`, `sed q`)
|
|
98
|
+
downstream of a line-numbered grep and falls back to match-line-only
|
|
99
|
+
credit, so a later edit to the uncredited context still requires a read.
|
|
100
|
+
|
|
101
|
+
- **Cascade tier-3 registry now resets at session_start (closes #1910)** — the outstanding-touch registry and its sweep-scoped expired/evicted counters (`clients/lsp/cascade-tier.ts`) used to survive a session replacement, so a new session inherited the prior session's outstanding touches and a stray eviction or expiry landed on the next session's first reconcile gauge. `handleSessionStart` now clears both, primary-only, same as every other per-session latch in that reset block.
|
|
102
|
+
|
|
103
|
+
- **Read-guard eviction telemetry is now always on (closes #1913)** — the
|
|
104
|
+
first time a file's read-guard record cap trims in a session, it now emits
|
|
105
|
+
a `read_cap_trimmed` log line (file, evicted count, credit-vs-genuine
|
|
106
|
+
split, raw read count) regardless of `PI_LENS_READ_GUARD_VERBOSE`, so a
|
|
107
|
+
live eviction regression is visible by default instead of only under
|
|
108
|
+
verbose logging. Later trims on the same file keep updating the running
|
|
109
|
+
totals (queryable via `ReadGuard.getTrimStats`, and via the degradation
|
|
110
|
+
ledger's own tally) without flooding the log on every subsequent trim.
|
|
111
|
+
|
|
112
|
+
- **agent_settled no longer crashes with extension_error when the session is replaced mid-run** —
|
|
113
|
+
stale extension ctx reads are guarded and the ambient abort signal is
|
|
114
|
+
cleared (closes #1924; thanks @Pluto-Yt).
|
|
115
|
+
|
|
116
|
+
- **A session event on a replaced session no longer reports a pi-lens handler error (closes #1925)** — pi invalidates a captured extension ctx on `newSession`/`fork`/`switchSession`/`reload`, and an event already queued when that happens still reaches pi-lens carrying the dead ctx. `tool_result`, `turn_start`, `agent_end`, and `turn_end` each read a ctx property before any guard, so the SDK's `assertActive()` error was caught by `ExtensionRunner.emit` and reported as an extension error against pi-lens. That report named the wrong cause, said nothing about which handler was affected, and counted nothing. All five session-event registrations, including `agent_settled` (#1924), now go through one wrapper in `clients/session-event-guard.ts`: it probes the ctx once before dispatch, still classifies a stale error that arrives mid-handler, and records every skip in the degradation ledger under `extension-ctx-stale` plus a bounded `session_event_stale_ctx_skip` row in `latency.log`, keyed by event name. A new registered-or-fail sweep reds any future `pi.on` or `pi.on?.` registration that is neither wrapped nor given a stated reason.
|
|
117
|
+
|
|
118
|
+
- **Git installs start about 750ms faster (refs #1926)** — pi supplies `typebox` and `@earendil-works/pi-tui` from its own runtime, but pi-lens declared both as runtime dependencies. A `git:` install therefore vendored a private second copy of each, and Node evaluated that whole extra module graph every time the extension loaded. Both are now optional peer dependencies, so nothing vendors them. The `PI_TIMING` module import for the dogfood git install drops from 941ms to 147-196ms, and the extensions block drops from 2165ms to 1449ms. One trade-off, stated plainly: because the bare specifiers no longer resolve on disk, pi loads the entry through jiti's transform path instead of a native import, so the FIRST start after an install, an update, or a jiti cache eviction is slower, around 5.6 to 8.2 seconds on the 4MB bundle. Every start after that is the fast one.
|
|
119
|
+
|
|
120
|
+
- **Updating pi-lens no longer costs a five-second first session (refs #1926)** — pi loads the extension through jiti, which transforms the ~4MB `dist/index.js` bundle and caches the result. Every `git:` install or update produced a new bundle, so the next interactive session paid that transform: 4847ms of `module import`, against a 138ms steady state. The `prepare` chain now runs the transform itself, in `scripts/warm-loader-cache.mjs`, writing the same cache entry pi reads. The first session after an update is warm. The step runs last, is best-effort, and never fails an install; set `PI_LENS_SKIP_WARM_CACHE` to skip it. Each run appends one line to `~/.pi-lens/install.log`.
|
|
121
|
+
|
|
122
|
+
- **A session swap that starves `session_start` or `context` is now counted, not misreported as a pi-lens crash (closes #1929)** — both handlers survived a dead extension ctx already, but each logged it as `session_start crashed: …` or `context event error: …` and recorded nothing, so a replacement that kept starving them was invisible in aggregate. Both now run through the shared stale-ctx wrapper and leave one bounded `extension-ctx-stale` record keyed to the event name. `context` uses a new value-returning wrapper variant that states its stale-path answer explicitly: `undefined`, pi's "this extension contributed nothing", so the host keeps its own message list rather than receiving a half-built injection. Seven of twelve `pi.on` registrations are now wrapped, up from five.
|
|
123
|
+
|
|
124
|
+
- **Diagnostics scans stop logging a phantom ast-grep cache-stats record (closes #1935)** — `#1715` already grows the tree-sitter parse cache to span a project scan's file count, so the diagnostics-scan path does not run at the 50-entry interactive default; that part of `#1935` was resolved before this fix landed. What was still broken: every scan logged a second `cache_stats` record under scope `project_diagnostics_ast_grep_scan`, always all-zero, because ast-grep-napi parses through its own native engine and never touches the WASM tree cache that record claimed to measure. That vacuous record is now removed instead of wired to a cache it structurally cannot use; its one real value — ast-grep's own duration and file count, often a scan's most expensive phase — now rides an `astGrep` sub-field on the surviving `project_diagnostics_scan` record instead of disappearing. Added a memory-safety test pinning `treeCacheTotalBytes` at the existing 500-entry scan ceiling (`TREE_CACHE_SCAN_CAPACITY_CEILING`) using realistic, varying per-file sizes: worst case is 6-18MB of resident source bytes at that ceiling, and `TreeCache` enforces no per-entry byte cap of its own, only the entry count. A regression test pins the scanner to never emit the dead ast-grep scope.
|
|
125
|
+
|
|
126
|
+
- **`cache_context` no longer logs an unusable `unknown` prefix pair on nearly every record (closes #1938)** — `prefixObservation` and `firstMessageChange` were computed from a hash of the pre-injection prefix capped at 64 messages / 2,048 characters. Any transcript past that cap set `prefixHashTruncated`, which forced both fields to `"unknown"`. Every real session outgrows the cap within the first few turns, so the pair reported `unknown` on 97.2% and 7.6% of records respectively — cost without signal. This PR removes the whole family that existed only to support that pair: `prefixObservation`, `prefixObservationUnknown`, `prefixBaseline`, `firstMessageChanged`, `firstMessageChange`, `firstMessageHashTruncated`, `beforeFirstMessageHash`, `afterFirstMessageHash`, `beforePrefixHash`, `afterPrefixHash`, `prefixHashTruncated`, `prefixMessageCountTruncated`, `prefixContentHashTruncated` — 13 fields in all. `cache_prefix_break` already tracks first-message stability with an unbounded hash and never truncates; it remains the source of truth for cache-prefix breaks.
|
|
127
|
+
|
|
128
|
+
- **Review-graph Tier-3 build never grew the tree cache past the 50-entry default (closes #1941)** —
|
|
129
|
+
`builder.ts`'s full-project rebuild parses every non-jsts file through the
|
|
130
|
+
shared `TreeSitterClient`'s parse-tree cache but never called
|
|
131
|
+
`ensureTreeCacheCapacity`, the #1715 fix already wired into the
|
|
132
|
+
diagnostics scanner. A project with more than 50 non-jsts files evicted
|
|
133
|
+
and re-parsed files past the 50th on every Tier-3 build. The build now
|
|
134
|
+
grows the cache to its actual per-parse working set before extraction
|
|
135
|
+
starts — the full file list on a cold build, or the checkpoint's remaining
|
|
136
|
+
files on a resumed build, since resumed files are reused from the
|
|
137
|
+
checkpoint graph and never re-parsed.
|
|
138
|
+
|
|
139
|
+
- **A demoted blocker no longer shouts STOP, and it stops re-serving forever (closes #1944)** — the past-EOF gate moved a blocker whose file had shrunk past the cited lines into the advisory channel, but the advisory embedded the blocker body verbatim, so the agent still read "🔴 STOP — 11 issue(s) must be fixed" with line numbers the file no longer had. Nothing retired the record either, so it re-served on every turn end for the rest of the session (measured live at 80+ minutes). A demoted body now drops the STOP banner and the "must be fixed" imperative, renders a dead coordinate as `L<n> (line no longer exists)`, and — when no re-run could ever confirm it — is delivered once and then retired, with the suppression recorded in the degradation ledger under `demoted-finding-retired`. `lens_diagnostics mode=delta` also stops printing 🔴 on a row whose coordinate it just replaced with the stale marker.
|
|
140
|
+
|
|
141
|
+
- **The oxlint runner now surfaces warning-severity findings instead of discarding them (closes #1947)** — oxlint exits 0 whenever nothing at error severity was found, and warning is oxlint's own default severity. The runner treated any exit 0 as "no findings" and returned early, so a real capture of oxlint on an unused variable — a full JSON report, exit 0 — was thrown away. The runner now parses stdout unconditionally and decides on the parsed diagnostic count instead of the exit code: zero diagnostics is still a clean `succeeded`/`none`, one or more is `failed` with `semantic: "warning"` (or `"blocking"` when a diagnostic is error severity), matching the mapping the runner already used for the nonzero-exit case. A captured real-bytes fixture (`tests/fixtures/runner-output/oxlint/warning-exit-zero.captured.json`, oxlint 1.79.0, exit 0) pins the behavior.
|
|
142
|
+
|
|
143
|
+
- **Merge-train warden's `update-branch` kick 403s under branch protection (#1959)** — the workflow's job permissions granted `pull-requests: write` but not `contents: write`. The PUT `pulls/N/update-branch` endpoint has GitHub create a merge commit on the PR branch, which needs write access to repository contents, not just to the pull request object; the inline comment attributing it to `pull-requests: write` was wrong. The warden also now tells apart the two reasons update-branch can 403: a fork-owned head PR records a distinct benign outcome (`update-branch-forbidden-fork`, logged, not a run failure), while an own-branch PR still fails the run loudly, since that case is a real permissions bug like this one.
|
|
144
|
+
|
|
145
|
+
- **The blind review-graph read no longer drops a valid snapshot on a HEAD-only move (closes #1961)** — `getCachedReviewGraph` verified the persisted snapshot's git stamp against the current HEAD, so a plain `git commit` made every reader see "graph: cold" until something rebuilt in-process. Measured stamp lifetime across four workspaces was 737s to 1562s (median), and two of the four were already missing at probe time. The read path now verifies tree IDENTITY, never revision — the same policy the build path's tier-2 load has followed since #300. A snapshot stamped for a different worktree is still dropped, since nothing downstream of this read content-verifies it. A snapshot stamped at a different HEAD is served, and both `project_report`'s trust notes and `module_report`'s warnings say which commit it was built at. That caveat is derived per call from the snapshot's stamped commit and the worktree's HEAD right now, so it never names a commit that has stopped being HEAD and it clears itself when HEAD returns to the stamped one. The mid-build resume checkpoint carried the same defect and got the same fix: it hashes every processed file on resume, so revision equality proved nothing the hashes did not. Both read verdicts now write a bounded `review-graph.log` record (`snapshot_read_dropped`, `snapshot_read_drifted`), rising-edge per workspace off the `review-graph-snapshot-read` ledger kind — the drop used to be silent, which is why it hid for weeks.
|
|
146
|
+
|
|
147
|
+
- **A settled review-graph build no longer suppresses every later build, and `project_report` only claims a retry when one started (closes #1962)** — `buildOrUpdateGraph`'s dedupe cache deleted its entry on rejection only, so a settled promise for a skipped or completed build answered every later call for the same key for the rest of the process. Only the dispatch pipeline's per-invocation `clearGraphCache()` ever removed it, and the background build `project_report` kicks off never goes through the pipeline: four calls over 37s produced one `build_started` record and three "A retry was started." messages with no retry behind them. The entry now lives only while its build is pending, so genuine concurrency still dedupes and a settled build never latches. `project_report` asks the builder whether a build is already in flight before it claims one started, and reports the current attempt rather than replaying the first one as if it were fresh. A `.pi-lens.json` at the repo root raises `reviewGraph.maxFiles` above the derived 1000-file cap this repo trips, so the tool works on this repo at all.
|
|
148
|
+
|
|
149
|
+
- **A language server that dies mid-session now records why, and the liveness probe stops provoking the deaths (closes #1969)** — an ast-grep child closed with `code=1` and empty stderr 14 times in one day. The visible fallout was 19 `lsp_client_skipped_broken` cooldowns and 32 coverage gaps; the cause was recorded nowhere. Two changes. First, an unprompted process `close` now writes an `lsp-server-unexpected-close` degradation-ledger entry naming the server, the exit code, the signal, and whether stderr carried anything. It is written on `close` rather than `exit`, so "stderr was empty" is a fact about the server and not a race with the pipe, and it is gated on `shutdownRequested`, so evictions and ordinary teardown record nothing. Second, the `pingLiveness` round-trip (#1277) no longer hardcodes `workspace/symbol`. It picks a method the server advertised, best first: `workspace/symbol`, then `textDocument/documentSymbol` or `textDocument/hover` on an already-open document, then nothing. ast-grep's tower_lsp backend logged "got a 'workspace/symbol' request, but it is not implemented" on every probe; it advertises `hoverProvider`, so it keeps a real round-trip. When no probe is available the ledger records `lsp-liveness-probe-unsupported`, because liveness derived from process and connection state alone is the weaker verdict and must not pass as the strong one. Also fixed on the same path: `createLSPClient`'s initialize-timeout catch killed the hung child without claiming the kill, so its own teardown reported as a crash. That fabricated the exact `code=1 signal=none stderr=empty` signature this change exists to make trustworthy, and it had been writing a false `lsp_server_unexpected_exit` latency line on every failed handshake since the #615 follow-up.
|
|
150
|
+
|
|
151
|
+
- **Log writer reopens and retries once on a failed write instead of dropping records silently (refs #1970)** — `ndjson-logger.ts` is the shared write-plumbing behind every NDJSON sink (`latency.log`, `extension.log`, `tree-sitter.log`, `cascade.log`, `word-index.log`, `review-graph.log`, and any other `createNdjsonLogger` caller). This hardens the sink against reachable `appendFile` failures (ENOENT after the parent directory is deleted, EBUSY/EPERM under a syncing OneDrive folder or antivirus lock, EMFILE, ENOSPC) — the specific `ERR_STREAM_DESTROYED` reports that motivated this issue were root-caused to a different process's persistent-stream logger, not this sink, but the reachable-failure classes above land here today with no recovery and no observability. A write that throws now gets one reopen-and-retry (re-verify the parent directory, then retry the write once) before it counts as a loss, matching the `pi-analyze#15` shape. An unrecovered write is counted in a per-sink, in-memory tally (`writeFailures`) rather than thrown or silently discarded, and `degradation-ledger.ts` folds that tally into `getDegradationSummary()` at READ time under a new `log-sink-write-failure` kind — so `pilens_health` names the sink and the dropped-write count. The fold happens by reading ndjson-logger's own tally, never by writing a durable ledger row back through the sink that just failed, so a permanently dead sink cannot recurse into an unbounded chain of self-reporting writes. The tally resets alongside the rest of the ledger at `session_start`.
|
|
152
|
+
|
|
153
|
+
- **Warmup and workspace-scan walkers gate by file extension before consulting the ignore matcher, cutting a 31.7s stall to sub-second on repos with large ignored file piles (closes #1974, reported by @0xkite)** — `isIgnored` recompiles minimatch patterns per ancestor directory on every call; a runtime-output directory like `wal/` holding tens of thousands of `.log` files, gitignored only by a file-level `*.log` pattern (so the directory itself is never pruned), forced every one of those files through that cost. Six walkers checked `isIgnored` before their own cheap extension gate, paying the expensive check for files the gate would have dropped anyway: `collectSourceFilesForWarmup` (`language-profile.ts`, the reported 31.7s case), `JscpdClient.hasSourceFilesRecursive` (`jscpd-client.ts`), `classifyEntry` (`source-filter.ts`), `makeSourceCountVisitor` (`startup-scan.ts`), `getModuleSourceFiles` (`review-graph/workspace-modules.ts`), and `collectWorkspaceDiagnosticFiles` (`lsp/index.ts`). All six now run their extension (or LSP-server) gate first; output is unchanged, since both gates still have to pass to keep a file — only the order changed. A repo-wide sweep of all 14 files that call `isIgnored` found `tree-sitter-client.ts` and `tools/lsp-diagnostics.ts` already extension-first, and cleared the remaining per-event/no-ext-gate sites by construction; see the PR body for the full per-site table.
|
|
154
|
+
|
|
155
|
+
- **oxfmt no longer fails on a file its own config ignores (refs #1844)** —
|
|
156
|
+
pi-lens offers oxfmt for every extension oxfmt supports, so in a project
|
|
157
|
+
whose oxfmt config carries `ignorePatterns`, oxfmt was selected for files it
|
|
158
|
+
then refused to touch. It exits 2 with "Expected at least one target file",
|
|
159
|
+
which the strict exit-code posture read as a formatting failure, so every
|
|
160
|
+
edit to an ignored file surfaced an error. pi-lens now passes
|
|
161
|
+
`--no-error-on-unmatched-pattern`, which makes an empty target set a clean
|
|
162
|
+
no-op. Every other nonzero exit, including an unparseable file, still fails.
|
|
163
|
+
|
|
164
|
+
- **ESLint warnings are no longer discarded (closes #1954)** — The eslint runner treated every exit 0 as "nothing found". ESLint exits 0 whenever no rule reaches error severity, so a run that produced only warning-severity findings was thrown away silently. The runner now parses stdout unconditionally and surfaces the findings; a clean file still reports clean, and exit 2 stays an unavailable skip.
|
|
165
|
+
|
|
166
|
+
- **Registered tools now always have descriptions in child/subagent sessions (closes #1988)** — the final registration boundary fills missing, empty, and
|
|
167
|
+
whitespace-only descriptions for active, lazy, activation, and wrapped tool
|
|
168
|
+
definitions.
|
|
169
|
+
|
|
170
|
+
- **mode=full no longer replays stale mid-edit blockers (refs #1993)** — a confirmed, fully-covered LSP sweep is now authoritative for its files: widget-store diagnostics captured from a since-fixed broken intermediate state are retired instead of rendering as current 🔴 blockers beside a clean sweep. Unconfirmed or timed-out sweeps keep the fail-open behavior.
|
|
171
|
+
|
|
172
|
+
- **Cache-miss attribution stays useful in long sessions (closes #1996)** — classify full model/provider identities and split unexplained or malformed cache evidence by bounded reason, with fail-closed request hashing, activation-owned primary/secondary isolation, sanitized numeric metadata, and a per-session cause summary.
|
|
173
|
+
|
|
174
|
+
- **Project snapshots no longer rewrite unchanged same-generation bodies (closes #1997)** — Persistence now coalesces concurrent requests and computes semantic identity on its worker before gzip or staging. Same-generation content changes and failed-write repairs still publish.
|
|
175
|
+
|
|
176
|
+
- **Classify oxlint no-match results as expected skips (closes #1998)**. Oxlint now carries `no-files-matched` through runner telemetry without emitting an extension error or claiming the file is clean. A single fail-closed state machine validates process completion, exit status, stderr, the captured banner, and every JSON summary field's type and range; truncated, malformed, wrong-status, or error-bearing lookalikes retain failure or unconfirmed telemetry.
|
|
177
|
+
|
|
178
|
+
- **Project-snapshot persistence validates gzip body integrity before skipping (closes #2008)** —
|
|
179
|
+
The skip decision trusted the meta sidecar and a fingerprint match alone, so a torn or
|
|
180
|
+
truncated gzip under an intact meta kept winning unchanged-dedupe and stayed canonical until
|
|
181
|
+
the project sequence advanced. The meta sidecar now records the on-disk gz byte length
|
|
182
|
+
(`gzBytes`) at every successful persist, and the dedupe baselines compare it against a live
|
|
183
|
+
stat: a size mismatch, or a legacy meta without the field, withholds the dedupe fingerprints
|
|
184
|
+
so the pending save republishes and rewrites the body. A skip that would honor evidence
|
|
185
|
+
failing this gate between dispatch and promotion is refused and rewritten synchronously.
|
|
186
|
+
Detections emit one `project_snapshot_body_integrity` latency record plus a bounded
|
|
187
|
+
`snapshot-integrity` degradation-ledger entry per corrupted body path. The baselines read is
|
|
188
|
+
also now a pure read: its in-process seeding write moved to the single dispatch seam that
|
|
189
|
+
owns the persist lifecycle.
|
|
190
|
+
|
|
191
|
+
- **Installer verify loop broken at the root (closes #2015)** — `verifyToolBinary` now spawns through `safeSpawnAsync` (tree-kill on timeout, typed kill-reason) instead of raw spawn whose SIGTERM orphaned grandchild node processes on Windows `.cmd` shims. A killed/inconclusive prober no longer counts as a verdict: the freshly installed binary is KEPT for cheap re-probe instead of deleted, ending the install/verify/reinstall churn (23 SIGTERMs and 4 cleanups observed in one day).
|
|
192
|
+
|
|
193
|
+
- **Windows `.cmd`/`.bat` spawns no longer fail when System32 is absent from the child PATH (closes #2023)** — The cmd.exe wrapper prefixed every spawn
|
|
194
|
+
with a bare `chcp 65001 &&`. When the child environment's PATH could not
|
|
195
|
+
resolve `chcp.com`, the lookup failed and `&&` short-circuited, so the whole
|
|
196
|
+
spawn exited 1 with empty output. chcp is now invoked via its pinned
|
|
197
|
+
`%SystemRoot%\System32\chcp.com` absolute path and chained with `&`, so a
|
|
198
|
+
code-page failure can never suppress the real command.
|
|
199
|
+
|
|
200
|
+
- **Register remaining agent-facing delivery surfaces (#2028)** — The per-edit 🔴 STOP block now drops blockers whose cited file no longer exists, so a deleted file's stale blocker no longer re-blocks the agent with no remediation. All five previously unregistered surfaces (stop blocker, `lsp_diagnostics` output, git-guard verdicts, read-guard preflight errors, thrashing notices) are registered in the finding-delivery gate registry.
|
|
201
|
+
|
|
202
|
+
- **Wall-clock budget tests get a quiet serial phase (closes #1920)** — Five test files asserting real elapsed-time budgets ran inside the default project's fork storm, measuring scheduler contention instead of code speed (`startup-overhead` measured 659-2321ms against a 500ms budget under load, green solo every time). They now run in a dedicated fully-serialized `wall-clock-budget` Vitest project that phases dead last on a quiet host. A coverage guard keeps the new include list from silently dropping renamed files.
|
|
203
|
+
|
|
19
204
|
## [4.1.0] - 2026-08-20
|
|
20
205
|
|
|
21
206
|
### Added
|
package/README.md
CHANGED
|
@@ -34,6 +34,112 @@ pi-lens gives AI coding agents fast, language-aware feedback while they write/ed
|
|
|
34
34
|
- MCP server (experimental) so Claude Code or any MCP client can drive the
|
|
35
35
|
same diagnostics/read-substitute tools pi-lens exposes to pi
|
|
36
36
|
|
|
37
|
+
## Architecture
|
|
38
|
+
|
|
39
|
+
Most lifecycle events enter through one wrapper, which drops and counts events
|
|
40
|
+
that arrive on a replaced session. `tool_call` registers raw: it delegates
|
|
41
|
+
straight to a handler that owns its own total guard. Events fan out into the
|
|
42
|
+
edit-time lane and the LSP lane. Both lanes write into the findings stores.
|
|
43
|
+
Nothing reaches the agent from those stores until a freshness gate or an
|
|
44
|
+
explicit age label clears it.
|
|
45
|
+
|
|
46
|
+
```mermaid
|
|
47
|
+
flowchart TD
|
|
48
|
+
subgraph host["pi host"]
|
|
49
|
+
HOST["Host events<br/>tool_call, tool_result, turn_start/end,<br/>session_start/shutdown, agent_end, context"]
|
|
50
|
+
WRAP["Stale-ctx wrapper<br/>skips and counts events on a replaced session<br/>tool_result, turn_start, turn_end, agent_end,<br/>agent_settled, session_start, context"]
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
subgraph guards["Guards"]
|
|
54
|
+
RG["Read-guard<br/>blocks edits that lack prior reading"]
|
|
55
|
+
GG["Git-guard<br/>holds commit/push while findings stay unresolved"]
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
subgraph edit["Edit-time lane"]
|
|
59
|
+
PIPE["Post-write pipeline<br/>secrets, format, autofix, sync, lint, tests"]
|
|
60
|
+
PLAN["Dispatch plan<br/>per file kind, per capability group"]
|
|
61
|
+
RUN["Runners<br/>format, lint, types, security, smells, docs"]
|
|
62
|
+
STRUCT["Structural rules<br/>tree-sitter queries and ast-grep"]
|
|
63
|
+
BUS["files-touched bus<br/>tells extensions which paths moved"]
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
subgraph lsp["LSP lane"]
|
|
67
|
+
POOL["Client pool<br/>warm reuse, idle eviction"]
|
|
68
|
+
DIAGS["File and workspace diagnostics"]
|
|
69
|
+
CASC["Impact cascade<br/>tiered wait policy"]
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
STORES["Findings stores<br/>widget state, warning caches, project snapshot"]
|
|
73
|
+
|
|
74
|
+
subgraph gate["Freshness gating"]
|
|
75
|
+
FRESH["Path freshness<br/>mtime vs scan time, past-EOF, dependency drift"]
|
|
76
|
+
DISPO["Dispositions<br/>false-positive, suppress, defer, flagged"]
|
|
77
|
+
LABEL["Explicit age label<br/>for findings no path gate can check"]
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
subgraph deliver["Delivery surfaces"]
|
|
81
|
+
TURN["Turn-end findings injection"]
|
|
82
|
+
WIDGET["Widget and footer tally"]
|
|
83
|
+
TOOLS["lens_diagnostics tool"]
|
|
84
|
+
NUDGE["Agent nudges"]
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
SESSION["Session lifecycle<br/>primary, sequential replacement, concurrent secondary"]
|
|
88
|
+
SINKS["Observability sinks<br/>latency.log, degradation ledger, bounded telemetry,<br/>cache observability, cascade and tree-sitter logs"]
|
|
89
|
+
|
|
90
|
+
HOST --> WRAP
|
|
91
|
+
HOST -->|tool_call, raw| RG
|
|
92
|
+
HOST -->|tool_call, raw| GG
|
|
93
|
+
WRAP -->|session_start| SESSION
|
|
94
|
+
WRAP -->|tool_result| PIPE
|
|
95
|
+
WRAP -->|tool_result, records reads and writes| RG
|
|
96
|
+
SESSION --> POOL
|
|
97
|
+
SESSION --> STORES
|
|
98
|
+
PIPE --> PLAN
|
|
99
|
+
PLAN --> RUN
|
|
100
|
+
PLAN --> STRUCT
|
|
101
|
+
PIPE --> POOL
|
|
102
|
+
PIPE --> BUS
|
|
103
|
+
POOL --> DIAGS
|
|
104
|
+
DIAGS --> CASC
|
|
105
|
+
RUN --> STORES
|
|
106
|
+
STRUCT --> STORES
|
|
107
|
+
DIAGS --> STORES
|
|
108
|
+
CASC --> STORES
|
|
109
|
+
BUS --> NUDGE
|
|
110
|
+
RG -->|read and edit history filter| NUDGE
|
|
111
|
+
STORES --> FRESH
|
|
112
|
+
STORES --> LABEL
|
|
113
|
+
FRESH --> DISPO
|
|
114
|
+
DISPO --> TURN
|
|
115
|
+
DISPO --> WIDGET
|
|
116
|
+
DISPO --> TOOLS
|
|
117
|
+
LABEL --> TURN
|
|
118
|
+
TURN --> GG
|
|
119
|
+
WRAP --> SINKS
|
|
120
|
+
PIPE --> SINKS
|
|
121
|
+
RUN --> SINKS
|
|
122
|
+
STRUCT --> SINKS
|
|
123
|
+
POOL --> SINKS
|
|
124
|
+
CASC --> SINKS
|
|
125
|
+
RG --> SINKS
|
|
126
|
+
GG --> SINKS
|
|
127
|
+
FRESH --> SINKS
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
Architecture-level view, updated when a lane changes. Per-tool inventories live
|
|
131
|
+
in [features](docs/features.md) and
|
|
132
|
+
[language coverage](docs/language-coverage.md). Today the edit-time lane carries
|
|
133
|
+
45+ runner modules over 35+ file kinds, and the LSP lane speaks to a dozen-plus
|
|
134
|
+
language servers.
|
|
135
|
+
|
|
136
|
+
The gating box is an abstraction, not a call order. Freshness covers several
|
|
137
|
+
independent mechanisms: path freshness against scan time, past-EOF line checks,
|
|
138
|
+
and forward-import dependency drift. Dispositions are one more filter alongside
|
|
139
|
+
them, not a second stage every finding walks through. Read the box as "a finding
|
|
140
|
+
passes the gates that apply to it", and see `clients/finding-delivery-gate.ts`
|
|
141
|
+
for the per-surface contract.
|
|
142
|
+
|
|
37
143
|
## Install
|
|
38
144
|
|
|
39
145
|
```bash
|
|
@@ -5,7 +5,8 @@ import { createNdjsonLogger } from "./ndjson-logger.js";
|
|
|
5
5
|
const AW_LOG_DIR = getGlobalPiLensDir();
|
|
6
6
|
const AW_LOG_FILE = path.join(AW_LOG_DIR, "actionable-warnings.log");
|
|
7
7
|
const AW_LOG_BACKUP_FILE = path.join(AW_LOG_DIR, "actionable-warnings.log.1");
|
|
8
|
-
const MAX_LOG_BYTES = Math.max(128 * 1024, Number.parseInt(process.env.PI_LENS_AW_LOG_MAX_BYTES ?? "1048576", 10) ||
|
|
8
|
+
const MAX_LOG_BYTES = Math.max(128 * 1024, Number.parseInt(process.env.PI_LENS_AW_LOG_MAX_BYTES ?? "1048576", 10) ||
|
|
9
|
+
1048576);
|
|
9
10
|
const writer = createNdjsonLogger({
|
|
10
11
|
filePath: AW_LOG_FILE,
|
|
11
12
|
maxBytes: MAX_LOG_BYTES,
|
|
@@ -633,7 +633,12 @@ export async function applyConservativeActionableWarningFixes(args) {
|
|
|
633
633
|
}
|
|
634
634
|
const edit = selected.edit;
|
|
635
635
|
const applied = await applyWorkspaceEdit(edit, args.cwd, args.mutationContext
|
|
636
|
-
? {
|
|
636
|
+
? {
|
|
637
|
+
mutationContext: {
|
|
638
|
+
...args.mutationContext,
|
|
639
|
+
emitSummary: false,
|
|
640
|
+
},
|
|
641
|
+
}
|
|
637
642
|
: undefined);
|
|
638
643
|
appliedResults.push(applied);
|
|
639
644
|
for (const changedFile of applied.files)
|
|
@@ -642,8 +647,7 @@ export async function applyConservativeActionableWarningFixes(args) {
|
|
|
642
647
|
}
|
|
643
648
|
catch (err) {
|
|
644
649
|
failedCount++;
|
|
645
|
-
const partial = err
|
|
646
|
-
.appliedWorkspaceEdit;
|
|
650
|
+
const partial = err.appliedWorkspaceEdit;
|
|
647
651
|
if (partial) {
|
|
648
652
|
appliedResults.push(partial);
|
|
649
653
|
for (const changedFile of partial.files)
|
|
@@ -656,13 +660,18 @@ export async function applyConservativeActionableWarningFixes(args) {
|
|
|
656
660
|
}
|
|
657
661
|
}
|
|
658
662
|
summary.changedFiles = [...changedFiles];
|
|
659
|
-
if (args.mutationContext &&
|
|
663
|
+
if (args.mutationContext &&
|
|
664
|
+
(summary.considered > 0 || appliedResults.length > 0)) {
|
|
660
665
|
recordLspMutationBatch(args.mutationContext, {
|
|
661
666
|
results: appliedResults,
|
|
662
667
|
considered: summary.considered,
|
|
663
668
|
completed: summary.applied,
|
|
664
669
|
failedCount,
|
|
665
|
-
status: failedCount > 0
|
|
670
|
+
status: failedCount > 0
|
|
671
|
+
? "failed"
|
|
672
|
+
: appliedResults.length > 0
|
|
673
|
+
? "success"
|
|
674
|
+
: "skipped",
|
|
666
675
|
bookkeep: false,
|
|
667
676
|
});
|
|
668
677
|
}
|
|
@@ -4,7 +4,7 @@ import * as path from "node:path";
|
|
|
4
4
|
import { logLatency } from "./latency-logger.js";
|
|
5
5
|
import { normalizeMapKey, toProjectRelativePath } from "./path-utils.js";
|
|
6
6
|
import { resolveRunnerPath } from "./dispatch/runner-context.js";
|
|
7
|
-
import {
|
|
7
|
+
import { freshnessFromMtime } from "./freshness.js";
|
|
8
8
|
export const MAX_ADVISORY_AFFECTED_FILES = 256;
|
|
9
9
|
export function advisoryPathKey(filePath, cwd) {
|
|
10
10
|
return normalizeMapKey(path.resolve(cwd, filePath));
|
|
@@ -68,25 +68,41 @@ function isFiniteNumber(value) {
|
|
|
68
68
|
return typeof value === "number" && Number.isFinite(value);
|
|
69
69
|
}
|
|
70
70
|
function isCapturedHash(value) {
|
|
71
|
-
return typeof value === "string" &&
|
|
72
|
-
(/^[a-f0-9]{64}$/.test(value) ||
|
|
71
|
+
return (typeof value === "string" &&
|
|
72
|
+
(/^[a-f0-9]{64}$/.test(value) ||
|
|
73
|
+
value === "missing" ||
|
|
74
|
+
value.startsWith("unreadable:")));
|
|
73
75
|
}
|
|
74
76
|
function isWellFormed(value) {
|
|
75
77
|
if (!value || typeof value !== "object")
|
|
76
78
|
return false;
|
|
77
79
|
const record = value;
|
|
78
80
|
const revision = record.revision;
|
|
79
|
-
return !!revision &&
|
|
80
|
-
|
|
81
|
-
isFiniteNumber(revision.
|
|
82
|
-
|
|
83
|
-
(
|
|
84
|
-
isFiniteNumber(
|
|
85
|
-
|
|
81
|
+
return (!!revision &&
|
|
82
|
+
typeof revision.sessionId === "string" &&
|
|
83
|
+
isFiniteNumber(revision.projectSeq) &&
|
|
84
|
+
isFiniteNumber(revision.turnIndex) &&
|
|
85
|
+
isFiniteNumber(revision.generation) &&
|
|
86
|
+
isFiniteNumber(revision.capturedAt) &&
|
|
87
|
+
Array.isArray(record.files) &&
|
|
88
|
+
record.files.length > 0 &&
|
|
89
|
+
record.files.every((file) => !!file &&
|
|
90
|
+
typeof file.path === "string" &&
|
|
91
|
+
(file.role === "source" ||
|
|
92
|
+
file.role === "test" ||
|
|
93
|
+
file.role === "affected") &&
|
|
94
|
+
isFiniteNumber(file.mtimeMs) &&
|
|
95
|
+
isFiniteNumber(file.size) &&
|
|
96
|
+
isCapturedHash(file.sha256)));
|
|
86
97
|
}
|
|
87
98
|
export function validateAdvisoryProvenance(record, cwd, runtime) {
|
|
88
99
|
if (!isWellFormed(record.provenance)) {
|
|
89
|
-
return {
|
|
100
|
+
return {
|
|
101
|
+
status: "unknown",
|
|
102
|
+
reasons: ["malformed-or-legacy-provenance"],
|
|
103
|
+
allFilesDeleted: false,
|
|
104
|
+
changedPathCount: 0,
|
|
105
|
+
};
|
|
90
106
|
}
|
|
91
107
|
const provenance = record.provenance;
|
|
92
108
|
const reasons = [];
|
|
@@ -149,9 +165,19 @@ export function validateAdvisoryProvenance(record, cwd, runtime) {
|
|
|
149
165
|
}
|
|
150
166
|
const allFilesDeleted = deletedFiles === provenance.files.length;
|
|
151
167
|
if (unknown)
|
|
152
|
-
return {
|
|
168
|
+
return {
|
|
169
|
+
status: "unknown",
|
|
170
|
+
reasons,
|
|
171
|
+
allFilesDeleted,
|
|
172
|
+
changedPathCount: changedPaths.size,
|
|
173
|
+
};
|
|
153
174
|
return reasons.length > 0
|
|
154
|
-
? {
|
|
175
|
+
? {
|
|
176
|
+
status: "superseded",
|
|
177
|
+
reasons,
|
|
178
|
+
allFilesDeleted,
|
|
179
|
+
changedPathCount: changedPaths.size,
|
|
180
|
+
}
|
|
155
181
|
: { status: "current", reasons: [], allFilesDeleted, changedPathCount: 0 };
|
|
156
182
|
}
|
|
157
183
|
// ── Finding-cited path existence (#1461 slice 1) ──────────────────────────────
|
|
@@ -224,11 +250,15 @@ const MAX_LOGGED_DEAD_PATHS = 3;
|
|
|
224
250
|
export function findingPathFreshness(resolvedPath, scannedAtMs) {
|
|
225
251
|
try {
|
|
226
252
|
const stat = fs.statSync(resolvedPath);
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
253
|
+
// No scan timestamp recorded: the pre-kernel behavior treated the
|
|
254
|
+
// path as live (no reference to be stale against) - preserved.
|
|
255
|
+
if (scannedAtMs === undefined)
|
|
256
|
+
return "live";
|
|
257
|
+
const verdict = freshnessFromMtime({
|
|
258
|
+
mtimeMs: stat.mtimeMs,
|
|
259
|
+
referenceMs: scannedAtMs,
|
|
260
|
+
});
|
|
261
|
+
return verdict.verdict === "stale" ? "stale" : "live";
|
|
232
262
|
}
|
|
233
263
|
catch (error) {
|
|
234
264
|
const code = error.code ?? "unknown";
|
|
@@ -113,9 +113,7 @@ export class AstGrepClient {
|
|
|
113
113
|
(fs.existsSync(projectRuleDir)
|
|
114
114
|
? projectRuleDir
|
|
115
115
|
: resolvePackagePath(import.meta.url, "rules"));
|
|
116
|
-
this.log = verbose
|
|
117
|
-
? createSubsystemLogger("ast-grep")
|
|
118
|
-
: () => { };
|
|
116
|
+
this.log = verbose ? createSubsystemLogger("ast-grep") : () => { };
|
|
119
117
|
this.ruleManager = new AstGrepRuleManager(this.ruleDir, this.log);
|
|
120
118
|
this.runner = new SgRunner(verbose);
|
|
121
119
|
}
|
|
@@ -19,7 +19,13 @@ export function stageOwnerPidFromName(name) {
|
|
|
19
19
|
}
|
|
20
20
|
export async function sweepOwnStagingFiles(directories, options) {
|
|
21
21
|
const uniqueDirectories = [...new Set(directories)].filter(Boolean);
|
|
22
|
-
const result = {
|
|
22
|
+
const result = {
|
|
23
|
+
directories: uniqueDirectories.length,
|
|
24
|
+
scanned: 0,
|
|
25
|
+
removed: 0,
|
|
26
|
+
liveSkipped: 0,
|
|
27
|
+
truncated: false,
|
|
28
|
+
};
|
|
23
29
|
for (const directory of uniqueDirectories) {
|
|
24
30
|
let handle;
|
|
25
31
|
try {
|
|
@@ -47,14 +53,20 @@ export async function sweepOwnStagingFiles(directories, options) {
|
|
|
47
53
|
continue;
|
|
48
54
|
}
|
|
49
55
|
try {
|
|
50
|
-
await fs.promises.rm(path.join(directory, entry.name), {
|
|
56
|
+
await fs.promises.rm(path.join(directory, entry.name), {
|
|
57
|
+
force: true,
|
|
58
|
+
});
|
|
51
59
|
result.removed++;
|
|
52
60
|
}
|
|
53
|
-
catch {
|
|
61
|
+
catch {
|
|
62
|
+
/* leave it for the next bounded sweep */
|
|
63
|
+
}
|
|
54
64
|
}
|
|
55
65
|
result.truncated ||= reachedEntryCap;
|
|
56
66
|
}
|
|
57
|
-
catch {
|
|
67
|
+
catch {
|
|
68
|
+
/* best-effort directory scan */
|
|
69
|
+
}
|
|
58
70
|
finally {
|
|
59
71
|
await handle.close().catch(() => { });
|
|
60
72
|
}
|