pi-lens 3.8.19 → 3.8.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +26 -0
- package/README.md +10 -7
- package/clients/dispatch/dispatcher.ts +53 -1
- package/clients/dispatch/integration.ts +37 -26
- package/clients/dispatch/plan.ts +26 -15
- package/clients/dispatch/runners/lsp.ts +6 -1
- package/clients/dispatch/runners/pyright.ts +4 -6
- package/clients/dispatch/runners/ruff.ts +52 -7
- package/clients/dispatch/runners/sqlfluff.ts +9 -3
- package/clients/dispatch/runners/yamllint.ts +3 -2
- package/clients/file-utils.ts +13 -2
- package/clients/formatters.ts +8 -4
- package/clients/installer/index.ts +371 -49
- package/clients/language-policy.ts +154 -0
- package/clients/language-profile.ts +167 -0
- package/clients/lsp/index.ts +81 -11
- package/clients/lsp/interactive-install.ts +35 -16
- package/clients/lsp/server.ts +357 -267
- package/clients/runtime-context.ts +26 -0
- package/clients/runtime-coordinator.ts +30 -2
- package/clients/runtime-session.ts +293 -103
- package/clients/runtime-tool-result.ts +8 -10
- package/clients/runtime-turn.ts +21 -4
- package/clients/todo-scanner.ts +6 -1
- package/index.ts +15 -3
- package/package.json +1 -1
|
@@ -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 {
|
|
@@ -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: {
|