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
|
@@ -6,9 +6,17 @@ import type { BiomeClient } from "./biome-client.js";
|
|
|
6
6
|
import type { CacheManager } from "./cache-manager.js";
|
|
7
7
|
import type { DependencyChecker } from "./dependency-checker.js";
|
|
8
8
|
import { getDiagnosticTracker } from "./diagnostic-tracker.js";
|
|
9
|
+
import { getKnipIgnorePatterns } from "./file-utils.js";
|
|
9
10
|
import type { GoClient } from "./go-client.js";
|
|
10
11
|
import type { JscpdClient } from "./jscpd-client.js";
|
|
11
12
|
import type { KnipClient } from "./knip-client.js";
|
|
13
|
+
import {
|
|
14
|
+
detectProjectLanguageProfile,
|
|
15
|
+
getDefaultStartupTools,
|
|
16
|
+
hasLanguage,
|
|
17
|
+
isLanguageConfigured,
|
|
18
|
+
} from "./language-profile.js";
|
|
19
|
+
import { canRunStartupHeavyScans } from "./language-policy.js";
|
|
12
20
|
import type { MetricsClient } from "./metrics-client.js";
|
|
13
21
|
import {
|
|
14
22
|
buildProjectIndex,
|
|
@@ -16,11 +24,11 @@ import {
|
|
|
16
24
|
loadIndex,
|
|
17
25
|
saveIndex,
|
|
18
26
|
} from "./project-index.js";
|
|
19
|
-
import { scanProjectRules } from "./rules-scanner.js";
|
|
20
27
|
import type { RuffClient } from "./ruff-client.js";
|
|
28
|
+
import { scanProjectRules } from "./rules-scanner.js";
|
|
21
29
|
import type { RuntimeCoordinator } from "./runtime-coordinator.js";
|
|
22
30
|
import type { RustClient } from "./rust-client.js";
|
|
23
|
-
import {
|
|
31
|
+
import { safeSpawn } from "./safe-spawn.js";
|
|
24
32
|
import { getSourceFiles } from "./scan-utils.js";
|
|
25
33
|
import { resolveStartupScanContext } from "./startup-scan.js";
|
|
26
34
|
import type { TestRunnerClient } from "./test-runner-client.js";
|
|
@@ -54,7 +62,43 @@ interface SessionStartDeps {
|
|
|
54
62
|
resetLSPService: () => void;
|
|
55
63
|
}
|
|
56
64
|
|
|
57
|
-
|
|
65
|
+
function isCommandAvailable(command: string, args: string[] = ["--version"]): boolean {
|
|
66
|
+
const result = safeSpawn(command, args, { timeout: 5000 });
|
|
67
|
+
return !result.error && result.status === 0;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function getLanguageInstallHints(
|
|
71
|
+
languageProfile: ReturnType<typeof detectProjectLanguageProfile>,
|
|
72
|
+
): string[] {
|
|
73
|
+
const hints: string[] = [];
|
|
74
|
+
const hasStrongSignal = (
|
|
75
|
+
kind: "go" | "rust" | "ruby",
|
|
76
|
+
minCount = 3,
|
|
77
|
+
): boolean => {
|
|
78
|
+
if (!hasLanguage(languageProfile, kind)) return false;
|
|
79
|
+
if (isLanguageConfigured(languageProfile, kind)) return true;
|
|
80
|
+
return (languageProfile.counts[kind] ?? 0) >= minCount;
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
if (hasStrongSignal("go") && !isCommandAvailable("gopls")) {
|
|
84
|
+
hints.push("Go detected: install gopls (`go install golang.org/x/tools/gopls@latest`).");
|
|
85
|
+
}
|
|
86
|
+
if (hasStrongSignal("rust") && !isCommandAvailable("rust-analyzer")) {
|
|
87
|
+
hints.push(
|
|
88
|
+
"Rust detected: install rust-analyzer (`rustup component add rust-analyzer`).",
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
if (hasStrongSignal("ruby") && !isCommandAvailable("ruby-lsp")) {
|
|
92
|
+
hints.push("Ruby detected: install ruby-lsp (`gem install ruby-lsp`).");
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return hints;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export async function handleSessionStart(
|
|
99
|
+
deps: SessionStartDeps,
|
|
100
|
+
): Promise<void> {
|
|
101
|
+
const sessionStartMs = Date.now();
|
|
58
102
|
const {
|
|
59
103
|
ctxCwd,
|
|
60
104
|
getFlag,
|
|
@@ -100,6 +144,13 @@ export async function handleSessionStart(deps: SessionStartDeps): Promise<void>
|
|
|
100
144
|
delete process.env.PI_LENS_AUTO_INSTALL;
|
|
101
145
|
}
|
|
102
146
|
|
|
147
|
+
if (getFlag("no-lsp-install")) {
|
|
148
|
+
process.env.PI_LENS_DISABLE_LSP_INSTALL = "1";
|
|
149
|
+
dbg("session_start: LSP install disabled (PI_LENS_DISABLE_LSP_INSTALL=1)");
|
|
150
|
+
} else {
|
|
151
|
+
delete process.env.PI_LENS_DISABLE_LSP_INSTALL;
|
|
152
|
+
}
|
|
153
|
+
|
|
103
154
|
const tools: string[] = [];
|
|
104
155
|
if (getFlag("lens-lsp") && !getFlag("no-lsp")) {
|
|
105
156
|
tools.push("LSP Service");
|
|
@@ -126,34 +177,76 @@ export async function handleSessionStart(deps: SessionStartDeps): Promise<void>
|
|
|
126
177
|
}
|
|
127
178
|
}
|
|
128
179
|
|
|
129
|
-
|
|
130
|
-
dbg("session_start: pre-installing TypeScript LSP...");
|
|
131
|
-
ensureTool("typescript-language-server")
|
|
132
|
-
.then((toolPath) => {
|
|
133
|
-
if (toolPath) {
|
|
134
|
-
dbg(`session_start: TypeScript LSP ready at ${toolPath}`);
|
|
135
|
-
} else {
|
|
136
|
-
console.error("[lens] TypeScript LSP installation failed");
|
|
137
|
-
}
|
|
138
|
-
})
|
|
139
|
-
.catch((err) => {
|
|
140
|
-
console.error("[lens] TypeScript LSP pre-install error:", err);
|
|
141
|
-
});
|
|
142
|
-
}
|
|
143
|
-
|
|
180
|
+
const hasWorkspaceCwd = typeof ctxCwd === "string" && ctxCwd.length > 0;
|
|
144
181
|
const cwd = ctxCwd ?? process.cwd();
|
|
145
182
|
const startupScan = resolveStartupScanContext(cwd);
|
|
146
183
|
const scanRoot = startupScan.projectRoot ?? cwd;
|
|
147
|
-
const
|
|
148
|
-
|
|
184
|
+
const useScanRootForSignals =
|
|
185
|
+
startupScan.canWarmCaches || startupScan.reason === "too-many-source-files";
|
|
186
|
+
const analysisRoot = useScanRootForSignals ? scanRoot : cwd;
|
|
187
|
+
runtime.projectRoot = cwd;
|
|
188
|
+
const languageProfile = detectProjectLanguageProfile(analysisRoot);
|
|
149
189
|
dbg(`session_start cwd: ${cwd}`);
|
|
150
190
|
dbg(
|
|
151
191
|
`session_start scan root: ${scanRoot} (warmCaches=${startupScan.canWarmCaches}${startupScan.reason ? `, reason=${startupScan.reason}` : ""})`,
|
|
152
192
|
);
|
|
153
|
-
|
|
193
|
+
dbg(`session_start analysis root: ${analysisRoot}`);
|
|
194
|
+
dbg(`session_start workspace root: ${runtime.projectRoot}`);
|
|
195
|
+
dbg(
|
|
196
|
+
`session_start language profile: ${languageProfile.detectedKinds.join(", ") || "none"}`,
|
|
197
|
+
);
|
|
198
|
+
dbg(
|
|
199
|
+
`session_start language counts: ${JSON.stringify(languageProfile.counts)} configured=${JSON.stringify(languageProfile.configured)}`,
|
|
200
|
+
);
|
|
201
|
+
dbg(`session_start workspace cwd available: ${hasWorkspaceCwd}`);
|
|
202
|
+
if (useScanRootForSignals && analysisRoot !== cwd) {
|
|
154
203
|
dbg(`session_start: monorepo analysis root override -> ${analysisRoot}`);
|
|
155
204
|
}
|
|
156
205
|
|
|
206
|
+
const lensLspEnabled = !!getFlag("lens-lsp") && !getFlag("no-lsp");
|
|
207
|
+
const startupDefaults = getDefaultStartupTools(languageProfile).filter((tool) => {
|
|
208
|
+
if (
|
|
209
|
+
(tool === "typescript-language-server" || tool === "pyright") &&
|
|
210
|
+
!lensLspEnabled
|
|
211
|
+
) {
|
|
212
|
+
return false;
|
|
213
|
+
}
|
|
214
|
+
if (tool === "ruff" && getFlag("no-autofix-ruff")) {
|
|
215
|
+
return false;
|
|
216
|
+
}
|
|
217
|
+
return true;
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
if (startupDefaults.length > 0) {
|
|
221
|
+
dbg(`session_start: pre-install defaults -> ${startupDefaults.join(", ")}`);
|
|
222
|
+
for (const tool of startupDefaults) {
|
|
223
|
+
const startedAt = Date.now();
|
|
224
|
+
dbg(`session_start preinstall ${tool}: start`);
|
|
225
|
+
ensureTool(tool)
|
|
226
|
+
.then((toolPath) => {
|
|
227
|
+
if (toolPath) {
|
|
228
|
+
dbg(`session_start: ${tool} ready at ${toolPath}`);
|
|
229
|
+
dbg(
|
|
230
|
+
`session_start preinstall ${tool}: success (${Date.now() - startedAt}ms)`,
|
|
231
|
+
);
|
|
232
|
+
} else {
|
|
233
|
+
dbg(`session_start: ${tool} installation unavailable`);
|
|
234
|
+
dbg(
|
|
235
|
+
`session_start preinstall ${tool}: unavailable (${Date.now() - startedAt}ms)`,
|
|
236
|
+
);
|
|
237
|
+
}
|
|
238
|
+
})
|
|
239
|
+
.catch((err) => {
|
|
240
|
+
dbg(`session_start: ${tool} pre-install error: ${err}`);
|
|
241
|
+
dbg(
|
|
242
|
+
`session_start preinstall ${tool}: error (${Date.now() - startedAt}ms)`,
|
|
243
|
+
);
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
} else {
|
|
247
|
+
dbg("session_start: no language defaults selected for pre-install");
|
|
248
|
+
}
|
|
249
|
+
|
|
157
250
|
{
|
|
158
251
|
const pkgPath = path.join(analysisRoot, "package.json");
|
|
159
252
|
try {
|
|
@@ -174,9 +267,7 @@ export async function handleSessionStart(deps: SessionStartDeps): Promise<void>
|
|
|
174
267
|
if (p) dbg(`session_start: prettier ready at ${p}`);
|
|
175
268
|
else dbg("session_start: prettier install failed silently");
|
|
176
269
|
})
|
|
177
|
-
.catch((err) =>
|
|
178
|
-
dbg(`session_start: prettier install error: ${err}`),
|
|
179
|
-
);
|
|
270
|
+
.catch((err) => dbg(`session_start: prettier install error: ${err}`));
|
|
180
271
|
}
|
|
181
272
|
} catch {
|
|
182
273
|
// no package.json at cwd root
|
|
@@ -196,14 +287,17 @@ export async function handleSessionStart(deps: SessionStartDeps): Promise<void>
|
|
|
196
287
|
dbg(`session_start tools: ${tools.join(", ")}`);
|
|
197
288
|
|
|
198
289
|
const startupNotes: string[] = [];
|
|
199
|
-
|
|
200
|
-
|
|
290
|
+
const agentStartupGuidance: string[] = [];
|
|
291
|
+
agentStartupGuidance.push(
|
|
292
|
+
"📌 pi-lens active — as you work on this project, fix any errors you encounter (including pre-existing). Prefer: lsp_navigation for definitions/references, ast_grep_search for code patterns, grep for text/TODO search.",
|
|
201
293
|
);
|
|
202
294
|
|
|
203
295
|
runtime.projectRulesScan = scanProjectRules(analysisRoot);
|
|
204
296
|
if (runtime.projectRulesScan.hasCustomRules) {
|
|
205
297
|
const ruleCount = runtime.projectRulesScan.rules.length;
|
|
206
|
-
const sources = [
|
|
298
|
+
const sources = [
|
|
299
|
+
...new Set(runtime.projectRulesScan.rules.map((r) => r.source)),
|
|
300
|
+
];
|
|
207
301
|
dbg(
|
|
208
302
|
`session_start: found ${ruleCount} project rule(s) from ${sources.join(", ")}`,
|
|
209
303
|
);
|
|
@@ -214,102 +308,196 @@ export async function handleSessionStart(deps: SessionStartDeps): Promise<void>
|
|
|
214
308
|
dbg("session_start: no project rules found");
|
|
215
309
|
}
|
|
216
310
|
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
`session_start
|
|
220
|
-
|
|
221
|
-
|
|
311
|
+
if (hasWorkspaceCwd) {
|
|
312
|
+
const installHints = getLanguageInstallHints(languageProfile);
|
|
313
|
+
dbg(`session_start tooling hints count: ${installHints.length}`);
|
|
314
|
+
if (installHints.length > 0) {
|
|
315
|
+
startupNotes.push(`🧰 Tooling hints: ${installHints.join(" ")}`);
|
|
316
|
+
}
|
|
317
|
+
} else {
|
|
318
|
+
dbg("session_start: skipping tooling hints (workspace cwd unavailable)");
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
if (agentStartupGuidance.length > 0) {
|
|
322
|
+
cacheManager.writeCache(
|
|
323
|
+
"session-start-guidance",
|
|
324
|
+
{ content: agentStartupGuidance.join("\n") },
|
|
325
|
+
analysisRoot,
|
|
326
|
+
);
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
const sessionGeneration = runtime.sessionGeneration;
|
|
330
|
+
const runStartupTask = (name: string, task: () => Promise<void>): void => {
|
|
331
|
+
const startedAt = Date.now();
|
|
332
|
+
dbg(`session_start task ${name}: start`);
|
|
333
|
+
runtime.markStartupScanInFlight(name, sessionGeneration);
|
|
334
|
+
void task()
|
|
335
|
+
.then(() => {
|
|
336
|
+
dbg(`session_start task ${name}: success (${Date.now() - startedAt}ms)`);
|
|
337
|
+
})
|
|
338
|
+
.catch((err) => {
|
|
339
|
+
dbg(`session_start: ${name} background scan failed: ${err}`);
|
|
340
|
+
dbg(`session_start task ${name}: failed (${Date.now() - startedAt}ms)`);
|
|
341
|
+
})
|
|
342
|
+
.finally(() => {
|
|
343
|
+
runtime.clearStartupScanInFlight(name, sessionGeneration);
|
|
344
|
+
dbg(`session_start task ${name}: end`);
|
|
345
|
+
});
|
|
346
|
+
};
|
|
347
|
+
|
|
348
|
+
// Fire off heavy scans as background tasks — don't block session start.
|
|
349
|
+
// Each consumer already handles the "not ready yet" case gracefully
|
|
350
|
+
// (cachedExports.size > 0, cachedProjectIndex != null, cache miss paths).
|
|
222
351
|
|
|
223
352
|
if (!startupScan.canWarmCaches) {
|
|
224
|
-
dbg(
|
|
353
|
+
dbg(
|
|
354
|
+
`session_start: skipping heavy scans (${startupScan.reason ?? "unknown"})`,
|
|
355
|
+
);
|
|
356
|
+
dbg(`session_start: skipping TODO scan (${startupScan.reason ?? "unknown"})`);
|
|
225
357
|
} else {
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
`session_start Knip: cache hit (${Math.round((Date.now() - new Date(cached.meta.timestamp).getTime()) / 1000)}s ago)`,
|
|
234
|
-
);
|
|
235
|
-
} else {
|
|
236
|
-
const startMs = Date.now();
|
|
237
|
-
const knipResult = knipClient.analyze(
|
|
238
|
-
analysisRoot,
|
|
239
|
-
getKnipIgnorePatterns(),
|
|
240
|
-
);
|
|
241
|
-
cacheManager.writeCache("knip", knipResult, analysisRoot, {
|
|
242
|
-
scanDurationMs: Date.now() - startMs,
|
|
243
|
-
});
|
|
244
|
-
dbg("session_start Knip scan done");
|
|
245
|
-
}
|
|
246
|
-
} else {
|
|
247
|
-
dbg("session_start Knip: not available");
|
|
358
|
+
const canRunJsTsHeavyScans = canRunStartupHeavyScans(
|
|
359
|
+
languageProfile,
|
|
360
|
+
"jsts",
|
|
361
|
+
);
|
|
362
|
+
const scanNames = ["todo"];
|
|
363
|
+
if (canRunJsTsHeavyScans) {
|
|
364
|
+
scanNames.push("knip", "jscpd", "ast-grep exports", "project index");
|
|
248
365
|
}
|
|
366
|
+
dbg(
|
|
367
|
+
`session_start: launching background scans (${scanNames.join(", ")})`,
|
|
368
|
+
);
|
|
249
369
|
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
370
|
+
runStartupTask("todo", async () => {
|
|
371
|
+
if (!runtime.isCurrentSession(sessionGeneration)) return;
|
|
372
|
+
const todoResult = todoScanner.scanDirectory(analysisRoot);
|
|
373
|
+
if (!runtime.isCurrentSession(sessionGeneration)) return;
|
|
374
|
+
dbg(
|
|
375
|
+
`session_start TODO scan: ${todoResult.items.length} items (baseline stored)`,
|
|
376
|
+
);
|
|
377
|
+
cacheManager.writeCache(
|
|
378
|
+
"todo-baseline",
|
|
379
|
+
{ items: todoResult.items },
|
|
253
380
|
analysisRoot,
|
|
254
381
|
);
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
scanDurationMs: Date.now() - startMs,
|
|
262
|
-
});
|
|
263
|
-
dbg("session_start jscpd scan done");
|
|
264
|
-
}
|
|
382
|
+
});
|
|
383
|
+
|
|
384
|
+
if (!canRunJsTsHeavyScans) {
|
|
385
|
+
dbg(
|
|
386
|
+
"session_start: skipping JS/TS startup scans (requires JS/TS language + project config)",
|
|
387
|
+
);
|
|
265
388
|
} else {
|
|
266
|
-
|
|
267
|
-
|
|
389
|
+
// Knip — dead code / unused exports
|
|
390
|
+
runStartupTask("knip", async () => {
|
|
391
|
+
if (await knipClient.ensureAvailable()) {
|
|
392
|
+
if (!runtime.isCurrentSession(sessionGeneration)) return;
|
|
393
|
+
const cached = cacheManager.readCache<
|
|
394
|
+
ReturnType<KnipClient["analyze"]>
|
|
395
|
+
>("knip", analysisRoot);
|
|
396
|
+
if (cached) {
|
|
397
|
+
if (!runtime.isCurrentSession(sessionGeneration)) return;
|
|
398
|
+
dbg(
|
|
399
|
+
`session_start Knip: cache hit (${Math.round((Date.now() - new Date(cached.meta.timestamp).getTime()) / 1000)}s ago)`,
|
|
400
|
+
);
|
|
401
|
+
} else {
|
|
402
|
+
const startMs = Date.now();
|
|
403
|
+
const knipResult = knipClient.analyze(
|
|
404
|
+
analysisRoot,
|
|
405
|
+
getKnipIgnorePatterns(),
|
|
406
|
+
);
|
|
407
|
+
if (!runtime.isCurrentSession(sessionGeneration)) return;
|
|
408
|
+
cacheManager.writeCache("knip", knipResult, analysisRoot, {
|
|
409
|
+
scanDurationMs: Date.now() - startMs,
|
|
410
|
+
});
|
|
411
|
+
dbg(`session_start Knip scan done (${Date.now() - startMs}ms)`);
|
|
412
|
+
}
|
|
413
|
+
} else {
|
|
414
|
+
if (!runtime.isCurrentSession(sessionGeneration)) return;
|
|
415
|
+
dbg("session_start Knip: not available");
|
|
416
|
+
}
|
|
417
|
+
});
|
|
268
418
|
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
419
|
+
// jscpd — duplicate code detection
|
|
420
|
+
runStartupTask("jscpd", async () => {
|
|
421
|
+
if (await jscpdClient.ensureAvailable()) {
|
|
422
|
+
if (!runtime.isCurrentSession(sessionGeneration)) return;
|
|
423
|
+
const cached = cacheManager.readCache<ReturnType<JscpdClient["scan"]>>(
|
|
424
|
+
"jscpd",
|
|
425
|
+
analysisRoot,
|
|
426
|
+
);
|
|
427
|
+
if (cached) {
|
|
428
|
+
if (!runtime.isCurrentSession(sessionGeneration)) return;
|
|
429
|
+
dbg("session_start jscpd: cache hit");
|
|
430
|
+
} else {
|
|
431
|
+
const startMs = Date.now();
|
|
432
|
+
const jscpdResult = jscpdClient.scan(analysisRoot);
|
|
433
|
+
if (!runtime.isCurrentSession(sessionGeneration)) return;
|
|
434
|
+
cacheManager.writeCache("jscpd", jscpdResult, analysisRoot, {
|
|
435
|
+
scanDurationMs: Date.now() - startMs,
|
|
436
|
+
});
|
|
437
|
+
dbg(`session_start jscpd scan done (${Date.now() - startMs}ms)`);
|
|
438
|
+
}
|
|
439
|
+
} else {
|
|
440
|
+
if (!runtime.isCurrentSession(sessionGeneration)) return;
|
|
441
|
+
dbg("session_start jscpd: not available");
|
|
442
|
+
}
|
|
443
|
+
});
|
|
276
444
|
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
(await isIndexFresh(analysisRoot))
|
|
283
|
-
) {
|
|
284
|
-
runtime.cachedProjectIndex = existing;
|
|
285
|
-
dbg(
|
|
286
|
-
`session_start: loaded fresh project index (${existing.entries.size} entries)`,
|
|
287
|
-
);
|
|
288
|
-
} else {
|
|
289
|
-
const sourceFiles = getSourceFiles(analysisRoot, true);
|
|
290
|
-
const tsFiles = sourceFiles.filter(
|
|
291
|
-
(f) => f.endsWith(".ts") || f.endsWith(".tsx"),
|
|
292
|
-
);
|
|
293
|
-
if (tsFiles.length > 0 && tsFiles.length <= 500) {
|
|
294
|
-
runtime.cachedProjectIndex = await buildProjectIndex(
|
|
445
|
+
// ast-grep — export scan for duplicate detection
|
|
446
|
+
runStartupTask("ast-grep-exports", async () => {
|
|
447
|
+
if (await astGrepClient.ensureAvailable()) {
|
|
448
|
+
if (!runtime.isCurrentSession(sessionGeneration)) return;
|
|
449
|
+
const exports = await astGrepClient.scanExports(
|
|
295
450
|
analysisRoot,
|
|
296
|
-
|
|
451
|
+
"typescript",
|
|
297
452
|
);
|
|
298
|
-
|
|
453
|
+
if (!runtime.isCurrentSession(sessionGeneration)) return;
|
|
454
|
+
dbg(`session_start exports scan: ${exports.size} functions found`);
|
|
455
|
+
for (const [name, file] of exports) {
|
|
456
|
+
runtime.cachedExports.set(name, file);
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
});
|
|
460
|
+
|
|
461
|
+
// Project index — structural similarity detection
|
|
462
|
+
runStartupTask("project-index", async () => {
|
|
463
|
+
const existing = await loadIndex(analysisRoot);
|
|
464
|
+
if (!runtime.isCurrentSession(sessionGeneration)) return;
|
|
465
|
+
if (
|
|
466
|
+
existing &&
|
|
467
|
+
existing.entries.size > 0 &&
|
|
468
|
+
(await isIndexFresh(analysisRoot))
|
|
469
|
+
) {
|
|
470
|
+
if (!runtime.isCurrentSession(sessionGeneration)) return;
|
|
471
|
+
runtime.cachedProjectIndex = existing;
|
|
299
472
|
dbg(
|
|
300
|
-
`session_start:
|
|
473
|
+
`session_start: loaded fresh project index (${existing.entries.size} entries)`,
|
|
301
474
|
);
|
|
302
475
|
} else {
|
|
303
|
-
|
|
476
|
+
const sourceFiles = getSourceFiles(analysisRoot, true);
|
|
477
|
+
const tsFiles = sourceFiles.filter(
|
|
478
|
+
(f) => f.endsWith(".ts") || f.endsWith(".tsx"),
|
|
479
|
+
);
|
|
480
|
+
if (tsFiles.length > 0 && tsFiles.length <= 500) {
|
|
481
|
+
runtime.cachedProjectIndex = await buildProjectIndex(
|
|
482
|
+
analysisRoot,
|
|
483
|
+
tsFiles,
|
|
484
|
+
);
|
|
485
|
+
if (!runtime.isCurrentSession(sessionGeneration)) return;
|
|
486
|
+
await saveIndex(runtime.cachedProjectIndex, analysisRoot);
|
|
487
|
+
dbg(
|
|
488
|
+
`session_start: built project index (${runtime.cachedProjectIndex.entries.size} entries from ${tsFiles.length} files)`,
|
|
489
|
+
);
|
|
490
|
+
} else {
|
|
491
|
+
if (!runtime.isCurrentSession(sessionGeneration)) return;
|
|
492
|
+
dbg(`session_start: skipped project index (${tsFiles.length} files)`);
|
|
493
|
+
}
|
|
304
494
|
}
|
|
305
|
-
}
|
|
306
|
-
} catch (err) {
|
|
307
|
-
dbg(`session_start: project index build failed: ${err}`);
|
|
495
|
+
});
|
|
308
496
|
}
|
|
309
497
|
}
|
|
310
498
|
|
|
311
499
|
dbg(
|
|
312
|
-
`session_start: scans
|
|
500
|
+
`session_start: background scans launched (${startupNotes.length} startup note(s))`,
|
|
313
501
|
);
|
|
314
502
|
|
|
315
503
|
const errorDebtEnabled = getFlag("error-debt");
|
|
@@ -368,4 +556,6 @@ export async function handleSessionStart(deps: SessionStartDeps): Promise<void>
|
|
|
368
556
|
if (startupNotes.length > 0) {
|
|
369
557
|
notify(startupNotes.join("\n"), "info");
|
|
370
558
|
}
|
|
559
|
+
|
|
560
|
+
dbg(`session_start total: ${Date.now() - sessionStartMs}ms`);
|
|
371
561
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import * as nodeFs from "node:fs";
|
|
2
|
-
import {
|
|
2
|
+
import { createFileTime } from "./file-time.js";
|
|
3
3
|
import { getFormatService } from "./format-service.js";
|
|
4
|
+
import { resolveLanguageRootForFile } from "./language-profile.js";
|
|
4
5
|
import { logLatency } from "./latency-logger.js";
|
|
5
6
|
import { runPipeline } from "./pipeline.js";
|
|
6
7
|
import type { BiomeClient } from "./biome-client.js";
|
|
@@ -101,19 +102,16 @@ export async function handleToolResult(
|
|
|
101
102
|
}
|
|
102
103
|
|
|
103
104
|
const sessionFileTime = createFileTime("default");
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
} catch (err: unknown) {
|
|
107
|
-
if (err instanceof FileTimeError) {
|
|
108
|
-
dbg(`⚠️ FileTime warning: ${err.message}`);
|
|
109
|
-
}
|
|
110
|
-
}
|
|
105
|
+
// tool_result is emitted after write/edit has already been applied.
|
|
106
|
+
// Asserting pre-write stamps here produces false positives on rapid edits.
|
|
111
107
|
sessionFileTime.read(filePath);
|
|
112
108
|
|
|
113
109
|
const toolResultStart = Date.now();
|
|
114
110
|
dbg(`tool_result: tracking turn state for ${event.toolName} on ${filePath}`);
|
|
115
111
|
|
|
116
|
-
const
|
|
112
|
+
const workspaceRoot = runtime.projectRoot;
|
|
113
|
+
const cwd = resolveLanguageRootForFile(filePath, workspaceRoot);
|
|
114
|
+
dbg(`tool_result: resolved dispatch cwd ${cwd} for ${filePath}`);
|
|
117
115
|
if (event.model || event.provider || event.sessionId || event.session?.id) {
|
|
118
116
|
runtime.setTelemetryIdentity({
|
|
119
117
|
model: event.model,
|
|
@@ -183,7 +181,7 @@ export async function handleToolResult(
|
|
|
183
181
|
result = await runPipeline(
|
|
184
182
|
{
|
|
185
183
|
filePath,
|
|
186
|
-
cwd
|
|
184
|
+
cwd,
|
|
187
185
|
toolName: event.toolName,
|
|
188
186
|
modifiedRanges,
|
|
189
187
|
telemetry: {
|
package/clients/runtime-turn.ts
CHANGED
|
@@ -82,7 +82,9 @@ export async function handleTurnEnd(deps: TurnEndDeps): Promise<void> {
|
|
|
82
82
|
blockerParts.push(runtime.consumeLastCascadeOutput());
|
|
83
83
|
}
|
|
84
84
|
|
|
85
|
-
if (
|
|
85
|
+
if (runtime.isStartupScanInFlight("jscpd")) {
|
|
86
|
+
dbg("turn_end: skipping jscpd (startup scan still in flight)");
|
|
87
|
+
} else if (await jscpdClient.ensureAvailable()) {
|
|
86
88
|
const jscpdFiles = cacheManager.getFilesForJscpd(cwd);
|
|
87
89
|
if (jscpdFiles.length > 0) {
|
|
88
90
|
dbg(`turn_end: jscpd scanning ${jscpdFiles.length} file(s)`);
|
|
@@ -135,7 +137,7 @@ export async function handleTurnEnd(deps: TurnEndDeps): Promise<void> {
|
|
|
135
137
|
report += ` ${displayA}:${clone.startA} ↔ ${displayB}:${clone.startB} (${clone.lines} lines)\n`;
|
|
136
138
|
}
|
|
137
139
|
if (firstPath) {
|
|
138
|
-
report += `
|
|
140
|
+
report += ` First location: ${firstPath}\n`;
|
|
139
141
|
}
|
|
140
142
|
blockerParts.push(report);
|
|
141
143
|
}
|
|
@@ -143,7 +145,9 @@ export async function handleTurnEnd(deps: TurnEndDeps): Promise<void> {
|
|
|
143
145
|
}
|
|
144
146
|
}
|
|
145
147
|
|
|
146
|
-
if (
|
|
148
|
+
if (runtime.isStartupScanInFlight("knip")) {
|
|
149
|
+
dbg("turn_end: skipping knip (startup scan still in flight)");
|
|
150
|
+
} else if (await knipClient.ensureAvailable()) {
|
|
147
151
|
const knipResult = knipClient.analyze(cwd, getKnipIgnorePatterns());
|
|
148
152
|
const prevKnip = cacheManager.readCache<ReturnType<KnipClient["analyze"]>>(
|
|
149
153
|
"knip",
|
|
@@ -178,7 +182,7 @@ export async function handleTurnEnd(deps: TurnEndDeps): Promise<void> {
|
|
|
178
182
|
report += ` ${display}${issue.line ? `:${issue.line}` : ""} — ${issue.type}: ${issue.name}\n`;
|
|
179
183
|
}
|
|
180
184
|
if (firstPath) {
|
|
181
|
-
report += `
|
|
185
|
+
report += ` First location: ${firstPath}\n`;
|
|
182
186
|
}
|
|
183
187
|
blockerParts.push(report);
|
|
184
188
|
}
|
|
@@ -231,7 +235,20 @@ export async function handleTurnEnd(deps: TurnEndDeps): Promise<void> {
|
|
|
231
235
|
`turn_end: ${blockerParts.length} blocker section(s) found, persisting for next context`,
|
|
232
236
|
);
|
|
233
237
|
const content = capTurnEndMessage(blockerParts.join("\n\n"));
|
|
238
|
+
const signature = `${files.slice().sort().join("|")}::${content}`;
|
|
239
|
+
const last = cacheManager.readCache<{ signature: string }>(
|
|
240
|
+
"turn-end-findings-last",
|
|
241
|
+
cwd,
|
|
242
|
+
);
|
|
243
|
+
if (last?.data?.signature === signature) {
|
|
244
|
+
dbg("turn_end: duplicate blocker findings detected, suppressing re-prompt");
|
|
245
|
+
cacheManager.clearTurnState(cwd);
|
|
246
|
+
runtime.fixedThisTurn.clear();
|
|
247
|
+
resetFormatService();
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
234
250
|
cacheManager.writeCache("turn-end-findings", { content }, cwd);
|
|
251
|
+
cacheManager.writeCache("turn-end-findings-last", { signature }, cwd);
|
|
235
252
|
} else {
|
|
236
253
|
cacheManager.clearTurnState(cwd);
|
|
237
254
|
}
|
package/clients/todo-scanner.ts
CHANGED
|
@@ -91,7 +91,12 @@ export class TodoScanner {
|
|
|
91
91
|
const absolutePath = path.resolve(filePath);
|
|
92
92
|
if (!fs.existsSync(absolutePath)) return [];
|
|
93
93
|
|
|
94
|
-
|
|
94
|
+
let content: string;
|
|
95
|
+
try {
|
|
96
|
+
content = fs.readFileSync(absolutePath, "utf-8");
|
|
97
|
+
} catch {
|
|
98
|
+
return [];
|
|
99
|
+
}
|
|
95
100
|
const lines = content.split("\n");
|
|
96
101
|
const items: TodoItem[] = [];
|
|
97
102
|
|
|
@@ -105,7 +105,7 @@ export class TypeCoverageClient {
|
|
|
105
105
|
if (result.percentage >= 95) icon = "✓";
|
|
106
106
|
else if (result.percentage >= 80) icon = "⚠";
|
|
107
107
|
|
|
108
|
-
let output = `[type-coverage] ${icon} ${pct}% typed (${result.typed}/${result.total} identifiers)`;
|
|
108
|
+
let output = `[type-coverage] ${icon} ${pct}% typed (${result.typed}/${result.total} identifiers; any-typed flagged)`;
|
|
109
109
|
|
|
110
110
|
if (result.untypedLocations.length === 0) {
|
|
111
111
|
output += " — fully typed\n";
|
package/commands/booboo.ts
CHANGED
|
@@ -910,6 +910,8 @@ export async function handleBooboo(
|
|
|
910
910
|
});
|
|
911
911
|
|
|
912
912
|
let fullSection = `## Type Coverage\n\n**${tcResult.percentage.toFixed(1)}% typed** (${tcResult.typed}/${tcResult.total} identifiers)\n\n`;
|
|
913
|
+
fullSection +=
|
|
914
|
+
"Type coverage highlights identifiers that resolve to `any` (implicit or explicit). Inferred non-`any` types are treated as typed.\n\n";
|
|
913
915
|
const byFile: Record<string, number> = {};
|
|
914
916
|
for (const u of filteredLocations) {
|
|
915
917
|
byFile[u.file] = (byFile[u.file] || 0) + 1;
|
|
@@ -920,7 +922,7 @@ export async function handleBooboo(
|
|
|
920
922
|
.slice(0, 10);
|
|
921
923
|
|
|
922
924
|
if (sortedFiles.length > 0) {
|
|
923
|
-
fullSection += `### Top Files by
|
|
925
|
+
fullSection += `### Top Files by Any-Typed Identifier Count\n\n| File | Any-Typed Count |\n|------|-----------------|\n`;
|
|
924
926
|
for (const [file, count] of sortedFiles) {
|
|
925
927
|
fullSection += `| ${file} | ${count} |\n`;
|
|
926
928
|
}
|