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.
@@ -18,24 +18,30 @@ import type { BiomeClient } from "./biome-client.js";
18
18
  import { getDiagnosticLogger } from "./diagnostic-logger.js";
19
19
  import { getDiagnosticTracker } from "./diagnostic-tracker.js";
20
20
  import { dispatchLintWithResult } from "./dispatch/integration.js";
21
+ import {
22
+ resolveRunnerPath,
23
+ toRunnerDisplayPath,
24
+ } from "./dispatch/runner-context.js";
21
25
  import type { PiAgentAPI } from "./dispatch/types.js";
22
26
  import { detectFileKind, getFileKindLabel } from "./file-kinds.js";
23
27
  import type { FormatService } from "./format-service.js";
24
28
  import { logLatency } from "./latency-logger.js";
25
29
  import { getLSPService } from "./lsp/index.js";
26
30
  import type { MetricsClient } from "./metrics-client.js";
31
+ import { normalizeMapKey } from "./path-utils.js";
27
32
  import type { RuffClient } from "./ruff-client.js";
28
33
  import { RUNTIME_CONFIG } from "./runtime-config.js";
29
34
  import { safeSpawnAsync } from "./safe-spawn.js";
30
35
  import { formatSecrets, scanForSecrets } from "./secrets-scanner.js";
31
36
  import type { TestRunnerClient } from "./test-runner-client.js";
32
- import { normalizeMapKey } from "./path-utils.js";
33
- import { resolveRunnerPath, toRunnerDisplayPath } from "./dispatch/runner-context.js";
34
37
 
35
38
  const LSP_MAX_FILE_BYTES = RUNTIME_CONFIG.pipeline.lspMaxFileBytes;
36
39
  const LSP_MAX_FILE_LINES = RUNTIME_CONFIG.pipeline.lspMaxFileLines;
37
40
 
38
- function exceedsLspSyncLimits(filePath: string, content: string): {
41
+ function exceedsLspSyncLimits(
42
+ filePath: string,
43
+ content: string,
44
+ ): {
39
45
  tooLarge: boolean;
40
46
  reason: string;
41
47
  } {
@@ -140,6 +146,8 @@ function createPhaseTracker(toolName: string, filePath: string): PhaseTracker {
140
146
 
141
147
  // --- ESLint autofix helpers ---
142
148
 
149
+ const BIOME_CONFIGS = ["biome.json", "biome.jsonc"];
150
+
143
151
  const ESLINT_CONFIGS = [
144
152
  ".eslintrc",
145
153
  ".eslintrc.js",
@@ -158,6 +166,19 @@ function isJsTs(filePath: string): boolean {
158
166
  return JSTS_EXTS.has(path.extname(filePath).toLowerCase());
159
167
  }
160
168
 
169
+ function hasBiomeConfig(cwd: string): boolean {
170
+ for (const cfg of BIOME_CONFIGS) {
171
+ if (nodeFs.existsSync(path.join(cwd, cfg))) return true;
172
+ }
173
+ try {
174
+ const pkg = JSON.parse(
175
+ nodeFs.readFileSync(path.join(cwd, "package.json"), "utf-8"),
176
+ );
177
+ if (pkg.devDependencies?.["@biomejs/biome"]) return true;
178
+ } catch {}
179
+ return false;
180
+ }
181
+
161
182
  function hasEslintConfig(cwd: string): boolean {
162
183
  for (const cfg of ESLINT_CONFIGS) {
163
184
  if (nodeFs.existsSync(path.join(cwd, cfg))) return true;
@@ -171,7 +192,10 @@ function hasEslintConfig(cwd: string): boolean {
171
192
  return false;
172
193
  }
173
194
 
174
- const _eslintCache = new Map<string, { available: boolean; bin: string | null }>();
195
+ const _eslintCache = new Map<
196
+ string,
197
+ { available: boolean; bin: string | null }
198
+ >();
175
199
 
176
200
  function findEslintBin(cwd: string): string {
177
201
  const isWin = process.platform === "win32";
@@ -331,7 +355,7 @@ export async function runPipeline(
331
355
  const deferLspSync =
332
356
  !getFlag("no-autofix") &&
333
357
  (ruffClient.isPythonFile(filePath) ||
334
- biomeClient.isSupportedFile(filePath) ||
358
+ (biomeClient.isSupportedFile(filePath) && hasBiomeConfig(cwd)) ||
335
359
  isJsTs(filePath));
336
360
 
337
361
  if (deferLspSync) {
@@ -386,7 +410,9 @@ export async function runPipeline(
386
410
  !noAutofixRuff && ruffClient.isPythonFile(filePath)
387
411
  ? ruffClient.ensureAvailable()
388
412
  : Promise.resolve(false),
389
- !noAutofixBiome && biomeClient.isSupportedFile(filePath)
413
+ !noAutofixBiome &&
414
+ biomeClient.isSupportedFile(filePath) &&
415
+ hasBiomeConfig(cwd)
390
416
  ? biomeClient.ensureAvailable()
391
417
  : Promise.resolve(false),
392
418
  ]);
@@ -494,12 +520,16 @@ export async function runPipeline(
494
520
  );
495
521
  for (const d of dispatchResult.diagnostics) {
496
522
  const shownInline = inlineKeys.has(toKey(d));
497
- logger.logCaught(d, {
498
- model: ctx.telemetry?.model ?? "unknown",
499
- sessionId: ctx.telemetry?.sessionId ?? "unknown",
500
- turnIndex: ctx.telemetry?.turnIndex ?? 0,
501
- writeIndex: ctx.telemetry?.writeIndex ?? 0,
502
- }, shownInline);
523
+ logger.logCaught(
524
+ d,
525
+ {
526
+ model: ctx.telemetry?.model ?? "unknown",
527
+ sessionId: ctx.telemetry?.sessionId ?? "unknown",
528
+ turnIndex: ctx.telemetry?.turnIndex ?? 0,
529
+ writeIndex: ctx.telemetry?.writeIndex ?? 0,
530
+ },
531
+ shownInline,
532
+ );
503
533
  }
504
534
  }
505
535
 
@@ -552,34 +582,34 @@ export async function runPipeline(
552
582
  target.runner,
553
583
  target.config,
554
584
  );
555
- const testDuration = Date.now() - testStart;
556
- logLatency({
557
- type: "phase",
558
- toolName,
559
- filePath,
560
- phase: "test_runner",
561
- durationMs: testDuration,
562
- metadata: {
563
- testFile: target.testFile,
564
- runner: target.runner,
565
- strategy: target.strategy,
566
- success: !testResult?.error,
567
- },
568
- });
569
- if (testResult && !testResult.error) {
570
- testSummary = {
571
- passed: testResult.passed,
572
- total: testResult.passed + testResult.failed + testResult.skipped,
573
- failed: testResult.failed,
574
- };
575
- if (testSummary.failed > 0) {
576
- hasBlockers = true;
577
- }
578
- const testOutput = testRunnerClient.formatResult(testResult);
579
- if (testOutput) {
580
- output += `\n\n${testOutput}`;
581
- }
585
+ const testDuration = Date.now() - testStart;
586
+ logLatency({
587
+ type: "phase",
588
+ toolName,
589
+ filePath,
590
+ phase: "test_runner",
591
+ durationMs: testDuration,
592
+ metadata: {
593
+ testFile: target.testFile,
594
+ runner: target.runner,
595
+ strategy: target.strategy,
596
+ success: !testResult?.error,
597
+ },
598
+ });
599
+ if (testResult && !testResult.error) {
600
+ testSummary = {
601
+ passed: testResult.passed,
602
+ total: testResult.passed + testResult.failed + testResult.skipped,
603
+ failed: testResult.failed,
604
+ };
605
+ if (testSummary.failed > 0) {
606
+ hasBlockers = true;
607
+ }
608
+ const testOutput = testRunnerClient.formatResult(testResult);
609
+ if (testOutput) {
610
+ output += `\n\n${testOutput}`;
582
611
  }
612
+ }
583
613
  }
584
614
  }
585
615
  phase.end("test_runner", { found: testInfoFound, ran: testRunnerRan });
@@ -608,7 +638,8 @@ export async function runPipeline(
608
638
 
609
639
  for (const [diagPath, diags] of allDiags) {
610
640
  const normalizedDiagPath = resolveRunnerPath(cwd, diagPath);
611
- if (normalizeMapKey(normalizedDiagPath) === normalizedEditedPath) continue;
641
+ if (normalizeMapKey(normalizedDiagPath) === normalizedEditedPath)
642
+ continue;
612
643
 
613
644
  if (!nodeFs.existsSync(normalizedDiagPath)) {
614
645
  stalePathsSkipped++;
@@ -25,3 +25,29 @@ export function consumeTurnEndFindings(
25
25
  ],
26
26
  };
27
27
  }
28
+
29
+ export function consumeSessionStartGuidance(
30
+ cacheManager: CacheManager,
31
+ cwd: string,
32
+ ): { messages: Array<{ role: "system"; content: string }> } | undefined {
33
+ const guidance = cacheManager.readCache<{ content: string }>(
34
+ "session-start-guidance",
35
+ cwd,
36
+ );
37
+ if (!guidance?.data?.content) return;
38
+
39
+ cacheManager.writeCache(
40
+ "session-start-guidance",
41
+ null as unknown as { content: string },
42
+ cwd,
43
+ );
44
+
45
+ return {
46
+ messages: [
47
+ {
48
+ role: "system",
49
+ content: `[pi-lens] Session guidance:\n\n${guidance.data.content}`,
50
+ },
51
+ ],
52
+ };
53
+ }
@@ -3,6 +3,7 @@ import type { FileComplexity } from "./complexity-client.js";
3
3
  import type { RuleScanResult } from "./rules-scanner.js";
4
4
  import { RUNTIME_CONFIG } from "./runtime-config.js";
5
5
  import type { ProjectIndex } from "./project-index.js";
6
+ import { normalizeMapKey } from "./path-utils.js";
6
7
 
7
8
  export interface ErrorDebtBaseline {
8
9
  testsPassed: boolean;
@@ -10,11 +11,13 @@ export interface ErrorDebtBaseline {
10
11
  }
11
12
 
12
13
  export class RuntimeCoordinator {
13
- private _projectRoot = process.cwd();
14
+ private _projectRoot = normalizeMapKey(process.cwd());
15
+ private _sessionGeneration = 0;
14
16
  private _errorDebtBaseline: ErrorDebtBaseline | null = null;
15
17
  private _pipelineCrashCounts = new Map<string, number>();
16
18
  private _cachedExports = new Map<string, string>();
17
19
  private _cachedProjectIndex: ProjectIndex | null = null;
20
+ private _startupScansInFlight = new Map<string, number>();
18
21
  private _lastCascadeOutput = "";
19
22
  private _complexityBaselines = new Map<string, FileComplexity>();
20
23
  private _fixedThisTurn = new Set<string>();
@@ -30,10 +33,12 @@ export class RuntimeCoordinator {
30
33
  private _gitGuardSummary = "";
31
34
 
32
35
  resetForSession(): void {
36
+ this._sessionGeneration += 1;
33
37
  this._complexityBaselines.clear();
34
38
  this._pipelineCrashCounts.clear();
35
39
  this._cachedExports.clear();
36
40
  this._cachedProjectIndex = null;
41
+ this._startupScansInFlight.clear();
37
42
  this._lastCascadeOutput = "";
38
43
  this._fixedThisTurn.clear();
39
44
  this._telemetrySessionId =
@@ -111,6 +116,29 @@ export class RuntimeCoordinator {
111
116
  return this._turnIndex;
112
117
  }
113
118
 
119
+ get sessionGeneration(): number {
120
+ return this._sessionGeneration;
121
+ }
122
+
123
+ isCurrentSession(generation: number): boolean {
124
+ return this._sessionGeneration === generation;
125
+ }
126
+
127
+ markStartupScanInFlight(name: string, generation: number): void {
128
+ this._startupScansInFlight.set(name, generation);
129
+ }
130
+
131
+ clearStartupScanInFlight(name: string, generation: number): void {
132
+ const owner = this._startupScansInFlight.get(name);
133
+ if (owner === generation) {
134
+ this._startupScansInFlight.delete(name);
135
+ }
136
+ }
137
+
138
+ isStartupScanInFlight(name: string): boolean {
139
+ return this._startupScansInFlight.has(name);
140
+ }
141
+
114
142
  formatPipelineCrashNotice(filePath: string, err: unknown): string {
115
143
  const key = path.resolve(filePath);
116
144
  const count = (this._pipelineCrashCounts.get(key) ?? 0) + 1;
@@ -140,7 +168,7 @@ export class RuntimeCoordinator {
140
168
  }
141
169
 
142
170
  set projectRoot(value: string) {
143
- this._projectRoot = value;
171
+ this._projectRoot = normalizeMapKey(value);
144
172
  }
145
173
 
146
174
  get errorDebtBaseline(): ErrorDebtBaseline | null {