pi-lens 3.8.18 → 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 +31 -0
- package/README.md +26 -17
- 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 +48 -1
- package/clients/dispatch/runners/yamllint.ts +50 -0
- 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/pipeline.ts +71 -40
- 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/clients/type-coverage-client.ts +1 -1
- package/commands/booboo.ts +3 -1
- package/index.ts +15 -3
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,37 @@
|
|
|
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
|
+
|
|
31
|
+
## [3.8.19] - 2026-04-07
|
|
32
|
+
|
|
33
|
+
### Fixed
|
|
34
|
+
- **Biome autofix gating** — Biome autofix/auto-install now runs only when the project has Biome configuration (`biome.json`/`biome.jsonc`) or `@biomejs/biome` in `devDependencies`, preventing unwanted Biome installs in non-Biome JS/TS projects.
|
|
35
|
+
|
|
5
36
|
## [3.8.18] - 2026-04-07
|
|
6
37
|
|
|
7
38
|
### Changed
|
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
|
|
|
@@ -82,19 +85,25 @@ Some runners are language/config-gated and may skip when not applicable.
|
|
|
82
85
|
|
|
83
86
|
## Dependencies
|
|
84
87
|
|
|
85
|
-
Auto-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
|
92
|
-
|
|
93
|
-
| `@biomejs/biome` | JS/TS lint/format/autofix | Yes |
|
|
94
|
-
| `
|
|
95
|
-
| `
|
|
96
|
-
|
|
|
97
|
-
| `
|
|
88
|
+
Auto-install behavior depends on gate type:
|
|
89
|
+
|
|
90
|
+
- **Config-gated**: installs only when project config/deps indicate usage.
|
|
91
|
+
- **Flow/language-gated**: installs when the runtime path needs it for the current file/session flow.
|
|
92
|
+
- **Operational prewarm**: installs during session warm scans / turn-end analysis paths.
|
|
93
|
+
|
|
94
|
+
| Tool | Purpose | Auto-installed | Gate |
|
|
95
|
+
|---|---|---|---|
|
|
96
|
+
| `@biomejs/biome` | JS/TS lint/format/autofix | Yes | Config-gated (`biome.json`/`biome.jsonc` or `@biomejs/biome` dep) |
|
|
97
|
+
| `prettier` | Formatting fallback | Yes | Config-gated (Prettier dep or `package.json#prettier`) |
|
|
98
|
+
| `yamllint` | YAML linting | Yes | Config-gated (`.yamllint*` / tool section / dep hint) |
|
|
99
|
+
| `sqlfluff` | SQL linting/formatting | Yes | Config-gated (`.sqlfluff` / tool section / dep hint) |
|
|
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) |
|
|
102
|
+
| `pyright` | Python type diagnostics fallback | Yes | Flow/language-gated (Python fallback paths) |
|
|
103
|
+
| `@ast-grep/cli` (`sg`) | AST scans/search/replace | Yes | Operational prewarm + analysis 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) |
|
|
106
|
+
| `madge` | Circular dependency analysis | Yes | Turn-end analysis flow |
|
|
98
107
|
|
|
99
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.
|
|
100
109
|
|
|
@@ -105,5 +114,5 @@ Optional safety switch:
|
|
|
105
114
|
|
|
106
115
|
## Notes
|
|
107
116
|
|
|
108
|
-
-
|
|
117
|
+
- Not every auto-install runs in every project: gate type decides when install is attempted.
|
|
109
118
|
- Rule packs are customizable via project-level rule directories.
|
|
@@ -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
|
};
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import * as nodeFs from "node:fs";
|
|
2
|
+
import * as path from "node:path";
|
|
1
3
|
import { ensureTool } from "../../installer/index.js";
|
|
2
4
|
import { safeSpawn } from "../../safe-spawn.js";
|
|
3
5
|
import { createAvailabilityChecker } from "./utils/runner-helpers.js";
|
|
@@ -10,6 +12,41 @@ import type {
|
|
|
10
12
|
|
|
11
13
|
const sqlfluff = createAvailabilityChecker("sqlfluff", ".exe");
|
|
12
14
|
|
|
15
|
+
const SQLFLUFF_CONFIGS = [".sqlfluff", "pyproject.toml", "setup.cfg", "tox.ini"];
|
|
16
|
+
|
|
17
|
+
export function hasSqlfluffConfig(cwd: string): boolean {
|
|
18
|
+
for (const cfg of SQLFLUFF_CONFIGS) {
|
|
19
|
+
const cfgPath = path.join(cwd, cfg);
|
|
20
|
+
if (!nodeFs.existsSync(cfgPath)) continue;
|
|
21
|
+
if (cfg === "pyproject.toml") {
|
|
22
|
+
try {
|
|
23
|
+
const content = nodeFs.readFileSync(cfgPath, "utf-8");
|
|
24
|
+
if (content.includes("[tool.sqlfluff]")) return true;
|
|
25
|
+
} catch {}
|
|
26
|
+
continue;
|
|
27
|
+
}
|
|
28
|
+
if (cfg === "setup.cfg" || cfg === "tox.ini") {
|
|
29
|
+
try {
|
|
30
|
+
const content = nodeFs.readFileSync(cfgPath, "utf-8");
|
|
31
|
+
if (content.includes("[sqlfluff]")) return true;
|
|
32
|
+
} catch {}
|
|
33
|
+
continue;
|
|
34
|
+
}
|
|
35
|
+
return true;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
for (const depFile of ["requirements.txt", "Pipfile", "pyproject.toml"]) {
|
|
39
|
+
const depPath = path.join(cwd, depFile);
|
|
40
|
+
if (!nodeFs.existsSync(depPath)) continue;
|
|
41
|
+
try {
|
|
42
|
+
const content = nodeFs.readFileSync(depPath, "utf-8").toLowerCase();
|
|
43
|
+
if (content.includes("sqlfluff")) return true;
|
|
44
|
+
} catch {}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
|
|
13
50
|
type SqlfluffJson = Array<{
|
|
14
51
|
filepath?: string;
|
|
15
52
|
violations?: Array<{
|
|
@@ -59,6 +96,11 @@ const sqlfluffRunner: RunnerDefinition = {
|
|
|
59
96
|
|
|
60
97
|
async run(ctx: DispatchContext): Promise<RunnerResult> {
|
|
61
98
|
const cwd = ctx.cwd || process.cwd();
|
|
99
|
+
const hasConfig = hasSqlfluffConfig(cwd);
|
|
100
|
+
if (!hasConfig) {
|
|
101
|
+
ctx.log("sqlfluff: no config detected, using ANSI dialect defaults");
|
|
102
|
+
}
|
|
103
|
+
|
|
62
104
|
let cmd: string | null = null;
|
|
63
105
|
if (sqlfluff.isAvailable(cwd)) {
|
|
64
106
|
cmd = sqlfluff.getCommand(cwd);
|
|
@@ -72,7 +114,12 @@ const sqlfluffRunner: RunnerDefinition = {
|
|
|
72
114
|
|
|
73
115
|
if (!cmd) return { status: "skipped", diagnostics: [], semantic: "none" };
|
|
74
116
|
|
|
75
|
-
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, {
|
|
76
123
|
timeout: 20000,
|
|
77
124
|
});
|
|
78
125
|
|