@cortexkit/aft-opencode 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
@@ -11599,6 +11599,7 @@ import { homedir } from "node:os";
11599
11599
  import { join } from "node:path";
11600
11600
  import { StringDecoder } from "node:string_decoder";
11601
11601
  var DEFAULT_BRIDGE_TIMEOUT_MS = 30000;
11602
+ var BRIDGE_HANG_TIMEOUT_THRESHOLD = 2;
11602
11603
  var SEMANTIC_TIMEOUT_SAFETY_MARGIN_MS = 5000;
11603
11604
  var MAX_STDOUT_BUFFER = 64 * 1024 * 1024;
11604
11605
  function tagStderrLine(line) {
@@ -11706,8 +11707,11 @@ class BinaryBridge {
11706
11707
  statusListeners = new Set;
11707
11708
  configureWarningClients = new Map;
11708
11709
  restartResetTimer = null;
11710
+ lastChildActivityAt = 0;
11711
+ consecutiveRequestTimeouts = 0;
11709
11712
  errorPrefix;
11710
11713
  logger;
11714
+ childEnv;
11711
11715
  constructor(binaryPath, cwd, options, configOverrides) {
11712
11716
  this.binaryPath = binaryPath;
11713
11717
  this.cwd = cwd;
@@ -11722,6 +11726,7 @@ class BinaryBridge {
11722
11726
  this.onBashPatternMatch = options?.onBashPatternMatch;
11723
11727
  this.errorPrefix = options?.errorPrefix ?? "[aft-bridge]";
11724
11728
  this.logger = options?.logger;
11729
+ this.childEnv = options?.childEnv;
11725
11730
  }
11726
11731
  logVia(message, meta) {
11727
11732
  const logger = this.logger ?? getActiveLogger();
@@ -11873,20 +11878,41 @@ class BinaryBridge {
11873
11878
  const line = `${JSON.stringify(request)}
11874
11879
  `;
11875
11880
  const keepBridgeOnTimeout = options?.keepBridgeOnTimeout === true;
11881
+ let requestSentAt = Date.now();
11876
11882
  const response = await new Promise((resolve, reject) => {
11877
11883
  const timer = setTimeout(() => {
11884
+ const entry = this.pending.get(id);
11885
+ if (!entry)
11886
+ return;
11878
11887
  this.pending.delete(id);
11879
- const restartSuffix = keepBridgeOnTimeout ? "" : " — restarting bridge";
11888
+ clearTimeout(entry.timer);
11889
+ if (keepBridgeOnTimeout) {
11890
+ const timeoutMsg2 = `Request "${command}" (id=${id}) timed out after ${effectiveTimeoutMs}ms`;
11891
+ if (requestSessionId) {
11892
+ this.sessionWarnVia(requestSessionId, timeoutMsg2);
11893
+ } else {
11894
+ this.warnVia(timeoutMsg2);
11895
+ }
11896
+ entry.reject(new Error(`${this.errorPrefix} Request "${command}" (id=${id}) timed out after ${effectiveTimeoutMs}ms`));
11897
+ return;
11898
+ }
11899
+ const childActiveSinceRequest = this.lastChildActivityAt > requestSentAt;
11900
+ const consecutiveTimeouts = this.consecutiveRequestTimeouts + 1;
11901
+ this.consecutiveRequestTimeouts = consecutiveTimeouts;
11902
+ const keepWarm = childActiveSinceRequest || consecutiveTimeouts < BRIDGE_HANG_TIMEOUT_THRESHOLD;
11903
+ const restartSuffix = keepWarm ? " — bridge kept warm" : " — restarting bridge";
11880
11904
  const timeoutMsg = `Request "${command}" (id=${id}) timed out after ${effectiveTimeoutMs}ms${restartSuffix}`;
11881
11905
  if (requestSessionId) {
11882
11906
  this.sessionWarnVia(requestSessionId, timeoutMsg);
11883
11907
  } else {
11884
11908
  this.warnVia(timeoutMsg);
11885
11909
  }
11886
- reject(new Error(`${this.errorPrefix} Request "${command}" (id=${id}) timed out after ${effectiveTimeoutMs}ms`));
11887
- if (!keepBridgeOnTimeout) {
11888
- this.handleTimeout(requestSessionId);
11910
+ if (keepWarm) {
11911
+ entry.reject(new Error(`${this.errorPrefix} request "${command}" timed out after ${effectiveTimeoutMs}ms (bridge busy/under load); bridge kept warm — retry`));
11912
+ return;
11889
11913
  }
11914
+ entry.reject(new Error(`${this.errorPrefix} Request "${command}" (id=${id}) timed out after ${effectiveTimeoutMs}ms`));
11915
+ this.handleTimeout(requestSessionId);
11890
11916
  }, effectiveTimeoutMs);
11891
11917
  this.pending.set(id, { resolve, reject, timer, onProgress: options?.onProgress });
11892
11918
  if (!this.process?.stdin?.writable) {
@@ -11895,6 +11921,7 @@ class BinaryBridge {
11895
11921
  reject(new Error(`${this.errorPrefix} stdin not writable for command "${command}"`));
11896
11922
  return;
11897
11923
  }
11924
+ requestSentAt = Date.now();
11898
11925
  this.process.stdin.write(line, (err) => {
11899
11926
  if (err) {
11900
11927
  const entry = this.pending.get(id);
@@ -12089,6 +12116,15 @@ class BinaryBridge {
12089
12116
  this.logVia(`ORT_DYLIB_PATH set from managed ONNX Runtime: ${ortLibraryPath}`);
12090
12117
  }
12091
12118
  }
12119
+ if (this.childEnv) {
12120
+ for (const [key, value] of Object.entries(this.childEnv)) {
12121
+ if (value === undefined) {
12122
+ delete env[key];
12123
+ } else {
12124
+ env[key] = value;
12125
+ }
12126
+ }
12127
+ }
12092
12128
  const child = spawn(this.binaryPath, [], {
12093
12129
  cwd: this.cwd,
12094
12130
  stdio: ["pipe", "pipe", "pipe"],
@@ -12138,6 +12174,8 @@ class BinaryBridge {
12138
12174
  this.process = child;
12139
12175
  this.stdoutBuffer = "";
12140
12176
  this.stderrBuffer = "";
12177
+ this.lastChildActivityAt = 0;
12178
+ this.consecutiveRequestTimeouts = 0;
12141
12179
  this.stderrTail = [];
12142
12180
  }
12143
12181
  pushStderrLine(line) {
@@ -12193,6 +12231,7 @@ class BinaryBridge {
12193
12231
  continue;
12194
12232
  try {
12195
12233
  const response = JSON.parse(line);
12234
+ this.lastChildActivityAt = Date.now();
12196
12235
  if (response.type === "progress") {
12197
12236
  const requestId = response.request_id;
12198
12237
  const entry = requestId ? this.pending.get(requestId) : undefined;
@@ -12245,6 +12284,7 @@ class BinaryBridge {
12245
12284
  continue;
12246
12285
  this.pending.delete(id);
12247
12286
  clearTimeout(entry.timer);
12287
+ this.consecutiveRequestTimeouts = 0;
12248
12288
  this.scheduleRestartCountReset();
12249
12289
  entry.resolve(response);
12250
12290
  } else if (typeof response.type === "string") {
@@ -12256,6 +12296,7 @@ class BinaryBridge {
12256
12296
  }
12257
12297
  }
12258
12298
  handleTimeout(triggeringSessionId) {
12299
+ this.consecutiveRequestTimeouts = 0;
12259
12300
  this.rejectAllPending(new Error(`${this.errorPrefix} bridge killed during sibling timeout — request aborted`));
12260
12301
  if (this.process) {
12261
12302
  this.process.kill("SIGKILL");
@@ -12334,7 +12375,7 @@ class BinaryBridge {
12334
12375
  // ../aft-bridge/dist/downloader.js
12335
12376
  import { spawnSync } from "node:child_process";
12336
12377
  import { createHash, randomUUID } from "node:crypto";
12337
- import { chmodSync, closeSync, createWriteStream, existsSync, mkdirSync, openSync, readFileSync, renameSync, rmSync, statSync, unlinkSync, writeSync } from "node:fs";
12378
+ import { chmodSync, closeSync, copyFileSync, createWriteStream, existsSync, mkdirSync, openSync, readFileSync, renameSync, rmSync, statSync, unlinkSync, writeSync } from "node:fs";
12338
12379
  import { homedir as homedir2 } from "node:os";
12339
12380
  import { join as join2 } from "node:path";
12340
12381
  import { Readable } from "node:stream";
@@ -12501,10 +12542,18 @@ async function downloadBinary(version) {
12501
12542
  throw new Error(`Checksum mismatch for ${assetName}: expected ${expectedHash}, got ${actualHash}. ` + "The binary may have been tampered with.");
12502
12543
  }
12503
12544
  log(`Checksum verified (SHA-256: ${actualHash.slice(0, 16)}...)`);
12504
- if (process.platform !== "win32") {
12545
+ if (process.platform === "win32") {
12546
+ copyFileSync(tmpPath, binaryPath);
12547
+ } else {
12505
12548
  chmodSync(tmpPath, 493);
12549
+ renameSync(tmpPath, binaryPath);
12550
+ }
12551
+ try {
12552
+ if (existsSync(tmpPath))
12553
+ unlinkSync(tmpPath);
12554
+ } catch {
12555
+ warn(`Could not clean up temporary download file ${tmpPath} — it can be removed manually.`);
12506
12556
  }
12507
- renameSync(tmpPath, binaryPath);
12508
12557
  log(`AFT binary ready at ${binaryPath}`);
12509
12558
  return binaryPath;
12510
12559
  } catch (err) {
@@ -12668,7 +12717,7 @@ function markAnnouncementSeen(storageRoot, harness, currentVersion) {
12668
12717
 
12669
12718
  // ../aft-bridge/dist/resolver.js
12670
12719
  import { execSync } from "node:child_process";
12671
- import { chmodSync as chmodSync2, copyFileSync, existsSync as existsSync3, mkdirSync as mkdirSync3, renameSync as renameSync3 } from "node:fs";
12720
+ import { chmodSync as chmodSync2, copyFileSync as copyFileSync2, existsSync as existsSync3, mkdirSync as mkdirSync3, renameSync as renameSync3, unlinkSync as unlinkSync2 } from "node:fs";
12672
12721
  import { createRequire as createRequire2 } from "node:module";
12673
12722
  import { homedir as homedir3 } from "node:os";
12674
12723
  import { join as join4 } from "node:path";
@@ -12691,10 +12740,15 @@ function copyToVersionedCache(npmBinaryPath, knownVersion) {
12691
12740
  }
12692
12741
  mkdirSync3(versionedDir, { recursive: true });
12693
12742
  const tmpPath = `${cachedPath}.${process.pid}.${Date.now()}.tmp`;
12694
- copyFileSync(npmBinaryPath, tmpPath);
12743
+ copyFileSync2(npmBinaryPath, tmpPath);
12695
12744
  if (process.platform !== "win32") {
12696
12745
  chmodSync2(tmpPath, 493);
12697
12746
  }
12747
+ if (process.platform === "win32" && existsSync3(cachedPath)) {
12748
+ try {
12749
+ unlinkSync2(cachedPath);
12750
+ } catch {}
12751
+ }
12698
12752
  renameSync3(tmpPath, cachedPath);
12699
12753
  log(`Copied npm binary to versioned cache: ${cachedPath}`);
12700
12754
  return cachedPath;
@@ -12946,7 +13000,7 @@ async function ensureStorageMigrated(opts) {
12946
13000
  // ../aft-bridge/dist/onnx-runtime.js
12947
13001
  import { execFileSync } from "node:child_process";
12948
13002
  import { createHash as createHash2 } from "node:crypto";
12949
- 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";
13003
+ 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";
12950
13004
  import { basename, dirname as dirname3, isAbsolute, join as join6, relative, resolve, win32 } from "node:path";
12951
13005
  import { Readable as Readable2 } from "node:stream";
12952
13006
  import { pipeline as pipeline2 } from "node:stream/promises";
@@ -13185,20 +13239,64 @@ function findSystemOnnxRuntime(libName) {
13185
13239
  } else if (process.platform === "linux") {
13186
13240
  searchPaths.push("/usr/lib", "/usr/lib/x86_64-linux-gnu", "/usr/lib/aarch64-linux-gnu", "/usr/local/lib");
13187
13241
  } else if (process.platform === "win32") {
13242
+ const programFiles = process.env.ProgramFiles ?? "C:\\Program Files";
13243
+ const programFilesX86 = process.env["ProgramFiles(x86)"] ?? "C:\\Program Files (x86)";
13244
+ searchPaths.push(join6(programFiles, "onnxruntime", "lib"), join6(programFiles, "Microsoft ONNX Runtime", "lib"), join6(programFiles, "Microsoft Machine Learning", "lib"), join6(programFilesX86, "onnxruntime", "lib"), ...(() => {
13245
+ const nugetPaths = [];
13246
+ const userProfile = process.env.USERPROFILE ?? "";
13247
+ if (!userProfile)
13248
+ return nugetPaths;
13249
+ const nugetPackageDir = join6(userProfile, ".nuget", "packages", "microsoft.ml.onnxruntime");
13250
+ if (!existsSync5(nugetPackageDir))
13251
+ return nugetPaths;
13252
+ try {
13253
+ for (const entry of readdirSync(nugetPackageDir, { withFileTypes: true })) {
13254
+ if (!entry.isDirectory())
13255
+ continue;
13256
+ if (entry.name === "__globalPackagesFolder" || entry.name.startsWith("."))
13257
+ continue;
13258
+ nugetPaths.push(join6(nugetPackageDir, entry.name, "runtimes", "win-x64", "native"), join6(nugetPackageDir, entry.name, "runtimes", "win-arm64", "native"));
13259
+ }
13260
+ } catch (err) {
13261
+ warn(`Failed to scan NuGet ONNX Runtime cache ${nugetPackageDir}: ${err instanceof Error ? err.message : String(err)}`);
13262
+ }
13263
+ return nugetPaths;
13264
+ })());
13188
13265
  searchPaths.push(...pathEntriesForPlatform());
13189
13266
  }
13267
+ const normalizeCase = process.platform === "win32" || process.platform === "darwin";
13190
13268
  const seen = new Set;
13191
- for (const dir of searchPaths) {
13192
- if (seen.has(dir))
13193
- continue;
13194
- seen.add(dir);
13195
- if (!directoryContainsLibrary(dir, libName))
13196
- continue;
13197
- const version = detectOnnxVersion(dir, libName);
13198
- if (version && !isOnnxVersionCompatible(version)) {
13199
- 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.`);
13269
+ const uniquePaths = searchPaths.filter((p) => {
13270
+ let key = resolve(p).replace(/[/\\]+$/, "");
13271
+ if (normalizeCase)
13272
+ key = key.toLowerCase();
13273
+ if (seen.has(key))
13274
+ return false;
13275
+ seen.add(key);
13276
+ return true;
13277
+ });
13278
+ for (const dir of uniquePaths) {
13279
+ const libPath = join6(dir, libName);
13280
+ if (process.platform === "win32") {
13281
+ if (!directoryContainsLibrary(dir, libName))
13282
+ continue;
13283
+ } else if (!existsSync5(libPath)) {
13200
13284
  continue;
13201
13285
  }
13286
+ let skipVersionCheck = false;
13287
+ if (process.platform === "win32") {
13288
+ const dirLower = dir.toLowerCase();
13289
+ const isCommonPath = dirLower.includes("program files") || dirLower.includes("onnxruntime");
13290
+ if (!isCommonPath)
13291
+ skipVersionCheck = true;
13292
+ }
13293
+ if (!skipVersionCheck) {
13294
+ const version = detectOnnxVersion(dir, libName);
13295
+ if (version && !isOnnxVersionCompatible(version)) {
13296
+ 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.`);
13297
+ continue;
13298
+ }
13299
+ }
13202
13300
  return dir;
13203
13301
  }
13204
13302
  return null;
@@ -13236,7 +13334,7 @@ async function downloadFileWithCap(url, destPath) {
13236
13334
  await pipeline2(nodeStream, createWriteStream2(destPath), { signal: controller.signal });
13237
13335
  } catch (err) {
13238
13336
  try {
13239
- unlinkSync2(destPath);
13337
+ unlinkSync3(destPath);
13240
13338
  } catch {}
13241
13339
  throw err;
13242
13340
  } finally {
@@ -13297,7 +13395,7 @@ async function downloadOnnxRuntime(info, targetDir) {
13297
13395
  await extractZipArchive(archivePath, tmpDir);
13298
13396
  }
13299
13397
  try {
13300
- unlinkSync2(archivePath);
13398
+ unlinkSync3(archivePath);
13301
13399
  } catch {}
13302
13400
  validateExtractedTree(tmpDir);
13303
13401
  const extractedDir = join6(tmpDir, info.assetName, "lib");
@@ -13346,7 +13444,7 @@ async function downloadOnnxRuntime(info, targetDir) {
13346
13444
  return null;
13347
13445
  }
13348
13446
  }
13349
- function copyOnnxLibraries(info, extractedDir, targetDir, realFiles, symlinks, copyFile = copyFileSync2) {
13447
+ function copyOnnxLibraries(info, extractedDir, targetDir, realFiles, symlinks, copyFile = copyFileSync3) {
13350
13448
  const requiredLibs = new Set([info.libName]);
13351
13449
  for (const libFile of realFiles) {
13352
13450
  const src = join6(extractedDir, libFile);
@@ -13367,7 +13465,7 @@ function copyOnnxLibraries(info, extractedDir, targetDir, realFiles, symlinks, c
13367
13465
  for (const link of symlinks) {
13368
13466
  const dst = join6(targetDir, link.name);
13369
13467
  try {
13370
- unlinkSync2(dst);
13468
+ unlinkSync3(dst);
13371
13469
  } catch {}
13372
13470
  try {
13373
13471
  symlinkSync(link.target, dst);
@@ -13478,7 +13576,7 @@ ${new Date().toISOString()}
13478
13576
  }
13479
13577
  log(`[onnx] reclaiming install lock (owner_pid=${owningPid ?? "unknown"}, alive=${ownerAlive}, age_ms=${age})`);
13480
13578
  try {
13481
- unlinkSync2(lockPath);
13579
+ unlinkSync3(lockPath);
13482
13580
  } catch {}
13483
13581
  return tryClaim();
13484
13582
  }
@@ -13503,7 +13601,7 @@ function releaseLock(lockPath) {
13503
13601
  return;
13504
13602
  }
13505
13603
  try {
13506
- unlinkSync2(lockPath);
13604
+ unlinkSync3(lockPath);
13507
13605
  } catch (unlinkErr) {
13508
13606
  const code = unlinkErr.code;
13509
13607
  if (code !== "ENOENT") {
@@ -14738,7 +14836,7 @@ function formatDuration(completion) {
14738
14836
 
14739
14837
  // src/config.ts
14740
14838
  var import_comment_json = __toESM(require_src2(), 1);
14741
- import { existsSync as existsSync6, readFileSync as readFileSync4, renameSync as renameSync4, unlinkSync as unlinkSync3, writeFileSync as writeFileSync3 } from "node:fs";
14839
+ import { existsSync as existsSync6, readFileSync as readFileSync4, renameSync as renameSync4, unlinkSync as unlinkSync4, writeFileSync as writeFileSync3 } from "node:fs";
14742
14840
  import { homedir as homedir5 } from "node:os";
14743
14841
  import { join as join8 } from "node:path";
14744
14842
 
@@ -28326,6 +28424,7 @@ var LspConfigSchema = exports_external.object({
28326
28424
  servers: exports_external.record(exports_external.string().trim().min(1), LspServerEntrySchema).optional(),
28327
28425
  disabled: exports_external.array(exports_external.string().trim().min(1)).optional(),
28328
28426
  python: exports_external.enum(["pyright", "ty", "auto"]).optional(),
28427
+ diagnostics_on_edit: exports_external.boolean().optional(),
28329
28428
  auto_install: exports_external.boolean().optional(),
28330
28429
  grace_days: exports_external.number().int().positive().optional(),
28331
28430
  versions: exports_external.record(exports_external.string().trim().min(1), exports_external.string().trim().min(1)).optional()
@@ -28349,6 +28448,24 @@ var BashFeaturesSchema = exports_external.object({
28349
28448
  long_running_reminder_interval_ms: exports_external.number().int().positive().optional()
28350
28449
  });
28351
28450
  var BashConfigSchema = exports_external.union([exports_external.boolean(), BashFeaturesSchema]);
28451
+ var InspectConfigSchema = exports_external.object({
28452
+ enabled: exports_external.boolean().optional(),
28453
+ tier2_idle_minutes: exports_external.number().min(0).optional(),
28454
+ categories: exports_external.record(exports_external.string(), exports_external.boolean()).optional(),
28455
+ tier2_soft_deadline_ms: exports_external.number().int().positive().optional(),
28456
+ max_drill_down_items: exports_external.number().int().positive().max(100).optional(),
28457
+ duplicates: exports_external.object({
28458
+ lower_bound: exports_external.number().int().positive().optional(),
28459
+ discard_cost: exports_external.number().int().min(0).optional(),
28460
+ anonymize: exports_external.object({
28461
+ variables: exports_external.boolean().optional(),
28462
+ fields: exports_external.boolean().optional(),
28463
+ methods: exports_external.boolean().optional(),
28464
+ types: exports_external.boolean().optional(),
28465
+ literals: exports_external.boolean().optional()
28466
+ }).optional()
28467
+ }).optional()
28468
+ });
28352
28469
  var AftConfigSchema = exports_external.object({
28353
28470
  $schema: exports_external.string().optional(),
28354
28471
  format_on_edit: exports_external.boolean().optional(),
@@ -28362,6 +28479,7 @@ var AftConfigSchema = exports_external.object({
28362
28479
  restrict_to_project_root: exports_external.boolean().optional(),
28363
28480
  search_index: exports_external.boolean().optional(),
28364
28481
  semantic_search: exports_external.boolean().optional(),
28482
+ inspect: InspectConfigSchema.optional(),
28365
28483
  bash: BashConfigSchema.optional(),
28366
28484
  experimental: ExperimentalConfigSchema.optional(),
28367
28485
  lsp: LspConfigSchema.optional(),
@@ -28438,6 +28556,8 @@ function resolveProjectOverridesForConfigure(config2) {
28438
28556
  Object.assign(overrides, resolveLspConfigForConfigure(config2));
28439
28557
  if (config2.semantic !== undefined)
28440
28558
  overrides.semantic = config2.semantic;
28559
+ if (config2.inspect !== undefined)
28560
+ overrides.inspect = config2.inspect;
28441
28561
  if (config2.max_callgraph_files !== undefined)
28442
28562
  overrides.max_callgraph_files = config2.max_callgraph_files;
28443
28563
  return overrides;
@@ -28653,7 +28773,7 @@ ${serialized}` : serialized;
28653
28773
  } catch (err) {
28654
28774
  if (tmpPath) {
28655
28775
  try {
28656
- unlinkSync3(tmpPath);
28776
+ unlinkSync4(tmpPath);
28657
28777
  } catch {}
28658
28778
  }
28659
28779
  if (isWritableMigrationError(err)) {
@@ -28746,6 +28866,9 @@ function mergeLspConfig(baseLsp, overrideLsp) {
28746
28866
  const projectLsp = {};
28747
28867
  if (overrideLsp?.python !== undefined)
28748
28868
  projectLsp.python = overrideLsp.python;
28869
+ if (overrideLsp?.diagnostics_on_edit !== undefined) {
28870
+ projectLsp.diagnostics_on_edit = overrideLsp.diagnostics_on_edit;
28871
+ }
28749
28872
  const userDisabled = baseLsp?.disabled ?? [];
28750
28873
  const lsp = {
28751
28874
  ...baseLsp,
@@ -28757,6 +28880,27 @@ function mergeLspConfig(baseLsp, overrideLsp) {
28757
28880
  }
28758
28881
  return Object.fromEntries(Object.entries(lsp).filter(([, value]) => value !== undefined));
28759
28882
  }
28883
+ function mergeInspectConfig(baseInspect, overrideInspect) {
28884
+ const inspect = {
28885
+ ...baseInspect,
28886
+ ...overrideInspect,
28887
+ duplicates: baseInspect?.duplicates || overrideInspect?.duplicates ? {
28888
+ ...baseInspect?.duplicates,
28889
+ ...overrideInspect?.duplicates,
28890
+ anonymize: baseInspect?.duplicates?.anonymize || overrideInspect?.duplicates?.anonymize ? {
28891
+ ...baseInspect?.duplicates?.anonymize,
28892
+ ...overrideInspect?.duplicates?.anonymize
28893
+ } : undefined
28894
+ } : undefined
28895
+ };
28896
+ if (inspect.duplicates && inspect.duplicates.anonymize === undefined) {
28897
+ delete inspect.duplicates.anonymize;
28898
+ }
28899
+ if (Object.values(inspect).every((value) => value === undefined)) {
28900
+ return;
28901
+ }
28902
+ return Object.fromEntries(Object.entries(inspect).filter(([, value]) => value !== undefined));
28903
+ }
28760
28904
  function mergeBashConfig(baseBash, overrideBash) {
28761
28905
  if (baseBash === undefined && overrideBash === undefined)
28762
28906
  return;
@@ -28816,6 +28960,7 @@ var PROJECT_SAFE_TOP_LEVEL_FIELDS = new Set([
28816
28960
  "validate_on_edit",
28817
28961
  "search_index",
28818
28962
  "semantic_search",
28963
+ "inspect",
28819
28964
  "experimental",
28820
28965
  "bash"
28821
28966
  ]);
@@ -28848,8 +28993,10 @@ function mergeConfigs(base, override) {
28848
28993
  const lsp = mergeLspConfig(base.lsp, override.lsp);
28849
28994
  const experimental = mergeExperimentalConfig(base.experimental, override.experimental);
28850
28995
  const bash = mergeBashConfig(base.bash, override.bash);
28996
+ const inspect = mergeInspectConfig(base.inspect, override.inspect);
28851
28997
  const safeOverride = pickProjectSafeFields(override);
28852
28998
  delete safeOverride.bash;
28999
+ delete safeOverride.inspect;
28853
29000
  return {
28854
29001
  ...base,
28855
29002
  ...safeOverride,
@@ -28857,6 +29004,7 @@ function mergeConfigs(base, override) {
28857
29004
  ...Object.keys(checker).length > 0 ? { checker } : {},
28858
29005
  ...lsp ? { lsp } : {},
28859
29006
  ...bash !== undefined ? { bash } : {},
29007
+ ...inspect !== undefined ? { inspect } : {},
28860
29008
  experimental,
28861
29009
  semantic,
28862
29010
  ...disabledTools.length > 0 ? { disabled_tools: [...new Set(disabledTools)] } : {}
@@ -29603,7 +29751,7 @@ import {
29603
29751
  openSync as openSync4,
29604
29752
  readFileSync as readFileSync8,
29605
29753
  statSync as statSync4,
29606
- unlinkSync as unlinkSync4,
29754
+ unlinkSync as unlinkSync5,
29607
29755
  writeFileSync as writeFileSync7
29608
29756
  } from "node:fs";
29609
29757
  import { homedir as homedir8 } from "node:os";
@@ -29733,7 +29881,7 @@ ${new Date().toISOString()}
29733
29881
  }
29734
29882
  log2(`[lsp] reclaiming install lock for ${lockKey} (owner_pid=${owningPid ?? "unknown"}, alive=${ownerAlive}, age_ms=${age})`);
29735
29883
  try {
29736
- unlinkSync4(lock);
29884
+ unlinkSync5(lock);
29737
29885
  } catch {}
29738
29886
  return tryClaim();
29739
29887
  }
@@ -29770,7 +29918,7 @@ function releaseInstallLock(lockKey) {
29770
29918
  return;
29771
29919
  }
29772
29920
  try {
29773
- unlinkSync4(lock);
29921
+ unlinkSync5(lock);
29774
29922
  } catch (unlinkErr) {
29775
29923
  const code = unlinkErr.code;
29776
29924
  if (code !== "ENOENT") {
@@ -30427,7 +30575,7 @@ function runAutoInstall(projectRoot, config2, fetchImpl = fetch) {
30427
30575
  import { execFileSync as execFileSync2 } from "node:child_process";
30428
30576
  import { createHash as createHash5, randomBytes } from "node:crypto";
30429
30577
  import {
30430
- copyFileSync as copyFileSync3,
30578
+ copyFileSync as copyFileSync4,
30431
30579
  createReadStream as createReadStream2,
30432
30580
  createWriteStream as createWriteStream3,
30433
30581
  existsSync as existsSync11,
@@ -30440,7 +30588,7 @@ import {
30440
30588
  renameSync as renameSync7,
30441
30589
  rmSync as rmSync6,
30442
30590
  statSync as statSync6,
30443
- unlinkSync as unlinkSync5,
30591
+ unlinkSync as unlinkSync6,
30444
30592
  writeFileSync as writeFileSync8
30445
30593
  } from "node:fs";
30446
30594
  import { dirname as dirname7, join as join15, relative as relative2, resolve as resolve3 } from "node:path";
@@ -30802,7 +30950,7 @@ async function downloadFile(url2, destPath, fetchImpl, assetSize, signal) {
30802
30950
  await pipeline3(nodeStream, createWriteStream3(destPath), { signal: timeout.signal });
30803
30951
  } catch (err) {
30804
30952
  try {
30805
- unlinkSync5(destPath);
30953
+ unlinkSync6(destPath);
30806
30954
  } catch {}
30807
30955
  throw err;
30808
30956
  } finally {
@@ -31034,7 +31182,7 @@ async function downloadAndInstall(spec, tag, assets, platform2, arch, fetchImpl,
31034
31182
  } catch (err) {
31035
31183
  error2(`[lsp] hash ${spec.id} failed: ${err}`);
31036
31184
  try {
31037
- unlinkSync5(archivePath);
31185
+ unlinkSync6(archivePath);
31038
31186
  } catch {}
31039
31187
  return null;
31040
31188
  }
@@ -31045,7 +31193,7 @@ async function downloadAndInstall(spec, tag, assets, platform2, arch, fetchImpl,
31045
31193
  if (previousArchiveSha256 !== archiveSha256) {
31046
31194
  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. ` + `Run \`aft doctor --clear\` to wipe the cache and force a fresh install if you've verified the change.`);
31047
31195
  try {
31048
- unlinkSync5(archivePath);
31196
+ unlinkSync6(archivePath);
31049
31197
  } catch {}
31050
31198
  return null;
31051
31199
  }
@@ -31057,7 +31205,7 @@ async function downloadAndInstall(spec, tag, assets, platform2, arch, fetchImpl,
31057
31205
  return null;
31058
31206
  } finally {
31059
31207
  try {
31060
- unlinkSync5(archivePath);
31208
+ unlinkSync6(archivePath);
31061
31209
  } catch {}
31062
31210
  }
31063
31211
  const innerBinaryPath = join15(extractDir, spec.binaryPathInArchive(platform2, arch, version2));
@@ -31068,7 +31216,7 @@ async function downloadAndInstall(spec, tag, assets, platform2, arch, fetchImpl,
31068
31216
  const targetBinary = ghBinaryPath(spec, platform2);
31069
31217
  mkdirSync9(dirname7(targetBinary), { recursive: true });
31070
31218
  try {
31071
- copyFileSync3(innerBinaryPath, targetBinary);
31219
+ copyFileSync4(innerBinaryPath, targetBinary);
31072
31220
  if (platform2 !== "win32") {
31073
31221
  const { chmodSync: chmodSync4 } = await import("node:fs");
31074
31222
  chmodSync4(targetBinary, 493);
@@ -31426,13 +31574,21 @@ async function getSessionMessages(client, sessionId) {
31426
31574
  async function sendIgnoredMessage(client, sessionId, text) {
31427
31575
  try {
31428
31576
  const c = client;
31429
- const agent = await resolveCurrentAgent(c, sessionId);
31577
+ const promptContext = await resolvePromptContext(c, sessionId);
31430
31578
  const body = {
31431
31579
  noReply: true,
31432
31580
  parts: [{ type: "text", text, ignored: true }]
31433
31581
  };
31434
- if (agent)
31435
- body.agent = agent;
31582
+ if (promptContext?.agent)
31583
+ body.agent = promptContext.agent;
31584
+ if (promptContext?.model) {
31585
+ body.model = {
31586
+ providerID: promptContext.model.providerID,
31587
+ modelID: promptContext.model.modelID
31588
+ };
31589
+ }
31590
+ if (promptContext?.variant)
31591
+ body.variant = promptContext.variant;
31436
31592
  const promptInput = {
31437
31593
  path: { id: sessionId },
31438
31594
  body
@@ -31450,14 +31606,6 @@ async function sendIgnoredMessage(client, sessionId, text) {
31450
31606
  }
31451
31607
  return false;
31452
31608
  }
31453
- async function resolveCurrentAgent(client, sessionId) {
31454
- try {
31455
- const ctx = await resolvePromptContext(client, sessionId);
31456
- return ctx?.agent;
31457
- } catch {
31458
- return;
31459
- }
31460
- }
31461
31609
  async function deleteMessage(serverUrl, sessionId, messageId) {
31462
31610
  const auth = getServerAuth();
31463
31611
  const url2 = `${serverUrl}/session/${encodeURIComponent(sessionId)}/message/${encodeURIComponent(messageId)}`;
@@ -31534,13 +31682,18 @@ function persistAnnouncedVersion(storageDir, version2) {
31534
31682
  markAnnouncementSeen(storageDir, "opencode", version2);
31535
31683
  }
31536
31684
  async function readWarnedTools(bridge) {
31685
+ let resp;
31686
+ try {
31687
+ resp = await bridge.send("db_get_state", { key: "warned_tools" });
31688
+ } catch {
31689
+ return null;
31690
+ }
31691
+ if (resp.success === false)
31692
+ return null;
31693
+ const value = resp.data?.value;
31694
+ if (typeof value !== "string")
31695
+ return {};
31537
31696
  try {
31538
- const resp = await bridge.send("db_get_state", { key: "warned_tools" });
31539
- if (resp.success === false)
31540
- return {};
31541
- const value = resp.data?.value;
31542
- if (typeof value !== "string")
31543
- return {};
31544
31697
  const parsed = JSON.parse(value);
31545
31698
  if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
31546
31699
  return {};
@@ -31549,12 +31702,16 @@ async function readWarnedTools(bridge) {
31549
31702
  return {};
31550
31703
  }
31551
31704
  }
31552
- async function hasWarnedFor(bridge, key) {
31705
+ async function warnedStatus(bridge, key) {
31553
31706
  const warned = await readWarnedTools(bridge);
31554
- return warned[key] === true || typeof warned[key] === "string";
31707
+ if (warned === null)
31708
+ return "unknown";
31709
+ return warned[key] === true || typeof warned[key] === "string" ? "warned" : "fresh";
31555
31710
  }
31556
31711
  async function recordWarning(bridge, key) {
31557
31712
  const warned = await readWarnedTools(bridge);
31713
+ if (warned === null)
31714
+ return;
31558
31715
  warned[key] = true;
31559
31716
  try {
31560
31717
  await bridge.send("db_set_state", {
@@ -31606,7 +31763,8 @@ async function deliverConfigureWarnings(opts, warnings) {
31606
31763
  return;
31607
31764
  for (const warning of warnings) {
31608
31765
  const key = warningKey(warning, opts.projectRoot);
31609
- if (await hasWarnedFor(opts.bridge, key))
31766
+ const status = await warnedStatus(opts.bridge, key);
31767
+ if (status !== "fresh")
31610
31768
  continue;
31611
31769
  const delivered = await sendIgnoredMessage(opts.client, opts.sessionId, formatConfigureWarning(warning));
31612
31770
  if (!delivered)
@@ -31755,7 +31913,7 @@ function writeTerminal(terminal, data) {
31755
31913
 
31756
31914
  // src/shared/rpc-server.ts
31757
31915
  import { randomBytes as randomBytes2 } from "node:crypto";
31758
- import { mkdirSync as mkdirSync10, renameSync as renameSync8, unlinkSync as unlinkSync6, writeFileSync as writeFileSync9 } from "node:fs";
31916
+ import { mkdirSync as mkdirSync10, renameSync as renameSync8, unlinkSync as unlinkSync7, writeFileSync as writeFileSync9 } from "node:fs";
31759
31917
  import { createServer } from "node:http";
31760
31918
  import { dirname as dirname8, join as join18 } from "node:path";
31761
31919
 
@@ -31829,7 +31987,7 @@ class AftRpcServer {
31829
31987
  }
31830
31988
  this.token = null;
31831
31989
  try {
31832
- unlinkSync6(this.portFilePath);
31990
+ unlinkSync7(this.portFilePath);
31833
31991
  } catch {}
31834
31992
  }
31835
31993
  dispatch(req, res) {
@@ -32215,24 +32373,28 @@ function getState() {
32215
32373
  }
32216
32374
  return g[GLOBAL_KEY];
32217
32375
  }
32218
- var shuttingDown = false;
32376
+ var runningCleanups = false;
32219
32377
  async function runCleanups(reason) {
32220
- if (shuttingDown)
32221
- return;
32222
- shuttingDown = true;
32223
- const state = getState();
32224
- if (state.cleanups.size === 0)
32378
+ if (runningCleanups)
32225
32379
  return;
32226
- log2(`Shutdown triggered by ${reason} — running ${state.cleanups.size} cleanup(s)`);
32227
- const cleanups = Array.from(state.cleanups);
32228
- state.cleanups.clear();
32229
- await Promise.allSettled(cleanups.map(async (fn) => {
32230
- try {
32231
- await fn();
32232
- } catch (err) {
32233
- log2(`Cleanup error: ${err.message}`);
32234
- }
32235
- }));
32380
+ runningCleanups = true;
32381
+ try {
32382
+ const state = getState();
32383
+ if (state.cleanups.size === 0)
32384
+ return;
32385
+ log2(`Shutdown triggered by ${reason} — running ${state.cleanups.size} cleanup(s)`);
32386
+ const cleanups = Array.from(state.cleanups);
32387
+ state.cleanups.clear();
32388
+ await Promise.allSettled(cleanups.map(async (fn) => {
32389
+ try {
32390
+ await fn();
32391
+ } catch (err) {
32392
+ log2(`Cleanup error: ${err.message}`);
32393
+ }
32394
+ }));
32395
+ } finally {
32396
+ runningCleanups = false;
32397
+ }
32236
32398
  }
32237
32399
  function installProcessHandlers() {
32238
32400
  const state = getState();
@@ -33082,7 +33244,14 @@ function applyUpdateChunks(originalContent, filePath, chunks) {
33082
33244
  lineIndex = contextIdx + 1;
33083
33245
  }
33084
33246
  if (chunk.old_lines.length === 0) {
33085
- const insertionIdx = originalLines.length > 0 && originalLines[originalLines.length - 1] === "" ? originalLines.length - 1 : originalLines.length;
33247
+ let insertionIdx;
33248
+ if (chunk.change_context) {
33249
+ insertionIdx = lineIndex;
33250
+ } else if (originalLines.length > 0 && originalLines[originalLines.length - 1] === "") {
33251
+ insertionIdx = originalLines.length - 1;
33252
+ } else {
33253
+ insertionIdx = originalLines.length;
33254
+ }
33086
33255
  replacements.push([insertionIdx, 0, chunk.new_lines]);
33087
33256
  continue;
33088
33257
  }
@@ -34067,6 +34236,10 @@ function inferBeforeStart(ops, from, beforeLen) {
34067
34236
  return beforeLen;
34068
34237
  }
34069
34238
  var z7 = tool7.schema;
34239
+ 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.";
34240
+ function diagnosticsOnEditDefault(ctx) {
34241
+ return ctx.config.lsp?.diagnostics_on_edit ?? false;
34242
+ }
34070
34243
  var READ_DESCRIPTION = `Read file contents or list directory entries.
34071
34244
 
34072
34245
  Use either startLine/endLine OR offset/limit to read a section of a file.
@@ -34084,8 +34257,7 @@ Examples:
34084
34257
  Read lines 50-100: { "filePath": "src/app.ts", "startLine": 50, "endLine": 100 }
34085
34258
  Read 30 lines from line 200: { "filePath": "src/app.ts", "offset": 200, "limit": 30 }
34086
34259
  List directory: { "filePath": "src/" }
34087
-
34088
- Returns: Line-numbered file content string. For directories: newline-joined sorted entries. For binary files: size/message string.`;
34260
+ `;
34089
34261
  function createReadTool(ctx) {
34090
34262
  return {
34091
34263
  description: READ_DESCRIPTION,
@@ -34210,24 +34382,26 @@ function getWriteDescription(editToolName) {
34210
34382
 
34211
34383
  Automatically creates parent directories. Backs up existing files before overwriting.
34212
34384
  If the project has a formatter configured (biome, prettier, rustfmt, etc.), the file
34213
- is auto-formatted after writing. Returns inline LSP diagnostics when available.
34385
+ is auto-formatted after writing. Edits return as soon as the write completes unless
34386
+ the configured \`lsp.diagnostics_on_edit\` default or a per-call \`diagnostics: true\`
34387
+ asks for legacy sync-wait behavior. Call \`aft_inspect\` afterward to check
34388
+ diagnostics across a batch of edits.
34214
34389
 
34215
34390
  **Behavior:**
34216
34391
  - Creates parent directories automatically (no need to mkdir first)
34217
34392
  - Existing files are backed up before overwriting (recoverable via aft_safety undo)
34218
34393
  - Auto-formats using project formatter if configured (biome.json, .prettierrc, etc.)
34219
- - Returns LSP error-level diagnostics inline if type errors are introduced
34394
+ - LSP diagnostics follow \`lsp.diagnostics_on_edit\` by default; pass \`diagnostics\` to override per call
34220
34395
  - Use this for creating new files or completely replacing file contents
34221
- - For partial edits (find/replace), use the \`${editToolName}\` tool instead
34222
-
34223
- Returns: Status message string (for example: "Created new file. Auto-formatted.") with optional inline LSP error lines.`;
34396
+ - For partial edits (find/replace), use the \`${editToolName}\` tool instead`;
34224
34397
  }
34225
34398
  function createWriteTool(ctx, editToolName = "edit") {
34226
34399
  return {
34227
34400
  description: getWriteDescription(editToolName),
34228
34401
  args: {
34229
34402
  filePath: z7.string().describe("Path to the file to write (absolute or relative to project root)"),
34230
- content: z7.string().describe("The full content to write to the file")
34403
+ content: z7.string().describe("The full content to write to the file"),
34404
+ diagnostics: z7.boolean().optional().describe(DIAGNOSTICS_PARAM_DESCRIPTION)
34231
34405
  },
34232
34406
  execute: async (args, context) => {
34233
34407
  const file2 = args.filePath;
@@ -34249,8 +34423,8 @@ function createWriteTool(ctx, editToolName = "edit") {
34249
34423
  file: filePath,
34250
34424
  content,
34251
34425
  create_dirs: true,
34252
- diagnostics: true,
34253
- include_diff: true
34426
+ diagnostics: args.diagnostics ?? diagnosticsOnEditDefault(ctx),
34427
+ include_diff_content: true
34254
34428
  });
34255
34429
  if (data.success === false) {
34256
34430
  throw new Error(data.message || "write failed");
@@ -34280,7 +34454,7 @@ LSP errors detected, please fix:
34280
34454
  if (pendingServers && pendingServers.length > 0) {
34281
34455
  output += `
34282
34456
 
34283
- Note: LSP server(s) did not respond in time: ${pendingServers.join(", ")}. Diagnostics may be incomplete; rerun lsp_diagnostics later for a fresh check.`;
34457
+ Note: LSP server(s) did not respond in time: ${pendingServers.join(", ")}. Diagnostics may be incomplete; call aft_inspect for a checkpoint diagnostics snapshot.`;
34284
34458
  }
34285
34459
  if (exitedServers && exitedServers.length > 0) {
34286
34460
  output += `
@@ -34362,11 +34536,8 @@ Note: Modes 6 and 7 are options on mode 5 (find/replace) — they require \`oldS
34362
34536
  - Auto-formats using project formatter if configured
34363
34537
  - Tree-sitter syntax validation on all edits
34364
34538
  - Symbol replace includes decorators, attributes, and doc comments in range
34365
- - LSP error-level diagnostics are returned automatically after edits
34366
-
34367
- Returns: JSON string for the selected edit mode. Edits may append inline LSP error lines.
34368
-
34369
- Common response fields: success (boolean), diff (object with before/after), backup_id (string), syntax_valid (boolean). Exact fields vary by mode.`;
34539
+ - 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.
34540
+ - Response is a JSON string for the selected edit mode; key fields include success, diff, backup_id, syntax_valid, and mode-specific fields.`;
34370
34541
  }
34371
34542
  function createEditTool(ctx, writeToolName = "write") {
34372
34543
  return {
@@ -34381,7 +34552,8 @@ function createEditTool(ctx, writeToolName = "write") {
34381
34552
  content: z7.string().optional().describe("Replacement content for symbol mode or operations[].command='write'. For whole-file writes, use the `write` tool."),
34382
34553
  appendContent: z7.string().optional().describe("Text to append to the end of filePath; creates the file if needed"),
34383
34554
  edits: z7.array(z7.record(z7.string(), z7.unknown())).optional().describe("Batch edits — array of { oldString: string, newString: string } or { startLine: number (1-based), endLine: number (1-based, inclusive), content: string }"),
34384
- operations: z7.array(z7.record(z7.string(), z7.unknown())).optional().describe("Transaction — array of { file: string, command: 'edit_match' | 'write', match?: string, replacement?: string, content?: string } for multi-file edits with rollback. Note: uses 'file'/'match'/'replacement' (not filePath/oldString/newString)")
34555
+ operations: z7.array(z7.record(z7.string(), z7.unknown())).optional().describe("Transaction — array of { file: string, command: 'edit_match' | 'write', match?: string, replacement?: string, content?: string } for multi-file edits with rollback. Note: uses 'file'/'match'/'replacement' (not filePath/oldString/newString)"),
34556
+ diagnostics: z7.boolean().optional().describe(DIAGNOSTICS_PARAM_DESCRIPTION)
34385
34557
  },
34386
34558
  execute: async (args, context) => {
34387
34559
  const argsRecord = args;
@@ -34477,8 +34649,8 @@ function createEditTool(ctx, writeToolName = "write") {
34477
34649
  const hint = typeof args.content === "string" ? ` To write the whole file, use the '${writeToolName}' tool. To edit existing content, provide 'oldString' (and optionally 'newString'), 'symbol' + 'content', or an 'edits' array.` : " Provide 'oldString' (+ optional 'newString'), 'symbol' + 'content', 'edits' array, or 'operations' array.";
34478
34650
  throw new Error(`edit: no edit mode resolved from arguments.${hint}`);
34479
34651
  }
34480
- params.diagnostics = true;
34481
- params.include_diff = true;
34652
+ params.diagnostics = args.diagnostics ?? diagnosticsOnEditDefault(ctx);
34653
+ params.include_diff_content = true;
34482
34654
  const data = await callBridge(ctx, context, command, params);
34483
34655
  if (data.success && data.diff) {
34484
34656
  const diff = data.diff;
@@ -34503,7 +34675,12 @@ function createEditTool(ctx, writeToolName = "write") {
34503
34675
  });
34504
34676
  }
34505
34677
  }
34506
- let result = JSON.stringify(data);
34678
+ const agentData = { ...data };
34679
+ if (agentData.diff && typeof agentData.diff === "object") {
34680
+ const d = agentData.diff;
34681
+ agentData.diff = { additions: d.additions ?? 0, deletions: d.deletions ?? 0 };
34682
+ }
34683
+ let result = JSON.stringify(agentData);
34507
34684
  const globSkipNote = formatGlobSkipReasonsNote(data.format_skip_reasons);
34508
34685
  if (globSkipNote)
34509
34686
  result += `
@@ -34531,7 +34708,7 @@ ${diagLines}`;
34531
34708
  if (pendingServers && pendingServers.length > 0) {
34532
34709
  result += `
34533
34710
 
34534
- Note: LSP server(s) did not respond in time: ${pendingServers.join(", ")}. Diagnostics may be incomplete; rerun lsp_diagnostics later for a fresh check.`;
34711
+ Note: LSP server(s) did not respond in time: ${pendingServers.join(", ")}. Diagnostics may be incomplete; call aft_inspect for a checkpoint diagnostics snapshot.`;
34535
34712
  }
34536
34713
  if (exitedServers && exitedServers.length > 0) {
34537
34714
  result += `
@@ -34592,15 +34769,17 @@ Example patch:
34592
34769
  - You must include a header with your intended action (Add/Delete/Update)
34593
34770
  - You must prefix new lines with \`+\` even when creating a new file
34594
34771
 
34595
- Returns: Status message string listing created, updated, moved, deleted, or failed file operations. May include inline LSP errors if type errors are introduced by the patch.`;
34772
+ 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.`;
34596
34773
  function createApplyPatchTool(ctx) {
34597
34774
  return {
34598
34775
  description: APPLY_PATCH_DESCRIPTION,
34599
34776
  args: {
34600
- patchText: z7.string().describe("The full patch text including Begin/End markers")
34777
+ patchText: z7.string().describe("The full patch text including Begin/End markers"),
34778
+ diagnostics: z7.boolean().optional().describe(DIAGNOSTICS_PARAM_DESCRIPTION)
34601
34779
  },
34602
34780
  execute: async (args, context) => {
34603
34781
  const patchText = args.patchText;
34782
+ const diagnostics = args.diagnostics ?? diagnosticsOnEditDefault(ctx);
34604
34783
  if (!patchText)
34605
34784
  throw new Error("'patchText' is required");
34606
34785
  let hunks;
@@ -34680,8 +34859,8 @@ function createApplyPatchTool(ctx) {
34680
34859
  file: filePath,
34681
34860
  content,
34682
34861
  create_dirs: true,
34683
- diagnostics: true,
34684
- include_diff: true,
34862
+ diagnostics,
34863
+ include_diff_content: true,
34685
34864
  multi_file_write_paths: multiFileWritePaths
34686
34865
  });
34687
34866
  const wrDiff = writeResult.diff;
@@ -34700,7 +34879,7 @@ function createApplyPatchTool(ctx) {
34700
34879
  const filePath2 = path4.resolve(context.directory, hunk.path);
34701
34880
  if (fs6.existsSync(filePath2)) {
34702
34881
  try {
34703
- await callBridge(ctx, context, "delete_file", { file: filePath2 });
34882
+ fs6.rmSync(filePath2, { force: true });
34704
34883
  } catch {}
34705
34884
  }
34706
34885
  }
@@ -34733,8 +34912,8 @@ function createApplyPatchTool(ctx) {
34733
34912
  file: targetPath,
34734
34913
  content: newContent,
34735
34914
  create_dirs: true,
34736
- diagnostics: true,
34737
- include_diff: true,
34915
+ diagnostics,
34916
+ include_diff_content: true,
34738
34917
  multi_file_write_paths: multiFileWritePaths
34739
34918
  });
34740
34919
  const diags = writeResult.lsp_diagnostics;
@@ -35007,7 +35186,7 @@ function aftPrefixedTools(ctx) {
35007
35186
  file: filePath,
35008
35187
  content: normalizedArgs.content,
35009
35188
  create_dirs: normalizedArgs.create_dirs !== false,
35010
- diagnostics: true
35189
+ diagnostics: normalizedArgs.diagnostics ?? diagnosticsOnEditDefault(ctx)
35011
35190
  };
35012
35191
  const response = await callBridge(ctx, context, "write", writeParams);
35013
35192
  if (response.success === false) {
@@ -35096,44 +35275,169 @@ function importTools(ctx) {
35096
35275
  };
35097
35276
  }
35098
35277
 
35099
- // src/tools/lsp.ts
35278
+ // src/tools/inspect.ts
35100
35279
  import { tool as tool9 } from "@opencode-ai/plugin";
35101
35280
  var z9 = tool9.schema;
35102
- function lspTools(ctx) {
35103
- const diagnosticsTool = {
35104
- description: "On-demand LSP file/scope check. NOT a project-wide type checker — use `tsc --noEmit`, `cargo check`, `pyright` etc. for full coverage.\n" + `
35105
- ` + "Honesty: `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>`.",
35281
+ function asRecord2(value) {
35282
+ if (!value || typeof value !== "object" || Array.isArray(value))
35283
+ return;
35284
+ return value;
35285
+ }
35286
+ function asNumber(value) {
35287
+ return typeof value === "number" && Number.isFinite(value) ? value : undefined;
35288
+ }
35289
+ function asString(value) {
35290
+ return typeof value === "string" ? value : undefined;
35291
+ }
35292
+ function asStringArray(value) {
35293
+ return Array.isArray(value) ? value.filter((item) => typeof item === "string") : [];
35294
+ }
35295
+ function diagnosticsServerSummary(section) {
35296
+ const pending = asStringArray(section.servers_pending);
35297
+ const notInstalled = asStringArray(section.servers_not_installed);
35298
+ const parts = [];
35299
+ if (pending.length > 0)
35300
+ parts.push(`pending: ${pending.join(", ")}`);
35301
+ if (notInstalled.length > 0)
35302
+ parts.push(`not installed: ${notInstalled.join(", ")}`);
35303
+ return parts.length > 0 ? parts.join("; ") : "none reported";
35304
+ }
35305
+ function formatDiagnosticsSummary(summary) {
35306
+ const section = asRecord2(summary?.diagnostics);
35307
+ if (!section)
35308
+ return;
35309
+ const errors3 = asNumber(section.errors);
35310
+ const warnings = asNumber(section.warnings);
35311
+ const info = asNumber(section.info);
35312
+ const hints = asNumber(section.hints);
35313
+ const hasCounts = [errors3, warnings, info, hints].some((value) => value !== undefined);
35314
+ const counts = `${errors3 ?? 0} errors, ${warnings ?? 0} warnings, ${info ?? 0} info, ${hints ?? 0} hints`;
35315
+ const status = asString(section.status);
35316
+ if (status === "pending") {
35317
+ return hasCounts ? `diagnostics: ${counts} so far — still pending (servers: ${diagnosticsServerSummary(section)})` : `diagnostics: pending (servers: ${diagnosticsServerSummary(section)})`;
35318
+ }
35319
+ if (status === "incomplete") {
35320
+ return hasCounts ? `diagnostics: ${counts} (incomplete — servers: ${diagnosticsServerSummary(section)})` : `diagnostics: unavailable (status incomplete; servers: ${diagnosticsServerSummary(section)})`;
35321
+ }
35322
+ if (hasCounts) {
35323
+ return `diagnostics: ${counts}`;
35324
+ }
35325
+ return;
35326
+ }
35327
+ function formatDiagnosticLocation(diagnostic) {
35328
+ const file2 = asString(diagnostic.file) ?? "(unknown file)";
35329
+ const line = asNumber(diagnostic.line);
35330
+ const column = asNumber(diagnostic.column);
35331
+ if (line === undefined)
35332
+ return file2;
35333
+ if (column === undefined)
35334
+ return `${file2}:${line}`;
35335
+ return `${file2}:${line}:${column}`;
35336
+ }
35337
+ function formatDiagnosticsDetails(details) {
35338
+ const diagnostics = Array.isArray(details?.diagnostics) ? details.diagnostics.map(asRecord2).filter(Boolean) : [];
35339
+ return diagnostics.map((diagnostic) => {
35340
+ const severity = asString(diagnostic.severity) ?? "information";
35341
+ const message = asString(diagnostic.message) ?? "(no message)";
35342
+ const source = asString(diagnostic.source);
35343
+ const suffix = source ? ` [${source}]` : "";
35344
+ return `${formatDiagnosticLocation(diagnostic)} ${severity} ${message}${suffix}`;
35345
+ });
35346
+ }
35347
+ function renderInspectDiagnostics(response) {
35348
+ const lines = [];
35349
+ const summaryLine = formatDiagnosticsSummary(asRecord2(response.summary));
35350
+ if (summaryLine)
35351
+ lines.push(summaryLine);
35352
+ const detailLines = formatDiagnosticsDetails(asRecord2(response.details));
35353
+ if (detailLines.length > 0) {
35354
+ lines.push("diagnostics details:", ...detailLines.map((line) => `- ${line}`));
35355
+ }
35356
+ return lines.join(`
35357
+ `);
35358
+ }
35359
+ function appendRenderedDiagnostics(text, response) {
35360
+ if (/^diagnostics[: ]/im.test(text))
35361
+ return text;
35362
+ const diagnostics = renderInspectDiagnostics(response);
35363
+ if (!diagnostics)
35364
+ return text;
35365
+ return text ? `${text}
35366
+
35367
+ ${diagnostics}` : diagnostics;
35368
+ }
35369
+ function arg(schema) {
35370
+ return schema;
35371
+ }
35372
+ function normalizeStringOrArray(value) {
35373
+ return isEmptyParam(value) ? undefined : value;
35374
+ }
35375
+ function inspectToolSurfaceEnabled(config2) {
35376
+ return (config2.tool_surface ?? "recommended") !== "minimal" && config2.inspect?.enabled !== false;
35377
+ }
35378
+ function createInspectTier2IdleScheduler(options) {
35379
+ const timers = new Map;
35380
+ const setTimer = options.setTimer ?? ((callback, delayMs) => setTimeout(callback, delayMs));
35381
+ const clearTimer = options.clearTimer ?? ((timer) => clearTimeout(timer));
35382
+ const clear = (sessionID) => {
35383
+ const timer = timers.get(sessionID);
35384
+ if (!timer)
35385
+ return;
35386
+ clearTimer(timer);
35387
+ timers.delete(sessionID);
35388
+ };
35389
+ const clearAll = () => {
35390
+ for (const timer of timers.values()) {
35391
+ clearTimer(timer);
35392
+ }
35393
+ timers.clear();
35394
+ };
35395
+ const schedule = (sessionID) => {
35396
+ if (!options.isEnabled())
35397
+ return;
35398
+ clear(sessionID);
35399
+ const idleMinutes = options.idleMinutes() ?? 4;
35400
+ const delayMs = Math.max(0, idleMinutes * 60 * 1000);
35401
+ const timer = setTimer(() => {
35402
+ timers.delete(sessionID);
35403
+ options.run(sessionID).catch((err) => {
35404
+ options.warn?.(`inspect_tier2_run failed: ${err instanceof Error ? err.message : String(err)}`);
35405
+ });
35406
+ }, delayMs);
35407
+ timers.set(sessionID, timer);
35408
+ };
35409
+ return { schedule, clear, clearAll };
35410
+ }
35411
+ function inspectTools(ctx) {
35412
+ const inspectTool = {
35413
+ 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 as background scans triggered on session idle; calls may return cached `stale_categories: [...]` results or `pending_categories: [...]` while a refresh is in progress (waits up to 1s for fresh data before falling back to cached).\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.
35414
+
35415
+ ` + "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.",
35106
35416
  args: {
35107
- filePath: z9.string().optional().describe("Path to a file to check. Mutually exclusive with 'directory'. Omit both to dump all cached diagnostics."),
35108
- directory: z9.string().optional().describe("Path to a directory. Returns cached diagnostics + workspace pull; capped at 200 walked files."),
35109
- severity: z9.enum(["error", "warning", "information", "hint", "all"]).optional().describe("Filter by severity (default: 'all')."),
35110
- waitMs: optionalInt(1, 1e4).describe("Wait up to N ms (max 10000) for push diagnostics. Push-only servers like bash-language-server and yaml-language-server — use after an edit.")
35417
+ sections: arg(z9.union([z9.string(), z9.array(z9.string())]).optional().describe("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.")),
35418
+ scope: arg(z9.union([z9.string(), z9.array(z9.string())]).optional().describe("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.")),
35419
+ topK: arg(z9.number().int().positive().max(100).optional().describe("Max drill-down items per category. Default 20, max 100."))
35111
35420
  },
35112
35421
  execute: async (args, context) => {
35113
- const filePath = args.filePath || undefined;
35114
- const directory = args.directory || undefined;
35115
- if (filePath !== undefined && directory !== undefined) {
35116
- throw new Error("'filePath' and 'directory' are mutually exclusive provide one or neither");
35117
- }
35118
- const params = {};
35119
- if (filePath !== undefined)
35120
- params.file = filePath;
35121
- if (directory !== undefined)
35122
- params.directory = directory;
35123
- if (args.severity !== undefined)
35124
- params.severity = args.severity;
35125
- if (args.waitMs !== undefined)
35126
- params.wait_ms = args.waitMs;
35127
- const result = await callBridge(ctx, context, "lsp_diagnostics", params);
35128
- if (result.success === false) {
35129
- throw new Error(result.message || "lsp_diagnostics failed");
35422
+ const sections = normalizeStringOrArray(args.sections);
35423
+ const scope = normalizeStringOrArray(args.scope);
35424
+ const topK = args.topK === undefined || args.topK === null ? undefined : args.topK;
35425
+ const response = await callBridge(ctx, context, "inspect", { sections, scope, topK });
35426
+ if (response.success === false) {
35427
+ throw new Error(response.message || "inspect failed");
35130
35428
  }
35131
- return JSON.stringify(result);
35429
+ if (typeof response.text === "string") {
35430
+ return appendRenderedDiagnostics(response.text, response);
35431
+ }
35432
+ const diagnostics = renderInspectDiagnostics(response);
35433
+ const json2 = JSON.stringify(response, null, 2);
35434
+ return diagnostics ? `${json2}
35435
+
35436
+ ${diagnostics}` : json2;
35132
35437
  }
35133
35438
  };
35134
- const hoisting = ctx.config.hoist_builtin_tools !== false;
35135
35439
  return {
35136
- [hoisting ? "lsp_diagnostics" : "aft_lsp_diagnostics"]: diagnosticsTool
35440
+ aft_inspect: inspectTool
35137
35441
  };
35138
35442
  }
35139
35443
 
@@ -35142,18 +35446,18 @@ import { tool as tool10 } from "@opencode-ai/plugin";
35142
35446
  var z10 = tool10.schema;
35143
35447
  function navigationTools(ctx) {
35144
35448
  return {
35145
- aft_navigate: {
35146
- description: `Navigate code structure across files using call graph analysis.
35449
+ aft_callgraph: {
35450
+ 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: who calls X, what X calls, what breaks if X changes, how execution reaches X, or how a value flows.
35147
35451
 
35148
35452
  ` + `Ops:
35149
- ` + `- 'call_tree': See what a function calls (forward traversal). Use to understand dependencies before modifying a function.
35150
35453
  ` + `- 'callers': Find all call sites of a symbol. Use before renaming or changing a function's signature.
35151
- ` + `- 'trace_to': Trace how execution reaches a function from entry points (routes, exports, main). Use to understand context around deeply-nested code.
35152
- ` + `- 'trace_to_symbol': Find the shortest call path from one symbol to another symbol across the codebase. Requires 'toSymbol'. If multiple targets match, the error returns candidate files; retry with 'toFile' to disambiguate.
35153
- ` + `- 'impact': Analyze what breaks if a symbol changes. Returns affected callers with signatures and entry point status.
35454
+ ` + `- 'impact': What breaks if a symbol changes — affected callers with signatures and entry-point status (blast radius). Use before a risky edit.
35455
+ ` + `- 'call_tree': What a function calls (forward traversal). Use to understand a function's dependencies before modifying it.
35456
+ ` + `- 'trace_to': How execution reaches a function from entry points (routes, exports, main). Use to understand context around deeply-nested code.
35457
+ ` + `- 'trace_to_symbol': Shortest call path from one symbol to another. Requires 'toSymbol'. If multiple targets match, the error returns candidate files; retry with 'toFile' to disambiguate.
35154
35458
  ` + `- 'trace_data': Follow a value through variable assignments and function parameters across files. Requires 'symbol' (scope to trace from) and 'expression'.
35155
35459
 
35156
- ` + `All ops require both 'filePath' and 'symbol'. The 'expression' parameter is additionally required for trace_data; 'toSymbol' is additionally required for trace_to_symbol.
35460
+ ` + `All ops require both 'filePath' and 'symbol'. 'expression' is additionally required for trace_data; 'toSymbol' for trace_to_symbol.
35157
35461
 
35158
35462
  `,
35159
35463
  args: {
@@ -35332,9 +35636,25 @@ function readingTools(ctx) {
35332
35636
  contextLines: optionalInt(1, Number.MAX_SAFE_INTEGER).describe("Lines of context before/after the symbol (default: 3)")
35333
35637
  },
35334
35638
  execute: async (args, context) => {
35639
+ const hasTargetsProvided = (t) => {
35640
+ if (isEmptyParam(t))
35641
+ return false;
35642
+ const entryEmpty = (entry) => {
35643
+ if (!entry || typeof entry !== "object")
35644
+ return true;
35645
+ const fp = entry.filePath;
35646
+ const sym = entry.symbol;
35647
+ const fpEmpty = typeof fp !== "string" || fp.length === 0;
35648
+ const symEmpty = typeof sym !== "string" || sym.length === 0;
35649
+ return fpEmpty && symEmpty;
35650
+ };
35651
+ if (Array.isArray(t))
35652
+ return !t.every(entryEmpty);
35653
+ return !entryEmpty(t);
35654
+ };
35335
35655
  const hasFilePath = !isEmptyParam(args.filePath);
35336
35656
  const hasUrl = !isEmptyParam(args.url);
35337
- const hasTargets = !isEmptyParam(args.targets);
35657
+ const hasTargets = hasTargetsProvided(args.targets);
35338
35658
  const hasSymbols = !isEmptyParam(args.symbols);
35339
35659
  const zoomCallID = getCallID3(context);
35340
35660
  if (zoomCallID) {
@@ -35790,7 +36110,7 @@ function expandTilde(input) {
35790
36110
  }
35791
36111
  return input;
35792
36112
  }
35793
- function arg(schema) {
36113
+ function arg2(schema) {
35794
36114
  return schema;
35795
36115
  }
35796
36116
  function formatGrepOutput(response) {
@@ -35888,9 +36208,9 @@ function searchTools(ctx) {
35888
36208
  const grepTool = {
35889
36209
  description: "Search file contents using regular expressions. Returns matching lines with file paths and line numbers (no surrounding context lines — use `read` for that). Always case-sensitive. Capped at 100 matches; if you hit the cap, narrow with `path` or `include` and re-run.",
35890
36210
  args: {
35891
- pattern: arg(exports_external.string().describe("Regular expression pattern to search for")),
35892
- include: arg(exports_external.string().optional().describe("File pattern to include (e.g. '*.ts', '*.{ts,tsx}')")),
35893
- path: arg(exports_external.string().optional().describe("Directory to search (absolute or relative to project root)"))
36211
+ pattern: arg2(exports_external.string().describe("Regular expression pattern to search for")),
36212
+ include: arg2(exports_external.string().optional().describe("File pattern to include (e.g. '*.ts', '*.{ts,tsx}')")),
36213
+ path: arg2(exports_external.string().optional().describe("Directory to search (absolute or relative to project root)"))
35894
36214
  },
35895
36215
  execute: async (args, context) => {
35896
36216
  const pattern = String(args.pattern);
@@ -35927,8 +36247,8 @@ function searchTools(ctx) {
35927
36247
  const globTool = {
35928
36248
  description: "Find files matching a glob pattern. Returns matching file paths sorted by modification time.",
35929
36249
  args: {
35930
- pattern: arg(exports_external.string().describe("Glob pattern to match (e.g. '**/*.ts', 'src/**/*.test.*')")),
35931
- path: arg(exports_external.string().optional().describe("Directory to search (absolute or relative to project root)"))
36250
+ pattern: arg2(exports_external.string().describe("Glob pattern to match (e.g. '**/*.ts', 'src/**/*.test.*')")),
36251
+ path: arg2(exports_external.string().optional().describe("Directory to search (absolute or relative to project root)"))
35932
36252
  },
35933
36253
  execute: async (args, context) => {
35934
36254
  let globPattern = expandTilde(String(args.pattern));
@@ -35985,7 +36305,19 @@ function searchTools(ctx) {
35985
36305
  // src/tools/semantic.ts
35986
36306
  import { tool as tool14 } from "@opencode-ai/plugin";
35987
36307
  var z14 = tool14.schema;
35988
- function arg2(schema) {
36308
+ function semanticHonestyNote(response) {
36309
+ const notes = [];
36310
+ if (response.more_available === true)
36311
+ notes.push("more results available");
36312
+ if (response.engine_capped === true)
36313
+ notes.push("enumeration capped");
36314
+ if (response.fully_degraded === true)
36315
+ notes.push("fully degraded");
36316
+ if (response.complete === false)
36317
+ notes.push("partial/incomplete");
36318
+ return notes.length > 0 ? `Search status: ${notes.join("; ")}.` : undefined;
36319
+ }
36320
+ function arg3(schema) {
35989
36321
  return schema;
35990
36322
  }
35991
36323
  function semanticTools(ctx) {
@@ -36003,19 +36335,19 @@ function semanticTools(ctx) {
36003
36335
  "When NOT to use:",
36004
36336
  "- You need exhaustive literal enumeration → use grep directly",
36005
36337
  "- You want the file/module structure → use aft_outline",
36006
- "- You're following a call chain → use aft_navigate",
36338
+ "- You're following a call chain → use aft_callgraph",
36007
36339
  "",
36008
36340
  "Set hint to 'regex', 'literal', 'semantic', or 'auto' to override or document routing intent."
36009
36341
  ].join(`
36010
36342
  `),
36011
36343
  args: {
36012
- query: arg2(z14.string().describe("Concept, regex, literal text, filename, or capability to find. Examples: 'fuzzy match with whitespace tolerance', '^export', 'Cargo.lock'.")),
36013
- topK: arg2(optionalInt(1, 100).describe("Number of results (default: 10, max: 100)")),
36014
- hint: arg2(z14.enum(["regex", "literal", "semantic", "auto"]).optional().describe("Optional routing hint. Defaults to 'auto'."))
36344
+ query: arg3(z14.string().describe("Concept, regex, literal text, filename, or capability to find. Examples: 'fuzzy match with whitespace tolerance', '^export', 'Cargo.lock'.")),
36345
+ topK: arg3(optionalInt(1, 100).describe("Number of results (default: 10, max: 100)")),
36346
+ hint: arg3(z14.enum(["regex", "literal", "semantic", "auto"]).optional().describe("Optional routing hint. Defaults to 'auto'."))
36015
36347
  },
36016
36348
  execute: async (args, context) => {
36017
- if (typeof args.query !== "string" || args.query.length === 0) {
36018
- throw new Error("semantic_search: invalid params: missing field `query`");
36349
+ if (isEmptyParam(args.query) || typeof args.query !== "string" || args.query.trim().length === 0) {
36350
+ throw new Error("semantic_search: invalid params: `query` must be a non-empty string");
36019
36351
  }
36020
36352
  const query = args.query;
36021
36353
  const hint = typeof args.hint === "string" ? args.hint : undefined;
@@ -36032,15 +36364,28 @@ function semanticTools(ctx) {
36032
36364
  bridgeParams.hint = hint;
36033
36365
  const response = await callBridge(ctx, context, "semantic_search", bridgeParams);
36034
36366
  if (response.success === false) {
36035
- if (response.code === "semantic_search_unavailable" && typeof response.message === "string") {
36036
- return response.message;
36037
- }
36038
- throw new Error(response.message || "semantic_search failed");
36039
- }
36040
- if (typeof response.text === "string") {
36041
- return response.text;
36367
+ const message = typeof response.message === "string" && response.message.length > 0 ? response.message : "semantic_search failed";
36368
+ const code = typeof response.code === "string" && response.code.length > 0 ? response.code : undefined;
36369
+ throw new Error(code ? `semantic_search: ${code} — ${message}` : message);
36370
+ }
36371
+ const honestyNote = semanticHonestyNote(response);
36372
+ if (typeof response.text === "string" && response.status === "disabled" && Array.isArray(response.results) && response.results.length === 0) {
36373
+ return honestyNote ? `${response.text}
36374
+ ${honestyNote}` : response.text;
36375
+ }
36376
+ const structured = JSON.stringify(response, null, 2);
36377
+ if (typeof response.text === "string" && response.text.length > 0) {
36378
+ const display = honestyNote ? `${response.text}
36379
+ ${honestyNote}` : response.text;
36380
+ return `${display}
36381
+
36382
+ Structured response:
36383
+ ${structured}`;
36042
36384
  }
36043
- return JSON.stringify(response);
36385
+ return honestyNote ? `${honestyNote}
36386
+
36387
+ Structured response:
36388
+ ${structured}` : structured;
36044
36389
  }
36045
36390
  };
36046
36391
  return {
@@ -36170,7 +36515,8 @@ function buildWorkflowHints(opts) {
36170
36515
  const hasZoom = !opts.disabledTools.has("aft_zoom");
36171
36516
  const hasGrep = opts.toolSurface !== "minimal" && !opts.disabledTools.has(grepName);
36172
36517
  const hasSearch = opts.toolSurface !== "minimal" && opts.semanticEnabled && !opts.disabledTools.has("aft_search");
36173
- const hasNavigate = opts.toolSurface === "all" && !opts.disabledTools.has("aft_navigate");
36518
+ const hasNavigate = opts.toolSurface === "all" && !opts.disabledTools.has("aft_callgraph");
36519
+ const hasInspect = opts.toolSurface !== "minimal" && !opts.disabledTools.has("aft_inspect");
36174
36520
  const hasBash = !opts.disabledTools.has(bashName);
36175
36521
  const hasBgBash = hasBash && opts.bashBackgroundEnabled && !opts.disabledTools.has(bashStatusName);
36176
36522
  if (hasOutline && hasZoom) {
@@ -36179,14 +36525,17 @@ function buildWorkflowHints(opts) {
36179
36525
  if (hasOutline && hasZoom && (hasGrep || hasSearch)) {
36180
36526
  if (hasSearch) {
36181
36527
  const grepFallback = hasGrep ? ` Use \`${grepName}\` directly only when you need exhaustive enumeration of literal text (every TODO, every import of X) without ranking.` : "";
36182
- 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}`);
36528
+ 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}`);
36183
36529
  } else {
36184
36530
  sections.push(`**Code exploration**: \`${grepName}\` to locate → \`aft_outline\` for structure → \`aft_zoom\` for symbol(s).`);
36185
36531
  }
36186
36532
  }
36533
+ if (hasInspect) {
36534
+ 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.");
36535
+ }
36187
36536
  if (hasNavigate) {
36188
36537
  sections.push([
36189
- "Use `aft_navigate` instead of grep + read chains for relationship questions:",
36538
+ "Use `aft_callgraph` for code-relationship questions instead of grep + read chains:",
36190
36539
  "- `callers` — find all call sites before changing a function signature",
36191
36540
  "- `impact` — blast radius (which functions/files will need updates)",
36192
36541
  "- `trace_to` — how execution reaches this code from entry points (routes, exports, main)",
@@ -36303,13 +36652,13 @@ var PLUGIN_VERSION = (() => {
36303
36652
  return "0.0.0";
36304
36653
  }
36305
36654
  })();
36306
- var ANNOUNCEMENT_VERSION = "0.32.0";
36655
+ var ANNOUNCEMENT_VERSION = "0.33.0";
36307
36656
  var ANNOUNCEMENT_FEATURES = [
36308
- "`aft_search` is now the primary code-search tool auto-routes regex / literal / semantic / hybrid by query shape, with a `hint` override.",
36309
- 'Semantic search stays queryable through edits no more "rebuilding" fallback after every save.',
36310
- "Workflow hints promote `aft_search` as primary; `grep` is positioned as the specialized fallback.",
36311
- "Bare `\\n`, `\\t`, `\\r` queries correctly route to regex mode.",
36312
- 'Empty params (`targets: []`, `url: ""`) no longer trigger misleading mutual-exclusion errors.'
36657
+ "New `aft_inspect` — one call for codebase health: diagnostics, metrics, TODOs, dead code, unused exports, and duplicates.",
36658
+ "Diagnostics now flow through `aft_inspect` (run it after a batch of edits) instead of arriving automatically on every edit.",
36659
+ "`aft_navigate` is renamed to `aft_callgraph`; the Rust call graph now resolves cross-file callers.",
36660
+ "Edits no longer echo the whole file back to the agent — much lower token cost per edit.",
36661
+ "Batch of `aft_search` correctness fixes and undo-history/SSRF/Windows hardening."
36313
36662
  ];
36314
36663
  var ANNOUNCEMENT_FOOTER = "Join us on Discord: https://discord.gg/F2uWxjGnU";
36315
36664
  var plugin = async (input) => initializePluginForDirectory(input);
@@ -36521,7 +36870,10 @@ ${lines}
36521
36870
  });
36522
36871
  }
36523
36872
  const rpcServer = new AftRpcServer(configOverrides.storage_dir, input.directory);
36524
- const unregisterShutdown = registerShutdownCleanup(async () => {
36873
+ let clearInspectTier2Idle = () => {};
36874
+ registerShutdownCleanup(async () => {
36875
+ autoUpdateAbort.abort();
36876
+ clearInspectTier2Idle();
36525
36877
  await Promise.allSettled([abortInFlightAutoInstalls(), abortInFlightGithubInstalls()]);
36526
36878
  try {
36527
36879
  rpcServer.stop();
@@ -36621,7 +36973,7 @@ Install: ${getManualInstallHint()}`).catch(() => {});
36621
36973
  }
36622
36974
  const surface = aftConfig.tool_surface ?? "recommended";
36623
36975
  const ALL_ONLY_TOOLS = new Set([
36624
- "aft_navigate",
36976
+ "aft_callgraph",
36625
36977
  "aft_delete",
36626
36978
  "aft_move",
36627
36979
  "aft_transform",
@@ -36636,9 +36988,9 @@ Install: ${getManualInstallHint()}`).catch(() => {});
36636
36988
  ...navigationTools(ctx),
36637
36989
  ...surface !== "minimal" && astTools(ctx),
36638
36990
  ...surface !== "minimal" && aftConfig.semantic_search === true && semanticTools(ctx),
36991
+ ...inspectToolSurfaceEnabled(aftConfig) && inspectTools(ctx),
36639
36992
  ...surface !== "minimal" && aftConfig.search_index === true && searchTools(ctx),
36640
36993
  ...refactoringTools(ctx),
36641
- ...surface !== "minimal" && lspTools(ctx),
36642
36994
  ...surface !== "minimal" && conflictTools(ctx)
36643
36995
  });
36644
36996
  if (surface !== "all") {
@@ -36669,7 +37021,8 @@ Install: ${getManualInstallHint()}`).catch(() => {});
36669
37021
  "aft_outline",
36670
37022
  "aft_zoom",
36671
37023
  "aft_search",
36672
- "aft_navigate",
37024
+ "aft_callgraph",
37025
+ "aft_inspect",
36673
37026
  "grep",
36674
37027
  "aft_grep",
36675
37028
  "bash",
@@ -36686,6 +37039,20 @@ Install: ${getManualInstallHint()}`).catch(() => {});
36686
37039
  if (hintsBlock) {
36687
37040
  log2(`Workflow hints injected (${hintsBlock.length} chars)`);
36688
37041
  }
37042
+ const inspectTier2Idle = createInspectTier2IdleScheduler({
37043
+ isEnabled: () => registeredTools.has("aft_inspect"),
37044
+ idleMinutes: () => aftConfig.inspect?.tier2_idle_minutes,
37045
+ warn: warn2,
37046
+ run: async (sessionID) => {
37047
+ const sessionDir = await getSessionDirectory(input.client, sessionID, input.directory) ?? input.directory;
37048
+ const bridge = ctx.pool.getActiveBridgeForRoot(sessionDir) ?? ctx.pool.getBridge(sessionDir);
37049
+ const response = await bridge.send("inspect_tier2_run", { session_id: sessionID });
37050
+ if (response.success === false) {
37051
+ warn2(response.message || "inspect_tier2_run failed");
37052
+ }
37053
+ }
37054
+ });
37055
+ clearInspectTier2Idle = () => inspectTier2Idle.clearAll();
36689
37056
  return {
36690
37057
  tool: allTools,
36691
37058
  "experimental.chat.system.transform": async (_input, output) => {
@@ -36695,11 +37062,17 @@ Install: ${getManualInstallHint()}`).catch(() => {});
36695
37062
  },
36696
37063
  event: async (eventInput) => {
36697
37064
  await autoUpdateEventHook(eventInput);
36698
- if (eventInput.event.type !== "session.idle")
36699
- return;
37065
+ const eventType = eventInput.event.type;
36700
37066
  const sessionID = extractSessionID(eventInput.event.properties);
37067
+ if ((eventType === "session.deleted" || eventType === "session.shutdown") && sessionID) {
37068
+ inspectTier2Idle.clear(sessionID);
37069
+ return;
37070
+ }
37071
+ if (eventType !== "session.idle")
37072
+ return;
36701
37073
  if (!sessionID)
36702
37074
  return;
37075
+ inspectTier2Idle.schedule(sessionID);
36703
37076
  const sessionDir = await getSessionDirectory(input.client, sessionID, input.directory) ?? input.directory;
36704
37077
  await handleIdleBgCompletions({
36705
37078
  ctx,
@@ -36713,6 +37086,10 @@ Install: ${getManualInstallHint()}`).catch(() => {});
36713
37086
  const sid = messageInput.sessionID ?? messageInput.sessionId ?? messageInput.id;
36714
37087
  warmSessionDirectory(input.client, sid, input.directory);
36715
37088
  },
37089
+ "tool.execute.before": async (toolInput) => {
37090
+ if (toolInput.sessionID)
37091
+ inspectTier2Idle.clear(toolInput.sessionID);
37092
+ },
36716
37093
  "command.execute.before": async (commandInput, _output) => {
36717
37094
  if (isTuiMode2() || commandInput.command !== STATUS_COMMAND) {
36718
37095
  return;
@@ -36763,12 +37140,7 @@ Install: ${getManualInstallHint()}`).catch(() => {});
36763
37140
  };
36764
37141
  },
36765
37142
  dispose: async () => {
36766
- autoUpdateAbort.abort();
36767
- unregisterShutdown();
36768
- await Promise.allSettled([abortInFlightAutoInstalls(), abortInFlightGithubInstalls()]);
36769
- rpcServer.stop();
36770
- await disposeAllPtyTerminals();
36771
- await pool.shutdown();
37143
+ await runCleanups("dispose");
36772
37144
  }
36773
37145
  };
36774
37146
  }