pi-lens 3.8.71 β 3.8.73
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 +152 -1
- package/README.md +31 -22
- package/config/markdownlint/core.json +6 -0
- package/dist/clients/atomic-write.js +83 -0
- package/dist/clients/config-enabled-shape.js +39 -0
- package/dist/clients/deps/js-yaml.js +1 -1
- package/dist/clients/diagnostic-dispositions.js +64 -2
- package/dist/clients/dispatch/integration.js +2 -1
- package/dist/clients/dispatch/runners/ast-grep-napi.js +60 -7
- package/dist/clients/dispatch/runners/lsp.js +54 -25
- package/dist/clients/dispatch/runners/markdownlint.js +7 -4
- package/dist/clients/dispatch/runners/pyright.js +1 -0
- package/dist/clients/dispatch/runners/tree-sitter.js +1 -1
- package/dist/clients/env-utils.js +38 -0
- package/dist/clients/file-utils.js +21 -3
- package/dist/clients/instance-registry.js +32 -21
- package/dist/clients/jscpd-client.js +52 -39
- package/dist/clients/language-profile.js +12 -19
- package/dist/clients/lens-config.js +106 -27
- package/dist/clients/lens-engine.js +129 -3
- package/dist/clients/lsp/cascade-tier.js +1 -38
- package/dist/clients/lsp/client.js +3 -1
- package/dist/clients/lsp/index.js +707 -109
- package/dist/clients/lsp/server-strategies.js +3 -224
- package/dist/clients/lsp/server.js +1 -1
- package/dist/clients/lsp/wait-policy/capability-snapshot.js +1 -0
- package/dist/clients/lsp/wait-policy/classification.js +63 -0
- package/dist/clients/lsp/wait-policy/index.js +10 -0
- package/dist/clients/lsp/wait-policy/strategies.js +244 -0
- package/dist/clients/lsp-budget.js +54 -16
- package/dist/clients/mcp/analyze.js +1 -1
- package/dist/clients/mcp/host-shim.js +12 -8
- package/dist/clients/mcp/ipc.js +118 -0
- package/dist/clients/mcp/session.js +2 -2
- package/dist/clients/pipeline.js +11 -6
- package/dist/clients/project-diagnostics/fresh-fetch.js +20 -1
- package/dist/clients/project-diagnostics/scanner.js +37 -6
- package/dist/clients/project-lens-config.js +167 -3
- package/dist/clients/project-report.js +627 -0
- package/dist/clients/project-scale.js +239 -0
- package/dist/clients/project-scan-policy.js +11 -1
- package/dist/clients/recent-touches.js +6 -11
- package/dist/clients/review-graph/builder.js +170 -13
- package/dist/clients/review-graph/import-resolvers.js +149 -13
- package/dist/clients/review-graph/tsconfig-paths.js +242 -0
- package/dist/clients/review-graph/workspace-modules.js +21 -9
- package/dist/clients/rules-scanner.js +15 -2
- package/dist/clients/runtime-agent-end.js +104 -17
- package/dist/clients/runtime-config.js +4 -0
- package/dist/clients/runtime-coordinator.js +50 -1
- package/dist/clients/runtime-session.js +41 -3
- package/dist/clients/runtime-tool-result.js +4 -3
- package/dist/clients/runtime-turn.js +4 -1
- package/dist/clients/safe-spawn.js +264 -40
- package/dist/clients/session-lifecycle.js +36 -0
- package/dist/clients/session-state-store.js +8 -3
- package/dist/clients/source-filter.js +136 -61
- package/dist/clients/source-walker.js +89 -0
- package/dist/clients/startup-scan.js +189 -111
- package/dist/clients/subagent-mode.js +14 -3
- package/dist/clients/todo-scanner.js +5 -1
- package/dist/clients/tool-policy.js +26 -4
- package/dist/clients/warm-attach.js +204 -0
- package/dist/clients/word-index.js +36 -5
- package/dist/clients/workspace-topology.js +266 -0
- package/dist/index.js +15205 -11330
- package/dist/mcp/server.js +125 -10
- package/dist/tools/ast-grep-search.js +21 -0
- package/dist/tools/lens-diagnostic-mark.js +198 -7
- package/dist/tools/lens-diagnostics.js +119 -21
- package/dist/tools/lsp-diagnostics.js +143 -10
- package/dist/tools/lsp-navigation.js +11 -2
- package/dist/tools/project-report.js +94 -0
- package/dist/tools/symbol-search.js +32 -8
- package/docs/environment-variables.md +10 -0
- package/docs/features.md +7 -1
- package/docs/globalconfig.md +70 -0
- package/docs/servercapabilities.md +1 -1
- package/docs/usage.md +13 -2
- package/package.json +8 -9
- package/rules/tree-sitter-queries/typescript/no-equality-in-for-condition.yml +1 -0
- package/rules/tree-sitter-queries/typescript/no-jump-in-finally.yml +1 -0
- package/scripts/download-grammars.js +1 -0
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,157 @@ All notable changes to pi-lens will be documented in this file.
|
|
|
4
4
|
|
|
5
5
|
## [Unreleased]
|
|
6
6
|
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
### Changed
|
|
10
|
+
|
|
11
|
+
### Fixed
|
|
12
|
+
|
|
13
|
+
## [3.8.73] - 2026-07-28
|
|
14
|
+
|
|
15
|
+
### Added
|
|
16
|
+
|
|
17
|
+
### Changed
|
|
18
|
+
|
|
19
|
+
- **`@types/js-yaml` removed; js-yaml 5 migration complete** (closes #391) β with
|
|
20
|
+
js-yaml 5.2.2 in (#843) the package ships its own declarations, so the stub
|
|
21
|
+
types dev-dependency is gone and the lockfile regenerated. This completes the
|
|
22
|
+
v5 migration started with the namespace-import compatibility pass below.
|
|
23
|
+
|
|
24
|
+
- **`js-yaml` imports are compatible with v5** (refs #391, #843) β use the
|
|
25
|
+
package's namespace exports so the central accessor and YAML rule tooling work
|
|
26
|
+
with both the current v4 dependency and the upcoming v5 release. `@types/js-yaml`
|
|
27
|
+
remains temporarily because js-yaml 4.3.0 needs its declarations; remove it
|
|
28
|
+
with the v5 Dependabot upgrade once the bundled declarations are active.
|
|
29
|
+
|
|
30
|
+
- **`build:dist` now compiles with TypeScript 7.0.2** (refs #809) β the dist emit pin moves from `typescript@6` to the same major as the dev dependency (#600); emitted output was verified byte-identical across all 328 files before the bump.
|
|
31
|
+
|
|
32
|
+
### Fixed
|
|
33
|
+
|
|
34
|
+
- **ast-grep Gate-B now follows LSP liveness and launcher resolution** (refs #868) β the NAPI fallback no longer runs beside a healthy ast-grep LSP when the bundled native binary is resolvable but `ast-grep` is absent from PATH; it still engages when neither resolution path nor a live client is available.
|
|
35
|
+
|
|
36
|
+
- **NAPI fallback unsupported-rule skips no longer spam the session log** (refs #282) β the per-rule terminal lines (one for each of the ~30 non-jsts catalog rules) are replaced by a single aggregated `astgrep_napi_unsupported_rules_skipped` entry in `latency.log` carrying per-language counts and rule ids.
|
|
37
|
+
|
|
38
|
+
- **Missing tree-sitter defect-class metadata is now covered** (refs #863) β loop-termination and `finally` control-flow rules now declare their taxonomy classes, keeping the rule audit complete.
|
|
39
|
+
|
|
40
|
+
- **MCP smoke deadlines scale under CI load (refs #860)** β the shared MCP
|
|
41
|
+
test harness keeps its 20s local default, accepts the
|
|
42
|
+
`PI_LENS_TEST_TIMEOUT_SCALE` multiplier, and the analyze-graph smoke pays
|
|
43
|
+
server startup through a throwaway health call before its assertions.
|
|
44
|
+
|
|
45
|
+
- **TypeScript 7 compatibility fixes (refs #809)** β narrowed ast-grep search test details and made the LSP `workspace/applyEdit` response overload-compatible, unblocking Dependabot PR #600.
|
|
46
|
+
|
|
47
|
+
- **ast-grep NAPI fallback now runs TSX-tagged rules and reports unsupported-language skips** (refs #282) β `.tsx` files use the `tsx` grammar for rule-language scoping, preserving the exact TypeScript/JavaScript twin behavior from #657; rules for languages the fallback cannot evaluate are logged once per rule instead of disappearing as zero matches.
|
|
48
|
+
|
|
49
|
+
- **CSS server root-policy coverage is hermetic on Windows** (refs #855) β the
|
|
50
|
+
server-policy fixture now pins its nearest `package.json` inside the temporary
|
|
51
|
+
workspace, so a marker in the host user's profile cannot change the result.
|
|
52
|
+
|
|
53
|
+
- **Multi-client LSP shutdown no longer serializes the grace-period tail** (#851) β retiring clients now tear down concurrently while generation handoff still waits for every client to settle; instance-registry child removals serialize their read-modify-write without serializing process kills.
|
|
54
|
+
|
|
55
|
+
- **LSP singleton resets no longer overlap live server generations** (closes #850) β `resetLSPService()` still returns synchronously and may allocate a replacement service immediately, but that replacement's first spawn now waits for every older generation's asynchronous teardown to settle. Repeated resets destroy an intermediate waiting generation before it can spawn, and post-root/post-client-cleanup guards prevent a retired service from starting a late process outside `state.inFlight`. The handoff is cleared after its first wait, preserving normal within-generation warm reuse and its hot path; concurrent-secondary sessions still skip destructive reset through the existing ownership guard. New deterministic coverage holds the old client's shutdown open and proves current `master`'s pre-fix second spawn/three-generation accumulation cannot recur.
|
|
56
|
+
|
|
57
|
+
## [3.8.72] - 2026-07-26
|
|
58
|
+
|
|
59
|
+
### Added
|
|
60
|
+
|
|
61
|
+
- **Subagent identity in the instance registry** (#822) β subagent sessions now persist their marker, agent type, parent PID, and run ID in `instances.json` for concurrency-profile analysis; primary and legacy entries remain unchanged.
|
|
62
|
+
|
|
63
|
+
- **Same-workspace warm LSP attach (opt-in soak)** (#822) β with `PI_LENS_WARM_ATTACH=1`, a new session discovers a PID-confirmed, heartbeat-fresh incumbent for the same workspace root and routes per-edit and project-sweep LSP diagnostic touches through its PID-scoped, versioned IPC endpoint instead of spawning a duplicate fleet. Attached dispatch diagnostics now also request bounded code actions from the incumbent, preserving quickfix enrichment without creating a local client; enrichment failures skip quickfixes without promoting because the authoritative diagnostics already succeeded. Sweep warm-up, pre-open, and workspace-pull paths stay dormant while attached; a mid-sweep attach failure permanently promotes the session and resumes the remaining files locally. Content hashes and deadlines guard freshness; any IPC, schema, freshness, timeout, or incumbent-liveness failure permanently promotes the session to today's local LSP path. Decisions and per-file routing sources are logged as `lsp_warm_attach`.
|
|
64
|
+
|
|
65
|
+
- **Resource-aware cross-process LSP budget policy** (closes #821) β budget pressure now shortens the current session's idle reset, lets pull-capable servers rely on pull diagnostics without a push-warm fallback, and can supplement the live-server ceiling with an opt-in aggregate RSS ceiling (`PI_LENS_LSP_BUDGET_RSS_MB`). Missing or stale resource samples fail open to count-only behavior; the existing kill switch disables every policy.
|
|
66
|
+
|
|
67
|
+
- **Review-graph JavaScript/TypeScript import resolution now follows tsconfig project references** (#819) β a bare package import declared through `"references": [{"path": "../lib"}]` resolves to the referenced package's source entry after `paths` aliases and before workspace-package fallback, including transitive references with cycle protection and directory or direct-config targets.
|
|
68
|
+
|
|
69
|
+
### Fixed
|
|
70
|
+
|
|
71
|
+
- **Markdownlint no longer passes the invalid `--disable MD013` CLI option** (closes #833) β unconfigured Markdown projects now use the package-owned default config, which disables MD013 and scopes MD024 to sibling headings so intentional repeated changelog category headings are allowed; project markdownlint-cli2 configs are preserved unchanged, including the CLI's JSONC/YAML/CJS/MJS filename variants.
|
|
72
|
+
|
|
73
|
+
- **`safeSpawnAsync` (and the deprecated sync `safeSpawn`) no longer build a cmd.exe command line from caller-tainted input on Windows** (closes #817, CodeQL alerts #17/#18/#19, CWE-78) β after #815/#816 replaced `shell:true` with an explicit `cmd.exe /d /s /c <hand-built string>` invocation, CodeQL re-flagged the same spawn: the escaping was genuinely unsound (`%VAR%` expands even inside double quotes and cannot be escaped on a `/c` line; cmd's quote-state toggling can defeat `""` doubling), not just a taint false positive. `clients/safe-spawn.ts` now resolves `command` itself via a cached PATH+PATHEXT walk (`resolveWindowsCommand`, a `where`-equivalent, keyed by `command`+`PATH`+`cwd`; reset hook `resetSafeSpawnWindowsCommandCache()` wired into `runtime-session.ts`'s `handleSessionStart` alongside the repo's other session-lived caches). `.exe`/`.com` resolutions β the overwhelming majority of spawns (sg, biome, ruff, git, node, `where.exe`...) β now spawn directly with `shell:false` and a real args array: no command line is ever built, so there's nothing for cmd.exe to reinterpret. `.cmd`/`.bat` shims (npm-installed tool wrappers, the common case) keep the cmd.exe wrapper since they can't run without it, but the interpreter is pinned to `%SystemRoot%\System32\cmd.exe` (never `ComSpec`/`COMSPEC`, addressing alert #18 β the same pin `killTree` already used for `taskkill`) and args are VALIDATED instead of escaped: a value containing `"`, `%`, `!`, or CR/LF is rejected with a clear "Refusing to spawn... via cmd.exe" `SpawnResult.error` rather than spawned with an unsound escape β validated against the *resolved* path (what's actually interpolated into the `/c` line), not just the caller's original command string, so a resolved path itself containing `%`/`!` can't slip through unvalidated either. An unresolvable command now returns a synthesized `ENOENT`-shaped error (message/`code`/`syscall`/`path`) instead of letting cmd.exe report "not recognized" from inside a shell, preserving existing `err.message.includes("ENOENT")`/`err.code === "ENOENT"` tool-not-installed detection at call sites like `sg-runner.ts`. The `chcp 65001` UTF-8 console-code-page mutation is preserved for the tools that needed it (sg/biome/ruff CP850/CP936/CP932 mis-decoding): the `.cmd`/`.bat` wrapper keeps its `chcp` prefix as-is, and a new one-shot `ensureUtf8ConsoleCodePageOnce()` runs the same static, hardcoded `chcp 65001` via a pinned-cmd.exe spawn exactly once per process before the first direct `.exe` spawn (the console code-page mutation persists for the console's lifetime, so once suffices β a strict improvement over today's per-spawn re-run, not a regression). A code-review pass on the first version of this fix found the deprecated synchronous `safeSpawn` (today's only caller: `test-runner-client.ts`'s sync pytest-on-PATH probe) still had the identical `buildWindowsCommand`/`shell:true` sink β its Windows branch (and the now-dead `escapeWindowsArg`/`buildWindowsCommand` helpers it alone used) is reimplemented against the exact same seams (`resolveWindowsCommand`/`findCmdUnsafeValue`/`buildWindowsShellCommand`/`synthesizeEnoentError`) with `spawnSync(..., { shell: false })` throughout; `test-runner-client.ts`'s call site is unchanged (its `detectRunner`/`findTestFile` call chain is synchronous with many sync callers/tests β migrating to async wasn't a trivial, in-scope change here). New coverage in `tests/clients/safe-spawn-windows-command.test.ts` (Windows-conditional, real spawns), mirrored for both the async and sync entry points: direct `.exe` PATH resolution + spawn, a `.cmd` fixture round-tripping an arg with spaces through the pinned wrapper, `%`/`"`-bearing args rejected against a `.cmd` shim with nothing spawned, the same dangerous args passing through literally to a direct `.exe` spawn, an unresolvable command producing the ENOENT-shaped error, and (async-only) resolver-cache reuse/reset. `buildWindowsShellCommand` remains exported (existing #214 regression tests unchanged). Verified end-to-end against the built compiled output for BOTH entry points: a `.cmd` shim (`npx.cmd --version`) and a bare `.exe` (`git --version`) spawn correctly, a `%`-bearing arg to a `.cmd` shim is rejected with nothing spawned, and an unresolvable command yields the ENOENT-shaped error. Related pre-existing cmd.exe-command-line-building patterns found outside `safe-spawn.ts` (`clients/lsp/client.ts`'s PS1βPowerShell fallback shell path, `clients/sg-runner.ts`'s non-bash Windows fallback) are out of scope for this issue (#817 is scoped to `safe-spawn.ts`) and are noted for a follow-up rather than folded in here.
|
|
74
|
+
|
|
75
|
+
- **Multi-server `clientScope: "all"` diagnostic touches now get the same silent-clean confirm #799 gave the single-server hot path** (closes #814) β `lens_diagnostics` mode=full's per-file sweep and `lsp_diagnostics`' `serverScope: "all"` both use `touchFile`'s `clientScope: "all"` path, which #799's clean-confirm gate never covered (it's scoped to `clientScope === "primary" && spawned.length === 1`), so a clean markdown file with marksman (push-only, `silentOnClean`) alongside other servers that all answered still burned marksman's own budget and reported the WHOLE touch `inconclusive`/`diagnosticsTimedOut`, blocking clean-caching and `demonstratedReady`. New `classifyServerWaitTier(serverId, snapshot)` in `clients/lsp/cascade-tier.ts` extracts the per-server classification rule `classifyCascadeWaitTier` (#458) already used for the file's primary server, so both share one classification, not two. `touchFile` (`clients/lsp/index.ts`) now runs a capability-aware aggregate gate after a `clientScope: "all"` touch's diagnostics wait times out: a spawned server counts as "still outstanding" when nothing landed in its per-file diagnostics cache for this touch (`getAllDiagnostics()` β cleared by this touch's own didOpen/didChange, so a present entry can only be a fresh answer, reusing the same #240 doctrine signal `cascade-tier.ts`'s Tier-3 reconcile already trusts); the touch is only resolved as CONFIRMED clean (0 diagnostics from the silent server(s), `diagnosticsTimedOut` cleared, not `inconclusive`) when EVERY still-outstanding server classifies `tier3-silent` (push-only + `silentOnClean`) AND its notify write actually succeeded. One ordinary push-only straggler (not marked `silentOnClean`) or a failed notify write keeps the touch cautious, matching today's behavior exactly. Fails safe (today's timeout/inconclusive behavior) on any classification error. Logs a distinguishable `lsp_silent_clean_confirm` phase with `aggregate: true` and the resolved `serverIds`. The pre-existing single-server `clientScope === "primary"` gate is left unchanged (a documented special case of this more general rule β a future cleanup could fold the two together once both have soaked). New coverage in `tests/clients/lsp/silent-clean-confirm.test.ts`'s "touchFile capability-aware AGGREGATE wait (#814)" describe block: one publisher + one silent server resolves early as confirmed clean with the publisher's findings kept; a failed notify write on the silent server still times out; an ordinary (non-`silentOnClean`) push-only straggler still runs to cap and stays inconclusive; every spawned server silent resolves at the max of their budgets as confirmed clean; and a classification-throwing failure falls back to today's timeout behavior.
|
|
76
|
+
|
|
77
|
+
### Added
|
|
78
|
+
|
|
79
|
+
- **`getWorkspaceTopology` β shared, memoized workspace-topology marker index** (closes #806) β new `clients/workspace-topology.ts` consolidates the per-subsystem per-directory marker walks (`.pi-lens.json`/`pi-lens.json`, `tsconfig.json`, and the workspace-manifest markers `workspace-modules.ts` cares about β `package.json`, `pnpm-workspace.yaml`, `Cargo.toml`, `go.work`) into one shared seam: `getDirectoryMarkers(dir)` collects ALL markers for a directory in a single `readdir` pass, cached and invalidated per-directory by the directory's own mtime; `findNearestDirWithMarker` layers the shared upward-walk discipline on top (`walkUpDirs` + `isAtOrAboveHomeDir` ceiling, an explicit 64-deep cap that logs a `workspace-topology-walk-cap` latency phase instead of truncating silently). Three consumers migrated with zero resolution-semantics change: `project-lens-config.ts`'s `findPiLensConfigInDir` (the #804 nested mutation-control walk's per-dir discovery), `review-graph/tsconfig-paths.ts`'s governing-tsconfig discovery, and `review-graph/workspace-modules.ts`'s `detectWorkspaceType` manifest-presence probes (manifest CONTENT parsing is unchanged/unmoved). New `resetWorkspaceTopology()` is wired into `runtime-session.ts`'s `handleSessionStart` alongside `tsconfig-paths.ts`'s `clearTsconfigPathsCache()` β an added behavior improvement: a `tsconfig.json` edited mid-session is now picked up on the next session/reset instead of never (the matcher cache was previously session-lived with no reset hook at all, noted on #805). Root/language-root resolution is explicitly NOT migrated (#806 lists it as a later follow-up). New `tests/clients/workspace-topology.test.ts` (marker index correctness incl. both `.pi-lens.json` basenames, single-`readdir`-pass collection, mtime invalidation, the reset hook, the home-dir ceiling, and cap-trip logging).
|
|
80
|
+
|
|
81
|
+
- **Root/language-root resolution migrated onto the workspace-topology index β second wave** (closes #807) β `language-profile.ts`'s `resolveLanguageRootForFile` now discovers its per-language-kind root marker (e.g. `tsconfig.json`/`package.json` for `jsts`, `Cargo.toml` for `rust`) via a new `findNearestDirWithAnyBasename(startDir, basenames, homeDir?)` in `clients/workspace-topology.ts` β the generalized, home-guarded/depth-capped/walk-cached counterpart of `findNearestDirWithMarker` for a per-call marker LIST rather than one of `DirectoryMarkers`' typed fields; each `DirectoryMarkers` entry now also carries a raw `entryNames` set (from the same single `readdir`) for this kind of type-agnostic, non-typed-field presence check. `resolveLanguageRootForFile`'s OWN policy is unchanged: it still clamps any discovered root back to `workspaceRoot` via its existing stricter workspace-relative check β the topology service supplies discovery only. `startup-scan.ts`'s `findNearestProjectRoot` now reads per-directory marker presence through `getDirectoryMarkers(dir).entryNames` (one cached `readdir` per directory instead of `PROJECT_ROOT_MARKERS.length` separate `existsSync` stats), but deliberately KEEPS its own hand-written, non-home-guarded walk-UP loop β its callers (`computeStartupScanContext`/`resolveStartupScanContextAsync`) need the actual marker location even at/above `$HOME` to distinguish the `"home-dir"` verdict from `"no-project-root"` (pinned by `tests/clients/startup-scan-home-ceiling.test.ts`); a home-guarded walker would silently misreport that case. A repo-root micro-benchmark (N=1000 resolutions over a synthetic 12-package, 8-levels-deep monorepo fixture) measured the migration as a net win on the hot per-edit path: ~68β75% faster spread across many files, ~79β82% faster for a repeated small working set (the realistic edit-loop pattern) versus the pre-migration hand-rolled `existsSync` loop. Sweep for remaining hand-rolled upward marker probes found several NOT migrated in this pass, each for a documented reason (see PR #807 description): `tool-policy.ts`'s `has*Config` family (already a documented exception, different shape β content-sniffing booleans); `dispatch/runners/{oxlint,vale,shellcheck,shfmt}.ts`, `dispatch/runners/utils/runner-helpers.ts`, `review-graph/git-identity.ts`, `file-utils.ts`, `metrics-history.ts`, `lsp/{config,server}.ts`, and `review-graph/import-resolvers.ts` (all currently un-home-guarded tool-config/VCS-root probes; routing them through the topology service would newly add a home-guard, a behavior change out of scope for an exact-semantics migration); `pipeline.ts`'s/`dispatch/runners/rust-clippy.ts`'s `findNearestContaining` calls for `Cargo.toml`/`pubspec.yaml` (already on the shared `path-utils.ts` primitive, same reasoning); `knip-client.ts`/`dead-code-client.ts` (already on the shared, guarded `findNearestMarkerRoot` primitive); `jscpd-client.ts` (explicitly does not resolve a project root, per its own comment); `package-root.ts` (resolves pi-lens's OWN installed package root from a module URL, a different domain than user-workspace discovery). `clients/project-scale.ts` was left untouched per this PR's constraints (concurrent work) and, on inspection, contains no such probe. Existing tests (`tests/clients/language-profile.test.ts`, `tests/clients/startup-scan-*.test.ts`) pass unmodified; new coverage lives in `tests/clients/workspace-topology.test.ts` for the added `entryNames` field/`findNearestDirWithAnyBasename` surface.
|
|
82
|
+
|
|
83
|
+
- **Empirical monorepo test harness β Track B of the #775 root-resolution audit** (refs #775) β a reusable `makeMonorepo(opts)` fixture builder (`tests/monorepo/fixture.ts`) that builds a temp npm-workspaces repo (root `package.json#workspaces`, N packages with names/deps/files, optional nested `.gitignore`/`.pi-lens.json`, optional file-count padding) plus eight committed regression files (`tests/monorepo/*.test.ts`, 29 tests) covering the audit's test matrix: cross-package graph edges post-#777 (entry + subpath specifiers, non-workspace bare specifiers stay external, `module_report`'s `usedBy`/blast-radius and `computeImpactCascade`'s module-level downstream expansion actually see them); size-cliff behavior (startup-scan's verdict flips exactly at `maxSourceFiles`, sync+async; review-graph bails above its cap and β answering the audit's open question β `getCachedReviewGraph` returns `undefined` after a fresh size-skipped build with no prior warm graph, but keeps serving a now-STALE prior graph once one already existed, and `module_report` degrades identically to a cold cache either way); the post-#779 `maxProjectFiles` scale knob changing effective startup-scan/review-graph budgets end-to-end via `.pi-lens.json`; session-in-package-subdir root-resolution scoping; nested `.gitignore` + `.pi-lens.json` layering (root+package-local gitignore both honored; a package-local `.pi-lens.json`'s `maxProjectFiles` wins for a package-scoped cwd, but its `ignore` field is never consulted β only the root's is, since the ignore matcher always re-anchors to the git root first); diagnostics-scanner `scanTruncated` reaching `lens-engine.ts`'s `projectScan` seam unmodified; `computeImpactCascade`'s module-graph fallback requiring an explicit `cwd` (present on the primary per-edit cascade path, absent β though available in scope β on `runBlastRadiusInBackground`'s call site); and a pnpm-store-shaped symlink layout (junction-based, Windows-safe) extending #777's symlink-safety pinning to `source-filter.ts`'s walker, including `followSymlinks:true` being observably inert for a Windows junction. Two `KNOWN GAP (#775)`-marked tests pin current (unfixed) behavior rather than asserting an ideal outcome. Tests-only β no production code changed.
|
|
84
|
+
|
|
85
|
+
- **New `project_report` registered tool (+ MCP mirror `pilens_project_report`)** (closes #773) β the top of the discovery funnel (`project_report` β `module_report` β `read_symbol`), answering "orient me in this project" purely from data the review graph already computes: a trust header (graph freshness, file coverage vs. the persisted project snapshot's file count, and the exact/import/receiver-type/name-only edge-resolution-quality mix, reusing `module_report`'s existing `resolution` tags), hubs (top fan-in files with `blastRadius` and a one-line role from their most-imported exported symbols), entry points (near-zero fan-in / high fan-out files), a directory-level subsystem map (first-path-segment clustering that collapses to a deeper segment only when one segment dominates the file set, directory-to-directory import edge counts, Tarjan-SCC cycle detection worst-first, and minority-direction layering violations for any directory pair with edges both ways), risk hotspots (fan-in Γ max per-symbol cyclomatic complexity), and suspected dead weight (zero-importer non-entry-point files, always shipped with a low-confidence disclaimer for dynamic-import/runtime-registration/test-only-reachability false positives). Every ranked list is capped by a single `limit` knob and re-rankable via an optional `focus` hint (module_report's `recommendedReads` ranking pattern). Read-only over the cached graph, mirroring `module_report`'s #256 no-build contract and `symbol_search`'s #348 cold-cache contract: a cold graph kicks off a single deduped background build (`buildOrUpdateGraph`, home-ceiling-guarded) and returns `available: false` with an actionable retry hint, never blocking the call. `view: "compact"` renders a line-oriented text summary instead of JSON, matching `module_report`'s convention. New engine module `clients/project-report.ts` (never imports from `tools/`, per the repo's layering rule) plus a thin `tools/project-report.ts` wrapper; registered as an always-active tool in `index.ts` alongside `module_report`/`symbol_search` (`tests/index-wiring.test.ts` updated), and mirrored in `mcp/server.ts` as `pilens_project_report` (schema/registration covered by `tests/mcp/server.smoke.test.ts`). Middle-man detection is deliberately NOT surfaced (a documented v1 deviation): the review graph doesn't persist that signal on symbol nodes today β `module_report` computes it per-file, on demand, from raw file content β and re-deriving it project-wide would mean re-scanning every class-bearing file on this never-blocks read path. New `tests/clients/project-report.test.ts` (cold path non-blocking + background build, all six warm-path section shapes, the `limit` knob scaling every cap, a synthetic-fixture directory cycle via Tarjan SCC, a synthetic-fixture minority-direction layering violation, the dead-weight disclaimer always present, and `focus` re-ranking hub order) and `tests/tools/project-report.test.ts` (cold/warm/compact-view wrapper behavior).
|
|
86
|
+
|
|
87
|
+
### Changed
|
|
88
|
+
|
|
89
|
+
- **Explicit `lsp_diagnostics` file batches now expose a consolidated contract** (refs #837) β runtime path lists are normalized and rejected above the 100-file cap (never silently truncated), preserve input order, enforce bounded per-file deadlines and abort-aware completion, and return honest per-file outcomes (`clean`, `findings`, `unsupported`, `unavailable`, `failed`, `inconclusive`) with aggregate counts. Primary-language diagnostics remain separated from auxiliary findings, and incomplete batches are rendered as unconfirmed.
|
|
90
|
+
|
|
91
|
+
- **Incumbent LSP wait policy now has one process-neutral module boundary** (#822) β per-server strategies, tier classification, and capability-snapshot types moved under `clients/lsp/wait-policy/` with compatibility re-exports at the old paths; this is an internal refactor with no behavior change.
|
|
92
|
+
|
|
93
|
+
- **Review-graph file budget now tapers instead of scaling flatly or cliff-dropping above 1,000 files** (refs #775 R1) β `project-scale.ts`'s `getReviewGraphMaxFilesDerived` keeps today's exact 0.5Γ `maxProjectFiles` ratio (1,000 files at the 2,000 default base) unchanged for every project at or below a 4,000-file `maxProjectFiles` base, then tapers growth asymptotically toward a 5,000-file hard ceiling for larger bases instead of continuing linearly forever. The ceiling is bound FIRST by the review graph's own disk-persist circuit-breaker (`GRAPH_PERSIST_MAX_ELEMENTS_DEFAULT` = 200,000 graph elements in `review-graph/builder.ts` β above it the graph is never persisted, logged + skipped, so a repo over that line pays a full cold build every session instead of once): at ~30 elements/file for a symbol-dense repo, 5,000 files β 150,000 elements, a ~25% margin below the persist cap (a 6,000-file ceiling considered during review left only ~10% margin β too little for a denser-than-average repo). SECOND and independently, it's consistent with the graph's measured per-file build cost (CHANGELOG 3.8.43: "2-4 s" full cold build at the then-1,000-file cap, ~2-4 ms/file; 5,000 files β 10-20 s worst-case, one-time and disk-cached thereafter). New `.pi-lens.json#reviewGraph.maxFiles` knob (R2) lets a monorepo opt into a bigger graph explicitly, bypassing the taper (tolerant numeric parse, clamped to `[100, 20_000]`, warn-once on invalid β mirrors `maxProjectFiles`'s parsing); the pre-existing `PI_LENS_REVIEW_GRAPH_MAX_FILES` env override still wins outright over both. Truncation (files found over the cap) now always logs a `review_graph_size_skip` latency phase directly from `review-graph/builder.ts`'s skip branch (R3), independent of which caller triggered the build, instead of relying on `dispatch/integration.ts`'s cascade-only `graph_build` log β closing a silent-cap gap for the other four `buildOrUpdateGraph` call sites (`lens-map.ts`, `project-report.ts`, `mcp/analyze.ts`, `runtime-session.ts`, `dispatch/runners/tree-sitter.ts`). Documented in `docs/globalconfig.md`'s project-config section. New tests: `taperedReviewGraphMaxFiles` shape (below/at/above the linear boundary, half-gap-at-taper-scale property, ceiling asymptote, monotonicity), the config knob's parse/clamp/warn-once behavior, and the new latency phase.
|
|
94
|
+
|
|
95
|
+
- **Review-graph JS/TS imports now honor `compilerOptions.baseUrl` and `paths` aliases, including relative `extends` chains** (refs #775) β aliased imports resolve to real workspace files before workspace-package/external fallback, with longest-prefix matching and existing extension/index probing.
|
|
96
|
+
|
|
97
|
+
- **Nested mutation controls now layer per edited file** (refs #792) β monorepo package `.pi-lens.json` files use closest-wins resolution independently for `format.enabled`, `autofix.enabled`, and `actionableWarnings.autoFix.enabled`, with explicit CLI disables still highest precedence and mtime-keyed per-directory config caching.
|
|
98
|
+
|
|
99
|
+
- **`symbol_search`/`pilens_symbol_search` gained `paths`/`lang` filters, per-hit `suggestedNext` discovery hints, and warm-review-graph annotations** (closes #771) β three additive ergonomics improvements over the registered tool (`tools/symbol-search.ts` β `clients/lens-engine.ts`'s `symbolSearch`, mirrored in `mcp/server.ts`), all reusing existing machinery: (1) optional `paths` (glob array, same shape/semantics as `ast_grep_search`'s `paths` β a bare directory/file entry scopes its subtree) and `lang` (same identifiers as `ast_grep_search`'s `lang`, mapped to extensions) scope the word index BEFORE ranking via a new `RankOptions.fileFilter` on `searchWordIndex` (`clients/word-index.ts`), so a surviving file's BM25/priors/centrality score is identical to an unfiltered run β omitting both params reproduces prior output byte-for-byte; (2) every hit now carries a `suggestedNext: { tool: "module_report", path }` hint (mirrors `ast-grep-search.ts`'s `suggestedDump` pattern), making the symbol_search β module_report β read_symbol discovery funnel machine-actionable instead of prose-only; (3) hits gain an optional `annotations: { fanIn, complexity }` β `fanIn` reuses the SAME reverse-dependency centrality value already computed for the ranking boost, `complexity` is the file's highest recorded per-symbol cyclomatic complexity from the review graph β populated only when `getCachedReviewGraph` (`clients/review-graph/builder.ts`, dynamic-imported to keep it off the hot path when unused) finds an already-warm graph; this accessor is READ-ONLY and never builds/blocks, so a cold graph simply omits `annotations` with no added latency. `symbolSearch` is now `async` (the dynamic import) β an additive signature change; both call sites (`tools/symbol-search.ts`, `mcp/server.ts`) now `await` it. New/extended coverage: `tests/clients/word-index.test.ts` (`fileFilter` score-preservation + omitted-filter equivalence), `tests/tools/symbol-search.test.ts` (paths/lang filtering, no-filter parity, per-hit `suggestedNext`, graph-cold no-annotation-no-build, graph-warm annotation presence), and `tests/index-wiring.test.ts` (asserts `paths`/`lang` are actually registered on the tool's parameter schema).
|
|
100
|
+
|
|
101
|
+
- **New `lazyEnvNumber(envName, fallback)` factory in `clients/env-utils.ts`** (closes #763) collapses the repeated "lazy + memoized, `toPositiveFinite(process.env[name])`-or-fallback" getter triple β each instance was a module-level memo variable plus a `getX()`/`_resetXForTests()` pair, ~20 lines apiece β into one line per call site while keeping every exported name/signature unchanged so callers and tests don't churn: `clients/startup-scan.ts`'s `getStartupScanVerdictTtlMs`/`_resetStartupScanVerdictTtlForTests` and `getStartupScanMaxEntries`/`_resetStartupScanMaxEntriesForTests` now re-export `lazyEnvNumber(...).get`/`._resetForTests`. `runtime-config.ts`'s `getRunnerTimeoutFloorMs` (also reads a config-file value and takes `Math.max` of the two sources), `slow-fs.ts`'s threshold resolver (memoized per-cwd probe result, not per env-value, and recomputed on every probe rather than cached standalone), and `subagent-mode.ts`'s classification cache (boolean/string shape, not a number) were left as-is β none match the number-only lazy-memoized-getter shape the factory targets. No behavior change. New `describe("lazyEnvNumber", ...)` block in `tests/clients/env-utils.test.ts` covers default/override/non-finite-fallback/memoization-until-reset/no-env-read-at-construction/independent-instance-memo, mirroring `tests/clients/startup-scan-entry-budget.test.ts`'s existing getter tests (left unmodified and still passing).
|
|
102
|
+
|
|
103
|
+
### Fixed
|
|
104
|
+
|
|
105
|
+
- **Windows subprocess arguments no longer pass through Node's `shell:true` concatenation** (CodeQL alert #17 / CWE-78) β `safeSpawnAsync` now invokes `cmd.exe` explicitly with `shell:false` and `windowsVerbatimArguments` so its manually quoted `/c` payload is not escaped a second time by Node. User-controlled paths and arguments remain arguments instead of becoming additional commands; regression coverage includes a shell-metacharacter argument and Windows command paths with spaces.
|
|
106
|
+
|
|
107
|
+
- **CI drift alerts no longer interpolate third-party output into shell code** β compat-smoke passes layer outcomes and versions through environment variables, and install-smoke disables persistent checkout credentials before running package-install tests. Intentional pinned ad-hoc package installs are documented with targeted zizmor suppressions.
|
|
108
|
+
|
|
109
|
+
- **Windows temp-dir teardown no longer fails tests with EPERM/ENOTEMPTY** (closes #810) β root cause of the deterministic `tests/clients/runtime-session.test.ts` failures (2 of 12, reproduced on clean master on this machine): in "full" mode with a `package.json` present, `scheduleStartupScans` (`clients/runtime-session.ts`) fires `todo` and `word-index` as fire-and-forget background tasks (`setImmediate`) that do REAL, unmocked fs reads against the test's temp project dir β unlike knip/jscpd/ast-grep-exports/etc, which the test stubs out entirely. The test helper's `runSessionStart` returned as soon as `handleSessionStart`'s own promise resolved, so the caller's `finally { env.cleanup() }` could run while those two tasks were still mid-read, racing the recursive `rm` against an open file handle inside the temp dir (deterministic here, not a load flake). Fixed by tracking the real in-flight-scan set (via `markStartupScanInFlight`/`clearStartupScanInFlight`, the same primitive production code uses) and having the returned `cleanup` wait for `todo`/`word-index` to settle before deleting the directory β `call-graph`/`codebase-model` are deliberately excluded since they're staggered 5+ seconds out by design and a stale/missing analysisRoot by the time they fire is a harmless no-op. Also added ONE shared hardened temp-dir cleanup helper, `removeTempDirSync` (`tests/clients/test-utils.ts`), implementing the #793 pattern (`maxRetries: 5, retryDelay: 200`) wrapped so a final failure warns via `console.warn` instead of failing the suite β a leftover temp dir is harmless, a red run from teardown is not β and swept every ad-hoc `fs.rmSync(dir, { recursive: true, force: true })` directory teardown across `tests/` onto it (~104 files), including two bespoke retry-and-swallow sites (`tests/clients/lens-map.test.ts`, `tests/clients/ast-grep-rule-precedence-followups.test.ts`) that had hand-rolled the same pattern locally. Single-file (non-recursive) `fs.rmSync` calls that delete one fixture file mid-test were left as-is (bespoke, not directory teardown).
|
|
110
|
+
|
|
111
|
+
- **`lens_diagnostic_mark` no longer trusts a stale caller-supplied `line` verbatim β it verifies/reanchors against current diagnostics before writing anything** (closes #802) β the tool handler (`tools/lens-diagnostic-mark.ts`) previously hashed/wrote at exactly the `line` the agent passed, with no check that the file's CURRENT content still put that finding there. A stale line silently rotted `false-positive`'s strict content-hash anchor (it never matched the real finding again) and, worse, made `suppress` write the inline `pi-lens-ignore` comment above the WRONG statement β the self-inflicted case being a top-down batch-suppress in one file, where each inserted comment shifts every later line down by one and invalidates the batch's own remaining coordinates. New `verifyLine` cross-checks the caller's tool/rule/normalizedMessage against `clients/widget-state.ts`'s live per-file diagnostics (`getFileDiagnostics`, reusing `diagnostic-dispositions.ts`'s own now-exported `normalizeMessage` β not a second normalizer) and reanchors to the diagnostic's CURRENT recorded line when it differs (multiple same-rule/message matches on different lines pick the one closest to the caller's line, the conservative choice for suppress). When widget-state has nothing for this file/finding (e.g. a fresh session), a fuzzy fallback accepts the caller's line if it's in-bounds and non-blank, else searches Β± 5 lines for the nearest non-blank line; if nothing plausible is found, the tool returns `isError` instead of guessing. `verifyLine` is internally defensive (never throws), and any unexpected failure degrades safely per-disposition: `false-positive`/`defer`/`flagged` fall back to trusting the caller's line (today's pre-fix worst case β a rotted anchor, not a corrupted write), while `suppress` refuses to write at an unverified position rather than risk a misplaced comment. The tool's description now documents line verification and recommends bottom-up ordering for multi-suppress in one file. New coverage in `tests/tools/lens-diagnostic-mark.test.ts`: stale-line-with-widget-match reanchoring (both for `false-positive`'s stored anchor and `suppress`'s comment placement), unchanged behavior when no widget state exists, out-of-bounds/all-blank-nearby lines erroring without writing, and a top-down two-finding batch-suppress in one file landing both comments correctly via reanchoring.
|
|
112
|
+
|
|
113
|
+
- **Silent-clean workspace-indexing LSP warm-ups no longer pay the generic 20-second cold floor** (refs #832) β `ensureWarmForSweep` now uses the configured short strategy wait when live capability classification identifies a workspace-indexing push-only server that is silent on clean files, while TypeScript-like cold-indexing servers retain the longer budget; ambiguous capability data fails closed.
|
|
114
|
+
|
|
115
|
+
- **Cold `lens_diagnostics` full sweeps no longer burn 2x20s on marksman for a clean markdown project, and no longer re-pay a failed warm-up on every retry sweep** (closes #799) β root cause was two compounding gaps: (1) marksman is push-only and publishes NOTHING on a clean file, so `ensureWarmForSweep`'s `warmupOverride` floor (`clients/lsp/index.ts`) guaranteed the full 20s warm-up budget on both the initial attempt AND its #744 retry, with zero diagnostics either time; (2) that inconclusive result never marked the server `demonstratedReady`, so a follow-up sweep in the same session re-paid the same 2x20s all over again. Fixed both: marksman now carries `silentOnClean: true` (`clients/lsp/server-strategies.ts`), and `touchFile`'s diagnostics wait gained a generic push-only clean-confirm fallback β reusing the existing `classifyCascadeWaitTier` (#458) capability-snapshot classification rather than typescript's TS-specific sync-command race β so a single-server primary-scope touch that times out with a successful notify write and a `silentOnClean` push-only server is now CONFIRMED clean (and marked `demonstratedReady`) instead of inconclusive. `ensureWarmForSweep`'s `warmupOverride` floor is now also scoped to the FIRST attempt only (`LSPTouchFileOptions.warmupAttempt`) β a retry no longer re-floors to the full cold-start budget, respecting the server's own (much shorter) strategy wait instead. Second: a new session-scoped negative cache (`LSPState.demonstratedCold`, keyed identically to `demonstratedReady`) records a server whose warm-up genuinely stayed cold after both attempts; a later sweep this session sees the cache and skips straight to the existing #744 group-skip accounting instead of re-running the warm-up round trip, logging a distinguishable `lsp_sweep_warmup_cached_cold` phase (and `skippedFromCache` metadata on the group-skip log) so this reads differently from a fresh failure. The cache clears automatically the moment a server demonstrates readiness through any path, and is discarded wholesale on `resetLSPService` (a fresh `LSPService` instance starts with an empty set) so a new session always retries fresh. New coverage in `tests/clients/lsp/sweep-warmup.test.ts` and a new `tests/clients/lsp/silent-clean-confirm.test.ts`.
|
|
116
|
+
|
|
117
|
+
- mutation-control config follow-ups (#792): global `autofix.enabled` now honored (docs promised it, code ignored it), the MCP LSP-navigation tool now resolves project config from the request cwd, global+project config share one `{enabled}` parser with warn-once on invalid values, and mutation-skip debug lines name the deciding config source
|
|
118
|
+
|
|
119
|
+
- deferred auto-format queue: records are now owned by the queuing session/turn β a read-only or concurrent-secondary subagent turn no longer flushes and formats another turn's pending files (#791)
|
|
120
|
+
|
|
121
|
+
- compat-smoke behavioral harness: scratch-dir cleanup crash (ENOTEMPTY race) no longer discards assertion results and falsely reports Layer B contract drift (#785)
|
|
122
|
+
|
|
123
|
+
- **A review graph no longer keeps serving a stale pre-cap snapshot once a repo grows past the review-graph file cap, and `project_report` now says so honestly** (closes #782) β the #775 harness pinned a `KNOWN GAP`: once a repo crossed `maxProjectFiles`-derived cap (`clients/project-scale.ts`)/`PI_LENS_REVIEW_GRAPH_MAX_FILES`, `getCachedReviewGraph` (`clients/review-graph/builder.ts`) kept indefinitely returning any graph cached/persisted from BEFORE the crossing β `usedBy`/`blastRadius`/fan-in silently reflected an old, smaller repo β and every consumer (`module_report`, `symbol_search` annotations, `project_report`) had no way to tell "graph disabled because the repo is too big" apart from "cold cache, build in progress"; `project_report`'s cold-path hint told the caller to "retry shortly", which is actively wrong when the build will never succeed at the current cap. The `too_many_files` skip branch now records a 15-minute-TTL'd size-skip verdict per cwd (`getReviewGraphSizeSkipVerdict`, TTL configurable via `PI_LENS_REVIEW_GRAPH_SIZE_SKIP_TTL_MS`, matching `project-report.ts`'s existing `STALE_THRESHOLD_MS`) β chosen over persisting the verdict to disk because every real caller either rebuilds within a session or restarts the process, unlike startup-scan's `too-many-source-files` verdict which is cached across process starts specifically to skip a slow walk. `getCachedReviewGraph` now consults that verdict before serving either cache tier, so a repo confirmed over cap gets a cold read instead of a silently stale one; a repo shrink or a raised cap clears the verdict immediately on the next successful build (or lets it lapse on TTL). `project_report`'s cold-path branches on the verdict to return `"review graph disabled: project has N files, cap is M β raise maxProjectFiles in .pi-lens.json or set PI_LENS_REVIEW_GRAPH_MAX_FILES"` instead of the generic retry hint, and skips kicking off another background build that would just re-hit the same cap. `tests/monorepo/size-cliff.test.ts`'s `KNOWN GAP (#775)` pinning test is flipped to assert the fixed behavior, plus new tests for verdict TTL expiry and the immediate clear-on-success path.
|
|
124
|
+
|
|
125
|
+
- **Package-local `.pi-lens.json` `ignore` patterns are now honored β layered the same way nested `.gitignore`s are** (closes #783) β the walk-wide ignore matcher (`clients/file-utils.ts`'s `getProjectIgnoreMatcher`) re-anchors to the git root before loading `.pi-lens.json`, so a package-local config's `ignore` field was silently never consulted, even for files inside that package (found empirically by the #775 Track B harness and pinned as a KNOWN GAP in `tests/monorepo/nested-ignore-config.test.ts`). `buildProjectIgnoreMatcher`'s per-ancestor-directory pattern collection (`patternsForDir`, the same layering that already merges nested `.gitignore`s) now also loads a `.pi-lens.json`/`pi-lens.json` found directly in each ancestor directory between the git root and the target β via new no-upward-walk helpers `findPiLensConfigInDir`/`loadPiLensConfigInDir` in `clients/project-lens-config.ts` that reuse the existing path+mtime config cache, so no JSON is re-read per file β and merges its `ignore` patterns anchored relative to that config's own directory (same anchoring semantics as a `.gitignore` there), tagged with the `pilens` layer so #703's "tracked files are never rescued from pi-lens-native intent" rule applies unchanged. The per-directory nested cache key now includes the config file's mtime alongside the `.gitignore` mtime, so editing a nested config takes effect without a restart. Root-config `ignore` behavior, precedence (`global` β `gitignore` β `pilens`, closer directory wins), and `maxProjectFiles`' upward-walk discovery are all unchanged; a nested config's patterns never apply outside its own directory subtree. The pinning test is flipped to assert package-local patterns ARE honored, with new coverage for root-config patterns still applying project-wide and for a nested config's identically-named pattern NOT leaking into sibling packages or the repo root; `docs/globalconfig.md`'s `ignore` section documents the layering.
|
|
126
|
+
|
|
127
|
+
- **Two Track B (#775) follow-ups from the empirical monorepo test harness** (closes #781, closes #784): (1) `runBlastRadiusInBackground` (`clients/dispatch/runners/tree-sitter.ts`) now passes `cwd` through to `computeImpactCascade` β `cwd` was already in scope (used the line above for `buildOrUpdateGraph`) but dropped at this call site, so the background blast-radius path's workspace module-graph fallback could never engage in a monorepo, unlike the primary per-edit cascade path (`clients/dispatch/integration.ts`) which already passed it correctly; the `tests/monorepo/impact-cascade-cwd.test.ts` pinning test that previously marked this a `KNOWN GAP` now asserts the fallback engages. (2) `scanTruncated` (the #760 scanner-budget flag) reached `lens-engine.ts`'s `projectScan` seam intact but nothing rendered it, so a capped scan read as a complete clean sweep to the agent/user β new `scanTruncationNotice(snapshot)` helper in `clients/lens-engine.ts` renders a one-line notice ("scan truncated at N file(s) β results are partial; raise maxProjectFiles in .pi-lens.json to scan fully.", matching the #777 warm-skip notify's style) that `mcp/server.ts`'s `pilens_project_scan` tool and `tools/lens-diagnostics.ts`'s `mode=full` renderer now both append; `tests/monorepo/diagnostics-truncation.test.ts` gained a test asserting the notice renders once truncated and stays silent otherwise.
|
|
128
|
+
|
|
129
|
+
- **Three monorepo quick-wins from the #775 root-resolution audit** (refs #775): (1) a size-skipped warm pipeline (`too-many-source-files`/`too-many-entries` startup-scan verdict, `clients/runtime-session.ts`) now fires a single visible `notify` β mirroring the existing slow-FS notice's shape/severity β instead of only a debug-log line, naming the `PI_LENS_STARTUP_SCAN_MAX_ENTRIES` override when that's the tripped budget; fires once per session even though heavy scans, the TODO scan, and the dominant-language LSP pre-warm all skip off the same verdict. (2) the audit's suspected symlink-cycle hang in the startup-scan counting walk (`clients/startup-scan.ts`) is verified a NON-issue and pinned by tests instead of "fixed": `fs.Dirent` reports a symlink-to-directory as `isSymbolicLink()`/not-`isDirectory()` (junctions included), so the visitor's directory branch never traverses symlinked dirs at all and a cycle cannot hang the walk β the misleading "always follows symlinks" doc claim is corrected and new tests pin both boundedness and the symlinked-subtree-not-counted behavior so a future stat-through-symlinks refactor can't silently reintroduce the risk. (3) `resolveJsTs` (`clients/review-graph/import-resolvers.ts`) and the warm graph builder's `localImportToFile` (`clients/review-graph/builder.ts`) now resolve a bare specifier (`@scope/pkg` or `@scope/pkg/subpath`) that names a sibling workspace package β detected via the existing `workspace-modules.ts` npm/pnpm/cargo/go.work scanner, memoized per cwd by `buildModuleGraph` so the workspace scan costs one filesystem pass per graph build, not one per import edge β to that package's entry file (`package.json` `main`/`module`/`exports`-main, falling back to `index.ts`/`.js`/`src/index.ts`) or, for a subpath import, a file within the package resolved with the same source-twin-preferring extension probe the relative resolver uses; a specifier matching no known workspace package still returns `[]` (external), unchanged. New coverage: `tests/clients/runtime-session-warm-skip-notify.test.ts` (notify fires once on an over-budget seeded verdict, names the entries override, stays silent on a normal small project), `tests/clients/startup-scan-symlink-cycle.test.ts` (self-loop and mutual-symlink fixtures complete instead of hanging, sync + async), and workspace-package fixtures in `tests/clients/review-graph/import-resolvers.test.ts` and `tests/clients/review-graph.service.test.ts` (cross-package entry/subpath edges, non-workspace bare specifiers stay external).
|
|
130
|
+
|
|
131
|
+
- **`collectSourceFiles`/`collectSourceFilesAsync` (`clients/source-filter.ts`) now bound the walk by directory entries VISITED, not just source files FOUND** (closes #760 β the #758 escape class at the remaining non-startup-verdict call sites) β the walks' only hard cap was the `maxFiles` RESULTS cap (#250), so a mixed tree with few source files but a huge pile of non-source files (the #758 reporter's shape: ~300 scripts among ~84k game-mod data files) never tripped it and still got a full-tree walk, dominated by one `ignoreMatcher.isIgnored()` call per entry β hitting the review-graph rebuild's per-edit hot path (`clients/review-graph/builder.ts`), the `lens_diagnostics` project scan (`clients/project-diagnostics/scanner.ts`), the SYNC caller-blocking todo scan (`clients/todo-scanner.ts`), and word-index build paths not gated by the #758 startup verdict (`clients/word-index.ts`). Fix mirrors #759's startup-scan shape and `jscpd-client.ts`'s `MAX_ENTRIES` prior art: a new `maxScanEntries` option (named consistently with startup-scan) charges one tick per entry TOUCHED β including ignored/skipped ones, since the per-entry ignore probe is the dominant cost β with a generous `DEFAULT_MAX_SCAN_ENTRIES` (200k: bounds the pathological misrooted/data-dominated case to seconds instead of an unbounded walk, while staying an order of magnitude above any healthy project's entry count so real repos never see truncation); when it trips the walk stops and returns the best-effort list collected so far. The existing `maxFiles` cap is unchanged and independent, exactly as in #758. Exposed non-invasively via new `collectSourceFilesWithBudget`/`collectSourceFilesWithBudgetAsync` cores returning `{ files, entryBudgetExceeded }` that the existing collectors wrap β the `string[]` contract of every current caller is untouched, and the default budget covers ALL callers automatically. Truncation awareness threaded per call site: the review-graph build logs a `review_graph_source_walk_entry_budget` latency phase (a truncated graph degrades like `maxFiles` trimming, so log-only); the project-diagnostics snapshot carries a new `scanTruncated` flag (surfaced like #747/#749's `unsafeRoot` but deliberately NOT a refusal β a truncated analysis is still useful, the flag only stops callers reading a partial result as a complete clean sweep) plus a `maxScanEntries` scan option; todo-scanner and word-index accept the default-bounded truncated list (comment-documented, no behavior change needed). New `tests/source-filter-entry-budget.test.ts` covers budget trip on a deterministic mixed tree, generous-budget completeness (no false positives), invalid-budget coercion to the finite default, sync/async flag agreement (and set-identical complete walks), `maxFiles` independence, and plain-wrapper equivalence; `tests/clients/project-diagnostics/scanner.test.ts` covers `scanTruncated` set-without-refusing and omitted-when-clean.
|
|
132
|
+
|
|
133
|
+
- **The startup source-file count walk now stops after a bounded number of directory entries instead of traversing the entire project tree** (closes #758) β `computeStartupScanContext`/`resolveStartupScanContextAsync` (`clients/startup-scan.ts`) count source files under the project root to decide `canWarmCaches`, and the walk early-exited ONLY once it had seen more than `MAX_STARTUP_SOURCE_FILES` (2000) `.ts/.js/.py/.go/.rs/.rb` files. A repo with FEW source files but a huge pile of non-source files never trips that exit β the reporter's case was a Hearts of Iron IV mod with ~300 scripts among ~84k data/`.txt` entries β so the walk enumerated the WHOLE tree, dominated by one `ignoreMatcher.isIgnored()` call per entry, blocking `session_start` for seconds on the cold-start path. The walk now carries a second, independent bound: a `MAX_STARTUP_SCAN_ENTRIES` (50000, env-tunable via `PI_LENS_STARTUP_SCAN_MAX_ENTRIES`) total-entry ceiling β the same finite-walk-cap pattern already guarding `jscpd-client.ts`'s `hasSourceFilesRecursive`. When a walk hits that ceiling before crossing the source-file limit, the tree is large and dominated by non-source files, so it returns a new `too-many-entries` verdict (`canWarmCaches: false`) rather than trusting the partial count and warming caches on (then re-walking) an enormous tree. `too-many-entries` is TTL'd by `isStartupScanVerdictFresh` exactly like `too-many-source-files` (#699) so a shrunk repo auto-recovers on the next session. The public `countSourceFilesWithinLimit`/`countSourceFilesWithinLimitAsync` wrappers keep their pre-#758 contract (only the source-file limit bounds them); the entry ceiling applies solely to the startup-scan verdict path via the shared `walkSourceCount`/`walkSourceCountAsync` cores. New `tests/clients/startup-scan-entry-budget.test.ts` drives the `too-many-entries` verdict deterministically with a tiny mixed-repo fixture and a small `maxScanEntries`, covers the env-tunable getter, and pins the TTL behavior on both the sync and async paths. `runtime-session.ts`'s `useScanRootForSignals` treats the new verdict exactly like its `too-many-source-files` sibling β both found a real project root, so session signals stay anchored there rather than falling back to cwd β and the sync/async walk loops share a single `visitCountEntry` per-entry step so the two bounds live in one place.
|
|
134
|
+
|
|
135
|
+
- **`writeState` in `clients/diagnostic-dispositions.ts` now writes the disposition store via tmp+rename instead of a plain `fs.writeFileSync`** (#690's rescope β a prerequisite for the cross-process review CLI, which will read/write this store from a second process) β a concurrent reader (`readState`) could previously observe a half-written file mid-`writeFileSync`, fail `JSON.parse`, and have its catch treat that as empty state, silently losing every disposition on that read. Now matches the repo's established `${target}.tmp-${pid}` atomic-write shape (`clients/instance-registry.ts`, `clients/recent-touches.ts`, `clients/review-graph/builder.ts`): `renameSync` replaces the destination atomically on both POSIX and Windows (libuv's `MOVEFILE_REPLACE_EXISTING`), so a reader now always sees either the fully-old or fully-new JSON, never a torn write. Unlike those best-effort writers, a rename failure here still throws (after best-effort tmp-file cleanup) rather than being swallowed β this store's writes are functionally load-bearing (a lost mark is a correctness bug, not a dropped observability sample), matching the pre-existing `writeFileSync`'s own no-catch behavior. No retry loop was added for transient Windows `EPERM`/antivirus contention on rename: none of this repo's other atomic writers retry either, and `renameSync`'s replace semantics already succeed in the common case. `readState`'s corrupt-JSON catch is now genuinely rare (a torn read can no longer trigger it) and its comment was updated to explain why caching `{}` against that stat is still safe rather than a permanent trap. New tests in `tests/clients/diagnostic-dispositions.test.ts` assert no `.tmp-*` file is left behind after a mark, that state round-trips through a cache reset (simulating a fresh process reading purely from disk), and that a 200-entry write is always fully parseable off disk.
|
|
136
|
+
|
|
137
|
+
- **`lens_diagnostic_mark` dispositions now apply to the cached `lens_diagnostics` delta/all modes immediately, without waiting for the file to be re-dispatched** (closes #755) β the #690 disposition filter ran in exactly two places: the per-edit dispatcher (`clients/dispatch/dispatcher.ts`, filtering BEFORE results are cached) and `mode=full`'s merge (`applyInlineSuppressionsToSummaries`, `tools/lens-diagnostics.ts`). But `mode=delta` (the default) and `mode=all` re-served the `actionable-warnings`/`code-quality-warnings` reports, the project-diagnostics delta report, and the widget summaries VERBATIM from dispatch time β before any mark existed β and `lens_diagnostic_mark`'s suppress writes its `pi-lens-ignore` comment via raw `fs.writeFile`, not an agent edit, so no `tool_result` hook fires and no re-dispatch happens: a suppressed/deferred finding reappeared on the next `lens_diagnostics` call until the file was next edited. `formatDeltaMode` and `formatAllMode` now re-apply the disposition filter at read time. A new `applyWeakDispositions` (`clients/diagnostic-dispositions.ts`) drops only the WEAK-anchored dispositions (`suppress`, `defer` β `file|tool|rule|message`, no line-content hash), which by construction needs ZERO file I/O, so the "instant" cache-only modes stay instant: no unconditional reads are added. `false-positive` is deliberately NOT filtered in these modes β it is STRICT-anchored (needs the flagged line's own content to re-derive its hash) and still filters on the next per-edit dispatch and in `mode=full`, both of which already have file content in hand; the store's weak `suppress` entry mirrors every inline comment the mark tool writes, so store filtering already covers marks made through the tool (a hand-written inline-only comment on an un-redispatched file remains the documented corner case, converging at next dispatch / in `mode=full`). Project-delta findings are anchored via `projectDiagnosticToWidget` so the `(tool, rule)` a mark binds to matches `mode=full`'s own project-runner filter. No mark-time cache purge was added: read-time filtering is the single robust choke point that converges every `lens_diagnostics` consumer, whereas purging the two report-cache shapes plus the in-memory widget state at mark time would duplicate that logic across fragile shapes and risk drift. New end-to-end coverage (`tests/tools/lens-diagnostics.test.ts`) marks suppress/defer via the real tool and asserts the finding is gone from delta and all with no re-dispatch (and that `mode=full` is unaffected); unit coverage of `applyWeakDispositions` (`tests/clients/diagnostic-dispositions.test.ts`) proves it drops suppress/defer with no content passed, keeps `flagged`, and leaves `false-positive` for the content-based filter.
|
|
138
|
+
|
|
139
|
+
- **Remaining cwd-rooted scan roots now carry the same `isAtOrAboveHomeDir` ceiling, and the source-file walk is capped structurally** (closes #751) β follow-up to #749, closing out the lower-exposure members of the #250/#253/#462/#747 escape class so no walker or scanner spawn can enumerate `$HOME` from a misrooted cwd. (1) `scanProjectDiagnostics` (`clients/project-diagnostics/scanner.ts`) β the cheap-tier project scan, reached from `lens_diagnostics mode=full` and MCP `pilens_project_scan` (cwd straight from tool args) β refuses to walk a cwd at/above home, returning an honest `unsafeRoot` snapshot (0 files, not "clean") that both callers render as a skip rather than a clean verdict. (2) `clients/source-filter.ts` `resolveCollectionConfig` no longer defaults an omitted `maxFiles` to `Infinity`: the new `DEFAULT_MAX_SOURCE_FILES` (20000) finite cap means no caller (`todo-scanner`, `scan-utils` pass-throughs, warm-path language ranking) can trigger an unbounded collection just by omitting the cap. (3) `ast_grep_search` (`tools/ast-grep-search.ts`) refuses only the *defaulted*-cwd case (`paths` omitted) when that cwd is at/above home β explicit user `paths` are never second-guessed. (4) the LSP workspace sweep (`collectWorkspaceDiagnosticFiles`, `clients/lsp/index.ts`) adds the home ceiling on top of its existing 5000-file cap, with `tools/lens-diagnostics.ts` rendering the refused sweep as a skip. (5) `JscpdClient.scan` (`clients/jscpd-client.ts`) gains an internal `isAtOrAboveHomeDir` guard (belt-and-braces β it resolves no marker root, so previously every caller had to guard for it). (6) `rules-scanner.ts` `findMarkdownFiles` gains a depth-8/500-file cap (it only ever walks `.claude/rules`/`.agents/rules` subtrees, so a depth/file bound is sufficient without the full home ceiling).
|
|
140
|
+
|
|
141
|
+
- **A sweep warm-up that times out now retries once and, if still cold, skips that server's files for the rest of the sweep instead of letting a wedged server drag the whole run** (closes #744) β live dogfooding of a 119-file `lens_diagnostics mode=full` sweep found `ensureWarmForSweep` (`clients/lsp/index.ts`) doing a single one-shot warm-up `touchFile` per representative file bounded by `warmupTimeoutMs()` (20s floor); when that warm-up timed out (observed: marksman, a `workspaceIndexing` server, burned the full 20s and stayed cold) the server was left cold with NO re-warm, NO backoff, and NO skip, so every subsequent per-file touch in the sweep re-paid a full per-file budget against it and timed out again β the sweep launched ~59s after an `lsp_service_reset`, straight into freshly-relaunched servers, exactly the state where warm-up failure is most likely. **What/why/how:** (1) `ensureWarmForSweep` now measures per-non-auxiliary-server warm-up success via the SAME `demonstratedReady` signal `touchFile` already marks on a confirmed (non-inconclusive) diagnostics round trip β a server whose key is still absent afterward never warmed. On a failed first attempt it retries exactly ONCE after a short `warmupRetryBackoffMs` breather (`PI_LENS_LSP_WARMUP_RETRY_BACKOFF_MS`, default 500ms, abort-aware β the failure state is usually a server mid-relaunch/index that just needs a moment, not a second full budget), and returns any still-cold server ids in a new `failedServerIds`. (2) `runWorkspaceDiagnostics`' per-group worker skips a group whose primary server is in `failedServerIds` (the sweep groups by primary server, so that primary is precisely the one whose per-file touches would drag), recording each skipped file as UNCONFIRMED (`timedOut: true` + a new `skippedWarmupFailure: true` result field) β never as confirmed-clean `[]`, so the existing confirmed/unconfirmed reporting (#630/#646, `tools/lens-diagnostics.ts`) and the per-primary-server breakdown surface these files honestly as "NOT the same as 0 diagnostics" rather than "ran clean", with a dedicated `lsp_sweep_warmup_failed`/`lsp_sweep_group_skipped_warmup` latency-log trail in `~/.pi-lens`. **Design notes:** the skip is deliberately SWEEP-SCOPED (the caller discards `failedServerIds` when the call ends), NOT the global `broken` cooldown map β a server that is merely still indexing is not broken and must not be cooldown-banned across the whole session; the `broken` map's 15s-base/exponential cooldown is for genuine spawn/initialize failures, a different failure class. Warm-up stays `clientScope:"primary"` (not the sweep's `"all"`) on purpose: `"all"` would additionally spawn the sweep-EXCLUDED auxiliaries (`WORKSPACE_SWEEP_EXCLUDED_SERVER_IDS`), and because `touchFile`'s `inconclusive` flag is touch-wide, one slow advisory auxiliary would then suppress the `demonstratedReady` marking of a perfectly healthy primary and falsely condemn it β recording per-primary-server outcomes covers the servers the sweep actually gates on without that regression. The issue's optional third bullet (gating/delaying a sweep that starts shortly after an `lsp_service_reset`) is left as a follow-up: it needs a reset-timestamp seam the sweep doesn't have today and didn't fall out of this change. New tests in `tests/clients/lsp/sweep-warmup.test.ts` cover warm-up timeout β one retry; retry-still-failing β server reported in `failedServerIds` and its files skipped as `skippedWarmupFailure`/unconfirmed (not clean); a server that warms on the retry participates normally; and the already-warm no-op path is unchanged.
|
|
142
|
+
|
|
143
|
+
- **A single backpressured LSP server no longer times out the didOpen/didChange notify write for an entire `clientScope:"all"` file touch, zeroing every co-touched healthy server's diagnostics** (closes #743 β observed live during a `lens_diagnostics mode=full` sweep on a 119-file project: writes split 20 ok / 16 timed out, the sweep appeared stuck at 20%, and touched files reported `notifyWriteTimedOut:true collectedDiagnostics:0` even for healthy servers). `LSPService.touchFile` (`clients/lsp/index.ts`) fanned the notify write across ALL spawned servers under one `Promise.all` bounded by a single `notifyWriteBudgetMs()` (2000ms) deadline, so one server that had stopped draining its stdin (backpressure β e.g. tsserver mid-recheck under load) timed out the write for the WHOLE file, flipping the touch to `notifyWriteTimedOut β inconclusive` for every server including perfectly healthy ones β contributing no diagnostics and marking nobody `demonstratedReady`. Fix, mirroring how the diagnostics wait already went per-server (#242/#573 via `perServerTimeout`): (1) each server's notify write now gets its OWN `notifyWriteBudgetMs()` deadline instead of one shared `Promise.all` deadline; (2) `notifyWriteTimedOut` is now per-server β a new `notifyWriteTimedOutServerIds` list records exactly which servers' writes did not land, the file-level `notifyWriteTimedOut` flag means "at least one server timed out" (kept for the conservative touch-wide `inconclusive` merge, since `collected` still merges across all servers), and the `demonstratedReady` gate now skips ONLY the servers whose own write timed out rather than gating the whole loop on the file-level flag β so a healthy server whose sibling's write stalled still becomes `demonstratedReady`; (3) a server whose notify write times out `NOTIFY_BACKPRESSURE_BROKEN_AFTER` (3) times in a row is demoted through the EXISTING `broken`-cooldown map (`recordNotifyWriteBackpressure` sets a `BROKEN_BASE_COOLDOWN_MS` cooldown and evicts the wedged client β an alive client is reused before the broken check in `ensureClientForServer`, so the cooldown only bites once the stale client is gone) so subsequent sweeps stop re-paying its notify budget on every file; a single successful write resets the consecutive-timeout streak. A write that REJECTS (not backpressure) still marks that server inconclusive but does not count toward the demotion streak. New `tests/clients/lsp/service-notify-per-server.test.ts` covers: a stalled sibling write not marking the healthy server timed out while the healthy server still reaches `demonstratedReady`; repeated timeouts tripping the broken cooldown and evicting the client; and a successful write resetting the streak so demotion needs a fresh consecutive run.
|
|
144
|
+
|
|
145
|
+
- **`lens_diagnostics mode=full` no longer launches heavyweight analyzers from `$HOME` or above** (closes #747) β `fetchFreshProjectDiagnostics` (`clients/project-diagnostics/fresh-fetch.ts`) resolved the session cwd as `analysisRoot` with no ceiling, so a `mode=full refreshRunners=cheap` call issued while pi's cwd was the home directory started a whole-home `jscpd .`/knip/madge/gitleaks/govulncheck/trivy/dead-code walk (the explicit `paths` scope only filters *reported* results, never what the analyzers walk). Observed failure: a jscpd scan from a WSL `$HOME` reached 44 GB RSS, exhausted swap, and OOM-killed the entire WSL instance. The fresh fetch now applies the same `isAtOrAboveHomeDir` ceiling as `startup-scan.ts`/`resolveSnapshotRoot`/the review-graph builder (the #250/#253 escape class) and refuses before spawning anything, reporting a dedicated "heavyweight analyzers skipped" note plus a machine-readable `analyzersUnsafeRoot` details flag instead of seven misleading per-analyzer "not applicable" reasons.
|
|
146
|
+
|
|
147
|
+
- **`symbol_search`'s cold word-index build no longer walks an unbounded tree from `$HOME`** (#747 hardening) β `triggerBackgroundWordIndexBuild` (`clients/word-index.ts`) was the one word-index build path with no `canWarmCaches` gate in front of it (cold in-process queries and MCP `pilens_symbol_search`, where cwd can be a raw tool argument), and `collectWordIndexDocs` ran `collectSourceFilesAsync` with no `maxFiles`, so the `WORD_INDEX_MAX_FILES` (6000) cap only trimmed results AFTER the entire tree had been enumerated β the same unbounded-walk-plus-in-memory-accumulation shape as the #747 jscpd OOM. The trigger now applies the `isAtOrAboveHomeDir` ceiling, and the cap is passed into the walk itself so every build path is bounded.
|
|
148
|
+
- **`hasOxlintConfig` now detects `.oxlintrc.jsonc`, `oxlint.config.ts`, and `oxlint.config.mts`** (closes #745) β previously only `.oxlintrc.json`/`oxlint.json` were recognized, so projects using oxlint's TS-based config format (`oxlint.config.ts` + `defineConfig`) or the `.oxlintrc.jsonc` variant were treated as having no oxlint config at all, causing `getPreferredJstsLintRunners` to fall back to `["oxlint", "biome-check-json"]` instead of `["oxlint"]` and run `biome-check-json` as an uninvited fallback linter. New `OXLINT_CONFIGS` list (mirroring the existing `ESLINT_CONFIGS` pattern) covers all four oxlint-documented auto-discovery filenames alongside the legacy `oxlint.json` name.
|
|
149
|
+
|
|
150
|
+
### Changed
|
|
151
|
+
|
|
152
|
+
- **Introduced a shared `writeFileAtomic`/`writeFileAtomicAsync` helper (`clients/atomic-write.ts`) and migrated four of the five hand-rolled `${target}.tmp-${pid}` + `renameSync`/`rename` writers onto it** (closes #762) β `instance-registry.ts`, `session-state-store.ts`, `recent-touches.ts`, `diagnostic-dispositions.ts`'s `writeState`, and `review-graph/builder.ts`'s sync exit-hook persist all independently hand-rolled the same tmp+rename shape, inviting drift (e.g. forgetting the tmp-cleanup-on-failure step); the helper takes a `bestEffort` option (default `true`, matching the majority: swallow write/rename failures after best-effort tmp cleanup) versus `bestEffort: false` (rethrow after the same cleanup β `diagnostic-dispositions.ts`'s #757 load-bearing policy, since a silently lost disposition mark is a correctness bug, not a dropped observability sample, preserved exactly). `session-state-store.ts`'s tmp file is now named `${file}.tmp-${pid}` instead of its former one-off `${file}.${pid}.tmp` β no observable behavior change, since nothing reads the intermediate tmp filename. `review-graph/builder.ts`'s OTHER writer (`writePending`, the debounced async persist) is deliberately left unmigrated: its callback-style `fs.mkdir`/`fs.writeFile`/`fs.rename` logs a distinct `console.error` per failing stage (mkdir vs write vs rename) that the shared helper's single swallow/rethrow outcome can't express without losing that diagnostic granularity or turning a non-blocking fire-and-forget write into an awaited promise chain. New `tests/clients/atomic-write.test.ts` covers both variants directly: successful atomic replace, no tmp file left behind on success or on a swallowed failure, `bestEffort: true` swallowing a rename failure, and `bestEffort: false` rethrowing it β rename failures are induced via a real filesystem obstruction (renaming onto an existing non-empty directory) rather than mocking `fs`, since this module's `import * as fs` style doesn't reliably re-bind under `vi.spyOn`. Existing suites (`tests/clients/instance-registry.test.ts`, `recent-touches.test.ts`, `session-state-store.test.ts`, and `diagnostic-dispositions.test.ts`'s no-tmp-left-behind/torn-read assertions) pass unmodified.
|
|
153
|
+
|
|
154
|
+
- **Consolidated the repo's recursive tree walkers onto a shared bounded-walk engine in `clients/source-walker.ts`** (closes #761) β every walker hardening episode was costing O(number-of-walkers) because each walker re-implemented the same traversal skeleton: the home-ceiling class took #747 then #751/#754 (seven sites), the entry-budget class took #758/#759 then #760/#764 (five consumers), each hand-rolling the `readdir` + for-entry loop, the sync-recursion / async-stack shape, and the `setImmediate` yield cadence. `source-walker.ts` already held the shared `readDirEntriesSafe` + `shouldRecurseIntoDir`; it now also owns the LOOP via three drivers β `walkTreeStackSync`/`walkTreeStackAsync` (depth-first stack, with an optional per-directory `shouldStop` loop guard and, on the async twin, a `yieldEvery` `setImmediate` cadence + a `beforeWalk` hook for `ensureTrackedIndex` priming) and `walkTreeRecursiveSync` (immediate-descent recursion, preserving result-array order) β each parameterized by a pluggable per-entry `WalkVisitor` returning `"recurse"`/`"skip"`/`"stop"`, generalized from #759's `visitCountEntry` and #764's `chargeEntryBudget`/`classifyEntry`. Phase 1 migrates the two count walks (`walkSourceCount`/`walkSourceCountAsync` in `clients/startup-scan.ts`) onto the stack drivers via a shared `makeSourceCountVisitor`, `hasSourceFilesRecursive` in `clients/jscpd-client.ts` (its `MAX_ENTRIES` 6000 budget expressed as the per-directory `shouldStop` guard, its skip-all-symlinks/multi-language-regex policy kept caller-side), and the `collectSourceFilesWithBudget`/`...Async` cores in `clients/source-filter.ts` (sync onto the recursive driver, async onto the stack driver) β every public contract byte-identical: the `string[]` wrappers, `SourceCollectionResult`, `maxFiles`/`maxScanEntries` semantics, the slow-FS sync clamp, the artifact-probe cache, and #703 tracked-index priming. Classification/policy stays pluggable per walker BY DESIGN β the bounds arithmetic genuinely differs by an off-by-one between the count family (post-classify `visited >= max`, the maxEntries-th entry counted) and the collect family (pre-classify `visited > max`, the tripping entry refused), so the budget/results caps stay in each walker's visitor rather than being forced onto one shared comparison that would shift a cliff by one entry; `lsp/index.ts`, `rules-scanner.ts`, review-graph, `language-profile.ts`, and word-index are deliberately NOT migrated (deferred to opportunistic follow-ups per the issue). The existing equivalence suites are the safety net and pass unmodified β `tests/clients/source-walker-equivalence.test.ts`, `tests/source-filter.test.ts`, `tests/source-filter-entry-budget.test.ts`, and the startup-scan entry-budget/home-ceiling/verdict-cache and jscpd suites β plus a new `tests/clients/source-walker-engine.test.ts` pinning the three drivers' traversal order, `stop`/`shouldStop` halting, and async `setImmediate` yield cadence directly.
|
|
155
|
+
|
|
156
|
+
- **Unified the five independently-hardcoded project-size budgets behind one configurable `maxProjectFiles` scale knob** (closes #776) β the project-diagnostics scanner (500 files), review graph (1,000 files), startup scan (2,000 source files), jscpd (6,000 dir entries), and word index (6,000 files) each degraded silently and separately as a repo grew, with no single lever to say "this repo is big; believe in it." New `clients/project-scale.ts` centralizes a `maxProjectFiles` base (default `2,000`) and a `PROJECT_SCALE_RATIOS` table (0.25Γ/0.5Γ/1Γ/3Γ/3Γ respectively) that reproduces today's five defaults exactly at the default base β a behavior-neutral refactor, no default value changed. Resolution order: each subsystem's PRE-EXISTING per-subsystem env override (`PI_LENS_REVIEW_GRAPH_MAX_FILES`, `PI_LENS_STARTUP_SCAN_MAX_ENTRIES`, etc.) still wins outright; below that, `getProjectScaleBase(cwd)` resolves `maxProjectFiles` from the project's `.pi-lens.json` (new field, extended schema in `clients/project-lens-config.ts`) β the new `PI_LENS_MAX_PROJECT_FILES` env var (via the house `lazyEnvNumber` pattern, #763) β the 2,000 default. Every call site with a `cwd`/`rootDir` in hand threads it through so a per-project override actually scales that subsystem (`project-diagnostics/scanner.ts`, `review-graph/builder.ts`, `startup-scan.ts`, `jscpd-client.ts`, `word-index.ts`); the old exported constants (`DEFAULT_MAX_FILES`, `MAX_STARTUP_SOURCE_FILES`, `WORD_INDEX_MAX_FILES`, `RUNTIME_CONFIG.reviewGraph.maxFiles`) are kept, deprecated in comments, for callers/tests that still reference the literals. Two entry-COUNT budgets (`startup-scan.ts`'s `MAX_STARTUP_SCAN_ENTRIES`, `source-filter.ts`'s `DEFAULT_MAX_SCAN_ENTRIES`) were deliberately left standalone rather than derived β they bound directory entries VISITED (a raw tree-walk safety valve), a fundamentally different quantity from source files KEPT whose healthy ratio varies wildly by project shape, and coupling them risked changing behavior on exactly the pathological-tree shapes those ceilings protect. New `tests/clients/project-scale.test.ts` covers the ratio table reproducing all five defaults at the default base, a `.pi-lens.json` override scaling all five derived budgets proportionally, per-subsystem env still beating the derived value, and `PI_LENS_MAX_PROJECT_FILES` beating the default but losing to both `.pi-lens.json` and per-subsystem env. Documented in `docs/globalconfig.md`.
|
|
157
|
+
|
|
7
158
|
## [3.8.71] - 2026-07-20
|
|
8
159
|
|
|
9
160
|
### Added
|
|
@@ -17,7 +168,7 @@ All notable changes to pi-lens will be documented in this file.
|
|
|
17
168
|
|
|
18
169
|
### Changed
|
|
19
170
|
|
|
20
|
-
- **`release.yml` publishes to npm via OIDC trusted publishing instead of a long-lived `NPM_TOKEN`, and `package.json` declares an `allowScripts` allowlist for npm v12's install-time script review** (closes #737) β follow-up to https://github.blog/changelog/2026-07-08-npm-install-time-security-and-gat-bypass2fa-deprecation
|
|
171
|
+
- **`release.yml` publishes to npm via OIDC trusted publishing instead of a long-lived `NPM_TOKEN`, and `package.json` declares an `allowScripts` allowlist for npm v12's install-time script review** (closes #737) β follow-up to <https://github.blog/changelog/2026-07-08-npm-install-time-security-and-gat-bypass2fa-deprecation/>. The `publish-npm` job now requests a GitHub Actions OIDC token (`permissions.id-token: write`) and runs a bare `npm publish`, no `NODE_AUTH_TOKEN`/`secrets.NPM_TOKEN` involved β the trusted publisher (repo `apmantza/pi-lens`, workflow `release.yml`) was already registered on npmjs.com. The workflow's `has_npm_token` gate is gone entirely (OIDC needs no secret to check for); release/dry-run/smoke-load steps now gate purely on the existing version-tag-missing check. The publish job pins `npm@11.18.0` before installing (`npm install -g npm@11.18.0 --ignore-scripts`) since OIDC trusted publishing needs npm >= 11.5.1 and the Node 22 runner's bundled npm isn't guaranteed new enough β pinned to the latest 11.x rather than 12.x, whose stricter `engines.node` (`^22.22.2 || ^24.15.0 || >=26.0.0`) isn't guaranteed by the workflow's `node-version: 22` pin. Separately, `package.json` gains an `allowScripts` field (the field npm v12's `npm approve-scripts` manages, verified by reading npm's own `docs/content/commands/npm-approve-scripts.md` and `lib/utils/allow-scripts-writer.js` locally rather than guessed) approving the runtime/dev dependencies confirmed (via `npm view <pkg> scripts` and a real `npm approve-scripts --all` run) to have install-time lifecycle scripts: `@ast-grep/cli` (`postinstall`, a direct runtime dependency) plus `@google/genai`/`protobufjs` (transitive through the `@earendil-works/pi-coding-agent` devDependency, needed so the repo's own CI installs β which build `dist/` via `prepare` β don't get flagged). `@ast-grep/napi`, `web-tree-sitter`, `pidusage`, and `@earendil-works/pi-tui` were also audited and have no install-time scripts, so nothing was added for them. README's install section gains a short note on `npm approve-scripts` and git-dependency approval for npm v12 consumers.
|
|
21
172
|
- **Consolidated the hand-duplicated "walk up directories looking for one of a list of config filenames" loop across 4 sites** (closes #680) β `clients/opengrep-config.ts`'s `findLocalOpengrepConfig`, `clients/typos-config.ts`'s `findLocalTyposConfig`, `clients/zizmor-config.ts`'s `findLocalZizmorConfig`, and `clients/sgconfig.ts`'s `findLocalSgconfig` each independently hand-rolled the identical `walkUpDirs` + nested-filename-check loop. New `findLocalToolConfig(startDir, names)` in `clients/path-utils.ts`, alongside the file's other shared walk-up primitives (`walkUpDirs`, `findNearestContaining`, `findNearestMarkerRoot`), is the single source of truth; all 4 call sites are now one-line wrappers over it with their own filename list, keeping their existing public name/signature (so no caller elsewhere needed to change). Pure internal refactor, zero behavior change β deliberately does NOT touch `clients/tool-policy.ts`'s `has*Config` family (different shape: `boolean` return, tool-specific content-sniffing) or `clients/project-lens-config.ts`'s `discoverPiLensProjectConfig` (adds mtime caching), both out of scope per the issue. New unit tests for `findLocalToolConfig` in `tests/clients/path-utils.test.ts`; all 4 migrated sites' existing test suites pass unmodified.
|
|
22
173
|
- **Extracted `index.ts`'s `tool_call` handler (read-guard preflight: LSP auto-touch, read-range tracking/expansion, oldText autopatch, edit-guard block/relocation, pre-write duplicate-export detection) into `clients/runtime-tool-call.ts`** (closes #678) β pure refactor, no behavior change. `tool_call` was the one lifecycle hook whose logic still lived inline in the entry point while its three siblings (`session_start` β `clients/runtime-session.ts`, `tool_result` β `clients/runtime-tool-result.ts`, `turn_end`/`agent_end` β `clients/runtime-turn.ts`/`clients/runtime-agent-end.ts`) had already been extracted; at ~800 lines it was also the largest, and the most safety-critical (it mutates the agent's own edits β relocation/autopatch β before they reach disk), living in the file that's hardest to unit-test directly. New `handleToolCall(deps)` follows the exact same dependency-injection shape as its siblings; `index.ts`'s `pi.on("tool_call", ...)` registration is now a thin call site. Verified line-by-line against the original (diff of the extracted body vs. the pre-extraction source shows only the necessary renames/import-path fixes, no logic changes) plus the full existing test suite (including the dedicated `tests/index-integration.test.ts` `tool_call` coverage) passing unmodified. New `tests/clients/runtime-tool-call.test.ts` unit-tests `handleToolCall` directly against mocked deps (lensEnabled short-circuit, read-guard read recording + LSP auto-touch, zero-read edit block, write-then-edit read-guard interaction, duplicate-export write block).
|
|
23
174
|
- **Consolidated the hand-duplicated "load a JSON cache file, try/catch, version/shape-validate" boilerplate across 7 sites** (closes #676) β `clients/project-diagnostics/cache.ts` (two load functions), `clients/lsp/workspace-diagnostics-cache.ts`, `clients/cache-manager.ts` (three load sites: `readCache`, `isCacheFresh`, `readTurnState`), `clients/cache/rule-cache.ts`, `clients/project-snapshot.ts`, and `clients/session-state-store.ts` each independently wrote the same `JSON.parse(fs.readFileSync(path, "utf-8"))` β structural/version guard β `undefined`/`null`-on-any-throw shape. New `clients/json-cache-read.ts` extracts only the read/parse/try-catch mechanics into `readJsonCache`/`readJsonCacheAsync` (the latter for `session-state-store.ts`'s `fs.promises` call site), taking a `validate(parsed): T | undefined` callback plus an optional `onError` hook for the one site (`CacheManager.readCache`) that logs a read failure rather than silently swallowing it. Every site's own validation logic β including `rule-cache.ts`'s extra `ruleHash` field check, alongside the version check every other site has β is preserved exactly as its `validate` callback; this is pure boilerplate deduplication, not a behavior change. (`clients/generated-artifacts.ts`, listed in the issue, turned out on inspection to have no JSON-cache-read pattern at all β no code changed there.) New `tests/clients/json-cache-read.test.ts` unit-tests the shared helper directly (valid payload, corrupt JSON, valid-but-wrong-shape, missing file, `validate`/`onError` throwing); all pre-existing tests for the migrated sites pass unmodified.
|
package/README.md
CHANGED
|
@@ -79,8 +79,8 @@ Thanks goes to these wonderful people:
|
|
|
79
79
|
<!-- prettier-ignore-start -->
|
|
80
80
|
<!-- markdownlint-disable -->
|
|
81
81
|
<table>
|
|
82
|
-
<tbody>
|
|
83
|
-
<tr>
|
|
82
|
+
<tbody>
|
|
83
|
+
<tr>
|
|
84
84
|
<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>
|
|
85
85
|
<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>
|
|
86
86
|
<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>
|
|
@@ -88,7 +88,7 @@ Thanks goes to these wonderful people:
|
|
|
88
88
|
<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>
|
|
89
89
|
<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>
|
|
90
90
|
<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>
|
|
91
|
-
</tr
|
|
91
|
+
</tr>
|
|
92
92
|
<tr>
|
|
93
93
|
<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>
|
|
94
94
|
<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>
|
|
@@ -97,7 +97,7 @@ Thanks goes to these wonderful people:
|
|
|
97
97
|
<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>
|
|
98
98
|
<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>
|
|
99
99
|
<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>
|
|
100
|
-
</tr
|
|
100
|
+
</tr>
|
|
101
101
|
<tr>
|
|
102
102
|
<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>
|
|
103
103
|
<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>
|
|
@@ -106,7 +106,7 @@ Thanks goes to these wonderful people:
|
|
|
106
106
|
<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>
|
|
107
107
|
<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>
|
|
108
108
|
<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>
|
|
109
|
-
</tr
|
|
109
|
+
</tr>
|
|
110
110
|
<tr>
|
|
111
111
|
<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>
|
|
112
112
|
<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>
|
|
@@ -115,71 +115,72 @@ Thanks goes to these wonderful people:
|
|
|
115
115
|
<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>
|
|
116
116
|
<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>
|
|
117
117
|
<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>
|
|
118
|
-
</tr
|
|
118
|
+
</tr>
|
|
119
119
|
<tr>
|
|
120
|
+
<td align="center" valign="top" width="14.28%"><a href="https://github.com/shaochong-theoai"><img src="https://avatars.githubusercontent.com/u/305592060?v=4" width="100px;" alt=""/><br /><sub><b>shaochong-theoai</b></sub></a><br /><a href="#code-shaochong-theoai" title="Code">π»</a></td>
|
|
120
121
|
<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>
|
|
121
122
|
<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>
|
|
122
123
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/jsmestad"><img src="https://avatars.githubusercontent.com/u/16150?v=4" width="100px;" alt=""/><br /><sub><b>Justin Smestad</b></sub></a><br /><a href="#code-jsmestad" title="Code">π»</a></td>
|
|
123
124
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/anasalsbey-glitch"><img src="https://avatars.githubusercontent.com/u/283609702?v=4" width="100px;" alt=""/><br /><sub><b>anasalsbey-glitch</b></sub></a><br /><a href="#code-anasalsbey-glitch" title="Code">π»</a> <a href="#bug-anasalsbey-glitch" title="Bug reports">π</a></td>
|
|
124
125
|
<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>
|
|
125
126
|
<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>
|
|
126
|
-
|
|
127
|
-
</tr><br />
|
|
127
|
+
</tr>
|
|
128
128
|
<tr>
|
|
129
|
+
<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>
|
|
129
130
|
<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>
|
|
130
131
|
<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>
|
|
131
132
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/grodingo"><img src="https://avatars.githubusercontent.com/u/244184972?v=4" width="100px;" alt=""/><br /><sub><b>Virgile</b></sub></a><br /><a href="#bug-grodingo" title="Bug reports">π</a></td>
|
|
132
133
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/finnvyrn"><img src="https://avatars.githubusercontent.com/u/90801772?v=4" width="100px;" alt=""/><br /><sub><b>Finn</b></sub></a><br /><a href="#ideas-finnvyrn" title="Ideas & Planning">π€</a></td>
|
|
133
134
|
<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>
|
|
134
135
|
<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>
|
|
135
|
-
|
|
136
|
-
</tr><br />
|
|
136
|
+
</tr>
|
|
137
137
|
<tr>
|
|
138
|
+
<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>
|
|
138
139
|
<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>
|
|
139
140
|
<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>
|
|
140
141
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/tackleberrybey"><img src="https://avatars.githubusercontent.com/u/156954032?v=4" width="100px;" alt=""/><br /><sub><b>tackleberrybey</b></sub></a><br /><a href="#bug-tackleberrybey" title="Bug reports">π</a></td>
|
|
141
142
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/TaterDoge"><img src="https://avatars.githubusercontent.com/u/121467933?v=4" width="100px;" alt=""/><br /><sub><b>Mariann Abshire</b></sub></a><br /><a href="#bug-TaterDoge" title="Bug reports">π</a></td>
|
|
142
143
|
<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>
|
|
143
144
|
<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>
|
|
144
|
-
|
|
145
|
-
</tr><br />
|
|
145
|
+
</tr>
|
|
146
146
|
<tr>
|
|
147
|
+
<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>
|
|
147
148
|
<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>
|
|
148
149
|
<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>
|
|
149
150
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/av1155"><img src="https://avatars.githubusercontent.com/u/117413846?v=4" width="100px;" alt=""/><br /><sub><b>Andrea Arturo Venti Fuentes</b></sub></a><br /><a href="#bug-av1155" title="Bug reports">π</a></td>
|
|
150
151
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/dacec354"><img src="https://avatars.githubusercontent.com/u/90093629?v=4" width="100px;" alt=""/><br /><sub><b>dacec354</b></sub></a><br /><a href="#bug-dacec354" title="Bug reports">π</a></td>
|
|
151
152
|
<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>
|
|
152
153
|
<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>
|
|
153
|
-
|
|
154
|
-
</tr><br />
|
|
154
|
+
</tr>
|
|
155
155
|
<tr>
|
|
156
|
+
<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>
|
|
156
157
|
<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>
|
|
157
158
|
<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>
|
|
158
159
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/JohannesKlauss"><img src="https://avatars.githubusercontent.com/u/6214415?v=4" width="100px;" alt=""/><br /><sub><b>Johannes Klauss</b></sub></a><br /><a href="#ideas-JohannesKlauss" title="Ideas & Planning">π€</a></td>
|
|
159
160
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/tan-yong-sheng"><img src="https://avatars.githubusercontent.com/u/64836390?v=4" width="100px;" alt=""/><br /><sub><b>Tan Yong Sheng</b></sub></a><br /><a href="#ideas-tan-yong-sheng" title="Ideas & Planning">π€</a></td>
|
|
160
161
|
<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>
|
|
161
162
|
<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>
|
|
162
|
-
|
|
163
|
-
</tr><br />
|
|
163
|
+
</tr>
|
|
164
164
|
<tr>
|
|
165
|
+
<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>
|
|
165
166
|
<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>
|
|
166
167
|
<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>
|
|
167
168
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/quantfiction"><img src="https://avatars.githubusercontent.com/u/49965454?v=4" width="100px;" alt=""/><br /><sub><b>quantfiction</b></sub></a><br /><a href="#bug-quantfiction" title="Bug reports">π</a></td>
|
|
168
169
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Artawower"><img src="https://avatars.githubusercontent.com/u/16963833?v=4" width="100px;" alt=""/><br /><sub><b>Art</b></sub></a><br /><a href="#bug-Artawower" title="Bug reports">π</a></td>
|
|
169
170
|
<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>
|
|
170
171
|
<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>
|
|
171
|
-
|
|
172
|
-
</tr><br />
|
|
172
|
+
</tr>
|
|
173
173
|
<tr>
|
|
174
|
+
<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>
|
|
174
175
|
<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>
|
|
175
176
|
<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>
|
|
176
177
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/smaharnav"><img src="https://avatars.githubusercontent.com/u/263328627?v=4" width="100px;" alt=""/><br /><sub><b>smaharnav</b></sub></a><br /><a href="#bug-smaharnav" title="Bug reports">π</a></td>
|
|
177
178
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/MzaGuille"><img src="https://avatars.githubusercontent.com/u/151482698?v=4" width="100px;" alt=""/><br /><sub><b>Mza.Guille</b></sub></a><br /><a href="#bug-MzaGuille" title="Bug reports">π</a></td>
|
|
178
179
|
<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>
|
|
179
180
|
<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>
|
|
180
|
-
|
|
181
|
-
</tr><br />
|
|
181
|
+
</tr>
|
|
182
182
|
<tr>
|
|
183
|
+
<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>
|
|
183
184
|
<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>
|
|
184
185
|
<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>
|
|
185
186
|
<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>
|
|
@@ -187,10 +188,18 @@ Thanks goes to these wonderful people:
|
|
|
187
188
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/ymek"><img src="https://avatars.githubusercontent.com/u/300569?v=4" width="100px;" alt=""/><br /><sub><b>Myke Stubbs</b></sub></a><br /><a href="#code-ymek" title="Code">π»</a> <a href="#bug-ymek" title="Bug reports">π</a></td>
|
|
188
189
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/black-snow"><img src="https://avatars.githubusercontent.com/u/579733?v=4" width="100px;" alt=""/><br /><sub><b>Ronald</b></sub></a><br /><a href="#bug-black-snow" title="Bug reports">π</a></td>
|
|
189
190
|
</tr>
|
|
190
|
-
|
|
191
|
+
<tr>
|
|
192
|
+
<td align="center" valign="top" width="14.28%"><a href="https://github.com/JSap0914"><img src="https://avatars.githubusercontent.com/u/116227558?v=4" width="100px;" alt=""/><br /><sub><b>JSap0914</b></sub></a><br /><a href="#code-JSap0914" title="Code">π»</a></td>
|
|
193
|
+
<td align="center" valign="top" width="14.28%"><a href="https://github.com/AngriestBird"><img src="https://avatars.githubusercontent.com/u/12663269?v=4" width="100px;" alt=""/><br /><sub><b>Kenneth McCormick</b></sub></a><br /><a href="#bug-AngriestBird" title="Bug reports">π</a></td>
|
|
194
|
+
<td align="center" valign="top" width="14.28%"><a href="https://github.com/knthmn"><img src="https://avatars.githubusercontent.com/u/60438008?v=4" width="100px;" alt=""/><br /><sub><b>Kent Hou Man</b></sub></a><br /><a href="#code-knthmn" title="Code">π»</a> <a href="#bug-knthmn" title="Bug reports">π</a></td>
|
|
195
|
+
<td align="center" valign="top" width="14.28%"><a href="https://github.com/aaronkyriesenbach"><img src="https://avatars.githubusercontent.com/u/12665860?v=4" width="100px;" alt=""/><br /><sub><b>Aaron Ky-Riesenbach</b></sub></a><br /><a href="#bug-aaronkyriesenbach" title="Bug reports">π</a></td>
|
|
196
|
+
<td align="center" valign="top" width="14.28%"><a href="https://github.com/LovelyLoong"><img src="https://avatars.githubusercontent.com/u/54889641?v=4" width="100px;" alt=""/><br /><sub><b>ε―η±ηιΎ</b></sub></a><br /><a href="#bug-LovelyLoong" title="Bug reports">π</a></td>
|
|
197
|
+
<td align="center" valign="top" width="14.28%"><a href="https://github.com/pppobear"><img src="https://avatars.githubusercontent.com/u/21952175?v=4" width="100px;" alt=""/><br /><sub><b>pppobear</b></sub></a><br /><a href="#bug-pppobear" title="Bug reports">π</a></td>
|
|
198
|
+
<td align="center" valign="top" width="14.28%"><a href="https://neumie.dev/"><img src="https://avatars.githubusercontent.com/u/23665482?v=4" width="100px;" alt=""/><br /><sub><b>Jakub Neumann</b></sub></a><br /><a href="#code-neumie" title="Code">π»</a></td>
|
|
199
|
+
</tr>
|
|
200
|
+
</tbody>
|
|
191
201
|
</table>
|
|
192
202
|
|
|
193
|
-
|
|
194
203
|
<!-- markdownlint-restore -->
|
|
195
204
|
<!-- prettier-ignore-end -->
|
|
196
205
|
|