@cortexkit/aft-pi 0.32.0 → 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.32.0",
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.32.0",
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.32.0",
14568
- "@cortexkit/aft-darwin-x64": "0.32.0",
14569
- "@cortexkit/aft-linux-arm64": "0.32.0",
14570
- "@cortexkit/aft-linux-x64": "0.32.0",
14571
- "@cortexkit/aft-win32-arm64": "0.32.0",
14572
- "@cortexkit/aft-win32-x64": "0.32.0"
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": "*",
@@ -15216,7 +15314,7 @@ function registerStatusCommand(pi, ctx) {
15216
15314
 
15217
15315
  // src/config.ts
15218
15316
  var import_comment_json = __toESM(require_src2(), 1);
15219
- 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";
15220
15318
  import { homedir as homedir6 } from "node:os";
15221
15319
  import { join as join8 } from "node:path";
15222
15320
 
@@ -28847,6 +28945,7 @@ var LspConfigSchema = exports_external.object({
28847
28945
  servers: exports_external.record(exports_external.string().trim().min(1), LspServerEntrySchema).optional(),
28848
28946
  disabled: exports_external.array(exports_external.string().trim().min(1)).optional(),
28849
28947
  python: exports_external.enum(["pyright", "ty", "auto"]).optional(),
28948
+ diagnostics_on_edit: exports_external.boolean().optional(),
28850
28949
  auto_install: exports_external.boolean().optional(),
28851
28950
  grace_days: exports_external.number().int().positive().optional(),
28852
28951
  versions: exports_external.record(exports_external.string().trim().min(1), exports_external.string().trim().min(1)).optional()
@@ -28869,6 +28968,24 @@ var BashFeaturesSchema = exports_external.object({
28869
28968
  long_running_reminder_interval_ms: exports_external.number().int().positive().optional()
28870
28969
  });
28871
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
+ });
28872
28989
  var AftConfigSchema = exports_external.object({
28873
28990
  $schema: exports_external.string().optional(),
28874
28991
  format_on_edit: exports_external.boolean().optional(),
@@ -28881,6 +28998,7 @@ var AftConfigSchema = exports_external.object({
28881
28998
  restrict_to_project_root: exports_external.boolean().optional(),
28882
28999
  search_index: exports_external.boolean().optional(),
28883
29000
  semantic_search: exports_external.boolean().optional(),
29001
+ inspect: InspectConfigSchema.optional(),
28884
29002
  bash: BashConfigSchema.optional(),
28885
29003
  experimental: ExperimentalConfigSchema.optional(),
28886
29004
  lsp: LspConfigSchema.optional(),
@@ -29091,7 +29209,7 @@ ${serialized}` : serialized;
29091
29209
  } catch (err) {
29092
29210
  if (tmpPath) {
29093
29211
  try {
29094
- unlinkSync3(tmpPath);
29212
+ unlinkSync4(tmpPath);
29095
29213
  } catch {}
29096
29214
  }
29097
29215
  if (isWritableMigrationError(err)) {
@@ -29175,6 +29293,9 @@ function mergeLspConfig(base, override) {
29175
29293
  const projectSafe = {};
29176
29294
  if (override?.python !== undefined)
29177
29295
  projectSafe.python = override.python;
29296
+ if (override?.diagnostics_on_edit !== undefined) {
29297
+ projectSafe.diagnostics_on_edit = override.diagnostics_on_edit;
29298
+ }
29178
29299
  const userDisabled = base?.disabled ?? [];
29179
29300
  const lsp = {
29180
29301
  ...base,
@@ -29185,6 +29306,27 @@ function mergeLspConfig(base, override) {
29185
29306
  return;
29186
29307
  return Object.fromEntries(Object.entries(lsp).filter(([, v]) => v !== undefined));
29187
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
+ }
29188
29330
  function mergeBashConfig(baseBash, overrideBash) {
29189
29331
  if (baseBash === undefined && overrideBash === undefined)
29190
29332
  return;
@@ -29241,6 +29383,7 @@ var PROJECT_SAFE_TOP_LEVEL_FIELDS = new Set([
29241
29383
  "validate_on_edit",
29242
29384
  "search_index",
29243
29385
  "semantic_search",
29386
+ "inspect",
29244
29387
  "experimental",
29245
29388
  "bash"
29246
29389
  ]);
@@ -29271,8 +29414,10 @@ function mergeConfigs(base, override) {
29271
29414
  const lsp = mergeLspConfig(base.lsp, override.lsp);
29272
29415
  const experimental = mergeExperimentalConfig(base.experimental, override.experimental);
29273
29416
  const bash = mergeBashConfig(base.bash, override.bash);
29417
+ const inspect = mergeInspectConfig(base.inspect, override.inspect);
29274
29418
  const safeOverride = pickProjectSafeFields(override);
29275
29419
  delete safeOverride.bash;
29420
+ delete safeOverride.inspect;
29276
29421
  return {
29277
29422
  ...base,
29278
29423
  ...safeOverride,
@@ -29280,6 +29425,7 @@ function mergeConfigs(base, override) {
29280
29425
  ...Object.keys(checker).length > 0 ? { checker } : {},
29281
29426
  ...lsp ? { lsp } : {},
29282
29427
  ...bash !== undefined ? { bash } : {},
29428
+ ...inspect !== undefined ? { inspect } : {},
29283
29429
  experimental,
29284
29430
  semantic,
29285
29431
  ...disabledTools.length > 0 ? { disabled_tools: [...new Set(disabledTools)] } : {}
@@ -29331,7 +29477,7 @@ import {
29331
29477
  openSync as openSync3,
29332
29478
  readFileSync as readFileSync5,
29333
29479
  statSync as statSync3,
29334
- unlinkSync as unlinkSync4,
29480
+ unlinkSync as unlinkSync5,
29335
29481
  writeFileSync as writeFileSync4
29336
29482
  } from "node:fs";
29337
29483
  import { homedir as homedir7 } from "node:os";
@@ -29461,7 +29607,7 @@ ${new Date().toISOString()}
29461
29607
  }
29462
29608
  log2(`[lsp] reclaiming install lock for ${lockKey} (owner_pid=${owningPid ?? "unknown"}, alive=${ownerAlive}, age_ms=${age})`);
29463
29609
  try {
29464
- unlinkSync4(lock);
29610
+ unlinkSync5(lock);
29465
29611
  } catch {}
29466
29612
  return tryClaim();
29467
29613
  }
@@ -29498,7 +29644,7 @@ function releaseInstallLock(lockKey) {
29498
29644
  return;
29499
29645
  }
29500
29646
  try {
29501
- unlinkSync4(lock);
29647
+ unlinkSync5(lock);
29502
29648
  } catch (unlinkErr) {
29503
29649
  const code = unlinkErr.code;
29504
29650
  if (code !== "ENOENT") {
@@ -30154,7 +30300,7 @@ function runAutoInstall(projectRoot, config2, fetchImpl = fetch) {
30154
30300
  import { execFileSync as execFileSync2 } from "node:child_process";
30155
30301
  import { createHash as createHash4, randomBytes } from "node:crypto";
30156
30302
  import {
30157
- copyFileSync as copyFileSync3,
30303
+ copyFileSync as copyFileSync4,
30158
30304
  createReadStream as createReadStream2,
30159
30305
  createWriteStream as createWriteStream3,
30160
30306
  existsSync as existsSync8,
@@ -30167,7 +30313,7 @@ import {
30167
30313
  renameSync as renameSync6,
30168
30314
  rmSync as rmSync4,
30169
30315
  statSync as statSync5,
30170
- unlinkSync as unlinkSync5,
30316
+ unlinkSync as unlinkSync6,
30171
30317
  writeFileSync as writeFileSync5
30172
30318
  } from "node:fs";
30173
30319
  import { dirname as dirname4, join as join12, relative as relative2, resolve as resolve2 } from "node:path";
@@ -30529,7 +30675,7 @@ async function downloadFile(url2, destPath, fetchImpl, assetSize, signal) {
30529
30675
  await pipeline3(nodeStream, createWriteStream3(destPath), { signal: timeout.signal });
30530
30676
  } catch (err) {
30531
30677
  try {
30532
- unlinkSync5(destPath);
30678
+ unlinkSync6(destPath);
30533
30679
  } catch {}
30534
30680
  throw err;
30535
30681
  } finally {
@@ -30761,7 +30907,7 @@ async function downloadAndInstall(spec, tag, assets, platform, arch, fetchImpl,
30761
30907
  } catch (err) {
30762
30908
  error2(`[lsp] hash ${spec.id} failed: ${err}`);
30763
30909
  try {
30764
- unlinkSync5(archivePath);
30910
+ unlinkSync6(archivePath);
30765
30911
  } catch {}
30766
30912
  return null;
30767
30913
  }
@@ -30772,7 +30918,7 @@ async function downloadAndInstall(spec, tag, assets, platform, arch, fetchImpl,
30772
30918
  if (previousArchiveSha256 !== archiveSha256) {
30773
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.`);
30774
30920
  try {
30775
- unlinkSync5(archivePath);
30921
+ unlinkSync6(archivePath);
30776
30922
  } catch {}
30777
30923
  return null;
30778
30924
  }
@@ -30784,7 +30930,7 @@ async function downloadAndInstall(spec, tag, assets, platform, arch, fetchImpl,
30784
30930
  return null;
30785
30931
  } finally {
30786
30932
  try {
30787
- unlinkSync5(archivePath);
30933
+ unlinkSync6(archivePath);
30788
30934
  } catch {}
30789
30935
  }
30790
30936
  const innerBinaryPath = join12(extractDir, spec.binaryPathInArchive(platform, arch, version2));
@@ -30795,7 +30941,7 @@ async function downloadAndInstall(spec, tag, assets, platform, arch, fetchImpl,
30795
30941
  const targetBinary = ghBinaryPath(spec, platform);
30796
30942
  mkdirSync8(dirname4(targetBinary), { recursive: true });
30797
30943
  try {
30798
- copyFileSync3(innerBinaryPath, targetBinary);
30944
+ copyFileSync4(innerBinaryPath, targetBinary);
30799
30945
  if (platform !== "win32") {
30800
30946
  const { chmodSync: chmodSync4 } = await import("node:fs");
30801
30947
  chmodSync4(targetBinary, 493);
@@ -30984,13 +31130,18 @@ function sendIgnoredMessage(client, sessionId, text) {
30984
31130
  }
30985
31131
  }
30986
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 {};
30987
31144
  try {
30988
- const resp = await bridge.send("db_get_state", { key: "warned_tools" });
30989
- if (resp.success === false)
30990
- return {};
30991
- const value = resp.data?.value;
30992
- if (typeof value !== "string")
30993
- return {};
30994
31145
  const parsed = JSON.parse(value);
30995
31146
  if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
30996
31147
  return {};
@@ -30999,12 +31150,16 @@ async function readWarnedTools(bridge) {
30999
31150
  return {};
31000
31151
  }
31001
31152
  }
31002
- async function hasWarnedFor(bridge, key) {
31153
+ async function warnedStatus(bridge, key) {
31003
31154
  const warned = await readWarnedTools(bridge);
31004
- 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";
31005
31158
  }
31006
31159
  async function recordWarning(bridge, key) {
31007
31160
  const warned = await readWarnedTools(bridge);
31161
+ if (warned === null)
31162
+ return;
31008
31163
  warned[key] = true;
31009
31164
  try {
31010
31165
  await bridge.send("db_set_state", {
@@ -31053,7 +31208,7 @@ async function deliverConfigureWarnings(opts, warnings) {
31053
31208
  return;
31054
31209
  for (const warning of warnings) {
31055
31210
  const key = warningKey(warning, opts.projectRoot);
31056
- if (await hasWarnedFor(opts.bridge, key))
31211
+ if (await warnedStatus(opts.bridge, key) !== "fresh")
31057
31212
  continue;
31058
31213
  if (!sendIgnoredMessage(opts.client, opts.sessionId, formatConfigureWarning(warning)))
31059
31214
  continue;
@@ -31321,24 +31476,6 @@ function groupByFile(items, getFile) {
31321
31476
  });
31322
31477
  return groups;
31323
31478
  }
31324
- function distinctCount(values) {
31325
- return new Set(values.filter((value) => Boolean(value))).size;
31326
- }
31327
- function severityBadge(theme, severity) {
31328
- const label = severity === "information" ? "info" : severity;
31329
- switch (severity) {
31330
- case "error":
31331
- return theme.fg("error", `[${label}]`);
31332
- case "warning":
31333
- return theme.fg("warning", `[${label}]`);
31334
- case "information":
31335
- return theme.fg("accent", `[${label}]`);
31336
- case "hint":
31337
- return theme.fg("muted", `[${label}]`);
31338
- default:
31339
- return theme.fg("muted", `[${label}]`);
31340
- }
31341
- }
31342
31479
  function formatTimestamp(value) {
31343
31480
  if (typeof value === "string" && value.length > 0)
31344
31481
  return value;
@@ -32541,6 +32678,10 @@ function formatDiffForPi(oldContent, newContent, contextLines = DEFAULT_CONTEXT_
32541
32678
  }
32542
32679
 
32543
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
+ }
32544
32685
  function containsPath(parent, child) {
32545
32686
  const rel = relative3(parent, child);
32546
32687
  return rel === "" || !rel.startsWith("..") && !isAbsolute2(rel);
@@ -32607,7 +32748,8 @@ var WriteParams = Type6.Object({
32607
32748
  filePath: Type6.String({
32608
32749
  description: "Path to the file to write (absolute or relative to project root)"
32609
32750
  }),
32610
- 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 }))
32611
32753
  });
32612
32754
  var EditParams = Type6.Object({
32613
32755
  filePath: Type6.String({
@@ -32619,7 +32761,8 @@ var EditParams = Type6.Object({
32619
32761
  occurrence: optionalInt(0, Number.MAX_SAFE_INTEGER),
32620
32762
  appendContent: Type6.Optional(Type6.String({
32621
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."
32622
- }))
32764
+ })),
32765
+ diagnostics: Type6.Optional(Type6.Boolean({ description: DIAGNOSTICS_PARAM_DESCRIPTION }))
32623
32766
  });
32624
32767
  var GrepParams = Type6.Object({
32625
32768
  pattern: Type6.String({ description: "Regex pattern to search for" }),
@@ -32670,8 +32813,8 @@ function registerHoistedTools(pi, ctx, surface) {
32670
32813
  pi.registerTool({
32671
32814
  name: "write",
32672
32815
  label: "write",
32673
- 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`).",
32674
- 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)",
32675
32818
  promptGuidelines: ["Use write only for new files or complete rewrites."],
32676
32819
  parameters: WriteParams,
32677
32820
  async execute(_toolCallId, params, _signal, _onUpdate, extCtx) {
@@ -32682,8 +32825,8 @@ function registerHoistedTools(pi, ctx, surface) {
32682
32825
  const response = await callBridge(bridge, "write", {
32683
32826
  file: params.filePath,
32684
32827
  content: params.content,
32685
- diagnostics: true,
32686
- include_diff: true
32828
+ diagnostics: params.diagnostics ?? diagnosticsOnEditDefault(ctx),
32829
+ include_diff_content: true
32687
32830
  }, extCtx);
32688
32831
  return buildMutationResult(params.filePath, response);
32689
32832
  },
@@ -32699,8 +32842,8 @@ function registerHoistedTools(pi, ctx, surface) {
32699
32842
  pi.registerTool({
32700
32843
  name: "edit",
32701
32844
  label: "edit",
32702
- 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.",
32703
- 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.",
32704
32847
  promptGuidelines: [
32705
32848
  "Prefer edit over write when changing part of an existing file.",
32706
32849
  "Include enough surrounding context in oldString to make the match unique, or set replaceAll/occurrence explicitly.",
@@ -32717,8 +32860,8 @@ function registerHoistedTools(pi, ctx, surface) {
32717
32860
  op: "append",
32718
32861
  file: params.filePath,
32719
32862
  append_content: params.appendContent,
32720
- diagnostics: true,
32721
- include_diff: true
32863
+ diagnostics: params.diagnostics ?? diagnosticsOnEditDefault(ctx),
32864
+ include_diff_content: true
32722
32865
  };
32723
32866
  const response2 = await callBridge(bridge, "edit_match", req2, extCtx);
32724
32867
  return buildMutationResult(params.filePath, response2);
@@ -32727,8 +32870,8 @@ function registerHoistedTools(pi, ctx, surface) {
32727
32870
  file: params.filePath,
32728
32871
  match: params.oldString ?? "",
32729
32872
  replacement: params.newString ?? "",
32730
- diagnostics: true,
32731
- include_diff: true
32873
+ diagnostics: params.diagnostics ?? diagnosticsOnEditDefault(ctx),
32874
+ include_diff_content: true
32732
32875
  };
32733
32876
  if (params.replaceAll === true)
32734
32877
  req.replace_all = true;
@@ -32797,15 +32940,6 @@ function buildMutationResult(filePath, response) {
32797
32940
  }
32798
32941
  const summaryHeader = replacements !== undefined ? `Edited ${filePath} (+${additions}/-${deletions}, ${replacements} replacement${replacements === 1 ? "" : "s"})` : `Wrote ${filePath} (+${additions}/-${deletions})`;
32799
32942
  let text = summaryHeader;
32800
- if (diffText)
32801
- text += `
32802
-
32803
- ${diffText}`;
32804
- if (truncated) {
32805
- text += `
32806
-
32807
- (diff truncated — file too large to include before/after content)`;
32808
- }
32809
32943
  if (noOp) {
32810
32944
  text += `
32811
32945
 
@@ -33085,106 +33219,234 @@ function registerImportTools(pi, ctx) {
33085
33219
  });
33086
33220
  }
33087
33221
 
33088
- // src/tools/lsp.ts
33089
- import { StringEnum as StringEnum3 } from "@earendil-works/pi-ai";
33222
+ // src/tools/inspect.ts
33090
33223
  import { Type as Type8 } from "typebox";
33091
- var LspDiagnosticsParams = Type8.Object({
33092
- filePath: Type8.Optional(Type8.String({ description: "File to get diagnostics for (mutually exclusive with directory)" })),
33093
- directory: Type8.Optional(Type8.String({
33094
- 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."
33095
33227
  })),
33096
- severity: Type8.Optional(StringEnum3(["error", "warning", "information", "hint", "all"], {
33097
- 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."
33098
33230
  })),
33099
- waitMs: Type8.Optional(Type8.Number({
33100
- 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."
33101
33236
  }))
33102
33237
  });
33103
- function buildDiagnosticsSections(payload, theme) {
33104
- const response = asRecord2(payload);
33105
- if (!response)
33106
- return [theme.fg("muted", "No diagnostics available.")];
33107
- const diagnostics = asRecords(response.diagnostics);
33108
- const total = asNumber(response.total) ?? diagnostics.length;
33109
- const filesWithErrors = asNumber(response.files_with_errors) ?? distinctCount(diagnostics.filter((diag) => asString(diag.severity) === "error").map((diag) => asString(diag.file)));
33110
- const filesCount = distinctCount(diagnostics.map((diag) => asString(diag.file)));
33111
- const sections = [
33112
- `${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"}`)}`
33113
- ];
33114
- if (diagnostics.length === 0) {
33115
- sections.push(theme.fg("muted", "No diagnostics found."));
33116
- 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");
33117
33251
  }
33118
- const grouped = groupByFile(diagnostics, (diag) => asString(diag.file));
33119
- for (const [file2, fileDiagnostics] of grouped.entries()) {
33120
- const lines = [theme.fg("accent", shortenPath(file2))];
33121
- fileDiagnostics.forEach((diagnostic) => {
33122
- const severity = asString(diagnostic.severity) ?? "information";
33123
- const line = asNumber(diagnostic.line) ?? 0;
33124
- const column = asNumber(diagnostic.column) ?? 0;
33125
- const code = asString(diagnostic.code);
33126
- const message = asString(diagnostic.message) ?? "(no message)";
33127
- const location = `${line}:${column}`;
33128
- lines.push(` ${severityBadge(theme, severity)} ${location}${code ? ` ${theme.fg("muted", code)}` : ""} ${message}`);
33129
- });
33130
- sections.push(lines.join(`
33131
- `));
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}`;
33132
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
+ }
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);
33133
33405
  return sections;
33134
33406
  }
33135
- function renderDiagnosticsCall(args, theme, context) {
33136
- const target = args.filePath ?? args.directory;
33137
- const summary = [
33138
- target ? accentPath(theme, target) : undefined,
33139
- args.severity ? theme.fg("toolOutput", args.severity) : undefined
33140
- ].filter(Boolean).join(" ");
33141
- 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);
33142
33412
  }
33143
- function renderDiagnosticsResult(result, theme, context) {
33413
+ function renderInspectResult(result, theme, context) {
33144
33414
  if (context.isError)
33145
- return renderErrorResult(result, "lsp diagnostics failed", theme, context);
33146
- return renderSections(buildDiagnosticsSections(extractStructuredPayload(result), theme), context);
33415
+ return renderErrorResult(result, "inspect failed", theme, context);
33416
+ return renderSections(buildInspectSections(extractStructuredPayload(result), theme), context);
33147
33417
  }
33148
- function registerLspTools(pi, ctx) {
33418
+ function registerInspectTool(pi, ctx) {
33149
33419
  pi.registerTool({
33150
- name: "lsp_diagnostics",
33151
- label: "lsp diagnostics",
33152
- 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>`.",
33153
- 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,
33154
33426
  async execute(_toolCallId, params, _signal, _onUpdate, extCtx) {
33155
- const hasFile = typeof params.filePath === "string" && params.filePath.length > 0;
33156
- const hasDir = typeof params.directory === "string" && params.directory.length > 0;
33157
- if (hasFile && hasDir) {
33158
- throw new Error("'filePath' and 'directory' are mutually exclusive — provide one or neither");
33159
- }
33160
33427
  const bridge = bridgeFor(ctx, extCtx.cwd);
33161
- const req = {};
33162
- if (hasFile)
33163
- req.file = params.filePath;
33164
- if (hasDir)
33165
- req.directory = params.directory;
33166
- if (params.severity !== undefined)
33167
- req.severity = params.severity;
33168
- if (params.waitMs !== undefined)
33169
- req.wait_ms = params.waitMs;
33170
- const response = await callBridge(bridge, "lsp_diagnostics", req, extCtx);
33171
- 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);
33172
33434
  },
33173
33435
  renderCall(args, theme, context) {
33174
- return renderDiagnosticsCall(args, theme, context);
33436
+ return renderInspectCall(args, theme, context);
33175
33437
  },
33176
33438
  renderResult(result, _options, theme, context) {
33177
- return renderDiagnosticsResult(result, theme, context);
33439
+ return renderInspectResult(result, theme, context);
33178
33440
  }
33179
33441
  });
33180
33442
  }
33181
33443
 
33182
33444
  // src/tools/navigate.ts
33183
- import { StringEnum as StringEnum4 } from "@earendil-works/pi-ai";
33445
+ import { StringEnum as StringEnum3 } from "@earendil-works/pi-ai";
33184
33446
  import { Type as Type9 } from "typebox";
33185
33447
  function navigateParamsSchema() {
33186
33448
  return Type9.Object({
33187
- 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"], {
33188
33450
  description: "Navigation operation"
33189
33451
  }),
33190
33452
  filePath: Type9.String({
@@ -33341,7 +33603,7 @@ function renderNavigateCall(args, theme, context) {
33341
33603
  theme.fg("toolOutput", args.symbol),
33342
33604
  args.toSymbol ? theme.fg("toolOutput", `→ ${args.toSymbol}`) : undefined
33343
33605
  ].filter(Boolean).join(" ");
33344
- return renderToolCall("navigate", summary, theme, context);
33606
+ return renderToolCall("callgraph", summary, theme, context);
33345
33607
  }
33346
33608
  function renderNavigateResult(result, args, theme, context) {
33347
33609
  if (context.isError)
@@ -33350,9 +33612,9 @@ function renderNavigateResult(result, args, theme, context) {
33350
33612
  }
33351
33613
  function registerNavigateTool(pi, ctx) {
33352
33614
  pi.registerTool({
33353
- name: "aft_navigate",
33354
- label: "navigate",
33355
- 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.",
33356
33618
  parameters: navigateParamsSchema(),
33357
33619
  async execute(_toolCallId, params, _signal, _onUpdate, extCtx) {
33358
33620
  if (params.op === "trace_data" && !params.expression) {
@@ -33614,9 +33876,25 @@ Pass a single \`target\`:
33614
33876
  parameters: ZoomParams,
33615
33877
  async execute(_toolCallId, params, _signal, _onUpdate, extCtx) {
33616
33878
  const bridge = bridgeFor(ctx, extCtx.cwd);
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
+ };
33617
33895
  const hasFilePath = !isEmptyParam(params.filePath);
33618
33896
  const hasUrl = !isEmptyParam(params.url);
33619
- const hasTargets = !isEmptyParam(params.targets);
33897
+ const hasTargets = hasTargetsProvided(params.targets);
33620
33898
  const hasSymbols = !isEmptyParam(params.symbols);
33621
33899
  if (hasTargets) {
33622
33900
  if (hasFilePath || hasUrl || hasSymbols) {
@@ -33770,10 +34048,10 @@ function formatOutlineFilesText(response) {
33770
34048
  }
33771
34049
 
33772
34050
  // src/tools/refactor.ts
33773
- import { StringEnum as StringEnum5 } from "@earendil-works/pi-ai";
34051
+ import { StringEnum as StringEnum4 } from "@earendil-works/pi-ai";
33774
34052
  import { Type as Type11 } from "typebox";
33775
34053
  var RefactorParams = Type11.Object({
33776
- op: StringEnum5(["move", "extract", "inline"], { description: "Refactoring operation" }),
34054
+ op: StringEnum4(["move", "extract", "inline"], { description: "Refactoring operation" }),
33777
34055
  filePath: Type11.String({
33778
34056
  description: "Source file (absolute or relative to project root)"
33779
34057
  }),
@@ -33881,10 +34159,10 @@ function registerRefactorTool(pi, ctx) {
33881
34159
  }
33882
34160
 
33883
34161
  // src/tools/safety.ts
33884
- import { StringEnum as StringEnum6 } from "@earendil-works/pi-ai";
34162
+ import { StringEnum as StringEnum5 } from "@earendil-works/pi-ai";
33885
34163
  import { Type as Type12 } from "typebox";
33886
34164
  var SafetyParams = Type12.Object({
33887
- op: StringEnum6(["undo", "history", "checkpoint", "restore", "list"], {
34165
+ op: StringEnum5(["undo", "history", "checkpoint", "restore", "list"], {
33888
34166
  description: "Safety operation"
33889
34167
  }),
33890
34168
  filePath: Type12.Optional(Type12.String({
@@ -34023,11 +34301,28 @@ function registerSafetyTool(pi, ctx) {
34023
34301
 
34024
34302
  // src/tools/semantic.ts
34025
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
+ }
34026
34316
  var SearchParams2 = Type13.Object({
34027
34317
  query: Type13.String({
34028
34318
  description: "Concept, regex, literal text, filename, or capability to find. Examples: 'fuzzy match with whitespace tolerance', '^export', 'Cargo.lock'."
34029
34319
  }),
34030
- 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
+ })),
34031
34326
  hint: Type13.Optional(Type13.Union([
34032
34327
  Type13.Literal("regex"),
34033
34328
  Type13.Literal("literal"),
@@ -34053,6 +34348,9 @@ function buildSemanticSections(args, payload, theme) {
34053
34348
  sections.push(warnings.map((warning) => theme.fg("warning", `⚠ ${warning}`)).join(`
34054
34349
  `));
34055
34350
  }
34351
+ const honestyNote = semanticHonestyNote(response, theme);
34352
+ if (honestyNote)
34353
+ sections.push(honestyNote);
34056
34354
  const results = asRecords(response.results);
34057
34355
  if (status !== "ready" && results.length === 0) {
34058
34356
  sections.push(asString(response.text) ?? theme.fg("muted", "Semantic index is not ready."));
@@ -34070,12 +34368,14 @@ function buildSemanticSections(args, payload, theme) {
34070
34368
  const line = asNumber(result.line);
34071
34369
  const column = asNumber(result.column);
34072
34370
  const lineText = asString(result.line_text) ?? "";
34073
- const location = line !== undefined ? `${line}${column !== undefined ? `:${column}` : ""}` : "?";
34074
- lines.push(` ↳ ${theme.fg("muted", `line ${location}`)} ${lineText}`);
34371
+ const location2 = line !== undefined ? `${line}${column !== undefined ? `:${column}` : ""}` : "?";
34372
+ lines.push(` ↳ ${theme.fg("muted", `line ${location2}`)} ${lineText}`);
34075
34373
  return;
34076
34374
  }
34077
34375
  const score = asNumber(result.score);
34078
34376
  const source = asString(result.source);
34377
+ const kind = asString(result.kind) ?? "symbol";
34378
+ const location = asString(result.location);
34079
34379
  if (source === "lexical") {
34080
34380
  lines.push(` ↳ ${theme.fg("muted", `[lexical match${score !== undefined ? ` — score ${score.toFixed(3)}` : ""}]`)}`);
34081
34381
  const snippet2 = asString(result.snippet);
@@ -34085,10 +34385,14 @@ function buildSemanticSections(args, payload, theme) {
34085
34385
  }
34086
34386
  return;
34087
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
+ }
34088
34393
  const startLine = asNumber(result.start_line);
34089
34394
  const endLine = asNumber(result.end_line);
34090
34395
  const range = startLine !== undefined ? `${startLine}${endLine && endLine !== startLine ? `-${endLine}` : ""}` : "?";
34091
- const kind = asString(result.kind) ?? "symbol";
34092
34396
  const name = asString(result.name) ?? "(unknown)";
34093
34397
  lines.push(` ↳ ${name} ${theme.fg("muted", `[${kind}] lines ${range}${score !== undefined ? ` score ${score.toFixed(3)}` : ""}`)}`);
34094
34398
  const snippet = asString(result.snippet);
@@ -34127,13 +34431,16 @@ function registerSemanticTool(pi, ctx) {
34127
34431
  "When NOT to use:",
34128
34432
  "- You need exhaustive literal enumeration → use grep directly",
34129
34433
  "- You want the file/module structure → use aft_outline",
34130
- "- You're following a call chain → use aft_navigate",
34434
+ "- You're following a call chain → use aft_callgraph",
34131
34435
  "",
34132
34436
  "Set hint to 'regex', 'literal', 'semantic', or 'auto' to override or document routing intent."
34133
34437
  ].join(`
34134
34438
  `),
34135
34439
  parameters: SearchParams2,
34136
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
+ }
34137
34444
  const bridge = bridgeFor(ctx, extCtx.cwd);
34138
34445
  const req = { query: params.query };
34139
34446
  if (params.topK !== undefined)
@@ -34141,7 +34448,7 @@ function registerSemanticTool(pi, ctx) {
34141
34448
  if (params.hint !== undefined)
34142
34449
  req.hint = params.hint;
34143
34450
  const response = await callBridge(bridge, "semantic_search", req, extCtx);
34144
- return textResult(response.text ?? JSON.stringify(response, null, 2));
34451
+ return textResult(response.text ?? JSON.stringify(response, null, 2), response);
34145
34452
  },
34146
34453
  renderCall(args, theme, context) {
34147
34454
  return renderSemanticCall(args, theme, context);
@@ -34153,10 +34460,10 @@ function registerSemanticTool(pi, ctx) {
34153
34460
  }
34154
34461
 
34155
34462
  // src/tools/structure.ts
34156
- import { StringEnum as StringEnum7 } from "@earendil-works/pi-ai";
34463
+ import { StringEnum as StringEnum6 } from "@earendil-works/pi-ai";
34157
34464
  import { Type as Type14 } from "typebox";
34158
34465
  var TransformParams = Type14.Object({
34159
- 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" }),
34160
34467
  filePath: Type14.String({
34161
34468
  description: "Path to the source file (absolute or relative to project root)"
34162
34469
  }),
@@ -34172,7 +34479,7 @@ var TransformParams = Type14.Object({
34172
34479
  position: Type14.Optional(Type14.String({
34173
34480
  description: "Position hint: 'first', 'last' (default), 'before:name', 'after:name' for add_member"
34174
34481
  })),
34175
- validate: Type14.Optional(StringEnum7(["syntax", "full"], {
34482
+ validate: Type14.Optional(StringEnum6(["syntax", "full"], {
34176
34483
  description: "Validation level (default: syntax)"
34177
34484
  }))
34178
34485
  });
@@ -34288,7 +34595,8 @@ function buildWorkflowHints(opts) {
34288
34595
  const hasZoom = !opts.absentTools.has("aft_zoom");
34289
34596
  const hasGrep = opts.toolSurface !== "minimal" && !opts.absentTools.has(grepName);
34290
34597
  const hasSearch = opts.toolSurface !== "minimal" && opts.semanticEnabled && !opts.absentTools.has("aft_search");
34291
- 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");
34292
34600
  const hasBgBash = opts.bashBackgroundEnabled && !opts.absentTools.has(bashName) && !opts.absentTools.has("bash_status");
34293
34601
  if (hasOutline && hasZoom) {
34294
34602
  sections.push(`**Web/URL access**: \`aft_outline({ target: "<url>" })\` first for structure, then \`aft_zoom({ url: "<url>", symbols: "<heading>" })\` for the specific section.`);
@@ -34296,14 +34604,17 @@ function buildWorkflowHints(opts) {
34296
34604
  if (hasOutline && hasZoom && (hasGrep || hasSearch)) {
34297
34605
  if (hasSearch) {
34298
34606
  const grepFallback = hasGrep ? ` Use \`${grepName}\` directly only when you need exhaustive enumeration of literal text (every TODO, every import of X) without ranking.` : "";
34299
- 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. Pass \`hint: "regex"\` / \`hint: "literal"\` / \`hint: "semantic"\` to override routing if needed. Then \`aft_outline\` for structure → \`aft_zoom\` for symbol(s).${grepFallback}`);
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}`);
34300
34608
  } else {
34301
34609
  sections.push(`**Code exploration**: \`${grepName}\` to locate → \`aft_outline\` for structure → \`aft_zoom\` for symbol(s).`);
34302
34610
  }
34303
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.");
34614
+ }
34304
34615
  if (hasNavigate) {
34305
34616
  sections.push([
34306
- "Use `aft_navigate` instead of grep + read chains for relationship questions:",
34617
+ "Use `aft_callgraph` for code-relationship questions instead of grep + read chains:",
34307
34618
  "- `callers` — find all call sites before changing a function signature",
34308
34619
  "- `impact` — blast radius (which functions/files will need updates)",
34309
34620
  "- `trace_to` — how execution reaches this code from entry points (routes, exports, main)",
@@ -34343,7 +34654,9 @@ function registerWorkflowHints(pi, config2, surface) {
34343
34654
  if (!surface.semantic)
34344
34655
  absent.add("aft_search");
34345
34656
  if (!surface.navigate)
34346
- absent.add("aft_navigate");
34657
+ absent.add("aft_callgraph");
34658
+ if (!surface.inspect)
34659
+ absent.add("aft_inspect");
34347
34660
  if (!surface.hoistGrep)
34348
34661
  absent.add("grep");
34349
34662
  if (!surface.hoistBash) {
@@ -34407,17 +34720,17 @@ var PLUGIN_VERSION = (() => {
34407
34720
  return "0.0.0";
34408
34721
  }
34409
34722
  })();
34410
- var ANNOUNCEMENT_VERSION = "0.32.0";
34723
+ var ANNOUNCEMENT_VERSION = "0.33.0";
34411
34724
  var ANNOUNCEMENT_FEATURES = [
34412
- "`aft_search` is now the primary code-search tool auto-routes regex / literal / semantic / hybrid by query shape, with a `hint` override.",
34413
- 'Semantic search stays queryable through edits no more "rebuilding" fallback after every save.',
34414
- "Workflow hints promote `aft_search` as primary; `grep` is positioned as the specialized fallback.",
34415
- "Bare `\\n`, `\\t`, `\\r` queries correctly route to regex mode.",
34416
- 'Empty params (`targets: []`, `url: ""`) no longer trigger misleading mutual-exclusion errors.'
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."
34417
34730
  ];
34418
34731
  var ANNOUNCEMENT_FOOTER = "Join us on Discord: https://discord.gg/F2uWxjGnU";
34419
34732
  var ALL_ONLY_TOOLS = new Set([
34420
- "aft_navigate",
34733
+ "aft_callgraph",
34421
34734
  "aft_delete",
34422
34735
  "aft_move",
34423
34736
  "aft_transform",
@@ -34491,6 +34804,7 @@ function resolveToolSurface(config2) {
34491
34804
  outline: ok("aft_outline"),
34492
34805
  zoom: ok("aft_zoom"),
34493
34806
  semantic: false,
34807
+ inspect: false,
34494
34808
  navigate: false,
34495
34809
  conflicts: false,
34496
34810
  importTool: false,
@@ -34499,7 +34813,6 @@ function resolveToolSurface(config2) {
34499
34813
  move: false,
34500
34814
  astSearch: false,
34501
34815
  astReplace: false,
34502
- lspDiagnostics: false,
34503
34816
  structure: false,
34504
34817
  refactor: false
34505
34818
  };
@@ -34514,6 +34827,7 @@ function resolveToolSurface(config2) {
34514
34827
  outline: ok("aft_outline"),
34515
34828
  zoom: ok("aft_zoom"),
34516
34829
  semantic: ok("aft_search") && config2.semantic_search === true,
34830
+ inspect: ok("aft_inspect") && config2.inspect?.enabled !== false,
34517
34831
  navigate: false,
34518
34832
  conflicts: ok("aft_conflicts"),
34519
34833
  importTool: ok("aft_import"),
@@ -34522,14 +34836,13 @@ function resolveToolSurface(config2) {
34522
34836
  move: false,
34523
34837
  astSearch: ok("ast_grep_search"),
34524
34838
  astReplace: ok("ast_grep_replace"),
34525
- lspDiagnostics: ok("lsp_diagnostics"),
34526
34839
  structure: false,
34527
34840
  refactor: false
34528
34841
  };
34529
34842
  if (surface === "all") {
34530
34843
  return {
34531
34844
  ...base,
34532
- navigate: allOnly("aft_navigate"),
34845
+ navigate: allOnly("aft_callgraph"),
34533
34846
  delete: allOnly("aft_delete"),
34534
34847
  move: allOnly("aft_move"),
34535
34848
  structure: allOnly("aft_transform"),
@@ -34577,6 +34890,8 @@ async function src_default(pi) {
34577
34890
  Object.assign(configOverrides, resolveLspConfigForConfigure(config2));
34578
34891
  if (config2.semantic !== undefined)
34579
34892
  configOverrides.semantic = config2.semantic;
34893
+ if (config2.inspect !== undefined)
34894
+ configOverrides.inspect = config2.inspect;
34580
34895
  if (config2.max_callgraph_files !== undefined)
34581
34896
  configOverrides.max_callgraph_files = config2.max_callgraph_files;
34582
34897
  if (config2.url_fetch_allow_private !== undefined)
@@ -34744,6 +35059,9 @@ ${lines}
34744
35059
  if (surface.semantic) {
34745
35060
  registerSemanticTool(pi, ctx);
34746
35061
  }
35062
+ if (surface.inspect) {
35063
+ registerInspectTool(pi, ctx);
35064
+ }
34747
35065
  if (surface.navigate) {
34748
35066
  registerNavigateTool(pi, ctx);
34749
35067
  }
@@ -34762,9 +35080,6 @@ ${lines}
34762
35080
  if (surface.delete || surface.move) {
34763
35081
  registerFsTools(pi, ctx, surface);
34764
35082
  }
34765
- if (surface.lspDiagnostics) {
34766
- registerLspTools(pi, ctx);
34767
- }
34768
35083
  if (surface.structure) {
34769
35084
  registerStructureTool(pi, ctx);
34770
35085
  }