pi-lens 3.8.19 → 3.8.21
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 +26 -0
- package/README.md +10 -7
- package/clients/dispatch/dispatcher.ts +53 -1
- package/clients/dispatch/integration.ts +37 -26
- package/clients/dispatch/plan.ts +26 -15
- package/clients/dispatch/runners/lsp.ts +6 -1
- package/clients/dispatch/runners/pyright.ts +4 -6
- package/clients/dispatch/runners/ruff.ts +52 -7
- package/clients/dispatch/runners/sqlfluff.ts +9 -3
- package/clients/dispatch/runners/yamllint.ts +3 -2
- package/clients/file-utils.ts +13 -2
- package/clients/formatters.ts +8 -4
- package/clients/installer/index.ts +371 -49
- package/clients/language-policy.ts +154 -0
- package/clients/language-profile.ts +167 -0
- package/clients/lsp/index.ts +81 -11
- package/clients/lsp/interactive-install.ts +35 -16
- package/clients/lsp/server.ts +357 -267
- package/clients/runtime-context.ts +26 -0
- package/clients/runtime-coordinator.ts +30 -2
- package/clients/runtime-session.ts +293 -103
- package/clients/runtime-tool-result.ts +8 -10
- package/clients/runtime-turn.ts +21 -4
- package/clients/todo-scanner.ts +6 -1
- package/index.ts +15 -3
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,32 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to pi-lens will be documented in this file.
|
|
4
4
|
|
|
5
|
+
## [3.8.21] - 2026-04-08
|
|
6
|
+
|
|
7
|
+
### Changed
|
|
8
|
+
- **Session guidance channeling** — session-start guidance is now injected as `system` context instead of synthetic `user` context, reducing acknowledgement-only first replies before task execution.
|
|
9
|
+
- **Coverage warning dedupe** — "Pi-lens analysis unavailable" warnings are now shown once per file per session and reset on session baseline reset.
|
|
10
|
+
|
|
11
|
+
### Fixed
|
|
12
|
+
- **Turn-end read-loop pressure** — turn-end findings now suppress duplicate persisted blocker prompts and avoid imperative "read this file" phrasing that could trigger repeated read loops.
|
|
13
|
+
|
|
14
|
+
## [3.8.20] - 2026-04-08
|
|
15
|
+
|
|
16
|
+
### Changed
|
|
17
|
+
- **Session startup hardening** — background startup tasks now run with session-generation safety guards and startup in-flight tracking, preventing stale task writes across session boundaries.
|
|
18
|
+
- **Turn-end overlap guardrails** — turn-end `knip`/`jscpd` checks now skip when the corresponding startup scan is still in-flight.
|
|
19
|
+
- **Language-profile centralization** — startup and dispatch now share a centralized project language profile for supported language detection and LSP-capable kind policy.
|
|
20
|
+
- **No-config startup defaults** — startup preinstall now applies language defaults (for example JS/TS -> `typescript-language-server`, Python -> `pyright`/`ruff`) while keeping heavy JS/TS scans config-gated.
|
|
21
|
+
- **Language setup hints** — `session_start` now emits actionable install hints for detected Go/Rust/Ruby projects when key tools are missing.
|
|
22
|
+
|
|
23
|
+
### Fixed
|
|
24
|
+
- **TODO baseline scan resilience** — unreadable files are now skipped safely instead of crashing TODO scanning in cloud-synced projects.
|
|
25
|
+
- **Startup scan gating consistency** — TODO warmup now respects startup warm-cache gating and avoids unnecessary scan work in restricted startup contexts.
|
|
26
|
+
- **Path exclusion coverage** — shared exclusion list now includes common agent/tooling directories (`.claude`, `.codex`, `.worktrees`, `.vscode`, and related dirs).
|
|
27
|
+
- **Ruff auto-install on Windows** — pip-based installation now supports fallback chains (`pip`, `py -m pip`, `python -m pip`) and process PATH normalization for user-level scripts.
|
|
28
|
+
- **Installer race duplication** — concurrent `ensureTool(...)` calls are now deduplicated per tool to avoid duplicate install attempts/noisy logs.
|
|
29
|
+
- **Python LSP root fallback** — Python LSP root detection now supports `.git` projects without Python config files.
|
|
30
|
+
|
|
5
31
|
## [3.8.19] - 2026-04-07
|
|
6
32
|
|
|
7
33
|
### Fixed
|
package/README.md
CHANGED
|
@@ -15,15 +15,18 @@ On every `write` and `edit`, pi-lens runs a fast, language-aware pipeline (check
|
|
|
15
15
|
- **Security checks**: secret scanning and structural security rules
|
|
16
16
|
- **Structural analysis**: tree-sitter + ast-grep for bug patterns across supported languages
|
|
17
17
|
- **Delta reporting**: prioritize new issues over legacy baseline noise
|
|
18
|
+
- **Coverage transparency**: when primary analysis tools are unavailable for a file kind, pi-lens emits a non-blocking inline "analysis unavailable" warning (deduped per file per session)
|
|
18
19
|
|
|
19
20
|
### Session Start
|
|
20
21
|
|
|
21
22
|
At `session_start`, pi-lens:
|
|
22
23
|
|
|
23
24
|
- resets runtime state and diagnostic telemetry
|
|
24
|
-
- detects project root and active tools
|
|
25
|
-
-
|
|
26
|
-
-
|
|
25
|
+
- detects project root, language profile, and active tools
|
|
26
|
+
- applies language-aware startup defaults for tool preinstall
|
|
27
|
+
- warms caches and optional indexes (with overlap/session guardrails)
|
|
28
|
+
- emits missing-tool install hints for detected languages when relevant
|
|
29
|
+
- injects session guidance through internal context (non-user channel) to reduce acknowledgement-only first responses
|
|
27
30
|
|
|
28
31
|
### Turn End
|
|
29
32
|
|
|
@@ -94,12 +97,12 @@ Auto-install behavior depends on gate type:
|
|
|
94
97
|
| `prettier` | Formatting fallback | Yes | Config-gated (Prettier dep or `package.json#prettier`) |
|
|
95
98
|
| `yamllint` | YAML linting | Yes | Config-gated (`.yamllint*` / tool section / dep hint) |
|
|
96
99
|
| `sqlfluff` | SQL linting/formatting | Yes | Config-gated (`.sqlfluff` / tool section / dep hint) |
|
|
97
|
-
| `ruff` | Python lint/format/autofix | Yes |
|
|
98
|
-
| `typescript-language-server` | Unified LSP diagnostics | Yes |
|
|
100
|
+
| `ruff` | Python lint/format/autofix | Yes | Language-default + flow-gated (Python detected; respects `--no-autofix-ruff`) |
|
|
101
|
+
| `typescript-language-server` | Unified LSP diagnostics | Yes | Language-default + flow-gated (JS/TS detected and LSP enabled) |
|
|
99
102
|
| `pyright` | Python type diagnostics fallback | Yes | Flow/language-gated (Python fallback paths) |
|
|
100
103
|
| `@ast-grep/cli` (`sg`) | AST scans/search/replace | Yes | Operational prewarm + analysis flows |
|
|
101
|
-
| `knip` | Dead code analysis | Yes | Operational prewarm + turn-end flows |
|
|
102
|
-
| `jscpd` | Duplicate code detection | Yes | Operational prewarm + turn-end flows |
|
|
104
|
+
| `knip` | Dead code analysis | Yes | Operational prewarm + turn-end flows (JS/TS language + config gated at startup) |
|
|
105
|
+
| `jscpd` | Duplicate code detection | Yes | Operational prewarm + turn-end flows (JS/TS language + config gated at startup) |
|
|
103
106
|
| `madge` | Circular dependency analysis | Yes | Turn-end analysis flow |
|
|
104
107
|
|
|
105
108
|
LSP is enabled by default. pi-lens includes many language-server definitions (including up to 31+ servers), and activates them when the server is installed and the project/root detection matches the file.
|
|
@@ -17,6 +17,8 @@
|
|
|
17
17
|
import * as path from "node:path";
|
|
18
18
|
import type { FileKind } from "../file-kinds.js";
|
|
19
19
|
import { detectFileKind } from "../file-kinds.js";
|
|
20
|
+
import { getPrimaryDispatchGroup } from "../language-policy.js";
|
|
21
|
+
import { resolveLanguageRootForFile } from "../language-profile.js";
|
|
20
22
|
import { isTestFile } from "../file-utils.js";
|
|
21
23
|
import { logLatency } from "../latency-logger.js";
|
|
22
24
|
import { normalizeMapKey } from "../path-utils.js";
|
|
@@ -129,7 +131,9 @@ export function createDispatchContext(
|
|
|
129
131
|
blockingOnly?: boolean,
|
|
130
132
|
modifiedRanges?: import("./types.js").ModifiedRange[],
|
|
131
133
|
): DispatchContext {
|
|
132
|
-
const normalizedCwd = normalizeMapKey(
|
|
134
|
+
const normalizedCwd = normalizeMapKey(
|
|
135
|
+
resolveLanguageRootForFile(path.resolve(cwd, filePath), cwd),
|
|
136
|
+
);
|
|
133
137
|
const normalizedFilePath = resolveRunnerPath(normalizedCwd, filePath);
|
|
134
138
|
const kind = detectFileKind(normalizedFilePath);
|
|
135
139
|
|
|
@@ -325,7 +329,46 @@ export interface DispatchLatencyReport {
|
|
|
325
329
|
warnings: number;
|
|
326
330
|
}
|
|
327
331
|
|
|
332
|
+
function buildCoverageNotice(
|
|
333
|
+
ctx: DispatchContext,
|
|
334
|
+
runnerLatencies: RunnerLatency[],
|
|
335
|
+
): Diagnostic | undefined {
|
|
336
|
+
if (!ctx.kind) return undefined;
|
|
337
|
+
const lspEnabled = !!ctx.pi.getFlag("lens-lsp") && !ctx.pi.getFlag("no-lsp");
|
|
338
|
+
const primary = getPrimaryDispatchGroup(ctx.kind, lspEnabled);
|
|
339
|
+
if (!primary || primary.runnerIds.length === 0) return undefined;
|
|
340
|
+
|
|
341
|
+
const relevant = runnerLatencies.filter((r) =>
|
|
342
|
+
primary.runnerIds.includes(r.runnerId),
|
|
343
|
+
);
|
|
344
|
+
if (relevant.length === 0) return undefined;
|
|
345
|
+
|
|
346
|
+
const hasCoverage = relevant.some(
|
|
347
|
+
(r) => r.status === "succeeded" || r.status === "failed",
|
|
348
|
+
);
|
|
349
|
+
if (hasCoverage) return undefined;
|
|
350
|
+
|
|
351
|
+
const allSkipped = relevant.every(
|
|
352
|
+
(r) => r.status === "skipped" || r.status === "when_skipped",
|
|
353
|
+
);
|
|
354
|
+
if (!allSkipped) return undefined;
|
|
355
|
+
|
|
356
|
+
const onceKey = `${ctx.kind}:${normalizeMapKey(ctx.filePath)}`;
|
|
357
|
+
if (coverageNoticeSeen.has(onceKey)) return undefined;
|
|
358
|
+
coverageNoticeSeen.add(onceKey);
|
|
359
|
+
|
|
360
|
+
return {
|
|
361
|
+
id: `coverage-unavailable:${ctx.kind}:${path.basename(ctx.filePath)}`,
|
|
362
|
+
message: `Pi-lens analysis unavailable. Tools for ${ctx.kind} not installed.`,
|
|
363
|
+
filePath: ctx.filePath,
|
|
364
|
+
severity: "warning",
|
|
365
|
+
semantic: "warning",
|
|
366
|
+
tool: "pi-lens",
|
|
367
|
+
};
|
|
368
|
+
}
|
|
369
|
+
|
|
328
370
|
const latencyReports: DispatchLatencyReport[] = [];
|
|
371
|
+
const coverageNoticeSeen = new Set<string>();
|
|
329
372
|
|
|
330
373
|
export function getLatencyReports(): DispatchLatencyReport[] {
|
|
331
374
|
return [...latencyReports];
|
|
@@ -335,6 +378,10 @@ export function clearLatencyReports(): void {
|
|
|
335
378
|
latencyReports.length = 0;
|
|
336
379
|
}
|
|
337
380
|
|
|
381
|
+
export function clearCoverageNoticeState(): void {
|
|
382
|
+
coverageNoticeSeen.clear();
|
|
383
|
+
}
|
|
384
|
+
|
|
338
385
|
export function formatLatencyReport(report: DispatchLatencyReport): string {
|
|
339
386
|
const lines: string[] = [];
|
|
340
387
|
lines.push(
|
|
@@ -606,11 +653,16 @@ export async function dispatchForFile(
|
|
|
606
653
|
const fixedItems = visibleDiagnostics.filter((d) => d.semantic === "fixed");
|
|
607
654
|
const inlineBlockers = blockers.filter((d) => d.tool !== "similarity");
|
|
608
655
|
const inlineFixed = fixedItems.filter((d) => d.tool !== "similarity");
|
|
656
|
+
const coverageNotice = buildCoverageNotice(ctx, runnerLatencies);
|
|
609
657
|
|
|
610
658
|
// Format output — only blocking issues shown inline
|
|
611
659
|
// Warnings tracked but not shown (noise) — surfaced via /lens-booboo
|
|
612
660
|
let output = formatDiagnostics(inlineBlockers, "blocking");
|
|
613
661
|
output += formatDiagnostics(inlineFixed, "fixed");
|
|
662
|
+
if (coverageNotice) {
|
|
663
|
+
output += formatDiagnostics([coverageNotice], "warning", 1);
|
|
664
|
+
warnings.push(coverageNotice);
|
|
665
|
+
}
|
|
614
666
|
|
|
615
667
|
// Generate and store latency report
|
|
616
668
|
const overallEnd = Date.now();
|
|
@@ -7,8 +7,13 @@
|
|
|
7
7
|
|
|
8
8
|
import { detectFileKind } from "../file-kinds.js";
|
|
9
9
|
import type { FileKind } from "../file-kinds.js";
|
|
10
|
+
import {
|
|
11
|
+
getLspCapableKinds,
|
|
12
|
+
getPrimaryDispatchGroup,
|
|
13
|
+
} from "../language-policy.js";
|
|
10
14
|
import {
|
|
11
15
|
clearLatencyReports,
|
|
16
|
+
clearCoverageNoticeState,
|
|
12
17
|
createBaselineStore,
|
|
13
18
|
createDispatchContext,
|
|
14
19
|
type DispatchLatencyReport,
|
|
@@ -40,35 +45,40 @@ import "./runners/index.js";
|
|
|
40
45
|
// store, so baselines.get() always returns undefined and every issue
|
|
41
46
|
// looks "new" every time.
|
|
42
47
|
const sessionBaselines: BaselineStore = createBaselineStore();
|
|
43
|
-
const LSP_CAPABLE_KINDS = new Set<FileKind>(
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
"go",
|
|
47
|
-
"rust",
|
|
48
|
-
"ruby",
|
|
49
|
-
"cxx",
|
|
50
|
-
"cmake",
|
|
51
|
-
"shell",
|
|
52
|
-
"json",
|
|
53
|
-
"markdown",
|
|
54
|
-
"css",
|
|
55
|
-
"yaml",
|
|
56
|
-
]);
|
|
57
|
-
|
|
58
|
-
function withPrimaryLspGroup(
|
|
48
|
+
const LSP_CAPABLE_KINDS = new Set<FileKind>(getLspCapableKinds());
|
|
49
|
+
|
|
50
|
+
function withPrimaryPolicyGroup(
|
|
59
51
|
kind: keyof typeof TOOL_PLANS,
|
|
60
52
|
groups: RunnerGroup[],
|
|
61
53
|
pi: PiAgentAPI,
|
|
62
54
|
): RunnerGroup[] {
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
55
|
+
const lspEnabled = !!pi.getFlag("lens-lsp") && !pi.getFlag("no-lsp");
|
|
56
|
+
const normalizedGroups = lspEnabled
|
|
57
|
+
? groups
|
|
58
|
+
: groups
|
|
59
|
+
.map((group) => {
|
|
60
|
+
const runnerIds = group.runnerIds.filter(
|
|
61
|
+
(id) => id !== "lsp" && id !== "ts-lsp",
|
|
62
|
+
);
|
|
63
|
+
if (runnerIds.length === 0) return null;
|
|
64
|
+
return {
|
|
65
|
+
...group,
|
|
66
|
+
runnerIds,
|
|
67
|
+
};
|
|
68
|
+
})
|
|
69
|
+
.filter((group): group is RunnerGroup => group !== null);
|
|
70
|
+
|
|
71
|
+
const primary = getPrimaryDispatchGroup(kind as FileKind, lspEnabled);
|
|
72
|
+
if (!primary) return normalizedGroups;
|
|
73
|
+
|
|
74
|
+
const alreadyHasPrimary = normalizedGroups.some((group) => {
|
|
75
|
+
if (group.mode !== primary.mode) return false;
|
|
76
|
+
if (group.runnerIds.length !== primary.runnerIds.length) return false;
|
|
77
|
+
return group.runnerIds.every((id, index) => primary.runnerIds[index] === id);
|
|
78
|
+
});
|
|
79
|
+
if (alreadyHasPrimary) return normalizedGroups;
|
|
80
|
+
|
|
81
|
+
return [primary, ...normalizedGroups];
|
|
72
82
|
}
|
|
73
83
|
|
|
74
84
|
export function getDispatchGroupsForKind(
|
|
@@ -86,7 +96,7 @@ export function getDispatchGroupsForKind(
|
|
|
86
96
|
}
|
|
87
97
|
return [];
|
|
88
98
|
}
|
|
89
|
-
return
|
|
99
|
+
return withPrimaryPolicyGroup(kind, plan.groups, pi);
|
|
90
100
|
}
|
|
91
101
|
|
|
92
102
|
/**
|
|
@@ -95,6 +105,7 @@ export function getDispatchGroupsForKind(
|
|
|
95
105
|
*/
|
|
96
106
|
export function resetDispatchBaselines(): void {
|
|
97
107
|
sessionBaselines.clear();
|
|
108
|
+
clearCoverageNoticeState();
|
|
98
109
|
}
|
|
99
110
|
|
|
100
111
|
/**
|
package/clients/dispatch/plan.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { FileKind } from "../file-kinds.js";
|
|
2
|
+
import { getPrimaryDispatchGroup } from "../language-policy.js";
|
|
2
3
|
import type { RunnerGroup, ToolPlan } from "./types.js";
|
|
3
4
|
|
|
4
5
|
type CapabilityDimension =
|
|
@@ -17,12 +18,20 @@ interface CapabilityMatrixEntry {
|
|
|
17
18
|
fullOnlyGroups?: RunnerGroup[];
|
|
18
19
|
}
|
|
19
20
|
|
|
21
|
+
function primary(kind: FileKind): RunnerGroup {
|
|
22
|
+
const group = getPrimaryDispatchGroup(kind, true);
|
|
23
|
+
if (!group) {
|
|
24
|
+
throw new Error(`Missing primary dispatch group for ${kind}`);
|
|
25
|
+
}
|
|
26
|
+
return group;
|
|
27
|
+
}
|
|
28
|
+
|
|
20
29
|
export const LANGUAGE_CAPABILITY_MATRIX: Record<FileKind, CapabilityMatrixEntry> = {
|
|
21
30
|
jsts: {
|
|
22
31
|
name: "JavaScript/TypeScript Linting",
|
|
23
32
|
capabilities: ["types", "security", "smells", "format", "lint", "architecture"],
|
|
24
33
|
writeGroups: [
|
|
25
|
-
|
|
34
|
+
primary("jsts"),
|
|
26
35
|
{ mode: "all", runnerIds: ["biome-check-json"], filterKinds: ["jsts"] },
|
|
27
36
|
{ mode: "all", runnerIds: ["tree-sitter"], filterKinds: ["jsts"] },
|
|
28
37
|
{ mode: "all", runnerIds: ["ast-grep-napi"], filterKinds: ["jsts"] },
|
|
@@ -39,7 +48,7 @@ export const LANGUAGE_CAPABILITY_MATRIX: Record<FileKind, CapabilityMatrixEntry>
|
|
|
39
48
|
name: "Python Linting",
|
|
40
49
|
capabilities: ["types", "lint", "architecture", "smells"],
|
|
41
50
|
writeGroups: [
|
|
42
|
-
|
|
51
|
+
primary("python"),
|
|
43
52
|
{ mode: "fallback", runnerIds: ["ruff-lint"], filterKinds: ["python"] },
|
|
44
53
|
{ mode: "fallback", runnerIds: ["architect"], filterKinds: ["python"] },
|
|
45
54
|
],
|
|
@@ -51,7 +60,7 @@ export const LANGUAGE_CAPABILITY_MATRIX: Record<FileKind, CapabilityMatrixEntry>
|
|
|
51
60
|
name: "Go Linting",
|
|
52
61
|
capabilities: ["types", "lint", "smells"],
|
|
53
62
|
writeGroups: [
|
|
54
|
-
|
|
63
|
+
primary("go"),
|
|
55
64
|
{ mode: "fallback", runnerIds: ["go-vet"], filterKinds: ["go"] },
|
|
56
65
|
{ mode: "fallback", runnerIds: ["golangci-lint"], filterKinds: ["go"] },
|
|
57
66
|
{ mode: "all", runnerIds: ["tree-sitter"], filterKinds: ["go"] },
|
|
@@ -61,7 +70,7 @@ export const LANGUAGE_CAPABILITY_MATRIX: Record<FileKind, CapabilityMatrixEntry>
|
|
|
61
70
|
name: "Rust Linting",
|
|
62
71
|
capabilities: ["types", "lint", "smells"],
|
|
63
72
|
writeGroups: [
|
|
64
|
-
|
|
73
|
+
primary("rust"),
|
|
65
74
|
{ mode: "fallback", runnerIds: ["rust-clippy"], filterKinds: ["rust"] },
|
|
66
75
|
{ mode: "all", runnerIds: ["tree-sitter"], filterKinds: ["rust"] },
|
|
67
76
|
],
|
|
@@ -70,7 +79,7 @@ export const LANGUAGE_CAPABILITY_MATRIX: Record<FileKind, CapabilityMatrixEntry>
|
|
|
70
79
|
name: "Ruby Linting",
|
|
71
80
|
capabilities: ["types", "lint", "smells"],
|
|
72
81
|
writeGroups: [
|
|
73
|
-
|
|
82
|
+
primary("ruby"),
|
|
74
83
|
{ mode: "fallback", runnerIds: ["rubocop"], filterKinds: ["ruby"] },
|
|
75
84
|
{ mode: "all", runnerIds: ["tree-sitter"], filterKinds: ["ruby"] },
|
|
76
85
|
],
|
|
@@ -78,42 +87,42 @@ export const LANGUAGE_CAPABILITY_MATRIX: Record<FileKind, CapabilityMatrixEntry>
|
|
|
78
87
|
cxx: {
|
|
79
88
|
name: "C/C++ Linting",
|
|
80
89
|
capabilities: ["types", "lint"],
|
|
81
|
-
writeGroups: [],
|
|
90
|
+
writeGroups: [primary("cxx")],
|
|
82
91
|
},
|
|
83
92
|
cmake: {
|
|
84
93
|
name: "CMake Processing",
|
|
85
94
|
capabilities: ["lint"],
|
|
86
|
-
writeGroups: [],
|
|
95
|
+
writeGroups: [primary("cmake")],
|
|
87
96
|
},
|
|
88
97
|
shell: {
|
|
89
98
|
name: "Shell Script Linting",
|
|
90
99
|
capabilities: ["lint", "security"],
|
|
91
|
-
writeGroups: [
|
|
100
|
+
writeGroups: [primary("shell")],
|
|
92
101
|
},
|
|
93
102
|
json: {
|
|
94
103
|
name: "JSON Processing",
|
|
95
104
|
capabilities: ["format"],
|
|
96
|
-
writeGroups: [],
|
|
105
|
+
writeGroups: [primary("json")],
|
|
97
106
|
},
|
|
98
107
|
markdown: {
|
|
99
108
|
name: "Markdown Processing",
|
|
100
109
|
capabilities: ["docs"],
|
|
101
|
-
writeGroups: [
|
|
110
|
+
writeGroups: [primary("markdown")],
|
|
102
111
|
},
|
|
103
112
|
css: {
|
|
104
113
|
name: "CSS Processing",
|
|
105
114
|
capabilities: ["format", "lint"],
|
|
106
|
-
writeGroups: [],
|
|
115
|
+
writeGroups: [primary("css")],
|
|
107
116
|
},
|
|
108
117
|
yaml: {
|
|
109
118
|
name: "YAML Processing",
|
|
110
119
|
capabilities: ["format", "lint"],
|
|
111
|
-
writeGroups: [
|
|
120
|
+
writeGroups: [primary("yaml")],
|
|
112
121
|
},
|
|
113
122
|
sql: {
|
|
114
123
|
name: "SQL Processing",
|
|
115
124
|
capabilities: ["format", "lint"],
|
|
116
|
-
writeGroups: [
|
|
125
|
+
writeGroups: [primary("sql")],
|
|
117
126
|
},
|
|
118
127
|
};
|
|
119
128
|
|
|
@@ -126,10 +135,11 @@ function toWritePlan(entry: CapabilityMatrixEntry): ToolPlan {
|
|
|
126
135
|
|
|
127
136
|
function toFullPlan(kind: FileKind, entry: CapabilityMatrixEntry): ToolPlan {
|
|
128
137
|
if (kind === "jsts") {
|
|
138
|
+
const primaryGroup = primary("jsts");
|
|
129
139
|
return {
|
|
130
140
|
name: "JavaScript/TypeScript Full Lint",
|
|
131
141
|
groups: [
|
|
132
|
-
|
|
142
|
+
primaryGroup,
|
|
133
143
|
{ mode: "all", runnerIds: ["tree-sitter"], filterKinds: ["jsts"] },
|
|
134
144
|
{ mode: "all", runnerIds: ["ast-grep-napi"], filterKinds: ["jsts"] },
|
|
135
145
|
...(entry.fullOnlyGroups ?? []),
|
|
@@ -142,10 +152,11 @@ function toFullPlan(kind: FileKind, entry: CapabilityMatrixEntry): ToolPlan {
|
|
|
142
152
|
}
|
|
143
153
|
|
|
144
154
|
if (kind === "python") {
|
|
155
|
+
const primaryGroup = primary("python");
|
|
145
156
|
return {
|
|
146
157
|
name: "Python Full Lint",
|
|
147
158
|
groups: [
|
|
148
|
-
|
|
159
|
+
primaryGroup,
|
|
149
160
|
{ mode: "fallback", runnerIds: ["ruff-lint"], filterKinds: ["python"] },
|
|
150
161
|
...(entry.fullOnlyGroups ?? []),
|
|
151
162
|
{ mode: "fallback", runnerIds: ["architect"], filterKinds: ["python"] },
|
|
@@ -115,7 +115,12 @@ const lspRunner: RunnerDefinition = {
|
|
|
115
115
|
}
|
|
116
116
|
|
|
117
117
|
if (lspDiags.length === 0) {
|
|
118
|
-
return {
|
|
118
|
+
return {
|
|
119
|
+
status: "succeeded",
|
|
120
|
+
diagnostics: [],
|
|
121
|
+
semantic: "none",
|
|
122
|
+
rawOutput: "no-diagnostics",
|
|
123
|
+
};
|
|
119
124
|
}
|
|
120
125
|
|
|
121
126
|
// Convert LSP diagnostics to our format
|
|
@@ -27,14 +27,12 @@ const pyrightRunner: RunnerDefinition = {
|
|
|
27
27
|
enabledByDefault: true,
|
|
28
28
|
|
|
29
29
|
async run(ctx: DispatchContext): Promise<RunnerResult> {
|
|
30
|
-
//
|
|
31
|
-
//
|
|
30
|
+
// Always allow pyright CLI fallback even when LSP is enabled.
|
|
31
|
+
// LSP can be present but still fail transiently for a file; in that case,
|
|
32
|
+
// pyright provides a resilient second signal path.
|
|
32
33
|
if (ctx.pi.getFlag("lens-lsp") && !ctx.pi.getFlag("no-lsp")) {
|
|
33
34
|
const lspService = getLSPService();
|
|
34
|
-
|
|
35
|
-
if (spawned) {
|
|
36
|
-
return { status: "skipped", diagnostics: [], semantic: "none" };
|
|
37
|
-
}
|
|
35
|
+
await lspService.getClientForFile(ctx.filePath);
|
|
38
36
|
}
|
|
39
37
|
|
|
40
38
|
const cwd = ctx.cwd || process.cwd();
|
|
@@ -9,6 +9,7 @@ import { ensureTool } from "../../installer/index.js";
|
|
|
9
9
|
import { safeSpawnAsync } from "../../safe-spawn.js";
|
|
10
10
|
import { stripAnsi } from "../../sanitize.js";
|
|
11
11
|
import type {
|
|
12
|
+
Diagnostic,
|
|
12
13
|
DispatchContext,
|
|
13
14
|
RunnerDefinition,
|
|
14
15
|
RunnerResult,
|
|
@@ -18,6 +19,39 @@ import { createAvailabilityChecker } from "./utils/runner-helpers.js";
|
|
|
18
19
|
|
|
19
20
|
const ruff = createAvailabilityChecker("ruff", ".exe");
|
|
20
21
|
|
|
22
|
+
function parseRuffJson(raw: string, filePath: string): Diagnostic[] {
|
|
23
|
+
try {
|
|
24
|
+
const parsed = JSON.parse(raw) as Array<{
|
|
25
|
+
code?: string;
|
|
26
|
+
message?: string;
|
|
27
|
+
filename?: string;
|
|
28
|
+
location?: { row?: number; column?: number };
|
|
29
|
+
severity?: string;
|
|
30
|
+
fix?: unknown;
|
|
31
|
+
}>;
|
|
32
|
+
if (!Array.isArray(parsed)) return [];
|
|
33
|
+
|
|
34
|
+
return parsed.map((item, index) => {
|
|
35
|
+
const severity = item.severity === "error" ? "error" : "warning";
|
|
36
|
+
const code = item.code || "ruff";
|
|
37
|
+
return {
|
|
38
|
+
id: `ruff-${code}-${item.location?.row ?? index + 1}`,
|
|
39
|
+
message: item.message || code,
|
|
40
|
+
filePath: item.filename || filePath,
|
|
41
|
+
line: item.location?.row ?? 1,
|
|
42
|
+
column: item.location?.column ?? 1,
|
|
43
|
+
severity,
|
|
44
|
+
semantic: severity === "error" ? "blocking" : "warning",
|
|
45
|
+
tool: "ruff",
|
|
46
|
+
rule: code,
|
|
47
|
+
fixable: Boolean(item.fix),
|
|
48
|
+
};
|
|
49
|
+
});
|
|
50
|
+
} catch {
|
|
51
|
+
return [];
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
21
55
|
const ruffRunner: RunnerDefinition = {
|
|
22
56
|
id: "ruff-lint",
|
|
23
57
|
appliesTo: ["python"],
|
|
@@ -47,25 +81,36 @@ const ruffRunner: RunnerDefinition = {
|
|
|
47
81
|
// not silent correction. Auto-fix (ruff --fix) already runs in the
|
|
48
82
|
// format phase before dispatch, handling all safe style transforms.
|
|
49
83
|
// Silently rewriting here would leave the agent's context window stale.
|
|
50
|
-
const args = ["check", ctx.filePath];
|
|
84
|
+
const args = ["check", "--output-format", "json", ctx.filePath];
|
|
51
85
|
|
|
52
86
|
const result = await safeSpawnAsync(cmd, args, {
|
|
53
87
|
timeout: 30000,
|
|
54
88
|
});
|
|
55
89
|
|
|
56
90
|
const raw = stripAnsi(result.stdout + result.stderr);
|
|
91
|
+
const diagnostics = parseRuffJson(result.stdout || "", ctx.filePath);
|
|
92
|
+
const parsedDiagnostics =
|
|
93
|
+
diagnostics.length > 0 ? diagnostics : parseRuffOutput(raw, ctx.filePath);
|
|
57
94
|
|
|
58
|
-
if (result.status === 0) {
|
|
95
|
+
if (result.status === 0 && parsedDiagnostics.length === 0) {
|
|
59
96
|
return { status: "succeeded", diagnostics: [], semantic: "none" };
|
|
60
97
|
}
|
|
61
98
|
|
|
62
|
-
|
|
63
|
-
|
|
99
|
+
if (parsedDiagnostics.length === 0) {
|
|
100
|
+
return {
|
|
101
|
+
status: "failed",
|
|
102
|
+
diagnostics: [],
|
|
103
|
+
semantic: "warning",
|
|
104
|
+
rawOutput: raw.slice(0, 500),
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const hasErrors = parsedDiagnostics.some((d) => d.severity === "error");
|
|
64
109
|
|
|
65
110
|
return {
|
|
66
|
-
status: "failed",
|
|
67
|
-
diagnostics,
|
|
68
|
-
semantic: "warning",
|
|
111
|
+
status: hasErrors ? "failed" : "succeeded",
|
|
112
|
+
diagnostics: parsedDiagnostics,
|
|
113
|
+
semantic: hasErrors ? "blocking" : "warning",
|
|
69
114
|
};
|
|
70
115
|
},
|
|
71
116
|
};
|
|
@@ -96,8 +96,9 @@ const sqlfluffRunner: RunnerDefinition = {
|
|
|
96
96
|
|
|
97
97
|
async run(ctx: DispatchContext): Promise<RunnerResult> {
|
|
98
98
|
const cwd = ctx.cwd || process.cwd();
|
|
99
|
-
|
|
100
|
-
|
|
99
|
+
const hasConfig = hasSqlfluffConfig(cwd);
|
|
100
|
+
if (!hasConfig) {
|
|
101
|
+
ctx.log("sqlfluff: no config detected, using ANSI dialect defaults");
|
|
101
102
|
}
|
|
102
103
|
|
|
103
104
|
let cmd: string | null = null;
|
|
@@ -113,7 +114,12 @@ const sqlfluffRunner: RunnerDefinition = {
|
|
|
113
114
|
|
|
114
115
|
if (!cmd) return { status: "skipped", diagnostics: [], semantic: "none" };
|
|
115
116
|
|
|
116
|
-
const
|
|
117
|
+
const args = ["lint", "--format", "json", ctx.filePath];
|
|
118
|
+
if (!hasConfig) {
|
|
119
|
+
args.splice(2, 0, "--dialect", "ansi");
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const result = safeSpawn(cmd, args, {
|
|
117
123
|
timeout: 20000,
|
|
118
124
|
});
|
|
119
125
|
|
|
@@ -89,8 +89,9 @@ const yamllintRunner: RunnerDefinition = {
|
|
|
89
89
|
|
|
90
90
|
async run(ctx: DispatchContext): Promise<RunnerResult> {
|
|
91
91
|
const cwd = ctx.cwd || process.cwd();
|
|
92
|
-
|
|
93
|
-
|
|
92
|
+
const hasConfig = hasYamllintConfig(cwd);
|
|
93
|
+
if (!hasConfig) {
|
|
94
|
+
ctx.log("yamllint: no config detected, running with default rules");
|
|
94
95
|
}
|
|
95
96
|
|
|
96
97
|
let cmd: string | null = null;
|
package/clients/file-utils.ts
CHANGED
|
@@ -23,8 +23,19 @@ export const EXCLUDED_DIRS = [
|
|
|
23
23
|
".gradle",
|
|
24
24
|
".next",
|
|
25
25
|
".pi-lens",
|
|
26
|
-
".pi",
|
|
27
|
-
".ruff_cache",
|
|
26
|
+
".pi", // pi agent directory
|
|
27
|
+
".ruff_cache", // Python linter cache
|
|
28
|
+
".worktrees",
|
|
29
|
+
".claude",
|
|
30
|
+
".codex",
|
|
31
|
+
".rescue",
|
|
32
|
+
".agents",
|
|
33
|
+
".gstack",
|
|
34
|
+
".superpowers",
|
|
35
|
+
".guardrails",
|
|
36
|
+
".playwright-cli",
|
|
37
|
+
".playwright-mcp",
|
|
38
|
+
".vscode",
|
|
28
39
|
"venv",
|
|
29
40
|
".venv",
|
|
30
41
|
"coverage",
|
package/clients/formatters.ts
CHANGED
|
@@ -346,6 +346,9 @@ export const ruffFormatter: FormatterInfo = {
|
|
|
346
346
|
async resolveCommand(filePath, cwd) {
|
|
347
347
|
const venv = await findInVenv("ruff", cwd);
|
|
348
348
|
if (venv) return [venv, "format", filePath];
|
|
349
|
+
const { getToolPath } = await import("./installer/index.js");
|
|
350
|
+
const installed = await getToolPath("ruff");
|
|
351
|
+
if (installed) return [installed, "format", filePath];
|
|
349
352
|
return null;
|
|
350
353
|
},
|
|
351
354
|
async detect(cwd: string) {
|
|
@@ -372,10 +375,11 @@ export const ruffFormatter: FormatterInfo = {
|
|
|
372
375
|
}
|
|
373
376
|
}
|
|
374
377
|
|
|
375
|
-
//
|
|
376
|
-
//
|
|
377
|
-
|
|
378
|
-
|
|
378
|
+
// No-config fallback: if Ruff is already available, allow formatter usage.
|
|
379
|
+
// This keeps Python default behavior consistent with startup defaults.
|
|
380
|
+
const { getToolPath } = await import("./installer/index.js");
|
|
381
|
+
const installed = await getToolPath("ruff");
|
|
382
|
+
return Boolean(installed);
|
|
379
383
|
},
|
|
380
384
|
};
|
|
381
385
|
|