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 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
- - warms caches and optional indexes
26
- - preps LSP/tool installers when needed
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 | Flow/language-gated (Python file paths/runners) |
98
- | `typescript-language-server` | Unified LSP diagnostics | Yes | Flow-gated (LSP enabled; default on unless `--no-lsp`) |
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(path.resolve(cwd));
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
- "jsts",
45
- "python",
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
- if (!pi.getFlag("lens-lsp") || !!pi.getFlag("no-lsp")) return groups;
64
-
65
- const alreadyHasLsp = groups.some((g) => g.runnerIds.includes("lsp"));
66
- if (alreadyHasLsp) return groups;
67
-
68
- return [
69
- { mode: "all", runnerIds: ["lsp"], filterKinds: [kind as FileKind] },
70
- ...groups,
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 withPrimaryLspGroup(kind, plan.groups, pi);
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
  /**
@@ -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
- { mode: "fallback", runnerIds: ["lsp", "ts-lsp"], filterKinds: ["jsts"] },
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
- { mode: "fallback", runnerIds: ["lsp", "pyright"], filterKinds: ["python"] },
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
- { mode: "all", runnerIds: ["lsp"], filterKinds: ["go"] },
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
- { mode: "all", runnerIds: ["lsp"], filterKinds: ["rust"] },
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
- { mode: "all", runnerIds: ["lsp"], filterKinds: ["ruby"] },
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: [{ mode: "fallback", runnerIds: ["shellcheck"] }],
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: [{ mode: "fallback", runnerIds: ["spellcheck"] }],
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: [{ mode: "fallback", runnerIds: ["yamllint"], filterKinds: ["yaml"] }],
120
+ writeGroups: [primary("yaml")],
112
121
  },
113
122
  sql: {
114
123
  name: "SQL Processing",
115
124
  capabilities: ["format", "lint"],
116
- writeGroups: [{ mode: "fallback", runnerIds: ["sqlfluff"], filterKinds: ["sql"] }],
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
- { mode: "fallback", runnerIds: ["lsp", "ts-lsp"], filterKinds: ["jsts"] },
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
- { mode: "fallback", runnerIds: ["lsp", "pyright"], filterKinds: ["python"] },
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 { status: "skipped", diagnostics: [], semantic: "none" };
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
- // When --lens-lsp is active, prefer the unified lsp runner.
31
- // But if LSP is unavailable for this file, keep pyright CLI fallback.
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
- const spawned = await lspService.getClientForFile(ctx.filePath);
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
- // Parse diagnostics
63
- const diagnostics = parseRuffOutput(raw, ctx.filePath);
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
- if (!hasSqlfluffConfig(cwd)) {
100
- return { status: "skipped", diagnostics: [], semantic: "none" };
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 result = safeSpawn(cmd, ["lint", "--format", "json", ctx.filePath], {
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
- if (!hasYamllintConfig(cwd)) {
93
- return { status: "skipped", diagnostics: [], semantic: "none" };
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;
@@ -23,8 +23,19 @@ export const EXCLUDED_DIRS = [
23
23
  ".gradle",
24
24
  ".next",
25
25
  ".pi-lens",
26
- ".pi", // pi agent directory
27
- ".ruff_cache", // Python linter 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",
@@ -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
- // Only enable if the project explicitly uses ruff (config or deps).
376
- // Do NOT fall back to "ruff binary is installed" — that would format
377
- // projects that never asked for ruff.
378
- return false;
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