pi-lens 3.8.69 β 3.8.70
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 +75 -0
- package/README.md +15 -13
- package/dist/clients/bootstrap.js +3 -1
- package/dist/clients/bus-events-logger.js +51 -0
- package/dist/clients/bus-publish.js +45 -2
- package/dist/clients/code-quality-warnings.js +27 -0
- package/dist/clients/dead-code-client.js +5 -23
- package/dist/clients/diagnostics-publish.js +38 -2
- package/dist/clients/dispatch/auxiliary-lsp.js +62 -6
- package/dist/clients/dispatch/facts/function-facts.js +83 -0
- package/dist/clients/dispatch/rules/async-noise.js +1 -1
- package/dist/clients/dispatch/rules/async-unnecessary-wrapper.js +1 -1
- package/dist/clients/dispatch/rules/cors-wildcard.js +1 -1
- package/dist/clients/dispatch/rules/error-obscuring.js +2 -1
- package/dist/clients/dispatch/rules/error-swallowing.js +2 -1
- package/dist/clients/dispatch/rules/framework-call-noise.js +67 -0
- package/dist/clients/dispatch/rules/high-complexity.js +12 -1
- package/dist/clients/dispatch/rules/high-fan-out.js +14 -3
- package/dist/clients/dispatch/rules/high-import-coupling.js +1 -1
- package/dist/clients/dispatch/rules/missing-error-propagation.js +1 -1
- package/dist/clients/dispatch/rules/no-commented-credentials.js +1 -1
- package/dist/clients/dispatch/rules/pass-through-wrappers.js +1 -1
- package/dist/clients/dispatch/rules/placeholder-comments.js +1 -1
- package/dist/clients/dispatch/rules/unsafe-boundary.js +1 -1
- package/dist/clients/dispatch/runners/ast-grep-napi.js +75 -18
- package/dist/clients/dispatch/runners/lsp.js +22 -6
- package/dist/clients/gitleaks-client.js +40 -5
- package/dist/clients/installer/index.js +123 -6
- package/dist/clients/instance-reaper.js +229 -2
- package/dist/clients/instance-registry.js +71 -2
- package/dist/clients/knip-client.js +35 -38
- package/dist/clients/language-profile.js +7 -11
- package/dist/clients/lens-engine.js +12 -0
- package/dist/clients/log-cleanup.js +61 -19
- package/dist/clients/lsp/cascade-tier.js +31 -19
- package/dist/clients/lsp/client.js +52 -7
- package/dist/clients/lsp/config.js +27 -1
- package/dist/clients/lsp/index.js +547 -59
- package/dist/clients/lsp/server-strategies.js +17 -8
- package/dist/clients/lsp/server.js +63 -27
- package/dist/clients/lsp-budget.js +148 -0
- package/dist/clients/mcp/session.js +1 -0
- package/dist/clients/middle-man-analysis.js +260 -0
- package/dist/clients/module-report.js +173 -11
- package/dist/clients/ndjson-logger.js +24 -0
- package/dist/clients/opengrep-client.js +217 -0
- package/dist/clients/path-utils.js +40 -0
- package/dist/clients/pipeline.js +1 -1
- package/dist/clients/project-diagnostics/extractors.js +21 -1
- package/dist/clients/project-diagnostics/fresh-fetch.js +275 -0
- package/dist/clients/project-diagnostics/runner-adapters/opengrep.js +40 -0
- package/dist/clients/project-diagnostics/runner-adapters/runner-findings.js +46 -0
- package/dist/clients/quiet-window.js +57 -2
- package/dist/clients/read-expansion.js +2 -3
- package/dist/clients/read-guard-tool-lines.js +10 -0
- package/dist/clients/resource-sampler.js +254 -0
- package/dist/clients/review-graph/builder.js +265 -11
- package/dist/clients/review-graph/symbol-id.js +57 -0
- package/dist/clients/runtime-session.js +40 -6
- package/dist/clients/runtime-turn.js +53 -11
- package/dist/clients/safe-spawn.js +48 -2
- package/dist/clients/source-filter.js +12 -25
- package/dist/clients/source-walker.js +66 -0
- package/dist/clients/startup-scan.js +37 -42
- package/dist/clients/symbol-containment.js +24 -0
- package/dist/clients/test-runner-client.js +544 -29
- package/dist/clients/tree-sitter-symbol-extractor.js +12 -5
- package/dist/clients/widget-state.js +61 -1
- package/dist/clients/write-ordering-guard.js +58 -0
- package/dist/clients/zizmor-config.js +27 -0
- package/dist/index.js +6640 -2947
- package/dist/mcp/server.js +95 -19
- package/dist/tools/activate-tools.js +72 -0
- package/dist/tools/ast-dump.js +2 -8
- package/dist/tools/lens-diagnostics.js +290 -30
- package/dist/tools/lsp-diagnostics.js +544 -104
- package/dist/tools/module-report.js +18 -5
- package/docs/agent-tooling-improvement-plan.md +660 -0
- package/docs/agent-tools.md +129 -0
- package/docs/ast-grep_rules_catalog.md +564 -0
- package/docs/astplayground.md +178 -0
- package/docs/audit1.md +177 -0
- package/docs/custom-rules.md +231 -0
- package/docs/dependencies.md +54 -0
- package/docs/env_variables.md +10 -0
- package/docs/environment-variables.md +67 -0
- package/docs/fable.md +139 -0
- package/docs/features.md +338 -0
- package/docs/globalconfig.md +79 -0
- package/docs/inspiration_gograph.md +601 -0
- package/docs/inspiration_neovim.md +1795 -0
- package/docs/inspiration_neovim2.md +402 -0
- package/docs/inspiration_tree_sitter_rules.md +369 -0
- package/docs/inspiration_tree_sitter_rules_investigation.md +328 -0
- package/docs/inspiration_tree_sitter_rules_round2.md +470 -0
- package/docs/language-coverage.md +48 -0
- package/docs/lsp-bridge-analysis.md +444 -0
- package/docs/lsp-capability-matrix.md +157 -0
- package/docs/lsp-latency-benchmark.md +169 -0
- package/docs/mcp.md +438 -0
- package/docs/module-report-read-symbol.md +258 -0
- package/docs/newinspiration.md +338 -0
- package/docs/servercapabilities.md +118 -0
- package/docs/subagent-compat.md +281 -0
- package/docs/tools.md +13 -0
- package/docs/tools_improvement2.md +345 -0
- package/docs/tree-sitter_rules_catalog.md +296 -0
- package/docs/usage.md +129 -0
- package/docs/word-index.md +150 -0
- package/grammars/tree-sitter-c.wasm +0 -0
- package/grammars/tree-sitter-c_sharp.wasm +0 -0
- package/grammars/tree-sitter-cpp.wasm +0 -0
- package/grammars/tree-sitter-dart.wasm +0 -0
- package/grammars/tree-sitter-elixir.wasm +0 -0
- package/grammars/tree-sitter-kotlin.wasm +0 -0
- package/grammars/tree-sitter-lua.wasm +0 -0
- package/grammars/tree-sitter-ocaml.wasm +0 -0
- package/grammars/tree-sitter-php.wasm +0 -0
- package/grammars/tree-sitter-ruby.wasm +0 -0
- package/grammars/tree-sitter-swift.wasm +0 -0
- package/grammars/tree-sitter-toml.wasm +0 -0
- package/grammars/tree-sitter-vue.wasm +0 -0
- package/grammars/tree-sitter-zig.wasm +0 -0
- package/package.json +5 -1
- package/rules/ast-grep-rules/rules/array-callback-return-js.yml +1 -1
- package/rules/ast-grep-rules/rules/jwt-no-verify-js.yml +1 -1
- package/rules/ast-grep-rules/rules/no-extra-boolean-cast-js.yml +1 -1
- package/rules/ast-grep-rules/rules/no-implied-eval-js.yml +1 -1
- package/rules/ast-grep-rules/rules/no-javascript-url-js.yml +1 -1
- package/rules/ast-grep-rules/rules/no-sql-in-code-js.yml +1 -1
- package/rules/ast-grep-rules/rules/weak-rsa-key-js.yml +1 -1
- package/skills/pi-lens-ast-grep/SKILL.md +3 -1
- package/dist/clients/production-readiness.js +0 -493
package/CHANGELOG.md
CHANGED
|
@@ -10,6 +10,81 @@ All notable changes to pi-lens will be documented in this file.
|
|
|
10
10
|
|
|
11
11
|
### Fixed
|
|
12
12
|
|
|
13
|
+
## [3.8.70] - 2026-07-14
|
|
14
|
+
|
|
15
|
+
### Added
|
|
16
|
+
|
|
17
|
+
- **Review-graph symbols now carry an owner-qualified display name, and `calls`/`references` edges gain two more resolution tiers** (refs #655 β second, still narrowly-scoped slice; `BehaviorFact` extraction, `SymbolMetadataVNext`, and per-language behavior adapters remain untouched) β phase 1 (#659) fixed symbol-node ID collisions but deliberately left qualified ownership (`ClassName.method`) and call-edge resolution at just `"exact"`/`"name-only"`. This slice adds both, reusing existing machinery rather than building parallel logic: (1) **Qualified names**: a new shared containment helper, `clients/symbol-containment.ts`'s `findOwnerName`, implements the SAME strict-range-containment/smallest-span algorithm `module-report.ts`'s outline nesting (`nestEntries`, #301) already uses, so a new `ReviewGraphNode.qualifiedName` field (e.g. `UserService.run`) is computed identically for tree-sitter-based languages (`builder.ts`'s `addTreeSitterFile`, over the file's own deduped symbol list) and jsts (`dispatch/facts/function-facts.ts`'s `functionFactProvider`, which now also collects class/interface ranges in the SAME already-parsed-tree walk and stamps each `FunctionSummary.owner`, since jsts graph nodes come from a different, lighter tree-sitter integration than module-report's own extractor β literally sharing one function call across the two wasn't possible, only the algorithm was factored out). `module-report.ts`'s `resolveUsedBy` now renders `node.qualifiedName ?? node.symbolName`, so two same-file, same-named methods on different classes are distinguishable in `usedBy`/`blastRadius` output without cross-referencing line numbers β and, as a side benefit, its caller-dedup key (previously bare name) no longer risks conflating two different classes' same-named callers. Cross-tool consistency was a hard requirement: a qualified name rendered here is guaranteed to be a valid `Class.method` input to `read_symbol`'s own resolver (`resolveQualifiedMatch`) because both derive from the same "smallest strictly-containing declaration" notion of ownership β verified by a new consistency test, not just asserted. (2) **Two new `resolution` tiers**, both computed only for jsts in this slice (the one ingestion path with import-specifier names and same-file type hints already cheaply available; other languages continue to fall back to `"exact"`/`"name-only"` β a deliberate, documented scope boundary, not an oversight): `"import"` narrows a bare-name callee to the specific in-project file its caller's own `import { x } from "./service.js"` names, via a new `importHintFile` edge-metadata hint threaded from `addJsTsFile` into `resolveDeferredSymbolEdges`, resolved when that ONE file has exactly one same-named symbol (falls through to the existing global-uniqueness `"exact"` check, then `"name-only"`, when it doesn't uniquely narrow). `"receiver-type"` resolves a `obj.method()` call directly to a specific class's method when the receiver's type is determinable from the SAME function's tree-sitter parse β a `const x = new ClassName()` assignment or a typed parameter β via two new, narrowly-scoped `function-facts.ts` collectors (`collectMemberCallSites`, `collectReceiverTypes`) covering only the clearest common shapes (chained/computed/`this.`-receiver calls, cross-function flow, and generics are conservatively left as the pre-existing "external" classification, never guessed). Both tiers fail closed: an owner+name pair that's ITSELF ambiguous (e.g. two same-named methods sharing one qualified name) resolves to a dedicated qualified-name placeholder tagged `"name-only"` rather than picking one of the 2+ candidates, and a genuinely undeterminable receiver type keeps today's plain external classification with no resolution tag at all β never a wrong `"exact"`/`"import"`/`"receiver-type"` claim. Fixed as a necessary side effect (not scope creep): `localImportToFile` never stripped a `.js`/`.jsx`/`.mjs`/`.cjs` extension from an import specifier before trying sibling extensions, so the extremely common TS-as-ESM pattern this very codebase uses everywhere (`import { x } from "./service.js"` pointing at a real `service.ts`) silently failed to resolve to a real file β a latent gap in the existing fileβfile `imports` edge that the new `"import"` tier depends on directly. Investigated and confirmed UNAFFECTED: `read_enclosing` (`clients/module-report.ts`'s `readEnclosing`) is purely positional (file + line, fresh tree-sitter extraction per call) and touches no symbol ID, resolution, or qualified-name machinery. New tests: `tests/clients/review-graph/qualified-name-resolution.test.ts` (qualified-name rendering for a same-file different-class collision, the read_symbol/module-report qualified-name-format consistency check, both new resolution tiers each with a positive resolving case and a case that correctly stays `"name-only"` rather than over-claiming); one pre-existing `module-report.test.ts` assertion updated from `"exact"` to `"import"` (a strictly more specific, and now correctly reported, tier for that scenario).
|
|
18
|
+
- **`lsp_diagnostics`/`lens_diagnostics` sweeps now warm up the primary server once before the per-file loop starts, instead of the first few files eating cold-spawn-adjacent timeouts** (closes #667) β live dogfooding on a 100-file `lsp_diagnostics` sweep found the first 5 files touched all hit the exact 1000ms per-file timeout with `serverCountReady:1`, while every file from the 6th on was clean and fast (~450-500ms). `serverCountReady:1` only proves the server process spawned and passed the LSP `initialize` handshake β it does NOT prove the server can usefully answer a diagnostics request yet; tsserver-style servers can still be loading/indexing the project internally for seconds after that, and neither sweep tool had any check for this before starting its per-file loop, so whichever files landed first paid that cost individually. New `LSPService.ensureWarmForSweep` (`clients/lsp/index.ts`) is the ONE shared fix both tools route through (they already share `groupFilesByPrimaryServer`/`runPerServerGroups` from #631): a real "has this server already answered a confirmed diagnostics touch this session" check (a new `demonstratedReady` key set, populated by `touchFile` only on a non-inconclusive diagnostics-mode result β strictly stronger than `isAlive()`/spawned/handshake-complete), not just a guessed delay. Cold β performs exactly one bounded warm-up `touchFile` round trip against one representative file from the group, with its own generous, one-time, env-tunable budget (`PI_LENS_LSP_WARMUP_TIMEOUT_MS`, default 20s) distinct from the per-file sweep budget β then the normal sweep proceeds from an already-demonstrated-ready server. Already-warm (from an earlier touch/sweep this session) β a no-op: no extra round trip, no added latency. Wired into `runWorkspaceDiagnostics` (the engine behind `lens_diagnostics mode=full`) per server group, ahead of the per-file pre-open/touch loop but AFTER the opt-in whole-group `workspace/diagnostic` pull fast path (which already gets its own generous per-server budget covering the whole group in one shot, so it doesn't need a separate warm-up); and into `tools/lsp-diagnostics.ts`'s `mapWithConcurrency` (the shared batch/directory-scan primitive), called once per server group before its own per-file loop. Does NOT change the existing per-file wait budgets or confirmed/unconfirmed contract (#242/#611/#634) β purely a pre-loop addition. New tests: `tests/clients/lsp/sweep-warmup.test.ts` covers the pure decision logic directly against `ensureWarmForSweep` (a cold server performs exactly one warm-up round trip then is treated as warm; an already-warm server from a prior `touchFile` is a no-op) and integration-style coverage through `runWorkspaceDiagnostics` itself (a cold sweep pays exactly one extra warm-up round trip on top of its normal per-file touches; an already-warm sweep pays none) β guarding specifically against the warm-up regressing into a mandatory extra round trip on every sweep.
|
|
19
|
+
- **Registry-independent backstop orphan sweep** (closes #658) β live dogfooding found 3 `opengrep.exe`/`opengrep-core.exe` process chains with confirmed-dead parents that survived every `session_start` sweep for ~2 days. Root cause: `clients/instance-reaper.ts`'s existing `sweepOrphans` (#472) is entirely registry-driven β it only ever considers pids currently listed in some instance's `lspChildren[]`, so once a child's registry reference is lost (a stale-heartbeat entry removal per #525's asymmetric design, or a `killProcessTree` call that failed silently), that child becomes permanently invisible to every future sweep; the reaper never asks "what's actually running on this machine." New `sweepUntrackedOrphans` runs as a strictly additive SECOND layer alongside the unchanged registry-driven sweep: it enumerates live OS processes by known pi-lens-managed binary name (`MANAGED_BINARY_NAMES` β ast-grep, opengrep, opengrep-core, marksman, zizmor, typos-lsp, yaml-language-server, typescript-language-server, drawn from `clients/lsp/server.ts`'s spawn candidates) via one batched `Get-CimInstance Win32_Process` WQL query on Windows (reusing the same query pattern as the existing marker-search) or `ps -eo pid=,ppid=,args=` on POSIX, then applies a new pure decision function, `decideBackstopOrphanReaping` β a process is kill-eligible ONLY when it is NOT already tracked in any instance's `lspChildren[]` (deferred to the registry-driven reaper instead) AND its parent pid is confirmed dead via the existing identity-verified `realIsPidAlive` (never on an ambiguous/unresolvable parent pid, never on a live parent, never on binary name alone). Confirmed kills reuse the existing `killPidTree` tree-kill mechanism. Kill-attempt retry was considered per the issue's own framing: no new retry-tracking state was added β a silently-failed kill leaves the process untracked with a still-dead parent, so it stays classified as backstop-kill-eligible and the very next `session_start` sweep naturally retries it, with zero extra bookkeeping (documented in `sweepUntrackedOrphans`'s doc comment; a future hardening making `killPidTree` return a success signal for sharper logging is a reasonable follow-up, not required for correctness). Wired into `index.ts` alongside the existing `sweepOrphans()`/`registerInstance()` call site β fire-and-forget, never blocks or throws into `session_start`. New tests in `tests/clients/instance-reaper.test.ts` (`decideBackstopOrphanReaping`) mirror the existing pure/impure split: an untracked process with a confirmed-dead parent is kill-eligible; a live parent, an already-tracked pid, an unverifiable (zero/negative/NaN) parent pid, and a malformed self-parenting row are all never kill-eligible.
|
|
20
|
+
- **Review-graph symbol-node IDs are now collision-safe** (refs #655 β first, narrowly-scoped slice of a larger tracking issue; the rest β behavior facets, per-language adapters, `SymbolMetadataVNext` β is explicitly NOT part of this change) β `clients/review-graph/builder.ts` minted every symbol node's ID as `${file}:${name}`, so an overloaded function/method, two same-named methods on different classes in the same file, or a same-named nested function all collapsed onto ONE graph node; `pilens_module_report`'s `usedBy`/`blastRadius` sections (which read that node's incoming edges directly) could then silently merge or misattribute two genuinely different symbols' callers. Fix: a new shared helper, `clients/review-graph/symbol-id.ts`'s `buildSymbolId(file, name, kind, startLine)`, builds `${file}:${name}:${kind}:${startLine}` β enough to give every one of those concrete collision cases a distinct ID, since they always sit on different lines. Deliberately scoped DOWN from #655's full proposed `<file>:<qualified-name>:<kind>:<start-line>:<start-column>` shape: no qualified ownership (e.g. `ClassName.method` β needs an owner-chain the extractors don't compute uniformly today, real work #655 leaves for later) and no start column (review-graph's JS/TS symbols come from a different extractor, `dispatch/facts/function-facts.ts`, than module-report's own outline extractor, `tree-sitter-symbol-extractor.ts` β the two agree on start line for every function-like declaration but can diverge by a few columns for arrow functions; line already resolves every in-scope collision case without needing column, and keeps IDs comparable across both extractors). Also fixed a related dormant bug the new ID shape surfaced: some grammars' symbol queries (e.g. python's) match one real declaration under two patterns β a class method also matches the generic top-level `function_definition` rule β yielding two `Symbol` records identical in name/line/column but differing only in `kind`; the old name-only ID silently collapsed these back into one node, so the new kind-qualified ID needed a same-position dedupe (`dedupeSamePositionSymbols`, preferring the more specific kind) to avoid manufacturing phantom duplicate graph nodes. `clients/review-graph/types.ts`'s `ReviewGraphEdge` gains an optional `resolution?: "exact" | "name-only"` field (a narrow slice of #655's full call-edge-metadata proposal): a `calls`/`references` edge starts `"name-only"` (bare-name match, no scope/type info), and `resolveDeferredSymbolEdges` upgrades it to `"exact"` only when exactly one same-named real symbol exists graph-wide β otherwise it's left `"name-only"` so a consumer knows the target may be a name-collision guess. `clients/module-report.ts`'s `ModuleSymbolUsedBy` surfaces that same `resolution` field, and its internal `toEntry` now builds its own graph-node lookup key via the same shared `buildSymbolId` helper (previously a separately-hand-assembled `${normalizedPath}:${sym.name}` string) β with one language-specific wrinkle: for jsts, the lookup always uses kind `"function"` regardless of the outline's own finer-grained `sym.kind` ("method" etc.), because builder.ts's jsts graph nodes come from `function-facts.ts`, which has no method/function distinction. `REVIEW_GRAPH_VERSION` bumped `v3` β `v4` (same safe-rebuild mechanism as the v2βv3 #260 bump: a v3 snapshot's nodes/edges still use the old ID shape throughout, so `loadPersistedGraph` rejects it outright rather than merging it with newly-built v4 IDs). Architecturally, this is an IN-PLACE, versioned extension of the existing review graph β no parallel "v2 graph" β per #655's own stated decision to enrich one graph and expose projections over it rather than build a second production-code model; the existing `REVIEW_GRAPH_VERSION`/`buildGeneration` machinery already exists for exactly this kind of safe schema change. New tests: `tests/clients/review-graph/symbol-id.test.ts` (the old scheme's literal collision, the new scheme's disambiguation by kind+line, and an end-to-end same-file-different-class Python method case demonstrating two distinct graph nodes); `tests/clients/review-graph.service.test.ts` gains a v3-snapshot migration-safety test (flagged stale, blind-read rejected, a fresh build produces `v4` IDs) and a resolution-confidence test (a globally-unique callee resolves `"exact"`, an ambiguous same-named callee across two files stays `"name-only"`); `tests/clients/module-report.test.ts` gains an `"exact"`-resolution assertion on the existing cross-file-caller test, a jsts class-method graph-node-lookup regression test (guarding the "function" idKind mapping), and an ambiguous-same-name-across-files test asserting `"name-only"` is surfaced rather than a false `"exact"`.
|
|
21
|
+
- **`docs/` is now included in the published npm package** (closes #651) β pi.dev rewrites GitHub repo links to npm-package-relative paths for its packages listing, so every `docs/*.md` link was a dead link there since `docs/` was never in `package.json`'s `files` array. Added `"docs/"` to `files`; only the 23 already git-tracked/public docs ship (a handful of untracked local scratch files like `inspiration_*.md` are gitignored and were never part of a real publish regardless). Package size grows ~200KB (3.0MB β 3.2MB packed) β negligible. Reported by @eisterman.
|
|
22
|
+
- **pi-lens can now measure its own total CPU/RAM footprint β host process, every live LSP child, and every transient analyzer spawn** (closes #620) β diagnosed during a 2026-07-13 dogfooding session where a `lens_diagnostics mode=full` sweep's per-file confirmation rate collapsed under real CPU contention from ~25 concurrent `node.exe` processes (other worktrees/sessions on the same box), a theory only confirmable after the fact via manual `tasklist` correlation β pi-lens had no first-class way to answer "how much CPU/RAM is pi-lens itself using right now." New `clients/resource-sampler.ts` wraps `pidusage` (chosen over hand-rolling `/proc` vs Windows `Get-CimInstance`/perf-counters β each OS exposes process CPU/RSS differently, and pidusage already abstracts that; it's a small pure-JS package with one transitive dep, `safe-buffer`, so it bundles like the repo's other pure-JS runtime deps rather than needing an `EXTERNAL` entry in `scripts/bundle-dist.mjs`) with two consumers: (1) **long-lived processes** β `clients/instance-registry.ts`'s `InstanceEntry`/`LspChildEntry` (#449) gain `cpuPercent`/`rssBytes` fields, sampled at the existing quiet-window heartbeat cadence (`clients/quiet-window.ts`'s `buildHeartbeatResourcePatch`, bounded to 2s via `Promise.race` so a slow sample can never block the idle-window task β the per-turn-end heartbeat stays RSS-only, deliberately, since that's a much hotter path and CPU sampling there would make the measurement itself new per-turn overhead); (2) **transient analyzer children** (jscpd, knip, madge, gitleaks, govulncheck, trivy, opengrep, zizmor, β¦) β every `safeSpawnAsync` invocation (`clients/safe-spawn.ts`) is now bracketed by `startSpawnUsageSampler`, a 750ms poll started right after `spawn()` and stopped in the existing `child.on("close"/"error", ...)` handlers, recording peak/average CPU%+RSS into a new `spawn_resource_usage` phase entry in the existing `latency.log` (alongside the timing data already logged there) and attaching a `resourceUsage` field to the resolved `SpawnResult`. Windows-specific gotcha handled: `safe-spawn.ts` spawns with `shell: true` on Windows, so the sampled pid is `cmd.exe`'s wrapper, not the real tool's β `findDescendantPidsWindows` resolves the live descendant tree via one CIM query per poll tick (pure BFS split out as `walkDescendantPids` for testability) so a `node`/`npx`-wrapped tool is actually captured instead of reporting cmd.exe's near-zero usage. Net query surface: `clients/instance-registry.ts`'s new `computeResourceFootprint`(pure)/`getResourceFootprint` aggregate every registered instance's host + LSP children into total RSS/CPU/child-count across every pi-lens process on the machine, re-exported through the `clients/lens-engine.ts` seam and surfaced in `pilens_health`'s existing text/JSON output (a new "Resource footprint: N pi-lens instance(s) Β· XMB RSS Β· Y% CPU Β· Z LSP child process(es)" line, best-effort β a footprint read failure never breaks the rest of the tool). Every sampling path is best-effort end to end (a sampling failure loses a data point, never throws into the heartbeat/spawn/turn_end path it's attached to) per this repo's existing instrumentation-must-never-fail-the-operation-it-measures convention. Deliberately out of scope (tracked separately in #650): cross-process LSP server sharing/deduplication β this issue is purely about MEASURING footprint, not reducing it. New tests: `tests/clients/resource-sampler.test.ts` (pure `UsageAccumulator` peak/average math, pure `walkDescendantPids` BFS including a cycle-safety guard, `sampleProcesses` batching/absent-pid semantics, `startSpawnUsageSampler`'s tick/stop lifecycle β all against a mocked `pidusage`, no real process tables); `tests/clients/instance-registry.test.ts` gains coverage for `updateHeartbeat`'s new `cpuPercent`/`childUsage` patch fields (including "untouched, never zeroed" semantics for an unsampled tick) and `computeResourceFootprint`/`getResourceFootprint`; `tests/clients/safe-spawn-resource-usage.test.ts` covers the safe-spawn wiring (sampler start/stop timing, `resourceUsage` attachment, the `spawn_resource_usage` log entry, and that a throwing sampler never breaks the underlying spawn) against a mocked `resource-sampler.js`/`latency-logger.js`, driving a real tiny `node -e` child process end to end.
|
|
23
|
+
- **Cross-process LSP budget β first prototype of #449 slice 2** (refs #449) β every concurrent pi-lens process (main session + subagents + concurrent worktrees) has always spawned its own independent LSP fleet with no awareness of any other instance's load; #449 slice 1 (#474/#475) made instances mutually visible via `~/.pi-lens/instances.json` but changed no behavior. This is the first slice that acts on that visibility: a new `clients/lsp-budget.ts` sums live LSP server counts (`lspChildren.length`) across every registry entry whose owning pid is still alive (reusing `instance-reaper.ts`'s existing `realIsPidAlive` liveness check β now exported β rather than a second one) and compares against a configurable ceiling, default 16 (`DEFAULT_LSP_BUDGET_CEILING`, `PI_LENS_LSP_BUDGET_CEILING` override) β a rough RAM-budget estimate (~250MB average per LSP child Γ ~4GB target machine-wide budget), not yet a measured figure (#620's CPU/RSS sampling hadn't landed as of this prototype). When the machine is at or over the ceiling, the NEW session (never an already-running one) degrades its own spawn plan for the rest of that session: it skips spawning auxiliary LSP servers (`role:"auxiliary"` in `clients/lsp/server.ts` β opengrep/ast-grep/zizmor/typos/marksman) and keeps only the primary language server per file, wired via `clients/dispatch/auxiliary-lsp.ts`'s `enabledAuxiliaryLspServerIds`. This is deliberately a "position limit, not a clearinghouse" (the issue's own framing): the check (`checkCrossProcessLspBudget`) fires fire-and-forget from `session_start` alongside the existing `registerInstance`/`sweepOrphans` calls, reads the registry once, decides locally, and never blocks session start or touches another instance's live servers. Feature-flagged: `PI_LENS_CROSS_PROCESS_BUDGET=0` disables the whole check (today's behavior β always spawn the full fleet). Explicitly NOT implemented in this first cut (documented follow-up): the issue's other suggested degrade mechanism (shortening this session's own idle-reaper timeout) and slice 3 (same-workspace warm attach via the MCP IPC channel) β both remain open. This is a first prototype per the issue's own "gated on registry dogfooding" framing; the ceiling number and the choice of degrade mechanism may both need tuning once real-world multi-agent data (#620) is available. New tests: `tests/clients/lsp-budget.test.ts` (pure `decideLspBudget` decision logic against fake registry data β under/at/over ceiling, dead-parent instances excluded from the live count, env-config parsing with NaN guards, module-scope cache defaults).
|
|
24
|
+
- **`lens_diagnostics mode=full` breaks its confirmed/unconfirmed LSP-sweep tally down per primary server, and separates primary-vs-auxiliary findings** (closes #646) β `tools/lsp-diagnostics.ts` already split every file's result into "primary confirmation" (the real language server, e.g. typescript/pyright) vs "auxiliary findings" (cross-cutting scanners attached via `clientScope: "all"` β ast-grep, opengrep, zizmor, typos, marksman, ...) via a local `primaryServerId(filePath)` helper, but `lens-diagnostics.ts`'s `formatFullMode` had no equivalent: its `confirmedLspResults`/`unconfirmedLspResults` tally (#630/#634) was one flat aggregate with no per-server breakdown. Concretely painful during dogfooding: a real sweep came back 34/155 files unconfirmed, and diagnosing WHICH server was responsible required manually grepping `latency.log` for file extensions instead of the tool just reporting it β it turned out to be 100% one push-only server (marksman). Fix: `primaryServerId` moved out of `tools/lsp-diagnostics.ts` into `clients/lsp/config.ts` (alongside `getServersForFileWithConfig`, which it already depended on) so both tools share the exact same primary/auxiliary classification instead of `lens-diagnostics.ts` growing a second hand-copied version β `lsp-diagnostics.ts` now imports the shared helper with no behavior change. `formatFullMode` gains `tallyLspByPrimaryServer` (groups the existing confirmed/unconfirmed partition by each file's primary server id) and `tallyLspPrimaryVsAuxiliary` (splits the raw per-file diagnostics into primary vs auxiliary counts, the same separation `lsp_diagnostics` already applies to its own findings rendering) β both computed from the raw LSP-sweep results, so neither disturbs the existing confirmed/unconfirmed merge into widget-state summaries or any #634 false-clean-prevention semantics. The rendered note now reads e.g. `"β LSP sweep: 121 file(s) confirmed via LSP, 34 unconfirmed (...) β by server: marksman: 0/34, typescript: 121/121 β NOT the same as 0 diagnostics for: ..."` (breakdown clause only rendered when more than one server is involved) plus a new `"LSP sweep findings: N primary (language server), M auxiliary (ast-grep/opengrep/zizmor/typos/marksman/...)"` line; `details` gains `lspServerBreakdown` (always present, per-server `{confirmed, total}`), `lspPrimaryDiagnosticsCount`, and `lspAuxiliaryDiagnosticsCount` so a caller can check this programmatically. New tests: `tests/clients/lsp/primary-server-id.test.ts` (the shared helper matches the old inline `lsp-diagnostics.ts` implementation exactly across a plain source file, a markdown/marksman file, an unmatched extension, and an auxiliary-only case); `tests/tools/lens-diagnostics.test.ts` gains coverage for the per-server breakdown grouping correctly across two different primary servers (mixed confirmed/unconfirmed), the breakdown clause being omitted for a single-server sweep, and the primary/auxiliary findings split.
|
|
25
|
+
- **`lens_diagnostics mode=full` no longer renders a timed-out (or errored) per-file LSP check as false-clean** (closes #630) β `formatFullMode`'s (`tools/lens-diagnostics.ts`) footer-cache reconciliation already excluded `timedOut`/`error` results (#571: `diagnostics` is a default-EMPTY placeholder for these, not a confirmed clean β see `LSPWorkspaceDiagnosticResult`'s doc comment in `clients/lsp/index.ts`), but the merge feeding the actually-rendered summary was NOT filtered the same way: the unfiltered `lspResults` went straight into `mergeDiagnosticsWithWidgetSummaries`, so a timed-out file's placeholder `[]` merged in and read to the agent as "0 diagnostics" β indistinguishable from a genuinely clean file. This is the exact #533/#570 false-negative class (`tools/lsp-diagnostics.ts`'s `confirmation: "clean"|"unconfirmed"` machinery already protects that tool against it) that `mode=full`'s merge path never got. Fix: `lspResults` is now partitioned once into `confirmedLspResults`/`unconfirmedLspResults`, both the footer write and the merge now consume only `confirmedLspResults` (a file already fed the footer-write filter is now also fed the render/details filter from the SAME partition, rather than two independently-maintained filters), and an unconfirmed file can still legitimately show findings from `widgetSummaries`/project-runner state if those independently have entries for it β only its LSP-sweep contribution is withheld. The rendered output gains a new note (mirroring the spirit, not the literal function, of `lsp-diagnostics.ts`'s `unconfirmedReasonClause`/`tallyConfirmation` β the two tools' output shapes already differ): `"β LSP sweep: N file(s) confirmed via LSP, M unconfirmed (...): <paths>. NOT the same as 0 diagnostics for these files"`, distinguishing a timeout from a hard error same as #570 does upstream, composed alongside the existing `coldNote`/`abortedNote`/`freshNote`/`missingNote` in both the aborted and non-aborted return branches. `details` gains `lspFilesConfirmed`, `lspFilesUnconfirmed`, and `unconfirmedLspFiles` (always present, even when zero, matching the existing `coldRunners`-always-in-details convention) so a caller can check this programmatically without re-parsing the text. Deliberately NOT touched: the #611 tsserver-sync escape hatch (that's `lsp_diagnostics`-specific single/batch-check machinery; adding a second per-file round trip to a project-wide sweep here would reintroduce the extra-round-trip problem #629 is fixing on the other tool) β this fix only reclassifies/reports the EXISTING `timedOut` signal `runWorkspaceDiagnostics` already produces, no new LSP calls. New tests in `tests/tools/lens-diagnostics.test.ts`: a mixed confirmed-clean/confirmed-with-diagnostics/timed-out sweep result where the timed-out file is asserted to be listed as unconfirmed (not clean, not silently dropped) while the footer-write exclusion (#571) is confirmed unaffected; an errored (not timed-out) result distinguishing the note's wording; and an all-confirmed sweep confirming no unconfirmed note/zeroed details fields appear when there's nothing to report.
|
|
26
|
+
- **`lsp_diagnostics` directory-mode file cap raised from 50 to 100**, matching the explicit `paths` batch cap (`MAX_BATCH_FILES`) β `tools/lsp-diagnostics.ts`'s `MAX_FILES` constant now reads `100`. There was no longer a reason for the two caps to diverge: dogfooding confirmed the tool's bounded-concurrency worker pool (default 8) stays fast and timeout-free at 100 files on a real ~150-file project, so directory mode was capping well below what the underlying mechanism can already handle cleanly.
|
|
27
|
+
- **`lsp_diagnostics`/`lens_diagnostics` can now render a genuinely confirmed "clean" for classic `typescript-language-server`, not just "unconfirmed"** (refs #611 β read-only-diagnostics scope; the per-edit dispatch consumer is a separate follow-up, see below) β classic `typescript-language-server` is `silentOnClean: true` (`clients/lsp/server-strategies.ts`): on a clean file it publishes nothing at all, not even an empty confirmation, so per #533/#570 pi-lens's honest response was to render ANY empty result from it as `"unconfirmed"` β a single-file `lsp_diagnostics <clean-file.ts>` call could never say "clean," no matter how long you waited. `tools/lsp-diagnostics.ts`'s `classifyEmptyResult` path now attempts one more thing before giving up: when `classifyCascadeWaitTier` (`clients/lsp/cascade-tier.ts`) says the primary server is `"tier3-silent"` (push-only + `silentOnClean` + NOT native-ts7, which already publishes on clean per #558 and never reaches this branch), it calls `typescript-language-server`'s `typescript.tsserverRequest` escape hatch via the existing #237-hardened `LSPService.executeCommand` (allowlisted by advertisement) with `semanticDiagnosticsSync`/`syntacticDiagnosticsSync` β genuine synchronous tsserver request/response commands, not push/timing-dependent. An empty response body is now a confirmed "clean"; a non-empty body surfaces real diagnostics tsserver had already computed but never published, merged into the result rather than discarded. Verified live against the actual installed `typescript-language-server`/`typescript@5.9.3` (this repo's own `tsconfig.json` as the fixture project, plus a deliberately broken scratch file): the response envelope is `{executed:true, result:{seq,type:"response",command,request_seq,success,body:[...]}}`, where each `body` entry is tsserver's NATIVE protocol diagnostic shape (`message`, `category` "error"/"warning"/"suggestion", `code`, `startLocation`/`endLocation` as 1-based `{line,offset}`) β NOT the LSP `Diagnostic` shape, so a new converter (`tsserverSyncDiagnosticToLsp`) maps it to pi-lens's 0-based `LSPDiagnostic`. Also verified live: a file the client never opened (outside any tsconfig project) makes `executeCommand` reject with a tsserver `ResponseError` ("No Project.") rather than a `success:false` response β every failure mode (command not advertised, `executeCommand` throwing or timing out, a malformed response shape) falls back to exactly today's `"unconfirmed"` behavior, fail-safe and non-throwing. New tests in `tests/tools/lsp-diagnostics.test.ts` (`#611 tsserver sync escape hatch`) cover confirmed-clean via the sync path, real diagnostics surfaced via the sync path (with the 1-basedβ0-based location conversion asserted), fallback-to-unconfirmed on a rejected `executeCommand` call, fallback when the command isn't advertised, fallback when the service exposes neither method at all, and that a `"waits"`-tier server (covering native-ts7) never even attempts the sync path. The higher-stakes per-edit dispatch consumer (`clients/dispatch/runners/lsp.ts`/`clientWaitForDiagnostics` in `clients/lsp/client.ts`) is explicitly NOT touched by this change β it runs on every edit and #611 calls for its own dedicated latency verification (`semanticDiagnosticsSync` blocks on tsserver's real analysis queue, so a backlogged server could see no speed win, only a correctness one) before it's worth attempting there.
|
|
28
|
+
- **`lens_diagnostics mode=full` runs gitleaks on any git repo, not only ones with an explicit gitleaks config** (#608, dogfooding finding) β session_start and per-edit dispatch keep #130's strict opt-in gate (`GitleaksClient.hasGitleaksSignal`: a `.gitleaks.toml`/`.gitleaksignore`/package.json reference/pre-commit hook) unchanged, since that issue explicitly weighed and rejected looser tiers for the routine/low-noise path. But `mode=full` is an explicitly-requested comprehensive review, and #130's own writeup considered β but didn't ship β a "smart-default" tier: fire on any tracked git repo, since gitleaks is cheap (~10MB binary, no external DB pull, unlike trivy's 30-200MB vuln-DB download which genuinely needs consent) and its findings are advisory-only. New `GitleaksClient.hasGitRepo`/`hasGitRepo()` (`clients/gitleaks-client.ts`) checks for a `.git` entry (file or directory, so both a normal clone and a worktree's gitdir-pointer file count); `GitleaksClient.scan()` gains an optional `{ requireSignal: false }` to skip the strict gate when the caller has already applied a looser one. `clients/project-diagnostics/fresh-fetch.ts`'s gitleaks task now gates on `hasGitRepo` instead of `hasGitleaksSignal` and passes `requireSignal: false`. `trivy`/`govulncheck`/`dead-code` are unaffected β trivy's gate is a genuine download-consent boundary (unchanged), and govulncheck/dead-code are gated by hard language applicability (no `go.mod`/no Python files), not a policy preference, so there was nothing to loosen there. New tests: `tests/clients/gitleaks-client.test.ts` (`hasGitRepo` against a directory `.git`, a worktree file `.git`, and no `.git`; `scan()`'s default-strict vs. `requireSignal:false` behavior), `tests/clients/project-diagnostics/fresh-fetch.test.ts` (gitleaks now fires on a bare git repo with no explicit gitleaks marker, and still fires when both are present).
|
|
29
|
+
- **A genuine `silentOnClean` drift finding now files/updates a persistent tracking issue instead of only riding along in the routine docs-refresh PR** (closes #594) β the nightly `tool-smoke` workflow's `probe-clean-signal.mjs` step (#529) already computed `driftWarnings` (a mismatch between an observed LSP server's clean-scan behavior and its hand-set `silentOnClean` marker in `clients/lsp/server-strategies.ts`) but only logged it to the step's stdout and a `## silentOnClean drift` footnote in `docs/lsp-capability-matrix.md` β both of which only surface via the existing `bot/lsp-docs-refresh` auto-PR, which looks identical to a no-op capability refresh, so nobody had a reason to actually check it. `probe-clean-signal.mjs` now also writes `driftWarnings` as a small JSON summary to a fixed path (`scripts/lib/clean-signal.mjs`'s new `DRIFT_SUMMARY_PATH`, a same-job runner-tmpdir file, never committed), and a new workflow step runs a new script, `scripts/notify-clean-signal-drift.mjs`, which reads that summary and files-or-updates a SINGLE persistent GitHub issue (a fixed `nightly-drift` label + fixed title, found by title match among open issues β never a new issue every night, which would spam) when there's a real finding, and closes a prior open one (with a self-resolved comment) once a nightly run finds no drift. This is purely additive: the probe's own drift-detection logic, its "telemetry only, never a CI gate" guarantee, and the existing docs-footnote/auto-PR behavior are all unchanged. The new step is `continue-on-error: true` (mirroring every other best-effort step in this workflow) and the script itself never exits nonzero on an internal error either β filing/updating/closing an issue is a side effect, not a build gate. Auth reuses the job's existing `GITHUB_TOKEN` via `gh` (the same token the docs-refresh PR step already receives), so the job's `permissions:` block gains `issues: write` alongside its existing `contents`/`pull-requests` write grants β no new auth plumbing. Pure body-building/lookup helpers live in `scripts/lib/drift-issue.mjs` (`buildDriftIssueBody`, `findDriftTrackingIssue`), unit-tested in `tests/scripts/drift-issue.test.ts`; the `gh` CLI shell-outs themselves are untested, matching the existing pattern for `scripts/backfill-github-releases.mjs`'s own `gh` calls elsewhere in this repo.
|
|
30
|
+
- **opengrep moved off the full-workspace LSP sweep onto a dedicated CLI extractor** (closes #584) β `runWorkspaceDiagnostics` (`lens_diagnostics mode=full` / `lsp_diagnostics` full-workspace scans) hardcodes `clientScope: "all"` per file, and #387's deliberate single-flight-per-server serialization meant every file paid opengrep's full per-server wait-tier budget (up to 3500ms) one at a time within its server group β on a real 50-file sweep this produced 49/50 files reporting "unconfirmed (timed out)". opengrep has no `workspace/diagnostic` pull support (push-only, `docs/servercapabilities.md`) and `reopenOnResync: true` (`clients/lsp/server-strategies.ts`) means every LSP touch already forces a full re-scan of that one file anyway, so there's no incremental efficiency lost by moving it off the per-file path for bulk scans. New `clients/opengrep-client.ts` (`OpengrepClient`, mirrors `GitleaksClient`/`TrivyClient`): a single project-wide `opengrep scan --config <local rule file | auto> --json` CLI invocation, config resolution reused from the existing `resolveOpengrepConfig` (same rule-choice logic the LSP server itself uses), parsed via `parseOpengrepReport` (verified against the real installed opengrep 1.25.0 binary's JSON schema β semgrep-compatible but with some CLI-surface drift, e.g. `--files-with-matches` requires `--experimental` where semgrep's doesn't). Wired into `scheduleStartupScans` (`clients/runtime-session.ts`) on the same session-start/cached cadence as knip/jscpd/gitleaks, and into the `project-diagnostics` extractor registry (`clients/project-diagnostics/extractors.ts` + new `runner-adapters/opengrep.ts`) so `lens_diagnostics mode=full` reads its cached findings β `ERROR` severity maps to a blocking diagnostic (mirrors gitleaks secrets), `WARNING`/`INFO` to advisory. `runWorkspaceDiagnostics`'s per-file sweep now explicitly excludes the opengrep server (`clients/lsp/index.ts`'s new `excludeServerIds` touch option / `WORKSPACE_SWEEP_EXCLUDED_SERVER_IDS`) β verified every extension opengrep covers (`OPENGREP_KINDS`) already has a dedicated primary LSP server (plus the `typos` auxiliary, which attaches to the same extension set), so no file loses sweep coverage from opengrep's removal. The per-edit real-time LSP path (`clientScope: "primary"`/`"with-auxiliary"`) is completely untouched β opengrep still attaches there exactly as before. Rebased onto #587 (`applyAuxiliarySuppressions` wired into `runWorkspaceDiagnostics`) β both changes coexist: opengrep is excluded from the sweep's per-file touch, and whatever else still flows through it (ast-grep, typos, β¦) still gets suppression-filtered. `// nosemgrep`/`# nosemgrep` needed NO equivalent filtering added to the new CLI extractor β verified empirically (not assumed) against the real installed opengrep 1.25.0 binary that, unlike opengrep's LSP mode (which does NOT honor it natively, the reason `isNosemgrepSuppressed`/`applyAuxiliarySuppressions` exist at all, #441/#586/#587), the CLI `scan --json` path suppresses `nosemgrep`-annotated findings itself before they reach `--json` output. Verified empirically: a real `opengrep scan --config auto` run against this repo's `clients/` directory (277 files, 1074+ community rules) completed in ~19s as a single process β versus the old per-file approach's worst case of 277 individual LSP touches each up to a 3500ms timeout ceiling.
|
|
31
|
+
- **`lens_diagnostics mode=full` now fetches the heavyweight project analyzers FRESH instead of reading a possibly-hours-stale cache** (#585) β `mode=full`'s `refreshRunners=cached/cheap/all` used to fold in knip/jscpd/madge/gitleaks/govulncheck/trivy/dead-code findings via `extractCachedProjectDiagnostics`, a deliberately cache-only read (per its own header comment) because relaunching those analyzers concurrently with a `session_start` background scan of the same tool could double-spawn a CPU-bound process. That prerequisite is now satisfied for all three previously-unguarded clients β `gitleaks-client.ts`, `govulncheck-client.ts`, `trivy-client.ts` β which already shared `SecurityScanClient`'s `dedupeScan` in-flight guard (landed in #313, verified before wiring this up rather than re-adding it), the same pattern `KnipClient`/`JscpdClient`/`DeadCodeClient` use. New `clients/project-diagnostics/fresh-fetch.ts` (`fetchFreshProjectDiagnostics`) mirrors each analyzer's `session_start` gating (`clients/runtime-session.ts`) but always performs β or, via the de-dupe guard, *joins* β an actual run instead of skipping on a cache hit, running all analyzers in **parallel** via `Promise.all` so total wait is bounded by the single slowest one (trivy's own ~180s ceiling) rather than their sum; every fresh result is written back to cache via the same `cacheManager.writeCache` `session_start`/`turn_end` use. No extra write-ordering guard (`clients/write-ordering-guard.ts`) was added on top β an overlapping call for the same analyzer/root always resolves to the exact same in-flight promise, so concurrent writers are always writing identical data, not racing a stale write over a fresher one. `tools/lens-diagnostics.ts`'s `formatFullMode` now calls `fetchFreshProjectDiagnostics` (via the process-wide `loadBootstrapClients()` singleton, so a fresh-fetch racing session_start/turn_end shares client instances and thus in-flight runs) in place of the old cache-only extractor, and the output now notes per-analyzer elapsed time ("fetched fresh this call: knip (1597ms), jscpd (4242ms), madge (11256ms)") alongside the existing cold-analyzer honesty note (now "not applicable / unavailable this run" rather than "not yet scanned this session", since every requested analyzer is now actually attempted). `session_start`'s and `turn_end`'s own scheduling (still skip-if-cached) and per-edit dispatch are unchanged β this is additive and `mode=full`-only. **Abort handling** (found in review before merge): `formatFullMode` already threads a combined Escape/turn-abort + hard wall-clock-ceiling signal (`FULL_SCAN_WALL_CLOCK_MS`) into the LSP sweep and cheap project-runner scan, but the initial fresh-fetch wiring left it unthreaded into `fetchFreshProjectDiagnostics` β an Escape or ceiling-fire would correctly stop the rest of the scan while the analyzer fresh-fetch kept running uncancelled for up to trivy's own ~180s ceiling before the tool call could return. None of the six analyzer clients accept a cancellation token (checked each `analyze()`/`scan()` signature β none does), so `fetchFreshProjectDiagnostics` now takes an optional `signal` and races the overall `Promise.all(tasks)` against it, returning whatever has already settled rather than cancelling in-flight spawns β the same "partial is OK, a hang is not" shape `clients/deadline-utils.ts`'s `withDeadline(..., onTimeout: "undefined")` and `clients/lsp/index.ts`'s `runWorkspaceDiagnostics` already use; already-spawned processes keep running in the background (bounded by their own per-tool timeout) and still populate the cache for the next caller. Analyzers still in flight when the abort fires are reported via a new `aborted`/`abortedIds` result field, folded into `cold` (so they never silently read as "ran clean") but surfaced in the tool's text as a distinct "stopped mid-scan" note rather than conflated with the "not applicable to this project" cold note. `tools/lens-diagnostics.ts` passes the same `options.signal` it already hands the LSP sweep. Verified empirically against this repo: two successive fresh-fetch calls each took ~11s (not a cache hit) and the knip cache's `meta.timestamp` advanced between them; two concurrent fresh-fetch calls produced identical results confirming the in-flight guard held; a fresh-fetch given a 500ms abort signal returned in ~511ms with `aborted: true` and the correct still-in-flight analyzer ids, instead of the ~11s+ a full run takes. New tests: `tests/clients/gitleaks-client.test.ts`/`govulncheck-client.test.ts`/`trivy-client.test.ts` each gain a de-dupe regression test mirroring `knip-client.test.ts`'s existing pattern (two concurrent `scan()`/`analyze()` calls against the same root spawn exactly one underlying run); `tests/clients/project-diagnostics/fresh-fetch.test.ts` covers per-analyzer gating, cache-key selection (jscpd's TS-project variant, dead-code's per-language keys), a timing-based regression guard proving the analyzers run in parallel, and an abort-mid-scan test confirming a prompt partial return; `tests/tools/lens-diagnostics.test.ts` updated to mock the new `fetchFreshProjectDiagnostics`/`loadBootstrapClients` seams instead of driving the old cache-only path, plus new coverage confirming the abort signal is the SAME instance the LSP sweep receives and that an aborted fresh-fetch renders its own distinct note.
|
|
32
|
+
- **`module_report` flags middle-man / delegate-only classes** (#325, split from #305) β Fowler's "Middle Man" smell (a class whose methods do nothing but forward to one held field) is a whole-class, *universal-quantification* judgment ("EVERY method forwards") that ast-grep's existence matching ("this class *has* a delegate method") can't soundly express without flooding on legitimate forwarding layers, so it's implemented as a structural pass over the already-extracted outline instead of an ast-grep rule. New `clients/middle-man-analysis.ts`: per class, computes a delegation ratio β the share of real methods (accessors and constructors excluded) whose entire body is a single pure-forwarding call (`return this.field.method(...)`, or the same shape without `return` for void methods) to ONE held field β and flags the class (`flags: ["middle man"]`, plus a `delegationRatio` field) only when that ratio clears 90% *and* the class isn't a named facade/adapter/proxy/wrapper/decorator (substring guard on the class name) *and* doesn't structurally `implements` an interface (a legitimate reason for near-total forwarding). Additional false-positive guards: too few methods to judge (<2) never flags, forwards split across more than one delegate field never flags ("one held field" per the issue), and a call that transforms/reorders its arguments doesn't count as pure forwarding. Wired into `moduleReport` (`clients/module-report.ts`) right after member nesting, so the flag rides the same `flags[]`/`delegationRatio` surface `pilens_module_report`/`module_report` already use for "high fanout"/"high complexity". v1 scope: typescript/tsx/javascript, java, kotlin, csharp, swift, dart, python, ruby, rust, php (languages with a deterministic self-reference token and dot-based member access); go/C++ are left for a follow-up since neither has a text-resolvable self-token/access-operator without real AST access. New fixture-driven test suite (`tests/clients/middle-man-analysis.test.ts`) exercises the flag end-to-end through `moduleReport`, with explicit non-flagging fixtures for each guard (named adapter/facade/proxy/wrapper, typed-interface adapter, split-field forwarding, argument-transforming forwarding, and a small class too tiny to judge) alongside positive TS and Python fixtures.
|
|
33
|
+
- **Retroactive changelog entry: `runtimeInstall` + canonical-bin discovery for gopls/csharp-ls/fsautocomplete (Go + .NET slice of #241)** β `ensureTool()` could only auto-install servers in the plain npm/pip/gem/GitHub/maven/archive registry, so toolchain-managed LSP servers stayed PATH-only. `gopls` (`b348ac46`) and `fsautocomplete` (`34427c69`, alongside `csharp-ls`) now use `resolveAndLaunch`'s `runtimeInstall` hook: when the owning runtime (`go` / `dotnet`) is on PATH, pi-lens runs the canonical install (`go install golang.org/x/tools/gopls@latest`; `dotnet tool install --tool-path <pi-lens bin> csharp-ls`/`fsautocomplete`) β never the runtime itself β and falls back to "unavailable" otherwise. New canonical-bin discovery (`goBinCandidates`/`dotnetToolCandidates` in `clients/lsp/server.ts`) also resolves an already-installed server that landed in `$GOPATH/bin` (or `~/go/bin`) or `~/.dotnet/tools` even when that directory isn't on the user's shell PATH, with the bare command tried first so PATH stays authoritative. `rust-analyzer` got the same `cargoBinCandidates` (`$CARGO_HOME/bin`/`~/.cargo/bin`) treatment as a byproduct. This slice was deliberately narrowed to Go + .NET, the two toolchains with a mainstream user base β sourcekit-lsp/haskell-language-server/ocamllsp/nixd remain out of scope and #241 stays open for them. Covered by `tests/clients/lsp/runtime-install-discovery.test.ts` (mocked `launchLSP`/`ensureTool`, no real `go install`/`dotnet tool install` shells out in tests).
|
|
34
|
+
|
|
35
|
+
- **Nightly compat-smoke now pins avtc-pi-subagent's env vocabulary too** (#518, refs #507/#508/#476) β the subagent-extension compat smoke's Layer A already pinned `nicobailon/pi-subagents`' spawn-env contract but had no equivalent guard for the second vocabulary `subagent-mode.ts` detects (`PI_SUBAGENT_CHILD_AGENT` + `PI_SUBAGENT_PARENT_PID`, added in #507). `scripts/lib/compat-contracts.mjs` gains `checkAvtcChildEnv`, a resilient pattern check (grep-verified against the published `avtc-pi-subagent@1.0.3` source, `src/process-runner.ts`) asserting BOTH env-var assignments still exist β mirroring `checkNicobailonChildEnv`'s shape; `scripts/compat-contracts.mjs` now also npm-installs and reads `avtc-pi-subagent`. Layer B (`scripts/compat-smoke-behavioral.mjs`) gains two new behavioral assertions: an avtc-only PAIR (no `PI_SUBAGENT_CHILD`) correctly engages light mode, and the inverse guard β a LONE avtc var (just `PI_SUBAGENT_CHILD_AGENT`, no `PI_SUBAGENT_PARENT_PID`) correctly does NOT, the specific false-positive-protection edge case `subagent-mode.ts`'s doc comment calls out as required. `docs/subagent-compat.md` updated to reflect avtc-pi-subagent as a fully Layer A + Layer B covered contract rather than a deferred gap.
|
|
36
|
+
- **Regression coverage for confusable-hyphen normalization in the read-guard's content comparison** (refs #505) β #505 bundled two items: a did-you-mean suggestion (already shipped) and "Unicode confusable-hyphen normalization before content comparison" (U+2010/2011/2012/2013/2014/2212 -> ASCII hyphen), comparison-only, never applied to written content. Investigating this bundled item found it was already delivered, under a different name, by the host-alignment normalization from #257: `normalizeForGuardMatch` (`clients/host-edit-normalize.ts`) folds `HOST_UNICODE_DASHES` (U+2010, U+2011, U+2012, U+2013, U+2014, U+2015, U+2212 -> ASCII `-`) β exactly the six codepoints #505 names, plus U+2015 β and is precisely the `normalizeContent` that `resolveOldTextEdits` (`clients/read-guard-tool-lines.ts`) uses on its *primary* oldText->range match, ahead of the Tier A/B/C autopatch fallbacks. No production normalization code was added (a second, divergent mechanism would only duplicate #257's). Added: explicit tests pinning this behavior under the #505 framing (`tests/clients/read-guard-tool-lines.test.ts` β ASCII oldText vs. each of the six confusable dashes in file content and vice versa; a hyphen-only difference does not mask an otherwise-different line; a genuine unrelated mismatch still blocks) and a written-content-preservation test (`tests/clients/partial-edit-apply.test.ts` β a newText containing a deliberate em-dash is written to disk byte-for-byte, confirming the normalization never leaks into what gets written), plus a doc comment on `normalizeContent` cross-referencing #505 for discoverability.
|
|
37
|
+
- **Persistent `bus-events.log` for `pilens:files:touched`/`pilens:diagnostics` publish outcomes** β both `pi.events` bus producers (`clients/bus-publish.ts` #482, `clients/diagnostics-publish.ts` #502) are fire-and-forget: on failure or a structural no-op (never wired, kill switch off) they only invoked an optional `dbg` callback, which is a documented no-op in the MCP host (`clients/mcp/session.ts`'s `dbg: noop`) β the same failure shape as the #544 MCP `session_start` incident, just for the bus instead. `clients/bus-events-logger.ts` now writes a small NDJSON summary line (`~/.pi-lens/bus-events.log`, house pattern from `clients/latency-logger.ts`) for every meaningful publish outcome: `emitted` (with a file/diagnostics count and, for diagnostics, the monotonic `seq`) and `emit_failed` (with the error) are logged on every call; `skipped_unwired` and `skipped_disabled` are logged once per process (they're static, session-lifetime facts β wiring happens once at extension factory time, the kill switch is a startup env read β so logging them per publish attempt would spam an identical line with no new information for the life of a long MCP session). The empty-batch no-op is not logged at all (every call site already guards against calling with nothing to report). The existing `dbg` callback contract is unchanged β this is additive, not a replacement.
|
|
38
|
+
- **MCP auto session_start is now visible and self-healing** (#544) β `PI_LENS_MCP_AUTO_SESSION=1`'s self-triggered `session_start` on `initialize` previously only logged to stderr (`console.error`), which Claude Code never surfaces, and had no retry if it never fired or threw before completing β a real incident left a long-lived MCP connection cold for its whole lifetime with no way to tell short of `claude --debug` log spelunking. `mcp/server.ts` now tracks `{ attempted, succeeded, firedAt, error }` state through `maybeAutoSessionStart()` instead of a bare fired boolean, and `pilens_health` surfaces it as an `autoSession` field (`null` when `PI_LENS_MCP_AUTO_SESSION` isn't set at all, distinguishing "feature off" from "attempted and failed"). Self-heal: the first `tools/call` on a connection now also invokes `maybeAutoSessionStart()`, which re-triggers `runSessionStart` if it never attempted, is still in flight, or previously failed β guarded so it's a no-op once a run has already succeeded (never re-runs session_start on every tool call).
|
|
39
|
+
- **`read_symbol` self-healing misses + doc-comment inclusion + duplicate-name disambiguation** (#523, both the pi tool and its `pilens_read_symbol` MCP mirror) β a 2026-07-11 dogfooding assessment found a miss cost an extra round-trip (miss β `module_report` β retry) for what's usually a typo or a qualified name, and that the returned body excluded an attached doc comment even though an agent reading a symbol to edit it needs the contract above it. Four changes, same `readSymbol` (`clients/module-report.ts`): (1) **doc-comment inclusion** (issue author's own follow-up: "probably the highest-value item") β the returned range now starts at an attached doc comment's start line rather than the declaration line, reusing #517's `extractDocCommentInfo` position-based, blank-line-gap-aware attachment computation (a new `docStartLine` field on `Symbol`, alongside the existing `doc` summary); the read-guard coverage recorded for the read (`tools/module-report.ts`'s `recordSymbolRead` tie-in) is extended to match, so editing only the doc comment on an already-read symbol is not wrongly zero-read/out-of-range-blocked. A symbol with no attached comment, or one separated by a blank-line gap, is unaffected. (2) **did-you-mean on miss** β a miss embeds the ~3 nearest symbol/callback names in the file (drawn from the same extraction data `module_report` already builds, no re-parse) via a small dedicated Levenshtein (character edit-distance) similarity function, threshold 0.45 on a normalized 0β1 score, so a typo self-corrects in one turn. Deliberately NOT built on the read-guard's `findSimilarLines`/`tokenSimilarity` (#505) β that does Jaccard similarity over whitespace-tokenized *line content* for relocated-block suggestions, a different comparison shape (a single identifier is one token to it, so a one-character typo on a long name scores 0); no existing levenshtein/editDistance utility was found elsewhere in the codebase. (3) **`Class.method` qualification** β `symbol` accepts a dotted name resolving to a member via line-range containment within the named parent (the same containment shape #301's member nesting uses for the outline, computed directly here since `readSymbol` works off the flat extractor list); an unresolved qualifier (unknown parent, or no matching member) falls through to the plain unqualified lookup and then the did-you-mean miss path, never a crash. (4) **duplicate-name disambiguation** (lowest priority per the issue) β when multiple same-file symbols share the requested name (overloads, an interface and a function sharing a name), the first match is still returned by default (unchanged behavior) but the response now sets `ambiguous: { count, kinds }` and the tool/MCP text notes it; a new optional `kind` parameter picks a specific match.
|
|
40
|
+
- **Shared `makeRunnerCtx()` test helper for dispatch-runner tests** (#187, Tier 2 follow-up to #171) β #171 consolidated the three parallel `ExtensionAPI` mocks onto `tests/support/pi-mock.ts`, but the unrelated `DispatchContext` shape (`clients/dispatch/types.ts`) used by `tests/clients/dispatch/runners/*.test.ts`/`dispatch/rules/*` stayed fragmented: ~26 files each hand-rolled their own local `createCtx(filePath, cwd)`. New `tests/support/runner-ctx.ts` exports `makeRunnerCtx(filePath, cwd, overrides?)`, typed against the real `DispatchContext` and filling in the fields runners actually read (`kind: "jsts"`, `fileRole: "source"`, `autofix: false`, `deltaMode: true`, a fresh `FactStore`, `hasTool` resolving `true`, no-op `log`), with per-test overrides merged on top. `tests/support/runner-ctx.test.ts` covers the helper itself. `ruff.test.ts`, `oxlint.test.ts`, and `biome-check-runner.test.ts` are migrated as the template (pure test-setup refactor, no assertion changes); the remaining ~23 bespoke `createCtx` blocks are tracked in #187 for opportunistic follow-on migration. `AGENTS.md` gains a "Testing dispatch runners (#187)" note pointing future runner tests at the helper.
|
|
41
|
+
- **Auto-install lua-language-server via the archive-tree bundle machinery** (#564, split from #241) β reuses the auto-install path built for clangd: a new `lua-language-server` `ArchiveSpec` in `clients/installer/index.ts` (platform/arch URL resolver over LuaLS's GitHub releases, verified against the live 3.18.2 release asset listing rather than guessed) and `LuaServer.spawn` (`clients/lsp/server.ts`) converted from plain PATH-only `createInteractiveServer` to `resolveAndLaunchTreeBinary`, same as `CppServer`. Resolution order: a system `lua-language-server` on PATH wins; otherwise the managed bundle is installed (when allowed) and `bin/lua-language-server` launched from within it; neither available degrades gracefully (coverage notice, never a hard failure). Covers darwin/linux Γ x64/arm64 and win32 Γ x64 (LuaLS has no win32/arm64 build). One asset-shape difference from clangd found during verification: LuaLS's release archives have **no wrapping version directory** (`bin/`, `LICENSE`, `locale/` sit at the archive root), so this entry uses `stripComponents: 0` where clangd uses `1`. Covered by `tests/clients/installer/archive-platform-url.test.ts` (URL-resolution matrix) and a new `tests/clients/lsp/lua-tree-binary.test.ts` (mocked `launchLSP`/`getToolPath`/`ensureTool` β PATH-first, fallback to an already-extracted bundle, on-demand install, and graceful skip when `allowInstall` is false; no real network/download in tests).
|
|
42
|
+
- **`lsp_diagnostics` separates primary-server confirmation from auxiliary-scanner findings, plus a new `serverScope` param** (closes #617, dogfooding finding) β `clientScope: "all"` (the tool's default) touches every attached server for a file, including cross-cutting auxiliaries (ast-grep, opengrep, zizmor, typos, marksman) β a real dogfooding run against `pi-drykiss` returned 55 diagnostics that were 54 ast-grep findings and 1 typescript entry, and the agent's own summary of the result glossed over the fact typescript HAD confirmed the file clean (verified after the fact from `latency.log`'s `lsp_diagnostics_aggregate` entries β `typescript: diagnosticCount:0, health:"ok_empty"` on every file) β real signal buried in aux noise, not a data-loss bug. `tools/lsp-diagnostics.ts` now: (1) always reports the file's actual language server's confirmation (clean/N diagnostics/unconfirmed/timed out) on its own line/section (`primaryServerId(filePath)`, keyed off `LSPServerInfo.role !== "auxiliary"`), independent of how many auxiliary findings accompany it, in single-file, batch, AND directory renders (`Primary LSP (typescript): confirmed clean.` vs. a separate `Auxiliary findings (N):` section) β the batch/directory `cleanFiles`/`unconfirmedFiles` tally was ALREADY primary-only (unchanged), only the flat findings listing was unlabeled; (2) a new optional `serverScope: "primary" | "all"` parameter (default `"all"`, preserving today's behavior) that, when `"primary"`, passes `clientScope: "primary"` to the underlying `touchFile` call and skips every auxiliary scanner entirely β for when the caller just wants "does this have real type errors," fast and low-noise. Deliberately NOT a full removal of auxiliary scanning from this tool: `clientScope: "all"` is the only path that surfaces ast-grep/opengrep findings for a file the agent hasn't dispatched through an edit this session (the same run's Semgrep ReDoS false-positive at `glob-utils.ts:77` was caught exactly this way), so the fix separates the two signals instead of dropping one. New tests in `tests/tools/lsp-diagnostics.test.ts`: single-file and directory-mode splitting of mixed-source diagnostics into Primary/Auxiliary sections, `serverScope: "primary"` threading `clientScope: "primary"` into `touchFile`, and the default still passing `"all"`.
|
|
43
|
+
- **`lens_diagnostics mode=full`/`lsp_diagnostics` no longer burns a full per-file wait on EVERY markdown file in a sweep** (closes #645, dogfooding finding against pi-drykiss) β marksman (the markdown LSP, `clients/lsp/server.ts`) is push-only and its value is CROSS-file (broken intra-repo links, missing anchors), which needs its own one-time whole-workspace index build before a push is meaningful; `server-strategies.ts`'s existing 1500ms `aggregateWaitMs` was tuned for a single warm per-edit touch, not a full-tree sweep. A `runWorkspaceDiagnostics` sweep instead fires a `didOpen` for every markdown file in the project, each one independently racing the SAME cold index build β a real ~155-file project with 34 markdown files came back 34/34 `diagnosticsTimedOut:true` (rendered `unconfirmed` per #634, not falsely clean, but ~51s of structurally-doomed wait for zero signal). Fix (option (a)/(c) from the issue, generalized rather than marksman-hardcoded): `DiagnosticStrategy` gains an optional `workspaceIndexing`/`workspaceIndexingWarmWaitMs` pair (`clients/lsp/server-strategies.ts`) marking a push-only server whose value depends on a one-time index rather than a per-file cost; `runWorkspaceDiagnostics` creates one `SweepIndexGate` per sweep call (`clients/lsp/index.ts`) and threads it through every `touchFile` call via a new `sweepIndexGate` option β the FIRST file touching a `workspaceIndexing` server in one sweep still pays the full `aggregateWaitMs` (1500ms), and every subsequent file in the SAME sweep uses the much shorter `workspaceIndexingWarmWaitMs` (250ms for marksman) instead of re-paying the full budget. Deliberately NOT a global/session-lifetime cache β the gate is a plain per-call object, never stored on `LSPService`, so it can't leak into a later sweep or affect a per-edit touch (which never receives one, and therefore always gets the full budget exactly as before β marksman's single-touch per-edit behavior is completely unchanged, per the issue's explicit non-goal). A genuine timeout on a warm-wait touch still reports `diagnosticsTimedOut`/`inconclusive` exactly as before β the #634 unconfirmed-not-false-clean contract is untouched, this only shrinks the wasted wait for files that pile in behind the first one. New tests: `tests/clients/lsp/workspace-diagnostics-sweep-index-gate.test.ts` asserts the first/subsequent wait-budget split directly (the `ms` argument passed to `waitForDiagnostics`), that a genuine later timeout still reports `timedOut: true`, and that a non-`workspaceIndexing` server (python) is unaffected by the gate.
|
|
44
|
+
- **Test-runner support for PHPUnit and mix test (ExUnit)** β `clients/test-runner-client.ts`'s per-language `RUNNERS` table gains two new entries. PHPUnit is detected via `phpunit.xml`/`phpunit.xml.dist` or a `composer.json` `require`/`require-dev` dependency on `phpunit/phpunit`, invoked via a local `vendor/bin/phpunit` binary when present (falling back to a global `phpunit`), and its default text summary (`OK (N tests, M assertions)` / `Tests: N, Assertions: M, Errors: E, Failures: F, Skipped: S.`) is parsed for pass/fail/skip counts and individual failure names. `mix test` (Elixir/ExUnit) is detected via `mix.exs` and invoked as `mix test <testFile>`, with its `N tests, M failures` summary line parsed similarly. Both add a new `SOURCE_TO_TEST_PATTERNS` entry; test-file discovery reuses the mirrored-directory mechanism from #547 (`relativeSourceDir`) rather than a second parallel one, plus a small targeted addition new `sourceRootMirroredCandidates` helper for the one thing that mechanism didn't already cover: stripping a conventional source-root segment (`src`/`lib`/`app`) and mirroring under each pattern's own configured test root (`SOURCE_TO_TEST_PATTERNS[i].dirs`) β PHPUnit's class-name convention (`src/Foo/Bar.php` -> `tests/Foo/BarTest.php`) and ExUnit's basename-suffix convention (`lib/accounts/user.ex` -> `test/accounts/user_test.exs`, whose `test/` root is singular and wouldn't otherwise be checked) are both discoverable.
|
|
45
|
+
|
|
46
|
+
### Performance
|
|
47
|
+
|
|
48
|
+
- **Pinned `jscpd` bumped from `3.5.10` to `5.0.12`** (#582) β the pin dated back to a real v4 *packaging* bug (`reprism`'s `lib/languages/` dir missing from the published tarball), not a compatibility decision, so v5's ground-up Rust rewrite needed independent re-verification rather than a blind bump. Confirmed by actually installing `jscpd@5.0.12` and running it (not just reading the npm page): the published package is correctly shaped (a real per-platform native binary β `jscpd-windows-x64-msvc` etc. β resolved via `optionalDependencies`, no missing-directory regression); every CLI flag `clients/jscpd-client.ts`'s `runScan()` passes (`--min-lines`, `--min-tokens`, `--reporters json`, `--output`, `--ignore`, plus the positional `.` path) still exists with the same meaning; and the JSON reporter's schema is unchanged for every field `parseReport()` actually reads (`statistics.total.duplicatedLines/lines/percentage`, `duplicates[].firstFile`/`secondFile.name`+`.start`, `.lines`, `.tokens`) β verified against both a synthetic two-file fixture and a real scan of this repo's `clients/` directory, including the exact cwd/positional-arg pattern pi-lens spawns with (`cwd` = scanned dir, arg `"."`), so no adapter changes were needed. Behaviorally, v5 found more clones on this repo (128 vs. v3's 97) and computes its `lines`/`percentage` denominator differently (raw physical lines vs. v3's blank/comment-stripped count β a display-only figure; the unused `formatResult()` helper is the only consumer) β noted as a real, if minor, behavior difference rather than glossed over. The core motivation checked out: ~54x faster on this repo's `clients/` directory (jscpd's own reported detection time 4.105s -> 76ms; wall clock ~4.8s -> ~0.4s), meeting the claimed 24-37x. `clients/installer/index.ts`'s stale v3.5.10-justification comment is replaced with what was verified and why v5 was chosen.
|
|
49
|
+
- **`KnipClient.runAnalyze()` now caches between runs** (#580) β `session_start` and every single `turn_end` (`clients/runtime-turn.ts:390`) each spawned `knip` fresh, forcing a full AST-traversal rescan of the whole project on every call. `runAnalyze()` now passes `--cache --cache-location <dir>` (knip's own disk cache, invalidated conservatively via mtime + file size), with `<dir>` routed through `getProjectDataDir(targetDir)` (`path.join(getProjectDataDir(targetDir), "cache", "knip")`) rather than knip's own `node_modules/.cache/knip` default β matching the existing project convention (`cache-manager.ts`, `call-graph.ts`) and covered by the repo-standing `.pi-lens/` gitignore entry, so the cache never risks getting committed. Verified against the installed knip 6.26.0 (no pinned version in `package.json`; resolved via the installer) rather than trusting the docs blindly: found and worked around a real Windows-specific bug where knip's own auto-`mkdir` for a not-yet-existing `--cache-location` directory silently fails (ENOENT, swallowed internally, only surfaced via `--debug`), degrading every run back to an uncached scan with no visible error. `runAnalyze()` now pre-creates the cache directory (`fs.mkdirSync(cacheLocation, { recursive: true })`) before spawning, sidestepping the bug entirely β confirmed end-to-end against this repo: a warm run reused the pre-created cache and was ~47% faster (1.0s vs. 1.9s) than the cold run, with byte-identical JSON output. Caveat carried over from knip's docs (intentionally not auto-handled): a cached run does not detect a newly-added `.gitignore` file β the cache directory must be deleted to pick it up. New test in `tests/clients/knip-client.test.ts` asserts `runAnalyze()`'s spawned args contain `--cache` and the exact `--cache-location` path, and that the directory actually gets created ahead of the spawn.
|
|
50
|
+
- **zizmor is no longer an LSP candidate for non-GitHub-Actions YAML files** (closes #636) β surfaced while investigating a separate, unrelated dogfooding report of a slow ~5s edit on `.github/workflows/ci.yml` (concluded NOT a bug: the deliberate cost of zizmor's online GitHub-API audit, already escapable via `ZIZMOR_OFFLINE=1`). That investigation's sibling finding: `ZizmorServer` (`clients/lsp/server.ts`) declares `extensions: KIND_EXTENSIONS["yaml"]`, so it was a candidate LSP server for EVERY `.yaml`/`.yml` file, not just actual workflow/action files β the header comment already noted zizmor "only ever emits findings for actual workflow/action files... other YAML is a quiet no-op," but that's a claim about zizmor's *output*, not about whether pi-lens still pays the LSP round-trip *cost* for files it can never report on. Verified empirically rather than assumed: installed real `zizmor` (`pip install zizmor`, v1.26.1) and spoke raw LSP over stdio to a `zizmor --lsp` process against a fixture repo β `.github/workflows/ci.yml` got a `publishDiagnostics` in ~113ms, while a plain `docker-compose.yml` got **no `publishDiagnostics` at all**, not even an empty one, within a 5s window. Cross-referenced against `clients/lsp/server-strategies.ts`'s `zizmor` strategy (`seedFirstPush: true`, `aggregateWaitMs: 2000`): since zizmor never publishes anything for a non-target file, `waitForDiagnostics` has nothing to resolve early on and burns its full per-server budget (2000ms, bounded by the per-edit caller cap) on every such touch, for zero signal, on every edit of any non-GitHub-Actions YAML file (docker-compose.yml, Kubernetes manifests, other CI configs, β¦) in any project with zizmor installed. Fix: new optional `LSPServerInfo.pathFilter` hook (`clients/lsp/server.ts`) β an additional, narrowing-only candidacy gate beyond `extensions` β wired into `getServersForFileWithConfig` (`clients/lsp/config.ts`). `ZizmorServer.pathFilter` is `isZizmorAuditTarget` (new export, `clients/zizmor-config.ts`), mirroring zizmor's own input-collection rules exactly: `.github/workflows/*.y[a]ml`, `action.yml`/`action.yaml` (anywhere in the repo β composite actions aren't root-only), and `.github/dependabot.y[a]ml` (GitHub only ever reads that one location, so a root-level `dependabot.yml` deliberately does NOT match). No other server needed this hook β zizmor is the only auxiliary whose extension match is provably broader than its useful path set. New tests: `tests/clients/zizmor-config.test.ts` unit-tests `isZizmorAuditTarget` against workflow/action/dependabot paths (including Windows-separator and absolute-path forms) and common non-matches (docker-compose.yml, k8s manifests, issue-template YAML, a root-level dependabot.yml); `tests/clients/lsp/server-policy.test.ts` exercises the real `getServersForFileWithConfig` end-to-end, proving a workflow file's candidate list includes `"zizmor"` and a plain YAML file's does not (while the primary `"yaml"` language server still attaches to both).
|
|
51
|
+
|
|
52
|
+
### Changed
|
|
53
|
+
|
|
54
|
+
- **Unify the directory-walk decision shared by the three source walkers** (refs #191) β `source-filter.ts` (`collectSourceFiles`/`collectSourceFilesAsync`), `language-profile.ts` (`collectSourceFilesForWarmup`), and `startup-scan.ts` (`countSourceFilesWithinLimit`/`countSourceFilesWithinLimitAsync`) each re-implemented a `readdirSync` + ignore-matcher + exclude-dir walk (the SonarCloud duplication flagged on PR #188's async variants). New `clients/source-walker.ts` centralizes the two genuinely-duplicated pieces β the `readdirSync`-with-try/catch boilerplate (`readDirEntriesSafe`) and the "should I recurse into this directory" decision (`shouldRecurseIntoDir`: ignore-matcher + exclude-dir-name, plus the generated-artifact-directory and symlink-following checks that only `source-filter.ts` opted into) β while each caller keeps its own loop shape, extension/regex rules, build-artifact + generated-header filtering, and hard-cap behavior exactly as before; none of that is silently unified. New `tests/clients/source-walker-equivalence.test.ts` pins a single fixture tree exercising every point where the three callers are supposed to disagree (extension sets, generated-dir skipping, build-artifact shadowing, generated-header sniffing) and asserts each walker's exact historical output, on top of the existing per-file test suites (all of which still pass unmodified). This is only the walker-unification item of #191 β the `isBuildArtifact` memo (already shipped, PR #496), the madge/`actionable_warnings` p99 tail, and `/lens-perf` surfacing remain open.
|
|
55
|
+
- **Adopt pi's dynamic tooling for situational ast-grep/lsp-navigation tools** β pi's `getActiveTools`/`setActiveTools` API lets an extension register tools inactive and activate a subset per-turn, so a lean default tool list doesn't force every situational tool onto every turn. Of pi-lens's 12 pi tools, 6 stay always-active (`lens_diagnostics`, `lsp_diagnostics`, `module_report`, `read_symbol`, `read_enclosing`, `symbol_search`); the other 5 (`ast_grep_search`, `ast_grep_replace`, `ast_grep_outline`, `ast_grep_dump`, `lsp_navigation`) are registered but start inactive, and a new always-active loader tool, `pi_lens_activate_tools` (`tools/activate-tools.ts`), lets the model activate the ones it needs via `pi.setActiveTools([...active, ...requested])` β additive, per the docs' contract; newly-activated tools are callable starting the next turn. Feature-detected the same way as the existing `agent_settled` registration (try/catch, "older pi host?"): `@earendil-works/pi-coding-agent` is a broad, unrestricted peer dependency, so on a host without `getActiveTools`/`setActiveTools` the 5 situational tools are simply left statically active β a silent, graceful fallback matching pi's own behavior on hosts without native deferred-loading support, not a thrown error. The session-start orientation text (`SESSION_START_GUIDANCE`) now calls out which tools are situational and names the loader.
|
|
56
|
+
- **Auxiliary-LSP profile lookup: shared blocking/semantic policy + memoized source lookup** (refs #277 R7) β all four `AUXILIARY_LSP_PROFILES` entries (opengrep, ast-grep, zizmor, typos) carried byte-identical `semantic` lambdas (block on ERROR only when the workspace opted into curated/authored rules, else advisory); extracted to a single shared `blockOnErrorWhenAllowed` so a future policy change is one edit instead of four. `findAuxiliaryProfileForSource` β called once per diagnostic, previously re-scanning all profiles' regexes every time β now memoizes by exact `source` string in a module-level cache, safe because `AUXILIARY_LSP_PROFILES` is a fixed const never mutated at runtime. Behavior-preserving; existing `tests/clients/dispatch/auxiliary-lsp.test.ts` and `tests/clients/dispatch/nosemgrep-suppression.test.ts` pass unmodified.
|
|
57
|
+
- **`dead-code-client.ts` and `knip-client.ts` no longer each hand-roll their own copy of the "climb up looking for a project-root marker" loop** (closes #625) β both `resolveProjectRoot` methods carried a byte-identical depth-64 climb with an inline `isAtOrAboveHomeDir` check per iteration and a `.git`/`.hg`/`.svn` boundary short-circuit, differing only in their marker list (Python project files vs. `package.json`/knip configs) β exactly the duplication `clients/path-utils.ts`'s `findNearestContaining` doc comment already called out as the thing it was meant to be the single source of truth for, except that helper had no boundary concept yet. New `findNearestMarkerRoot(startDir, markers, { boundaries, homeDir })` in `clients/path-utils.ts` extends the same climb (home-ceiling check, then marker match, then boundary short-circuit, then depth-capped parent step, `null` on failure β never a fallback to `startDir`) parameterized by both marker list and boundary list; both call sites now delegate to it instead of maintaining their own copy. Before touching either implementation, pinned the EXACT existing behavior with new tests run against the pre-migration code first (nested-directory resolution, home-dir-itself, above-home ancestor, VCS-boundary stop, no-marker-found) β `tests/clients/dead-code-client.test.ts` gained the same shape of pin tests `tests/clients/knip-client.test.ts` already had (which is how the two were confirmed to be genuine, safe-to-merge duplicates rather than assumed identical); all pins pass unchanged before and after the migration, plus new dedicated `findNearestMarkerRoot` unit tests in `tests/clients/path-utils.test.ts` (including a boundary-vs-marker-at-the-same-directory ordering case, since the marker check must win when both are found in the same directory). Deliberately left alone after auditing: `clients/package-root.ts`'s `getPackageRoot` (same loop shape but a different contract β resolves pi-lens's OWN install root from `import.meta.url`, not a user project, so the home-dir ceiling doesn't apply; and on failure it falls back to the last-reached directory rather than returning `null`, an intentionally different "always resolve to something" semantics that a shared null-on-failure helper would have silently changed); `clients/startup-scan.ts`'s `findNearestProjectRoot` (already shared correctly by `clients/runtime-session.ts`, fixed marker list, no boundaries β confirmed fine as-is, not a duplicate); and everything else that turned up in a grep for the generic `path.dirname(current)`/`parent === current` termination idiom (`clients/dispatch/runner-context.ts`, `.../runners/{shellcheck,shfmt,vale}.ts`, `.../runners/utils/runner-helpers.ts`, `clients/lsp/server.ts`, `clients/file-utils.ts`'s `resolveGitIgnoreRoot`, `clients/formatters.ts`) β each read individually and confirmed to be a genuinely different pattern (single tool-config-file probes, binary/dependency location resolution, gitignore-root resolution with its own always-a-fallback contract, or completely unrelated realpath canonicalization), not assumed to match just because the loop shape looks similar.
|
|
58
|
+
- **`lsp_diagnostics`' batch/directory scan now serializes touches within a single LSP server the same way `lens_diagnostics mode=full` has since #387** (closes #631) β `tools/lsp-diagnostics.ts`'s `collectBatchDiagnostics` used to fan a `paths`/directory file list out across a flat, server-oblivious bounded-concurrency pool (`mapWithConcurrency`, default 8, max 16): up to 8 concurrent touches at the SAME shared, single-threaded LSP server whenever a batch was mostly one language (the common case). `runWorkspaceDiagnostics` (`clients/lsp/index.ts`, the engine behind `lens_diagnostics mode=full`) has been protected against exactly this since #387 (concurrent touches to one server queue server-side instead of parallelizing, cascading per-file timeouts by queue position β observed 51/123 files "timed out" purely from queue position in a flat pool), but `lsp_diagnostics` had no equivalent guard; its current 100-file caps happened to hold up empirically, but that was incidental, not a designed safety property. Extracted `runWorkspaceDiagnostics`'s inline grouping-by-primary-server and one-worker-per-server-group scheduling into two reusable, exported primitives in `clients/lsp/index.ts` β `groupFilesByPrimaryServer` (keys off the same `getServersForFileWithConfig` grouping `runWorkspaceDiagnostics` already used) and `runPerServerGroups` (at most one in-flight `processGroup` call per server group, parallelized across distinct groups up to a `concurrency` cap) β and refactored `runWorkspaceDiagnostics` itself to call them instead of keeping a second, drifting copy of the same logic inline. `tools/lsp-diagnostics.ts`'s `mapWithConcurrency` now groups its file list the same way and schedules through `runPerServerGroups`, preserving the original flat pool's positional result ordering (`results[i]` still matches `items[i]`, via a per-file pending-index queue that also handles duplicate paths in an explicit `paths` batch). The `concurrency` parameter's meaning changes accordingly β it now caps how many DISTINCT server groups run at once, not how many individual files run at once; a single-language batch (the overwhelmingly common case) collapses to one group and runs effectively serially regardless of `concurrency`, which is the CORRECT, intended #387 behavior, not a regression (the tool's parameter description is updated to say so explicitly). New `tests/tools/lsp-diagnostics-per-server-concurrency.test.ts` mirrors `tests/clients/lsp/workspace-diagnostics-per-server.test.ts`'s own proof of the #387 property, driven through the actual `lsp_diagnostics` tool: N files targeting the SAME primary server never have more than 1 in-flight touch at a time while files targeting DIFFERENT servers run concurrently, and `concurrency: 1` correctly forces even distinct server groups to run one at a time. `tests/tools/lsp-diagnostics.test.ts`'s existing mock of `clients/lsp/index.js` now uses `vi.importActual` to keep the real `groupFilesByPrimaryServer`/`runPerServerGroups` wired through alongside its faked `getLSPService` β all 35 existing tests pass unmodified against the real scheduling primitives, not a mock that would trivially satisfy the new behavior either way.
|
|
59
|
+
|
|
60
|
+
### Fixed
|
|
61
|
+
|
|
62
|
+
- **`nested-ternary`/`long-parameter-list`/`no-dupe-class-members` had ZERO coverage in the ast-grep NAPI fallback runner on `.ts` files** (closes #660, follow-up finding from #657) β `clients/dispatch/runners/ast-grep-napi.ts`'s `TREE_SITTER_OVERLAP` skip-set assumed the tree-sitter query runner already covered these 5 rule ids (`constructor-super`, `empty-catch`, `long-parameter-list`, `nested-ternary`, `no-dupe-class-members`), so the NAPI runner deliberately skipped loading its own copies to avoid double-reporting. Investigated via `git log --follow` on the disabled tree-sitter query (`rules/tree-sitter-queries/typescript-disabled/nested-ternary.yml`): the disable was an intentional April 2026 architecture decision (`84dca1ee`, "ast-grep owns TS/JS rules; tree-sitter owns multi-language structural rules"), not a broken query β so re-enabling the tree-sitter query would have fought that decision, not fixed a bug. Auditing the full skip-set found the false assumption applied to 3 of the 5 entries: `nested-ternary`, `long-parameter-list`, and `no-dupe-class-members` have no active tree-sitter query at all (disabled/never written, confirmed against `clients/tree-sitter-query-loader.ts`'s `-disabled/`-directory exclusion), yet all had a perfectly good, shipped, active ast-grep rule sitting unused. Before #657's fix, `nested-ternary` coverage on `.ts` files was accidentally coming ONLY from the `nested-ternary-js` twin cross-firing on `.ts` β exactly the double-firing bug #657 closes β so closing that bug correctly also closed this rule's only accidental coverage path. The other 2 entries (`constructor-super`, `empty-catch`) are disabled everywhere (ast-grep AND tree-sitter, `rules-disabled/`, #206), so skipping them was already a no-op either way. Fix: removed the entire `TREE_SITTER_OVERLAP` skip-set and its check β a blanket assumption-based list with no verification mechanism is exactly how this drifted false for 3/5 entries without anyone noticing. New tests in `tests/clients/dispatch/runners/ast-grep-sonar-rules.test.ts` cover `nested-ternary` firing on `.ts` again (chained + parenthesized nested ternary, no self-match on a single ternary, and no cross-firing of the `-js` twin per #657) and `long-parameter-list` firing on a 5-required-parameter `.ts` function. Investigating `no-dupe-class-members` surfaced a separate, deeper bug (filed as #663): its rule YAML uses a top-level `utils:` block that `yaml-rule-parser.ts`/the native `findAll` call silently drop, so it still doesn't fire post-fix β documented with a test pinning the current (still-gapped) behavior rather than a false claim of coverage; #663 affects 5 shipped rules total.
|
|
63
|
+
- **ast-grep NAPI fallback runner no longer silently drops a rule's `utils:` block** (closes #663, found while adding a regression test for #660) β `clients/dispatch/runners/yaml-rule-parser.ts`'s `YamlRule` interface had no `utils` field, and `ast-grep-napi.ts`'s native `findAll` call only ever built `{ rule, constraints }`, so any rule with a top-level `utils:` block (reusable named matchers referenced via `matches: <name>` inside `rule`/`constraints` β standard ast-grep YAML syntax) got that block dropped: the unresolved `matches: <name>` reference then either made napi reject the whole rule (silently swallowed by the surrounding `try { ... } catch { matches = []; }`) or matched nothing, so the rule produced zero diagnostics with no error surfaced anywhere. 5 shipped rules declare `utils:` β `no-dupe-class-members`, `no-dupe-keys`, `no-dupe-keys-js`, `rust-2024-let-chain-candidate`, `unnecessary-react-hook` β two of them (`no-dupe-keys`/`-js`) real correctness rules, not cosmetic. Scoped to the NAPI in-process fallback only; the ast-grep CLI/LSP path (used whenever the `ast-grep` binary is installed) already handles `utils:` correctly. Fix: `YamlRule` gains `utils?: Record<string, YamlRuleCondition>` (parsed straight through by the existing faithful js-yaml parse, no parser changes needed), and the native config construction in `evaluateAstGrepRules` now passes `utils` through alongside `rule`/`constraints` β verified against `@ast-grep/napi`'s own `NapiConfig.utils: Record<string, Rule>` type declaration, the exact shape napi's native engine expects. Also: the swallowed `findAll` rejection is now logged via the runner's existing `ctx.log` (a new optional `AstGrepEvaluateOptions.log` sink), so a future malformed-or-unresolvable rule doesn't silently produce zero diagnostics again with zero trace. New tests (`tests/clients/dispatch/runners/ast-grep-utils-block.test.ts`): `no-dupe-keys`/`no-dupe-keys-js` fire on their real duplicate-key fixtures, and (post-#660's removal of `TREE_SITTER_OVERLAP`) `no-dupe-class-members` now also fires end-to-end β all three verified through the actual `runner.run()` front door; `unnecessary-react-hook` (still gated out of the full runner path by an unrelated pre-existing filter β the runner's typescript/javascript-only language filter, since it's tagged `Tsx`, untouched by this fix) is verified directly against napi's real native engine using the exact `{ rule, constraints, utils }` shape the fix now builds; `rust-2024-let-chain-candidate`'s `utils:` block is confirmed to survive YAML parsing (this napi build has no Rust parser at all β Rust rules run via the CLI/LSP path only, already covered by `ast-grep-catalog-rules.test.ts`'s CLI-based fixtures).
|
|
64
|
+
- **ast-grep `-js` twin rules no longer double-fire on `.ts` files in the in-process NAPI runner** (closes #657, confirmed via `~/.pi-lens/latency.log` dogfooding β `hardcoded-url`/`hardcoded-url-js` both firing on one line of a real `.ts` file) β TypeScript's grammar is a syntactic superset of JavaScript, so a `language: JavaScript`-tagged rule using generic node kinds (`variable_declarator`, `assignment_expression`, β¦) still matched a parsed `.ts` root in `clients/dispatch/runners/ast-grep-napi.ts`'s `evaluateAstGrepRules`, which only filtered OUT non-TS/JS languages (Python, Go, β¦) but never distinguished TypeScript from JavaScript for the family it actually supports. Audited all 66 `<base>.yml`/`<base>-js.yml` candidate pairs in `rules/ast-grep-rules/rules/`: 65 have byte-identical `rule:`/`message:`/`severity:` bodies (intentional twins β the ast-grep CLI/LSP surface, which DOES correctly gate by `language:`, needs both files shipped for real per-grammar `.js` coverage, per the existing authoring convention in `skills/pi-lens-write-ast-grep-rule/SKILL.md`); 1 (`no-flag-argument`/`no-flag-argument-js`) is genuinely different (`required_parameter` vs `assignment_pattern` node kinds for typed vs default params) and was left untouched. ast-grep's rule schema has no multi-language single-rule mechanism (`language:` is a single required value, confirmed against `rule-schema.json` and every other rule in the catalog), so β matching the issue's own fallback ask β the fix scopes the NAPI runner's dispatch itself: new `ruleLanguageForFile()` maps a file's extension to `"typescript"`/`"javascript"` and a rule's `language:` tag must now match the FILE's actual grammar (not just be "some" JS/TS value) to run against it, closing the bug structurally for every current and future twin pair rather than deleting/merging the intentionally-duplicated rule files. Auditing also surfaced a real, separate copy-paste bug: 7 of the 65 "twin" `-js.yml` files (`array-callback-return-js`, `jwt-no-verify-js`, `no-extra-boolean-cast-js`, `no-implied-eval-js`, `no-javascript-url-js`, `no-sql-in-code-js`, `weak-rsa-key-js`) still carried `language: TypeScript` β their language tag was never updated off the copy-pasted base file, so both files always ran under the SAME language and each had zero actual `.js` coverage under the CLI/LSP path (confirmed against `rules/rule-catalog.json`, which already listed `javascript` for 4 of these ids); corrected all 7 to `language: JavaScript`. New regression coverage: a runner-level vitest test (`tests/clients/ast-grep-rule-precedence-followups.test.ts`) reproduces the exact bug with both a synthetic generic-node-kind rule pair and the real bundled `hardcoded-url`/`hardcoded-url-js` rules, asserting exactly one twin fires per file extension; a new catalog-hygiene script, `scripts/audit-astgrep-rule-pairs.mjs` (`npm run audit:astgrep-rule-pairs`, wired as a new blocking CI lint step), flags any future rule pair with an identical body under the SAME `language:` tag, an identical-body twin whose language tags aren't exactly `{TypeScript, JavaScript}`, or an identical-body twin whose `message`/`severity` has drifted β it would have caught the 7-file copy-paste bug directly. `docs/ast-grep_rules_catalog.md` regenerated (`npm run docs:rule-catalogs`) to reflect the 7 corrected language tags. Deliberately out of scope per the issue: the SEPARATE tree-sitter-engine `rules/rule-catalog.json` catalog (its own `canonical_concept` grouping mechanism, untouched). Also noticed but NOT fixed here (follow-up filed): `nested-ternary` is listed in this same runner's `TREE_SITTER_OVERLAP` skip-set on the assumption the tree-sitter query runner covers it, but that query currently lives under `rules/tree-sitter-queries/typescript-disabled/` (disabled) β meaning `nested-ternary` coverage on `.ts` files was accidentally coming ONLY from the `nested-ternary-js` cross-firing bug this PR fixes, and is now genuinely uncovered by the NAPI runner on `.ts` files (the ast-grep CLI/LSP path, when available, is unaffected since it already gates by language correctly).
|
|
65
|
+
- **Dynamic tool deactivation for the 5 situational tools now actually runs, instead of failing silently on every session_start** (closes #643, found via `~/.pi-lens/sessionstart.log` dogfooding) β the #dynamic-tooling feature (`pi.getActiveTools`/`setActiveTools`) deactivates `ast_grep_search`/`ast_grep_replace`/`ast_grep_outline`/`ast_grep_dump`/`lsp_navigation` at load so the model must call `pi_lens_activate_tools` to bring them in, but the call to `setActiveTools` ran synchronously in `index.ts` right after `registerTool`, still inside the extension's own load/activation function β a point at which the runtime is not yet initialized, so the call structurally cannot succeed on ANY host regardless of version. Every session's log showed the identical caught error, `Error: Extension runtime not initialized. Action methods cannot be called during extension loading`, misleadingly logged as `dynamic tool deactivation failed (older pi host, or tools not registered?)` β the feature-detection `typeof` guard already proved the host supports the API, so the real problem was call-site timing, not host capability. Net effect: the 5 lazy tools were likely never successfully deactivated even once since the feature shipped, defeating the point of the on-demand design. Fix: moved the `getActiveTools()`/`setActiveTools()` call (same feature-detection guard, same filtered-list computation) out of the synchronous registration block and into the existing `pi.on("session_start", ...)` handler, which fires after the extension has finished loading β the correct lifecycle point. Runs on every `session_start` firing (fork/reload/new/resume all fire it in one process's lifetime); safe to re-run each time since `setActiveTools` just replaces the current active set rather than being additive/stateful across calls. The caught-error log message was also reworded, since "older pi host" is now an accurate description if the call still fails there. New/updated tests in `tests/index-wiring.test.ts` drive the real `session_start` handler (mocking `clients/bootstrap.js`/`clients/runtime-session.js` the same way `tests/index-integration.test.ts` does, to keep it a fast wiring check) and assert the 5 situational tools land inactive after it fires, both on a dynamic-tooling host and β unchanged β statically active as a graceful fallback on a host without the API.
|
|
66
|
+
- **`lsp_diagnostics`'s `serverScope: "primary"` actually skips auxiliary scanners now, and the common case drops back to one LSP round trip per file instead of two** (closes #629, #619 regression, live-debugging finding via `latency.log`) β `collectDiagnosticsForFile` (`tools/lsp-diagnostics.ts`) took the `touchFile` branch (correctly `clientScope`-scoped, `collectDiagnostics: true`) whenever `waitMs` was passed or `serverScope: "primary"` was requested, but only ever read `touched?.inconclusive` off its return value and then discarded the array β a second, UNCONDITIONAL `lspService.getDiagnostics()` call (which takes no `serverScope`/`clientScope` argument at all and always queries every registered server) supplied the actual diagnostics content every time. Confirmed live via `latency.log`: a `serverScope:"primary"`/`waitMs:1000` call showed the primary confirmation touch timing out on typescript alone (`clientScope:"primary"`) while a separate `lsp_diagnostics_aggregate` entry for the SAME file, moments later, showed all 4 servers touched (`opengrep` waiting its full 3500ms) β two genuinely separate round trips merged into one inconsistent result, and `serverScope: "primary"`'s own doc comment ("for when the caller just wants confirmation, not a full security/lint pass") was silently broken since the day #619 introduced the parameter. Fix: `touched` β already the correctly-scoped, already-collected `LSPDiagnostic[]` β is now used directly as the diagnostics content whenever the `touchFile` branch was taken and it resolved to something defined; the `getDiagnostics()` call only still runs as a fallback when `touchFile` itself couldn't produce a result (service destroyed, no clients resolved) or when neither `waitMs` nor `serverScope: "primary"` was set in the first place (the pre-existing `openFile`-only path, genuinely unchanged β it never collected anything and still needs the follow-up read). `applyAuxiliarySuppressions` (#586's `// nosemgrep`-style inline-suppression filtering) runs on whichever path produced the diagnostics, so suppression behavior is identical either way. New/updated tests in `tests/tools/lsp-diagnostics.test.ts`: `serverScope: "primary"` and the default `waitMs`-only path now both assert `getDiagnostics` is NOT called at all (not just that the final content matches); a dedicated test proves the rendered diagnostic content comes from `touchFile`'s own return value even when a still-wired `getDiagnostics` mock returns a different (aux-only) finding, which must never leak through; a fallback test confirms `getDiagnostics` is still called when `touchFile` resolves to `undefined`; and a regression guard pins the untouched `openFile`-only path (`getDiagnostics(path, "full")` called, `touchFile` never called) when neither `waitMs` nor `serverScope: "primary"` is set.
|
|
67
|
+
- **Per-edit test-runner findings: fixed a broad vitest include glob producing vacuous `0p/0f` "passes" on plain source files, and stopped silently discarding real results when the turn advanced mid-run** (closes #628, live dogfooding on pi-drykiss) β `~/.pi-lens/sessionstart.log` from a real session showed `src/background-review.ts β test vitest src\background-review.ts (self)` followed by `PASS 0p/0f (0ms)`: a plain source file was targeted as its own test ("self" strategy) and vitest, finding zero actual tests in it, reported a technically-true, practically-meaningless pass. Root cause: `isTestFile()` (`clients/test-runner-client.ts`) let a vitest project's own `test.include` glob override a clean `detectFileRole(...) !== "test"` naming-convention verdict on ANY match β a common real-world glob shape like `src/**/*.ts` (or an unrestricted `**/*.ts`) matches every source file in the tree, not just tests, so the override fired on ordinary edited files. Fixed by tightening the override to a NARROW glob signal (`isNarrowTestGlob`): trusted only when a literal path segment names a conventional test location (`tests/`, `test/`, `spec/`, `specs/`, `__tests__/` β preserving the legitimate case this override exists for, a project whose tests live in such a directory with no `.test.`/`.spec.` in the filename) or the glob's static suffix after its last wildcard encodes more than the bare language extension (e.g. `**/*.check.ts`, preserving the existing unconventional-naming test case); a bare `src/**/*.ts`/`**/*.ts` now fails both checks and no longer self-classifies. Pinned first: new tests reproducing the exact `background-review.ts`-shaped bug were confirmed to fail against the pre-fix code, then confirmed to pass after the fix, alongside the pre-existing narrow-glob legitimate case and a new bare-`tests/`-directory legitimate case, both still passing. Second, independent bug in the same turn_end test-fire path (`clients/runtime-turn.ts`): the deliberately async/non-blocking test-fire (`Promise.allSettled` over `runTestFileAsync`, so tests never stall an edit) detected `runtime.turnIndex !== firedAtTurn` (the turn advanced β another edit landed β before the fired subprocess resolved) and unconditionally discarded the results, logging `discarding test results β turn advanced while tests ran` and writing nothing to the `test-runner-findings` cache the next turn's context injection reads from; real logs showed this firing routinely in a fast-moving session, not as a rare edge case, so the "next turn gets test feedback" contract was regularly unfulfilled. Fixed by caching the results regardless of staleness β a late result is still real information about what's currently broken β tagged `stale: true` and with a `[from a prior turn β β¦]` prefix on the cached content so a downstream consumer (or the agent reading it) can tell it wasn't from the immediately-preceding edit. Also extended turn_end's test-fire loop to target the test companions of this turn's cascade neighbors (files that import an edited file), not just the edited file itself β a neighbor's own tests can break even though the neighbor's source wasn't touched. Reuses `cascadeResults`, already computed earlier in the same turn_end for the LSP cascade-diagnostics merge (#450's deferred-cascade drain) β no second reverse-dependency walk, and the neighbor set inherits whatever budget (`CASCADE_NEIGHBOUR_BUDGET`) the cascade compute already applied, so this can't turn into unbounded per-edit work. New tests in `tests/clients/runtime-turn-session.test.ts` cover: a stale result still gets cached (with the `stale`/prior-turn tagging), a non-stale result is cached without the tag, and a cascade neighbor's test companion is fired alongside the edited file's own target. Once the underlying data was trustworthy, added a new cache-only extractor (`clients/project-diagnostics/runner-adapters/test-runner.ts`, registered in `extractors.ts`) so `lens_diagnostics mode=full` can also surface `test-runner-findings` per-file, the same way jscpd/knip/madge already do β reads the cache's newly-added structured `results: TestResult[]` field (alongside the pre-existing formatted `content` string the context-injection consumer reads), emits one blocking diagnostic per test failure, and never launches a test run itself (running a whole suite is explicitly out of scope). Covered by new tests in `tests/clients/project-diagnostics.test.ts` (adapts cached failures into per-file diagnostics with the failure name/message; an all-passing cached result contributes nothing).
|
|
68
|
+
- **`lens_diagnostics mode=full`'s workspace sweep no longer front-loads an entire server group's `didOpen` burst in one uninterrupted pass** (closes #621, dogfooding finding) β dogfooding on `pi-drykiss` (~150 TS files) found a full sweep collapsing to near-100% per-file timeouts on every recent run, despite the #608/#616 fixes around the same window being confirmed innocent. Root cause: #608's fix (correctly) pre-opens every file in a server group *before* the per-file diagnostics-wait loop starts, so `WatchedFilesQueue`'s 100ms debounce coalesces the resulting watched-files notifications into one project recheck instead of N cascading ones β but for a single-language project (the common case, one server = one group) that meant firing ALL ~150 files' `didOpen` in one burst, dumping the whole batch on tsserver's single-threaded request queue at once and forcing it to ingest/typecheck the entire burst before any per-file diagnostics request even got a turn. `lsp_diagnostics`' batch/directory mode (`tools/lsp-diagnostics.ts`) never hit this because its flat bounded-concurrency worker pool (default 8) only ever has ~8 files in flight β confirmed directly: an explicit 100-file `paths` batch on `pi-drykiss` via that path completed fast with zero timeouts, while the same project's `mode=full` sweep timed out on nearly every file. The fix chunks `runWorkspaceDiagnostics`'s (`clients/lsp/index.ts`) pre-open+process cycle to `WORKSPACE_SWEEP_PREOPEN_CHUNK_SIZE` (default 8, env-tunable via `PI_LENS_LSP_WORKSPACE_PREOPEN_CHUNK`, matching `lsp_diagnostics`' own default concurrency) instead of the whole group: each chunk's opens still land inside the same 100ms debounce window and coalesce into one flush (preserving #608's "no per-file cascade" guarantee β verified an explicit chunked-burst test still coalesces, never regressing to one flush per file), but no single burst dumped on the server ever exceeds the chunk width regardless of total group size, matching the same bounded-concurrency shape that was already proven safe in `lsp_diagnostics`. #387's per-server serialization (one in-flight touch per server, parallel across distinct servers) is unchanged β this only paces the pre-open burst *within* a group, it does not flood across servers. New `tests/clients/lsp/workspace-diagnostics-sweep-preopen-chunk.test.ts` proves both properties directly against the real `WatchedFilesQueue` coalescing primitive: a 40-file group never bursts more than the configured chunk size while still coalescing one flush per chunk (5, not 1 and not 40), and a group smaller than the chunk size is unaffected (still a single flush). The existing `workspace-diagnostics-sweep-batch-open.test.ts` (#608) is updated in place β its 20-file fixture now spans 3 chunks at the default width, so it asserts 3 flushes instead of 1, while still guarding against the original one-flush-per-file regression.
|
|
69
|
+
- **Launching Pi from `$HOME` and then editing an absolute-path file in another repo could block Pi's event loop for 470-500+ SECONDS while the cascade pipeline walked the entire home directory** (closes #622, live dogfooding report) β `runPipeline -> computeCascadeForFile -> buildOrUpdateGraph -> getGraphSourceFiles -> collectSourceFiles -> scan -> isIgnored -> minimatch` enumerated 206,551+ files before the review-graph's `maxGraphFiles` safety cap (#250) even had a chance to trip, because that cap counts post-filter *kept* files, not directory entries visited β an unfiltered tree the size of `$HOME` costs the full readdir/stat walk regardless of how quickly the cap would reject it once hit. Root cause: `buildOrUpdateGraph` (`clients/review-graph/builder.ts`) trusts its `cwd` parameter to already BE a validated project root, an assumption 3 of its 4 real call sites break by passing pi's raw session/pipeline cwd straight through with zero validation β `dispatch/integration.ts`'s `computeCascadeForFile` (the exact path in this issue's stack trace), `mcp/analyze.ts`'s warm-analysis graph update, and `dispatch/runners/tree-sitter.ts`'s `runBlastRadiusInBackground` β while the 4th (`runtime-session.ts`'s session-start call-graph task) is already safe, gated behind `startupScan.canWarmCaches`, itself already rejecting a home-dir cwd. This is the same class of `$HOME` escape `startup-scan.ts`, `dead-code-client.ts`, and `knip-client.ts` each independently closed for their own walks (#250/#253), but the review-graph builder β the one component actually named in this issue's live-inspector stack β never got the equivalent guard. Fixed at the single shared choke point instead of patching each of the 3 unsafe callers separately: `_doBuildGraph` (`clients/review-graph/builder.ts`) now checks `isAtOrAboveHomeDir(path.resolve(cwd))` (`clients/path-utils.ts`, the same ceiling helper the other 4 call sites already use) before any walk, seq-fastpath attempt, or cache lookup runs, and returns an empty, unpersisted graph tagged `mode: "skipped", skipReason: "unsafe_root"` (mirroring the existing `too_many_files` skip shape/`GraphBuildInfo` contract) rather than falling back to walking `cwd` anyway β matching the issue's own stated expected behavior ("skip graph construction when no safe project root exists"). A normal project cwd (the overwhelmingly common case, including one nested under `$HOME` like `~/code/app`) is completely unaffected β only a cwd that IS `$HOME` or an ancestor of it is rejected. The secondary, lower-priority item from the investigation β whether `collectSourceFilesAsync`'s cap should also account for total directory entries visited, not just kept files, as defense in depth β was deliberately left out of this fix (real but separate scope) rather than bundled in. New tests in `tests/clients/review-graph.service.test.ts`: a `buildOrUpdateGraph(os.homedir(), ...)` call now skips near-instantly (`skipReason: "unsafe_root"`, empty graph, well under the ~500s the unfixed walk took) instead of ever starting the walk, and a regression guard confirms a normal project living UNDER home still builds normally.
|
|
70
|
+
- **A genuine LSP server crash could go completely unlogged when the JSON-RPC connection tore down before the process's own `exit` event fired** (closes #618, follow-up to #615) β a live sweep against `pi-drykiss` showed its `ast-grep` client dying and respawning 5 times in ~75s with NOTHING in `latency.log` explaining why (no exit code, no signal, no stderr) β investigation traced it to `setupConnectionLifecycle` (`clients/lsp/client.ts`)'s `lsp_server_unexpected_exit` log being gated on `wasConnected` (captured at the moment the process `exit` event fires): a genuine crash's `connection.onClose`/`onError` handler can run synchronously off the dying stdio pipe and flip `isConnected` false BEFORE `exit` fires, so by the time the exit handler ran, the crash looked indistinguishable from an intentional `clientShutdown()` call and the log was silently skipped. Fixed by adding an explicit `LSPClientState.shutdownRequested` flag, set ONLY by `clientShutdown()`, and gating the exit log on that instead of `isConnected` β a genuine crash is now always logged regardless of event ordering. Also added the missing `exitSignal` (Node's `exit` event's second argument, previously dropped entirely) and a `stderrTail` (last 20 lines, reusing the existing `recentStderr` ring buffer already exposed on the client) to the log's metadata, so a future crash is actually diagnosable instead of just "respawned, uptime was Xms". New `tests/clients/lsp/client-crash-logging.test.ts`, using the same real fake-LSP-server child process the existing integration tests spawn: confirms a `SIGKILL`'d child (no `shutdown()` call first) logs `lsp_server_unexpected_exit` with the real exit signal and a stderr tail, and that an intentional `client.shutdown()` still logs nothing β both against a real process, not a mock. Confirmed the new test fails against the pre-fix `wasConnected` gate (missing `exitSignal`) before confirming it passes against the fix.
|
|
71
|
+
- **`runWorkspaceDiagnostics`'s batch pre-open pass (#608) could hang an entire full-workspace sweep un-abortably** (closes #615, live dogfooding incident) β `preOpenGroupFiles`, added by #608/#610 to fix a watched-files debounce issue, ran ahead of the sweep's already-`withDeadline`-wrapped per-file loop but had no bound of its own: a hung `getClientsForFile` (stuck server spawn/initialize) or `notify.open` (stuck notification write) call froze the whole sweep with no heartbeat, and pressing Escape didn't help either β the loop's `signal?.aborted` check only runs between files, never while one is mid-await. Live symptom: `lsp_workspace_diagnostics_start` logged, then 13+ minutes of total silence, unresponsive to abort. Fix adds two independent bounds around the pre-open attempt: `withDeadline(..., { ms: perFileMs, onTimeout: "undefined" })` (catches a hang even with no user action) and a `Promise.race` against the abort signal (an explicit Escape/turn-abort now unblocks immediately instead of waiting out the rest of the per-file budget). New tests in `tests/clients/lsp/workspace-diagnostics-sweep-batch-open.test.ts` (`#615` block): a pre-open call that never resolves doesn't hang the sweep, and aborting mid-pre-open unblocks well before the per-file deadline would fire β both confirmed to actually hang/timeout against the pre-fix unbounded code before confirming they pass against the fix. Added a standing invariant to AGENTS.md's Performance section: any new async step added to an existing bounded loop needs BOTH a timeout bound and an abort-signal race, not just one β this is the second time in one day a bounded-loop change shipped with an unbounded new step inside it.
|
|
72
|
+
- **`lens_diagnostics mode=full` now runs the heavyweight-analyzer fresh-fetch CONCURRENTLY with the LSP sweep instead of after it, fixing a real-world case where all 7 analyzers went cold** (closes #613, dogfooding finding) β `formatFullMode` (`tools/lens-diagnostics.ts`) `await`ed `fetchFreshProjectDiagnostics` (#585/#590) only AFTER its own `Promise.all([runWorkspaceDiagnostics, ...])` had already fully resolved, sequentially spending the SAME shared wall-clock ceiling (`FULL_SCAN_WALL_CLOCK_MS`) the LSP sweep had already eaten into β despite a comment directly above the call claiming it was "run in parallel with the rest." On a real ~150-file project the LSP sweep alone can take 100+ seconds, leaving the analyzer fetch almost no budget before the shared abort signal fired β killing ALL 7 analyzers (including unconditional ones like knip/jscpd/madge that should always attempt) before any could complete, and rendering `(7 cold: govulncheck, trivy, dead-code, knip, jscpd, madge, gitleaks)` on a scan that had, in fact, barely started the analyzer phase at all. Fix: `fetchFreshProjectDiagnostics`'s promise (built via `analyzersPromise`, gated the same way by `shouldIncludeProjectRunners`) is now included in the SAME `Promise.all` as the LSP sweep and the cheap project-runner scan, so all three phases race the same signal from the same starting point β genuinely concurrent, not stacked. There's no data dependency between the phases (the analyzer fetch never reads the LSP sweep's results), so nothing else about the merge/reconcile logic below needed to change. New test `tests/tools/lens-diagnostics.test.ts` ("starts the analyzer fresh-fetch CONCURRENTLY with the LSP sweep, not after it resolves") holds the LSP sweep's mock promise open and asserts `fetchFreshProjectDiagnostics` has already been invoked before the sweep resolves β verified this test actually fails against the pre-fix sequential code (confirmed by temporarily reverting the fix and re-running it) before confirming it passes against the fix.
|
|
73
|
+
- **`lens_diagnostics mode=full`/`lsp_diagnostics` full-workspace sweeps no longer defeat #271's watched-files debounce, fixing a 0%-100% run-to-run false-timeout rate** (closes #608) β dogfooding a full sweep on an unchanged ~151-file TS project showed a wildly variable per-file timeout rate across successive runs (0%, 100%, 33%, 64%) with the tsserver process never restarted in between, ruling out cold-indexing and already-ruled-out #591 (opengrep exclusion). Root cause: `handleNotifyOpen` (`clients/lsp/client.ts`), the first time it sees a not-yet-open file, enqueues a `workspace/didChangeWatchedFiles` notification via `WatchedFilesQueue` (`clients/lsp/watch-queue.ts`, #271) β built specifically to coalesce a *burst* of file-opens into ONE project-wide recheck instead of N, because classic tsserver (and most push-diagnostics servers) kicks off an expensive full re-analysis on every such notification. `WatchedFilesQueue.enqueue` only arms its 100ms debounce timer on the FIRST call in a burst and just accumulates on every call after β but `runWorkspaceDiagnostics` (`clients/lsp/index.ts`) processes files SERIALLY, waiting up to several seconds per file for its own diagnostics before touching the next one, so consecutive first-opens during a sweep always landed far outside that 100ms window: every previously-unopened file fired its OWN watched-files notification, each independently triggering a project-wide recheck, and later files timed out purely from queueing behind those rechecks β not because anything was actually wrong with them. Whether a given run hit this depended entirely on how many swept files happened to already be open from earlier per-edit dispatch activity that session, explaining the run-to-run variance. Fix: each server group's files are now batch pre-opened (`preOpenGroupFiles`) in one fast, back-to-back pass β with no diagnostics wait between opens β immediately before that group's existing per-file diagnostics loop starts (right after the #387 Item 2 `workspace/diagnostic` pull attempt, so a group whose pull succeeds skips pre-opening entirely and still does zero per-file opens). Firing every open notification with no wait between them keeps every `enqueue()` call inside the 100ms debounce window, so `WatchedFilesQueue` coalesces them into a single flush per server the same way a per-edit dispatch burst already does; by the time the main per-file loop's own `touchFile` call runs, each document is already in `openDocuments`, so `handleNotifyOpen` takes the cheap already-open `didChange` branch and enqueues nothing further. File content read during pre-open is cached so the main loop doesn't re-read the same file from disk. Preserves, unmodified: #387's per-server serialization (pre-opening is itself serialized WITHIN a server group and parallelized ACROSS groups, the identical shape as the diagnostics loop it precedes β sending concurrent opens to one server would reintroduce the exact flooding pathology #387 fixed), #571's inconclusive-signal handling, #586's auxiliary-suppression filtering, and #591's opengrep sweep-exclusion (`WORKSPACE_SWEEP_EXCLUDED_SERVER_IDS`, reused as-is for pre-open's own `getClientsForFile` call) β none of these paths were touched. Per-edit dispatch's own burst-coalescing (`handleNotifyOpen`/`WatchedFilesQueue` themselves) is completely unchanged; this fix is scoped entirely to `runWorkspaceDiagnostics`. New `tests/clients/lsp/workspace-diagnostics-sweep-batch-open.test.ts` imports the real `WatchedFilesQueue` (not reimplemented) and a fake client whose `notify.open` mirrors `handleNotifyOpen`'s open/already-open branch split plus a deliberately slow (150ms, past the debounce window) `waitForDiagnostics` standing in for tsserver's real per-file latency β proving a 20-file sweep over previously-unopened files produces exactly one watched-files flush instead of 20. `tests/clients/lsp/workspace-diagnostics-per-server.test.ts` (#387), `-opengrep-exclusion.test.ts` (#591), and `-suppression.test.ts` (#586) all pass unmodified.
|
|
74
|
+
- **Managed npm tools now re-install when their `packageName` version pin changes** (#589) β `ensureTool()`/`getToolPath()` (`clients/installer/index.ts`) resolved an already-installed managed tool purely by existence + runnability: `verifyToolBinary()` spawned `<bin> --version` and checked only the exit code, discarding the reported version string, so an installed tool never picked up a later pin bump in code (e.g. a `jscpd@3.5.10` -> `jscpd@X.Y.Z`-style change) β it kept running whatever version it happened to install first, forever. Fix, scoped to `installStrategy: "npm"` entries with an explicit `@version` in `packageName` (the only kind with drift to detect β unpinned entries like `madge` have none): `verifyToolBinary` now accepts an optional `onVersionOutput` callback, invoked with the captured `--version` stdout on success; `getToolPath()`'s managed-local-install checks pass it for pinned npm tools, stashing the parsed version (`extractVersionToken`) into a new `lastManagedInstallVersion` map keyed by toolId. `ensureTool()`'s existing-install path compares that against the current pin (`parsePinnedVersion`) and, on mismatch, recurses through the EXISTING `forceReinstall` codepath rather than a new mechanism. Deliberately piggybacks on the spawn `verifyToolBinary` already performs on `ensureTool`'s slow path (post cache-miss) β the in-memory session cache and the 24h persistent probe cache are untouched, so a matching-version tool still resolves with zero new spawns on the hot path, and drift is detected at most once per session (or once per probe-cache TTL, whichever the caller hits first). Investigated whether `installStrategy: "github"/"maven"/"archive"` tools have the same drift bug: they do (an archive tree bundle's extract dir, e.g. `TOOLS_DIR/clangd`, isn't version-named, so bumping a hardcoded version constant in its download URL doesn't force a fresh extract), but none of those resolution paths ever spawn `--version` during discovery (`findGitHubToolPath`/`getArchiveTreeBundlePath` are pure `fs.access` checks) β there is no existing spawn to piggyback on, so fixing them would mean inventing a new mechanism, explicitly out of scope for this fix; tracked separately. New `tests/clients/installer/version-drift.test.ts` covers: a stale-versioned `jscpd` install forces reinstall; a matching-version install resolves normally with no install spawn; a second `ensureTool()` call on an unchanged, matching install hits the in-memory cache with zero new spawns; an unpinned npm tool (`madge`) is completely unaffected; and `getToolPath()` itself still reports a drifted binary as found (discovery is not gated on version β only `ensureTool()` routes drift to reinstall).
|
|
75
|
+
- **`// nosemgrep` inline suppression now honored by `lsp_diagnostics`/`lens_diagnostics`, not just per-edit dispatch** (#586) β pi-lens's own `# nosemgrep`/`// nosemgrep` inline-suppression parser (`isNosemgrepSuppressed`, added for #441 because opengrep's LSP mode doesn't honor it natively) was only ever consulted from the per-edit dispatch runner (`clients/dispatch/runners/lsp.ts`), so a suppression comment that correctly hid a finding during real-time editing still surfaced the identical finding when the same file was queried via the standalone `lsp_diagnostics` tool or `lens_diagnostics mode=full`'s workspace sweep β a real dogfooding report caught opengrep's `detected-github-token`/`detected-jwt-token` rules re-flagging an already-suppressed line. Extracted the profile-lookup-then-`isSuppressed` logic that `clients/dispatch/runners/lsp.ts` already had into two shared, generic helpers in `clients/dispatch/auxiliary-lsp.ts` β `isAuxiliaryDiagnosticSuppressed` (single-diagnostic predicate) and `applyAuxiliarySuppressions` (list filter) β so any `AUXILIARY_LSP_PROFILES` entry with an `isSuppressed` callback (currently only opengrep's) is honored uniformly, not hardcoded to opengrep. `clients/dispatch/runners/lsp.ts` now calls the shared predicate instead of duplicating the lookup; `tools/lsp-diagnostics.ts`'s `collectDiagnosticsForFile` and `clients/lsp/index.ts`'s `runWorkspaceDiagnostics` now filter their raw diagnostics through `applyAuxiliarySuppressions` using the file content already read at each call site (fail-open to unfiltered diagnostics if the content read itself failed). Covered by new tests in `tests/clients/dispatch/auxiliary-lsp.test.ts` (the shared helpers directly), `tests/tools/lsp-diagnostics.test.ts` (single-file and batch `lsp_diagnostics` suppression), and `tests/clients/lsp/workspace-diagnostics-suppression.test.ts` (the `runWorkspaceDiagnostics` sweep).
|
|
76
|
+
- **`high-fan-out`/`high-complexity` no longer false-positive on `describe()`/`it()` test bodies** (#577, found via #576's live-evidence follow-up) β running the real dispatch pipeline against 4 real test files in this repo showed every one triggering a `high-fan-out`/`high-complexity` finding on the outer `describe(...)` callback (up to "51 distinct functions" / cyclomatic complexity 15), while the other 11 `FactRule`s in `clients/dispatch/rules/` had no evidence of test-file noise. Root cause was two-fold, confirmed by directly running `functionFactProvider` against the reported lines: (1) `expect(x).matcher(...)` assertion chains each counted as a *distinct* callee in `high-fan-out`'s fan-out count, because the callee text is the verbatim member-expression source including the differing arguments (`expect(a).toBe` vs `expect(b).toContain` are never deduplicated); (2) the shared tree-sitter walk that computes a function's `outgoingCalls`/`cyclomaticComplexity`/`maxNestingDepth` (`clients/dispatch/facts/function-facts.ts`) doesn't stop at nested-function boundaries, so a `describe()` wrapper's own metrics aggregate every call and branch from ALL of its nested `it()` bodies (each of which also gets its own, correctly-scoped, separate `FunctionSummary`) β a `for` loop inside several sibling `it()`s sums into the enclosing `describe()`'s complexity even though no single test is complex. Scoped narrowly to just these two rules (not a runner-level `skipTestFiles` on `fact-rules.ts`, which would have silently dropped signal from the other 11 rules too, and not a blanket `ctx.fileRole === "test"` exemption, which would have also suppressed genuinely complex test HELPER functions). New shared `clients/dispatch/rules/framework-call-noise.ts` (module named to avoid the pre-existing `test-*.ts` gitignore pattern meant for ad-hoc scratch scripts) exports two call-name-based heuristics used by both rules: `isTestFrameworkNoiseCall` extends `high-fan-out`'s existing "meaningful calls" filter (same style as its `console.*`/`Math.*`/etc. exclusions) with `expect(...)`/`expect`, test lifecycle names (`it`/`test`/`describe`/`beforeEach`/`afterEach`/`beforeAll`/`afterAll`, including `.only`/`.skip`/`.each` variants), and `vi.*`/`jest.*` mock-library prefixes; `isTestSuiteOrganizer` skips evaluating a function entirely (in both rules) when its raw outgoing calls include a direct call to `it`/`test`/`describe` β i.e. it groups nested tests rather than implementing logic itself. Verified empirically that call-name filtering alone was insufficient for the largest reported case (52 calls filtered down to ~25, still over the 20 threshold) before adding the organizer check. A genuinely tangled test HELPER function that does NOT itself call `it`/`describe`/`test` is still flagged by both rules (preserving real signal), and production (non-test) files are completely unaffected β both covered by `tests/clients/dispatch/rules/high-fan-out-complexity-test-noise.test.ts`, which also reproduces the real `describe()`-wrapping-multiple-`it()`s shape from `tests/clients/widget-state.test.ts` end-to-end through `functionFactProvider`.
|
|
77
|
+
- **`fact-rules` diagnostics now stamp `tool: "fact-rules"` instead of their own rule id, fixing scattered turn-summary grouping** (#578) β `clients/dispatch/types.ts`'s `Diagnostic` contract documents `tool` as "which runner produced this" and `rule` as "the specific check within it"; `ast-grep`'s runner follows it correctly (one `tool: "ast-grep"` value shared across every pattern), but all 13 rules under `clients/dispatch/rules/*.ts` (the `fact-rules` runner) stamped their own rule id into `tool` instead (`{ tool: "high-fan-out", rule: "high-fan-out" }`), so `clients/turn-summary.ts`'s collapsed one-liner β which groups by `event.tool` β scattered N fact-rule findings into N separate per-rule-name buckets instead of one clean `fact-rules N` bucket like every other runner. Fixed at the emission source in all 13 files (`async-noise.ts`, `async-unnecessary-wrapper.ts`, `cors-wildcard.ts`, `error-obscuring.ts`, `error-swallowing.ts`, `high-complexity.ts`, `high-fan-out.ts`, `high-import-coupling.ts`, `missing-error-propagation.ts`, `no-commented-credentials.ts`, `pass-through-wrappers.ts`, `placeholder-comments.ts`, `unsafe-boundary.ts`) β `rule` is untouched, so `detectFactRuleId` (`clients/dispatch/integration.ts`, rule-first with an id-prefix fallback) and `code-quality-warnings.ts`'s `warning.rule ?? warning.tool` grouping both keep resolving to the specific rule id. Also closed a latent gap the fix surfaced: `error-obscuring.ts` and `error-swallowing.ts` had no `rule` field at all (only `tool` carried the rule id), which would have made `code-quality-warnings.ts`'s `rule ?? tool` fallback collapse them into `"fact-rules"` too post-fix β both now set `rule` explicitly, matching the other 11 rules' existing shape. New test in `tests/clients/turn-summary.test.ts` constructs three fact-rule diagnostics with distinct `rule` values sharing `tool: "fact-rules"` and asserts they collapse into a single `fact-rules 3` bucket via `formatTurnSummaryLine`.
|
|
78
|
+
- **`clientScope: "all"` (the standalone `lsp_diagnostics` tool and `lens_diagnostics_full`) now gets per-server-aware diagnostics timeouts instead of one flat cap for every spawned server** (#573, found investigating the #570 incident: a 150-file `lens_diagnostics_full` scan took 114s and saturated the TypeScript LSP server) β `LSPService.touchFile` (`clients/lsp/index.ts`) already computed a `perServerTimeout` that reads each server's own `aggregateWaitMs` budget from `server-strategies.ts` (TS ~1s, rust-analyzer 3s, opengrep 3.5s, β¦), but only wired it up for the single-server `"primary"` hot path (#203) and, later, `clientScope: "with-auxiliary"` (#242). `clientScope: "all"` fell through to the pre-#203 flat `callerCap ?? modeFloor` branch β a leftover from #203 explicitly deferring the "full/cascade path," never revisited when #242 added per-server budgeting for auxiliaries. Every server (fast primary, slow auxiliary) was held to one shared number, either wasting time on a fast server capped to a slow auxiliary's ceiling (multiplied across a full-workspace scan) or starving a slow auxiliary of the time its own strategy says it needs. Fix: `clientScope === "all"` is now included alongside `"with-auxiliary"` in the `perServerTimeout` branch, so each spawned server's individual `waitForDiagnostics` call is bounded by `min(callerCap, ownStrategyBudget)` rather than the flat number. The touch's overall detection deadline (used only for the `lsp_diagnostics_timeout` latency log) is unchanged β it's still `Math.max(...)` over every spawned server's timeout, so "all" still waits for the slowest server before logging a timeout; nothing about auxiliary coverage regresses. `envWait` (`PI_LENS_LSP_DIAGNOSTICS_MAX_WAIT_MS`) and the existing `"primary"`/`"with-auxiliary"` behavior are untouched β this is purely additive coverage of the previously-flattened `"all"` case. Evaluated whether the richer live capability-matrix data (`getCapabilitySnapshots()`'s push/pull `mode`, `diagnosticProviderKind`) should feed `perServerTimeout` beyond what `server-strategies.ts` already encodes; found no case in the current budgeting logic it would improve (the existing `aggregateWaitMs`/`silentOnClean` table already differentiates push vs. pull servers where it matters) β left as a documented non-integration rather than added complexity; `cascade-tier.ts`'s existing capability-matrix consumption is a separate lane (cascade-lane skip-decision, not touch-time budgeting) and is unaffected. New tests in `tests/clients/lsp/service-touch-collect.test.ts` (`#573`): each server on the `"all"` scope gets its own caller-cap-bounded deadline rather than a shared flat number; a fast primary's own wait isn't held to a slow auxiliary's larger budget; a tight caller cap still binds every server as a ceiling; the env override still wins; and a regression guard confirming `"primary"`/`"with-auxiliary"` per-server behavior is unchanged.
|
|
79
|
+
- **A timed-out LSP diagnostics check no longer presents as a confirmed-clean result and no longer erases known-good diagnostic state** (#570, found via live log analysis on a dogfooding project) β `LSPService.touchFile` (`clients/lsp/index.ts`) already tracked `notifyWriteTimedOut`/`diagnosticsTimedOut` per touch (logged to `lsp_touch_file` latency events) but never used them to gate anything: an inconclusive empty `collected` result was cached and returned identically to a genuinely server-confirmed empty result. Concretely, a timeout unconditionally deleted `lastKnownDiagnostics`/`lastKnownContentHash` for the file (so a hot-path consumer like `actionable-warnings` at `turn_end` would see "no known diagnostics" for a file that may still have real errors), and callers of `touchFile` (the per-edit dispatch runner, the `lsp_diagnostics` tool) had no way to distinguish "confirmed 0" from "timed out, defaulted to 0". Fix: `touchFile` now computes `inconclusive = notifyWriteTimedOut || diagnosticsTimedOut` (deliberately touch-wide/conservative β `collected` merges diagnostics across every spawned server, so even a partial per-server timeout means the merge may be incomplete) and (1) skips the `lastKnownDiagnostics` set-or-delete block entirely when inconclusive, leaving whatever was cached from the last confirmed check untouched; (2) flags the returned diagnostics array with a non-enumerable `inconclusive: true` bonus field (a plain array otherwise β existing callers that only read it as an array are unaffected) so callers that care can check `.inconclusive` without a breaking return-type change across `touchFile`'s ~10 production call sites. Two consumers wired: `clients/dispatch/runners/lsp.ts` (the per-edit path feeding the footer/widget via `recordDiagnostics`) now returns `status: "skipped"` (same treatment as "no LSP client was ready") instead of `"succeeded"` with an empty diagnostics list when the touch was inconclusive β this automatically feeds the existing dispatcher coverage-notice mechanism (`getCoverageNotice`), so an inconclusive edit is flagged the same way a fully-skipped one is. The standalone `lsp_diagnostics` tool (`tools/lsp-diagnostics.ts`, and its `pilens_lsp_diagnostics` MCP mirror, which reuses the same `createLspDiagnosticsTool()`) now threads the priming `touchFile`'s `inconclusive` flag through `collectDiagnosticsForFile`/`collectFileDiagnosticResult` and folds a timed-out check into the existing #533 "unconfirmed" bucket (never a bare "0 diagnostics"), while distinguishing WHY in the rendered text/compact-render/batch-and-directory tallies ("N timed out" vs. #533's "cannot confirm clean β push-only, silent-on-clean") via a new per-result `timedOut` field and `timedOutFiles`/`unconfirmedReasonClause` aggregation, rather than collapsing the two distinct reasons into one misleading message. The non-timeout path is unchanged: a genuinely fast, confirmed empty result still clears the cache and reports clean exactly as before. New tests: `tests/clients/lsp/service-touch-collect.test.ts` (`#570` describe block β a timed-out touch does NOT clear a prior confirmed non-empty `lastKnownDiagnostics` record; a confirmed non-timeout empty result still clears it as before), `tests/clients/dispatch/runners/runner-status-semantics.test.ts` (the lsp runner returns `"skipped"` on an inconclusive touch), `tests/tools/lsp-diagnostics.test.ts` (`#570` describe block β single-file/batch renders distinguish timed-out from confirmed-clean/silent-unconfirmed).
|
|
80
|
+
- **`lens_diagnostics` mode=full and `lsp_diagnostics` now reconcile fresh scan results into the footer/widget-state cache** (#571) β `recordDiagnostics` (`clients/widget-state.ts`), the sole writer to the footer's `allDiagnostics` store, previously had exactly one caller: `pipeline.ts`'s per-edit dispatch. A `lens_diagnostics` mode=full workspace scan or a standalone `lsp_diagnostics` check fetches fresh, authoritative diagnostics for files it examines but, until now, only reported that data back to the caller β never reconciled it into the footer. Practical consequence: if file A's diagnostics only became stale/fresh because of a change in file B (e.g. a shared interface), and A itself was never directly re-edited, the footer for A could stay stale indefinitely even after a scan proved the fresher truth. Both tools now call a new shared choke point, `clients/widget-state.ts`'s `reconcileScanDiagnostics(filePath, diagnostics, confirmed, writeIndex?)`, for each file they get a result for; `index.ts` injects the same monotonic `RuntimeCoordinator.nextWriteIndex()` source `pipeline.ts`'s per-edit writes draw from, so the existing `WriteOrderingGuard` (#555/#560) can't be clobbered by (or clobber) a concurrent, genuinely newer per-edit write for the same file. Guardrail: a result the check can't vouch for is never reconciled. `#570` landed in the same window as this fix (see above) and its `inconclusive` signal (`touchFile`'s non-enumerable `.inconclusive` flag, set when the notify write or the diagnostics wait itself timed out) is what BOTH tools now key off: `lsp_diagnostics` reuses it directly (threaded from `collectFileDiagnosticResult`/`runFileDiagnostics`'s own `timedOut`/`confirmation` fields), and `lens_diagnostics` mode=full's per-file LSP sweep (`runWorkspaceDiagnostics`) reads the same flag off each `touchFile` call's result and ORs it with its own outer per-file deadline/throw (`LSPWorkspaceDiagnosticResult.timedOut`) β either reason skips reconciliation for that file. No new batching/throttling was added for full-scan bursts β `recordDiagnostics`'s render trigger is a standard TUI dirty-flag request (already exercised by today's per-edit multi-file cascades without dedicated batching) and each write is independent/synchronous, so a scan touching many files behaves the same as N sequential per-edit writes already do. New tests: `tests/clients/widget-state.test.ts` (`reconcileScanDiagnostics`'s confirmed/unconfirmed gating and write-ordering-guard interaction), `tests/tools/lens-diagnostics.test.ts` and `tests/tools/lsp-diagnostics.test.ts` (each tool's confirmed-vs-timed-out/unconfirmed reconciliation wiring, including batch mode).
|
|
81
|
+
- **Native TS7 no longer inherits classic typescript-language-server's `silentOnClean` cascade fast path** (#558, reverts #541) β PR #541 (2026-07-11) classified TS7's native `tsc --lsp --stdio` launch variant (PR #526) as `silentOnClean`, letting the cascade lane (`clients/lsp/cascade-tier.ts`) skip its in-lane diagnostic wait for native-ts7 edits on the strength of a clean-signal probe run that appeared to show it silent, same as classic. A 2026-07-12 dual-environment re-measurement (nightly CI on Linux and a live local run on Windows dev, same `typescript@7.0.2` both times) found native-ts7 now publishes 2 version-less diagnostic sets on the clean transition (`cleanPubs=2(v:0)`) β it is NOT silent, a drift from the #541 measurement, and skipping the wait could miss or delay real diagnostics on native-ts7 edits. `cascade-tier.ts`'s classifier again routes a `launchVariant === "native-ts7"` snapshot through the fail-safe full-wait path; `server-strategies.ts`'s `silentOnClean: true` for `"typescript"` is effectively classic-only again. Classic typescript-language-server is unaffected β re-confirmed silent (`cleanPubs=0(v:0)`) in the same run. `scripts/probe-clean-signal.mjs`'s nightly drift check no longer routes native-ts7 rows through classic's shared marker; it now compares them against an explicit `false` expectation, so a future TS7 build that goes silent again surfaces as a `silent-not-marked` signal instead of being silently skipped.
|
|
82
|
+
- **LSP diagnostics no longer briefly cache stale results after rapid edits** β `textDocument/publishDiagnostics` pushes were written into the diagnostics cache (`pushDiagnostics`) unconditionally, even when the server's own reported `version` field showed the push was computed against an EARLIER edit than the one currently in flight (e.g. the server was still finishing analysis of edit N when edit N+1 already landed). `isVersionStale()` (`clientWaitForDiagnostics`'s staleness check) only gated whether a diagnostics *wait* resolved early β it was never consulted by the plain read path (`client.getDiagnostics()`/`getAllDiagnostics()`/`pruneDiagnostics()`), so a late push could get cached and served as "current" until the next genuinely fresh push overwrote it: diagnostics would transiently show stale results right after a rapid edit, then self-correct a moment later once real analysis caught up. The `publishDiagnostics` handler in `clients/lsp/client.ts` now drops a push before it reaches the cache (no `pushDiagnostics.set`, no `diagnosticsVersion` bump, no `diagnostics` event emit) whenever the push reports a version that's behind the currently-tracked document version for that path β checked at write time (after the debounce timer fires, not at notification-receipt time) so a push that arrives fresh but whose debounce window straddles a later edit is still caught. Version-less servers (no `version` field reported) are unaffected β that remains an intentional, documented tradeoff, unchanged by this fix. A dropped push correctly emits nothing, so a pending `clientWaitForDiagnostics` call still falls through to its other resolution paths (a later genuinely-fresh push, or the existing timeout backstop) rather than resolving on stale data. Deliberately out of scope, left as known follow-ups: the pull-diagnostics path (`clientRequestPullDiagnostics`/`clientRequestWorkspaceDiagnostics`) has no version stamp to compare against in this codebase's current handling, so nothing analogous is applied there; and `diagnosticsVersion` remains a single global counter rather than per-path, so an unrelated path's fresh push can still satisfy a wait baselined on this path's version β both are separate, larger-blast-radius changes.
|
|
83
|
+
- **`recordDiagnostics` drops superseded writes to the widget-state diagnostics cache (same race class as #555)** β pi-lens deliberately allows concurrent pipeline runs for the same file across different same-turn edits (dedupe key is `filePath + contentHash`, not just `filePath`), so an older edit's pipeline can still be running analysis (e.g. a slow lint/LSP runner) when a newer edit's pipeline has already finished. `clients/widget-state.ts`'s `recordDiagnostics()` β the store `lens_diagnostics` and the TUI widget read as "current" for a file β previously overwrote a file's diagnostics unconditionally on every call, with no ordering check. If the older edit's write landed after the newer edit's, the diagnostics shown for that file could transiently reflect the older, superseded edit rather than the one currently on disk β self-correcting only if/when a later write happened to land. `recordDiagnostics` now takes an optional `writeIndex` (threaded from the monotonic per-edit token already assigned in `clients/runtime-tool-result.ts`, via `clients/pipeline.ts`) and drops a write whose token lags the last one already recorded for that path β no diagnostics/count/timestamp update and no render trigger for a dropped write, so it can't partially corrupt the winning write's state. Call sites with no ordering token (e.g. `clients/mcp/analyze.ts`'s on-demand recorder) are unaffected, same as version-less LSP servers in the #555 fix. Extracted the guard shape into a small, reusable, non-diagnostics-specific primitive, `clients/write-ordering-guard.ts`'s `WriteOrderingGuard` (a `Map`-backed "only proceed if this token is >= the last-seen token for this key" check), available for #557's tracked follow-up (`code-quality-warnings.ts`, suspected same bug shape) rather than duplicating the check a second time. `clients/lsp/client.ts`'s already-merged #555 fix is left as-is (additive only, not retrofitted).
|
|
84
|
+
- **Dropped the redundant `ast_dump` tool registration** β `createAstGrepDumpTool`/`createAstDumpTool` (`tools/ast-dump.ts`) were two separate `registerTool` calls wrapping the exact same implementation under two names, doubling that tool's weight in the tool list for zero functional benefit. `ast_grep_dump` (the already-documented preferred name, referenced throughout `AGENTS.md`/`docs/agent-tools.md`/the ast-grep skill) is kept; the `ast_dump` alias registration and its now-unused `createAstDumpTool` export are removed.
|
|
85
|
+
- **`bus-events.log` registered with the log-cleanup retention sweep, and `MANAGED_LOG_FILES` replaced with auto-derivation** β #551 added `clients/bus-events-logger.ts` (`~/.pi-lens/bus-events.log`) but never added it to `clients/log-cleanup.ts`'s hand-maintained `MANAGED_LOG_FILES` array, so it grew unbounded and untouched by retention β the third time this exact class of mistake has happened (after actionable-warnings/ast-grep-tools/dead-code, per the module's own doc comment). Rather than patch the array again, `clients/ndjson-logger.ts`'s `createNdjsonLogger` now self-registers the absolute path of every static-`filePath` instance into an exported registry (`getRegisteredLogFiles()`) at construction time β i.e. at the `*-logger.ts` module's own load time, with zero action needed in `log-cleanup.ts`. `log-cleanup.ts`'s new `getManagedLogFiles()` derives its list by unioning: (1) that registry, filtered to the target directory; (2) a direct `~/.pi-lens/*.log` directory read (excluding rotated-backup names via the existing `ROTATED_BACKUP_RE`) as an import-order safety net, in case a future logger module is only ever dynamically imported and hasn't self-registered by sweep time; (3) a small explicit `UNMANAGED_STRAGGLER_LOG_FILES` list (currently just `sessionstart.log`, which predates `createNdjsonLogger` and writes via a bespoke `fs.appendFile` in several modules, so it can't self-register). Both `rotateLogIfNeeded`'s sweep and `getLogStorageSummary` now read from `getManagedLogFiles()` instead of the removed static array β a new global log built on the shared `createNdjsonLogger` writer gets retention/rotation coverage automatically, no second list to remember. Audited the four loggers that separately pass their own `maxBytes`/`backupPath` to `createNdjsonLogger` (`actionable-warnings-logger.ts`, `ast-grep-tool-logger.ts`, `dead-code-logger.ts`, `read-guard-logger.ts`, all rotating at a ~1MB default into a `.log.1` backup) against the centralized 10MB-default sweep (which rotates into a `.<timestamp>.log` backup): both mechanisms are active on the same files, but not in real conflict β the lower per-instance threshold fires first in practice, so the centralized sweep is a rarely-triggered backstop for those four, not a race; left as-is rather than restructuring rotation for an unrelated set of files.
|
|
86
|
+
- **Test runner now resolves mirrored test-tree layouts** (#547) β `TestRunnerClient.findTestFile` only checked same-directory, `dir/__tests__/`, and flat top-level `tests/<basename>` locations, missing the common "mirrored subdirectory" layout (`tests/<same-relative-subdir>/<basename>.test.ts`) used by this repo itself (`clients/knip-client.ts` β `tests/clients/knip-client.test.ts`). As a result, pi-lens's own `turn_end` test-runner integration could only resolve its own tests via the slower/brittle import-scan fallback. `findTestFile` now also checks `tests/<relative-subdir>/` and `__tests__/<relative-subdir>/` for TS/JS exact-match candidates, and the equivalent mirrored directory for the Python glob search (`test_*.py` / `*_test.py`), before falling back to import scanning. Additive β existing same-dir, `__tests__/`, and flat-`tests/` candidates are unchanged and still checked first. Two follow-up gaps closed in the same fix: (1) `detectRunner`'s `node_modules` check only looked in `cwd`, missing hoisted monorepo layouts (npm/yarn/pnpm workspaces) where a workspace package's own `node_modules` doesn't exist and vitest/jest only live at the workspace root several directories up β `findHoistedNodeModulesPackage` now walks up parent directories looking for the runner package, bounded to `MAX_NODE_MODULES_WALK_UP` (5) levels and stopping at the filesystem root, never an unbounded walk. (2) Python suites that group tests by kind (`tests/unit/`, `tests/integration/`) rather than mirroring the source tree still fell through to the import-scan fallback, which doesn't handle `.py` files at all β `findPytestMatchRecursive` now does a depth-bounded breadth-first search under the test root (`MAX_PYTEST_RECURSE_DEPTH`, 3 levels, skipping hidden dirs and `__pycache__`) as a last resort before import scanning, only engaged when the exact-mirror candidates don't match so it never overrides the existing mirrored-match preference. Two more follow-up gaps closed: (3) `getTestRunTarget` called `findTestFile` unconditionally even when the edited file was itself already a test file β e.g. editing `foo.test.ts` directly stripped the extension to basename `foo.test` and searched for nonsense candidates like `foo.test.test.ts`, found nothing, fell through the import-scan fallback (which also finds nothing, since nothing imports a test file), and returned `null` β silently disabling the `turn_end` test-runner integration for the very common case of editing a test file directly. `getTestRunTarget` now checks whether the edited file is itself a test file (reusing the shared `detectFileRole` classifier from `clients/file-role.ts` β no second parallel detector) and, if so, skips discovery entirely and returns the file itself as the target (new `strategy: "self"`), while still preferring the existing failed-first rerun path when that same test file is already in the known-failing set. (4) Added a best-effort, text-only scrape of a vitest config's `test.include`/`test.exclude` arrays (`parseVitestTestGlobs`, cached per `cwd`) as a secondary signal alongside `detectFileRole` β deliberately not a real config load (no ESM/TS execution, which `runtime-turn.ts`'s per-edit `turn_end` hot path can't afford), just a regex extraction of a plain string-literal array when the config is written in that simple shape, falling back to `null` (zero behavior change) for anything more dynamic (function calls, spreads, computed values) or when no vitest config exists.
|
|
87
|
+
|
|
13
88
|
## [3.8.69] - 2026-07-11
|
|
14
89
|
|
|
15
90
|
### Fixed
|
package/README.md
CHANGED
|
@@ -66,7 +66,7 @@ Thanks goes to these wonderful people:
|
|
|
66
66
|
<!-- markdownlint-disable -->
|
|
67
67
|
<table>
|
|
68
68
|
<tbody>
|
|
69
|
-
|
|
69
|
+
<tr>
|
|
70
70
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/apmantza"><img src="https://avatars.githubusercontent.com/u/247365598?v=4" width="100px;" alt=""/><br /><sub><b>Apostolos Mantzaris</b></sub></a><br /><a href="#code-apmantza" title="Code">π»</a> <a href="#doc-apmantza" title="Documentation">π</a> <a href="#ideas-apmantza" title="Ideas & Planning">π€</a> <a href="#maintenance-apmantza" title="Maintenance">π§</a> <a href="#review-apmantza" title="Reviewed Pull Requests">π</a></td>
|
|
71
71
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/anh-chu"><img src="https://avatars.githubusercontent.com/u/34973633?v=4" width="100px;" alt=""/><br /><sub><b>Anh Chu</b></sub></a><br /><a href="#code-anh-chu" title="Code">π»</a></td>
|
|
72
72
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/loss-and-quick"><img src="https://avatars.githubusercontent.com/u/39405619?v=4" width="100px;" alt=""/><br /><sub><b>minicx</b></sub></a><br /><a href="#code-loss-and-quick" title="Code">π»</a></td>
|
|
@@ -74,7 +74,7 @@ Thanks goes to these wonderful people:
|
|
|
74
74
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/tifandotme"><img src="https://avatars.githubusercontent.com/u/33323177?v=4" width="100px;" alt=""/><br /><sub><b>Tifan Dwi Avianto</b></sub></a><br /><a href="#code-tifandotme" title="Code">π»</a></td>
|
|
75
75
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/amit-gshe"><img src="https://avatars.githubusercontent.com/u/7383028?v=4" width="100px;" alt=""/><br /><sub><b>Amit</b></sub></a><br /><a href="#code-amit-gshe" title="Code">π»</a> <a href="#bug-amit-gshe" title="Bug reports">π</a></td>
|
|
76
76
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/fractiunate"><img src="https://avatars.githubusercontent.com/u/78024279?v=4" width="100px;" alt=""/><br /><sub><b>Fractiunate // David RahΓ€user</b></sub></a><br /><a href="#code-fractiunate" title="Code">π»</a></td>
|
|
77
|
-
</tr
|
|
77
|
+
</tr><br />
|
|
78
78
|
<tr>
|
|
79
79
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/vkarasen"><img src="https://avatars.githubusercontent.com/u/7932536?v=4" width="100px;" alt=""/><br /><sub><b>Vitali Karasenko</b></sub></a><br /><a href="#code-vkarasen" title="Code">π»</a></td>
|
|
80
80
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/rfxlamia"><img src="https://avatars.githubusercontent.com/u/222023708?v=4" width="100px;" alt=""/><br /><sub><b>V</b></sub></a><br /><a href="#code-rfxlamia" title="Code">π»</a></td>
|
|
@@ -83,7 +83,7 @@ Thanks goes to these wonderful people:
|
|
|
83
83
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/mehalter"><img src="https://avatars.githubusercontent.com/u/1591837?v=4" width="100px;" alt=""/><br /><sub><b>Micah Halter</b></sub></a><br /><a href="#code-mehalter" title="Code">π»</a></td>
|
|
84
84
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/JulienEllie"><img src="https://avatars.githubusercontent.com/u/670518?v=4" width="100px;" alt=""/><br /><sub><b>Julien Ellie</b></sub></a><br /><a href="#code-JulienEllie" title="Code">π»</a></td>
|
|
85
85
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Istar-Eldritch"><img src="https://avatars.githubusercontent.com/u/3746468?v=4" width="100px;" alt=""/><br /><sub><b>Ruben Paz</b></sub></a><br /><a href="#code-Istar-Eldritch" title="Code">π»</a> <a href="#bug-Istar-Eldritch" title="Bug reports">π</a></td>
|
|
86
|
-
</tr
|
|
86
|
+
</tr><br />
|
|
87
87
|
<tr>
|
|
88
88
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/cjunxiang"><img src="https://avatars.githubusercontent.com/u/26619858?v=4" width="100px;" alt=""/><br /><sub><b>C.Junxiang</b></sub></a><br /><a href="#code-cjunxiang" title="Code">π»</a> <a href="#bug-cjunxiang" title="Bug reports">π</a></td>
|
|
89
89
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/trvon"><img src="https://avatars.githubusercontent.com/u/6031322?v=4" width="100px;" alt=""/><br /><sub><b>Trevon</b></sub></a><br /><a href="#code-trvon" title="Code">π»</a></td>
|
|
@@ -92,7 +92,7 @@ Thanks goes to these wonderful people:
|
|
|
92
92
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/spyrosbazios"><img src="https://avatars.githubusercontent.com/u/37960233?v=4" width="100px;" alt=""/><br /><sub><b>spyrosbazios</b></sub></a><br /><a href="#code-spyrosbazios" title="Code">π»</a></td>
|
|
93
93
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Roman-Galeev"><img src="https://avatars.githubusercontent.com/u/40388226?v=4" width="100px;" alt=""/><br /><sub><b>Roman Galeev</b></sub></a><br /><a href="#code-Roman-Galeev" title="Code">π»</a></td>
|
|
94
94
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/jerryfan"><img src="https://avatars.githubusercontent.com/u/2540814?v=4" width="100px;" alt=""/><br /><sub><b>jerryfan</b></sub></a><br /><a href="#code-jerryfan" title="Code">π»</a></td>
|
|
95
|
-
</tr
|
|
95
|
+
</tr><br />
|
|
96
96
|
<tr>
|
|
97
97
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/shaDmx"><img src="https://avatars.githubusercontent.com/u/91132641?v=4" width="100px;" alt=""/><br /><sub><b>Max L.</b></sub></a><br /><a href="#code-shaDmx" title="Code">π»</a></td>
|
|
98
98
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/radge"><img src="https://avatars.githubusercontent.com/u/129205?v=4" width="100px;" alt=""/><br /><sub><b>David Ryan</b></sub></a><br /><a href="#code-radge" title="Code">π»</a></td>
|
|
@@ -101,7 +101,7 @@ Thanks goes to these wonderful people:
|
|
|
101
101
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/feoh"><img src="https://avatars.githubusercontent.com/u/330070?v=4" width="100px;" alt=""/><br /><sub><b>Chris Patti</b></sub></a><br /><a href="#code-feoh" title="Code">π»</a> <a href="#bug-feoh" title="Bug reports">π</a></td>
|
|
102
102
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/chenxin-yan"><img src="https://avatars.githubusercontent.com/u/71162231?v=4" width="100px;" alt=""/><br /><sub><b>Chenxin Yan</b></sub></a><br /><a href="#code-chenxin-yan" title="Code">π»</a> <a href="#doc-chenxin-yan" title="Documentation">π</a> <a href="#bug-chenxin-yan" title="Bug reports">π</a></td>
|
|
103
103
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/flowing-abyss"><img src="https://avatars.githubusercontent.com/u/98622217?v=4" width="100px;" alt=""/><br /><sub><b>flowing-abyss</b></sub></a><br /><a href="#code-flowing-abyss" title="Code">π»</a></td>
|
|
104
|
-
</tr
|
|
104
|
+
</tr><br />
|
|
105
105
|
<tr>
|
|
106
106
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/xuli500177"><img src="https://avatars.githubusercontent.com/u/62830942?v=4" width="100px;" alt=""/><br /><sub><b>Xu Yili</b></sub></a><br /><a href="#bug-xuli500177" title="Bug reports">π</a></td>
|
|
107
107
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/ricardoraposo"><img src="https://avatars.githubusercontent.com/u/50217712?v=4" width="100px;" alt=""/><br /><sub><b>Ricardo Raposo</b></sub></a><br /><a href="#code-ricardoraposo" title="Code">π»</a></td>
|
|
@@ -110,7 +110,7 @@ Thanks goes to these wonderful people:
|
|
|
110
110
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/PeraSite"><img src="https://avatars.githubusercontent.com/u/19837403?v=4" width="100px;" alt=""/><br /><sub><b>μ μ ν</b></sub></a><br /><a href="#code-PeraSite" title="Code">π»</a></td>
|
|
111
111
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/carolitascl"><img src="https://avatars.githubusercontent.com/u/26188349?v=4" width="100px;" alt=""/><br /><sub><b>Carolina</b></sub></a><br /><a href="#bug-carolitascl" title="Bug reports">π</a></td>
|
|
112
112
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/leakedby"><img src="https://avatars.githubusercontent.com/u/4213260?v=4" width="100px;" alt=""/><br /><sub><b>LeakedBy</b></sub></a><br /><a href="#bug-leakedby" title="Bug reports">π</a></td>
|
|
113
|
-
</tr
|
|
113
|
+
</tr><br />
|
|
114
114
|
<tr>
|
|
115
115
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/mdbooth"><img src="https://avatars.githubusercontent.com/u/1318691?v=4" width="100px;" alt=""/><br /><sub><b>Matthew Booth</b></sub></a><br /><a href="#bug-mdbooth" title="Bug reports">π</a></td>
|
|
116
116
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Wiedzmin89"><img src="https://avatars.githubusercontent.com/u/61706855?v=4" width="100px;" alt=""/><br /><sub><b>Wiedzmin89</b></sub></a><br /><a href="#ideas-Wiedzmin89" title="Ideas & Planning">π€</a></td>
|
|
@@ -119,7 +119,7 @@ Thanks goes to these wonderful people:
|
|
|
119
119
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/ywh555hhh"><img src="https://avatars.githubusercontent.com/u/121592812?v=4" width="100px;" alt=""/><br /><sub><b>Wayne E</b></sub></a><br /><a href="#bug-ywh555hhh" title="Bug reports">π</a></td>
|
|
120
120
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/RimuruW"><img src="https://avatars.githubusercontent.com/u/59136309?v=4" width="100px;" alt=""/><br /><sub><b>RimuruW</b></sub></a><br /><a href="#ideas-RimuruW" title="Ideas & Planning">π€</a></td>
|
|
121
121
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Bjynt"><img src="https://avatars.githubusercontent.com/u/22177300?v=4" width="100px;" alt=""/><br /><sub><b>Bjynt</b></sub></a><br /><a href="#bug-Bjynt" title="Bug reports">π</a> <a href="#ideas-Bjynt" title="Ideas & Planning">π€</a></td>
|
|
122
|
-
</tr
|
|
122
|
+
</tr><br />
|
|
123
123
|
<tr>
|
|
124
124
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/pvtri96"><img src="https://avatars.githubusercontent.com/u/28696888?v=4" width="100px;" alt=""/><br /><sub><b>Tri Van Pham</b></sub></a><br /><a href="#bug-pvtri96" title="Bug reports">π</a></td>
|
|
125
125
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/BingWuJ"><img src="https://avatars.githubusercontent.com/u/117666511?v=4" width="100px;" alt=""/><br /><sub><b>BingWuJ</b></sub></a><br /><a href="#bug-BingWuJ" title="Bug reports">π</a></td>
|
|
@@ -128,7 +128,7 @@ Thanks goes to these wonderful people:
|
|
|
128
128
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/ProbabilityEngineer"><img src="https://avatars.githubusercontent.com/u/38498804?v=4" width="100px;" alt=""/><br /><sub><b>ProbabilityEngineer</b></sub></a><br /><a href="#ideas-ProbabilityEngineer" title="Ideas & Planning">π€</a></td>
|
|
129
129
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/LeonardoRick"><img src="https://avatars.githubusercontent.com/u/17517057?v=4" width="100px;" alt=""/><br /><sub><b>Leonardo Rick</b></sub></a><br /><a href="#bug-LeonardoRick" title="Bug reports">π</a> <a href="#ideas-LeonardoRick" title="Ideas & Planning">π€</a></td>
|
|
130
130
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/kenbanks-peng"><img src="https://avatars.githubusercontent.com/u/26904200?v=4" width="100px;" alt=""/><br /><sub><b>Ken Banks</b></sub></a><br /><a href="#bug-kenbanks-peng" title="Bug reports">π</a></td>
|
|
131
|
-
</tr
|
|
131
|
+
</tr><br />
|
|
132
132
|
<tr>
|
|
133
133
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/rstacruz"><img src="https://avatars.githubusercontent.com/u/74385?v=4" width="100px;" alt=""/><br /><sub><b>Rico Sta. Cruz</b></sub></a><br /><a href="#ideas-rstacruz" title="Ideas & Planning">π€</a></td>
|
|
134
134
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/acidnik"><img src="https://avatars.githubusercontent.com/u/1227955?v=4" width="100px;" alt=""/><br /><sub><b>Nikita Bilous</b></sub></a><br /><a href="#bug-acidnik" title="Bug reports">π</a></td>
|
|
@@ -137,7 +137,7 @@ Thanks goes to these wonderful people:
|
|
|
137
137
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/nodnarbnitram"><img src="https://avatars.githubusercontent.com/u/44812862?v=4" width="100px;" alt=""/><br /><sub><b>Brandon Martin</b></sub></a><br /><a href="#bug-nodnarbnitram" title="Bug reports">π</a></td>
|
|
138
138
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/alpertarhan"><img src="https://avatars.githubusercontent.com/u/50966980?v=4" width="100px;" alt=""/><br /><sub><b>Alper Tarhan</b></sub></a><br /><a href="#bug-alpertarhan" title="Bug reports">π</a></td>
|
|
139
139
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/asolopovas"><img src="https://avatars.githubusercontent.com/u/6893216?v=4" width="100px;" alt=""/><br /><sub><b>Andrius Solopovas</b></sub></a><br /><a href="#bug-asolopovas" title="Bug reports">π</a></td>
|
|
140
|
-
</tr
|
|
140
|
+
</tr><br />
|
|
141
141
|
<tr>
|
|
142
142
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/GrahamJenkins"><img src="https://avatars.githubusercontent.com/u/6607975?v=4" width="100px;" alt=""/><br /><sub><b>Graham Jenkins</b></sub></a><br /><a href="#bug-GrahamJenkins" title="Bug reports">π</a></td>
|
|
143
143
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/wings1848"><img src="https://avatars.githubusercontent.com/u/120104016?v=4" width="100px;" alt=""/><br /><sub><b>Wings Butterfly</b></sub></a><br /><a href="#bug-wings1848" title="Bug reports">π</a></td>
|
|
@@ -146,7 +146,7 @@ Thanks goes to these wonderful people:
|
|
|
146
146
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/fmatray"><img src="https://avatars.githubusercontent.com/u/8267716?v=4" width="100px;" alt=""/><br /><sub><b>FrΓ©dΓ©ric</b></sub></a><br /><a href="#bug-fmatray" title="Bug reports">π</a></td>
|
|
147
147
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/fuentesjr"><img src="https://avatars.githubusercontent.com/u/9240?v=4" width="100px;" alt=""/><br /><sub><b>Salvador Fuentes Jr</b></sub></a><br /><a href="#bug-fuentesjr" title="Bug reports">π</a></td>
|
|
148
148
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Viterkim"><img src="https://avatars.githubusercontent.com/u/17838985?v=4" width="100px;" alt=""/><br /><sub><b>Viktor</b></sub></a><br /><a href="#bug-Viterkim" title="Bug reports">π</a></td>
|
|
149
|
-
</tr
|
|
149
|
+
</tr><br />
|
|
150
150
|
<tr>
|
|
151
151
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/ortonomy"><img src="https://avatars.githubusercontent.com/u/6688676?v=4" width="100px;" alt=""/><br /><sub><b>Gregory Orton</b></sub></a><br /><a href="#bug-ortonomy" title="Bug reports">π</a></td>
|
|
152
152
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/jimallen"><img src="https://avatars.githubusercontent.com/u/868773?v=4" width="100px;" alt=""/><br /><sub><b>Jim Allen</b></sub></a><br /><a href="#bug-jimallen" title="Bug reports">π</a></td>
|
|
@@ -155,7 +155,7 @@ Thanks goes to these wonderful people:
|
|
|
155
155
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/chozandrias76"><img src="https://avatars.githubusercontent.com/u/2087677?v=4" width="100px;" alt=""/><br /><sub><b>Colin Swenson-Healey</b></sub></a><br /><a href="#bug-chozandrias76" title="Bug reports">π</a></td>
|
|
156
156
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/JustlyAI"><img src="https://avatars.githubusercontent.com/u/12634140?v=4" width="100px;" alt=""/><br /><sub><b>Laurent Wiesel</b></sub></a><br /><a href="#bug-JustlyAI" title="Bug reports">π</a></td>
|
|
157
157
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/stark-bit"><img src="https://avatars.githubusercontent.com/u/44064758?v=4" width="100px;" alt=""/><br /><sub><b>Rei Starks</b></sub></a><br /><a href="#bug-stark-bit" title="Bug reports">π</a></td>
|
|
158
|
-
</tr
|
|
158
|
+
</tr><br />
|
|
159
159
|
<tr>
|
|
160
160
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/robertoecf"><img src="https://avatars.githubusercontent.com/u/54923863?v=4" width="100px;" alt=""/><br /><sub><b>Roberto Freitas</b></sub></a><br /><a href="#bug-robertoecf" title="Bug reports">π</a></td>
|
|
161
161
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/tmttodd"><img src="https://avatars.githubusercontent.com/u/160077416?v=4" width="100px;" alt=""/><br /><sub><b>tmttodd</b></sub></a><br /><a href="#bug-tmttodd" title="Bug reports">π</a></td>
|
|
@@ -164,15 +164,17 @@ Thanks goes to these wonderful people:
|
|
|
164
164
|
<td align="center" valign="top" width="14.28%"><a href="http://jasonrimmer.com/"><img src="https://avatars.githubusercontent.com/u/629?v=4" width="100px;" alt=""/><br /><sub><b>Jason Rimmer</b></sub></a><br /><a href="#bug-jrimmer" title="Bug reports">π</a></td>
|
|
165
165
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/ArthurHeymans"><img src="https://avatars.githubusercontent.com/u/15137817?v=4" width="100px;" alt=""/><br /><sub><b>Arthur Heymans</b></sub></a><br /><a href="#ideas-ArthurHeymans" title="Ideas & Planning">π€</a></td>
|
|
166
166
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Blue-B"><img src="https://avatars.githubusercontent.com/u/55532956?v=4" width="100px;" alt=""/><br /><sub><b>Blue-B</b></sub></a><br /><a href="#bug-Blue-B" title="Bug reports">π</a></td>
|
|
167
|
-
</tr
|
|
167
|
+
</tr><br />
|
|
168
168
|
<tr>
|
|
169
169
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/bisko"><img src="https://avatars.githubusercontent.com/u/184938?v=4" width="100px;" alt=""/><br /><sub><b>Biser Perchinkov</b></sub></a><br /><a href="#bug-bisko" title="Bug reports">π</a></td>
|
|
170
170
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/xificurC"><img src="https://avatars.githubusercontent.com/u/4800719?v=4" width="100px;" alt=""/><br /><sub><b>Peter Nagy</b></sub></a><br /><a href="#bug-xificurC" title="Bug reports">π</a></td>
|
|
171
171
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/orest-tokovenko-block"><img src="https://avatars.githubusercontent.com/u/190554467?v=4" width="100px;" alt=""/><br /><sub><b>Orest Tokovenko</b></sub></a><br /><a href="#bug-orest-tokovenko-block" title="Bug reports">π</a></td>
|
|
172
|
+
<td align="center" valign="top" width="14.28%"><a href="https://eisterman.dev/"><img src="https://avatars.githubusercontent.com/u/3028953?v=4" width="100px;" alt=""/><br /><sub><b>Federico Pasqua</b></sub></a><br /><a href="#bug-eisterman" title="Bug reports">π</a></td>
|
|
172
173
|
</tr>
|
|
173
|
-
|
|
174
|
+
</tbody>
|
|
174
175
|
</table>
|
|
175
176
|
|
|
177
|
+
|
|
176
178
|
<!-- markdownlint-restore -->
|
|
177
179
|
<!-- prettier-ignore-end -->
|
|
178
180
|
|