pi-lens 3.8.37 → 3.8.39
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 +21 -0
- package/README.md +2 -2
- package/banner.png +0 -0
- package/banner.svg +31 -16
- package/clients/cache/rule-cache.ts +2 -1
- package/clients/cache-manager.ts +4 -1
- package/clients/dispatch/fact-scheduler.ts +2 -1
- package/clients/dispatch/fact-store.ts +4 -6
- package/clients/dispatch/integration.ts +2 -5
- package/clients/dispatch/rules/quality-rules.ts +10 -15
- package/clients/dispatch/runners/biome-check.ts +1 -1
- package/clients/dispatch/runners/eslint.ts +1 -1
- package/clients/dispatch/runners/lsp.ts +1 -5
- package/clients/dispatch/runners/psscriptanalyzer.ts +1 -1
- package/clients/dispatch/runners/similarity.ts +1 -4
- package/clients/dispatch/utils/lsp-diagnostics.ts +3 -9
- package/clients/formatters.ts +12 -7
- package/clients/lsp/index.ts +8 -10
- package/clients/lsp/launch.ts +5 -8
- package/clients/lsp/server.ts +1 -1
- package/clients/pipeline.ts +6 -11
- package/clients/read-guard.ts +5 -6
- package/clients/runtime-coordinator.ts +2 -2
- package/clients/runtime-tool-result.ts +1 -3
- package/clients/secrets-scanner.ts +1 -1
- package/clients/session-summary.ts +1 -1
- package/clients/tree-sitter-query-loader.ts +3 -2
- package/commands/booboo.ts +15 -14
- package/index.ts +5 -5
- package/package.json +3 -2
- package/rules/rule-catalog.json +52 -1
- package/rules/tree-sitter-queries/abap/delete-where.yml +46 -0
- package/rules/tree-sitter-queries/c/case-range-multiple-values.yml +65 -0
- package/rules/tree-sitter-queries/c/goto-into-block.yml +72 -0
- package/rules/tree-sitter-queries/c/goto-label-order.yml +65 -0
- package/rules/tree-sitter-queries/cobol/alter-statement.yml +39 -0
- package/rules/tree-sitter-queries/cpp/no-auto-ptr.yml +49 -0
- package/rules/tree-sitter-queries/cpp/no-confused-move-forward.yml +59 -0
- package/rules/tree-sitter-queries/cpp/no-memset-sensitive-data.yml +57 -0
- package/rules/tree-sitter-queries/cpp/no-scoped-lock-without-args.yml +52 -0
- package/rules/tree-sitter-queries/cpp/noexcept-functions.yml +58 -0
- package/rules/tree-sitter-queries/csharp/async-await-identifiers.yml +50 -0
- package/rules/tree-sitter-queries/csharp/is-with-this.yml +52 -0
- package/rules/tree-sitter-queries/csharp/no-dangerous-get-handle.yml +59 -0
- package/rules/tree-sitter-queries/csharp/no-operator-eq-reference.yml +60 -0
- package/rules/tree-sitter-queries/csharp/no-thread-resume-suspend.yml +60 -0
- package/rules/tree-sitter-queries/css/calc-spacing.yml +50 -0
- package/rules/tree-sitter-queries/java/junit-call-super.yml +63 -0
- package/rules/tree-sitter-queries/java/main-should-not-throw.yml +63 -0
- package/rules/tree-sitter-queries/java/no-clone-override.yml +60 -0
- package/rules/tree-sitter-queries/java/no-double-checked-locking.yml +74 -0
- package/rules/tree-sitter-queries/java/no-exit-methods.yml +50 -0
- package/rules/tree-sitter-queries/java/no-field-shadowing.yml +66 -0
- package/rules/tree-sitter-queries/java/no-future-keywords.yml +49 -0
- package/rules/tree-sitter-queries/java/no-threadgroup.yml +52 -0
- package/rules/tree-sitter-queries/java/no-threads-in-constructors.yml +73 -0
- package/rules/tree-sitter-queries/java/no-wait-notify-on-thread.yml +58 -0
- package/rules/tree-sitter-queries/java/prepared-statement-valid-indices.yml +55 -0
- package/rules/tree-sitter-queries/java/spring-session-attributes-setcomplete.yml +75 -0
- package/rules/tree-sitter-queries/java/springboot-default-package.yml +69 -0
- package/rules/tree-sitter-queries/java/switch-fall-through.yml +70 -0
- package/rules/tree-sitter-queries/java/switch-non-case-labels.yml +62 -0
- package/rules/tree-sitter-queries/javascript/switch-non-case-labels.yml +52 -0
- package/rules/tree-sitter-queries/kotlin/prepared-statement-indices.yml +49 -0
- package/rules/tree-sitter-queries/php/no-exit-die.yml +52 -0
- package/rules/tree-sitter-queries/php/this-in-static-context.yml +75 -0
- package/rules/tree-sitter-queries/plsql/delete-update-where.yml +56 -0
- package/rules/tree-sitter-queries/plsql/end-loop-semicolon.yml +51 -0
- package/rules/tree-sitter-queries/plsql/fetch-bulk-collect-limit.yml +47 -0
- package/rules/tree-sitter-queries/plsql/forallsave-exceptions.yml +51 -0
- package/rules/tree-sitter-queries/plsql/no-synchronize.yml +47 -0
- package/rules/tree-sitter-queries/plsql/not-null-initialization.yml +59 -0
- package/rules/tree-sitter-queries/plsql/raise-application-error-codes.yml +50 -0
- package/rules/tree-sitter-queries/python/exit-signature-check.yml +66 -0
- package/rules/tree-sitter-queries/python/in-operator-unsupported.yml +57 -0
- package/rules/tree-sitter-queries/python/iter-return-iterator.yml +59 -0
- package/rules/tree-sitter-queries/python/notimplemented-boolean-context.yml +67 -0
- package/rules/tree-sitter-queries/python/return-in-generator.yml +58 -0
- package/rules/tree-sitter-queries/python/return-in-init.yml +59 -0
- package/rules/tree-sitter-queries/python/send-file-mimetype.yml +53 -0
- package/rules/tree-sitter-queries/python/yield-return-outside-function.yml +57 -0
- package/tools/lsp-navigation.js +14 -16
- package/tools/lsp-navigation.ts +14 -18
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,27 @@ All notable changes to pi-lens will be documented in this file.
|
|
|
4
4
|
|
|
5
5
|
## [Unreleased]
|
|
6
6
|
|
|
7
|
+
## [3.8.39] - 2026-05-02
|
|
8
|
+
|
|
9
|
+
### Fixed
|
|
10
|
+
|
|
11
|
+
- **Context injection now prepends guidance before the user prompt** — pi-lens previously appended session guidance after the user's message; provider bridges that treat the last message as the active user action would demote the real request. Guidance is now prepended so the user's prompt stays last. (PR #48 by @tifandotme)
|
|
12
|
+
- **jscpd no longer runs on YAML/JSON/Markdown files** — `getFilesForJscpd` now filters to source code extensions only, preventing multi-second delays at `turn_end` when editing rule YAMLs or config files.
|
|
13
|
+
- **ReDoS S5852 final (gleam/zig parsers)** — rewrote `gleamRe` and `zigRe` as line-by-line parsers, eliminating the multiline flag that SonarCloud continued to flag despite `[ \t]*` substitution.
|
|
14
|
+
- **SonarCloud MAJOR code smells (batch 1 & 2)** — `readonly` members, `void` operator removals, nested ternaries, nested template literals, optional chains, duplicate branches, and redundant type alias across 15+ files.
|
|
15
|
+
- **9 tree-sitter query bugs in new rule files** — predicate outside outermost parens (`cpp/no-auto-ptr`); false-positive `post_filter` gate added (`cpp/no-confused-move-forward`); leaf-node child match removed (`php/this-in-static-context`); invalid node name `class_hereditary` replaced (`java/no-field-shadowing`); field order corrected (`java/no-wait-notify-on-thread`); duplicate `modifiers` blocks merged (`java/spring-session-attributes-setcomplete`); invalid anonymous-node field label removed (`csharp/is-with-this`); inline alternation replaced with two patterns (`python/in-operator-unsupported`); adjacent sibling requirement removed, delegated to `post_filter` (`python/return-in-generator`).
|
|
16
|
+
|
|
17
|
+
## [3.8.38] - 2026-05-02
|
|
18
|
+
|
|
19
|
+
### Added
|
|
20
|
+
|
|
21
|
+
- **`RuleCache` respects `PILENS_DATA_DIR`** — tree-sitter rule cache files are now stored under `getProjectDataDir(rootDir)` instead of `<cwd>/.pi-lens/cache`, consistent with all other pi-lens data files. Projects using `PILENS_DATA_DIR` no longer get a stray `.pi-lens` directory created in the project root. (PR #47 by @tifandotme)
|
|
22
|
+
|
|
23
|
+
### Fixed
|
|
24
|
+
|
|
25
|
+
- **ReDoS: `gleamRe` and `zigRe` compiler parsers** — residual `\s*` quantifiers (which match `\n` in JS) replaced with `[ \t]*` to eliminate cross-line backtracking. Completes the SonarCloud S5852 remediation started in 3.8.37.
|
|
26
|
+
- **Test env leak in `file-utils.test.ts`** — `PILENS_DATA_DIR` is now saved and restored in a `finally` block so it doesn't bleed into subsequent tests in the suite.
|
|
27
|
+
|
|
7
28
|
## [3.8.37] - 2026-05-02
|
|
8
29
|
|
|
9
30
|
### Fixed
|
package/README.md
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<p align="center">
|
|
2
|
-
<img src="https://raw.githubusercontent.com/apmantza/pi-lens/master/banner.
|
|
2
|
+
<img src="https://raw.githubusercontent.com/apmantza/pi-lens/master/banner.png" alt="pi-lens" width="1100">
|
|
3
3
|
</p>
|
|
4
4
|
|
|
5
5
|
# pi-lens
|
|
@@ -41,7 +41,7 @@ At `session_start`, pi-lens:
|
|
|
41
41
|
- applies language-aware startup defaults for tool preinstall
|
|
42
42
|
- warms caches and optional indexes (with overlap/session guardrails)
|
|
43
43
|
- emits missing-tool install hints for detected languages when relevant
|
|
44
|
-
-
|
|
44
|
+
- prepends session guidance before the user's prompt so provider bridges keep the real prompt active
|
|
45
45
|
- opens `warmFiles` (if configured in `.pi-lens/lsp.json`) to seed lazy-indexing language servers like clangd before the first symbol query
|
|
46
46
|
|
|
47
47
|
For one-shot print sessions (for example `pi --print ...`), pi-lens auto-uses a quick startup path that skips heavy bootstrap work to reduce startup latency. Override with `PI_LENS_STARTUP_MODE=full|minimal|quick`.
|
package/banner.png
ADDED
|
Binary file
|
package/banner.svg
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
<svg width="1100" height="
|
|
1
|
+
<svg width="1100" height="310" viewBox="0 0 1100 310" xmlns="http://www.w3.org/2000/svg">
|
|
2
2
|
<defs>
|
|
3
3
|
<linearGradient id="bg" x1="0%" y1="0%" x2="100%" y2="100%">
|
|
4
4
|
<stop offset="0%" stop-color="#0d1117"/>
|
|
@@ -13,26 +13,26 @@
|
|
|
13
13
|
<stop offset="100%" stop-color="#2f81f700"/>
|
|
14
14
|
</linearGradient>
|
|
15
15
|
<clipPath id="bounds">
|
|
16
|
-
<rect width="1100" height="
|
|
16
|
+
<rect width="1100" height="310" rx="12"/>
|
|
17
17
|
</clipPath>
|
|
18
18
|
</defs>
|
|
19
19
|
|
|
20
20
|
<!-- Background -->
|
|
21
|
-
<rect width="1100" height="
|
|
21
|
+
<rect width="1100" height="310" fill="url(#bg)" rx="12"/>
|
|
22
22
|
|
|
23
23
|
<!-- Top accent line -->
|
|
24
24
|
<rect x="0" y="0" width="1100" height="3" fill="url(#accent)" rx="1.5" clip-path="url(#bounds)"/>
|
|
25
25
|
|
|
26
26
|
<!-- Decorative aperture rings (right side) -->
|
|
27
27
|
<g opacity="0.07" clip-path="url(#bounds)">
|
|
28
|
-
<circle cx="920" cy="
|
|
29
|
-
<circle cx="920" cy="
|
|
30
|
-
<circle cx="920" cy="
|
|
31
|
-
<circle cx="920" cy="
|
|
32
|
-
<line x1="740" y1="
|
|
33
|
-
<line x1="920" y1="-40" x2="920" y2="
|
|
34
|
-
<line x1="793" y1="
|
|
35
|
-
<line x1="1047" y1="
|
|
28
|
+
<circle cx="920" cy="155" r="180" fill="none" stroke="#2f81f7" stroke-width="1.5"/>
|
|
29
|
+
<circle cx="920" cy="155" r="145" fill="none" stroke="#2f81f7" stroke-width="1"/>
|
|
30
|
+
<circle cx="920" cy="155" r="110" fill="none" stroke="#2f81f7" stroke-width="1"/>
|
|
31
|
+
<circle cx="920" cy="155" r="75" fill="none" stroke="#2f81f7" stroke-width="1"/>
|
|
32
|
+
<line x1="740" y1="155" x2="1100" y2="155" stroke="#2f81f7" stroke-width="0.8"/>
|
|
33
|
+
<line x1="920" y1="-40" x2="920" y2="350" stroke="#2f81f7" stroke-width="0.8"/>
|
|
34
|
+
<line x1="793" y1="28" x2="1047" y2="282" stroke="#2f81f7" stroke-width="0.6"/>
|
|
35
|
+
<line x1="1047" y1="28" x2="793" y2="282" stroke="#2f81f7" stroke-width="0.6"/>
|
|
36
36
|
</g>
|
|
37
37
|
|
|
38
38
|
<!-- Lens icon -->
|
|
@@ -53,7 +53,7 @@
|
|
|
53
53
|
<!-- Tagline -->
|
|
54
54
|
<text x="261" y="164" font-family="-apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, sans-serif" font-size="21" font-weight="400" fill="#8b949e" letter-spacing="0.3">Real-time code intelligence for AI agents</text>
|
|
55
55
|
|
|
56
|
-
<!-- Feature pills -->
|
|
56
|
+
<!-- Feature pills — row 1 -->
|
|
57
57
|
<g font-family="-apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, sans-serif" font-size="12" font-weight="500">
|
|
58
58
|
<rect x="261" y="196" width="56" height="24" rx="12" fill="#2f81f715" stroke="#2f81f740" stroke-width="1"/>
|
|
59
59
|
<text x="289" y="213" fill="#58a6ff" text-anchor="middle">LSP</text>
|
|
@@ -64,10 +64,25 @@
|
|
|
64
64
|
<rect x="405" y="196" width="90" height="24" rx="12" fill="#2f81f715" stroke="#2f81f740" stroke-width="1"/>
|
|
65
65
|
<text x="450" y="213" fill="#58a6ff" text-anchor="middle">Formatters</text>
|
|
66
66
|
|
|
67
|
-
<rect x="505" y="196" width="
|
|
68
|
-
<text x="
|
|
67
|
+
<rect x="505" y="196" width="84" height="24" rx="12" fill="#2f81f715" stroke="#2f81f740" stroke-width="1"/>
|
|
68
|
+
<text x="547" y="213" fill="#58a6ff" text-anchor="middle">Auto-Fix</text>
|
|
69
69
|
|
|
70
|
-
<rect x="
|
|
71
|
-
<text x="
|
|
70
|
+
<rect x="599" y="196" width="100" height="24" rx="12" fill="#2f81f715" stroke="#2f81f740" stroke-width="1"/>
|
|
71
|
+
<text x="649" y="213" fill="#58a6ff" text-anchor="middle">Read-Guard</text>
|
|
72
|
+
</g>
|
|
73
|
+
|
|
74
|
+
<!-- Feature pills — row 2 -->
|
|
75
|
+
<g font-family="-apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, sans-serif" font-size="12" font-weight="500">
|
|
76
|
+
<rect x="261" y="228" width="152" height="24" rx="12" fill="#2f81f715" stroke="#2f81f740" stroke-width="1"/>
|
|
77
|
+
<text x="337" y="245" fill="#58a6ff" text-anchor="middle">Structural Analysis</text>
|
|
78
|
+
|
|
79
|
+
<rect x="423" y="228" width="96" height="24" rx="12" fill="#2f81f715" stroke="#2f81f740" stroke-width="1"/>
|
|
80
|
+
<text x="471" y="245" fill="#58a6ff" text-anchor="middle">Cascade</text>
|
|
81
|
+
|
|
82
|
+
<rect x="529" y="228" width="100" height="24" rx="12" fill="#2f81f715" stroke="#2f81f740" stroke-width="1"/>
|
|
83
|
+
<text x="579" y="245" fill="#58a6ff" text-anchor="middle">Tree-sitter</text>
|
|
84
|
+
|
|
85
|
+
<rect x="639" y="228" width="96" height="24" rx="12" fill="#2f81f715" stroke="#2f81f740" stroke-width="1"/>
|
|
86
|
+
<text x="687" y="245" fill="#58a6ff" text-anchor="middle">ast-grep</text>
|
|
72
87
|
</g>
|
|
73
88
|
</svg>
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
import * as crypto from "node:crypto";
|
|
9
9
|
import * as fs from "node:fs";
|
|
10
10
|
import * as path from "node:path";
|
|
11
|
+
import { getProjectDataDir } from "../file-utils.js";
|
|
11
12
|
|
|
12
13
|
const CACHE_VERSION = "v1";
|
|
13
14
|
|
|
@@ -34,7 +35,7 @@ export class RuleCache {
|
|
|
34
35
|
private cacheDir: string;
|
|
35
36
|
|
|
36
37
|
constructor(language: string, rootDir = process.cwd()) {
|
|
37
|
-
this.cacheDir = path.join(rootDir, "
|
|
38
|
+
this.cacheDir = path.join(getProjectDataDir(rootDir), "cache");
|
|
38
39
|
this.cacheFile = path.join(
|
|
39
40
|
this.cacheDir,
|
|
40
41
|
`${language}-rules-${CACHE_VERSION}.json`,
|
package/clients/cache-manager.ts
CHANGED
|
@@ -307,10 +307,13 @@ export class CacheManager {
|
|
|
307
307
|
|
|
308
308
|
/**
|
|
309
309
|
* Get files that need jscpd re-scan (any edit).
|
|
310
|
+
* Only returns source code files jscpd can meaningfully analyse.
|
|
310
311
|
*/
|
|
311
312
|
getFilesForJscpd(cwd: string): string[] {
|
|
312
313
|
const state = this.readTurnState(cwd);
|
|
313
|
-
return Object.keys(state.files)
|
|
314
|
+
return Object.keys(state.files).filter((f) =>
|
|
315
|
+
/\.(ts|tsx|js|jsx|mjs|cjs|py|go|rs|rb|java|cs|php|cpp|c|h|hpp|swift|kt)$/.test(f),
|
|
316
|
+
);
|
|
314
317
|
}
|
|
315
318
|
|
|
316
319
|
/**
|
|
@@ -61,7 +61,8 @@ export function scheduleProviders(providers: FactProvider[]): FactProvider[] {
|
|
|
61
61
|
}
|
|
62
62
|
}
|
|
63
63
|
}
|
|
64
|
-
|
|
64
|
+
nextWave.sort((a, b) => a.id.localeCompare(b.id));
|
|
65
|
+
wave = nextWave;
|
|
65
66
|
}
|
|
66
67
|
|
|
67
68
|
if (result.length < providers.length) {
|
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
import { normalizeMapKey } from "../path-utils.js";
|
|
2
2
|
|
|
3
|
-
type FactValue = unknown;
|
|
4
|
-
|
|
5
3
|
export interface ReadonlyFactStore {
|
|
6
4
|
getFileFact<T>(filePath: string, factId: string): T | undefined;
|
|
7
5
|
hasFileFact(filePath: string, factId: string): boolean;
|
|
@@ -10,8 +8,8 @@ export interface ReadonlyFactStore {
|
|
|
10
8
|
}
|
|
11
9
|
|
|
12
10
|
export class FactStore implements ReadonlyFactStore {
|
|
13
|
-
private readonly fileFacts = new Map<string, Map<string,
|
|
14
|
-
private readonly sessionFacts = new Map<string,
|
|
11
|
+
private readonly fileFacts = new Map<string, Map<string, unknown>>();
|
|
12
|
+
private readonly sessionFacts = new Map<string, unknown>();
|
|
15
13
|
|
|
16
14
|
// All file-keyed methods normalize the path internally via normalizeMapKey().
|
|
17
15
|
// Callers always pass raw/resolved paths — normalization is not their concern.
|
|
@@ -20,7 +18,7 @@ export class FactStore implements ReadonlyFactStore {
|
|
|
20
18
|
return this.fileFacts.get(normalizeMapKey(filePath))?.get(factId) as T | undefined;
|
|
21
19
|
}
|
|
22
20
|
|
|
23
|
-
setFileFact(filePath: string, factId: string, value:
|
|
21
|
+
setFileFact(filePath: string, factId: string, value: unknown): void {
|
|
24
22
|
const key = normalizeMapKey(filePath);
|
|
25
23
|
let facts = this.fileFacts.get(key);
|
|
26
24
|
if (!facts) {
|
|
@@ -51,7 +49,7 @@ export class FactStore implements ReadonlyFactStore {
|
|
|
51
49
|
return this.sessionFacts.get(factId) as T | undefined;
|
|
52
50
|
}
|
|
53
51
|
|
|
54
|
-
setSessionFact(factId: string, value:
|
|
52
|
+
setSessionFact(factId: string, value: unknown): void {
|
|
55
53
|
this.sessionFacts.set(factId, value);
|
|
56
54
|
}
|
|
57
55
|
|
|
@@ -479,7 +479,7 @@ export async function computeCascadeForFile(
|
|
|
479
479
|
.filter((n) => !primaryFilesThisTurn.has(normalizeMapKey(n)))
|
|
480
480
|
.sort((a, b) => {
|
|
481
481
|
const rank = (p: string) =>
|
|
482
|
-
importerSet.has(p) ? 0 : callerSet.has(p) ? 1 : 2;
|
|
482
|
+
importerSet.has(p) ? 0 : (callerSet.has(p) ? 1 : 2);
|
|
483
483
|
return rank(a) - rank(b);
|
|
484
484
|
})
|
|
485
485
|
.slice(0, MAX_FILES);
|
|
@@ -584,10 +584,7 @@ export async function computeCascadeForFile(
|
|
|
584
584
|
// write sequence. A new write (higher writeSeq) invalidates the cache entry.
|
|
585
585
|
const cached =
|
|
586
586
|
writeSeq != null ? neighborTouchCache.get(cacheKey) : undefined;
|
|
587
|
-
if (
|
|
588
|
-
cached != null &&
|
|
589
|
-
cached.turnSeq === turnSeq
|
|
590
|
-
) {
|
|
587
|
+
if (cached?.turnSeq === turnSeq) {
|
|
591
588
|
producedLspData = true;
|
|
592
589
|
const durationMs = Date.now() - neighborStart;
|
|
593
590
|
logCascade({
|
|
@@ -149,21 +149,16 @@ export const noBooleanParamsRule: FactRule = {
|
|
|
149
149
|
ts.isIdentifier(param.name) ? param.name.text : "";
|
|
150
150
|
if (BOOLEAN_PREFIX_OK.test(name)) continue;
|
|
151
151
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
t.literal.kind === ts.SyntaxKind.FalseKeyword)),
|
|
163
|
-
)
|
|
164
|
-
) {
|
|
165
|
-
isBoolean = true;
|
|
166
|
-
}
|
|
152
|
+
const isBoolean =
|
|
153
|
+
param.type.kind === ts.SyntaxKind.BooleanKeyword ||
|
|
154
|
+
(ts.isUnionTypeNode(param.type) &&
|
|
155
|
+
param.type.types.every(
|
|
156
|
+
(t) =>
|
|
157
|
+
t.kind === ts.SyntaxKind.BooleanKeyword ||
|
|
158
|
+
(ts.isLiteralTypeNode(t) &&
|
|
159
|
+
(t.literal.kind === ts.SyntaxKind.TrueKeyword ||
|
|
160
|
+
t.literal.kind === ts.SyntaxKind.FalseKeyword)),
|
|
161
|
+
));
|
|
167
162
|
|
|
168
163
|
if (!isBoolean) continue;
|
|
169
164
|
const { line, character } = sf.getLineAndCharacterOfPosition(param.getStart(sf));
|
|
@@ -134,7 +134,7 @@ const biomeCheckJsonRunner: RunnerDefinition = {
|
|
|
134
134
|
diagnostics: [
|
|
135
135
|
{
|
|
136
136
|
id: "biome:parse-error:1",
|
|
137
|
-
message:
|
|
137
|
+
message: "Biome JSON parse failed: " + parsed.parseError + (preview ? " (output preview: " + preview + ")" : ""),
|
|
138
138
|
filePath: ctx.filePath,
|
|
139
139
|
line: 1,
|
|
140
140
|
column: 1,
|
|
@@ -125,7 +125,7 @@ const eslintRunner: RunnerDefinition = {
|
|
|
125
125
|
diagnostics: [
|
|
126
126
|
{
|
|
127
127
|
id: "eslint:parse-error:1",
|
|
128
|
-
message:
|
|
128
|
+
message: "ESLint JSON parse failed: " + parsed.parseError + (preview ? " (output preview: " + preview + ")" : ""),
|
|
129
129
|
filePath: ctx.filePath,
|
|
130
130
|
line: 1,
|
|
131
131
|
column: 1,
|
|
@@ -212,11 +212,7 @@ const lspRunner: RunnerDefinition = {
|
|
|
212
212
|
);
|
|
213
213
|
|
|
214
214
|
const hasErrors = diagnostics.some((d) => d.semantic === "blocking");
|
|
215
|
-
const resultSemantic = hasErrors
|
|
216
|
-
? "blocking"
|
|
217
|
-
: diagnostics.length > 0
|
|
218
|
-
? "warning"
|
|
219
|
-
: "none";
|
|
215
|
+
const resultSemantic = hasErrors ? "blocking" : (diagnostics.length > 0 ? "warning" : "none");
|
|
220
216
|
|
|
221
217
|
return {
|
|
222
218
|
status: hasErrors ? "failed" : "succeeded",
|
|
@@ -86,7 +86,7 @@ function parsePSAnalyzerOutput(raw: string, filePath: string): Diagnostic[] {
|
|
|
86
86
|
.map((item) => {
|
|
87
87
|
const sev = (item.Severity ?? "Warning").toLowerCase();
|
|
88
88
|
const severity: "error" | "warning" | "info" =
|
|
89
|
-
sev === "error" || sev === "parseerror" ? "error" : sev === "information" ? "info" : "warning";
|
|
89
|
+
(sev === "error" || sev === "parseerror") ? "error" : (sev === "information" ? "info" : "warning");
|
|
90
90
|
const rule = item.RuleName ?? "PSScriptAnalyzer";
|
|
91
91
|
return {
|
|
92
92
|
id: `psscriptanalyzer-${rule}-${item.Line}`,
|
|
@@ -394,10 +394,7 @@ function getArrowSignature(
|
|
|
394
394
|
| import("typescript").ArrowFunction
|
|
395
395
|
| import("typescript").FunctionExpression,
|
|
396
396
|
): string {
|
|
397
|
-
|
|
398
|
-
.map((p) => (tsModule.isIdentifier(p.name) ? p.name.text : "param"))
|
|
399
|
-
.join(", ");
|
|
400
|
-
return `(${params})`;
|
|
397
|
+
return getSignature(tsModule, node as unknown as import("typescript").FunctionDeclaration);
|
|
401
398
|
}
|
|
402
399
|
|
|
403
400
|
// ============================================================================
|
|
@@ -16,16 +16,10 @@ export function convertLspDiagnostics(
|
|
|
16
16
|
return diags
|
|
17
17
|
.filter((d) => d.range?.start?.line !== undefined)
|
|
18
18
|
.map((d, idx) => {
|
|
19
|
-
const
|
|
20
|
-
|
|
21
|
-
? "error"
|
|
22
|
-
: d.severity === 2
|
|
23
|
-
? "warning"
|
|
24
|
-
: d.severity === 4
|
|
25
|
-
? "hint"
|
|
26
|
-
: "info";
|
|
19
|
+
const severityMap: Record<number, "error" | "warning" | "hint"> = { 1: "error", 2: "warning", 4: "hint" };
|
|
20
|
+
const severity: "error" | "warning" | "info" | "hint" = severityMap[d.severity] ?? "info";
|
|
27
21
|
const semantic =
|
|
28
|
-
d.severity === 1 ? "blocking" : d.severity === 2 ? "warning" : "none";
|
|
22
|
+
d.severity === 1 ? "blocking" : (d.severity === 2 ? "warning" : "none");
|
|
29
23
|
const code = String(d.code ?? "unknown");
|
|
30
24
|
const source = options.source ?? d.source ?? tool;
|
|
31
25
|
const hasSuggestion = options.fixSuggestionByIndex?.has(idx) ?? false;
|
package/clients/formatters.ts
CHANGED
|
@@ -54,7 +54,7 @@ async function tryLazyInstallFormatterTool(
|
|
|
54
54
|
const ok = !res.error && res.status === 0;
|
|
55
55
|
if (!ok) {
|
|
56
56
|
console.error(
|
|
57
|
-
`[format] lazy-install rubocop failed: ${res.error?.message ?? res.stderr ??
|
|
57
|
+
`[format] lazy-install rubocop failed: ${res.error?.message ?? res.stderr ?? "exit " + res.status}`,
|
|
58
58
|
);
|
|
59
59
|
}
|
|
60
60
|
return ok;
|
|
@@ -67,7 +67,7 @@ async function tryLazyInstallFormatterTool(
|
|
|
67
67
|
const ok = !res.error && res.status === 0;
|
|
68
68
|
if (!ok) {
|
|
69
69
|
console.error(
|
|
70
|
-
`[format] lazy-install rustfmt failed: ${res.error?.message ?? res.stderr ??
|
|
70
|
+
`[format] lazy-install rustfmt failed: ${res.error?.message ?? res.stderr ?? "exit " + res.status}`,
|
|
71
71
|
);
|
|
72
72
|
}
|
|
73
73
|
return ok;
|
|
@@ -919,11 +919,16 @@ export async function getFormattersForFile(
|
|
|
919
919
|
|
|
920
920
|
const enabled = selected ? [selected] : [];
|
|
921
921
|
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
922
|
+
let selectionReason: string;
|
|
923
|
+
if (!selected) {
|
|
924
|
+
selectionReason = "none";
|
|
925
|
+
} else if (!formatterPolicy) {
|
|
926
|
+
selectionReason = "detect";
|
|
927
|
+
} else {
|
|
928
|
+
selectionReason = candidateFormatters.some((f) => hasExplicitFormatterConfig(f.name, cwd))
|
|
929
|
+
? "explicit-config"
|
|
930
|
+
: "smart-default";
|
|
931
|
+
}
|
|
927
932
|
logLatency({
|
|
928
933
|
type: "phase",
|
|
929
934
|
phase: "formatter_selected",
|
package/clients/lsp/index.ts
CHANGED
|
@@ -135,13 +135,13 @@ export interface LSPTouchFileOptions {
|
|
|
135
135
|
|
|
136
136
|
export class LSPService {
|
|
137
137
|
private state: LSPState;
|
|
138
|
-
private workspaceProbeLogged = new Set<string>();
|
|
139
|
-
private warmStartLogged = new Set<string>();
|
|
140
|
-
private optionalFailureLogged = new Set<string>();
|
|
141
|
-
private optionalDisabled = new Set<string>();
|
|
138
|
+
private readonly workspaceProbeLogged = new Set<string>();
|
|
139
|
+
private readonly warmStartLogged = new Set<string>();
|
|
140
|
+
private readonly optionalFailureLogged = new Set<string>();
|
|
141
|
+
private readonly optionalDisabled = new Set<string>();
|
|
142
142
|
/** Consecutive failure counts for exponential backoff circuit breaker */
|
|
143
|
-
private failureCounts = new Map<string, number>();
|
|
144
|
-
private recentTouches = new Map<
|
|
143
|
+
private readonly failureCounts = new Map<string, number>();
|
|
144
|
+
private readonly recentTouches = new Map<
|
|
145
145
|
string,
|
|
146
146
|
{ fingerprint: string; touchedAt: number; clientScope: "primary" | "all" }
|
|
147
147
|
>();
|
|
@@ -309,7 +309,7 @@ export class LSPService {
|
|
|
309
309
|
if (!root) continue;
|
|
310
310
|
const key = `${server.id}:${normalizeMapKey(root)}`;
|
|
311
311
|
const existing = this.state.clients.get(key);
|
|
312
|
-
if (existing
|
|
312
|
+
if (existing?.isAlive()) {
|
|
313
313
|
return { client: existing, info: server };
|
|
314
314
|
}
|
|
315
315
|
}
|
|
@@ -392,9 +392,7 @@ export class LSPService {
|
|
|
392
392
|
}
|
|
393
393
|
}
|
|
394
394
|
|
|
395
|
-
private shouldAllowInstall(
|
|
396
|
-
void filePath;
|
|
397
|
-
void root;
|
|
395
|
+
private shouldAllowInstall(_filePath: string, _root: string): boolean {
|
|
398
396
|
return process.env.PI_LENS_DISABLE_LSP_INSTALL !== "1";
|
|
399
397
|
}
|
|
400
398
|
|
package/clients/lsp/launch.ts
CHANGED
|
@@ -402,7 +402,7 @@ function _attachErrorHandler(
|
|
|
402
402
|
proc.on("error", (err) => {
|
|
403
403
|
if (logContext) {
|
|
404
404
|
logSessionStart(
|
|
405
|
-
|
|
405
|
+
"lsp process " + context + ": spawn-error command=" + logContext.command + " args=" + JSON.stringify(logContext.args) + " cwd=" + logContext.cwd + " pid=" + (logContext.pid ?? 0) + " error=" + err.message + (stderrPreview ? " stderr=" + compactLogValue(stderrPreview) : ""),
|
|
406
406
|
);
|
|
407
407
|
}
|
|
408
408
|
|
|
@@ -422,12 +422,12 @@ function _attachErrorHandler(
|
|
|
422
422
|
if (code !== 0 && code !== null) {
|
|
423
423
|
if (logContext) {
|
|
424
424
|
logSessionStart(
|
|
425
|
-
|
|
425
|
+
"lsp process " + context + ": closed code=" + code + (signal ? " signal=" + signal : "") + " command=" + logContext.command + " args=" + JSON.stringify(logContext.args) + " cwd=" + logContext.cwd + " pid=" + (logContext.pid ?? 0) + (stderrPreview ? " stderr=" + compactLogValue(stderrPreview) : ""),
|
|
426
426
|
);
|
|
427
427
|
}
|
|
428
428
|
} else if (signal && logContext) {
|
|
429
429
|
logSessionStart(
|
|
430
|
-
|
|
430
|
+
"lsp process " + context + ": closed signal=" + signal + " command=" + logContext.command + " args=" + JSON.stringify(logContext.args) + " cwd=" + logContext.cwd + " pid=" + (logContext.pid ?? 0) + (stderrPreview ? " stderr=" + compactLogValue(stderrPreview) : ""),
|
|
431
431
|
);
|
|
432
432
|
}
|
|
433
433
|
});
|
|
@@ -467,11 +467,8 @@ export async function launchLSP(
|
|
|
467
467
|
// - If already absolute, use as-is
|
|
468
468
|
// - If it's a simple command (no path separators), let system find it via PATH
|
|
469
469
|
// - Otherwise, resolve relative to cwd
|
|
470
|
-
const
|
|
471
|
-
|
|
472
|
-
: command.includes(path.sep) || command.includes("/")
|
|
473
|
-
? path.resolve(cwd, command)
|
|
474
|
-
: command;
|
|
470
|
+
const isRelativePath = !path.isAbsolute(command) && (command.includes(path.sep) || command.includes("/"));
|
|
471
|
+
const explicitCommand = isRelativePath ? path.resolve(cwd, command) : command;
|
|
475
472
|
const resolvedCommand =
|
|
476
473
|
!path.isAbsolute(command) &&
|
|
477
474
|
!command.includes(path.sep) &&
|
package/clients/lsp/server.ts
CHANGED
|
@@ -89,7 +89,7 @@ function logSessionStart(message: string): void {
|
|
|
89
89
|
return;
|
|
90
90
|
}
|
|
91
91
|
const line = `[${new Date().toISOString()}] ${message}\n`;
|
|
92
|
-
|
|
92
|
+
mkdir(SESSIONSTART_LOG_DIR, { recursive: true })
|
|
93
93
|
.then(() => appendFile(SESSIONSTART_LOG, line))
|
|
94
94
|
.catch(() => {
|
|
95
95
|
// best-effort logging
|
package/clients/pipeline.ts
CHANGED
|
@@ -96,7 +96,7 @@ function diffProjectSnapshot(root: string, before: FileSnapshot): string[] {
|
|
|
96
96
|
const changed = new Set<string>();
|
|
97
97
|
for (const [filePath, next] of after) {
|
|
98
98
|
const prev = before.get(filePath);
|
|
99
|
-
if (
|
|
99
|
+
if (prev?.mtimeMs !== next.mtimeMs || prev?.size !== next.size) {
|
|
100
100
|
changed.add(filePath);
|
|
101
101
|
}
|
|
102
102
|
}
|
|
@@ -725,7 +725,7 @@ export async function runFormatPhase(
|
|
|
725
725
|
if (result.anyChanged) {
|
|
726
726
|
formatChanged = true;
|
|
727
727
|
dbg(
|
|
728
|
-
|
|
728
|
+
"autoformat: " + result.formatters.map((f) => f.name + "(" + (f.changed ? "changed" : "unchanged") + ")").join(", "),
|
|
729
729
|
);
|
|
730
730
|
}
|
|
731
731
|
if (!result.allSucceeded) {
|
|
@@ -734,7 +734,7 @@ export async function runFormatPhase(
|
|
|
734
734
|
...failures.map((f) => `${f.name}: ${f.error ?? "unknown error"}`),
|
|
735
735
|
);
|
|
736
736
|
dbg(
|
|
737
|
-
|
|
737
|
+
"autoformat: " + failures.map((f) => f.name + " failed: " + (f.error ?? "unknown error")).join("; "),
|
|
738
738
|
);
|
|
739
739
|
}
|
|
740
740
|
} catch (err) {
|
|
@@ -946,14 +946,9 @@ export async function runPipeline(
|
|
|
946
946
|
const changedList = [...piChangedFiles].map((changedFile) =>
|
|
947
947
|
toRunnerDisplayPath(cwd, changedFile),
|
|
948
948
|
);
|
|
949
|
-
const
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
.map((f) => ` - ${f}`)
|
|
953
|
-
.join(
|
|
954
|
-
"\n",
|
|
955
|
-
)}${changedList.length > 8 ? `\n - ... and ${changedList.length - 8} more` : ""}`
|
|
956
|
-
: "";
|
|
949
|
+
const topFiles = changedList.slice(0, 8).map((f) => " - " + f).join("\n");
|
|
950
|
+
const overflow = changedList.length > 8 ? "\n - ... and " + (changedList.length - 8) + " more" : "";
|
|
951
|
+
const fileList = changedList.length ? "\nModified files:\n" + topFiles + overflow : "";
|
|
957
952
|
output += `\n\n⚠️ **File was modified by auto-format/fix. You MUST re-read modified file(s) before making any further edits — the content on disk has changed (whitespace, indentation, quotes, or code). Editing from memory will produce mismatches.**${fileList}`;
|
|
958
953
|
}
|
|
959
954
|
phase.end("dispatch_lint", {
|
package/clients/read-guard.ts
CHANGED
|
@@ -210,9 +210,12 @@ export class ReadGuard {
|
|
|
210
210
|
const lastRead = fileReads[fileReads.length - 1];
|
|
211
211
|
const [editStart, editEnd] = touchedLines;
|
|
212
212
|
const lastReadEnd = lastRead.effectiveOffset + lastRead.effectiveLimit - 1;
|
|
213
|
+
const symbolCtx = lastRead.enclosingSymbol
|
|
214
|
+
? ` (${lastRead.enclosingSymbol.kind} \`${lastRead.enclosingSymbol.name}\`)`
|
|
215
|
+
: "";
|
|
213
216
|
const verdict = this.blockOrWarn(
|
|
214
217
|
"out-of-range",
|
|
215
|
-
`🔴 BLOCKED — Edit outside read range\n\nYou read \`${filePath}\` lines ${lastRead.effectiveOffset}-${lastReadEnd}${
|
|
218
|
+
`🔴 BLOCKED — Edit outside read range\n\nYou read \`${filePath}\` lines ${lastRead.effectiveOffset}-${lastReadEnd}${symbolCtx}, but your edit touches lines ${editStart}-${editEnd}.\n\nThe edit target is outside the context you previously read.\nTo proceed:\n 1. Read the relevant section: \`read path="${filePath}" offset=${Math.max(1, editStart - 5)} limit=${Math.min(30, editEnd - editStart + 10)}\`\n 2. Or read the full file: \`read path="${filePath}"\``,
|
|
216
219
|
{
|
|
217
220
|
editRange: touchedLines,
|
|
218
221
|
readRanges: fileReads.map((r) => ({
|
|
@@ -470,11 +473,7 @@ export class ReadGuard {
|
|
|
470
473
|
const reads = this.reads.get(filePath) ?? [];
|
|
471
474
|
logReadGuardEvent({
|
|
472
475
|
event:
|
|
473
|
-
verdict.action === "allow"
|
|
474
|
-
? "edit_allowed"
|
|
475
|
-
: verdict.action === "warn"
|
|
476
|
-
? "edit_warned"
|
|
477
|
-
: "edit_blocked",
|
|
476
|
+
verdict.action === "allow" ? "edit_allowed" : (verdict.action === "warn" ? "edit_warned" : "edit_blocked"),
|
|
478
477
|
sessionId: this.sessionId,
|
|
479
478
|
filePath,
|
|
480
479
|
metadata: {
|
|
@@ -44,7 +44,7 @@ export class RuntimeCoordinator {
|
|
|
44
44
|
};
|
|
45
45
|
private _complexityBaselines = new Map<string, FileComplexity>();
|
|
46
46
|
private _fixedThisTurn = new Set<string>();
|
|
47
|
-
private _reportedThisTurn = new Set<string>();
|
|
47
|
+
private readonly _reportedThisTurn = new Set<string>();
|
|
48
48
|
private _projectRulesScan: RuleScanResult = {
|
|
49
49
|
rules: [],
|
|
50
50
|
hasCustomRules: false,
|
|
@@ -56,7 +56,7 @@ export class RuntimeCoordinator {
|
|
|
56
56
|
private _gitGuardHasBlockers = false;
|
|
57
57
|
private _gitGuardSummary = "";
|
|
58
58
|
private _readGuard: ReadGuard | null = null;
|
|
59
|
-
private _pendingDeferredFormatFiles = new Map<string, DeferredFormatRecord>();
|
|
59
|
+
private readonly _pendingDeferredFormatFiles = new Map<string, DeferredFormatRecord>();
|
|
60
60
|
private readonly _lspReadWarmState = new Map<
|
|
61
61
|
string,
|
|
62
62
|
{ status: "warming" | "ready"; ts: number }
|
|
@@ -108,9 +108,7 @@ export async function handleToolResult(deps: ToolResultDeps): Promise<{
|
|
|
108
108
|
const rawFilePath = (event.input as { path?: string }).path;
|
|
109
109
|
const workspaceRoot = runtime.projectRoot || process.cwd();
|
|
110
110
|
const filePath = rawFilePath
|
|
111
|
-
? path.isAbsolute(rawFilePath)
|
|
112
|
-
? rawFilePath
|
|
113
|
-
: path.resolve(workspaceRoot, rawFilePath)
|
|
111
|
+
? (path.isAbsolute(rawFilePath) ? rawFilePath : path.resolve(workspaceRoot, rawFilePath))
|
|
114
112
|
: rawFilePath;
|
|
115
113
|
const behaviorWarnings = agentBehaviorRecord(event.toolName, filePath);
|
|
116
114
|
|
|
@@ -126,7 +126,7 @@ const SECRET_PATTERNS: SecretPattern[] = [
|
|
|
126
126
|
},
|
|
127
127
|
{
|
|
128
128
|
pattern:
|
|
129
|
-
/\b(secret|api_?key|token|access_?key)\b\s*[:=]\s*["']([a-zA-Z0-9_
|
|
129
|
+
/\b(secret|api_?key|token|access_?key)\b\s*[:=]\s*["']([a-zA-Z0-9_./-]{8,})["']/gi,
|
|
130
130
|
name: "hardcoded-secret",
|
|
131
131
|
message: "Possible hardcoded secret or API key",
|
|
132
132
|
},
|
|
@@ -46,7 +46,7 @@ export function formatSlopScoreSummary(summary: SlopScoreSummary): string {
|
|
|
46
46
|
const topRules = summary.ruleCounts.slice(0, 3);
|
|
47
47
|
const detail =
|
|
48
48
|
topRules.length > 0
|
|
49
|
-
?
|
|
49
|
+
? " (" + topRules.map((entry) => entry.ruleId + " ×" + entry.count).join(", ") + ")"
|
|
50
50
|
: "";
|
|
51
51
|
|
|
52
52
|
return `Slop score: ${summary.scorePerKloc.toFixed(1)}/KLOC${detail}`;
|
|
@@ -243,8 +243,9 @@ export class TreeSitterQueryLoader {
|
|
|
243
243
|
const nestedMatch = nextLine.match(/^\s+(\w+):\s*(.+)$/);
|
|
244
244
|
if (nestedMatch) {
|
|
245
245
|
let nv = nestedMatch[2].trim();
|
|
246
|
-
if (nv.startsWith('"') && nv.endsWith('"')) nv
|
|
247
|
-
|
|
246
|
+
if ((nv.startsWith('"') && nv.endsWith('"')) || (nv.startsWith("'") && nv.endsWith("'"))) {
|
|
247
|
+
nv = nv.slice(1, -1);
|
|
248
|
+
}
|
|
248
249
|
nestedObj[nestedMatch[1]] = nv;
|
|
249
250
|
}
|
|
250
251
|
}
|