pi-lens 3.8.21 → 3.8.22

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.
Files changed (47) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/README.md +2 -0
  3. package/clients/dispatch/runners/lsp.ts +58 -3
  4. package/clients/dispatch/runners/tree-sitter.ts +467 -0
  5. package/clients/lsp/client.ts +229 -3
  6. package/clients/lsp/index.ts +111 -1
  7. package/clients/pipeline.ts +2 -2
  8. package/clients/runtime-session.ts +43 -5
  9. package/clients/tree-sitter-client.ts +162 -0
  10. package/clients/tree-sitter-logger.ts +47 -0
  11. package/clients/tree-sitter-query-loader.ts +13 -2
  12. package/package.json +3 -1
  13. package/rules/rule-catalog.json +64 -0
  14. package/rules/tree-sitter-queries/go/go-bare-error.yml +19 -7
  15. package/rules/tree-sitter-queries/go/go-command-injection.yml +55 -0
  16. package/rules/tree-sitter-queries/go/go-direct-panic.yml +45 -0
  17. package/rules/tree-sitter-queries/go/go-empty-if-err.yml +47 -0
  18. package/rules/tree-sitter-queries/go/go-goroutine-loop-capture.yml +49 -0
  19. package/rules/tree-sitter-queries/go/go-ignored-call-result.yml +51 -0
  20. package/rules/tree-sitter-queries/go/go-insecure-random.yml +51 -0
  21. package/rules/tree-sitter-queries/go/go-log-fatal.yml +49 -0
  22. package/rules/tree-sitter-queries/go/go-path-traversal.yml +51 -0
  23. package/rules/tree-sitter-queries/go/go-shared-map-write-goroutine.yml +54 -0
  24. package/rules/tree-sitter-queries/go/go-sql-injection.yml +55 -0
  25. package/rules/tree-sitter-queries/go/go-weak-hash.yml +51 -0
  26. package/rules/tree-sitter-queries/python/python-command-injection.yml +63 -0
  27. package/rules/tree-sitter-queries/python/python-insecure-deserialization.yml +48 -0
  28. package/rules/tree-sitter-queries/python/python-insecure-random.yml +51 -0
  29. package/rules/tree-sitter-queries/python/python-path-traversal.yml +55 -0
  30. package/rules/tree-sitter-queries/python/python-sql-injection.yml +47 -0
  31. package/rules/tree-sitter-queries/python/python-ssrf.yml +50 -0
  32. package/rules/tree-sitter-queries/python/python-thread-global-write.yml +58 -0
  33. package/rules/tree-sitter-queries/python/python-weak-hash.yml +51 -0
  34. package/rules/tree-sitter-queries/ruby/ruby-command-injection.yml +56 -0
  35. package/rules/tree-sitter-queries/ruby/ruby-insecure-deserialization.yml +47 -0
  36. package/rules/tree-sitter-queries/ruby/ruby-insecure-random.yml +54 -0
  37. package/rules/tree-sitter-queries/ruby/ruby-weak-hash.yml +50 -0
  38. package/rules/tree-sitter-queries/rust/rust-lock-held-across-await.yml +59 -0
  39. package/rules/tree-sitter-queries/typescript/ts-command-injection.yml +60 -0
  40. package/rules/tree-sitter-queries/typescript/ts-detached-async-call.yml +56 -0
  41. package/rules/tree-sitter-queries/typescript/ts-insecure-random.yml +54 -0
  42. package/rules/tree-sitter-queries/typescript/ts-ssrf.yml +53 -0
  43. package/rules/tree-sitter-queries/typescript/ts-weak-hash.yml +54 -0
  44. package/scripts/validate-rule-catalog.mjs +227 -0
  45. package/skills/lsp-navigation/SKILL.md +15 -3
  46. package/tools/lsp-navigation.js +259 -28
  47. package/tools/lsp-navigation.ts +294 -29
@@ -49,6 +49,53 @@ export interface LSPHover {
49
49
  range?: LSPLocation["range"];
50
50
  }
51
51
 
52
+ export interface LSPSignatureHelp {
53
+ signatures: Array<{
54
+ label: string;
55
+ documentation?: string | { kind: string; value: string };
56
+ parameters?: Array<{
57
+ label: string | [number, number];
58
+ documentation?: string | { kind: string; value: string };
59
+ }>;
60
+ }>;
61
+ activeSignature?: number;
62
+ activeParameter?: number;
63
+ }
64
+
65
+ export interface LSPCodeAction {
66
+ title: string;
67
+ kind?: string;
68
+ diagnostics?: LSPDiagnostic[];
69
+ edit?: unknown;
70
+ command?: unknown;
71
+ data?: unknown;
72
+ }
73
+
74
+ export interface LSPWorkspaceEdit {
75
+ changes?: Record<string, unknown[]>;
76
+ documentChanges?: unknown[];
77
+ changeAnnotations?: Record<string, unknown>;
78
+ }
79
+
80
+ export interface LSPWorkspaceDiagnosticsSupport {
81
+ advertised: boolean;
82
+ mode: "pull" | "push-only";
83
+ diagnosticProviderKind: string;
84
+ }
85
+
86
+ export interface LSPOperationSupport {
87
+ definition: boolean;
88
+ references: boolean;
89
+ hover: boolean;
90
+ signatureHelp: boolean;
91
+ documentSymbol: boolean;
92
+ workspaceSymbol: boolean;
93
+ codeAction: boolean;
94
+ rename: boolean;
95
+ implementation: boolean;
96
+ callHierarchy: boolean;
97
+ }
98
+
52
99
  export interface LSPSymbol {
53
100
  name: string;
54
101
  kind: number;
@@ -93,6 +140,10 @@ export interface LSPClientInfo {
93
140
  waitForDiagnostics(filePath: string, timeoutMs?: number): Promise<void>;
94
141
  /** Get all tracked diagnostics (for cascade checking) */
95
142
  getAllDiagnostics(): Map<string, LSPDiagnostic[]>;
143
+ /** Capability snapshot for workspace diagnostics support */
144
+ getWorkspaceDiagnosticsSupport(): LSPWorkspaceDiagnosticsSupport;
145
+ /** Capability snapshot for navigation/edit operations */
146
+ getOperationSupport(): LSPOperationSupport;
96
147
  /** Go to definition — returns Location[] */
97
148
  definition(
98
149
  filePath: string,
@@ -112,10 +163,31 @@ export interface LSPClientInfo {
112
163
  line: number,
113
164
  character: number,
114
165
  ): Promise<LSPHover | null>;
166
+ /** Signature help at position */
167
+ signatureHelp(
168
+ filePath: string,
169
+ line: number,
170
+ character: number,
171
+ ): Promise<LSPSignatureHelp | null>;
115
172
  /** Symbols in a document */
116
173
  documentSymbol(filePath: string): Promise<LSPSymbol[]>;
117
174
  /** Workspace-wide symbol search */
118
175
  workspaceSymbol(query: string): Promise<LSPSymbol[]>;
176
+ /** Available code actions at a range */
177
+ codeAction(
178
+ filePath: string,
179
+ line: number,
180
+ character: number,
181
+ endLine: number,
182
+ endCharacter: number,
183
+ ): Promise<LSPCodeAction[]>;
184
+ /** Rename symbol at position */
185
+ rename(
186
+ filePath: string,
187
+ line: number,
188
+ character: number,
189
+ newName: string,
190
+ ): Promise<LSPWorkspaceEdit | null>;
119
191
  /** Go to implementation */
120
192
  implementation(
121
193
  filePath: string,
@@ -141,8 +213,18 @@ export interface LSPClientInfo {
141
213
 
142
214
  // --- Constants ---
143
215
 
144
- const DIAGNOSTICS_DEBOUNCE_MS = 150; // ms — waits for follow-up semantic diagnostics
145
- const INITIALIZE_TIMEOUT_MS = 15_000; // 15s — npx downloads are handled by ensureTool, not here
216
+ const DIAGNOSTICS_DEBOUNCE_MS = positiveIntFromEnv(
217
+ "PI_LENS_LSP_DIAGNOSTICS_DEBOUNCE_MS",
218
+ 150,
219
+ ); // ms — waits for follow-up semantic diagnostics
220
+ const INITIALIZE_TIMEOUT_MS = positiveIntFromEnv(
221
+ "PI_LENS_LSP_INIT_TIMEOUT_MS",
222
+ 15_000,
223
+ ); // 15s — npx downloads are handled by ensureTool, not here
224
+ const DIAGNOSTICS_WAIT_TIMEOUT_MS = positiveIntFromEnv(
225
+ "PI_LENS_LSP_DIAGNOSTICS_WAIT_MS",
226
+ 10_000,
227
+ );
146
228
 
147
229
  // --- Client Factory ---
148
230
 
@@ -327,6 +409,9 @@ export async function createLSPClient(options: {
327
409
  );
328
410
  }
329
411
 
412
+ const workspaceDiagnosticsSupport = detectWorkspaceDiagnosticsSupport(initResult);
413
+ const operationSupport = detectOperationSupport(initResult);
414
+
330
415
  // Send initialized notification
331
416
  await safeSendNotification(connection, "initialized", {});
332
417
 
@@ -437,7 +522,15 @@ export async function createLSPClient(options: {
437
522
  return new Map(diagnostics);
438
523
  },
439
524
 
440
- async waitForDiagnostics(filePath, timeoutMs = 10000) {
525
+ getWorkspaceDiagnosticsSupport() {
526
+ return workspaceDiagnosticsSupport;
527
+ },
528
+
529
+ getOperationSupport() {
530
+ return operationSupport;
531
+ },
532
+
533
+ async waitForDiagnostics(filePath, timeoutMs = DIAGNOSTICS_WAIT_TIMEOUT_MS) {
441
534
  const normalizedPath = normalizeMapKey(filePath);
442
535
 
443
536
  // Fast path: diagnostics already available
@@ -517,6 +610,20 @@ export async function createLSPClient(options: {
517
610
  return result ?? null;
518
611
  },
519
612
 
613
+ async signatureHelp(filePath, line, character) {
614
+ if (!isProcessAlive()) return null;
615
+ const uri = pathToFileURL(filePath).href;
616
+ const result = await safeSendRequest<LSPSignatureHelp>(
617
+ connection,
618
+ "textDocument/signatureHelp",
619
+ {
620
+ textDocument: { uri },
621
+ position: { line, character },
622
+ },
623
+ );
624
+ return result ?? null;
625
+ },
626
+
520
627
  async documentSymbol(filePath) {
521
628
  if (!isProcessAlive()) return [];
522
629
  const uri = pathToFileURL(filePath).href;
@@ -542,6 +649,51 @@ export async function createLSPClient(options: {
542
649
  return result ?? [];
543
650
  },
544
651
 
652
+ async codeAction(
653
+ filePath,
654
+ line,
655
+ character,
656
+ endLine,
657
+ endCharacter,
658
+ ) {
659
+ if (!isProcessAlive()) return [];
660
+ const uri = pathToFileURL(filePath).href;
661
+ const result = await safeSendRequest<unknown[]>(
662
+ connection,
663
+ "textDocument/codeAction",
664
+ {
665
+ textDocument: { uri },
666
+ range: {
667
+ start: { line, character },
668
+ end: { line: endLine, character: endCharacter },
669
+ },
670
+ context: {
671
+ diagnostics: diagnostics.get(normalizeMapKey(filePath)) ?? [],
672
+ },
673
+ },
674
+ );
675
+ if (!result || !Array.isArray(result)) return [];
676
+ return result.filter(
677
+ (item): item is LSPCodeAction =>
678
+ typeof item === "object" && item !== null && "title" in item,
679
+ );
680
+ },
681
+
682
+ async rename(filePath, line, character, newName) {
683
+ if (!isProcessAlive()) return null;
684
+ const uri = pathToFileURL(filePath).href;
685
+ const result = await safeSendRequest<LSPWorkspaceEdit>(
686
+ connection,
687
+ "textDocument/rename",
688
+ {
689
+ textDocument: { uri },
690
+ position: { line, character },
691
+ newName,
692
+ },
693
+ );
694
+ return result ?? null;
695
+ },
696
+
545
697
  async implementation(filePath, line, character) {
546
698
  if (!isProcessAlive()) return [];
547
699
  const uri = pathToFileURL(filePath).href;
@@ -722,3 +874,77 @@ async function withTimeout<T>(
722
874
  ),
723
875
  ]);
724
876
  }
877
+
878
+ function positiveIntFromEnv(name: string, fallback: number): number {
879
+ const raw = process.env[name];
880
+ if (!raw) return fallback;
881
+ const parsed = Number.parseInt(raw, 10);
882
+ if (!Number.isFinite(parsed) || parsed <= 0) return fallback;
883
+ return parsed;
884
+ }
885
+
886
+ function detectWorkspaceDiagnosticsSupport(
887
+ initResult: unknown,
888
+ ): LSPWorkspaceDiagnosticsSupport {
889
+ const capabilities =
890
+ typeof initResult === "object" && initResult !== null
891
+ ? (initResult as { capabilities?: Record<string, unknown> }).capabilities
892
+ : undefined;
893
+ const diagnosticProvider = capabilities?.diagnosticProvider;
894
+ if (!diagnosticProvider) {
895
+ return {
896
+ advertised: false,
897
+ mode: "push-only",
898
+ diagnosticProviderKind: "none",
899
+ };
900
+ }
901
+
902
+ if (typeof diagnosticProvider === "boolean") {
903
+ return {
904
+ advertised: diagnosticProvider,
905
+ mode: diagnosticProvider ? "pull" : "push-only",
906
+ diagnosticProviderKind: "boolean",
907
+ };
908
+ }
909
+
910
+ if (typeof diagnosticProvider === "object") {
911
+ return {
912
+ advertised: true,
913
+ mode: "pull",
914
+ diagnosticProviderKind: "object",
915
+ };
916
+ }
917
+
918
+ return {
919
+ advertised: false,
920
+ mode: "push-only",
921
+ diagnosticProviderKind: typeof diagnosticProvider,
922
+ };
923
+ }
924
+
925
+ function detectOperationSupport(initResult: unknown): LSPOperationSupport {
926
+ const capabilities =
927
+ typeof initResult === "object" && initResult !== null
928
+ ? (initResult as { capabilities?: Record<string, unknown> }).capabilities
929
+ : undefined;
930
+
931
+ const hasProvider = (key: string): boolean => {
932
+ const value = capabilities?.[key];
933
+ if (value === undefined || value === null) return false;
934
+ if (typeof value === "boolean") return value;
935
+ return true;
936
+ };
937
+
938
+ return {
939
+ definition: hasProvider("definitionProvider"),
940
+ references: hasProvider("referencesProvider"),
941
+ hover: hasProvider("hoverProvider"),
942
+ signatureHelp: hasProvider("signatureHelpProvider"),
943
+ documentSymbol: hasProvider("documentSymbolProvider"),
944
+ workspaceSymbol: hasProvider("workspaceSymbolProvider"),
945
+ codeAction: hasProvider("codeActionProvider"),
946
+ rename: hasProvider("renameProvider"),
947
+ implementation: hasProvider("implementationProvider"),
948
+ callHierarchy: hasProvider("callHierarchyProvider"),
949
+ };
950
+ }
@@ -53,6 +53,7 @@ export interface SpawnedServer {
53
53
  export class LSPService {
54
54
  private state: LSPState;
55
55
  private languagePolicyCache = new Map<string, { allowInstall: boolean; expiresAt: number }>();
56
+ private workspaceProbeLogged = new Set<string>();
56
57
 
57
58
  constructor() {
58
59
  this.state = {
@@ -192,11 +193,25 @@ export class LSPService {
192
193
  root,
193
194
  initialization: spawned.initialization,
194
195
  });
196
+ const wsDiag =
197
+ typeof client.getWorkspaceDiagnosticsSupport === "function"
198
+ ? client.getWorkspaceDiagnosticsSupport()
199
+ : {
200
+ advertised: false,
201
+ mode: "push-only" as const,
202
+ diagnosticProviderKind: "unavailable",
203
+ };
195
204
 
196
205
  this.state.clients.set(key, client);
197
206
  logSessionStart(
198
207
  `lsp spawn ${server.id}: success source=${spawned.source ?? server.installPolicy ?? "unknown"} (${Date.now() - startedAt}ms)`,
199
208
  );
209
+ if (!this.workspaceProbeLogged.has(key)) {
210
+ logSessionStart(
211
+ `lsp workspace-diag probe ${server.id}: advertised=${wsDiag.advertised} mode=${wsDiag.mode} provider=${wsDiag.diagnosticProviderKind}`,
212
+ );
213
+ this.workspaceProbeLogged.add(key);
214
+ }
200
215
  return { client, info: server };
201
216
  } catch (err) {
202
217
  logSessionStart(
@@ -294,6 +309,15 @@ export class LSPService {
294
309
  return spawned.client.hover(filePath, line, character);
295
310
  }
296
311
 
312
+ /**
313
+ * Navigation: signature help at cursor position
314
+ */
315
+ async signatureHelp(filePath: string, line: number, character: number) {
316
+ const spawned = await this.getClientForFile(filePath);
317
+ if (!spawned) return null;
318
+ return spawned.client.signatureHelp(filePath, line, character);
319
+ }
320
+
297
321
  /**
298
322
  * Navigation: symbols in document
299
323
  */
@@ -306,13 +330,98 @@ export class LSPService {
306
330
  /**
307
331
  * Navigation: workspace-wide symbol search
308
332
  */
309
- async workspaceSymbol(query: string) {
333
+ async workspaceSymbol(query: string, filePath?: string) {
334
+ if (filePath) {
335
+ const spawned = await this.getClientForFile(filePath);
336
+ if (!spawned) return [];
337
+ return spawned.client.workspaceSymbol(query);
338
+ }
339
+
310
340
  // Use the first active client for workspace-level queries
311
341
  const clients = Array.from(this.state.clients.values());
312
342
  if (clients.length === 0) return [];
313
343
  return clients[0].workspaceSymbol(query);
314
344
  }
315
345
 
346
+ /**
347
+ * Capability snapshot for LSP operations.
348
+ * If filePath is provided, probes that server; otherwise uses first active client.
349
+ */
350
+ async getOperationSupport(filePath?: string): Promise<
351
+ import("./client.js").LSPOperationSupport | null
352
+ > {
353
+ if (filePath) {
354
+ const spawned = await this.getClientForFile(filePath);
355
+ if (!spawned) return null;
356
+ const getter = spawned.client.getOperationSupport;
357
+ if (typeof getter !== "function") return null;
358
+ return getter();
359
+ }
360
+
361
+ const first = this.state.clients.values().next().value;
362
+ if (!first) return null;
363
+ const getter = first.getOperationSupport;
364
+ if (typeof getter !== "function") return null;
365
+ return getter();
366
+ }
367
+
368
+ /**
369
+ * Capability snapshot for workspace diagnostics support.
370
+ * If filePath is provided, probes that server; otherwise uses first active client.
371
+ */
372
+ async getWorkspaceDiagnosticsSupport(filePath?: string): Promise<
373
+ import("./client.js").LSPWorkspaceDiagnosticsSupport | null
374
+ > {
375
+ if (filePath) {
376
+ const spawned = await this.getClientForFile(filePath);
377
+ if (!spawned) return null;
378
+ const getter = spawned.client.getWorkspaceDiagnosticsSupport;
379
+ if (typeof getter !== "function") return null;
380
+ return getter();
381
+ }
382
+
383
+ const first = this.state.clients.values().next().value;
384
+ if (!first) return null;
385
+ const getter = first.getWorkspaceDiagnosticsSupport;
386
+ if (typeof getter !== "function") return null;
387
+ return getter();
388
+ }
389
+
390
+ /**
391
+ * Navigation: available code actions at position/range
392
+ */
393
+ async codeAction(
394
+ filePath: string,
395
+ line: number,
396
+ character: number,
397
+ endLine: number,
398
+ endCharacter: number,
399
+ ) {
400
+ const spawned = await this.getClientForFile(filePath);
401
+ if (!spawned) return [];
402
+ return spawned.client.codeAction(
403
+ filePath,
404
+ line,
405
+ character,
406
+ endLine,
407
+ endCharacter,
408
+ );
409
+ }
410
+
411
+ /**
412
+ * Navigation: rename symbol at position
413
+ */
414
+ async rename(
415
+ filePath: string,
416
+ line: number,
417
+ character: number,
418
+ newName: string,
419
+ ) {
420
+ const spawned = await this.getClientForFile(filePath);
421
+ if (!spawned) return null;
422
+ return spawned.client.rename(filePath, line, character, newName);
423
+ }
424
+
316
425
  /**
317
426
  * Navigation: go to implementation
318
427
  */
@@ -402,6 +511,7 @@ export class LSPService {
402
511
  }
403
512
  this.state.clients.clear();
404
513
  this.state.broken.clear();
514
+ this.workspaceProbeLogged.clear();
405
515
  }
406
516
 
407
517
  /**
@@ -672,8 +672,8 @@ export async function runPipeline(
672
672
  for (const e of limited) {
673
673
  const line = (e.range?.start?.line ?? 0) + 1;
674
674
  const col = (e.range?.start?.character ?? 0) + 1;
675
- const code = e.code ? ` [${e.code}]` : "";
676
- c += `\n ${code} (${line}:${col}) ${e.message.split("\n")[0].slice(0, 100)}`;
675
+ const code = e.code ? ` code=${String(e.code)}` : "";
676
+ c += `\n line ${line}, col ${col}${code}: ${e.message.split("\n")[0].slice(0, 100)}`;
677
677
  }
678
678
  c += `${suffix}\n</diagnostics>`;
679
679
  }
@@ -62,11 +62,27 @@ interface SessionStartDeps {
62
62
  resetLSPService: () => void;
63
63
  }
64
64
 
65
+ type StartupMode = "full" | "minimal" | "quick";
66
+
65
67
  function isCommandAvailable(command: string, args: string[] = ["--version"]): boolean {
66
68
  const result = safeSpawn(command, args, { timeout: 5000 });
67
69
  return !result.error && result.status === 0;
68
70
  }
69
71
 
72
+ function resolveStartupMode(): StartupMode {
73
+ const envMode = (process.env.PI_LENS_STARTUP_MODE ?? "").trim().toLowerCase();
74
+ if (envMode === "full" || envMode === "minimal" || envMode === "quick") {
75
+ return envMode;
76
+ }
77
+
78
+ const argv = process.argv;
79
+ if (argv.includes("--print") || argv.includes("-p")) {
80
+ return "quick";
81
+ }
82
+
83
+ return "full";
84
+ }
85
+
70
86
  function getLanguageInstallHints(
71
87
  languageProfile: ReturnType<typeof detectProjectLanguageProfile>,
72
88
  ): string[] {
@@ -99,6 +115,9 @@ export async function handleSessionStart(
99
115
  deps: SessionStartDeps,
100
116
  ): Promise<void> {
101
117
  const sessionStartMs = Date.now();
118
+ const startupMode = resolveStartupMode();
119
+ const allowBootstrapTasks = startupMode === "full";
120
+ const quickMode = startupMode === "quick";
102
121
  const {
103
122
  ctxCwd,
104
123
  getFlag,
@@ -131,10 +150,14 @@ export async function handleSessionStart(
131
150
  runtime.complexityBaselines.clear();
132
151
  resetDispatchBaselines();
133
152
  runtime.resetForSession();
153
+ dbg(`session_start startup mode: ${startupMode}`);
134
154
 
135
155
  if (getFlag("lens-lsp") && !getFlag("no-lsp")) {
136
156
  resetLSPService();
137
157
  dbg("session_start: LSP service reset");
158
+ dbg(
159
+ "session_start: phase0 workspace diagnostics observation enabled (capability probe only)",
160
+ );
138
161
  }
139
162
 
140
163
  if (getFlag("auto-install")) {
@@ -166,7 +189,7 @@ export async function handleSessionStart(
166
189
  log(`Active tools: ${tools.join(", ")}`);
167
190
  dbg(`session_start tools: ${tools.join(", ")}`);
168
191
 
169
- if (getFlag("lens-lsp") && !getFlag("no-lsp")) {
192
+ if (allowBootstrapTasks && getFlag("lens-lsp") && !getFlag("no-lsp")) {
170
193
  const cleaned = cleanStaleTsBuildInfo(ctxCwd ?? process.cwd());
171
194
  if (cleaned.length > 0) {
172
195
  notify(
@@ -179,6 +202,15 @@ export async function handleSessionStart(
179
202
 
180
203
  const hasWorkspaceCwd = typeof ctxCwd === "string" && ctxCwd.length > 0;
181
204
  const cwd = ctxCwd ?? process.cwd();
205
+ if (quickMode) {
206
+ runtime.projectRoot = cwd;
207
+ dbg(
208
+ "session_start: quick mode active - skipping language profiling, preinstall, scans, and error debt baseline",
209
+ );
210
+ dbg(`session_start total: ${Date.now() - sessionStartMs}ms`);
211
+ return;
212
+ }
213
+
182
214
  const startupScan = resolveStartupScanContext(cwd);
183
215
  const scanRoot = startupScan.projectRoot ?? cwd;
184
216
  const useScanRootForSignals =
@@ -217,7 +249,9 @@ export async function handleSessionStart(
217
249
  return true;
218
250
  });
219
251
 
220
- if (startupDefaults.length > 0) {
252
+ if (!allowBootstrapTasks) {
253
+ dbg("session_start: skipping tool preinstall (startup mode)");
254
+ } else if (startupDefaults.length > 0) {
221
255
  dbg(`session_start: pre-install defaults -> ${startupDefaults.join(", ")}`);
222
256
  for (const tool of startupDefaults) {
223
257
  const startedAt = Date.now();
@@ -247,7 +281,7 @@ export async function handleSessionStart(
247
281
  dbg("session_start: no language defaults selected for pre-install");
248
282
  }
249
283
 
250
- {
284
+ if (allowBootstrapTasks) {
251
285
  const pkgPath = path.join(analysisRoot, "package.json");
252
286
  try {
253
287
  const raw = await nodeFs.promises.readFile(pkgPath, "utf-8");
@@ -272,6 +306,8 @@ export async function handleSessionStart(
272
306
  } catch {
273
307
  // no package.json at cwd root
274
308
  }
309
+ } else {
310
+ dbg("session_start: skipping prettier preinstall probe (startup mode)");
275
311
  }
276
312
 
277
313
  const hasArchitectRules = architectClient.loadConfig(analysisRoot);
@@ -349,7 +385,9 @@ export async function handleSessionStart(
349
385
  // Each consumer already handles the "not ready yet" case gracefully
350
386
  // (cachedExports.size > 0, cachedProjectIndex != null, cache miss paths).
351
387
 
352
- if (!startupScan.canWarmCaches) {
388
+ if (!allowBootstrapTasks) {
389
+ dbg("session_start: skipping startup background scans (startup mode)");
390
+ } else if (!startupScan.canWarmCaches) {
353
391
  dbg(
354
392
  `session_start: skipping heavy scans (${startupScan.reason ?? "unknown"})`,
355
393
  );
@@ -500,7 +538,7 @@ export async function handleSessionStart(
500
538
  `session_start: background scans launched (${startupNotes.length} startup note(s))`,
501
539
  );
502
540
 
503
- const errorDebtEnabled = getFlag("error-debt");
541
+ const errorDebtEnabled = allowBootstrapTasks && getFlag("error-debt");
504
542
  const pendingDebt = cacheManager.readCache<{
505
543
  pendingCheck: boolean;
506
544
  baselineTestsPassed: boolean;