@cortexkit/aft-opencode 0.31.1 → 0.33.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -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) {
@@ -31990,6 +32148,19 @@ function formatFlag(enabled) {
31990
32148
  function formatCount(value) {
31991
32149
  return value == null ? "—" : value.toLocaleString("en-US");
31992
32150
  }
32151
+ function formatSemanticIndexStatus(status, stage) {
32152
+ if ((status === "loading" || status === "building") && stage === "fingerprint_change") {
32153
+ return "Rebuilding (model changed)";
32154
+ }
32155
+ return status;
32156
+ }
32157
+ function formatSemanticRefreshing(refreshingCount) {
32158
+ if (!Number.isFinite(refreshingCount) || refreshingCount <= 0)
32159
+ return null;
32160
+ if (refreshingCount > 20)
32161
+ return "Ready (many files refreshing)";
32162
+ return `Ready (${refreshingCount} file(s) refreshing)`;
32163
+ }
31993
32164
  function formatBytes(bytes) {
31994
32165
  if (!Number.isFinite(bytes) || bytes <= 0)
31995
32166
  return "0 B";
@@ -32041,6 +32212,7 @@ function coerceAftStatus(response) {
32041
32212
  files: readOptionalNumber(semanticIndex.files),
32042
32213
  entries_done: readOptionalNumber(semanticIndex.entries_done),
32043
32214
  entries_total: readOptionalNumber(semanticIndex.entries_total),
32215
+ refreshing_count: readNumber(semanticIndex.refreshing_count),
32044
32216
  entries: readOptionalNumber(semanticIndex.entries),
32045
32217
  dimension: readOptionalNumber(semanticIndex.dimension),
32046
32218
  error: readNullableString(semanticIndex.error)
@@ -32086,9 +32258,13 @@ function formatStatusMarkdown(status) {
32086
32258
  `- **Trigrams:** ${formatCount(status.search_index.trigrams)}`,
32087
32259
  "",
32088
32260
  "### Semantic index",
32089
- `- **Status:** \`${status.semantic_index.status}\``,
32090
- `- **Entries:** ${formatCount(status.semantic_index.entries)}`
32261
+ `- **Status:** \`${formatSemanticIndexStatus(status.semantic_index.status, status.semantic_index.stage)}\``
32091
32262
  ];
32263
+ const refreshing = formatSemanticRefreshing(status.semantic_index.refreshing_count);
32264
+ if (refreshing) {
32265
+ lines.push(`- **Refresh:** ${refreshing}`);
32266
+ }
32267
+ lines.push(`- **Entries:** ${formatCount(status.semantic_index.entries)}`);
32092
32268
  if (status.semantic_index.backend) {
32093
32269
  lines.push(`- **Backend:** ${status.semantic_index.backend}`);
32094
32270
  }
@@ -32197,24 +32373,28 @@ function getState() {
32197
32373
  }
32198
32374
  return g[GLOBAL_KEY];
32199
32375
  }
32200
- var shuttingDown = false;
32376
+ var runningCleanups = false;
32201
32377
  async function runCleanups(reason) {
32202
- if (shuttingDown)
32203
- return;
32204
- shuttingDown = true;
32205
- const state = getState();
32206
- if (state.cleanups.size === 0)
32378
+ if (runningCleanups)
32207
32379
  return;
32208
- log2(`Shutdown triggered by ${reason} — running ${state.cleanups.size} cleanup(s)`);
32209
- const cleanups = Array.from(state.cleanups);
32210
- state.cleanups.clear();
32211
- await Promise.allSettled(cleanups.map(async (fn) => {
32212
- try {
32213
- await fn();
32214
- } catch (err) {
32215
- log2(`Cleanup error: ${err.message}`);
32216
- }
32217
- }));
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
+ }
32218
32398
  }
32219
32399
  function installProcessHandlers() {
32220
32400
  const state = getState();
@@ -32249,6 +32429,17 @@ import * as path2 from "node:path";
32249
32429
  import { tool as tool2 } from "@opencode-ai/plugin";
32250
32430
  var z2 = tool2.schema;
32251
32431
  var optionalInt = (min, max) => z2.number().int().min(min).max(max).optional();
32432
+ function isEmptyParam(value) {
32433
+ if (value === undefined || value === null)
32434
+ return true;
32435
+ if (typeof value === "string")
32436
+ return value.length === 0;
32437
+ if (Array.isArray(value))
32438
+ return value.length === 0;
32439
+ if (typeof value === "object")
32440
+ return Object.keys(value).length === 0;
32441
+ return false;
32442
+ }
32252
32443
  var LONG_RUNNING_COMMAND_TIMEOUT_MS = {
32253
32444
  callers: 60000,
32254
32445
  trace_to: 60000,
@@ -32257,7 +32448,7 @@ var LONG_RUNNING_COMMAND_TIMEOUT_MS = {
32257
32448
  impact: 60000,
32258
32449
  grep: 60000,
32259
32450
  glob: 60000,
32260
- semantic_search: 45000
32451
+ semantic_search: 60000
32261
32452
  };
32262
32453
  function timeoutForCommand(command) {
32263
32454
  return LONG_RUNNING_COMMAND_TIMEOUT_MS[command];
@@ -32569,9 +32760,9 @@ function astTools(ctx) {
32569
32760
  pattern: args.pattern,
32570
32761
  lang: args.lang
32571
32762
  };
32572
- if (args.paths)
32763
+ if (!isEmptyParam(args.paths))
32573
32764
  params.paths = args.paths;
32574
- if (args.globs)
32765
+ if (!isEmptyParam(args.globs))
32575
32766
  params.globs = args.globs;
32576
32767
  if (args.contextLines !== undefined)
32577
32768
  params.context = Number(args.contextLines);
@@ -32689,9 +32880,9 @@ ${hint}`;
32689
32880
  rewrite: args.rewrite,
32690
32881
  lang: args.lang
32691
32882
  };
32692
- if (args.paths)
32883
+ if (!isEmptyParam(args.paths))
32693
32884
  params.paths = args.paths;
32694
- if (args.globs)
32885
+ if (!isEmptyParam(args.globs))
32695
32886
  params.globs = args.globs;
32696
32887
  params.dry_run = args.dryRun === true;
32697
32888
  const response = await callBridge(ctx, context, "ast_replace", params);
@@ -33053,7 +33244,14 @@ function applyUpdateChunks(originalContent, filePath, chunks) {
33053
33244
  lineIndex = contextIdx + 1;
33054
33245
  }
33055
33246
  if (chunk.old_lines.length === 0) {
33056
- 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
+ }
33057
33255
  replacements.push([insertionIdx, 0, chunk.new_lines]);
33058
33256
  continue;
33059
33257
  }
@@ -34038,6 +34236,10 @@ function inferBeforeStart(ops, from, beforeLen) {
34038
34236
  return beforeLen;
34039
34237
  }
34040
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
+ }
34041
34243
  var READ_DESCRIPTION = `Read file contents or list directory entries.
34042
34244
 
34043
34245
  Use either startLine/endLine OR offset/limit to read a section of a file.
@@ -34055,8 +34257,7 @@ Examples:
34055
34257
  Read lines 50-100: { "filePath": "src/app.ts", "startLine": 50, "endLine": 100 }
34056
34258
  Read 30 lines from line 200: { "filePath": "src/app.ts", "offset": 200, "limit": 30 }
34057
34259
  List directory: { "filePath": "src/" }
34058
-
34059
- Returns: Line-numbered file content string. For directories: newline-joined sorted entries. For binary files: size/message string.`;
34260
+ `;
34060
34261
  function createReadTool(ctx) {
34061
34262
  return {
34062
34263
  description: READ_DESCRIPTION,
@@ -34181,24 +34382,26 @@ function getWriteDescription(editToolName) {
34181
34382
 
34182
34383
  Automatically creates parent directories. Backs up existing files before overwriting.
34183
34384
  If the project has a formatter configured (biome, prettier, rustfmt, etc.), the file
34184
- 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.
34185
34389
 
34186
34390
  **Behavior:**
34187
34391
  - Creates parent directories automatically (no need to mkdir first)
34188
34392
  - Existing files are backed up before overwriting (recoverable via aft_safety undo)
34189
34393
  - Auto-formats using project formatter if configured (biome.json, .prettierrc, etc.)
34190
- - 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
34191
34395
  - Use this for creating new files or completely replacing file contents
34192
- - For partial edits (find/replace), use the \`${editToolName}\` tool instead
34193
-
34194
- 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`;
34195
34397
  }
34196
34398
  function createWriteTool(ctx, editToolName = "edit") {
34197
34399
  return {
34198
34400
  description: getWriteDescription(editToolName),
34199
34401
  args: {
34200
34402
  filePath: z7.string().describe("Path to the file to write (absolute or relative to project root)"),
34201
- 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)
34202
34405
  },
34203
34406
  execute: async (args, context) => {
34204
34407
  const file2 = args.filePath;
@@ -34220,8 +34423,8 @@ function createWriteTool(ctx, editToolName = "edit") {
34220
34423
  file: filePath,
34221
34424
  content,
34222
34425
  create_dirs: true,
34223
- diagnostics: true,
34224
- include_diff: true
34426
+ diagnostics: args.diagnostics ?? diagnosticsOnEditDefault(ctx),
34427
+ include_diff_content: true
34225
34428
  });
34226
34429
  if (data.success === false) {
34227
34430
  throw new Error(data.message || "write failed");
@@ -34251,7 +34454,7 @@ LSP errors detected, please fix:
34251
34454
  if (pendingServers && pendingServers.length > 0) {
34252
34455
  output += `
34253
34456
 
34254
- 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.`;
34255
34458
  }
34256
34459
  if (exitedServers && exitedServers.length > 0) {
34257
34460
  output += `
@@ -34333,11 +34536,8 @@ Note: Modes 6 and 7 are options on mode 5 (find/replace) — they require \`oldS
34333
34536
  - Auto-formats using project formatter if configured
34334
34537
  - Tree-sitter syntax validation on all edits
34335
34538
  - Symbol replace includes decorators, attributes, and doc comments in range
34336
- - LSP error-level diagnostics are returned automatically after edits
34337
-
34338
- Returns: JSON string for the selected edit mode. Edits may append inline LSP error lines.
34339
-
34340
- 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.`;
34341
34541
  }
34342
34542
  function createEditTool(ctx, writeToolName = "write") {
34343
34543
  return {
@@ -34352,7 +34552,8 @@ function createEditTool(ctx, writeToolName = "write") {
34352
34552
  content: z7.string().optional().describe("Replacement content for symbol mode or operations[].command='write'. For whole-file writes, use the `write` tool."),
34353
34553
  appendContent: z7.string().optional().describe("Text to append to the end of filePath; creates the file if needed"),
34354
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 }"),
34355
- 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)
34356
34557
  },
34357
34558
  execute: async (args, context) => {
34358
34559
  const argsRecord = args;
@@ -34448,8 +34649,8 @@ function createEditTool(ctx, writeToolName = "write") {
34448
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.";
34449
34650
  throw new Error(`edit: no edit mode resolved from arguments.${hint}`);
34450
34651
  }
34451
- params.diagnostics = true;
34452
- params.include_diff = true;
34652
+ params.diagnostics = args.diagnostics ?? diagnosticsOnEditDefault(ctx);
34653
+ params.include_diff_content = true;
34453
34654
  const data = await callBridge(ctx, context, command, params);
34454
34655
  if (data.success && data.diff) {
34455
34656
  const diff = data.diff;
@@ -34474,7 +34675,12 @@ function createEditTool(ctx, writeToolName = "write") {
34474
34675
  });
34475
34676
  }
34476
34677
  }
34477
- 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);
34478
34684
  const globSkipNote = formatGlobSkipReasonsNote(data.format_skip_reasons);
34479
34685
  if (globSkipNote)
34480
34686
  result += `
@@ -34502,7 +34708,7 @@ ${diagLines}`;
34502
34708
  if (pendingServers && pendingServers.length > 0) {
34503
34709
  result += `
34504
34710
 
34505
- 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.`;
34506
34712
  }
34507
34713
  if (exitedServers && exitedServers.length > 0) {
34508
34714
  result += `
@@ -34563,15 +34769,17 @@ Example patch:
34563
34769
  - You must include a header with your intended action (Add/Delete/Update)
34564
34770
  - You must prefix new lines with \`+\` even when creating a new file
34565
34771
 
34566
- 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.`;
34567
34773
  function createApplyPatchTool(ctx) {
34568
34774
  return {
34569
34775
  description: APPLY_PATCH_DESCRIPTION,
34570
34776
  args: {
34571
- 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)
34572
34779
  },
34573
34780
  execute: async (args, context) => {
34574
34781
  const patchText = args.patchText;
34782
+ const diagnostics = args.diagnostics ?? diagnosticsOnEditDefault(ctx);
34575
34783
  if (!patchText)
34576
34784
  throw new Error("'patchText' is required");
34577
34785
  let hunks;
@@ -34651,8 +34859,8 @@ function createApplyPatchTool(ctx) {
34651
34859
  file: filePath,
34652
34860
  content,
34653
34861
  create_dirs: true,
34654
- diagnostics: true,
34655
- include_diff: true,
34862
+ diagnostics,
34863
+ include_diff_content: true,
34656
34864
  multi_file_write_paths: multiFileWritePaths
34657
34865
  });
34658
34866
  const wrDiff = writeResult.diff;
@@ -34671,7 +34879,7 @@ function createApplyPatchTool(ctx) {
34671
34879
  const filePath2 = path4.resolve(context.directory, hunk.path);
34672
34880
  if (fs6.existsSync(filePath2)) {
34673
34881
  try {
34674
- await callBridge(ctx, context, "delete_file", { file: filePath2 });
34882
+ fs6.rmSync(filePath2, { force: true });
34675
34883
  } catch {}
34676
34884
  }
34677
34885
  }
@@ -34704,8 +34912,8 @@ function createApplyPatchTool(ctx) {
34704
34912
  file: targetPath,
34705
34913
  content: newContent,
34706
34914
  create_dirs: true,
34707
- diagnostics: true,
34708
- include_diff: true,
34915
+ diagnostics,
34916
+ include_diff_content: true,
34709
34917
  multi_file_write_paths: multiFileWritePaths
34710
34918
  });
34711
34919
  const diags = writeResult.lsp_diagnostics;
@@ -34978,7 +35186,7 @@ function aftPrefixedTools(ctx) {
34978
35186
  file: filePath,
34979
35187
  content: normalizedArgs.content,
34980
35188
  create_dirs: normalizedArgs.create_dirs !== false,
34981
- diagnostics: true
35189
+ diagnostics: normalizedArgs.diagnostics ?? diagnosticsOnEditDefault(ctx)
34982
35190
  };
34983
35191
  const response = await callBridge(ctx, context, "write", writeParams);
34984
35192
  if (response.success === false) {
@@ -35067,44 +35275,169 @@ function importTools(ctx) {
35067
35275
  };
35068
35276
  }
35069
35277
 
35070
- // src/tools/lsp.ts
35278
+ // src/tools/inspect.ts
35071
35279
  import { tool as tool9 } from "@opencode-ai/plugin";
35072
35280
  var z9 = tool9.schema;
35073
- function lspTools(ctx) {
35074
- const diagnosticsTool = {
35075
- description: "On-demand LSP file/scope check. NOT a project-wide type checker — use `tsc --noEmit`, `cargo check`, `pyright` etc. for full coverage.\n" + `
35076
- ` + "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.",
35077
35416
  args: {
35078
- filePath: z9.string().optional().describe("Path to a file to check. Mutually exclusive with 'directory'. Omit both to dump all cached diagnostics."),
35079
- directory: z9.string().optional().describe("Path to a directory. Returns cached diagnostics + workspace pull; capped at 200 walked files."),
35080
- severity: z9.enum(["error", "warning", "information", "hint", "all"]).optional().describe("Filter by severity (default: 'all')."),
35081
- 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."))
35082
35420
  },
35083
35421
  execute: async (args, context) => {
35084
- const filePath = args.filePath || undefined;
35085
- const directory = args.directory || undefined;
35086
- if (filePath !== undefined && directory !== undefined) {
35087
- throw new Error("'filePath' and 'directory' are mutually exclusive provide one or neither");
35088
- }
35089
- const params = {};
35090
- if (filePath !== undefined)
35091
- params.file = filePath;
35092
- if (directory !== undefined)
35093
- params.directory = directory;
35094
- if (args.severity !== undefined)
35095
- params.severity = args.severity;
35096
- if (args.waitMs !== undefined)
35097
- params.wait_ms = args.waitMs;
35098
- const result = await callBridge(ctx, context, "lsp_diagnostics", params);
35099
- if (result.success === false) {
35100
- 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");
35101
35428
  }
35102
- 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;
35103
35437
  }
35104
35438
  };
35105
- const hoisting = ctx.config.hoist_builtin_tools !== false;
35106
35439
  return {
35107
- [hoisting ? "lsp_diagnostics" : "aft_lsp_diagnostics"]: diagnosticsTool
35440
+ aft_inspect: inspectTool
35108
35441
  };
35109
35442
  }
35110
35443
 
@@ -35113,18 +35446,18 @@ import { tool as tool10 } from "@opencode-ai/plugin";
35113
35446
  var z10 = tool10.schema;
35114
35447
  function navigationTools(ctx) {
35115
35448
  return {
35116
- aft_navigate: {
35117
- 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.
35118
35451
 
35119
35452
  ` + `Ops:
35120
- ` + `- 'call_tree': See what a function calls (forward traversal). Use to understand dependencies before modifying a function.
35121
35453
  ` + `- 'callers': Find all call sites of a symbol. Use before renaming or changing a function's signature.
35122
- ` + `- 'trace_to': Trace how execution reaches a function from entry points (routes, exports, main). Use to understand context around deeply-nested code.
35123
- ` + `- '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.
35124
- ` + `- '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.
35125
35458
  ` + `- 'trace_data': Follow a value through variable assignments and function parameters across files. Requires 'symbol' (scope to trace from) and 'expression'.
35126
35459
 
35127
- ` + `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.
35128
35461
 
35129
35462
  `,
35130
35463
  args: {
@@ -35174,7 +35507,7 @@ function getCallID3(ctx) {
35174
35507
  return c.callID ?? c.callId ?? c.call_id;
35175
35508
  }
35176
35509
  function buildZoomTitle(args) {
35177
- if (args.targets !== undefined && args.targets !== null) {
35510
+ if (!isEmptyParam(args.targets)) {
35178
35511
  if (Array.isArray(args.targets)) {
35179
35512
  if (args.targets.length === 1 && args.targets[0]) {
35180
35513
  return `${args.targets[0].filePath}#${args.targets[0].symbol}`;
@@ -35303,14 +35636,42 @@ function readingTools(ctx) {
35303
35636
  contextLines: optionalInt(1, Number.MAX_SAFE_INTEGER).describe("Lines of context before/after the symbol (default: 3)")
35304
35637
  },
35305
35638
  execute: async (args, context) => {
35306
- const hasFilePath = typeof args.filePath === "string" && args.filePath.length > 0;
35307
- const hasUrl = typeof args.url === "string" && args.url.length > 0;
35308
- const hasTargets = args.targets !== undefined && args.targets !== null;
35309
- const hasSymbols = args.symbols !== undefined && args.symbols !== null && (typeof args.symbols === "string" ? args.symbols.length > 0 : Array.isArray(args.symbols) && args.symbols.length > 0);
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
+ };
35655
+ const hasFilePath = !isEmptyParam(args.filePath);
35656
+ const hasUrl = !isEmptyParam(args.url);
35657
+ const hasTargets = hasTargetsProvided(args.targets);
35658
+ const hasSymbols = !isEmptyParam(args.symbols);
35310
35659
  const zoomCallID = getCallID3(context);
35311
35660
  if (zoomCallID) {
35312
35661
  const title = buildZoomTitle(args);
35313
- storeToolMetadata(context.sessionID, zoomCallID, { title, metadata: { title } });
35662
+ const display = { title };
35663
+ if (hasFilePath)
35664
+ display.filePath = args.filePath;
35665
+ if (hasUrl)
35666
+ display.url = args.url;
35667
+ if (hasSymbols) {
35668
+ display.symbols = typeof args.symbols === "string" ? args.symbols : JSON.stringify(args.symbols);
35669
+ }
35670
+ if (hasTargets)
35671
+ display.targets = JSON.stringify(args.targets);
35672
+ if (args.contextLines !== undefined)
35673
+ display.contextLines = args.contextLines;
35674
+ storeToolMetadata(context.sessionID, zoomCallID, { title, metadata: display });
35314
35675
  }
35315
35676
  if (hasTargets) {
35316
35677
  if (hasFilePath || hasUrl || hasSymbols) {
@@ -35560,14 +35921,14 @@ function refactoringTools(ctx) {
35560
35921
  },
35561
35922
  execute: async (args, context) => {
35562
35923
  const op = args.op;
35563
- if ((op === "move" || op === "inline") && typeof args.symbol !== "string") {
35924
+ if ((op === "move" || op === "inline") && isEmptyParam(args.symbol)) {
35564
35925
  throw new Error(`'symbol' is required for '${op}' op`);
35565
35926
  }
35566
- if (op === "move" && typeof args.destination !== "string") {
35927
+ if (op === "move" && isEmptyParam(args.destination)) {
35567
35928
  throw new Error("'destination' is required for 'move' op");
35568
35929
  }
35569
35930
  if (op === "extract") {
35570
- if (typeof args.name !== "string")
35931
+ if (isEmptyParam(args.name))
35571
35932
  throw new Error("'name' is required for 'extract' op");
35572
35933
  if (args.startLine === undefined)
35573
35934
  throw new Error("'startLine' is required for 'extract' op");
@@ -35749,7 +36110,7 @@ function expandTilde(input) {
35749
36110
  }
35750
36111
  return input;
35751
36112
  }
35752
- function arg(schema) {
36113
+ function arg2(schema) {
35753
36114
  return schema;
35754
36115
  }
35755
36116
  function formatGrepOutput(response) {
@@ -35847,9 +36208,9 @@ function searchTools(ctx) {
35847
36208
  const grepTool = {
35848
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.",
35849
36210
  args: {
35850
- pattern: arg(exports_external.string().describe("Regular expression pattern to search for")),
35851
- include: arg(exports_external.string().optional().describe("File pattern to include (e.g. '*.ts', '*.{ts,tsx}')")),
35852
- 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)"))
35853
36214
  },
35854
36215
  execute: async (args, context) => {
35855
36216
  const pattern = String(args.pattern);
@@ -35886,8 +36247,8 @@ function searchTools(ctx) {
35886
36247
  const globTool = {
35887
36248
  description: "Find files matching a glob pattern. Returns matching file paths sorted by modification time.",
35888
36249
  args: {
35889
- pattern: arg(exports_external.string().describe("Glob pattern to match (e.g. '**/*.ts', 'src/**/*.test.*')")),
35890
- 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)"))
35891
36252
  },
35892
36253
  execute: async (args, context) => {
35893
36254
  let globPattern = expandTilde(String(args.pattern));
@@ -35944,48 +36305,87 @@ function searchTools(ctx) {
35944
36305
  // src/tools/semantic.ts
35945
36306
  import { tool as tool14 } from "@opencode-ai/plugin";
35946
36307
  var z14 = tool14.schema;
35947
- 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) {
35948
36321
  return schema;
35949
36322
  }
35950
36323
  function semanticTools(ctx) {
35951
36324
  const searchTool = {
35952
36325
  description: [
35953
- "Find symbols by concept using hybrid semantic + lexical search. Returns ranked code matches with similarity scores and provenance tags.",
36326
+ "Find code with unified semantic, lexical, literal, and regex search. Returns ranked symbol/file results or exact matching lines, with routing metadata.",
35954
36327
  "",
35955
36328
  "When to reach for it:",
35956
36329
  "- Exploring an unfamiliar area: 'where is rate limiting handled', 'how does auth flow work'",
35957
36330
  "- Concept doesn't appear as a literal string: 'retry logic', 'cache invalidation', 'graceful shutdown'",
35958
36331
  "- Filename-shaped concepts: 'the bridge spawn helper', 'the session detection module'",
35959
- "- After 2+ grep attempts that came back empty or noisy",
36332
+ "- Regex-shaped or exact text queries when you want AFT to classify and route automatically",
35960
36333
  "- You know roughly what the function does but not what it's named",
35961
36334
  "",
35962
36335
  "When NOT to use:",
35963
- "- You have an error message or stack trace → use grep",
36336
+ "- You need exhaustive literal enumeration → use grep directly",
35964
36337
  "- You want the file/module structure → use aft_outline",
35965
- "- You're following a call chain → use aft_navigate",
36338
+ "- You're following a call chain → use aft_callgraph",
35966
36339
  "",
35967
- "Each result tags `source` as one of: 'semantic' (embedding match only), 'lexical' (trigram exact-token match the embedding lane missed), or 'hybrid' (both lanes agreed strongest signal)."
36340
+ "Set hint to 'regex', 'literal', 'semantic', or 'auto' to override or document routing intent."
35968
36341
  ].join(`
35969
36342
  `),
35970
36343
  args: {
35971
- query: arg2(z14.string().describe("Concept or capability to find, phrased as a programmer would describe the code. Examples: 'fuzzy match with whitespace tolerance', 'undo backup before edit', 'retry failed network request'.")),
35972
- topK: arg2(optionalInt(1, 100).describe("Number of results (default: 10, max: 100)"))
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'."))
35973
36347
  },
35974
36348
  execute: async (args, context) => {
35975
- const response = await callBridge(ctx, context, "semantic_search", {
35976
- query: args.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");
36351
+ }
36352
+ const query = args.query;
36353
+ const hint = typeof args.hint === "string" ? args.hint : undefined;
36354
+ if (hint !== "semantic") {
36355
+ const denied = await askGrepPermission(context, query);
36356
+ if (denied)
36357
+ return permissionDeniedResponse(denied);
36358
+ }
36359
+ const bridgeParams = {
36360
+ query,
35977
36361
  top_k: args.topK ?? 10
35978
- });
36362
+ };
36363
+ if (hint)
36364
+ bridgeParams.hint = hint;
36365
+ const response = await callBridge(ctx, context, "semantic_search", bridgeParams);
35979
36366
  if (response.success === false) {
35980
- if (response.code === "semantic_search_unavailable" && typeof response.message === "string") {
35981
- return response.message;
35982
- }
35983
- throw new Error(response.message || "semantic_search failed");
35984
- }
35985
- if (typeof response.text === "string") {
35986
- 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}`;
35987
36384
  }
35988
- return JSON.stringify(response);
36385
+ return honestyNote ? `${honestyNote}
36386
+
36387
+ Structured response:
36388
+ ${structured}` : structured;
35989
36389
  }
35990
36390
  };
35991
36391
  return {
@@ -36115,19 +36515,27 @@ function buildWorkflowHints(opts) {
36115
36515
  const hasZoom = !opts.disabledTools.has("aft_zoom");
36116
36516
  const hasGrep = opts.toolSurface !== "minimal" && !opts.disabledTools.has(grepName);
36117
36517
  const hasSearch = opts.toolSurface !== "minimal" && opts.semanticEnabled && !opts.disabledTools.has("aft_search");
36118
- 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");
36119
36520
  const hasBash = !opts.disabledTools.has(bashName);
36120
36521
  const hasBgBash = hasBash && opts.bashBackgroundEnabled && !opts.disabledTools.has(bashStatusName);
36121
36522
  if (hasOutline && hasZoom) {
36122
36523
  sections.push(`**Web/URL access**: \`aft_outline({ target: url })\` first for structure, then \`aft_zoom({ url, symbols: "<heading>" })\` for the specific section.`);
36123
36524
  }
36124
36525
  if (hasOutline && hasZoom && (hasGrep || hasSearch)) {
36125
- const locator = hasGrep ? `\`${grepName}\`` : "`aft_search`";
36126
- sections.push(hasGrep && hasSearch ? `**Code exploration**: For exact identifiers (\`useState\`, function names, env vars), error messages, or path-shaped queries → \`${grepName}\` first. For broad concepts ('where is X handled', 'how does Y work') \`aft_search\`. Then use \`aft_outline\` for structure → \`aft_zoom\` for symbol(s).` : `**Code exploration**: ${locator} to locate → \`aft_outline\` for structure → \`aft_zoom\` for symbol(s).`);
36526
+ if (hasSearch) {
36527
+ const grepFallback = hasGrep ? ` Use \`${grepName}\` directly only when you need exhaustive enumeration of literal text (every TODO, every import of X) without ranking.` : "";
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}`);
36529
+ } else {
36530
+ sections.push(`**Code exploration**: \`${grepName}\` to locate → \`aft_outline\` for structure → \`aft_zoom\` for symbol(s).`);
36531
+ }
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.");
36127
36535
  }
36128
36536
  if (hasNavigate) {
36129
36537
  sections.push([
36130
- "Use `aft_navigate` instead of grep + read chains for relationship questions:",
36538
+ "Use `aft_callgraph` for code-relationship questions instead of grep + read chains:",
36131
36539
  "- `callers` — find all call sites before changing a function signature",
36132
36540
  "- `impact` — blast radius (which functions/files will need updates)",
36133
36541
  "- `trace_to` — how execution reaches this code from entry points (routes, exports, main)",
@@ -36244,13 +36652,13 @@ var PLUGIN_VERSION = (() => {
36244
36652
  return "0.0.0";
36245
36653
  }
36246
36654
  })();
36247
- var ANNOUNCEMENT_VERSION = "0.31.0";
36655
+ var ANNOUNCEMENT_VERSION = "0.33.0";
36248
36656
  var ANNOUNCEMENT_FEATURES = [
36249
- "`aft_navigate` has a new `trace_to_symbol` op for path-between-symbols queries. One call returns the shortest call path from one function to another with file + line for every hop. Honest errors for ambiguous, missing, or unreachable targets including a candidate list to disambiguate.",
36250
- '`aft_outline target: "<dir>", files: true` returns an indexed file tree with language, symbol count, and byte size per file. Lets agents pick which files to actually open without first reading symbol bodies. Honest truncation (`complete: false` + `walk_truncated`/`unchecked_files`) when the directory exceeds the walk cap.',
36251
- '`aft_zoom` adds `targets` for cross-file batches — pull bodies from different files in one call. Schema breaking change: `symbol` is removed; pass `symbols: "name"` (string) or `symbols: ["a", "b"]` (array). Same shape works for `url` mode so you can grab multiple sections from one URL fetch.',
36252
- "Bun/npm/pnpm test compressors now match on output shape, not just on the head token. Wrapper invocations like `bun run --cwd packages/foo test`, `npm test`, and `pnpm test` keep failing-test bodies on the first run instead of dropping them to a generic summary.",
36253
- "Tool descriptions trimmed: ~1.2K agent-facing tokens removed (-19% on OpenCode, -2% on Pi) by dropping redundant `Returns:` blocks and compressing `lsp_diagnostics` while preserving its load-bearing honesty guidance."
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."
36254
36662
  ];
36255
36663
  var ANNOUNCEMENT_FOOTER = "Join us on Discord: https://discord.gg/F2uWxjGnU";
36256
36664
  var plugin = async (input) => initializePluginForDirectory(input);
@@ -36462,7 +36870,10 @@ ${lines}
36462
36870
  });
36463
36871
  }
36464
36872
  const rpcServer = new AftRpcServer(configOverrides.storage_dir, input.directory);
36465
- const unregisterShutdown = registerShutdownCleanup(async () => {
36873
+ let clearInspectTier2Idle = () => {};
36874
+ registerShutdownCleanup(async () => {
36875
+ autoUpdateAbort.abort();
36876
+ clearInspectTier2Idle();
36466
36877
  await Promise.allSettled([abortInFlightAutoInstalls(), abortInFlightGithubInstalls()]);
36467
36878
  try {
36468
36879
  rpcServer.stop();
@@ -36562,7 +36973,7 @@ Install: ${getManualInstallHint()}`).catch(() => {});
36562
36973
  }
36563
36974
  const surface = aftConfig.tool_surface ?? "recommended";
36564
36975
  const ALL_ONLY_TOOLS = new Set([
36565
- "aft_navigate",
36976
+ "aft_callgraph",
36566
36977
  "aft_delete",
36567
36978
  "aft_move",
36568
36979
  "aft_transform",
@@ -36577,9 +36988,9 @@ Install: ${getManualInstallHint()}`).catch(() => {});
36577
36988
  ...navigationTools(ctx),
36578
36989
  ...surface !== "minimal" && astTools(ctx),
36579
36990
  ...surface !== "minimal" && aftConfig.semantic_search === true && semanticTools(ctx),
36991
+ ...inspectToolSurfaceEnabled(aftConfig) && inspectTools(ctx),
36580
36992
  ...surface !== "minimal" && aftConfig.search_index === true && searchTools(ctx),
36581
36993
  ...refactoringTools(ctx),
36582
- ...surface !== "minimal" && lspTools(ctx),
36583
36994
  ...surface !== "minimal" && conflictTools(ctx)
36584
36995
  });
36585
36996
  if (surface !== "all") {
@@ -36610,7 +37021,8 @@ Install: ${getManualInstallHint()}`).catch(() => {});
36610
37021
  "aft_outline",
36611
37022
  "aft_zoom",
36612
37023
  "aft_search",
36613
- "aft_navigate",
37024
+ "aft_callgraph",
37025
+ "aft_inspect",
36614
37026
  "grep",
36615
37027
  "aft_grep",
36616
37028
  "bash",
@@ -36627,6 +37039,20 @@ Install: ${getManualInstallHint()}`).catch(() => {});
36627
37039
  if (hintsBlock) {
36628
37040
  log2(`Workflow hints injected (${hintsBlock.length} chars)`);
36629
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();
36630
37056
  return {
36631
37057
  tool: allTools,
36632
37058
  "experimental.chat.system.transform": async (_input, output) => {
@@ -36636,11 +37062,17 @@ Install: ${getManualInstallHint()}`).catch(() => {});
36636
37062
  },
36637
37063
  event: async (eventInput) => {
36638
37064
  await autoUpdateEventHook(eventInput);
36639
- if (eventInput.event.type !== "session.idle")
36640
- return;
37065
+ const eventType = eventInput.event.type;
36641
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;
36642
37073
  if (!sessionID)
36643
37074
  return;
37075
+ inspectTier2Idle.schedule(sessionID);
36644
37076
  const sessionDir = await getSessionDirectory(input.client, sessionID, input.directory) ?? input.directory;
36645
37077
  await handleIdleBgCompletions({
36646
37078
  ctx,
@@ -36654,6 +37086,10 @@ Install: ${getManualInstallHint()}`).catch(() => {});
36654
37086
  const sid = messageInput.sessionID ?? messageInput.sessionId ?? messageInput.id;
36655
37087
  warmSessionDirectory(input.client, sid, input.directory);
36656
37088
  },
37089
+ "tool.execute.before": async (toolInput) => {
37090
+ if (toolInput.sessionID)
37091
+ inspectTier2Idle.clear(toolInput.sessionID);
37092
+ },
36657
37093
  "command.execute.before": async (commandInput, _output) => {
36658
37094
  if (isTuiMode2() || commandInput.command !== STATUS_COMMAND) {
36659
37095
  return;
@@ -36704,12 +37140,7 @@ Install: ${getManualInstallHint()}`).catch(() => {});
36704
37140
  };
36705
37141
  },
36706
37142
  dispose: async () => {
36707
- autoUpdateAbort.abort();
36708
- unregisterShutdown();
36709
- await Promise.allSettled([abortInFlightAutoInstalls(), abortInFlightGithubInstalls()]);
36710
- rpcServer.stop();
36711
- await disposeAllPtyTerminals();
36712
- await pool.shutdown();
37143
+ await runCleanups("dispose");
36713
37144
  }
36714
37145
  };
36715
37146
  }