@cortexkit/aft-pi 0.31.1 → 0.33.0

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/dist/index.js CHANGED
@@ -11680,6 +11680,7 @@ import { homedir } from "node:os";
11680
11680
  import { join } from "node:path";
11681
11681
  import { StringDecoder } from "node:string_decoder";
11682
11682
  var DEFAULT_BRIDGE_TIMEOUT_MS = 30000;
11683
+ var BRIDGE_HANG_TIMEOUT_THRESHOLD = 2;
11683
11684
  var SEMANTIC_TIMEOUT_SAFETY_MARGIN_MS = 5000;
11684
11685
  var MAX_STDOUT_BUFFER = 64 * 1024 * 1024;
11685
11686
  function tagStderrLine(line) {
@@ -11787,8 +11788,11 @@ class BinaryBridge {
11787
11788
  statusListeners = new Set;
11788
11789
  configureWarningClients = new Map;
11789
11790
  restartResetTimer = null;
11791
+ lastChildActivityAt = 0;
11792
+ consecutiveRequestTimeouts = 0;
11790
11793
  errorPrefix;
11791
11794
  logger;
11795
+ childEnv;
11792
11796
  constructor(binaryPath, cwd, options, configOverrides) {
11793
11797
  this.binaryPath = binaryPath;
11794
11798
  this.cwd = cwd;
@@ -11803,6 +11807,7 @@ class BinaryBridge {
11803
11807
  this.onBashPatternMatch = options?.onBashPatternMatch;
11804
11808
  this.errorPrefix = options?.errorPrefix ?? "[aft-bridge]";
11805
11809
  this.logger = options?.logger;
11810
+ this.childEnv = options?.childEnv;
11806
11811
  }
11807
11812
  logVia(message, meta) {
11808
11813
  const logger = this.logger ?? getActiveLogger();
@@ -11954,20 +11959,41 @@ class BinaryBridge {
11954
11959
  const line = `${JSON.stringify(request)}
11955
11960
  `;
11956
11961
  const keepBridgeOnTimeout = options?.keepBridgeOnTimeout === true;
11962
+ let requestSentAt = Date.now();
11957
11963
  const response = await new Promise((resolve, reject) => {
11958
11964
  const timer = setTimeout(() => {
11965
+ const entry = this.pending.get(id);
11966
+ if (!entry)
11967
+ return;
11959
11968
  this.pending.delete(id);
11960
- const restartSuffix = keepBridgeOnTimeout ? "" : " — restarting bridge";
11969
+ clearTimeout(entry.timer);
11970
+ if (keepBridgeOnTimeout) {
11971
+ const timeoutMsg2 = `Request "${command}" (id=${id}) timed out after ${effectiveTimeoutMs}ms`;
11972
+ if (requestSessionId) {
11973
+ this.sessionWarnVia(requestSessionId, timeoutMsg2);
11974
+ } else {
11975
+ this.warnVia(timeoutMsg2);
11976
+ }
11977
+ entry.reject(new Error(`${this.errorPrefix} Request "${command}" (id=${id}) timed out after ${effectiveTimeoutMs}ms`));
11978
+ return;
11979
+ }
11980
+ const childActiveSinceRequest = this.lastChildActivityAt > requestSentAt;
11981
+ const consecutiveTimeouts = this.consecutiveRequestTimeouts + 1;
11982
+ this.consecutiveRequestTimeouts = consecutiveTimeouts;
11983
+ const keepWarm = childActiveSinceRequest || consecutiveTimeouts < BRIDGE_HANG_TIMEOUT_THRESHOLD;
11984
+ const restartSuffix = keepWarm ? " — bridge kept warm" : " — restarting bridge";
11961
11985
  const timeoutMsg = `Request "${command}" (id=${id}) timed out after ${effectiveTimeoutMs}ms${restartSuffix}`;
11962
11986
  if (requestSessionId) {
11963
11987
  this.sessionWarnVia(requestSessionId, timeoutMsg);
11964
11988
  } else {
11965
11989
  this.warnVia(timeoutMsg);
11966
11990
  }
11967
- reject(new Error(`${this.errorPrefix} Request "${command}" (id=${id}) timed out after ${effectiveTimeoutMs}ms`));
11968
- if (!keepBridgeOnTimeout) {
11969
- this.handleTimeout(requestSessionId);
11991
+ if (keepWarm) {
11992
+ entry.reject(new Error(`${this.errorPrefix} request "${command}" timed out after ${effectiveTimeoutMs}ms (bridge busy/under load); bridge kept warm — retry`));
11993
+ return;
11970
11994
  }
11995
+ entry.reject(new Error(`${this.errorPrefix} Request "${command}" (id=${id}) timed out after ${effectiveTimeoutMs}ms`));
11996
+ this.handleTimeout(requestSessionId);
11971
11997
  }, effectiveTimeoutMs);
11972
11998
  this.pending.set(id, { resolve, reject, timer, onProgress: options?.onProgress });
11973
11999
  if (!this.process?.stdin?.writable) {
@@ -11976,6 +12002,7 @@ class BinaryBridge {
11976
12002
  reject(new Error(`${this.errorPrefix} stdin not writable for command "${command}"`));
11977
12003
  return;
11978
12004
  }
12005
+ requestSentAt = Date.now();
11979
12006
  this.process.stdin.write(line, (err) => {
11980
12007
  if (err) {
11981
12008
  const entry = this.pending.get(id);
@@ -12170,6 +12197,15 @@ class BinaryBridge {
12170
12197
  this.logVia(`ORT_DYLIB_PATH set from managed ONNX Runtime: ${ortLibraryPath}`);
12171
12198
  }
12172
12199
  }
12200
+ if (this.childEnv) {
12201
+ for (const [key, value] of Object.entries(this.childEnv)) {
12202
+ if (value === undefined) {
12203
+ delete env[key];
12204
+ } else {
12205
+ env[key] = value;
12206
+ }
12207
+ }
12208
+ }
12173
12209
  const child = spawn(this.binaryPath, [], {
12174
12210
  cwd: this.cwd,
12175
12211
  stdio: ["pipe", "pipe", "pipe"],
@@ -12219,6 +12255,8 @@ class BinaryBridge {
12219
12255
  this.process = child;
12220
12256
  this.stdoutBuffer = "";
12221
12257
  this.stderrBuffer = "";
12258
+ this.lastChildActivityAt = 0;
12259
+ this.consecutiveRequestTimeouts = 0;
12222
12260
  this.stderrTail = [];
12223
12261
  }
12224
12262
  pushStderrLine(line) {
@@ -12274,6 +12312,7 @@ class BinaryBridge {
12274
12312
  continue;
12275
12313
  try {
12276
12314
  const response = JSON.parse(line);
12315
+ this.lastChildActivityAt = Date.now();
12277
12316
  if (response.type === "progress") {
12278
12317
  const requestId = response.request_id;
12279
12318
  const entry = requestId ? this.pending.get(requestId) : undefined;
@@ -12326,6 +12365,7 @@ class BinaryBridge {
12326
12365
  continue;
12327
12366
  this.pending.delete(id);
12328
12367
  clearTimeout(entry.timer);
12368
+ this.consecutiveRequestTimeouts = 0;
12329
12369
  this.scheduleRestartCountReset();
12330
12370
  entry.resolve(response);
12331
12371
  } else if (typeof response.type === "string") {
@@ -12337,6 +12377,7 @@ class BinaryBridge {
12337
12377
  }
12338
12378
  }
12339
12379
  handleTimeout(triggeringSessionId) {
12380
+ this.consecutiveRequestTimeouts = 0;
12340
12381
  this.rejectAllPending(new Error(`${this.errorPrefix} bridge killed during sibling timeout — request aborted`));
12341
12382
  if (this.process) {
12342
12383
  this.process.kill("SIGKILL");
@@ -12415,7 +12456,7 @@ class BinaryBridge {
12415
12456
  // ../aft-bridge/dist/downloader.js
12416
12457
  import { spawnSync } from "node:child_process";
12417
12458
  import { createHash, randomUUID } from "node:crypto";
12418
- import { chmodSync, closeSync, createWriteStream, existsSync, mkdirSync, openSync, readFileSync, renameSync, rmSync, statSync, unlinkSync, writeSync } from "node:fs";
12459
+ import { chmodSync, closeSync, copyFileSync, createWriteStream, existsSync, mkdirSync, openSync, readFileSync, renameSync, rmSync, statSync, unlinkSync, writeSync } from "node:fs";
12419
12460
  import { homedir as homedir2 } from "node:os";
12420
12461
  import { join as join2 } from "node:path";
12421
12462
  import { Readable } from "node:stream";
@@ -12582,10 +12623,18 @@ async function downloadBinary(version) {
12582
12623
  throw new Error(`Checksum mismatch for ${assetName}: expected ${expectedHash}, got ${actualHash}. ` + "The binary may have been tampered with.");
12583
12624
  }
12584
12625
  log(`Checksum verified (SHA-256: ${actualHash.slice(0, 16)}...)`);
12585
- if (process.platform !== "win32") {
12626
+ if (process.platform === "win32") {
12627
+ copyFileSync(tmpPath, binaryPath);
12628
+ } else {
12586
12629
  chmodSync(tmpPath, 493);
12630
+ renameSync(tmpPath, binaryPath);
12631
+ }
12632
+ try {
12633
+ if (existsSync(tmpPath))
12634
+ unlinkSync(tmpPath);
12635
+ } catch {
12636
+ warn(`Could not clean up temporary download file ${tmpPath} — it can be removed manually.`);
12587
12637
  }
12588
- renameSync(tmpPath, binaryPath);
12589
12638
  log(`AFT binary ready at ${binaryPath}`);
12590
12639
  return binaryPath;
12591
12640
  } catch (err) {
@@ -12756,7 +12805,7 @@ function markAnnouncementSeen(storageRoot, harness, currentVersion) {
12756
12805
 
12757
12806
  // ../aft-bridge/dist/resolver.js
12758
12807
  import { execSync } from "node:child_process";
12759
- import { chmodSync as chmodSync2, copyFileSync, existsSync as existsSync3, mkdirSync as mkdirSync3, renameSync as renameSync3 } from "node:fs";
12808
+ import { chmodSync as chmodSync2, copyFileSync as copyFileSync2, existsSync as existsSync3, mkdirSync as mkdirSync3, renameSync as renameSync3, unlinkSync as unlinkSync2 } from "node:fs";
12760
12809
  import { createRequire as createRequire2 } from "node:module";
12761
12810
  import { homedir as homedir3 } from "node:os";
12762
12811
  import { join as join4 } from "node:path";
@@ -12779,10 +12828,15 @@ function copyToVersionedCache(npmBinaryPath, knownVersion) {
12779
12828
  }
12780
12829
  mkdirSync3(versionedDir, { recursive: true });
12781
12830
  const tmpPath = `${cachedPath}.${process.pid}.${Date.now()}.tmp`;
12782
- copyFileSync(npmBinaryPath, tmpPath);
12831
+ copyFileSync2(npmBinaryPath, tmpPath);
12783
12832
  if (process.platform !== "win32") {
12784
12833
  chmodSync2(tmpPath, 493);
12785
12834
  }
12835
+ if (process.platform === "win32" && existsSync3(cachedPath)) {
12836
+ try {
12837
+ unlinkSync2(cachedPath);
12838
+ } catch {}
12839
+ }
12786
12840
  renameSync3(tmpPath, cachedPath);
12787
12841
  log(`Copied npm binary to versioned cache: ${cachedPath}`);
12788
12842
  return cachedPath;
@@ -13034,7 +13088,7 @@ async function ensureStorageMigrated(opts) {
13034
13088
  // ../aft-bridge/dist/onnx-runtime.js
13035
13089
  import { execFileSync } from "node:child_process";
13036
13090
  import { createHash as createHash2 } from "node:crypto";
13037
- import { chmodSync as chmodSync3, closeSync as closeSync2, copyFileSync as copyFileSync2, createWriteStream as createWriteStream2, existsSync as existsSync5, lstatSync, mkdirSync as mkdirSync5, openSync as openSync2, readdirSync, readFileSync as readFileSync3, readlinkSync, realpathSync, rmSync as rmSync2, statSync as statSync2, symlinkSync, unlinkSync as unlinkSync2, writeFileSync as writeFileSync2 } from "node:fs";
13091
+ import { chmodSync as chmodSync3, closeSync as closeSync2, copyFileSync as copyFileSync3, createWriteStream as createWriteStream2, existsSync as existsSync5, lstatSync, mkdirSync as mkdirSync5, openSync as openSync2, readdirSync, readFileSync as readFileSync3, readlinkSync, realpathSync, rmSync as rmSync2, statSync as statSync2, symlinkSync, unlinkSync as unlinkSync3, writeFileSync as writeFileSync2 } from "node:fs";
13038
13092
  import { basename, dirname as dirname3, isAbsolute, join as join6, relative, resolve, win32 } from "node:path";
13039
13093
  import { Readable as Readable2 } from "node:stream";
13040
13094
  import { pipeline as pipeline2 } from "node:stream/promises";
@@ -13270,20 +13324,64 @@ function findSystemOnnxRuntime(libName) {
13270
13324
  } else if (process.platform === "linux") {
13271
13325
  searchPaths.push("/usr/lib", "/usr/lib/x86_64-linux-gnu", "/usr/lib/aarch64-linux-gnu", "/usr/local/lib");
13272
13326
  } else if (process.platform === "win32") {
13327
+ const programFiles = process.env.ProgramFiles ?? "C:\\Program Files";
13328
+ const programFilesX86 = process.env["ProgramFiles(x86)"] ?? "C:\\Program Files (x86)";
13329
+ searchPaths.push(join6(programFiles, "onnxruntime", "lib"), join6(programFiles, "Microsoft ONNX Runtime", "lib"), join6(programFiles, "Microsoft Machine Learning", "lib"), join6(programFilesX86, "onnxruntime", "lib"), ...(() => {
13330
+ const nugetPaths = [];
13331
+ const userProfile = process.env.USERPROFILE ?? "";
13332
+ if (!userProfile)
13333
+ return nugetPaths;
13334
+ const nugetPackageDir = join6(userProfile, ".nuget", "packages", "microsoft.ml.onnxruntime");
13335
+ if (!existsSync5(nugetPackageDir))
13336
+ return nugetPaths;
13337
+ try {
13338
+ for (const entry of readdirSync(nugetPackageDir, { withFileTypes: true })) {
13339
+ if (!entry.isDirectory())
13340
+ continue;
13341
+ if (entry.name === "__globalPackagesFolder" || entry.name.startsWith("."))
13342
+ continue;
13343
+ nugetPaths.push(join6(nugetPackageDir, entry.name, "runtimes", "win-x64", "native"), join6(nugetPackageDir, entry.name, "runtimes", "win-arm64", "native"));
13344
+ }
13345
+ } catch (err) {
13346
+ warn(`Failed to scan NuGet ONNX Runtime cache ${nugetPackageDir}: ${err instanceof Error ? err.message : String(err)}`);
13347
+ }
13348
+ return nugetPaths;
13349
+ })());
13273
13350
  searchPaths.push(...pathEntriesForPlatform());
13274
13351
  }
13352
+ const normalizeCase = process.platform === "win32" || process.platform === "darwin";
13275
13353
  const seen = new Set;
13276
- for (const dir of searchPaths) {
13277
- if (seen.has(dir))
13278
- continue;
13279
- seen.add(dir);
13280
- if (!directoryContainsLibrary(dir, libName))
13281
- continue;
13282
- const version = detectOnnxVersion(dir, libName);
13283
- if (version && !isOnnxVersionCompatible(version)) {
13284
- warn(`Skipping system ONNX Runtime at ${dir} (v${version}); AFT requires ` + `v${REQUIRED_ORT_MAJOR}.${REQUIRED_ORT_MIN_MINOR}+. Falling through to AFT-managed download.`);
13354
+ const uniquePaths = searchPaths.filter((p) => {
13355
+ let key = resolve(p).replace(/[/\\]+$/, "");
13356
+ if (normalizeCase)
13357
+ key = key.toLowerCase();
13358
+ if (seen.has(key))
13359
+ return false;
13360
+ seen.add(key);
13361
+ return true;
13362
+ });
13363
+ for (const dir of uniquePaths) {
13364
+ const libPath = join6(dir, libName);
13365
+ if (process.platform === "win32") {
13366
+ if (!directoryContainsLibrary(dir, libName))
13367
+ continue;
13368
+ } else if (!existsSync5(libPath)) {
13285
13369
  continue;
13286
13370
  }
13371
+ let skipVersionCheck = false;
13372
+ if (process.platform === "win32") {
13373
+ const dirLower = dir.toLowerCase();
13374
+ const isCommonPath = dirLower.includes("program files") || dirLower.includes("onnxruntime");
13375
+ if (!isCommonPath)
13376
+ skipVersionCheck = true;
13377
+ }
13378
+ if (!skipVersionCheck) {
13379
+ const version = detectOnnxVersion(dir, libName);
13380
+ if (version && !isOnnxVersionCompatible(version)) {
13381
+ warn(`Skipping system ONNX Runtime at ${dir} (v${version}); AFT requires ` + `v${REQUIRED_ORT_MAJOR}.${REQUIRED_ORT_MIN_MINOR}+. Falling through to AFT-managed download.`);
13382
+ continue;
13383
+ }
13384
+ }
13287
13385
  return dir;
13288
13386
  }
13289
13387
  return null;
@@ -13321,7 +13419,7 @@ async function downloadFileWithCap(url, destPath) {
13321
13419
  await pipeline2(nodeStream, createWriteStream2(destPath), { signal: controller.signal });
13322
13420
  } catch (err) {
13323
13421
  try {
13324
- unlinkSync2(destPath);
13422
+ unlinkSync3(destPath);
13325
13423
  } catch {}
13326
13424
  throw err;
13327
13425
  } finally {
@@ -13382,7 +13480,7 @@ async function downloadOnnxRuntime(info, targetDir) {
13382
13480
  await extractZipArchive(archivePath, tmpDir);
13383
13481
  }
13384
13482
  try {
13385
- unlinkSync2(archivePath);
13483
+ unlinkSync3(archivePath);
13386
13484
  } catch {}
13387
13485
  validateExtractedTree(tmpDir);
13388
13486
  const extractedDir = join6(tmpDir, info.assetName, "lib");
@@ -13431,7 +13529,7 @@ async function downloadOnnxRuntime(info, targetDir) {
13431
13529
  return null;
13432
13530
  }
13433
13531
  }
13434
- function copyOnnxLibraries(info, extractedDir, targetDir, realFiles, symlinks, copyFile = copyFileSync2) {
13532
+ function copyOnnxLibraries(info, extractedDir, targetDir, realFiles, symlinks, copyFile = copyFileSync3) {
13435
13533
  const requiredLibs = new Set([info.libName]);
13436
13534
  for (const libFile of realFiles) {
13437
13535
  const src = join6(extractedDir, libFile);
@@ -13452,7 +13550,7 @@ function copyOnnxLibraries(info, extractedDir, targetDir, realFiles, symlinks, c
13452
13550
  for (const link of symlinks) {
13453
13551
  const dst = join6(targetDir, link.name);
13454
13552
  try {
13455
- unlinkSync2(dst);
13553
+ unlinkSync3(dst);
13456
13554
  } catch {}
13457
13555
  try {
13458
13556
  symlinkSync(link.target, dst);
@@ -13563,7 +13661,7 @@ ${new Date().toISOString()}
13563
13661
  }
13564
13662
  log(`[onnx] reclaiming install lock (owner_pid=${owningPid ?? "unknown"}, alive=${ownerAlive}, age_ms=${age})`);
13565
13663
  try {
13566
- unlinkSync2(lockPath);
13664
+ unlinkSync3(lockPath);
13567
13665
  } catch {}
13568
13666
  return tryClaim();
13569
13667
  }
@@ -13588,7 +13686,7 @@ function releaseLock(lockPath) {
13588
13686
  return;
13589
13687
  }
13590
13688
  try {
13591
- unlinkSync2(lockPath);
13689
+ unlinkSync3(lockPath);
13592
13690
  } catch (unlinkErr) {
13593
13691
  const code = unlinkErr.code;
13594
13692
  if (code !== "ENOENT") {
@@ -14534,7 +14632,7 @@ import {
14534
14632
  // package.json
14535
14633
  var package_default = {
14536
14634
  name: "@cortexkit/aft-pi",
14537
- version: "0.31.1",
14635
+ version: "0.33.0",
14538
14636
  type: "module",
14539
14637
  description: "Pi coding agent extension for Agent File Tools (AFT) — tree-sitter and LSP-powered code analysis",
14540
14638
  main: "dist/index.js",
@@ -14542,7 +14640,7 @@ var package_default = {
14542
14640
  license: "MIT",
14543
14641
  repository: {
14544
14642
  type: "git",
14545
- url: "https://github.com/cortexkit/aft"
14643
+ url: "git+https://github.com/cortexkit/aft.git"
14546
14644
  },
14547
14645
  files: [
14548
14646
  "dist",
@@ -14556,7 +14654,7 @@ var package_default = {
14556
14654
  prepublishOnly: "bun run build"
14557
14655
  },
14558
14656
  dependencies: {
14559
- "@cortexkit/aft-bridge": "0.31.1",
14657
+ "@cortexkit/aft-bridge": "0.33.0",
14560
14658
  "@xterm/headless": "^5.5.0",
14561
14659
  "comment-json": "^5.0.0",
14562
14660
  diff: "^8.0.4",
@@ -14564,12 +14662,12 @@ var package_default = {
14564
14662
  zod: "^4.1.8"
14565
14663
  },
14566
14664
  optionalDependencies: {
14567
- "@cortexkit/aft-darwin-arm64": "0.31.1",
14568
- "@cortexkit/aft-darwin-x64": "0.31.1",
14569
- "@cortexkit/aft-linux-arm64": "0.31.1",
14570
- "@cortexkit/aft-linux-x64": "0.31.1",
14571
- "@cortexkit/aft-win32-arm64": "0.31.1",
14572
- "@cortexkit/aft-win32-x64": "0.31.1"
14665
+ "@cortexkit/aft-darwin-arm64": "0.33.0",
14666
+ "@cortexkit/aft-darwin-x64": "0.33.0",
14667
+ "@cortexkit/aft-linux-arm64": "0.33.0",
14668
+ "@cortexkit/aft-linux-x64": "0.33.0",
14669
+ "@cortexkit/aft-win32-arm64": "0.33.0",
14670
+ "@cortexkit/aft-win32-x64": "0.33.0"
14573
14671
  },
14574
14672
  devDependencies: {
14575
14673
  "@earendil-works/pi-coding-agent": "*",
@@ -14638,6 +14736,19 @@ function formatFlag(enabled) {
14638
14736
  function formatCount(value) {
14639
14737
  return value == null ? "—" : value.toLocaleString("en-US");
14640
14738
  }
14739
+ function formatSemanticIndexStatus(status, stage) {
14740
+ if ((status === "loading" || status === "building") && stage === "fingerprint_change") {
14741
+ return "Rebuilding (model changed)";
14742
+ }
14743
+ return status;
14744
+ }
14745
+ function formatSemanticRefreshing(refreshingCount) {
14746
+ if (!Number.isFinite(refreshingCount) || refreshingCount <= 0)
14747
+ return null;
14748
+ if (refreshingCount > 20)
14749
+ return "Ready (many files refreshing)";
14750
+ return `Ready (${refreshingCount} file(s) refreshing)`;
14751
+ }
14641
14752
  function formatBytes(bytes) {
14642
14753
  if (!Number.isFinite(bytes) || bytes <= 0)
14643
14754
  return "0 B";
@@ -14688,6 +14799,7 @@ function coerceAftStatus(response) {
14688
14799
  files: readOptionalNumber(semanticIndex.files),
14689
14800
  entries_done: readOptionalNumber(semanticIndex.entries_done),
14690
14801
  entries_total: readOptionalNumber(semanticIndex.entries_total),
14802
+ refreshing_count: readNumber(semanticIndex.refreshing_count),
14691
14803
  entries: readOptionalNumber(semanticIndex.entries),
14692
14804
  dimension: readOptionalNumber(semanticIndex.dimension),
14693
14805
  error: readNullableString(semanticIndex.error)
@@ -14730,9 +14842,13 @@ function formatStatusDialogMessage(status) {
14730
14842
  `- trigrams: ${formatCount(status.search_index.trigrams)}`,
14731
14843
  "",
14732
14844
  "Semantic index",
14733
- `- status: ${status.semantic_index.status}`,
14734
- `- entries: ${formatCount(status.semantic_index.entries)}`
14845
+ `- status: ${formatSemanticIndexStatus(status.semantic_index.status, status.semantic_index.stage)}`
14735
14846
  ];
14847
+ const refreshing = formatSemanticRefreshing(status.semantic_index.refreshing_count);
14848
+ if (refreshing) {
14849
+ lines.push(`- ${refreshing}`);
14850
+ }
14851
+ lines.push(`- entries: ${formatCount(status.semantic_index.entries)}`);
14736
14852
  if (status.semantic_index.backend) {
14737
14853
  lines.push(`- backend: ${status.semantic_index.backend}`);
14738
14854
  }
@@ -14779,6 +14895,17 @@ function coerceOptionalInt(v, paramName, min, max) {
14779
14895
  }
14780
14896
  return n;
14781
14897
  }
14898
+ function isEmptyParam(value) {
14899
+ if (value === undefined || value === null)
14900
+ return true;
14901
+ if (typeof value === "string")
14902
+ return value.length === 0;
14903
+ if (Array.isArray(value))
14904
+ return value.length === 0;
14905
+ if (typeof value === "object")
14906
+ return Object.keys(value).length === 0;
14907
+ return false;
14908
+ }
14782
14909
  var LONG_RUNNING_COMMAND_TIMEOUT_MS = {
14783
14910
  callers: 60000,
14784
14911
  trace_to: 60000,
@@ -14787,7 +14914,7 @@ var LONG_RUNNING_COMMAND_TIMEOUT_MS = {
14787
14914
  impact: 60000,
14788
14915
  grep: 60000,
14789
14916
  glob: 60000,
14790
- semantic_search: 45000
14917
+ semantic_search: 60000
14791
14918
  };
14792
14919
  function timeoutForCommand(command) {
14793
14920
  return LONG_RUNNING_COMMAND_TIMEOUT_MS[command];
@@ -15000,7 +15127,10 @@ function renderInner(s, error3, theme, innerWidth) {
15000
15127
  left.push(kv("trigrams", formatCountShort(s.search_index.trigrams), theme));
15001
15128
  left.push(kv("disk", formatBytes(s.disk.trigram_disk_bytes), theme));
15002
15129
  right.push(theme.fg("muted", "Semantic index"));
15003
- right.push(kv("status", colorStatus(s.semantic_index.status, theme), theme));
15130
+ right.push(kv("status", colorStatus(s.semantic_index.status, theme, formatSemanticIndexStatus(s.semantic_index.status, s.semantic_index.stage)), theme));
15131
+ const semanticRefreshing = formatSemanticRefreshing(s.semantic_index.refreshing_count);
15132
+ if (semanticRefreshing)
15133
+ right.push(theme.fg("muted", ` ${semanticRefreshing}`));
15004
15134
  right.push(kv("entries", formatCountShort(s.semantic_index.entries), theme));
15005
15135
  if (s.semantic_index.backend)
15006
15136
  right.push(kv("backend", s.semantic_index.backend, theme));
@@ -15074,24 +15204,24 @@ function formatCompressionStatusRows(compression) {
15074
15204
  appendCompressionScope(rows, "Project", compression.project);
15075
15205
  return rows;
15076
15206
  }
15077
- function colorStatus(status, theme) {
15207
+ function colorStatus(status, theme, label = status) {
15078
15208
  switch (status) {
15079
15209
  case "ready":
15080
15210
  try {
15081
- return theme.fg("success", status);
15211
+ return theme.fg("success", label);
15082
15212
  } catch {
15083
- return theme.fg("accent", status);
15213
+ return theme.fg("accent", label);
15084
15214
  }
15085
15215
  case "loading":
15086
15216
  case "building":
15087
- return theme.fg("warning", status);
15217
+ return theme.fg("warning", label);
15088
15218
  case "failed":
15089
15219
  case "error":
15090
- return theme.fg("error", status);
15220
+ return theme.fg("error", label);
15091
15221
  case "disabled":
15092
- return theme.fg("muted", status);
15222
+ return theme.fg("muted", label);
15093
15223
  default:
15094
- return status;
15224
+ return label;
15095
15225
  }
15096
15226
  }
15097
15227
  function featureBadge(name, enabled, theme) {
@@ -15184,7 +15314,7 @@ function registerStatusCommand(pi, ctx) {
15184
15314
 
15185
15315
  // src/config.ts
15186
15316
  var import_comment_json = __toESM(require_src2(), 1);
15187
- import { existsSync as existsSync6, readFileSync as readFileSync4, renameSync as renameSync4, unlinkSync as unlinkSync3, writeFileSync as writeFileSync3 } from "node:fs";
15317
+ import { existsSync as existsSync6, readFileSync as readFileSync4, renameSync as renameSync4, unlinkSync as unlinkSync4, writeFileSync as writeFileSync3 } from "node:fs";
15188
15318
  import { homedir as homedir6 } from "node:os";
15189
15319
  import { join as join8 } from "node:path";
15190
15320
 
@@ -28815,6 +28945,7 @@ var LspConfigSchema = exports_external.object({
28815
28945
  servers: exports_external.record(exports_external.string().trim().min(1), LspServerEntrySchema).optional(),
28816
28946
  disabled: exports_external.array(exports_external.string().trim().min(1)).optional(),
28817
28947
  python: exports_external.enum(["pyright", "ty", "auto"]).optional(),
28948
+ diagnostics_on_edit: exports_external.boolean().optional(),
28818
28949
  auto_install: exports_external.boolean().optional(),
28819
28950
  grace_days: exports_external.number().int().positive().optional(),
28820
28951
  versions: exports_external.record(exports_external.string().trim().min(1), exports_external.string().trim().min(1)).optional()
@@ -28837,6 +28968,24 @@ var BashFeaturesSchema = exports_external.object({
28837
28968
  long_running_reminder_interval_ms: exports_external.number().int().positive().optional()
28838
28969
  });
28839
28970
  var BashConfigSchema = exports_external.union([exports_external.boolean(), BashFeaturesSchema]);
28971
+ var InspectConfigSchema = exports_external.object({
28972
+ enabled: exports_external.boolean().optional(),
28973
+ tier2_idle_minutes: exports_external.number().min(0).optional(),
28974
+ categories: exports_external.record(exports_external.string(), exports_external.boolean()).optional(),
28975
+ tier2_soft_deadline_ms: exports_external.number().int().positive().optional(),
28976
+ max_drill_down_items: exports_external.number().int().positive().max(100).optional(),
28977
+ duplicates: exports_external.object({
28978
+ lower_bound: exports_external.number().int().positive().optional(),
28979
+ discard_cost: exports_external.number().int().min(0).optional(),
28980
+ anonymize: exports_external.object({
28981
+ variables: exports_external.boolean().optional(),
28982
+ fields: exports_external.boolean().optional(),
28983
+ methods: exports_external.boolean().optional(),
28984
+ types: exports_external.boolean().optional(),
28985
+ literals: exports_external.boolean().optional()
28986
+ }).optional()
28987
+ }).optional()
28988
+ });
28840
28989
  var AftConfigSchema = exports_external.object({
28841
28990
  $schema: exports_external.string().optional(),
28842
28991
  format_on_edit: exports_external.boolean().optional(),
@@ -28849,6 +28998,7 @@ var AftConfigSchema = exports_external.object({
28849
28998
  restrict_to_project_root: exports_external.boolean().optional(),
28850
28999
  search_index: exports_external.boolean().optional(),
28851
29000
  semantic_search: exports_external.boolean().optional(),
29001
+ inspect: InspectConfigSchema.optional(),
28852
29002
  bash: BashConfigSchema.optional(),
28853
29003
  experimental: ExperimentalConfigSchema.optional(),
28854
29004
  lsp: LspConfigSchema.optional(),
@@ -29059,7 +29209,7 @@ ${serialized}` : serialized;
29059
29209
  } catch (err) {
29060
29210
  if (tmpPath) {
29061
29211
  try {
29062
- unlinkSync3(tmpPath);
29212
+ unlinkSync4(tmpPath);
29063
29213
  } catch {}
29064
29214
  }
29065
29215
  if (isWritableMigrationError(err)) {
@@ -29143,6 +29293,9 @@ function mergeLspConfig(base, override) {
29143
29293
  const projectSafe = {};
29144
29294
  if (override?.python !== undefined)
29145
29295
  projectSafe.python = override.python;
29296
+ if (override?.diagnostics_on_edit !== undefined) {
29297
+ projectSafe.diagnostics_on_edit = override.diagnostics_on_edit;
29298
+ }
29146
29299
  const userDisabled = base?.disabled ?? [];
29147
29300
  const lsp = {
29148
29301
  ...base,
@@ -29153,6 +29306,27 @@ function mergeLspConfig(base, override) {
29153
29306
  return;
29154
29307
  return Object.fromEntries(Object.entries(lsp).filter(([, v]) => v !== undefined));
29155
29308
  }
29309
+ function mergeInspectConfig(baseInspect, overrideInspect) {
29310
+ const inspect = {
29311
+ ...baseInspect,
29312
+ ...overrideInspect,
29313
+ duplicates: baseInspect?.duplicates || overrideInspect?.duplicates ? {
29314
+ ...baseInspect?.duplicates,
29315
+ ...overrideInspect?.duplicates,
29316
+ anonymize: baseInspect?.duplicates?.anonymize || overrideInspect?.duplicates?.anonymize ? {
29317
+ ...baseInspect?.duplicates?.anonymize,
29318
+ ...overrideInspect?.duplicates?.anonymize
29319
+ } : undefined
29320
+ } : undefined
29321
+ };
29322
+ if (inspect.duplicates && inspect.duplicates.anonymize === undefined) {
29323
+ delete inspect.duplicates.anonymize;
29324
+ }
29325
+ if (Object.values(inspect).every((value) => value === undefined)) {
29326
+ return;
29327
+ }
29328
+ return Object.fromEntries(Object.entries(inspect).filter(([, value]) => value !== undefined));
29329
+ }
29156
29330
  function mergeBashConfig(baseBash, overrideBash) {
29157
29331
  if (baseBash === undefined && overrideBash === undefined)
29158
29332
  return;
@@ -29209,6 +29383,7 @@ var PROJECT_SAFE_TOP_LEVEL_FIELDS = new Set([
29209
29383
  "validate_on_edit",
29210
29384
  "search_index",
29211
29385
  "semantic_search",
29386
+ "inspect",
29212
29387
  "experimental",
29213
29388
  "bash"
29214
29389
  ]);
@@ -29239,8 +29414,10 @@ function mergeConfigs(base, override) {
29239
29414
  const lsp = mergeLspConfig(base.lsp, override.lsp);
29240
29415
  const experimental = mergeExperimentalConfig(base.experimental, override.experimental);
29241
29416
  const bash = mergeBashConfig(base.bash, override.bash);
29417
+ const inspect = mergeInspectConfig(base.inspect, override.inspect);
29242
29418
  const safeOverride = pickProjectSafeFields(override);
29243
29419
  delete safeOverride.bash;
29420
+ delete safeOverride.inspect;
29244
29421
  return {
29245
29422
  ...base,
29246
29423
  ...safeOverride,
@@ -29248,6 +29425,7 @@ function mergeConfigs(base, override) {
29248
29425
  ...Object.keys(checker).length > 0 ? { checker } : {},
29249
29426
  ...lsp ? { lsp } : {},
29250
29427
  ...bash !== undefined ? { bash } : {},
29428
+ ...inspect !== undefined ? { inspect } : {},
29251
29429
  experimental,
29252
29430
  semantic,
29253
29431
  ...disabledTools.length > 0 ? { disabled_tools: [...new Set(disabledTools)] } : {}
@@ -29299,7 +29477,7 @@ import {
29299
29477
  openSync as openSync3,
29300
29478
  readFileSync as readFileSync5,
29301
29479
  statSync as statSync3,
29302
- unlinkSync as unlinkSync4,
29480
+ unlinkSync as unlinkSync5,
29303
29481
  writeFileSync as writeFileSync4
29304
29482
  } from "node:fs";
29305
29483
  import { homedir as homedir7 } from "node:os";
@@ -29429,7 +29607,7 @@ ${new Date().toISOString()}
29429
29607
  }
29430
29608
  log2(`[lsp] reclaiming install lock for ${lockKey} (owner_pid=${owningPid ?? "unknown"}, alive=${ownerAlive}, age_ms=${age})`);
29431
29609
  try {
29432
- unlinkSync4(lock);
29610
+ unlinkSync5(lock);
29433
29611
  } catch {}
29434
29612
  return tryClaim();
29435
29613
  }
@@ -29466,7 +29644,7 @@ function releaseInstallLock(lockKey) {
29466
29644
  return;
29467
29645
  }
29468
29646
  try {
29469
- unlinkSync4(lock);
29647
+ unlinkSync5(lock);
29470
29648
  } catch (unlinkErr) {
29471
29649
  const code = unlinkErr.code;
29472
29650
  if (code !== "ENOENT") {
@@ -30122,7 +30300,7 @@ function runAutoInstall(projectRoot, config2, fetchImpl = fetch) {
30122
30300
  import { execFileSync as execFileSync2 } from "node:child_process";
30123
30301
  import { createHash as createHash4, randomBytes } from "node:crypto";
30124
30302
  import {
30125
- copyFileSync as copyFileSync3,
30303
+ copyFileSync as copyFileSync4,
30126
30304
  createReadStream as createReadStream2,
30127
30305
  createWriteStream as createWriteStream3,
30128
30306
  existsSync as existsSync8,
@@ -30135,7 +30313,7 @@ import {
30135
30313
  renameSync as renameSync6,
30136
30314
  rmSync as rmSync4,
30137
30315
  statSync as statSync5,
30138
- unlinkSync as unlinkSync5,
30316
+ unlinkSync as unlinkSync6,
30139
30317
  writeFileSync as writeFileSync5
30140
30318
  } from "node:fs";
30141
30319
  import { dirname as dirname4, join as join12, relative as relative2, resolve as resolve2 } from "node:path";
@@ -30497,7 +30675,7 @@ async function downloadFile(url2, destPath, fetchImpl, assetSize, signal) {
30497
30675
  await pipeline3(nodeStream, createWriteStream3(destPath), { signal: timeout.signal });
30498
30676
  } catch (err) {
30499
30677
  try {
30500
- unlinkSync5(destPath);
30678
+ unlinkSync6(destPath);
30501
30679
  } catch {}
30502
30680
  throw err;
30503
30681
  } finally {
@@ -30729,7 +30907,7 @@ async function downloadAndInstall(spec, tag, assets, platform, arch, fetchImpl,
30729
30907
  } catch (err) {
30730
30908
  error2(`[lsp] hash ${spec.id} failed: ${err}`);
30731
30909
  try {
30732
- unlinkSync5(archivePath);
30910
+ unlinkSync6(archivePath);
30733
30911
  } catch {}
30734
30912
  return null;
30735
30913
  }
@@ -30740,7 +30918,7 @@ async function downloadAndInstall(spec, tag, assets, platform, arch, fetchImpl,
30740
30918
  if (previousArchiveSha256 !== archiveSha256) {
30741
30919
  error2(`[lsp] ${spec.id} ${tag}: TOFU sha256 mismatch — refusing install. ` + `Previously installed archive sha256=${previousArchiveSha256}, downloaded sha256=${archiveSha256}. ` + `This means the published release for tag ${tag} changed. Investigate before proceeding.`);
30742
30920
  try {
30743
- unlinkSync5(archivePath);
30921
+ unlinkSync6(archivePath);
30744
30922
  } catch {}
30745
30923
  return null;
30746
30924
  }
@@ -30752,7 +30930,7 @@ async function downloadAndInstall(spec, tag, assets, platform, arch, fetchImpl,
30752
30930
  return null;
30753
30931
  } finally {
30754
30932
  try {
30755
- unlinkSync5(archivePath);
30933
+ unlinkSync6(archivePath);
30756
30934
  } catch {}
30757
30935
  }
30758
30936
  const innerBinaryPath = join12(extractDir, spec.binaryPathInArchive(platform, arch, version2));
@@ -30763,7 +30941,7 @@ async function downloadAndInstall(spec, tag, assets, platform, arch, fetchImpl,
30763
30941
  const targetBinary = ghBinaryPath(spec, platform);
30764
30942
  mkdirSync8(dirname4(targetBinary), { recursive: true });
30765
30943
  try {
30766
- copyFileSync3(innerBinaryPath, targetBinary);
30944
+ copyFileSync4(innerBinaryPath, targetBinary);
30767
30945
  if (platform !== "win32") {
30768
30946
  const { chmodSync: chmodSync4 } = await import("node:fs");
30769
30947
  chmodSync4(targetBinary, 493);
@@ -30952,13 +31130,18 @@ function sendIgnoredMessage(client, sessionId, text) {
30952
31130
  }
30953
31131
  }
30954
31132
  async function readWarnedTools(bridge) {
31133
+ let resp;
31134
+ try {
31135
+ resp = await bridge.send("db_get_state", { key: "warned_tools" });
31136
+ } catch {
31137
+ return null;
31138
+ }
31139
+ if (resp.success === false)
31140
+ return null;
31141
+ const value = resp.data?.value;
31142
+ if (typeof value !== "string")
31143
+ return {};
30955
31144
  try {
30956
- const resp = await bridge.send("db_get_state", { key: "warned_tools" });
30957
- if (resp.success === false)
30958
- return {};
30959
- const value = resp.data?.value;
30960
- if (typeof value !== "string")
30961
- return {};
30962
31145
  const parsed = JSON.parse(value);
30963
31146
  if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
30964
31147
  return {};
@@ -30967,12 +31150,16 @@ async function readWarnedTools(bridge) {
30967
31150
  return {};
30968
31151
  }
30969
31152
  }
30970
- async function hasWarnedFor(bridge, key) {
31153
+ async function warnedStatus(bridge, key) {
30971
31154
  const warned = await readWarnedTools(bridge);
30972
- return warned[key] === true || typeof warned[key] === "string";
31155
+ if (warned === null)
31156
+ return "unknown";
31157
+ return warned[key] === true || typeof warned[key] === "string" ? "warned" : "fresh";
30973
31158
  }
30974
31159
  async function recordWarning(bridge, key) {
30975
31160
  const warned = await readWarnedTools(bridge);
31161
+ if (warned === null)
31162
+ return;
30976
31163
  warned[key] = true;
30977
31164
  try {
30978
31165
  await bridge.send("db_set_state", {
@@ -31021,7 +31208,7 @@ async function deliverConfigureWarnings(opts, warnings) {
31021
31208
  return;
31022
31209
  for (const warning of warnings) {
31023
31210
  const key = warningKey(warning, opts.projectRoot);
31024
- if (await hasWarnedFor(opts.bridge, key))
31211
+ if (await warnedStatus(opts.bridge, key) !== "fresh")
31025
31212
  continue;
31026
31213
  if (!sendIgnoredMessage(opts.client, opts.sessionId, formatConfigureWarning(warning)))
31027
31214
  continue;
@@ -31289,24 +31476,6 @@ function groupByFile(items, getFile) {
31289
31476
  });
31290
31477
  return groups;
31291
31478
  }
31292
- function distinctCount(values) {
31293
- return new Set(values.filter((value) => Boolean(value))).size;
31294
- }
31295
- function severityBadge(theme, severity) {
31296
- const label = severity === "information" ? "info" : severity;
31297
- switch (severity) {
31298
- case "error":
31299
- return theme.fg("error", `[${label}]`);
31300
- case "warning":
31301
- return theme.fg("warning", `[${label}]`);
31302
- case "information":
31303
- return theme.fg("accent", `[${label}]`);
31304
- case "hint":
31305
- return theme.fg("muted", `[${label}]`);
31306
- default:
31307
- return theme.fg("muted", `[${label}]`);
31308
- }
31309
- }
31310
31479
  function formatTimestamp(value) {
31311
31480
  if (typeof value === "string" && value.length > 0)
31312
31481
  return value;
@@ -31531,9 +31700,9 @@ function registerAstTools(pi, ctx, surface) {
31531
31700
  pattern: params.pattern,
31532
31701
  lang: params.lang
31533
31702
  };
31534
- if (params.paths !== undefined)
31703
+ if (!isEmptyParam(params.paths))
31535
31704
  req.paths = params.paths;
31536
- if (params.globs !== undefined)
31705
+ if (!isEmptyParam(params.globs))
31537
31706
  req.globs = params.globs;
31538
31707
  if (params.contextLines !== undefined)
31539
31708
  req.context_lines = params.contextLines;
@@ -31561,9 +31730,9 @@ function registerAstTools(pi, ctx, surface) {
31561
31730
  rewrite: params.rewrite,
31562
31731
  lang: params.lang
31563
31732
  };
31564
- if (params.paths !== undefined)
31733
+ if (!isEmptyParam(params.paths))
31565
31734
  req.paths = params.paths;
31566
- if (params.globs !== undefined)
31735
+ if (!isEmptyParam(params.globs))
31567
31736
  req.globs = params.globs;
31568
31737
  req.dry_run = params.dryRun === true;
31569
31738
  const response = await callBridge(bridge, "ast_replace", req, extCtx);
@@ -32509,6 +32678,10 @@ function formatDiffForPi(oldContent, newContent, contextLines = DEFAULT_CONTEXT_
32509
32678
  }
32510
32679
 
32511
32680
  // src/tools/hoisted.ts
32681
+ var DIAGNOSTICS_PARAM_DESCRIPTION = "When true, wait up to 3 seconds for fresh LSP diagnostics on the edited file and include them in the result. Defaults to the configured `lsp.diagnostics_on_edit` value (false unless configured); per-call true/false overrides. Use aft_inspect to check diagnostics across a batch of edits or before tests/commits.";
32682
+ function diagnosticsOnEditDefault(ctx) {
32683
+ return ctx.config.lsp?.diagnostics_on_edit ?? false;
32684
+ }
32512
32685
  function containsPath(parent, child) {
32513
32686
  const rel = relative3(parent, child);
32514
32687
  return rel === "" || !rel.startsWith("..") && !isAbsolute2(rel);
@@ -32575,7 +32748,8 @@ var WriteParams = Type6.Object({
32575
32748
  filePath: Type6.String({
32576
32749
  description: "Path to the file to write (absolute or relative to project root)"
32577
32750
  }),
32578
- content: Type6.String({ description: "Full file contents to write" })
32751
+ content: Type6.String({ description: "Full file contents to write" }),
32752
+ diagnostics: Type6.Optional(Type6.Boolean({ description: DIAGNOSTICS_PARAM_DESCRIPTION }))
32579
32753
  });
32580
32754
  var EditParams = Type6.Object({
32581
32755
  filePath: Type6.String({
@@ -32587,7 +32761,8 @@ var EditParams = Type6.Object({
32587
32761
  occurrence: optionalInt(0, Number.MAX_SAFE_INTEGER),
32588
32762
  appendContent: Type6.Optional(Type6.String({
32589
32763
  description: "Append text to the end of the file (creates the file if missing, parent dirs auto-created). When set, oldString/newString are ignored."
32590
- }))
32764
+ })),
32765
+ diagnostics: Type6.Optional(Type6.Boolean({ description: DIAGNOSTICS_PARAM_DESCRIPTION }))
32591
32766
  });
32592
32767
  var GrepParams = Type6.Object({
32593
32768
  pattern: Type6.String({ description: "Regex pattern to search for" }),
@@ -32638,8 +32813,8 @@ function registerHoistedTools(pi, ctx, surface) {
32638
32813
  pi.registerTool({
32639
32814
  name: "write",
32640
32815
  label: "write",
32641
- description: "Write a file atomically with per-file backup, optional auto-format, and inline LSP diagnostics. Parent directories are created automatically. Overwrites existing files. Uses `filePath` (not `path`).",
32642
- promptSnippet: "Create or overwrite files (uses filePath; auto-formats; returns LSP diagnostics inline)",
32816
+ description: "Write a file atomically with per-file backup and optional auto-format. Parent directories are created automatically. Overwrites existing files. Uses `filePath` (not `path`). Edits return as soon as the write completes unless `lsp.diagnostics_on_edit` or a per-call `diagnostics: true` requests legacy sync-wait behavior. Call `aft_inspect` afterward to check diagnostics across a batch of edits.",
32817
+ promptSnippet: "Create or overwrite files (uses filePath; auto-formats; diagnostics follow lsp.diagnostics_on_edit unless overridden)",
32643
32818
  promptGuidelines: ["Use write only for new files or complete rewrites."],
32644
32819
  parameters: WriteParams,
32645
32820
  async execute(_toolCallId, params, _signal, _onUpdate, extCtx) {
@@ -32650,8 +32825,8 @@ function registerHoistedTools(pi, ctx, surface) {
32650
32825
  const response = await callBridge(bridge, "write", {
32651
32826
  file: params.filePath,
32652
32827
  content: params.content,
32653
- diagnostics: true,
32654
- include_diff: true
32828
+ diagnostics: params.diagnostics ?? diagnosticsOnEditDefault(ctx),
32829
+ include_diff_content: true
32655
32830
  }, extCtx);
32656
32831
  return buildMutationResult(params.filePath, response);
32657
32832
  },
@@ -32667,8 +32842,8 @@ function registerHoistedTools(pi, ctx, surface) {
32667
32842
  pi.registerTool({
32668
32843
  name: "edit",
32669
32844
  label: "edit",
32670
- description: "Find-and-replace edit with progressive fuzzy matching (handles whitespace and Unicode drift). Uses `filePath`, `oldString`, `newString`. Errors on multiple matches — use `occurrence` to pick one, or `replaceAll: true`. Always returns LSP diagnostics inline.",
32671
- promptSnippet: "Targeted find-and-replace (uses filePath/oldString/newString; occurrence or replaceAll for disambiguation; fuzzy whitespace matching). Pass appendContent to append to a file (creates if missing).",
32845
+ description: "Find-and-replace edit with progressive fuzzy matching (handles whitespace and Unicode drift). Uses `filePath`, `oldString`, `newString`. Errors on multiple matches — use `occurrence` to pick one, or `replaceAll: true`. Edits return as soon as the write completes unless `lsp.diagnostics_on_edit` or a per-call `diagnostics: true` requests legacy sync-wait behavior. Call `aft_inspect` afterward to check diagnostics across a batch of edits.",
32846
+ promptSnippet: "Targeted find-and-replace (uses filePath/oldString/newString; occurrence or replaceAll for disambiguation; fuzzy whitespace matching). Pass appendContent to append to a file (creates if missing). Diagnostics follow lsp.diagnostics_on_edit unless overridden.",
32672
32847
  promptGuidelines: [
32673
32848
  "Prefer edit over write when changing part of an existing file.",
32674
32849
  "Include enough surrounding context in oldString to make the match unique, or set replaceAll/occurrence explicitly.",
@@ -32685,8 +32860,8 @@ function registerHoistedTools(pi, ctx, surface) {
32685
32860
  op: "append",
32686
32861
  file: params.filePath,
32687
32862
  append_content: params.appendContent,
32688
- diagnostics: true,
32689
- include_diff: true
32863
+ diagnostics: params.diagnostics ?? diagnosticsOnEditDefault(ctx),
32864
+ include_diff_content: true
32690
32865
  };
32691
32866
  const response2 = await callBridge(bridge, "edit_match", req2, extCtx);
32692
32867
  return buildMutationResult(params.filePath, response2);
@@ -32695,8 +32870,8 @@ function registerHoistedTools(pi, ctx, surface) {
32695
32870
  file: params.filePath,
32696
32871
  match: params.oldString ?? "",
32697
32872
  replacement: params.newString ?? "",
32698
- diagnostics: true,
32699
- include_diff: true
32873
+ diagnostics: params.diagnostics ?? diagnosticsOnEditDefault(ctx),
32874
+ include_diff_content: true
32700
32875
  };
32701
32876
  if (params.replaceAll === true)
32702
32877
  req.replace_all = true;
@@ -32765,15 +32940,6 @@ function buildMutationResult(filePath, response) {
32765
32940
  }
32766
32941
  const summaryHeader = replacements !== undefined ? `Edited ${filePath} (+${additions}/-${deletions}, ${replacements} replacement${replacements === 1 ? "" : "s"})` : `Wrote ${filePath} (+${additions}/-${deletions})`;
32767
32942
  let text = summaryHeader;
32768
- if (diffText)
32769
- text += `
32770
-
32771
- ${diffText}`;
32772
- if (truncated) {
32773
- text += `
32774
-
32775
- (diff truncated — file too large to include before/after content)`;
32776
- }
32777
32943
  if (noOp) {
32778
32944
  text += `
32779
32945
 
@@ -33053,106 +33219,234 @@ function registerImportTools(pi, ctx) {
33053
33219
  });
33054
33220
  }
33055
33221
 
33056
- // src/tools/lsp.ts
33057
- import { StringEnum as StringEnum3 } from "@earendil-works/pi-ai";
33222
+ // src/tools/inspect.ts
33058
33223
  import { Type as Type8 } from "typebox";
33059
- var LspDiagnosticsParams = Type8.Object({
33060
- filePath: Type8.Optional(Type8.String({ description: "File to get diagnostics for (mutually exclusive with directory)" })),
33061
- directory: Type8.Optional(Type8.String({
33062
- description: "Directory to get diagnostics for (mutually exclusive with filePath)"
33224
+ var InspectParams = Type8.Object({
33225
+ sections: Type8.Optional(Type8.Union([Type8.String(), Type8.Array(Type8.String())], {
33226
+ description: "Categories to include in detailed drill-down (e.g. 'todos' or ['todos', 'dead_code']). Use 'all' for every active category. Omit for summary-only mode."
33063
33227
  })),
33064
- severity: Type8.Optional(StringEnum3(["error", "warning", "information", "hint", "all"], {
33065
- description: "Filter by severity (default: all)"
33228
+ scope: Type8.Optional(Type8.Union([Type8.String(), Type8.Array(Type8.String())], {
33229
+ description: "Restrict scan/results to paths under this scope (file or directory, absolute or relative to project root). Tier 1 scopes the scan; Tier 2 scans project-wide and applies scope as a result filter."
33066
33230
  })),
33067
- waitMs: Type8.Optional(Type8.Number({
33068
- description: "Wait N ms for fresh diagnostics (max 10000, default: 0)"
33231
+ topK: Type8.Optional(Type8.Integer({
33232
+ minimum: 1,
33233
+ maximum: 100,
33234
+ default: 20,
33235
+ description: "Max drill-down items per category. Default 20, max 100."
33069
33236
  }))
33070
33237
  });
33071
- function buildDiagnosticsSections(payload, theme) {
33072
- const response = asRecord2(payload);
33073
- if (!response)
33074
- return [theme.fg("muted", "No diagnostics available.")];
33075
- const diagnostics = asRecords(response.diagnostics);
33076
- const total = asNumber(response.total) ?? diagnostics.length;
33077
- const filesWithErrors = asNumber(response.files_with_errors) ?? distinctCount(diagnostics.filter((diag) => asString(diag.severity) === "error").map((diag) => asString(diag.file)));
33078
- const filesCount = distinctCount(diagnostics.map((diag) => asString(diag.file)));
33079
- const sections = [
33080
- `${theme.fg(total > 0 ? "warning" : "success", `${total} diagnostic${total === 1 ? "" : "s"}`)} ${theme.fg("muted", `across ${filesCount} file${filesCount === 1 ? "" : "s"}, ${filesWithErrors} error file${filesWithErrors === 1 ? "" : "s"}`)}`
33081
- ];
33082
- if (diagnostics.length === 0) {
33083
- sections.push(theme.fg("muted", "No diagnostics found."));
33084
- return sections;
33238
+ var TIER2_INSPECT_CATEGORIES = new Set(["dead_code", "unused_exports", "duplicates"]);
33239
+ var INSPECT_TIER2_RUN_TIMEOUT_MS = 5 * 60000;
33240
+ var INSPECT_TIER2_MIN_TRIGGER_INTERVAL_MS = 4 * 60000;
33241
+ var runningTier2Categories = new WeakMap;
33242
+ var lastTier2TriggerAtByBridge = new WeakMap;
33243
+ function normalizeStringOrArray(value) {
33244
+ return isEmptyParam(value) ? undefined : value;
33245
+ }
33246
+ function validateOptionalTopK(value) {
33247
+ if (value === undefined || value === null || value === "")
33248
+ return;
33249
+ if (typeof value !== "number" || !Number.isInteger(value)) {
33250
+ throw new Error("topK must be an integer between 1 and 100");
33085
33251
  }
33086
- const grouped = groupByFile(diagnostics, (diag) => asString(diag.file));
33087
- for (const [file2, fileDiagnostics] of grouped.entries()) {
33088
- const lines = [theme.fg("accent", shortenPath(file2))];
33089
- fileDiagnostics.forEach((diagnostic) => {
33090
- const severity = asString(diagnostic.severity) ?? "information";
33091
- const line = asNumber(diagnostic.line) ?? 0;
33092
- const column = asNumber(diagnostic.column) ?? 0;
33093
- const code = asString(diagnostic.code);
33094
- const message = asString(diagnostic.message) ?? "(no message)";
33095
- const location = `${line}:${column}`;
33096
- lines.push(` ${severityBadge(theme, severity)} ${location}${code ? ` ${theme.fg("muted", code)}` : ""} ${message}`);
33097
- });
33098
- sections.push(lines.join(`
33099
- `));
33252
+ if (value < 1 || value > 100) {
33253
+ throw new Error("topK must be between 1 and 100");
33254
+ }
33255
+ return value;
33256
+ }
33257
+ function diagnosticsServerSummary(section) {
33258
+ const pending = Array.isArray(section.servers_pending) ? section.servers_pending.filter((item) => typeof item === "string") : [];
33259
+ const notInstalled = Array.isArray(section.servers_not_installed) ? section.servers_not_installed.filter((item) => typeof item === "string") : [];
33260
+ const parts = [];
33261
+ if (pending.length > 0)
33262
+ parts.push(`pending: ${pending.join(", ")}`);
33263
+ if (notInstalled.length > 0)
33264
+ parts.push(`not installed: ${notInstalled.join(", ")}`);
33265
+ return parts.length > 0 ? parts.join("; ") : "none reported";
33266
+ }
33267
+ function diagnosticsSummaryPart(summary) {
33268
+ const section = asRecord2(summary?.diagnostics);
33269
+ if (!section)
33270
+ return;
33271
+ const errors3 = asNumber(section.errors);
33272
+ const warnings = asNumber(section.warnings);
33273
+ const info = asNumber(section.info);
33274
+ const hints = asNumber(section.hints);
33275
+ const hasCounts = [errors3, warnings, info, hints].some((value) => value !== undefined);
33276
+ const counts = `${errors3 ?? 0} errors/${warnings ?? 0} warnings/${info ?? 0} info/${hints ?? 0} hints`;
33277
+ const status = asString(section.status);
33278
+ if (status === "pending") {
33279
+ return hasCounts ? `diagnostics ${counts} so far — still pending (servers: ${diagnosticsServerSummary(section)})` : `diagnostics pending (servers: ${diagnosticsServerSummary(section)})`;
33280
+ }
33281
+ if (status === "incomplete") {
33282
+ return hasCounts ? `diagnostics ${counts} (incomplete — servers: ${diagnosticsServerSummary(section)})` : `diagnostics unavailable (status incomplete; servers: ${diagnosticsServerSummary(section)})`;
33283
+ }
33284
+ if (hasCounts) {
33285
+ return `diagnostics ${counts}`;
33286
+ }
33287
+ return;
33288
+ }
33289
+ function diagnosticLocation(diagnostic) {
33290
+ const file2 = asString(diagnostic.file) ?? "(unknown file)";
33291
+ const line = asNumber(diagnostic.line);
33292
+ const column = asNumber(diagnostic.column);
33293
+ if (line === undefined)
33294
+ return file2;
33295
+ if (column === undefined)
33296
+ return `${file2}:${line}`;
33297
+ return `${file2}:${line}:${column}`;
33298
+ }
33299
+ function diagnosticsDetailSection(details) {
33300
+ const diagnostics = asRecords(details?.diagnostics);
33301
+ if (diagnostics.length === 0)
33302
+ return;
33303
+ const lines = ["diagnostics"];
33304
+ for (const diagnostic of diagnostics) {
33305
+ const severity = asString(diagnostic.severity) ?? "information";
33306
+ const message = asString(diagnostic.message) ?? "(no message)";
33307
+ const source = asString(diagnostic.source);
33308
+ const suffix = source ? ` [${source}]` : "";
33309
+ lines.push(`- ${diagnosticLocation(diagnostic)} ${severity} ${message}${suffix}`);
33310
+ }
33311
+ return lines.join(`
33312
+ `);
33313
+ }
33314
+ function countFrom(summary, key) {
33315
+ const section = asRecord2(summary?.[key]);
33316
+ return asNumber(section?.count);
33317
+ }
33318
+ function tier2SummaryPart(summary, key, label) {
33319
+ const section = asRecord2(summary?.[key]);
33320
+ const count = asNumber(section?.count);
33321
+ if (count !== undefined)
33322
+ return `${label} ${count}`;
33323
+ const status = asString(section?.status);
33324
+ return `${label} ${status ?? "unavailable"}`;
33325
+ }
33326
+ function tier2RefreshCategories(response) {
33327
+ const scannerState = asRecord2(response.scanner_state);
33328
+ const categories = new Set;
33329
+ for (const key of ["pending_categories", "stale_categories"]) {
33330
+ const values = scannerState?.[key];
33331
+ if (!Array.isArray(values))
33332
+ continue;
33333
+ for (const category of values) {
33334
+ if (typeof category === "string" && TIER2_INSPECT_CATEGORIES.has(category)) {
33335
+ categories.add(category);
33336
+ }
33337
+ }
33100
33338
  }
33339
+ return [...categories];
33340
+ }
33341
+ function runPendingTier2Categories(bridge, categories, extCtx) {
33342
+ const now = Date.now();
33343
+ const running = runningTier2Categories.get(bridge) ?? new Set;
33344
+ const lastTriggerAt = lastTier2TriggerAtByBridge.get(bridge) ?? new Map;
33345
+ const toRun = categories.filter((category) => {
33346
+ if (running.has(category))
33347
+ return false;
33348
+ const previousTriggerAt = lastTriggerAt.get(category);
33349
+ return previousTriggerAt === undefined || previousTriggerAt + INSPECT_TIER2_MIN_TRIGGER_INTERVAL_MS <= now;
33350
+ });
33351
+ if (toRun.length === 0)
33352
+ return;
33353
+ for (const category of toRun) {
33354
+ running.add(category);
33355
+ lastTriggerAt.set(category, now);
33356
+ }
33357
+ runningTier2Categories.set(bridge, running);
33358
+ lastTier2TriggerAtByBridge.set(bridge, lastTriggerAt);
33359
+ callBridge(bridge, "inspect_tier2_run", { categories: toRun }, extCtx, {
33360
+ transportTimeoutMs: INSPECT_TIER2_RUN_TIMEOUT_MS
33361
+ }).catch(() => {}).finally(() => {
33362
+ const active = runningTier2Categories.get(bridge);
33363
+ if (!active)
33364
+ return;
33365
+ for (const category of toRun) {
33366
+ active.delete(category);
33367
+ }
33368
+ if (active.size === 0) {
33369
+ runningTier2Categories.delete(bridge);
33370
+ }
33371
+ });
33372
+ }
33373
+ function buildInspectSections(payload, theme) {
33374
+ const response = asRecord2(payload);
33375
+ if (!response)
33376
+ return [theme.fg("muted", "No inspect snapshot available.")];
33377
+ const summary = asRecord2(response.summary);
33378
+ const metrics = asRecord2(summary?.metrics);
33379
+ const scannerState = asRecord2(response.scanner_state);
33380
+ const stale = Array.isArray(scannerState?.stale_categories) ? scannerState.stale_categories.length : 0;
33381
+ const pending = Array.isArray(scannerState?.pending_categories) ? scannerState.pending_categories.length : 0;
33382
+ const parts = [
33383
+ `todos ${countFrom(summary, "todos") ?? 0}`,
33384
+ diagnosticsSummaryPart(summary),
33385
+ `metrics ${asNumber(metrics?.files) ?? 0} files/${asNumber(metrics?.symbols) ?? 0} symbols`,
33386
+ tier2SummaryPart(summary, "dead_code", "dead code"),
33387
+ tier2SummaryPart(summary, "unused_exports", "unused exports"),
33388
+ tier2SummaryPart(summary, "duplicates", "duplicates")
33389
+ ].filter((part) => Boolean(part));
33390
+ const sections = [theme.fg("accent", parts.join(" · "))];
33391
+ if (stale > 0 || pending > 0) {
33392
+ sections.push(theme.fg("warning", `scanner state: ${stale} stale · ${pending} pending`));
33393
+ }
33394
+ const details = asRecord2(response.details);
33395
+ if (details) {
33396
+ const names = Object.keys(details);
33397
+ sections.push(names.length > 0 ? `details: ${names.join(", ")}` : theme.fg("muted", "No drill-down details returned."));
33398
+ const diagnosticsDetails = diagnosticsDetailSection(details);
33399
+ if (diagnosticsDetails)
33400
+ sections.push(diagnosticsDetails);
33401
+ }
33402
+ const text = asString(response.text);
33403
+ if (text)
33404
+ sections.push(text);
33101
33405
  return sections;
33102
33406
  }
33103
- function renderDiagnosticsCall(args, theme, context) {
33104
- const target = args.filePath ?? args.directory;
33105
- const summary = [
33106
- target ? accentPath(theme, target) : undefined,
33107
- args.severity ? theme.fg("toolOutput", args.severity) : undefined
33108
- ].filter(Boolean).join(" ");
33109
- return renderToolCall("lsp diagnostics", summary, theme, context);
33407
+ function renderInspectCall(args, theme, context) {
33408
+ const sections = Array.isArray(args.sections) ? `${args.sections.length} sections` : args.sections;
33409
+ const scope = Array.isArray(args.scope) ? `${args.scope.length} scopes` : args.scope;
33410
+ const summary = [sections, scope, args.topK ? `topK=${args.topK}` : undefined].filter(Boolean).join(" ");
33411
+ return renderToolCall("inspect", summary ? theme.fg("toolOutput", summary) : undefined, theme, context);
33110
33412
  }
33111
- function renderDiagnosticsResult(result, theme, context) {
33413
+ function renderInspectResult(result, theme, context) {
33112
33414
  if (context.isError)
33113
- return renderErrorResult(result, "lsp diagnostics failed", theme, context);
33114
- return renderSections(buildDiagnosticsSections(extractStructuredPayload(result), theme), context);
33415
+ return renderErrorResult(result, "inspect failed", theme, context);
33416
+ return renderSections(buildInspectSections(extractStructuredPayload(result), theme), context);
33115
33417
  }
33116
- function registerLspTools(pi, ctx) {
33418
+ function registerInspectTool(pi, ctx) {
33117
33419
  pi.registerTool({
33118
- name: "lsp_diagnostics",
33119
- label: "lsp diagnostics",
33120
- description: "On-demand LSP file/scope check. NOT a project-wide type checkeruse `tsc --noEmit`, `cargo check`, `pyright` etc. for full coverage.\n\nHonesty: `total: 0` is only clean when `complete: true` AND `lsp_servers_used[].status` includes `pull_ok`. Empty `lsp_servers_used`, or any `binary_not_installed`/`spawn_failed`/`no_root_marker`/`push_only` without diagnostics means the file wasn't actually checked say so, don't report 'clean'. For per-server breakdown run `npx @cortexkit/aft doctor lsp <filePath>`.",
33121
- parameters: LspDiagnosticsParams,
33420
+ name: "aft_inspect",
33421
+ label: "inspect",
33422
+ description: "Codebase health snapshot. One call returns summary stats for: TODOs, diagnostics, file/symbol metrics, dead code, unused exports, code duplicates. Pass `sections` for per-category drill-down details.\n\n" + "Categories run in tiers Tier 1 (todos, metrics) return synchronously from cache. Tier 2 (dead_code, unused_exports, duplicates) run asynchronously on demand: when a call sees cold `pending_categories: [...]` or stale `stale_categories: [...]`, Pi quietly starts a background Tier 2 warmup (deduped while in-flight and rate-limited per category to at most once every 4 minutes, matching OpenCode's default idle window). The current call may still return pending results while the cache warms; a later call can use cached data.\n\n" + `Use when: starting work on unfamiliar code, after multi-edit batches to check diagnostics, before a refactor, before review, or to verify cleanup completeness.
33423
+
33424
+ ` + "Treat `dead_code` as a hint, not proof: reachability is call-based, so symbols reached only via method dispatch or referenced only in type position may be false positives — verify before deleting.",
33425
+ parameters: InspectParams,
33122
33426
  async execute(_toolCallId, params, _signal, _onUpdate, extCtx) {
33123
- const hasFile = typeof params.filePath === "string" && params.filePath.length > 0;
33124
- const hasDir = typeof params.directory === "string" && params.directory.length > 0;
33125
- if (hasFile && hasDir) {
33126
- throw new Error("'filePath' and 'directory' are mutually exclusive — provide one or neither");
33127
- }
33128
33427
  const bridge = bridgeFor(ctx, extCtx.cwd);
33129
- const req = {};
33130
- if (hasFile)
33131
- req.file = params.filePath;
33132
- if (hasDir)
33133
- req.directory = params.directory;
33134
- if (params.severity !== undefined)
33135
- req.severity = params.severity;
33136
- if (params.waitMs !== undefined)
33137
- req.wait_ms = params.waitMs;
33138
- const response = await callBridge(bridge, "lsp_diagnostics", req, extCtx);
33139
- return textResult(JSON.stringify(response, null, 2));
33428
+ const sections = normalizeStringOrArray(params.sections);
33429
+ const scope = normalizeStringOrArray(params.scope);
33430
+ const topK = validateOptionalTopK(params.topK);
33431
+ const response = await callBridge(bridge, "inspect", { sections, scope, topK }, extCtx);
33432
+ runPendingTier2Categories(bridge, tier2RefreshCategories(response), extCtx);
33433
+ return textResult(response.text ?? JSON.stringify(response, null, 2), response);
33140
33434
  },
33141
33435
  renderCall(args, theme, context) {
33142
- return renderDiagnosticsCall(args, theme, context);
33436
+ return renderInspectCall(args, theme, context);
33143
33437
  },
33144
33438
  renderResult(result, _options, theme, context) {
33145
- return renderDiagnosticsResult(result, theme, context);
33439
+ return renderInspectResult(result, theme, context);
33146
33440
  }
33147
33441
  });
33148
33442
  }
33149
33443
 
33150
33444
  // src/tools/navigate.ts
33151
- import { StringEnum as StringEnum4 } from "@earendil-works/pi-ai";
33445
+ import { StringEnum as StringEnum3 } from "@earendil-works/pi-ai";
33152
33446
  import { Type as Type9 } from "typebox";
33153
33447
  function navigateParamsSchema() {
33154
33448
  return Type9.Object({
33155
- op: StringEnum4(["call_tree", "callers", "trace_to", "trace_to_symbol", "impact", "trace_data"], {
33449
+ op: StringEnum3(["call_tree", "callers", "trace_to", "trace_to_symbol", "impact", "trace_data"], {
33156
33450
  description: "Navigation operation"
33157
33451
  }),
33158
33452
  filePath: Type9.String({
@@ -33309,7 +33603,7 @@ function renderNavigateCall(args, theme, context) {
33309
33603
  theme.fg("toolOutput", args.symbol),
33310
33604
  args.toSymbol ? theme.fg("toolOutput", `→ ${args.toSymbol}`) : undefined
33311
33605
  ].filter(Boolean).join(" ");
33312
- return renderToolCall("navigate", summary, theme, context);
33606
+ return renderToolCall("callgraph", summary, theme, context);
33313
33607
  }
33314
33608
  function renderNavigateResult(result, args, theme, context) {
33315
33609
  if (context.isError)
@@ -33318,9 +33612,9 @@ function renderNavigateResult(result, args, theme, context) {
33318
33612
  }
33319
33613
  function registerNavigateTool(pi, ctx) {
33320
33614
  pi.registerTool({
33321
- name: "aft_navigate",
33322
- label: "navigate",
33323
- description: "Navigate code structure across files using call graph analysis. All ops require both `filePath` and `symbol`. Use `call_tree` for what a function calls, `callers` for call sites, `trace_to` for entry points, `trace_to_symbol` for a path from one symbol to another (requires `toSymbol`; if ambiguous, the error returns candidate files — retry with `toFile`), `impact` for blast radius, `trace_data` to follow a value.",
33615
+ name: "aft_callgraph",
33616
+ label: "callgraph",
33617
+ description: "Answer code-relationship questions from a real call graph — instead of grep + read chains. Reach for this whenever the question is about how symbols connect. All ops require both `filePath` and `symbol`. Use `callers` for call sites (before renaming/signature changes), `impact` for blast radius (what breaks if a symbol changes), `call_tree` for what a function calls, `trace_to` for how execution reaches a symbol from entry points, `trace_to_symbol` for the shortest path from one symbol to another (requires `toSymbol`; if ambiguous, the error returns candidate files — retry with `toFile`), `trace_data` to follow a value across assignments/params.",
33324
33618
  parameters: navigateParamsSchema(),
33325
33619
  async execute(_toolCallId, params, _signal, _onUpdate, extCtx) {
33326
33620
  if (params.op === "trace_data" && !params.expression) {
@@ -33582,10 +33876,26 @@ Pass a single \`target\`:
33582
33876
  parameters: ZoomParams,
33583
33877
  async execute(_toolCallId, params, _signal, _onUpdate, extCtx) {
33584
33878
  const bridge = bridgeFor(ctx, extCtx.cwd);
33585
- const hasFilePath = typeof params.filePath === "string" && params.filePath.length > 0;
33586
- const hasUrl = typeof params.url === "string" && params.url.length > 0;
33587
- const hasTargets = params.targets !== undefined && params.targets !== null;
33588
- const hasSymbols = params.symbols !== undefined && params.symbols !== null && (typeof params.symbols === "string" ? params.symbols.length > 0 : Array.isArray(params.symbols) && params.symbols.length > 0);
33879
+ const hasTargetsProvided = (t) => {
33880
+ if (isEmptyParam(t))
33881
+ return false;
33882
+ const entryEmpty = (entry) => {
33883
+ if (!entry || typeof entry !== "object")
33884
+ return true;
33885
+ const fp = entry.filePath;
33886
+ const sym = entry.symbol;
33887
+ const fpEmpty = typeof fp !== "string" || fp.length === 0;
33888
+ const symEmpty = typeof sym !== "string" || sym.length === 0;
33889
+ return fpEmpty && symEmpty;
33890
+ };
33891
+ if (Array.isArray(t))
33892
+ return !t.every(entryEmpty);
33893
+ return !entryEmpty(t);
33894
+ };
33895
+ const hasFilePath = !isEmptyParam(params.filePath);
33896
+ const hasUrl = !isEmptyParam(params.url);
33897
+ const hasTargets = hasTargetsProvided(params.targets);
33898
+ const hasSymbols = !isEmptyParam(params.symbols);
33589
33899
  if (hasTargets) {
33590
33900
  if (hasFilePath || hasUrl || hasSymbols) {
33591
33901
  throw new Error("'targets' is mutually exclusive with 'filePath', 'url', and 'symbols'");
@@ -33738,10 +34048,10 @@ function formatOutlineFilesText(response) {
33738
34048
  }
33739
34049
 
33740
34050
  // src/tools/refactor.ts
33741
- import { StringEnum as StringEnum5 } from "@earendil-works/pi-ai";
34051
+ import { StringEnum as StringEnum4 } from "@earendil-works/pi-ai";
33742
34052
  import { Type as Type11 } from "typebox";
33743
34053
  var RefactorParams = Type11.Object({
33744
- op: StringEnum5(["move", "extract", "inline"], { description: "Refactoring operation" }),
34054
+ op: StringEnum4(["move", "extract", "inline"], { description: "Refactoring operation" }),
33745
34055
  filePath: Type11.String({
33746
34056
  description: "Source file (absolute or relative to project root)"
33747
34057
  }),
@@ -33808,14 +34118,23 @@ function registerRefactorTool(pi, ctx) {
33808
34118
  extract: "extract_function",
33809
34119
  inline: "inline_symbol"
33810
34120
  };
34121
+ if ((params.op === "move" || params.op === "inline") && isEmptyParam(params.symbol)) {
34122
+ throw new Error(`'symbol' is required for '${params.op}' op`);
34123
+ }
34124
+ if (params.op === "move" && isEmptyParam(params.destination)) {
34125
+ throw new Error("'destination' is required for 'move' op");
34126
+ }
34127
+ if (params.op === "extract" && isEmptyParam(params.name)) {
34128
+ throw new Error("'name' is required for 'extract' op");
34129
+ }
33811
34130
  const req = { file: params.filePath };
33812
- if (params.symbol !== undefined)
34131
+ if (!isEmptyParam(params.symbol))
33813
34132
  req.symbol = params.symbol;
33814
- if (params.destination !== undefined)
34133
+ if (!isEmptyParam(params.destination))
33815
34134
  req.destination = params.destination;
33816
- if (params.scope !== undefined)
34135
+ if (!isEmptyParam(params.scope))
33817
34136
  req.scope = params.scope;
33818
- if (params.name !== undefined)
34137
+ if (!isEmptyParam(params.name))
33819
34138
  req.name = params.name;
33820
34139
  const startLine = coerceOptionalInt(params.startLine, "startLine", 1, Number.MAX_SAFE_INTEGER);
33821
34140
  const endLine = coerceOptionalInt(params.endLine, "endLine", 1, Number.MAX_SAFE_INTEGER);
@@ -33840,10 +34159,10 @@ function registerRefactorTool(pi, ctx) {
33840
34159
  }
33841
34160
 
33842
34161
  // src/tools/safety.ts
33843
- import { StringEnum as StringEnum6 } from "@earendil-works/pi-ai";
34162
+ import { StringEnum as StringEnum5 } from "@earendil-works/pi-ai";
33844
34163
  import { Type as Type12 } from "typebox";
33845
34164
  var SafetyParams = Type12.Object({
33846
- op: StringEnum6(["undo", "history", "checkpoint", "restore", "list"], {
34165
+ op: StringEnum5(["undo", "history", "checkpoint", "restore", "list"], {
33847
34166
  description: "Safety operation"
33848
34167
  }),
33849
34168
  filePath: Type12.Optional(Type12.String({
@@ -33982,35 +34301,81 @@ function registerSafetyTool(pi, ctx) {
33982
34301
 
33983
34302
  // src/tools/semantic.ts
33984
34303
  import { Type as Type13 } from "typebox";
34304
+ function semanticHonestyNote(response, theme) {
34305
+ const notes = [];
34306
+ if (response.more_available === true)
34307
+ notes.push("more results available");
34308
+ if (response.engine_capped === true)
34309
+ notes.push("enumeration capped");
34310
+ if (response.fully_degraded === true)
34311
+ notes.push("fully degraded");
34312
+ if (response.complete === false)
34313
+ notes.push("partial/incomplete");
34314
+ return notes.length > 0 ? theme.fg("warning", `Search status: ${notes.join("; ")}.`) : undefined;
34315
+ }
33985
34316
  var SearchParams2 = Type13.Object({
33986
34317
  query: Type13.String({
33987
- description: "Concept or capability to find, phrased as a programmer would describe the code. Examples: 'fuzzy match with whitespace tolerance', 'undo backup before edit', 'retry failed network request'."
34318
+ description: "Concept, regex, literal text, filename, or capability to find. Examples: 'fuzzy match with whitespace tolerance', '^export', 'Cargo.lock'."
33988
34319
  }),
33989
- topK: Type13.Optional(Type13.Number({ description: "Maximum number of results (default: 10, max: 100)" }))
34320
+ topK: Type13.Optional(Type13.Integer({
34321
+ minimum: 1,
34322
+ maximum: 100,
34323
+ default: 10,
34324
+ description: "Maximum number of results (default: 10, max: 100)"
34325
+ })),
34326
+ hint: Type13.Optional(Type13.Union([
34327
+ Type13.Literal("regex"),
34328
+ Type13.Literal("literal"),
34329
+ Type13.Literal("semantic"),
34330
+ Type13.Literal("auto")
34331
+ ], {
34332
+ description: "Optional routing hint. Defaults to 'auto'."
34333
+ }))
33990
34334
  });
33991
34335
  function buildSemanticSections(args, payload, theme) {
33992
34336
  const response = asRecord2(payload);
33993
34337
  if (!response)
33994
- return [theme.fg("muted", "No semantic search result.")];
34338
+ return [theme.fg("muted", "No search result.")];
33995
34339
  const status = asString(response.status) ?? "unknown";
34340
+ const semanticStatus = asString(response.semantic_status) ?? status;
34341
+ const interpretedAs = asString(response.interpreted_as) ?? "unknown";
34342
+ const queryKind = asString(response.query_kind);
33996
34343
  const sections = [
33997
- `${theme.fg(status === "ready" ? "success" : "warning", `index: ${status}`)} ${theme.fg("muted", `query=${JSON.stringify(args.query)} topK=${args.topK ?? 10}`)}`
34344
+ `${theme.fg(semanticStatus === "ready" ? "success" : "warning", `semantic: ${semanticStatus}`)} ${theme.fg("muted", `mode=${interpretedAs}${queryKind ? ` kind=${queryKind}` : ""} query=${JSON.stringify(args.query)} topK=${args.topK ?? 10}`)}`
33998
34345
  ];
33999
- if (status !== "ready") {
34346
+ const warnings = Array.isArray(response.warnings) ? response.warnings.filter((warning) => typeof warning === "string") : [];
34347
+ if (warnings.length > 0) {
34348
+ sections.push(warnings.map((warning) => theme.fg("warning", `⚠ ${warning}`)).join(`
34349
+ `));
34350
+ }
34351
+ const honestyNote = semanticHonestyNote(response, theme);
34352
+ if (honestyNote)
34353
+ sections.push(honestyNote);
34354
+ const results = asRecords(response.results);
34355
+ if (status !== "ready" && results.length === 0) {
34000
34356
  sections.push(asString(response.text) ?? theme.fg("muted", "Semantic index is not ready."));
34001
34357
  return sections;
34002
34358
  }
34003
- const results = asRecords(response.results);
34004
34359
  if (results.length === 0) {
34005
- sections.push(theme.fg("muted", "No semantic matches found."));
34360
+ sections.push(theme.fg("muted", "No matches found."));
34006
34361
  return sections;
34007
34362
  }
34008
34363
  const grouped = groupByFile(results, (result) => asString(result.file));
34009
34364
  for (const [file2, fileResults] of grouped.entries()) {
34010
34365
  const lines = [theme.fg("accent", shortenPath(file2))];
34011
34366
  fileResults.forEach((result) => {
34367
+ if (asString(result.kind) === "GrepLine") {
34368
+ const line = asNumber(result.line);
34369
+ const column = asNumber(result.column);
34370
+ const lineText = asString(result.line_text) ?? "";
34371
+ const location2 = line !== undefined ? `${line}${column !== undefined ? `:${column}` : ""}` : "?";
34372
+ lines.push(` ↳ ${theme.fg("muted", `line ${location2}`)} ${lineText}`);
34373
+ return;
34374
+ }
34012
34375
  const score = asNumber(result.score);
34013
34376
  const source = asString(result.source);
34377
+ const kind = asString(result.kind) ?? "symbol";
34378
+ const location = asString(result.location);
34014
34379
  if (source === "lexical") {
34015
34380
  lines.push(` ↳ ${theme.fg("muted", `[lexical match${score !== undefined ? ` — score ${score.toFixed(3)}` : ""}]`)}`);
34016
34381
  const snippet2 = asString(result.snippet);
@@ -34020,10 +34385,14 @@ function buildSemanticSections(args, payload, theme) {
34020
34385
  }
34021
34386
  return;
34022
34387
  }
34388
+ if (kind === "file_summary" || location === "[file summary]") {
34389
+ const summary = asString(result.snippet) ?? asString(result.name) ?? "(no summary)";
34390
+ lines.push(` ↳ ${summary} ${theme.fg("muted", `[file summary${score !== undefined ? ` score ${score.toFixed(3)}` : ""}]`)}`);
34391
+ return;
34392
+ }
34023
34393
  const startLine = asNumber(result.start_line);
34024
34394
  const endLine = asNumber(result.end_line);
34025
34395
  const range = startLine !== undefined ? `${startLine}${endLine && endLine !== startLine ? `-${endLine}` : ""}` : "?";
34026
- const kind = asString(result.kind) ?? "symbol";
34027
34396
  const name = asString(result.name) ?? "(unknown)";
34028
34397
  lines.push(` ↳ ${name} ${theme.fg("muted", `[${kind}] lines ${range}${score !== undefined ? ` score ${score.toFixed(3)}` : ""}`)}`);
34029
34398
  const snippet = asString(result.snippet);
@@ -34038,43 +34407,48 @@ function buildSemanticSections(args, payload, theme) {
34038
34407
  return sections;
34039
34408
  }
34040
34409
  function renderSemanticCall(args, theme, context) {
34041
- return renderToolCall("semantic search", theme.fg("toolOutput", args.query), theme, context);
34410
+ return renderToolCall("search", theme.fg("toolOutput", args.query), theme, context);
34042
34411
  }
34043
34412
  function renderSemanticResult(result, args, theme, context) {
34044
34413
  if (context.isError)
34045
- return renderErrorResult(result, "semantic search failed", theme, context);
34414
+ return renderErrorResult(result, "search failed", theme, context);
34046
34415
  return renderSections(buildSemanticSections(args, extractStructuredPayload(result), theme), context);
34047
34416
  }
34048
34417
  function registerSemanticTool(pi, ctx) {
34049
34418
  pi.registerTool({
34050
34419
  name: "aft_search",
34051
- label: "semantic search",
34420
+ label: "search",
34052
34421
  description: [
34053
- "Find symbols by concept using hybrid semantic + lexical search. Returns ranked code matches with similarity scores and provenance tags.",
34422
+ "Find code with unified semantic, lexical, literal, and regex search. Returns ranked symbol/file results or exact matching lines, with routing metadata.",
34054
34423
  "",
34055
34424
  "When to reach for it:",
34056
34425
  "- Exploring an unfamiliar area: 'where is rate limiting handled', 'how does auth flow work'",
34057
34426
  "- Concept doesn't appear as a literal string: 'retry logic', 'cache invalidation', 'graceful shutdown'",
34058
34427
  "- Filename-shaped concepts: 'the bridge spawn helper', 'the session detection module'",
34059
- "- After 2+ grep attempts that came back empty or noisy",
34428
+ "- Regex-shaped or exact text queries when you want AFT to classify and route automatically",
34060
34429
  "- You know roughly what the function does but not what it's named",
34061
34430
  "",
34062
34431
  "When NOT to use:",
34063
- "- You have an error message or stack trace → use grep",
34432
+ "- You need exhaustive literal enumeration → use grep directly",
34064
34433
  "- You want the file/module structure → use aft_outline",
34065
- "- You're following a call chain → use aft_navigate",
34434
+ "- You're following a call chain → use aft_callgraph",
34066
34435
  "",
34067
- "Each result tags `source` as one of: 'semantic' (embedding match only), 'lexical' (trigram exact-token match the embedding lane missed), or 'hybrid' (both lanes agreed strongest signal)."
34436
+ "Set hint to 'regex', 'literal', 'semantic', or 'auto' to override or document routing intent."
34068
34437
  ].join(`
34069
34438
  `),
34070
34439
  parameters: SearchParams2,
34071
34440
  async execute(_toolCallId, params, _signal, _onUpdate, extCtx) {
34441
+ if (isEmptyParam(params.query) || typeof params.query !== "string" || params.query.trim().length === 0) {
34442
+ throw new Error("semantic_search: invalid params: `query` must be a non-empty string");
34443
+ }
34072
34444
  const bridge = bridgeFor(ctx, extCtx.cwd);
34073
34445
  const req = { query: params.query };
34074
34446
  if (params.topK !== undefined)
34075
34447
  req.top_k = params.topK;
34448
+ if (params.hint !== undefined)
34449
+ req.hint = params.hint;
34076
34450
  const response = await callBridge(bridge, "semantic_search", req, extCtx);
34077
- return textResult(response.text ?? JSON.stringify(response, null, 2));
34451
+ return textResult(response.text ?? JSON.stringify(response, null, 2), response);
34078
34452
  },
34079
34453
  renderCall(args, theme, context) {
34080
34454
  return renderSemanticCall(args, theme, context);
@@ -34086,10 +34460,10 @@ function registerSemanticTool(pi, ctx) {
34086
34460
  }
34087
34461
 
34088
34462
  // src/tools/structure.ts
34089
- import { StringEnum as StringEnum7 } from "@earendil-works/pi-ai";
34463
+ import { StringEnum as StringEnum6 } from "@earendil-works/pi-ai";
34090
34464
  import { Type as Type14 } from "typebox";
34091
34465
  var TransformParams = Type14.Object({
34092
- op: StringEnum7(["add_member", "add_derive", "wrap_try_catch", "add_decorator", "add_struct_tags"], { description: "Transformation operation" }),
34466
+ op: StringEnum6(["add_member", "add_derive", "wrap_try_catch", "add_decorator", "add_struct_tags"], { description: "Transformation operation" }),
34093
34467
  filePath: Type14.String({
34094
34468
  description: "Path to the source file (absolute or relative to project root)"
34095
34469
  }),
@@ -34105,7 +34479,7 @@ var TransformParams = Type14.Object({
34105
34479
  position: Type14.Optional(Type14.String({
34106
34480
  description: "Position hint: 'first', 'last' (default), 'before:name', 'after:name' for add_member"
34107
34481
  })),
34108
- validate: Type14.Optional(StringEnum7(["syntax", "full"], {
34482
+ validate: Type14.Optional(StringEnum6(["syntax", "full"], {
34109
34483
  description: "Validation level (default: syntax)"
34110
34484
  }))
34111
34485
  });
@@ -34221,18 +34595,26 @@ function buildWorkflowHints(opts) {
34221
34595
  const hasZoom = !opts.absentTools.has("aft_zoom");
34222
34596
  const hasGrep = opts.toolSurface !== "minimal" && !opts.absentTools.has(grepName);
34223
34597
  const hasSearch = opts.toolSurface !== "minimal" && opts.semanticEnabled && !opts.absentTools.has("aft_search");
34224
- const hasNavigate = opts.toolSurface === "all" && !opts.absentTools.has("aft_navigate");
34598
+ const hasNavigate = opts.toolSurface === "all" && !opts.absentTools.has("aft_callgraph");
34599
+ const hasInspect = opts.toolSurface !== "minimal" && !opts.absentTools.has("aft_inspect");
34225
34600
  const hasBgBash = opts.bashBackgroundEnabled && !opts.absentTools.has(bashName) && !opts.absentTools.has("bash_status");
34226
34601
  if (hasOutline && hasZoom) {
34227
34602
  sections.push(`**Web/URL access**: \`aft_outline({ target: "<url>" })\` first for structure, then \`aft_zoom({ url: "<url>", symbols: "<heading>" })\` for the specific section.`);
34228
34603
  }
34229
34604
  if (hasOutline && hasZoom && (hasGrep || hasSearch)) {
34230
- const locator = hasGrep ? `\`${grepName}\`` : "`aft_search`";
34231
- sections.push(hasGrep && hasSearch ? `**Code exploration**: For exact identifiers (\`useState\`, function names, env vars), error messages, or path-shaped queries → \`${grepName}\` first. For broad concepts ('where is X handled', 'how does Y work') \`aft_search\`. Then use \`aft_outline\` for structure → \`aft_zoom\` for symbol(s).` : `**Code exploration**: ${locator} to locate → \`aft_outline\` for structure → \`aft_zoom\` for symbol(s).`);
34605
+ if (hasSearch) {
34606
+ const grepFallback = hasGrep ? ` Use \`${grepName}\` directly only when you need exhaustive enumeration of literal text (every TODO, every import of X) without ranking.` : "";
34607
+ sections.push(`**Code exploration**: \`aft_search\` is the primary code-search tool. It auto-routes by query shape — exact identifiers, regex, error messages, natural language all use the same call. Very short queries fall back to literal scans; pass \`hint: "regex"\` / \`hint: "literal"\` / \`hint: "semantic"\` to override routing if needed. Then \`aft_outline\` for structure → \`aft_zoom\` for symbol(s).${grepFallback}`);
34608
+ } else {
34609
+ sections.push(`**Code exploration**: \`${grepName}\` to locate → \`aft_outline\` for structure → \`aft_zoom\` for symbol(s).`);
34610
+ }
34611
+ }
34612
+ if (hasInspect) {
34613
+ sections.push("**Codebase health & diagnostics**: AFT does not surface compile/type errors automatically after edits — pull them with `aft_inspect`. Run it after a batch of edits and before you run tests or commit, when starting in unfamiliar code, or before a refactor/review. One call summarizes diagnostics (compile/type errors), TODOs, metrics, dead code, unused exports, and duplicates; pass `sections` for focused drill-down and `scope` to actively pull diagnostics for a specific file or directory. Its diagnostics are a fast checkpoint, not the authority — a clean `tsc` / `cargo check` / `pyright` run is the real gate. Treat `stale_categories` as a genuine stale-cache signal while an async Tier 2 refresh catches up.");
34232
34614
  }
34233
34615
  if (hasNavigate) {
34234
34616
  sections.push([
34235
- "Use `aft_navigate` instead of grep + read chains for relationship questions:",
34617
+ "Use `aft_callgraph` for code-relationship questions instead of grep + read chains:",
34236
34618
  "- `callers` — find all call sites before changing a function signature",
34237
34619
  "- `impact` — blast radius (which functions/files will need updates)",
34238
34620
  "- `trace_to` — how execution reaches this code from entry points (routes, exports, main)",
@@ -34272,7 +34654,9 @@ function registerWorkflowHints(pi, config2, surface) {
34272
34654
  if (!surface.semantic)
34273
34655
  absent.add("aft_search");
34274
34656
  if (!surface.navigate)
34275
- absent.add("aft_navigate");
34657
+ absent.add("aft_callgraph");
34658
+ if (!surface.inspect)
34659
+ absent.add("aft_inspect");
34276
34660
  if (!surface.hoistGrep)
34277
34661
  absent.add("grep");
34278
34662
  if (!surface.hoistBash) {
@@ -34336,17 +34720,17 @@ var PLUGIN_VERSION = (() => {
34336
34720
  return "0.0.0";
34337
34721
  }
34338
34722
  })();
34339
- var ANNOUNCEMENT_VERSION = "0.31.0";
34723
+ var ANNOUNCEMENT_VERSION = "0.33.0";
34340
34724
  var ANNOUNCEMENT_FEATURES = [
34341
- "`aft_navigate` has a new `trace_to_symbol` op for path-between-symbols queries. One call returns the shortest call path from one function to another with file + line for every hop. Honest errors for ambiguous, missing, or unreachable targets including a candidate list to disambiguate.",
34342
- '`aft_outline target: "<dir>", files: true` returns an indexed file tree with language, symbol count, and byte size per file. Lets agents pick which files to actually open without first reading symbol bodies. Honest truncation (`complete: false` + `walk_truncated`/`unchecked_files`) when the directory exceeds the walk cap.',
34343
- '`aft_zoom` adds `targets` for cross-file batches — pull bodies from different files in one call. Schema breaking change: `symbol` is removed; pass `symbols: "name"` (string) or `symbols: ["a", "b"]` (array). Same shape works for `url` mode so you can grab multiple sections from one URL fetch.',
34344
- "Pi `grep`/`write`/`edit` no longer hang on external paths. The plugin used to nag with an `Allow external directory access?` prompt even under the Pi default (`restrict_to_project_root: false`) which explicitly opts into no restriction. The plugin now defers to Rust without prompting in that case, expands `~/...` paths correctly, and bounds the prompt with a 30s timeout when strict mode is enabled.",
34345
- "Tool descriptions trimmed: ~1.2K agent-facing tokens removed by dropping redundant `Returns:` blocks and compressing `lsp_diagnostics` while preserving its load-bearing honesty guidance."
34725
+ "New `aft_inspect` one call for codebase health: diagnostics, metrics, TODOs, dead code, unused exports, and duplicates.",
34726
+ "Diagnostics now flow through `aft_inspect` (run it after a batch of edits) instead of arriving automatically on every edit.",
34727
+ "`aft_navigate` is renamed to `aft_callgraph`; the Rust call graph now resolves cross-file callers.",
34728
+ "Edits no longer echo the whole file back to the agent much lower token cost per edit.",
34729
+ "Batch of `aft_search` correctness fixes and undo-history/SSRF/Windows hardening."
34346
34730
  ];
34347
34731
  var ANNOUNCEMENT_FOOTER = "Join us on Discord: https://discord.gg/F2uWxjGnU";
34348
34732
  var ALL_ONLY_TOOLS = new Set([
34349
- "aft_navigate",
34733
+ "aft_callgraph",
34350
34734
  "aft_delete",
34351
34735
  "aft_move",
34352
34736
  "aft_transform",
@@ -34420,6 +34804,7 @@ function resolveToolSurface(config2) {
34420
34804
  outline: ok("aft_outline"),
34421
34805
  zoom: ok("aft_zoom"),
34422
34806
  semantic: false,
34807
+ inspect: false,
34423
34808
  navigate: false,
34424
34809
  conflicts: false,
34425
34810
  importTool: false,
@@ -34428,7 +34813,6 @@ function resolveToolSurface(config2) {
34428
34813
  move: false,
34429
34814
  astSearch: false,
34430
34815
  astReplace: false,
34431
- lspDiagnostics: false,
34432
34816
  structure: false,
34433
34817
  refactor: false
34434
34818
  };
@@ -34443,6 +34827,7 @@ function resolveToolSurface(config2) {
34443
34827
  outline: ok("aft_outline"),
34444
34828
  zoom: ok("aft_zoom"),
34445
34829
  semantic: ok("aft_search") && config2.semantic_search === true,
34830
+ inspect: ok("aft_inspect") && config2.inspect?.enabled !== false,
34446
34831
  navigate: false,
34447
34832
  conflicts: ok("aft_conflicts"),
34448
34833
  importTool: ok("aft_import"),
@@ -34451,14 +34836,13 @@ function resolveToolSurface(config2) {
34451
34836
  move: false,
34452
34837
  astSearch: ok("ast_grep_search"),
34453
34838
  astReplace: ok("ast_grep_replace"),
34454
- lspDiagnostics: ok("lsp_diagnostics"),
34455
34839
  structure: false,
34456
34840
  refactor: false
34457
34841
  };
34458
34842
  if (surface === "all") {
34459
34843
  return {
34460
34844
  ...base,
34461
- navigate: allOnly("aft_navigate"),
34845
+ navigate: allOnly("aft_callgraph"),
34462
34846
  delete: allOnly("aft_delete"),
34463
34847
  move: allOnly("aft_move"),
34464
34848
  structure: allOnly("aft_transform"),
@@ -34506,6 +34890,8 @@ async function src_default(pi) {
34506
34890
  Object.assign(configOverrides, resolveLspConfigForConfigure(config2));
34507
34891
  if (config2.semantic !== undefined)
34508
34892
  configOverrides.semantic = config2.semantic;
34893
+ if (config2.inspect !== undefined)
34894
+ configOverrides.inspect = config2.inspect;
34509
34895
  if (config2.max_callgraph_files !== undefined)
34510
34896
  configOverrides.max_callgraph_files = config2.max_callgraph_files;
34511
34897
  if (config2.url_fetch_allow_private !== undefined)
@@ -34673,6 +35059,9 @@ ${lines}
34673
35059
  if (surface.semantic) {
34674
35060
  registerSemanticTool(pi, ctx);
34675
35061
  }
35062
+ if (surface.inspect) {
35063
+ registerInspectTool(pi, ctx);
35064
+ }
34676
35065
  if (surface.navigate) {
34677
35066
  registerNavigateTool(pi, ctx);
34678
35067
  }
@@ -34691,9 +35080,6 @@ ${lines}
34691
35080
  if (surface.delete || surface.move) {
34692
35081
  registerFsTools(pi, ctx, surface);
34693
35082
  }
34694
- if (surface.lspDiagnostics) {
34695
- registerLspTools(pi, ctx);
34696
- }
34697
35083
  if (surface.structure) {
34698
35084
  registerStructureTool(pi, ctx);
34699
35085
  }