@cortexkit/aft-pi 0.39.2 → 0.39.4

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
@@ -12177,6 +12177,21 @@ class BridgeReplacedDuringVersionCheck extends Error {
12177
12177
  }
12178
12178
  }
12179
12179
 
12180
+ class BridgeTransportTimeoutError extends Error {
12181
+ command;
12182
+ timeoutMs;
12183
+ code = "transport_timeout";
12184
+ constructor(command, timeoutMs, message) {
12185
+ super(message);
12186
+ this.command = command;
12187
+ this.timeoutMs = timeoutMs;
12188
+ this.name = "BridgeTransportTimeoutError";
12189
+ }
12190
+ }
12191
+ function isBridgeTransportTimeout(err) {
12192
+ return err instanceof Error && err.code === "transport_timeout";
12193
+ }
12194
+
12180
12195
  class BinaryBridge {
12181
12196
  static RESTART_RESET_MS = 5 * 60 * 1000;
12182
12197
  static STDERR_TAIL_MAX = 20;
@@ -12404,7 +12419,7 @@ class BinaryBridge {
12404
12419
  } else {
12405
12420
  this.warnVia(timeoutMsg2);
12406
12421
  }
12407
- entry.reject(new Error(`${this.errorPrefix} Request "${command}" (id=${id}) timed out after ${effectiveTimeoutMs}ms`));
12422
+ entry.reject(new BridgeTransportTimeoutError(command, effectiveTimeoutMs, `${this.errorPrefix} Request "${command}" (id=${id}) timed out after ${effectiveTimeoutMs}ms`));
12408
12423
  return;
12409
12424
  }
12410
12425
  const childActiveSinceRequest = this.lastChildActivityAt > requestSentAt;
@@ -12940,6 +12955,220 @@ class BinaryBridge {
12940
12955
  }
12941
12956
  }
12942
12957
  }
12958
+ // ../aft-bridge/dist/callgraph-format.js
12959
+ import { homedir as homedir3 } from "node:os";
12960
+ var PLAIN_CALLGRAPH_THEME = {
12961
+ fg: (_role, text) => text
12962
+ };
12963
+ function asRecord(value) {
12964
+ if (!value || typeof value !== "object" || Array.isArray(value))
12965
+ return;
12966
+ return value;
12967
+ }
12968
+ function asRecords(value) {
12969
+ return Array.isArray(value) ? value.map(asRecord).filter(Boolean) : [];
12970
+ }
12971
+ function asString(value) {
12972
+ return typeof value === "string" ? value : undefined;
12973
+ }
12974
+ function asNumber(value) {
12975
+ return typeof value === "number" && Number.isFinite(value) ? value : undefined;
12976
+ }
12977
+ function asBoolean(value) {
12978
+ return typeof value === "boolean" ? value : undefined;
12979
+ }
12980
+ function shortenPath(path2) {
12981
+ const home = homedir3();
12982
+ if (path2.startsWith(home))
12983
+ return `~${path2.slice(home.length)}`;
12984
+ return path2;
12985
+ }
12986
+ function joinNonEmpty(parts, separator = " · ") {
12987
+ return parts.filter((part) => Boolean(part && part.length > 0)).join(separator);
12988
+ }
12989
+ function treeLine(depth, text) {
12990
+ return `${" ".repeat(depth)}${depth === 0 ? "" : "↳ "}${text}`;
12991
+ }
12992
+ function nameMatchEdgeMarker(record, theme) {
12993
+ return asString(record.resolved_by) === "name_match" ? ` ${theme.fg("warning", "~")}` : "";
12994
+ }
12995
+ function renderCallTreeNode(node, depth, lines, theme) {
12996
+ const name = asString(node.name) ?? "(unknown)";
12997
+ const file = shortenPath(asString(node.file) ?? "(unknown file)");
12998
+ const line = asNumber(node.line);
12999
+ const unresolved = node.resolved === false ? ` ${theme.fg("warning", "[unresolved]")}` : "";
13000
+ const nameMatch = nameMatchEdgeMarker(node, theme);
13001
+ const location = line !== undefined ? `[${file}:${line}]` : `[${file}]`;
13002
+ lines.push(treeLine(depth, `${name} ${location}${unresolved}${nameMatch}`));
13003
+ asRecords(node.children).forEach((child) => {
13004
+ renderCallTreeNode(child, depth + 1, lines, theme);
13005
+ });
13006
+ }
13007
+ function depthWarning(response, theme, depthField = "depth_limited", truncatedField = "truncated") {
13008
+ const limited = asBoolean(response[depthField]);
13009
+ const truncated = asNumber(response[truncatedField]) ?? 0;
13010
+ if (!limited && truncated === 0)
13011
+ return "";
13012
+ const detail = truncated > 0 ? `, ${truncated} truncated` : "";
13013
+ return theme.fg("warning", `(depth limited${detail})`);
13014
+ }
13015
+ function renderTracePath(path2, index, lines, theme) {
13016
+ lines.push(`Path ${index + 1}`);
13017
+ asRecords(path2.hops).forEach((hop, hopIndex) => {
13018
+ const symbol = asString(hop.symbol) ?? "(unknown)";
13019
+ const file = shortenPath(asString(hop.file) ?? "(unknown file)");
13020
+ const line = asNumber(hop.line);
13021
+ const entry = hop.is_entry_point === true ? " [entry]" : "";
13022
+ const nameMatch = nameMatchEdgeMarker(hop, theme);
13023
+ lines.push(treeLine(hopIndex + 1, `${symbol}${entry} ${line !== undefined ? `[${file}:${line}]` : `[${file}]`}${nameMatch}`));
13024
+ });
13025
+ }
13026
+ function renderCallersGroupLines(group, theme) {
13027
+ const file = shortenPath(asString(group.file) ?? "(unknown file)");
13028
+ const lines = [theme.fg("accent", file)];
13029
+ const callers = asRecords(group.callers);
13030
+ const bySymbolProvenance = new Map;
13031
+ for (const caller of callers) {
13032
+ const symbol = asString(caller.symbol) ?? "(unknown)";
13033
+ const provenanceKey = asString(caller.resolved_by) === "name_match" ? `${symbol}\x00name_match` : `${symbol}\x00exact`;
13034
+ const line = asNumber(caller.line);
13035
+ const bucket = bySymbolProvenance.get(provenanceKey) ?? [];
13036
+ if (line !== undefined)
13037
+ bucket.push(line);
13038
+ bySymbolProvenance.set(provenanceKey, bucket);
13039
+ }
13040
+ const keys = [...bySymbolProvenance.keys()].sort((a, b) => a.localeCompare(b));
13041
+ for (const key of keys) {
13042
+ const symbol = key.split("\x00")[0] ?? "(unknown)";
13043
+ const isNameMatch = key.endsWith("\x00name_match");
13044
+ const lineNums = (bySymbolProvenance.get(key) ?? []).sort((a, b) => a - b);
13045
+ const linePart = lineNums.length > 0 ? lineNums.map(String).join(", ") : "?";
13046
+ const marker = isNameMatch ? ` ${theme.fg("warning", "~")}` : "";
13047
+ lines.push(` ↳ ${symbol}:${linePart}${marker}`);
13048
+ }
13049
+ return lines;
13050
+ }
13051
+ function formatCallgraphSections(op, response, theme = PLAIN_CALLGRAPH_THEME) {
13052
+ const record = asRecord(response);
13053
+ if (!record)
13054
+ return [theme.fg("muted", "No navigation result.")];
13055
+ if (op === "call_tree") {
13056
+ const lines = [];
13057
+ renderCallTreeNode(record, 0, lines, theme);
13058
+ const warning = depthWarning(record, theme);
13059
+ if (warning)
13060
+ lines.push(warning);
13061
+ return lines.length > 0 ? lines : [theme.fg("muted", "No call tree available.")];
13062
+ }
13063
+ if (op === "callers") {
13064
+ const groups = asRecords(record.callers);
13065
+ const warning = depthWarning(record, theme);
13066
+ const total = asNumber(record.total_callers) ?? 0;
13067
+ const sections2 = [
13068
+ joinNonEmpty([
13069
+ theme.fg("success", `${total} caller${total === 1 ? "" : "s"}`),
13070
+ theme.fg("muted", `${groups.length} file group${groups.length === 1 ? "" : "s"}`),
13071
+ warning
13072
+ ])
13073
+ ];
13074
+ groups.forEach((group) => {
13075
+ sections2.push(renderCallersGroupLines(group, theme).join(`
13076
+ `));
13077
+ });
13078
+ return sections2;
13079
+ }
13080
+ if (op === "trace_to_symbol") {
13081
+ const path2 = asRecords(record.path);
13082
+ const complete = asBoolean(record.complete);
13083
+ const reason = asString(record.reason);
13084
+ if (path2.length === 0) {
13085
+ const prefix = complete === false ? theme.fg("warning", "No complete path") : theme.fg("muted", "No path");
13086
+ return [`${prefix}${reason ? ` (${reason})` : ""}`];
13087
+ }
13088
+ const lines = [theme.fg("success", `${path2.length} hop${path2.length === 1 ? "" : "s"}`)];
13089
+ path2.forEach((hop, index) => {
13090
+ const symbol = asString(hop.symbol) ?? "(unknown)";
13091
+ const file = shortenPath(asString(hop.file) ?? "(unknown file)");
13092
+ const line = asNumber(hop.line);
13093
+ const nameMatch = nameMatchEdgeMarker(hop, theme);
13094
+ lines.push(treeLine(index + 1, `${symbol} ${line !== undefined ? `[${file}:${line}]` : `[${file}]`}${nameMatch}`));
13095
+ });
13096
+ return lines;
13097
+ }
13098
+ if (op === "trace_to") {
13099
+ const paths = asRecords(record.paths);
13100
+ const warning = depthWarning(record, theme, "max_depth_reached", "truncated_paths");
13101
+ const totalPaths = asNumber(record.total_paths) ?? paths.length;
13102
+ const entryPoints = asNumber(record.entry_points_found) ?? 0;
13103
+ const sections2 = [
13104
+ joinNonEmpty([
13105
+ theme.fg("success", `${totalPaths} path${totalPaths === 1 ? "" : "s"}`),
13106
+ theme.fg("muted", `${entryPoints} entry point${entryPoints === 1 ? "" : "s"}`),
13107
+ warning
13108
+ ])
13109
+ ];
13110
+ if (paths.length === 0)
13111
+ sections2.push(theme.fg("muted", "No entry paths found."));
13112
+ paths.forEach((path2, index) => {
13113
+ const lines = [];
13114
+ renderTracePath(path2, index, lines, theme);
13115
+ sections2.push(lines.join(`
13116
+ `));
13117
+ });
13118
+ return sections2;
13119
+ }
13120
+ if (op === "impact") {
13121
+ const callers = asRecords(record.callers);
13122
+ const warning = depthWarning(record, theme);
13123
+ const totalAffected = asNumber(record.total_affected) ?? callers.length;
13124
+ const affectedFiles = asNumber(record.affected_files) ?? 0;
13125
+ const sections2 = [
13126
+ joinNonEmpty([
13127
+ theme.fg("warning", `${totalAffected} affected call site${totalAffected === 1 ? "" : "s"}`),
13128
+ theme.fg("muted", `${affectedFiles} file${affectedFiles === 1 ? "" : "s"}`),
13129
+ warning
13130
+ ])
13131
+ ];
13132
+ if (callers.length === 0)
13133
+ sections2.push(theme.fg("muted", "No impacted callers found."));
13134
+ callers.forEach((caller) => {
13135
+ const file = shortenPath(asString(caller.caller_file) ?? "(unknown file)");
13136
+ const symbol = asString(caller.caller_symbol) ?? "(unknown)";
13137
+ const line = asNumber(caller.line) ?? 0;
13138
+ const entry = caller.is_entry_point === true ? ` ${theme.fg("warning", "[entry]")}` : "";
13139
+ const nameMatch = nameMatchEdgeMarker(caller, theme);
13140
+ const expression = asString(caller.call_expression);
13141
+ const params = Array.isArray(caller.parameters) ? caller.parameters.map(String).join(", ") : "";
13142
+ sections2.push([
13143
+ `${theme.fg("accent", file)}:${line}`,
13144
+ ` ↳ ${symbol}${entry}${nameMatch}`,
13145
+ expression ? ` ${theme.fg("muted", expression)}` : undefined,
13146
+ params ? ` ${theme.fg("muted", `params: ${params}`)}` : undefined
13147
+ ].filter(Boolean).join(`
13148
+ `));
13149
+ });
13150
+ return sections2;
13151
+ }
13152
+ const hops = asRecords(record.hops);
13153
+ const sections = [
13154
+ joinNonEmpty([
13155
+ theme.fg("success", `${hops.length} hop${hops.length === 1 ? "" : "s"}`),
13156
+ asBoolean(record.depth_limited) ? theme.fg("warning", "(depth limited)") : undefined
13157
+ ])
13158
+ ];
13159
+ if (hops.length === 0)
13160
+ sections.push(theme.fg("muted", "No data-flow hops found."));
13161
+ hops.forEach((hop, index) => {
13162
+ const file = shortenPath(asString(hop.file) ?? "(unknown file)");
13163
+ const symbol = asString(hop.symbol) ?? "(unknown)";
13164
+ const variable = asString(hop.variable) ?? "(unknown)";
13165
+ const line = asNumber(hop.line) ?? 0;
13166
+ const approximate = hop.approximate === true ? ` ${theme.fg("warning", "[approx]")}` : "";
13167
+ const nameMatch = nameMatchEdgeMarker(hop, theme);
13168
+ sections.push(treeLine(index, `${variable} ${theme.fg("muted", `${asString(hop.flow_type) ?? "flow"}`)} ${symbol} [${file}:${line}]${approximate}${nameMatch}`));
13169
+ });
13170
+ return sections;
13171
+ }
12943
13172
  // ../aft-bridge/dist/coerce.js
12944
13173
  function coerceStringArray(value) {
12945
13174
  if (Array.isArray(value)) {
@@ -12990,7 +13219,7 @@ function isEmptyParam(value) {
12990
13219
  import { spawnSync } from "node:child_process";
12991
13220
  import { createHash, randomUUID } from "node:crypto";
12992
13221
  import { chmodSync, closeSync, copyFileSync, createWriteStream, existsSync, mkdirSync, openSync, readFileSync, renameSync, rmSync, statSync, unlinkSync, writeSync } from "node:fs";
12993
- import { homedir as homedir3 } from "node:os";
13222
+ import { homedir as homedir4 } from "node:os";
12994
13223
  import { join as join3 } from "node:path";
12995
13224
  import { Readable } from "node:stream";
12996
13225
  import { pipeline } from "node:stream/promises";
@@ -13048,10 +13277,10 @@ function isExpectedCachedBinary(binaryPath, tag) {
13048
13277
  function getCacheDir() {
13049
13278
  if (process.platform === "win32") {
13050
13279
  const localAppData = process.env.LOCALAPPDATA || process.env.APPDATA;
13051
- const base2 = localAppData || join3(homedir3(), "AppData", "Local");
13280
+ const base2 = localAppData || join3(homedir4(), "AppData", "Local");
13052
13281
  return join3(base2, "aft", "bin");
13053
13282
  }
13054
- const base = process.env.XDG_CACHE_HOME || join3(homedir3(), ".cache");
13283
+ const base = process.env.XDG_CACHE_HOME || join3(homedir4(), ".cache");
13055
13284
  return join3(base, "aft", "bin");
13056
13285
  }
13057
13286
  function getBinaryName() {
@@ -13293,7 +13522,7 @@ function formatEditSummary(data) {
13293
13522
  if (data.created === true) {
13294
13523
  let s2 = `Created file (${counts}).`;
13295
13524
  if (data.formatted)
13296
- s2 += " Auto-formatted.";
13525
+ s2 += formatAutoFormattedSuffix(data);
13297
13526
  return s2;
13298
13527
  }
13299
13528
  let detail = counts;
@@ -13304,9 +13533,21 @@ function formatEditSummary(data) {
13304
13533
  }
13305
13534
  let s = `Edited (${detail}).`;
13306
13535
  if (data.formatted)
13307
- s += " Auto-formatted.";
13536
+ s += formatAutoFormattedSuffix(data);
13308
13537
  return s;
13309
13538
  }
13539
+ function formatAutoFormattedSuffix(data) {
13540
+ const reflowText = data.reformatted?.text;
13541
+ if (typeof reflowText === "string" && reflowText.length > 0) {
13542
+ return `
13543
+ Auto-formatted — the formatter reflowed your edit. On disk now:
13544
+ ${reflowText}`;
13545
+ }
13546
+ if (data.reformatted?.extensive === true) {
13547
+ return " Auto-formatted — extensive reflow; re-read the file before your next anchored edit.";
13548
+ }
13549
+ return " Auto-formatted.";
13550
+ }
13310
13551
  // ../aft-bridge/dist/format.js
13311
13552
  function compressionSavingsPercent(original, compressed) {
13312
13553
  if (original <= 0)
@@ -13331,7 +13572,7 @@ function stripJsoncSymbols(value) {
13331
13572
  // ../aft-bridge/dist/migration.js
13332
13573
  import { spawnSync as spawnSync2 } from "node:child_process";
13333
13574
  import { existsSync as existsSync4, mkdirSync as mkdirSync4 } from "node:fs";
13334
- import { homedir as homedir5, tmpdir } from "node:os";
13575
+ import { homedir as homedir6, tmpdir } from "node:os";
13335
13576
  import { dirname as dirname2, join as join6 } from "node:path";
13336
13577
 
13337
13578
  // ../aft-bridge/dist/paths.js
@@ -13388,7 +13629,7 @@ function markAnnouncementSeen(storageRoot, harness, currentVersion) {
13388
13629
  import { execSync } from "node:child_process";
13389
13630
  import { chmodSync as chmodSync2, closeSync as closeSync2, copyFileSync as copyFileSync2, existsSync as existsSync3, mkdirSync as mkdirSync3, openSync as openSync2, readSync, renameSync as renameSync3, unlinkSync as unlinkSync2 } from "node:fs";
13390
13631
  import { createRequire as createRequire2 } from "node:module";
13391
- import { homedir as homedir4 } from "node:os";
13632
+ import { homedir as homedir5 } from "node:os";
13392
13633
  import { join as join5 } from "node:path";
13393
13634
  var ensureBinaryForResolver = ensureBinary;
13394
13635
  function copyToVersionedCache(npmBinaryPath, knownVersion) {
@@ -13430,7 +13671,7 @@ function normalizeBareVersion(version) {
13430
13671
  return version.startsWith("v") ? version.slice(1) : version;
13431
13672
  }
13432
13673
  function homeDirFromEnv(env) {
13433
- return (process.platform === "win32" ? env.USERPROFILE || env.HOME : env.HOME) || homedir4();
13674
+ return (process.platform === "win32" ? env.USERPROFILE || env.HOME : env.HOME) || homedir5();
13434
13675
  }
13435
13676
  function cacheDirFromEnv(env) {
13436
13677
  if (process.platform === "win32") {
@@ -13612,8 +13853,8 @@ function dataHome() {
13612
13853
  }
13613
13854
  function homeDir() {
13614
13855
  if (process.platform === "win32")
13615
- return process.env.USERPROFILE || process.env.HOME || homedir5();
13616
- return process.env.HOME || homedir5();
13856
+ return process.env.USERPROFILE || process.env.HOME || homedir6();
13857
+ return process.env.HOME || homedir6();
13617
13858
  }
13618
13859
  function resolveLegacyStorageRoot(harness) {
13619
13860
  if (harness === "pi")
@@ -13703,13 +13944,13 @@ async function ensureStorageMigrated(opts) {
13703
13944
  }
13704
13945
  // ../aft-bridge/dist/npm-resolver.js
13705
13946
  import { readdirSync, statSync as statSync2 } from "node:fs";
13706
- import { homedir as homedir6 } from "node:os";
13947
+ import { homedir as homedir7 } from "node:os";
13707
13948
  import { delimiter, dirname as dirname3, isAbsolute as isAbsolute2, join as join7 } from "node:path";
13708
13949
  function defaultDeps() {
13709
13950
  return {
13710
13951
  platform: process.platform,
13711
13952
  env: process.env,
13712
- home: homedir6(),
13953
+ home: homedir7(),
13713
13954
  execPath: process.execPath
13714
13955
  };
13715
13956
  }
@@ -15044,13 +15285,13 @@ function tokenizeStage(stage) {
15044
15285
  }
15045
15286
  // ../aft-bridge/dist/pool.js
15046
15287
  import { realpathSync as realpathSync2 } from "node:fs";
15047
- import { homedir as homedir7 } from "node:os";
15288
+ import { homedir as homedir8 } from "node:os";
15048
15289
  var DEFAULT_IDLE_TIMEOUT_MS = 30 * 60 * 1000;
15049
15290
  var DEFAULT_MAX_POOL_SIZE = 8;
15050
15291
  var CLEANUP_INTERVAL_MS = 60 * 1000;
15051
15292
  function canonicalHomeDir() {
15052
15293
  try {
15053
- const home = homedir7();
15294
+ const home = homedir8();
15054
15295
  if (!home)
15055
15296
  return null;
15056
15297
  try {
@@ -16073,7 +16314,7 @@ import {
16073
16314
  // package.json
16074
16315
  var package_default = {
16075
16316
  name: "@cortexkit/aft-pi",
16076
- version: "0.39.2",
16317
+ version: "0.39.4",
16077
16318
  type: "module",
16078
16319
  description: "Pi coding agent extension for Agent File Tools (AFT) — tree-sitter and LSP-powered code analysis",
16079
16320
  main: "dist/index.js",
@@ -16096,7 +16337,7 @@ var package_default = {
16096
16337
  "test:unit": "bun test src/__tests__ --path-ignore-patterns 'src/__tests__/e2e/**'"
16097
16338
  },
16098
16339
  dependencies: {
16099
- "@cortexkit/aft-bridge": "0.39.2",
16340
+ "@cortexkit/aft-bridge": "0.39.4",
16100
16341
  "@xterm/headless": "^5.5.0",
16101
16342
  "comment-json": "^5.0.0",
16102
16343
  diff: "^8.0.4",
@@ -16104,12 +16345,12 @@ var package_default = {
16104
16345
  zod: "^4.1.8"
16105
16346
  },
16106
16347
  optionalDependencies: {
16107
- "@cortexkit/aft-darwin-arm64": "0.39.2",
16108
- "@cortexkit/aft-darwin-x64": "0.39.2",
16109
- "@cortexkit/aft-linux-arm64": "0.39.2",
16110
- "@cortexkit/aft-linux-x64": "0.39.2",
16111
- "@cortexkit/aft-win32-arm64": "0.39.2",
16112
- "@cortexkit/aft-win32-x64": "0.39.2"
16348
+ "@cortexkit/aft-darwin-arm64": "0.39.4",
16349
+ "@cortexkit/aft-darwin-x64": "0.39.4",
16350
+ "@cortexkit/aft-linux-arm64": "0.39.4",
16351
+ "@cortexkit/aft-linux-x64": "0.39.4",
16352
+ "@cortexkit/aft-win32-arm64": "0.39.4",
16353
+ "@cortexkit/aft-win32-x64": "0.39.4"
16113
16354
  },
16114
16355
  devDependencies: {
16115
16356
  "@earendil-works/pi-coding-agent": "*",
@@ -16136,7 +16377,7 @@ var package_default = {
16136
16377
  };
16137
16378
 
16138
16379
  // src/shared/status.ts
16139
- function asRecord(value) {
16380
+ function asRecord2(value) {
16140
16381
  return typeof value === "object" && value !== null ? value : {};
16141
16382
  }
16142
16383
  function readString(value, fallback = "") {
@@ -16155,7 +16396,7 @@ function readOptionalNumber(value) {
16155
16396
  return typeof value === "number" && Number.isFinite(value) ? value : null;
16156
16397
  }
16157
16398
  function readCompressionAggregate(value) {
16158
- const aggregate = asRecord(value);
16399
+ const aggregate = asRecord2(value);
16159
16400
  return {
16160
16401
  events: readNumber(aggregate.events),
16161
16402
  original_tokens: readNumber(aggregate.original_tokens),
@@ -16166,7 +16407,7 @@ function readCompressionAggregate(value) {
16166
16407
  function readCompression(value) {
16167
16408
  if (typeof value !== "object" || value === null)
16168
16409
  return;
16169
- const compression = asRecord(value);
16410
+ const compression = asRecord2(value);
16170
16411
  return {
16171
16412
  project: readCompressionAggregate(compression.project),
16172
16413
  session: readCompressionAggregate(compression.session)
@@ -16175,7 +16416,7 @@ function readCompression(value) {
16175
16416
  function readStatusBar(value) {
16176
16417
  if (typeof value !== "object" || value === null)
16177
16418
  return;
16178
- const bar = asRecord(value);
16419
+ const bar = asRecord2(value);
16179
16420
  return {
16180
16421
  errors: readNumber(bar.errors),
16181
16422
  warnings: readNumber(bar.warnings),
@@ -16219,16 +16460,16 @@ function formatBytes(bytes) {
16219
16460
  return `${value.toFixed(decimals)} ${units[unitIndex]}`;
16220
16461
  }
16221
16462
  function coerceAftStatus(response) {
16222
- const features = asRecord(response.features);
16223
- const searchIndex = asRecord(response.search_index);
16224
- const semanticIndex = asRecord(response.semantic_index);
16463
+ const features = asRecord2(response.features);
16464
+ const searchIndex = asRecord2(response.search_index);
16465
+ const semanticIndex = asRecord2(response.semantic_index);
16225
16466
  const semanticConfig = {
16226
- ...asRecord(response.semantic),
16227
- ...asRecord(response.semantic_config)
16467
+ ...asRecord2(response.semantic),
16468
+ ...asRecord2(response.semantic_config)
16228
16469
  };
16229
- const disk = asRecord(response.disk);
16230
- const symbolCache = asRecord(response.symbol_cache);
16231
- const session = asRecord(response.session);
16470
+ const disk = asRecord2(response.disk);
16471
+ const symbolCache = asRecord2(response.symbol_cache);
16472
+ const session = asRecord2(response.session);
16232
16473
  return {
16233
16474
  version: readString(response.version, "unknown"),
16234
16475
  project_root: readNullableString(response.project_root),
@@ -16684,7 +16925,7 @@ function registerStatusCommand(pi, ctx) {
16684
16925
 
16685
16926
  // src/config.ts
16686
16927
  import { existsSync as existsSync6, readFileSync as readFileSync4, renameSync as renameSync4, unlinkSync as unlinkSync4, writeFileSync as writeFileSync3 } from "node:fs";
16687
- import { homedir as homedir8 } from "node:os";
16928
+ import { homedir as homedir9 } from "node:os";
16688
16929
  import { join as join10 } from "node:path";
16689
16930
  var import_comment_json = __toESM(require_src2(), 1);
16690
16931
 
@@ -30381,6 +30622,8 @@ var AftConfigSchema = exports_external.object({
30381
30622
  restrict_to_project_root: exports_external.boolean().optional(),
30382
30623
  search_index: exports_external.boolean().optional(),
30383
30624
  semantic_search: exports_external.boolean().optional(),
30625
+ callgraph_store: exports_external.boolean().optional(),
30626
+ callgraph_chunk_size: exports_external.number().optional(),
30384
30627
  inspect: InspectConfigSchema.optional(),
30385
30628
  bash: BashConfigSchema.optional(),
30386
30629
  experimental: ExperimentalConfigSchema.optional(),
@@ -30441,6 +30684,37 @@ function resolveLspConfigForConfigure(config2) {
30441
30684
  }
30442
30685
  return overrides;
30443
30686
  }
30687
+ function resolveProjectOverridesForConfigure(config2) {
30688
+ const overrides = {};
30689
+ if (config2.format_on_edit !== undefined)
30690
+ overrides.format_on_edit = config2.format_on_edit;
30691
+ if (config2.formatter_timeout_secs !== undefined)
30692
+ overrides.formatter_timeout_secs = config2.formatter_timeout_secs;
30693
+ if (config2.validate_on_edit !== undefined)
30694
+ overrides.validate_on_edit = config2.validate_on_edit;
30695
+ if (config2.formatter !== undefined)
30696
+ overrides.formatter = config2.formatter;
30697
+ if (config2.checker !== undefined)
30698
+ overrides.checker = config2.checker;
30699
+ overrides.restrict_to_project_root = config2.restrict_to_project_root ?? false;
30700
+ if (config2.search_index !== undefined)
30701
+ overrides.search_index = config2.search_index;
30702
+ if (config2.semantic_search !== undefined)
30703
+ overrides.semantic_search = config2.semantic_search;
30704
+ if (config2.callgraph_store !== undefined)
30705
+ overrides.callgraph_store = config2.callgraph_store;
30706
+ if (config2.callgraph_chunk_size !== undefined)
30707
+ overrides.callgraph_chunk_size = config2.callgraph_chunk_size;
30708
+ Object.assign(overrides, resolveExperimentalConfigForConfigure(config2));
30709
+ Object.assign(overrides, resolveLspConfigForConfigure(config2));
30710
+ if (config2.semantic !== undefined)
30711
+ overrides.semantic = config2.semantic;
30712
+ if (config2.inspect !== undefined)
30713
+ overrides.inspect = config2.inspect;
30714
+ if (config2.max_callgraph_files !== undefined)
30715
+ overrides.max_callgraph_files = config2.max_callgraph_files;
30716
+ return overrides;
30717
+ }
30444
30718
  function resolveExperimentalConfigForConfigure(config2) {
30445
30719
  const overrides = {};
30446
30720
  const bash = resolveBashConfig(config2);
@@ -30617,6 +30891,17 @@ function detectConfigFile(basePath) {
30617
30891
  return { format: "json", path: jsonPath };
30618
30892
  return { format: "none", path: jsonPath };
30619
30893
  }
30894
+ var configLoadErrors = [];
30895
+ function getConfigLoadErrors() {
30896
+ return configLoadErrors;
30897
+ }
30898
+ function formatConfigParseFailureMessage(configPath, errorMessage) {
30899
+ return `AFT config at ${configPath} failed to parse and was ignored (running on defaults): ${errorMessage}. ` + "Fix the syntax or run `npx @cortexkit/aft doctor`.";
30900
+ }
30901
+ function recordConfigParseFailure(configPath, errorMessage) {
30902
+ configLoadErrors.push({ path: configPath, message: errorMessage });
30903
+ warn2(formatConfigParseFailureMessage(configPath, errorMessage));
30904
+ }
30620
30905
  function loadConfigFromPath(configPath) {
30621
30906
  try {
30622
30907
  if (!existsSync6(configPath))
@@ -30624,7 +30909,7 @@ function loadConfigFromPath(configPath) {
30624
30909
  const content = readFileSync4(configPath, "utf-8");
30625
30910
  const rawConfig = import_comment_json.parse(content);
30626
30911
  if (!rawConfig || typeof rawConfig !== "object" || Array.isArray(rawConfig)) {
30627
- warn2(`Config validation error in ${configPath}: root must be an object`);
30912
+ recordConfigParseFailure(configPath, "root must be an object");
30628
30913
  return null;
30629
30914
  }
30630
30915
  migrateRawConfig(rawConfig, configPath, { log: log2, warn: warn2 });
@@ -30640,6 +30925,7 @@ function loadConfigFromPath(configPath) {
30640
30925
  } catch (err) {
30641
30926
  const errorMsg = err instanceof Error ? err.message : String(err);
30642
30927
  error2(`Error loading config from ${configPath}: ${errorMsg}`);
30928
+ recordConfigParseFailure(configPath, errorMsg);
30643
30929
  return null;
30644
30930
  }
30645
30931
  }
@@ -30775,6 +31061,8 @@ var PROJECT_SAFE_TOP_LEVEL_FIELDS = new Set([
30775
31061
  "configure_warnings_delivery",
30776
31062
  "search_index",
30777
31063
  "semantic_search",
31064
+ "callgraph_store",
31065
+ "callgraph_chunk_size",
30778
31066
  "inspect",
30779
31067
  "experimental",
30780
31068
  "bash"
@@ -30836,9 +31124,10 @@ function resolveBridgePoolTransportOptions(config2) {
30836
31124
  };
30837
31125
  }
30838
31126
  function getGlobalPiDir() {
30839
- return join10(homedir8(), ".pi", "agent");
31127
+ return join10(homedir9(), ".pi", "agent");
30840
31128
  }
30841
31129
  function loadAftConfig(projectDirectory) {
31130
+ configLoadErrors = [];
30842
31131
  const userBasePath = join10(getGlobalPiDir(), "aft");
30843
31132
  migrateAftConfigFile(`${userBasePath}.jsonc`);
30844
31133
  migrateAftConfigFile(`${userBasePath}.json`);
@@ -30893,7 +31182,7 @@ import {
30893
31182
  unlinkSync as unlinkSync5,
30894
31183
  writeFileSync as writeFileSync4
30895
31184
  } from "node:fs";
30896
- import { homedir as homedir9 } from "node:os";
31185
+ import { homedir as homedir10 } from "node:os";
30897
31186
  import { join as join11 } from "node:path";
30898
31187
  function aftCacheBase() {
30899
31188
  const override = process.env.AFT_CACHE_DIR;
@@ -30901,10 +31190,10 @@ function aftCacheBase() {
30901
31190
  return override;
30902
31191
  if (process.platform === "win32") {
30903
31192
  const localAppData = process.env.LOCALAPPDATA || process.env.APPDATA;
30904
- const base2 = localAppData || join11(homedir9(), "AppData", "Local");
31193
+ const base2 = localAppData || join11(homedir10(), "AppData", "Local");
30905
31194
  return join11(base2, "aft");
30906
31195
  }
30907
- const base = process.env.XDG_CACHE_HOME || join11(homedir9(), ".cache");
31196
+ const base = process.env.XDG_CACHE_HOME || join11(homedir10(), ".cache");
30908
31197
  return join11(base, "aft");
30909
31198
  }
30910
31199
  function lspCacheRoot() {
@@ -32617,6 +32906,8 @@ function warningTitle(warning) {
32617
32906
  return "Checker is not installed";
32618
32907
  case "lsp_binary_missing":
32619
32908
  return "LSP binary is missing";
32909
+ case "config_parse_failed":
32910
+ return "Config failed to parse";
32620
32911
  }
32621
32912
  }
32622
32913
  function formatConfigureWarning(warning) {
@@ -32862,7 +33153,7 @@ import { Type as Type3 } from "typebox";
32862
33153
 
32863
33154
  // src/tools/hoisted.ts
32864
33155
  import { stat } from "node:fs/promises";
32865
- import { homedir as homedir10 } from "node:os";
33156
+ import { homedir as homedir11 } from "node:os";
32866
33157
  import { isAbsolute as isAbsolute4, relative as relative4, resolve as resolve4, sep } from "node:path";
32867
33158
  import {
32868
33159
  renderDiff
@@ -32984,9 +33275,9 @@ function expandTilde2(path3) {
32984
33275
  if (!path3 || !path3.startsWith("~"))
32985
33276
  return path3;
32986
33277
  if (path3 === "~")
32987
- return homedir10();
33278
+ return homedir11();
32988
33279
  if (path3.startsWith(`~${sep}`) || path3.startsWith("~/")) {
32989
- return resolve4(homedir10(), path3.slice(2));
33280
+ return resolve4(homedir11(), path3.slice(2));
32990
33281
  }
32991
33282
  return path3;
32992
33283
  }
@@ -33044,14 +33335,7 @@ function appendSkippedSearchPaths(text, missing) {
33044
33335
 
33045
33336
  ${note}` : note;
33046
33337
  }
33047
- function externalDirectoryPromptTimeoutMs() {
33048
- const raw = process.env.AFT_PI_EXTERNAL_PROMPT_TIMEOUT_MS;
33049
- if (raw === undefined)
33050
- return 30000;
33051
- const parsed = Number.parseInt(raw, 10);
33052
- return Number.isFinite(parsed) && parsed > 0 ? parsed : 30000;
33053
- }
33054
- async function assertExternalDirectoryPermission(extCtx, target, action = "modify", options = {}) {
33338
+ async function assertExternalDirectoryPermission(extCtx, target, options = {}) {
33055
33339
  if (!target)
33056
33340
  return;
33057
33341
  const expanded = expandTilde2(target);
@@ -33060,30 +33344,7 @@ async function assertExternalDirectoryPermission(extCtx, target, action = "modif
33060
33344
  return;
33061
33345
  if (options.restrictToProjectRoot === false)
33062
33346
  return;
33063
- const confirmFn = extCtx.ui?.confirm;
33064
- if (extCtx.hasUI === false || !confirmFn) {
33065
- throw new Error(`Permission denied: cannot prompt for ${action} outside the project (${absoluteTarget}).`);
33066
- }
33067
- const timeoutMs = externalDirectoryPromptTimeoutMs();
33068
- let timer;
33069
- const timeoutPromise = new Promise((resolve5) => {
33070
- timer = setTimeout(() => resolve5("timeout"), timeoutMs);
33071
- });
33072
- try {
33073
- const result = await Promise.race([
33074
- confirmFn("Allow external directory access?", `AFT wants to ${action} outside the project: ${absoluteTarget}`),
33075
- timeoutPromise
33076
- ]);
33077
- if (result === true)
33078
- return;
33079
- if (result === "timeout") {
33080
- throw new Error(`Permission denied: external directory prompt timed out after ${timeoutMs}ms.`);
33081
- }
33082
- throw new Error("Permission denied: external directory access was cancelled.");
33083
- } finally {
33084
- if (timer !== undefined)
33085
- clearTimeout(timer);
33086
- }
33347
+ throw new Error(`Blocked: '${absoluteTarget}' is outside the project root and restrict_to_project_root is ` + "enabled (AFT full isolation). Not overridable per-call; set restrict_to_project_root: false " + "in aft.jsonc to allow external paths.");
33087
33348
  }
33088
33349
  var ReadParams = Type2.Object({
33089
33350
  path: Type2.String({
@@ -33132,7 +33393,7 @@ function registerHoistedTools(pi, ctx, surface) {
33132
33393
  const offset = coerceOptionalInt(params.offset, "offset", 1, Number.MAX_SAFE_INTEGER);
33133
33394
  const limit = coerceOptionalInt(params.limit, "limit", 1, Number.MAX_SAFE_INTEGER);
33134
33395
  const filePath = await resolvePathArg(extCtx.cwd, params.path);
33135
- await assertExternalDirectoryPermission(extCtx, filePath, "read", {
33396
+ await assertExternalDirectoryPermission(extCtx, filePath, {
33136
33397
  restrictToProjectRoot: surface.restrictToProjectRoot
33137
33398
  });
33138
33399
  const req = {
@@ -33170,7 +33431,7 @@ function registerHoistedTools(pi, ctx, surface) {
33170
33431
  parameters: WriteParams,
33171
33432
  async execute(_toolCallId, params, _signal, _onUpdate, extCtx) {
33172
33433
  const filePath = await resolvePathArg(extCtx.cwd, params.filePath);
33173
- await assertExternalDirectoryPermission(extCtx, filePath, "modify", {
33434
+ await assertExternalDirectoryPermission(extCtx, filePath, {
33174
33435
  restrictToProjectRoot: surface.restrictToProjectRoot
33175
33436
  });
33176
33437
  const bridge = bridgeFor(ctx, extCtx.cwd);
@@ -33204,7 +33465,7 @@ function registerHoistedTools(pi, ctx, surface) {
33204
33465
  parameters: EditParams,
33205
33466
  async execute(_toolCallId, params, _signal, _onUpdate, extCtx) {
33206
33467
  const filePath = await resolvePathArg(extCtx.cwd, params.filePath);
33207
- await assertExternalDirectoryPermission(extCtx, filePath, "modify", {
33468
+ await assertExternalDirectoryPermission(extCtx, filePath, {
33208
33469
  restrictToProjectRoot: surface.restrictToProjectRoot
33209
33470
  });
33210
33471
  const bridge = bridgeFor(ctx, extCtx.cwd);
@@ -33257,7 +33518,7 @@ function registerHoistedTools(pi, ctx, surface) {
33257
33518
  if (params.path) {
33258
33519
  pathSplit = await splitSearchPathArg(extCtx.cwd, params.path);
33259
33520
  for (const target of pathSplit.paths) {
33260
- await assertExternalDirectoryPermission(extCtx, absoluteSearchPath(extCtx.cwd, target), "search", {
33521
+ await assertExternalDirectoryPermission(extCtx, absoluteSearchPath(extCtx.cwd, target), {
33261
33522
  restrictToProjectRoot: surface.restrictToProjectRoot
33262
33523
  });
33263
33524
  }
@@ -33380,7 +33641,7 @@ function reuseContainer(last) {
33380
33641
  }
33381
33642
  function renderMutationCall(toolName, filePath, theme, context) {
33382
33643
  const text = reuseText(context.lastComponent);
33383
- const pathDisplay = filePath ? theme.fg("accent", shortenPath(filePath)) : theme.fg("toolOutput", "...");
33644
+ const pathDisplay = filePath ? theme.fg("accent", shortenPath2(filePath)) : theme.fg("toolOutput", "...");
33384
33645
  text.setText(`${theme.fg("toolTitle", theme.bold(toolName))} ${pathDisplay}`);
33385
33646
  return text;
33386
33647
  }
@@ -33416,8 +33677,8 @@ ${summary}${suffix}`);
33416
33677
  container.addChild(new Text(renderDiff(diff), 1, 0));
33417
33678
  return container;
33418
33679
  }
33419
- function shortenPath(path3) {
33420
- const home = homedir10();
33680
+ function shortenPath2(path3) {
33681
+ const home = homedir11();
33421
33682
  if (path3.startsWith(home))
33422
33683
  return `~${path3.slice(home.length)}`;
33423
33684
  return path3;
@@ -33467,7 +33728,7 @@ function formatReadFooter2(agentSpecifiedRange, data) {
33467
33728
  }
33468
33729
 
33469
33730
  // src/tools/render-helpers.ts
33470
- import { homedir as homedir11 } from "node:os";
33731
+ import { homedir as homedir12 } from "node:os";
33471
33732
  import { renderDiff as renderDiff2 } from "@earendil-works/pi-coding-agent";
33472
33733
  import { Container as Container2, Spacer as Spacer2, Text as Text2 } from "@earendil-works/pi-tui";
33473
33734
  function reuseText2(last) {
@@ -33476,8 +33737,8 @@ function reuseText2(last) {
33476
33737
  function reuseContainer2(last) {
33477
33738
  return last instanceof Container2 ? last : new Container2;
33478
33739
  }
33479
- function shortenPath2(path3) {
33480
- const home = homedir11();
33740
+ function shortenPath3(path3) {
33741
+ const home = homedir12();
33481
33742
  if (path3.startsWith(home))
33482
33743
  return `~${path3.slice(home.length)}`;
33483
33744
  return path3;
@@ -33491,7 +33752,7 @@ function renderToolCall(toolName, summary, theme, context) {
33491
33752
  function accentPath(theme, path3) {
33492
33753
  if (!path3)
33493
33754
  return theme.fg("toolOutput", "...");
33494
- return theme.fg("accent", shortenPath2(path3));
33755
+ return theme.fg("accent", shortenPath3(path3));
33495
33756
  }
33496
33757
  function collectTextContent(result) {
33497
33758
  return result.content.filter((part) => part.type === "text").map((part) => part.text ?? "").join(`
@@ -33530,23 +33791,20 @@ function renderSections(sections, context) {
33530
33791
  });
33531
33792
  return container;
33532
33793
  }
33533
- function asRecord2(value) {
33794
+ function asRecord3(value) {
33534
33795
  if (!value || typeof value !== "object" || Array.isArray(value))
33535
33796
  return;
33536
33797
  return value;
33537
33798
  }
33538
- function asRecords(value) {
33539
- return Array.isArray(value) ? value.map(asRecord2).filter(Boolean) : [];
33799
+ function asRecords2(value) {
33800
+ return Array.isArray(value) ? value.map(asRecord3).filter(Boolean) : [];
33540
33801
  }
33541
- function asString(value) {
33802
+ function asString2(value) {
33542
33803
  return typeof value === "string" ? value : undefined;
33543
33804
  }
33544
- function asNumber(value) {
33805
+ function asNumber2(value) {
33545
33806
  return typeof value === "number" && Number.isFinite(value) ? value : undefined;
33546
33807
  }
33547
- function asBoolean(value) {
33548
- return typeof value === "boolean" ? value : undefined;
33549
- }
33550
33808
  function formatValue(value) {
33551
33809
  if (Array.isArray(value))
33552
33810
  return value.map(formatValue).join(", ");
@@ -33659,7 +33917,7 @@ var ReplaceParams = Type3.Object({
33659
33917
  dryRun: Type3.Optional(Type3.Boolean({ description: "Preview without applying (default: false)" }))
33660
33918
  });
33661
33919
  function appendHintSection(response, sections, theme) {
33662
- const hint = asString(response.hint);
33920
+ const hint = asString2(response.hint);
33663
33921
  if (hint && hint.length > 0) {
33664
33922
  sections.push(theme.fg("warning", hint));
33665
33923
  }
@@ -33668,8 +33926,8 @@ function appendScopeSections(response, sections, theme) {
33668
33926
  if (response.no_files_matched_scope === true) {
33669
33927
  sections.push(theme.fg("warning", "No files matched the scope (paths/globs resolved to zero files)"));
33670
33928
  }
33671
- const warnings = asRecords(response.scope_warnings);
33672
- const warningStrings = Array.isArray(response.scope_warnings) ? response.scope_warnings.filter((w) => typeof w === "string") : warnings.map((w) => asString(w.warning) ?? "").filter(Boolean);
33929
+ const warnings = asRecords2(response.scope_warnings);
33930
+ const warningStrings = Array.isArray(response.scope_warnings) ? response.scope_warnings.filter((w) => typeof w === "string") : warnings.map((w) => asString2(w.warning) ?? "").filter(Boolean);
33673
33931
  if (warningStrings.length > 0) {
33674
33932
  sections.push(`${theme.fg("muted", "Scope warnings:")}
33675
33933
  ${warningStrings.map((w) => ` ${w}`).join(`
@@ -33681,7 +33939,7 @@ async function resolveAstPaths(extCtx, paths) {
33681
33939
  return;
33682
33940
  return Promise.all(paths.filter((path3) => typeof path3 === "string").map((path3) => resolvePathArg(extCtx.cwd, path3)));
33683
33941
  }
33684
- async function assertAstPathsPermission(extCtx, paths, action, restrictToProjectRoot) {
33942
+ async function assertAstPathsPermission(extCtx, paths, restrictToProjectRoot) {
33685
33943
  if (paths === undefined || paths.length === 0)
33686
33944
  return;
33687
33945
  const checked = new Set;
@@ -33689,17 +33947,17 @@ async function assertAstPathsPermission(extCtx, paths, action, restrictToProject
33689
33947
  if (checked.has(path3))
33690
33948
  continue;
33691
33949
  checked.add(path3);
33692
- await assertExternalDirectoryPermission(extCtx, path3, action, { restrictToProjectRoot });
33950
+ await assertExternalDirectoryPermission(extCtx, path3, { restrictToProjectRoot });
33693
33951
  }
33694
33952
  }
33695
33953
  function buildAstSearchSections(payload, theme) {
33696
- const response = asRecord2(payload);
33954
+ const response = asRecord3(payload);
33697
33955
  if (!response)
33698
33956
  return [theme.fg("muted", "No AST search results.")];
33699
- const matches = asRecords(response.matches);
33700
- const totalMatches = asNumber(response.total_matches) ?? matches.length;
33701
- const filesWithMatches = asNumber(response.files_with_matches) ?? groupByFile(matches, (match) => asString(match.file)).size;
33702
- const filesSearched = asNumber(response.files_searched);
33957
+ const matches = asRecords2(response.matches);
33958
+ const totalMatches = asNumber2(response.total_matches) ?? matches.length;
33959
+ const filesWithMatches = asNumber2(response.files_with_matches) ?? groupByFile(matches, (match) => asString2(match.file)).size;
33960
+ const filesSearched = asNumber2(response.files_searched);
33703
33961
  const header = [
33704
33962
  theme.fg("success", `${totalMatches} match${totalMatches === 1 ? "" : "es"}`),
33705
33963
  theme.fg("accent", `${filesWithMatches} file${filesWithMatches === 1 ? "" : "s"}`),
@@ -33711,26 +33969,26 @@ function buildAstSearchSections(payload, theme) {
33711
33969
  appendHintSection(response, sections2, theme);
33712
33970
  return sections2;
33713
33971
  }
33714
- const grouped = groupByFile(matches, (match) => asString(match.file));
33972
+ const grouped = groupByFile(matches, (match) => asString2(match.file));
33715
33973
  const sections = [header];
33716
33974
  for (const [file2, fileMatches] of grouped.entries()) {
33717
- const lines = [theme.fg("accent", shortenPath2(file2))];
33975
+ const lines = [theme.fg("accent", shortenPath3(file2))];
33718
33976
  fileMatches.forEach((match, index) => {
33719
- const line = asNumber(match.line) ?? 0;
33720
- const column = asNumber(match.column) ?? 0;
33721
- const snippet = asString(match.text)?.trim() || "(empty match)";
33977
+ const line = asNumber2(match.line) ?? 0;
33978
+ const column = asNumber2(match.column) ?? 0;
33979
+ const snippet = asString2(match.text)?.trim() || "(empty match)";
33722
33980
  lines.push(` ${index + 1}. ${theme.fg("muted", `${line}:${column}`)} ${snippet}`);
33723
- const metaVars = asRecord2(match.meta_variables);
33981
+ const metaVars = asRecord3(match.meta_variables);
33724
33982
  if (metaVars && Object.keys(metaVars).length > 0) {
33725
33983
  Object.entries(metaVars).forEach(([name, value]) => {
33726
33984
  lines.push(` ${theme.fg("muted", `${name} =`)} ${formatValue(value)}`);
33727
33985
  });
33728
33986
  }
33729
- const context = asRecords(match.context);
33987
+ const context = asRecords2(match.context);
33730
33988
  context.forEach((ctxLine) => {
33731
- const ctxNumber = asNumber(ctxLine.line) ?? 0;
33989
+ const ctxNumber = asNumber2(ctxLine.line) ?? 0;
33732
33990
  const prefix = ctxLine.is_match === true ? theme.fg("accent", ">") : theme.fg("muted", "|");
33733
- lines.push(` ${prefix} ${ctxNumber}: ${asString(ctxLine.text) ?? ""}`);
33991
+ lines.push(` ${prefix} ${ctxNumber}: ${asString2(ctxLine.text) ?? ""}`);
33734
33992
  });
33735
33993
  });
33736
33994
  sections.push(lines.join(`
@@ -33739,13 +33997,13 @@ function buildAstSearchSections(payload, theme) {
33739
33997
  return sections;
33740
33998
  }
33741
33999
  function buildAstReplaceSections(payload, theme) {
33742
- const response = asRecord2(payload);
34000
+ const response = asRecord3(payload);
33743
34001
  if (!response)
33744
34002
  return [theme.fg("muted", "No AST replace results.")];
33745
- const files = asRecords(response.files);
33746
- const totalReplacements = asNumber(response.total_replacements) ?? 0;
33747
- const totalFiles = asNumber(response.total_files) ?? files.length;
33748
- const filesWithMatches = asNumber(response.files_with_matches);
34003
+ const files = asRecords2(response.files);
34004
+ const totalReplacements = asNumber2(response.total_replacements) ?? 0;
34005
+ const totalFiles = asNumber2(response.total_files) ?? files.length;
34006
+ const filesWithMatches = asNumber2(response.files_with_matches);
33749
34007
  const dryRun = response.dry_run === true;
33750
34008
  const headerParts = [
33751
34009
  dryRun ? theme.fg("warning", "[dry run]") : theme.fg("success", "[applied]"),
@@ -33761,10 +34019,10 @@ function buildAstReplaceSections(payload, theme) {
33761
34019
  return sections;
33762
34020
  }
33763
34021
  files.forEach((fileResult) => {
33764
- const file2 = shortenPath2(asString(fileResult.file) ?? "(unknown file)");
33765
- const replacements = asNumber(fileResult.replacements) ?? 0;
33766
- const error50 = asString(fileResult.error);
33767
- const diff = asString(fileResult.diff);
34022
+ const file2 = shortenPath3(asString2(fileResult.file) ?? "(unknown file)");
34023
+ const replacements = asNumber2(fileResult.replacements) ?? 0;
34024
+ const error50 = asString2(fileResult.error);
34025
+ const diff = asString2(fileResult.diff);
33768
34026
  const lines = [
33769
34027
  `${theme.fg("accent", file2)} ${theme.fg("muted", `(${replacements} replacement${replacements === 1 ? "" : "s"})`)}`
33770
34028
  ];
@@ -33774,7 +34032,7 @@ function buildAstReplaceSections(payload, theme) {
33774
34032
  const rendered = renderUnifiedDiff(diff);
33775
34033
  lines.push(rendered || theme.fg("muted", "No diff available."));
33776
34034
  } else {
33777
- const backupId = asString(fileResult.backup_id);
34035
+ const backupId = asString2(fileResult.backup_id);
33778
34036
  lines.push(backupId ? `${theme.fg("success", "saved")} ${theme.fg("muted", backupId)}` : theme.fg("success", "saved"));
33779
34037
  }
33780
34038
  sections.push(lines.join(`
@@ -33808,7 +34066,7 @@ function registerAstTools(pi, ctx, surface) {
33808
34066
  parameters: SearchParams,
33809
34067
  async execute(_toolCallId, params, _signal, _onUpdate, extCtx) {
33810
34068
  const paths = await resolveAstPaths(extCtx, params.paths);
33811
- await assertAstPathsPermission(extCtx, paths, "search", ctx.config.restrict_to_project_root ?? false);
34069
+ await assertAstPathsPermission(extCtx, paths, ctx.config.restrict_to_project_root ?? false);
33812
34070
  const bridge = bridgeFor(ctx, extCtx.cwd);
33813
34071
  const req = {
33814
34072
  pattern: params.pattern,
@@ -33839,7 +34097,7 @@ function registerAstTools(pi, ctx, surface) {
33839
34097
  parameters: ReplaceParams,
33840
34098
  async execute(_toolCallId, params, _signal, _onUpdate, extCtx) {
33841
34099
  const paths = await resolveAstPaths(extCtx, params.paths);
33842
- await assertAstPathsPermission(extCtx, paths, params.dryRun === true ? "search" : "modify", ctx.config.restrict_to_project_root ?? false);
34100
+ await assertAstPathsPermission(extCtx, paths, ctx.config.restrict_to_project_root ?? false);
33843
34101
  const bridge = bridgeFor(ctx, extCtx.cwd);
33844
34102
  const req = {
33845
34103
  pattern: params.pattern,
@@ -33871,7 +34129,7 @@ import { Type as Type4 } from "typebox";
33871
34129
  var FOREGROUND_POLL_INTERVAL_MS = 100;
33872
34130
  var BASH_WAIT_POLL_INTERVAL_MS = 100;
33873
34131
  var DEFAULT_BASH_STATUS_WAIT_TIMEOUT_MS = 30000;
33874
- var MAX_BASH_STATUS_WAIT_TIMEOUT_MS = 300000;
34132
+ var MAX_BASH_STATUS_WAIT_TIMEOUT_MS = 30 * 60 * 1000;
33875
34133
  var REGEX_WAIT_SCAN_WINDOW_BYTES = 64 * 1024;
33876
34134
  function resolveForegroundWaitMs(configured) {
33877
34135
  const override = process.env.AFT_TEST_FOREGROUND_WAIT_MS;
@@ -33883,7 +34141,8 @@ function resolveForegroundWaitMs(configured) {
33883
34141
  return configured;
33884
34142
  }
33885
34143
  var BASH_TRANSPORT_TIMEOUT_MS = 30000;
33886
- var BashParams = Type4.Object({
34144
+ var DEFAULT_HARD_TIMEOUT_MS = 30 * 60 * 1000;
34145
+ var BashBaseParams = {
33887
34146
  command: Type4.String({
33888
34147
  description: "Shell command to execute. Supports pipes, redirections, and shell syntax."
33889
34148
  }),
@@ -33893,19 +34152,38 @@ var BashParams = Type4.Object({
33893
34152
  })),
33894
34153
  description: Type4.Optional(Type4.String({
33895
34154
  description: "Human-readable description shown in UI logs. Helps users understand what the command does without reading shell syntax."
33896
- })),
34155
+ }))
34156
+ };
34157
+ var BashBackgroundFlagParam = {
33897
34158
  background: Type4.Optional(Type4.Boolean({
33898
- description: "Spawn command in background and return immediately with a task_id. Use bash_status to poll completion and bash_kill to terminate. Ideal for long-running tasks like builds or dev servers."
33899
- })),
34159
+ description: "Spawn command in background and return immediately with a task_id. Use bash_watch to wait for completion or output patterns; bash_status for a one-shot snapshot only. Use bash_kill to terminate. Ideal for long-running tasks like builds or dev servers."
34160
+ }))
34161
+ };
34162
+ var BashCompressionParam = {
33900
34163
  compressed: Type4.Optional(Type4.Boolean({
33901
34164
  description: "Compress output by removing ANSI codes, carriage returns, and excessive blank lines. Default: true. Set to false for raw terminal output including color codes."
33902
- })),
34165
+ }))
34166
+ };
34167
+ var BashPtyParams = {
33903
34168
  pty: Type4.Optional(Type4.Boolean({
33904
34169
  description: 'Spawn the command in a real PTY for interactive programs. Implies background: true automatically. Inspect with bash_status({ task_id, output_mode: "screen" }) and send input with bash_write.'
33905
34170
  })),
33906
34171
  ptyRows: optionalInt(1, 60),
33907
34172
  ptyCols: optionalInt(1, 140)
34173
+ };
34174
+ var BashParams = Type4.Object({
34175
+ ...BashBaseParams,
34176
+ ...BashBackgroundFlagParam,
34177
+ ...BashCompressionParam,
34178
+ ...BashPtyParams
34179
+ });
34180
+ var BashForegroundOnlyParams = Type4.Object({
34181
+ ...BashBaseParams,
34182
+ ...BashCompressionParam
33908
34183
  });
34184
+ function bashParamsForConfig(backgroundEnabled) {
34185
+ return backgroundEnabled ? BashParams : BashForegroundOnlyParams;
34186
+ }
33909
34187
  var BashTaskParams = Type4.Object({
33910
34188
  task_id: Type4.String({
33911
34189
  description: "Background bash task id returned by bash({ background: true })."
@@ -33946,6 +34224,9 @@ var BashWriteParams = Type4.Object({
33946
34224
  description: "Either a string of verbatim bytes (e.g. 'print(1)\\n') OR an array mixing strings " + "and { key: '<name>' } objects for atomic text+key sequences. " + "Example: [ 'iHello', { key: 'esc' }, ':wq', { key: 'enter' } ]. " + "Allowed key names: enter, return, tab, space, backspace, esc, escape, up, down, " + "left, right, home, end, page-up, page-down, delete, insert, f1..f12, ctrl-a..ctrl-z."
33947
34225
  })
33948
34226
  });
34227
+ function unavailableSnapshot() {
34228
+ return { status: "unknown" };
34229
+ }
33949
34230
  async function callBashBridge(bridge, command, params = {}, extCtx, options) {
33950
34231
  return await callBridge(bridge, command, params, extCtx, {
33951
34232
  transportTimeoutMs: BASH_TRANSPORT_TIMEOUT_MS,
@@ -33979,28 +34260,30 @@ function registerBashTool(pi, ctx, aftSearchRegistered = false) {
33979
34260
  const searchSteer = aftSearchRegistered ? "use `aft_search` (concepts, identifiers, regex, literals), `read`, `aft_outline`, or `aft_zoom` instead" : "use the `grep` tool, `read`, `aft_outline`, or `aft_zoom` instead";
33980
34261
  const bashCfg = resolveBashConfig(ctx.config);
33981
34262
  const compressionSentence = bashCfg.compress ? " Output is compressed by default; pass `compressed: false` for raw output." : "";
33982
- const tasksSentence = bashCfg.background ? ' Pass `background: true` to run in the background and get a task_id for `bash_status`/`bash_kill`. Pass `pty: true` for interactive programs (REPLs, TUIs) and drive them with `bash_status({ output_mode: "screen" })` plus `bash_write`.' : " Commands that outlive the foreground wait window are promoted to background tasks; inspect them with `bash_status({ task_id })` or terminate with `bash_kill`.";
34263
+ const tasksSentence = bashCfg.background ? ' Pass `background: true` to run in the background and get a task_id for `bash_status`/`bash_watch`/`bash_kill`. Pass `pty: true` for interactive programs (REPLs, TUIs) and drive them with `bash_status({ output_mode: "screen" })` plus `bash_write`. Use `bash_watch` to wait for exit or output patterns (sync blocks, async notifies). Do not loop `bash_status` to wait.' : " Commands run in the foreground to completion; `timeout` is the hard kill cap (default 30 minutes).";
33983
34264
  pi.registerTool({
33984
34265
  name: "bash",
33985
34266
  label: "bash",
33986
34267
  description: `Execute shell commands.${compressionSentence}${tasksSentence}
33987
34268
 
33988
34269
  DO NOT use bash for code search or code exploration. If you are about to run grep, rg, sed, awk, find, or cat through bash to locate or read code: STOP — ${searchSteer}.`,
33989
- promptSnippet: "Run shell commands (timeout in milliseconds; supports workdir, background tasks, compressed output, PTY mode)",
34270
+ promptSnippet: bashCfg.background ? "Run shell commands (timeout in milliseconds; supports workdir, background tasks, compressed output, PTY mode)" : "Run shell commands (timeout in milliseconds; supports workdir and compressed output)",
33990
34271
  promptGuidelines: [
33991
34272
  `DO NOT use bash for code search or exploration — ${searchSteer}.`,
33992
34273
  "Set compressed: false when you need ANSI color codes in the output."
33993
34274
  ],
33994
- parameters: BashParams,
34275
+ parameters: bashParamsForConfig(bashCfg.background),
33995
34276
  async execute(_toolCallId, params, _signal, onUpdate, extCtx) {
33996
34277
  const bridge = bridgeFor(ctx, extCtx.cwd);
33997
34278
  const bashCfg2 = resolveBashConfig(ctx.config);
33998
34279
  const foregroundWaitMs = resolveForegroundWaitMs(bashCfg2.foreground_wait_window_ms);
34280
+ const backgroundDisabled = !bashCfg2.background;
33999
34281
  const timeout = coerceOptionalInt(params.timeout, "timeout", 1, Number.MAX_SAFE_INTEGER);
34000
- const ptyRows = coerceOptionalInt(params.ptyRows, "ptyRows", 1, 60);
34001
- const ptyCols = coerceOptionalInt(params.ptyCols, "ptyCols", 1, 140);
34002
- const effectiveBackground = params.background === true || params.pty === true;
34003
- const effectiveTimeout = effectiveBackground ? timeout : resolveBashKillTimeout(timeout, foregroundWaitMs);
34282
+ const ptyRows = backgroundDisabled ? undefined : coerceOptionalInt(params.ptyRows, "ptyRows", 1, 60);
34283
+ const ptyCols = backgroundDisabled ? undefined : coerceOptionalInt(params.ptyCols, "ptyCols", 1, 140);
34284
+ const requestedPty = !backgroundDisabled && params.pty === true;
34285
+ const effectiveBackground = !backgroundDisabled && (params.background === true || requestedPty);
34286
+ const effectiveTimeout = effectiveBackground || backgroundDisabled ? timeout : resolveBashKillTimeout(timeout, foregroundWaitMs);
34004
34287
  let spawnContext = {
34005
34288
  command: params.command,
34006
34289
  cwd: params.workdir
@@ -34025,7 +34308,7 @@ DO NOT use bash for code search or code exploration. If you are about to run gre
34025
34308
  background: effectiveBackground,
34026
34309
  notify_on_completion: effectiveBackground,
34027
34310
  compressed: params.compressed,
34028
- pty: params.pty,
34311
+ pty: requestedPty,
34029
34312
  pty_rows: ptyRows,
34030
34313
  pty_cols: ptyCols
34031
34314
  }, extCtx, {
@@ -34047,9 +34330,9 @@ DO NOT use bash for code search or code exploration. If you are about to run gre
34047
34330
  if (response.status === "running" && taskId) {
34048
34331
  if (effectiveBackground) {
34049
34332
  trackBgTask(resolveSessionId(extCtx), taskId);
34050
- return bashResult(appendPipeStripNote(formatBackgroundLaunch(taskId, params.pty === true), pipeStrip.note), { task_id: taskId });
34333
+ return bashResult(appendPipeStripNote(formatBackgroundLaunch(taskId, requestedPty), pipeStrip.note), { task_id: taskId });
34051
34334
  }
34052
- const waitTimeoutMs = effectiveTimeout !== undefined ? Math.min(effectiveTimeout, foregroundWaitMs) : foregroundWaitMs;
34335
+ const waitTimeoutMs = backgroundDisabled ? effectiveTimeout ?? DEFAULT_HARD_TIMEOUT_MS : effectiveTimeout !== undefined ? Math.min(effectiveTimeout, foregroundWaitMs) : foregroundWaitMs;
34053
34336
  const startedAt = Date.now();
34054
34337
  while (true) {
34055
34338
  const status = await callBashBridge(bridge, "bash_status", { task_id: taskId }, extCtx);
@@ -34066,6 +34349,10 @@ DO NOT use bash for code search or code exploration. If you are about to run gre
34066
34349
  });
34067
34350
  }
34068
34351
  if (Date.now() - startedAt >= waitTimeoutMs) {
34352
+ if (backgroundDisabled) {
34353
+ await sleep(FOREGROUND_POLL_INTERVAL_MS);
34354
+ continue;
34355
+ }
34069
34356
  const promoted = await callBashBridge(bridge, "bash_promote", { task_id: taskId }, extCtx);
34070
34357
  if (promoted.success === false) {
34071
34358
  throw new Error(promoted.message ?? "bash_promote failed");
@@ -34093,10 +34380,12 @@ DO NOT use bash for code search or code exploration. If you are about to run gre
34093
34380
  return renderBashResult(result, theme, context);
34094
34381
  }
34095
34382
  });
34096
- pi.registerTool(createBashStatusTool(ctx));
34097
- pi.registerTool(createBashWatchTool(ctx));
34098
- pi.registerTool(createBashWriteTool(ctx));
34099
- pi.registerTool(createBashKillTool(ctx));
34383
+ if (bashCfg.background) {
34384
+ pi.registerTool(createBashStatusTool(ctx));
34385
+ pi.registerTool(createBashWatchTool(ctx));
34386
+ pi.registerTool(createBashWriteTool(ctx));
34387
+ pi.registerTool(createBashKillTool(ctx));
34388
+ }
34100
34389
  }
34101
34390
  function formatBackgroundLaunch(taskId, isPty) {
34102
34391
  if (isPty) {
@@ -34115,7 +34404,7 @@ function createBashStatusTool(ctx) {
34115
34404
  return {
34116
34405
  name: "bash_status",
34117
34406
  label: "bash_status",
34118
- description: "Read-only snapshot of a background bash task. Returns immediately. Never waits. Use bash_watch to block on or register for pattern matches and exit events.",
34407
+ description: "Read-only snapshot of a background bash task. Returns immediately. Never waits. One look to check on a task is fine — never loop it to wait for completion. To wait, use bash_watch.",
34119
34408
  promptSnippet: "Inspect a background bash task by task_id",
34120
34409
  parameters: BashStatusParams,
34121
34410
  async execute(_toolCallId, params, _signal, _onUpdate, extCtx) {
@@ -34130,7 +34419,7 @@ function createBashWatchTool(ctx) {
34130
34419
  return {
34131
34420
  name: "bash_watch",
34132
34421
  label: "bash_watch",
34133
- description: "Watch a background bash task. Two modes. Async (background:true, requires pattern) registers a non-blocking notification and returns immediately — use this to be pinged when a specific line appears or the task exits, without freezing your turn. Sync (default) blocks until a pattern matches/the task exits/timeout, and is ONLY for short bounded waits (seconds, e.g. a dev server printing a readiness line). Do NOT sync-wait for a long task (build/test/install): blocking locks the user out until it ends instead end your turn and let the automatic completion reminder arrive, or use async mode.",
34422
+ description: "Watch a background bash task. Sync (default) blocks until a pattern matches, the task exits, or timeout — use it when the result is the next thing you need, even for long builds/tests/installs (pass timeout_ms up to 30 min for those). The user can interrupt anytime; the wait auto-converts to an async notification. Async (background:true, requires pattern) registers a non-blocking notification and returns immediately use when you have parallel work or want to end your turn. Never loop bash_status to wait.",
34134
34423
  promptSnippet: "Wait for or watch a background bash task",
34135
34424
  parameters: BashWatchParams,
34136
34425
  async execute(_toolCallId, params, _signal, _onUpdate, extCtx) {
@@ -34284,9 +34573,31 @@ async function waitForBashStatus(ctx, bridge, extCtx, taskId, outputMode, waitFo
34284
34573
  if (waitForExit)
34285
34574
  markTaskWaiting(sessionId, taskId);
34286
34575
  let sawTerminal = false;
34576
+ let lastData;
34287
34577
  try {
34288
34578
  while (true) {
34289
- const data = await bashStatusSnapshot(bridge, extCtx, taskId, outputMode, bridgeOptions);
34579
+ let data;
34580
+ try {
34581
+ data = await bashStatusSnapshot(bridge, extCtx, taskId, outputMode, bridgeOptions);
34582
+ } catch (err) {
34583
+ if (!isBridgeTransportTimeout(err))
34584
+ throw err;
34585
+ if (isSyncWatchAborted(sessionId)) {
34586
+ return withWaited(lastData ?? unavailableSnapshot(), {
34587
+ reason: "user_message",
34588
+ elapsed_ms: Date.now() - startedAt
34589
+ });
34590
+ }
34591
+ if (Date.now() >= deadline) {
34592
+ return withWaited(lastData ?? unavailableSnapshot(), {
34593
+ reason: "unavailable",
34594
+ elapsed_ms: Date.now() - startedAt
34595
+ });
34596
+ }
34597
+ await sleep(Math.min(BASH_WAIT_POLL_INTERVAL_MS, Math.max(0, deadline - Date.now())));
34598
+ continue;
34599
+ }
34600
+ lastData = data;
34290
34601
  const terminal = isTerminalStatus(data.status);
34291
34602
  if (waitFor) {
34292
34603
  const scan = await readNewTaskOutput(extCtx, taskId, data, spillCursor);
@@ -34478,6 +34789,9 @@ function formatWaitSummary(waited, details) {
34478
34789
  if (waited.reason === "timeout") {
34479
34790
  return `Waited ${waited.elapsed_ms}ms; timeout reached without match.`;
34480
34791
  }
34792
+ if (waited.reason === "unavailable") {
34793
+ return `Waited ${waited.elapsed_ms}ms; the bridge stayed busy and status couldn't be read. The task may still be running — check with bash_status({ taskId }).`;
34794
+ }
34481
34795
  const exit = typeof details.exit_code === "number" ? `, exit ${details.exit_code}` : "";
34482
34796
  return `Waited ${waited.elapsed_ms}ms; task exited (${details.status}${exit}).`;
34483
34797
  }
@@ -34647,7 +34961,7 @@ function registerConflictsTool(pi, ctx) {
34647
34961
  const reqParams = {};
34648
34962
  const path3 = params?.path;
34649
34963
  if (typeof path3 === "string" && path3.trim() !== "") {
34650
- await assertExternalDirectoryPermission(extCtx, path3, "inspect", {
34964
+ await assertExternalDirectoryPermission(extCtx, path3, {
34651
34965
  restrictToProjectRoot: ctx.config.restrict_to_project_root ?? false
34652
34966
  });
34653
34967
  reqParams.path = await resolvePathArg(extCtx.cwd, path3);
@@ -34703,10 +35017,10 @@ function renderFsResult(toolName, args, result, theme, context) {
34703
35017
  const skipped = data.skipped_files ?? [];
34704
35018
  const lines = [];
34705
35019
  for (const entry of deletedPaths) {
34706
- lines.push(`${theme.fg("success", "✓ deleted")} ${theme.fg("accent", shortenPath2(entry))}`);
35020
+ lines.push(`${theme.fg("success", "✓ deleted")} ${theme.fg("accent", shortenPath3(entry))}`);
34707
35021
  }
34708
35022
  for (const entry of skipped) {
34709
- lines.push(`${theme.fg("error", "✗ skipped")} ${theme.fg("accent", shortenPath2(entry.file))} ${theme.fg("muted", `(${entry.reason})`)}`);
35023
+ lines.push(`${theme.fg("error", "✗ skipped")} ${theme.fg("accent", shortenPath3(entry.file))} ${theme.fg("muted", `(${entry.reason})`)}`);
34710
35024
  }
34711
35025
  if (lines.length === 0) {
34712
35026
  lines.push(theme.fg("muted", "(no files deleted)"));
@@ -34716,8 +35030,8 @@ function renderFsResult(toolName, args, result, theme, context) {
34716
35030
  }
34717
35031
  const moveArgs = args;
34718
35032
  return renderSections([
34719
- `${theme.fg("success", "✓ moved")} ${theme.fg("accent", shortenPath2(moveArgs.filePath))}`,
34720
- `${theme.fg("muted", "to")} ${theme.fg("accent", shortenPath2(moveArgs.destination))}`
35033
+ `${theme.fg("success", "✓ moved")} ${theme.fg("accent", shortenPath3(moveArgs.filePath))}`,
35034
+ `${theme.fg("muted", "to")} ${theme.fg("accent", shortenPath3(moveArgs.destination))}`
34721
35035
  ], context);
34722
35036
  }
34723
35037
  function registerFsTools(pi, ctx, surface) {
@@ -34738,7 +35052,7 @@ function registerFsTools(pi, ctx, surface) {
34738
35052
  if (checked.has(file2))
34739
35053
  continue;
34740
35054
  checked.add(file2);
34741
- await assertExternalDirectoryPermission(extCtx, file2, "modify", {
35055
+ await assertExternalDirectoryPermission(extCtx, file2, {
34742
35056
  restrictToProjectRoot: ctx.config.restrict_to_project_root ?? false
34743
35057
  });
34744
35058
  }
@@ -34782,7 +35096,7 @@ function registerFsTools(pi, ctx, surface) {
34782
35096
  const destination = await resolvePathArg(extCtx.cwd, params.destination);
34783
35097
  const checked = new Set([filePath, destination]);
34784
35098
  for (const file2 of checked) {
34785
- await assertExternalDirectoryPermission(extCtx, file2, "modify", {
35099
+ await assertExternalDirectoryPermission(extCtx, file2, {
34786
35100
  restrictToProjectRoot: ctx.config.restrict_to_project_root ?? false
34787
35101
  });
34788
35102
  }
@@ -34831,33 +35145,33 @@ var ImportParams = Type7.Object({
34831
35145
  }))
34832
35146
  });
34833
35147
  function buildImportSections(args, payload, theme) {
34834
- const response = asRecord2(payload);
35148
+ const response = asRecord3(payload);
34835
35149
  if (!response)
34836
35150
  return [theme.fg("muted", "No import result.")];
34837
35151
  if (args.op === "organize") {
34838
- const groups = asRecords(response.groups);
34839
- const groupText = groups.length > 0 ? groups.map((group) => `${asString(group.name) ?? "unknown"}: ${asNumber(group.count) ?? 0}`).join(" · ") : "No imports found";
35152
+ const groups = asRecords2(response.groups);
35153
+ const groupText = groups.length > 0 ? groups.map((group) => `${asString2(group.name) ?? "unknown"}: ${asNumber2(group.count) ?? 0}`).join(" · ") : "No imports found";
34840
35154
  return [
34841
- `${theme.fg("success", "organized")} ${theme.fg("accent", asString(response.file) ?? args.filePath)}`,
35155
+ `${theme.fg("success", "organized")} ${theme.fg("accent", asString2(response.file) ?? args.filePath)}`,
34842
35156
  `${theme.fg("muted", "groups")} ${groupText}`,
34843
- `${theme.fg("muted", "duplicates removed")} ${asNumber(response.removed_duplicates) ?? 0}`
35157
+ `${theme.fg("muted", "duplicates removed")} ${asNumber2(response.removed_duplicates) ?? 0}`
34844
35158
  ];
34845
35159
  }
34846
35160
  if (args.op === "add") {
34847
- const moduleName2 = asString(response.module) ?? args.module ?? "(module)";
35161
+ const moduleName2 = asString2(response.module) ?? args.module ?? "(module)";
34848
35162
  const status = response.already_present === true ? theme.fg("warning", "already present") : theme.fg("success", "added");
34849
35163
  return [
34850
35164
  `${status} ${theme.fg("accent", moduleName2)}`,
34851
- `${theme.fg("muted", "file")} ${theme.fg("accent", asString(response.file) ?? args.filePath)}`,
34852
- `${theme.fg("muted", "group")} ${asString(response.group) ?? "—"}`
35165
+ `${theme.fg("muted", "file")} ${theme.fg("accent", asString2(response.file) ?? args.filePath)}`,
35166
+ `${theme.fg("muted", "group")} ${asString2(response.group) ?? "—"}`
34853
35167
  ];
34854
35168
  }
34855
- const moduleName = asString(response.module) ?? args.module ?? "(module)";
35169
+ const moduleName = asString2(response.module) ?? args.module ?? "(module)";
34856
35170
  const didRemove = response.removed !== false;
34857
35171
  const removeStatus = didRemove ? `${theme.fg("success", "removed")} ${theme.fg("accent", moduleName)}` : `${theme.fg("warning", "not present")} ${theme.fg("accent", moduleName)}`;
34858
35172
  return [
34859
35173
  removeStatus,
34860
- `${theme.fg("muted", "file")} ${theme.fg("accent", asString(response.file) ?? args.filePath)}`,
35174
+ `${theme.fg("muted", "file")} ${theme.fg("accent", asString2(response.file) ?? args.filePath)}`,
34861
35175
  args.removeName ? `${theme.fg("muted", "name")} ${args.removeName}` : `${theme.fg("muted", "scope")} entire import`
34862
35176
  ];
34863
35177
  }
@@ -34886,7 +35200,7 @@ function registerImportTools(pi, ctx) {
34886
35200
  throw new Error(`op='${params.op}' requires 'module'`);
34887
35201
  }
34888
35202
  const filePath = await resolvePathArg(extCtx.cwd, params.filePath);
34889
- await assertExternalDirectoryPermission(extCtx, filePath, "modify", {
35203
+ await assertExternalDirectoryPermission(extCtx, filePath, {
34890
35204
  restrictToProjectRoot: ctx.config.restrict_to_project_root ?? false
34891
35205
  });
34892
35206
  const bridge = bridgeFor(ctx, extCtx.cwd);
@@ -34962,7 +35276,7 @@ async function resolveAndGateScope(extCtx, ctx, scope) {
34962
35276
  if (checked.has(target))
34963
35277
  continue;
34964
35278
  checked.add(target);
34965
- await assertExternalDirectoryPermission(extCtx, target, "read", {
35279
+ await assertExternalDirectoryPermission(extCtx, target, {
34966
35280
  restrictToProjectRoot: ctx.config.restrict_to_project_root ?? false
34967
35281
  });
34968
35282
  }
@@ -34990,16 +35304,16 @@ function diagnosticsServerSummary(section) {
34990
35304
  return parts.length > 0 ? parts.join("; ") : "none reported";
34991
35305
  }
34992
35306
  function diagnosticsSummaryPart(summary) {
34993
- const section = asRecord2(summary?.diagnostics);
35307
+ const section = asRecord3(summary?.diagnostics);
34994
35308
  if (!section)
34995
35309
  return;
34996
- const errors3 = asNumber(section.errors);
34997
- const warnings = asNumber(section.warnings);
34998
- const info = asNumber(section.info);
34999
- const hints = asNumber(section.hints);
35310
+ const errors3 = asNumber2(section.errors);
35311
+ const warnings = asNumber2(section.warnings);
35312
+ const info = asNumber2(section.info);
35313
+ const hints = asNumber2(section.hints);
35000
35314
  const hasCounts = [errors3, warnings, info, hints].some((value) => value !== undefined);
35001
35315
  const counts = `${errors3 ?? 0} errors/${warnings ?? 0} warnings/${info ?? 0} info/${hints ?? 0} hints`;
35002
- const status = asString(section.status);
35316
+ const status = asString2(section.status);
35003
35317
  if (status === "pending") {
35004
35318
  return hasCounts ? `diagnostics ${counts} so far — still pending (servers: ${diagnosticsServerSummary(section)})` : `diagnostics pending (servers: ${diagnosticsServerSummary(section)})`;
35005
35319
  }
@@ -35012,9 +35326,9 @@ function diagnosticsSummaryPart(summary) {
35012
35326
  return;
35013
35327
  }
35014
35328
  function diagnosticLocation(diagnostic) {
35015
- const file2 = asString(diagnostic.file) ?? "(unknown file)";
35016
- const line = asNumber(diagnostic.line);
35017
- const column = asNumber(diagnostic.column);
35329
+ const file2 = asString2(diagnostic.file) ?? "(unknown file)";
35330
+ const line = asNumber2(diagnostic.line);
35331
+ const column = asNumber2(diagnostic.column);
35018
35332
  if (line === undefined)
35019
35333
  return file2;
35020
35334
  if (column === undefined)
@@ -35022,14 +35336,14 @@ function diagnosticLocation(diagnostic) {
35022
35336
  return `${file2}:${line}:${column}`;
35023
35337
  }
35024
35338
  function diagnosticsDetailSection(details) {
35025
- const diagnostics = asRecords(details?.diagnostics);
35339
+ const diagnostics = asRecords2(details?.diagnostics);
35026
35340
  if (diagnostics.length === 0)
35027
35341
  return;
35028
35342
  const lines = ["diagnostics"];
35029
35343
  for (const diagnostic of diagnostics) {
35030
- const severity = asString(diagnostic.severity) ?? "information";
35031
- const message = asString(diagnostic.message) ?? "(no message)";
35032
- const source = asString(diagnostic.source);
35344
+ const severity = asString2(diagnostic.severity) ?? "information";
35345
+ const message = asString2(diagnostic.message) ?? "(no message)";
35346
+ const source = asString2(diagnostic.source);
35033
35347
  const suffix = source ? ` [${source}]` : "";
35034
35348
  lines.push(`- ${diagnosticLocation(diagnostic)} ${severity} ${message}${suffix}`);
35035
35349
  }
@@ -35037,15 +35351,15 @@ function diagnosticsDetailSection(details) {
35037
35351
  `);
35038
35352
  }
35039
35353
  function countFrom(summary, key) {
35040
- const section = asRecord2(summary?.[key]);
35041
- return asNumber(section?.count);
35354
+ const section = asRecord3(summary?.[key]);
35355
+ return asNumber2(section?.count);
35042
35356
  }
35043
35357
  function tier2SummaryPart(summary, key, label) {
35044
- const section = asRecord2(summary?.[key]);
35045
- const count = asNumber(section?.count);
35358
+ const section = asRecord3(summary?.[key]);
35359
+ const count = asNumber2(section?.count);
35046
35360
  if (count !== undefined)
35047
35361
  return `${label} ${count}`;
35048
- const status = asString(section?.status);
35362
+ const status = asString2(section?.status);
35049
35363
  return `${label} ${status ?? "unavailable"}`;
35050
35364
  }
35051
35365
  function shortDupOccurrence(entry) {
@@ -35054,12 +35368,12 @@ function shortDupOccurrence(entry) {
35054
35368
  }
35055
35369
  function tier2TopPreview(summary, theme) {
35056
35370
  const lines = [];
35057
- const dup = asRecord2(summary?.duplicates);
35371
+ const dup = asRecord3(summary?.duplicates);
35058
35372
  const dupTop = Array.isArray(dup?.top) ? dup.top : [];
35059
35373
  for (const group of dupTop) {
35060
- const record2 = asRecord2(group);
35374
+ const record2 = asRecord3(group);
35061
35375
  const files = Array.isArray(record2?.files) ? record2.files : [];
35062
- const cost = asNumber(record2?.cost);
35376
+ const cost = asNumber2(record2?.cost);
35063
35377
  if (files.length < 2)
35064
35378
  continue;
35065
35379
  const a = shortDupOccurrence(String(files[0]));
@@ -35070,12 +35384,12 @@ function tier2TopPreview(summary, theme) {
35070
35384
  ["dead_code", "dead"],
35071
35385
  ["unused_exports", "unused"]
35072
35386
  ]) {
35073
- const section = asRecord2(summary?.[key]);
35387
+ const section = asRecord3(summary?.[key]);
35074
35388
  const top = Array.isArray(section?.top) ? section.top : [];
35075
35389
  for (const item of top) {
35076
- const record2 = asRecord2(item);
35077
- const file2 = asString(record2?.file);
35078
- const symbol2 = asString(record2?.symbol);
35390
+ const record2 = asRecord3(item);
35391
+ const file2 = asString2(record2?.file);
35392
+ const symbol2 = asString2(record2?.symbol);
35079
35393
  if (!file2 || !symbol2)
35080
35394
  continue;
35081
35395
  lines.push(` ${label} ${symbol2} (${file2.split("/").pop()})`);
@@ -35088,7 +35402,7 @@ ${lines.join(`
35088
35402
  `)}`;
35089
35403
  }
35090
35404
  function tier2RefreshCategories(response) {
35091
- const scannerState = asRecord2(response.scanner_state);
35405
+ const scannerState = asRecord3(response.scanner_state);
35092
35406
  const categories = new Set;
35093
35407
  for (const key of ["pending_categories", "stale_categories"]) {
35094
35408
  const values = scannerState?.[key];
@@ -35135,18 +35449,18 @@ function runPendingTier2Categories(bridge, categories, extCtx) {
35135
35449
  });
35136
35450
  }
35137
35451
  function buildInspectSections(payload, theme) {
35138
- const response = asRecord2(payload);
35452
+ const response = asRecord3(payload);
35139
35453
  if (!response)
35140
35454
  return [theme.fg("muted", "No inspect snapshot available.")];
35141
- const summary = asRecord2(response.summary);
35142
- const metrics = asRecord2(summary?.metrics);
35143
- const scannerState = asRecord2(response.scanner_state);
35455
+ const summary = asRecord3(response.summary);
35456
+ const metrics = asRecord3(summary?.metrics);
35457
+ const scannerState = asRecord3(response.scanner_state);
35144
35458
  const stale = Array.isArray(scannerState?.stale_categories) ? scannerState.stale_categories.length : 0;
35145
35459
  const pending = Array.isArray(scannerState?.pending_categories) ? scannerState.pending_categories.length : 0;
35146
35460
  const parts = [
35147
35461
  `todos ${countFrom(summary, "todos") ?? 0}`,
35148
35462
  diagnosticsSummaryPart(summary),
35149
- `metrics ${asNumber(metrics?.files) ?? 0} files/${asNumber(metrics?.symbols) ?? 0} symbols`,
35463
+ `metrics ${asNumber2(metrics?.files) ?? 0} files/${asNumber2(metrics?.symbols) ?? 0} symbols`,
35150
35464
  tier2SummaryPart(summary, "dead_code", "dead code"),
35151
35465
  tier2SummaryPart(summary, "unused_exports", "unused exports"),
35152
35466
  tier2SummaryPart(summary, "duplicates", "duplicates")
@@ -35158,7 +35472,7 @@ function buildInspectSections(payload, theme) {
35158
35472
  const topPreview = tier2TopPreview(summary, theme);
35159
35473
  if (topPreview)
35160
35474
  sections.push(topPreview);
35161
- const details = asRecord2(response.details);
35475
+ const details = asRecord3(response.details);
35162
35476
  if (details) {
35163
35477
  const names = Object.keys(details);
35164
35478
  sections.push(names.length > 0 ? `details: ${names.join(", ")}` : theme.fg("muted", "No drill-down details returned."));
@@ -35166,7 +35480,7 @@ function buildInspectSections(payload, theme) {
35166
35480
  if (diagnosticsDetails)
35167
35481
  sections.push(diagnosticsDetails);
35168
35482
  }
35169
- const text = asString(response.text);
35483
+ const text = asString2(response.text);
35170
35484
  if (text)
35171
35485
  sections.push(text);
35172
35486
  return sections;
@@ -35199,7 +35513,10 @@ function registerInspectTool(pi, ctx) {
35199
35513
  runPendingTier2Categories(bridge, tier2RefreshCategories(response), extCtx);
35200
35514
  const body = response.text;
35201
35515
  if (typeof body === "string") {
35202
- const diagnostics = diagnosticsSummaryPart(asRecord2(response.summary));
35516
+ const diagnosticsSummary = diagnosticsSummaryPart(asRecord3(response.summary));
35517
+ const diagnosticsDetail = diagnosticsDetailSection(asRecord3(response.details));
35518
+ const diagnostics = [diagnosticsSummary, diagnosticsDetail].filter(Boolean).join(`
35519
+ `);
35203
35520
  const text = diagnostics ? body ? `${body}
35204
35521
 
35205
35522
  ${diagnostics}` : diagnostics : body;
@@ -35239,138 +35556,11 @@ function navigateParamsSchema() {
35239
35556
  }))
35240
35557
  });
35241
35558
  }
35242
- function treeLine(depth, text) {
35243
- return `${" ".repeat(depth)}${depth === 0 ? "" : "↳ "}${text}`;
35244
- }
35245
- function renderCallTreeNode(node, depth, lines) {
35246
- const name = asString(node.name) ?? "(unknown)";
35247
- const file2 = shortenPath2(asString(node.file) ?? "(unknown file)");
35248
- const line = asNumber(node.line);
35249
- lines.push(treeLine(depth, `${name} ${line !== undefined ? `[${file2}:${line}]` : `[${file2}]`}`));
35250
- asRecords(node.children).forEach((child) => {
35251
- renderCallTreeNode(child, depth + 1, lines);
35252
- });
35253
- }
35254
- function depthWarning(response, theme, depthField = "depth_limited", truncatedField = "truncated") {
35255
- const limited = asBoolean(response[depthField]);
35256
- const truncated = asNumber(response[truncatedField]) ?? 0;
35257
- if (!limited && truncated === 0)
35258
- return "";
35259
- const detail = truncated > 0 ? `, ${truncated} truncated` : "";
35260
- return theme.fg("warning", `(depth limited${detail})`);
35261
- }
35262
- function renderTracePath(path3, index, lines) {
35263
- lines.push(`Path ${index + 1}`);
35264
- asRecords(path3.hops).forEach((hop, hopIndex) => {
35265
- const symbol2 = asString(hop.symbol) ?? "(unknown)";
35266
- const file2 = shortenPath2(asString(hop.file) ?? "(unknown file)");
35267
- const line = asNumber(hop.line);
35268
- const entry = hop.is_entry_point === true ? " [entry]" : "";
35269
- lines.push(treeLine(hopIndex + 1, `${symbol2}${entry} ${line !== undefined ? `[${file2}:${line}]` : `[${file2}]`}`));
35270
- });
35271
- }
35272
35559
  function buildNavigateSections(args, payload, theme) {
35273
- const response = asRecord2(payload);
35274
- if (!response)
35275
- return [theme.fg("muted", "No navigation result.")];
35276
- if (args.op === "call_tree") {
35277
- const lines = [];
35278
- renderCallTreeNode(response, 0, lines);
35279
- const warning = depthWarning(response, theme);
35280
- if (warning)
35281
- lines.push(warning);
35282
- return lines.length > 0 ? lines : [theme.fg("muted", "No call tree available.")];
35283
- }
35284
- if (args.op === "callers") {
35285
- const groups = asRecords(response.callers);
35286
- const warning = depthWarning(response, theme);
35287
- const sections2 = [
35288
- `${theme.fg("success", `${asNumber(response.total_callers) ?? 0} caller${(asNumber(response.total_callers) ?? 0) === 1 ? "" : "s"}`)} ${theme.fg("muted", `${groups.length} file group${groups.length === 1 ? "" : "s"}`)} ${warning}`.trim()
35289
- ];
35290
- groups.forEach((group) => {
35291
- const file2 = shortenPath2(asString(group.file) ?? "(unknown file)");
35292
- const lines = [theme.fg("accent", file2)];
35293
- asRecords(group.callers).forEach((caller) => {
35294
- lines.push(` ↳ ${asString(caller.symbol) ?? "(unknown)"} ${theme.fg("muted", `line ${asNumber(caller.line) ?? "?"}`)}`);
35295
- });
35296
- sections2.push(lines.join(`
35297
- `));
35298
- });
35299
- return sections2;
35300
- }
35301
- if (args.op === "trace_to_symbol") {
35302
- const path3 = asRecords(response.path);
35303
- const complete = asBoolean(response.complete);
35304
- const reason = asString(response.reason);
35305
- if (path3.length === 0) {
35306
- const prefix = complete === false ? theme.fg("warning", "No complete path") : theme.fg("muted", "No path");
35307
- return [`${prefix}${reason ? ` (${reason})` : ""}`];
35308
- }
35309
- const lines = [theme.fg("success", `${path3.length} hop${path3.length === 1 ? "" : "s"}`)];
35310
- path3.forEach((hop, index) => {
35311
- const symbol2 = asString(hop.symbol) ?? "(unknown)";
35312
- const file2 = shortenPath2(asString(hop.file) ?? "(unknown file)");
35313
- const line = asNumber(hop.line);
35314
- lines.push(treeLine(index + 1, `${symbol2} ${line !== undefined ? `[${file2}:${line}]` : `[${file2}]`}`));
35315
- });
35316
- return lines;
35317
- }
35318
- if (args.op === "trace_to") {
35319
- const paths = asRecords(response.paths);
35320
- const warning = depthWarning(response, theme, "max_depth_reached", "truncated_paths");
35321
- const sections2 = [
35322
- `${theme.fg("success", `${asNumber(response.total_paths) ?? paths.length} path${(asNumber(response.total_paths) ?? paths.length) === 1 ? "" : "s"}`)} ${theme.fg("muted", `${asNumber(response.entry_points_found) ?? 0} entry point${(asNumber(response.entry_points_found) ?? 0) === 1 ? "" : "s"}`)} ${warning}`.trim()
35323
- ];
35324
- if (paths.length === 0)
35325
- sections2.push(theme.fg("muted", "No entry paths found."));
35326
- paths.forEach((path3, index) => {
35327
- const lines = [];
35328
- renderTracePath(path3, index, lines);
35329
- sections2.push(lines.join(`
35330
- `));
35331
- });
35332
- return sections2;
35333
- }
35334
- if (args.op === "impact") {
35335
- const callers = asRecords(response.callers);
35336
- const warning = depthWarning(response, theme);
35337
- const sections2 = [
35338
- `${theme.fg("warning", `${asNumber(response.total_affected) ?? callers.length} affected call site${(asNumber(response.total_affected) ?? callers.length) === 1 ? "" : "s"}`)} ${theme.fg("muted", `${asNumber(response.affected_files) ?? 0} file${(asNumber(response.affected_files) ?? 0) === 1 ? "" : "s"}`)} ${warning}`.trim()
35339
- ];
35340
- if (callers.length === 0)
35341
- sections2.push(theme.fg("muted", "No impacted callers found."));
35342
- callers.forEach((caller) => {
35343
- const file2 = shortenPath2(asString(caller.caller_file) ?? "(unknown file)");
35344
- const symbol2 = asString(caller.caller_symbol) ?? "(unknown)";
35345
- const line = asNumber(caller.line) ?? 0;
35346
- const entry = caller.is_entry_point === true ? ` ${theme.fg("warning", "[entry]")}` : "";
35347
- const expression = asString(caller.call_expression);
35348
- const params = Array.isArray(caller.parameters) ? caller.parameters.map(String).join(", ") : "";
35349
- sections2.push([
35350
- `${theme.fg("accent", file2)}:${line}`,
35351
- ` ↳ ${symbol2}${entry}`,
35352
- expression ? ` ${theme.fg("muted", expression)}` : undefined,
35353
- params ? ` ${theme.fg("muted", `params: ${params}`)}` : undefined
35354
- ].filter(Boolean).join(`
35355
- `));
35356
- });
35357
- return sections2;
35358
- }
35359
- const hops = asRecords(response.hops);
35360
- const sections = [
35361
- `${theme.fg("success", `${hops.length} hop${hops.length === 1 ? "" : "s"}`)} ${asBoolean(response.depth_limited) ? theme.fg("warning", "(depth limited)") : ""}`.trim()
35362
- ];
35363
- if (hops.length === 0)
35364
- sections.push(theme.fg("muted", "No data-flow hops found."));
35365
- hops.forEach((hop, index) => {
35366
- const file2 = shortenPath2(asString(hop.file) ?? "(unknown file)");
35367
- const symbol2 = asString(hop.symbol) ?? "(unknown)";
35368
- const variable = asString(hop.variable) ?? "(unknown)";
35369
- const line = asNumber(hop.line) ?? 0;
35370
- const approximate = hop.approximate === true ? ` ${theme.fg("warning", "[approx]")}` : "";
35371
- sections.push(treeLine(index, `${variable} ${theme.fg("muted", `${asString(hop.flow_type) ?? "flow"}`)} ${symbol2} [${file2}:${line}]${approximate}`));
35372
- });
35373
- return sections;
35560
+ const themeAdapter = {
35561
+ fg: (role, s) => theme.fg(role, s)
35562
+ };
35563
+ return formatCallgraphSections(args.op, payload, themeAdapter);
35374
35564
  }
35375
35565
  function renderNavigateCall(args, theme, context) {
35376
35566
  const summary = [
@@ -35390,7 +35580,7 @@ function registerNavigateTool(pi, ctx) {
35390
35580
  pi.registerTool({
35391
35581
  name: "aft_callgraph",
35392
35582
  label: "callgraph",
35393
- description: "Answer code-relationship questions from a real call graph — instead of grep + read chains. Reach for this whenever the question is about how symbols connect. All ops require both `filePath` and `symbol`. Use `callers` for call sites (before renaming/signature changes), `impact` for blast radius (what breaks if a symbol changes), `call_tree` for what a function calls, `trace_to` for how execution reaches a symbol from entry points, `trace_to_symbol` for the shortest path from one symbol to another (requires `toSymbol`; if ambiguous, the error returns candidate files — retry with `toFile`), `trace_data` to follow a value across assignments/params.",
35583
+ description: "Answer code-relationship questions from a real call graph — instead of grep + read chains. Reach for this whenever the question is about how symbols connect. All ops require both `filePath` and `symbol`. Use `callers` for call sites (before renaming/signature changes), `impact` for blast radius (what breaks if a symbol changes), `call_tree` for what a function calls, `trace_to` for how execution reaches a symbol from entry points, `trace_to_symbol` for the shortest path from one symbol to another (requires `toSymbol`; if ambiguous, the error returns candidate files — retry with `toFile`), `trace_data` to follow a value across assignments/params. Markers: ~ = edge resolved by name only (may point at the wrong same-named symbol); [unresolved] = callee not resolved to a definition, so the location shown is the call site. Unmarked edges are resolved exactly.",
35394
35584
  parameters: navigateParamsSchema(),
35395
35585
  async execute(_toolCallId, params, _signal, _onUpdate, extCtx) {
35396
35586
  if (isEmptyParam(params.filePath)) {
@@ -35412,7 +35602,7 @@ function registerNavigateTool(pi, ctx) {
35412
35602
  if (checked.has(target))
35413
35603
  continue;
35414
35604
  checked.add(target);
35415
- await assertExternalDirectoryPermission(extCtx, target, "read", {
35605
+ await assertExternalDirectoryPermission(extCtx, target, {
35416
35606
  restrictToProjectRoot: ctx.config.restrict_to_project_root ?? false
35417
35607
  });
35418
35608
  }
@@ -35433,7 +35623,8 @@ function registerNavigateTool(pi, ctx) {
35433
35623
  req.toFile = toFile;
35434
35624
  try {
35435
35625
  const response = await callBridge(bridge, params.op, req, extCtx);
35436
- return textResult(JSON.stringify(response, null, 2));
35626
+ return textResult(formatCallgraphSections(params.op, response, PLAIN_CALLGRAPH_THEME).join(`
35627
+ `), response);
35437
35628
  } catch (error50) {
35438
35629
  if (error50 instanceof BridgeError && CALLGRAPH_SOFT_CODES.has(error50.code)) {
35439
35630
  return textResult(error50.message);
@@ -35491,7 +35682,7 @@ async function assertReadPathPermissions(extCtx, ctx, paths) {
35491
35682
  if (!target || checked.has(target))
35492
35683
  continue;
35493
35684
  checked.add(target);
35494
- await assertExternalDirectoryPermission(extCtx, target, "read", {
35685
+ await assertExternalDirectoryPermission(extCtx, target, {
35495
35686
  restrictToProjectRoot: ctx.config.restrict_to_project_root ?? false
35496
35687
  });
35497
35688
  }
@@ -35511,25 +35702,25 @@ function buildOutlineSections(text, theme) {
35511
35702
  `)];
35512
35703
  }
35513
35704
  function buildZoomSections(args, payload, theme) {
35514
- const batch = asRecord2(payload);
35705
+ const batch = asRecord3(payload);
35515
35706
  const batchItems = Array.isArray(batch?.symbols) ? batch.symbols : Array.isArray(batch?.entries) ? batch.entries : null;
35516
35707
  if (batchItems) {
35517
35708
  const header = batch?.complete === false ? [theme.fg("warning", "Incomplete zoom results")] : [];
35518
35709
  return [
35519
35710
  ...header,
35520
35711
  ...batchItems.map((item) => {
35521
- const record2 = asRecord2(item);
35712
+ const record2 = asRecord3(item);
35522
35713
  if (!record2)
35523
35714
  return theme.fg("muted", "No zoom result available.");
35524
- const name = asString(record2.name) ?? "(unknown symbol)";
35525
- const itemTargetLabel = asString(record2.targetLabel) ?? zoomTargetLabel(args);
35715
+ const name = asString2(record2.name) ?? "(unknown symbol)";
35716
+ const itemTargetLabel = asString2(record2.targetLabel) ?? zoomTargetLabel(args);
35526
35717
  if (record2.success === false) {
35527
- const location = record2.targetLabel ? ` in ${shortenPath2(itemTargetLabel)}` : "";
35528
- return theme.fg("error", `Symbol "${name}" not found${location}: ${asString(record2.error) ?? "zoom failed"}`);
35718
+ const location = record2.targetLabel ? ` in ${shortenPath3(itemTargetLabel)}` : "";
35719
+ return theme.fg("error", `Symbol "${name}" not found${location}: ${asString2(record2.error) ?? "zoom failed"}`);
35529
35720
  }
35530
- const content = asString(record2.content);
35721
+ const content = asString2(record2.content);
35531
35722
  return [
35532
- `${theme.fg("accent", name)} ${theme.fg("muted", shortenPath2(itemTargetLabel))}`,
35723
+ `${theme.fg("accent", name)} ${theme.fg("muted", shortenPath3(itemTargetLabel))}`,
35533
35724
  content
35534
35725
  ].filter(Boolean).join(`
35535
35726
  `);
@@ -35540,32 +35731,32 @@ function buildZoomSections(args, payload, theme) {
35540
35731
  if (items.length === 0)
35541
35732
  return [theme.fg("muted", "No zoom result available.")];
35542
35733
  return items.map((item) => {
35543
- const record2 = asRecord2(item);
35734
+ const record2 = asRecord3(item);
35544
35735
  if (!record2)
35545
35736
  return theme.fg("muted", "No zoom result available.");
35546
- const name = asString(record2.name) ?? "(unknown symbol)";
35547
- const kind = asString(record2.kind) ?? "symbol";
35548
- const range = asRecord2(record2.range);
35737
+ const name = asString2(record2.name) ?? "(unknown symbol)";
35738
+ const kind = asString2(record2.kind) ?? "symbol";
35739
+ const range = asRecord3(record2.range);
35549
35740
  const startLine = range && typeof range.start_line === "number" ? range.start_line : undefined;
35550
35741
  const endLine = range && typeof range.end_line === "number" ? range.end_line : undefined;
35551
35742
  const targetLabel = zoomTargetLabel(args);
35552
- const location = startLine !== undefined ? `${shortenPath2(targetLabel)}:${startLine}${endLine && endLine !== startLine ? `-${endLine}` : ""}` : shortenPath2(targetLabel);
35743
+ const location = startLine !== undefined ? `${shortenPath3(targetLabel)}:${startLine}${endLine && endLine !== startLine ? `-${endLine}` : ""}` : shortenPath3(targetLabel);
35553
35744
  const lines = [`${theme.fg("accent", name)} ${theme.fg("muted", `[${kind}] ${location}`)}`];
35554
- const content = asString(record2.content);
35745
+ const content = asString2(record2.content);
35555
35746
  if (content) {
35556
35747
  lines.push(content.split(`
35557
35748
  `).map((line) => ` ${line}`).join(`
35558
35749
  `));
35559
35750
  }
35560
- const annotations = asRecord2(record2.annotations);
35561
- const callsOut = annotations ? asRecords(annotations.calls_out) : [];
35562
- const calledBy = annotations ? asRecords(annotations.called_by) : [];
35751
+ const annotations = asRecord3(record2.annotations);
35752
+ const callsOut = annotations ? asRecords2(annotations.calls_out) : [];
35753
+ const calledBy = annotations ? asRecords2(annotations.called_by) : [];
35563
35754
  if (callsOut.length > 0) {
35564
- lines.push(`${theme.fg("muted", "calls out")}`, callsOut.map((call) => ` ↳ ${asString(call.name) ?? "(unknown)"}${typeof call.line === "number" ? `:${call.line}` : ""}`).join(`
35755
+ lines.push(`${theme.fg("muted", "calls out")}`, callsOut.map((call) => ` ↳ ${asString2(call.name) ?? "(unknown)"}${typeof call.line === "number" ? `:${call.line}` : ""}`).join(`
35565
35756
  `));
35566
35757
  }
35567
35758
  if (calledBy.length > 0) {
35568
- lines.push(`${theme.fg("muted", "called by")}`, calledBy.map((call) => ` ↳ ${asString(call.name) ?? "(unknown)"}${typeof call.line === "number" ? `:${call.line}` : ""}`).join(`
35759
+ lines.push(`${theme.fg("muted", "called by")}`, calledBy.map((call) => ` ↳ ${asString2(call.name) ?? "(unknown)"}${typeof call.line === "number" ? `:${call.line}` : ""}`).join(`
35569
35760
  `));
35570
35761
  }
35571
35762
  return lines.join(`
@@ -35904,32 +36095,32 @@ var RefactorParams = Type11.Object({
35904
36095
  callSiteLine: optionalInt(1, Number.MAX_SAFE_INTEGER)
35905
36096
  });
35906
36097
  function buildRefactorSections(args, payload, theme) {
35907
- const response = asRecord2(payload);
36098
+ const response = asRecord3(payload);
35908
36099
  if (!response)
35909
36100
  return [theme.fg("muted", "No refactor result.")];
35910
36101
  if (args.op === "move") {
35911
- const results = asRecords(response.results);
36102
+ const results = asRecords2(response.results);
35912
36103
  return [
35913
36104
  `${theme.fg("success", "moved symbol")} ${theme.fg("toolOutput", args.symbol ?? "(symbol)")}`,
35914
- `${theme.fg("muted", "files modified")} ${asNumber(response.files_modified) ?? results.length}`,
35915
- `${theme.fg("muted", "consumers updated")} ${asNumber(response.consumers_updated) ?? 0}`,
35916
- results.length > 0 ? results.map((entry) => ` ↳ ${shortenPath2(asString(entry.file) ?? "(unknown file)")}`).join(`
36105
+ `${theme.fg("muted", "files modified")} ${asNumber2(response.files_modified) ?? results.length}`,
36106
+ `${theme.fg("muted", "consumers updated")} ${asNumber2(response.consumers_updated) ?? 0}`,
36107
+ results.length > 0 ? results.map((entry) => ` ↳ ${shortenPath3(asString2(entry.file) ?? "(unknown file)")}`).join(`
35917
36108
  `) : theme.fg("muted", "No files reported.")
35918
36109
  ];
35919
36110
  }
35920
36111
  if (args.op === "extract") {
35921
36112
  return [
35922
- `${theme.fg("success", "extracted")} ${theme.fg("toolOutput", asString(response.name) ?? args.name ?? "(function)")}`,
35923
- `${theme.fg("muted", "file")} ${theme.fg("accent", shortenPath2(asString(response.file) ?? args.filePath))}`,
36113
+ `${theme.fg("success", "extracted")} ${theme.fg("toolOutput", asString2(response.name) ?? args.name ?? "(function)")}`,
36114
+ `${theme.fg("muted", "file")} ${theme.fg("accent", shortenPath3(asString2(response.file) ?? args.filePath))}`,
35924
36115
  `${theme.fg("muted", "params")} ${Array.isArray(response.parameters) ? response.parameters.join(", ") || "none" : "none"}`,
35925
- `${theme.fg("muted", "return type")} ${asString(response.return_type) ?? "unknown"}`
36116
+ `${theme.fg("muted", "return type")} ${asString2(response.return_type) ?? "unknown"}`
35926
36117
  ];
35927
36118
  }
35928
36119
  return [
35929
- `${theme.fg("success", "inlined")} ${theme.fg("toolOutput", asString(response.symbol) ?? args.symbol ?? "(symbol)")}`,
35930
- `${theme.fg("muted", "file")} ${theme.fg("accent", shortenPath2(asString(response.file) ?? args.filePath))}`,
35931
- `${theme.fg("muted", "context")} ${asString(response.call_context) ?? "unknown"}`,
35932
- `${theme.fg("muted", "substitutions")} ${asNumber(response.substitutions) ?? 0}`
36120
+ `${theme.fg("success", "inlined")} ${theme.fg("toolOutput", asString2(response.symbol) ?? args.symbol ?? "(symbol)")}`,
36121
+ `${theme.fg("muted", "file")} ${theme.fg("accent", shortenPath3(asString2(response.file) ?? args.filePath))}`,
36122
+ `${theme.fg("muted", "context")} ${asString2(response.call_context) ?? "unknown"}`,
36123
+ `${theme.fg("muted", "substitutions")} ${asNumber2(response.substitutions) ?? 0}`
35933
36124
  ];
35934
36125
  }
35935
36126
  function renderRefactorCall(args, theme, context) {
@@ -35986,7 +36177,7 @@ function registerRefactorTool(pi, ctx) {
35986
36177
  if (checked.has(target))
35987
36178
  continue;
35988
36179
  checked.add(target);
35989
- await assertExternalDirectoryPermission(extCtx, target, "modify", {
36180
+ await assertExternalDirectoryPermission(extCtx, target, {
35990
36181
  restrictToProjectRoot: ctx.config.restrict_to_project_root ?? false
35991
36182
  });
35992
36183
  }
@@ -36038,34 +36229,34 @@ var SafetyParams = Type12.Object({
36038
36229
  }))
36039
36230
  });
36040
36231
  function buildSafetySections(args, payload, theme) {
36041
- const response = asRecord2(payload);
36232
+ const response = asRecord3(payload);
36042
36233
  if (!response)
36043
36234
  return [theme.fg("muted", "No safety result.")];
36044
36235
  if (args.op === "undo") {
36045
36236
  if (response.operation === true) {
36046
36237
  return [
36047
- `${theme.fg("success", "restored operation")} ${theme.fg("accent", asString(response.op_id) ?? "(operation)")}`,
36048
- `${theme.fg("muted", "files")} ${asNumber(response.restored_count) ?? asRecords(response.restored).length}`
36238
+ `${theme.fg("success", "restored operation")} ${theme.fg("accent", asString2(response.op_id) ?? "(operation)")}`,
36239
+ `${theme.fg("muted", "files")} ${asNumber2(response.restored_count) ?? asRecords2(response.restored).length}`
36049
36240
  ];
36050
36241
  }
36051
36242
  return [
36052
- `${theme.fg("success", "restored")} ${theme.fg("accent", shortenPath2(asString(response.path) ?? args.filePath ?? "(file)"))}`,
36053
- `${theme.fg("muted", "backup")} ${asString(response.backup_id) ?? "—"}`
36243
+ `${theme.fg("success", "restored")} ${theme.fg("accent", shortenPath3(asString2(response.path) ?? args.filePath ?? "(file)"))}`,
36244
+ `${theme.fg("muted", "backup")} ${asString2(response.backup_id) ?? "—"}`
36054
36245
  ];
36055
36246
  }
36056
36247
  if (args.op === "history") {
36057
- const entries = asRecords(response.entries);
36248
+ const entries = asRecords2(response.entries);
36058
36249
  const sections2 = [
36059
- theme.fg("accent", shortenPath2(asString(response.file) ?? args.filePath ?? "(file)"))
36250
+ theme.fg("accent", shortenPath3(asString2(response.file) ?? args.filePath ?? "(file)"))
36060
36251
  ];
36061
36252
  if (entries.length === 0) {
36062
36253
  sections2.push(theme.fg("muted", "No history entries."));
36063
36254
  return sections2;
36064
36255
  }
36065
36256
  sections2.push(entries.map((entry, index) => {
36066
- const backupId = asString(entry.backup_id) ?? `entry-${index + 1}`;
36257
+ const backupId = asString2(entry.backup_id) ?? `entry-${index + 1}`;
36067
36258
  const timestamp = formatTimestamp(entry.timestamp) ?? "unknown time";
36068
- const description = asString(entry.description) ?? "";
36259
+ const description = asString2(entry.description) ?? "";
36069
36260
  return `${index + 1}. ${backupId} ${theme.fg("muted", timestamp)}${description ? `
36070
36261
  ${description}` : ""}`;
36071
36262
  }).join(`
@@ -36073,22 +36264,22 @@ function buildSafetySections(args, payload, theme) {
36073
36264
  return sections2;
36074
36265
  }
36075
36266
  if (args.op === "checkpoint") {
36076
- const skipped = asRecords(response.skipped);
36267
+ const skipped = asRecords2(response.skipped);
36077
36268
  return [
36078
- `${theme.fg("success", "checkpoint created")} ${theme.fg("accent", asString(response.name) ?? args.name ?? "(checkpoint)")}`,
36079
- `${theme.fg("muted", "files")} ${asNumber(response.file_count) ?? 0}`,
36269
+ `${theme.fg("success", "checkpoint created")} ${theme.fg("accent", asString2(response.name) ?? args.name ?? "(checkpoint)")}`,
36270
+ `${theme.fg("muted", "files")} ${asNumber2(response.file_count) ?? 0}`,
36080
36271
  skipped.length > 0 ? `${theme.fg("warning", "skipped")}
36081
- ${skipped.map((entry) => ` ↳ ${shortenPath2(asString(entry.file) ?? "(file)")}: ${asString(entry.error) ?? "unknown error"}`).join(`
36272
+ ${skipped.map((entry) => ` ↳ ${shortenPath3(asString2(entry.file) ?? "(file)")}: ${asString2(entry.error) ?? "unknown error"}`).join(`
36082
36273
  `)}` : theme.fg("muted", "No skipped files.")
36083
36274
  ];
36084
36275
  }
36085
36276
  if (args.op === "restore") {
36086
36277
  return [
36087
- `${theme.fg("success", "checkpoint restored")} ${theme.fg("accent", asString(response.name) ?? args.name ?? "(checkpoint)")}`,
36088
- `${theme.fg("muted", "files")} ${asNumber(response.file_count) ?? 0}`
36278
+ `${theme.fg("success", "checkpoint restored")} ${theme.fg("accent", asString2(response.name) ?? args.name ?? "(checkpoint)")}`,
36279
+ `${theme.fg("muted", "files")} ${asNumber2(response.file_count) ?? 0}`
36089
36280
  ];
36090
36281
  }
36091
- const checkpoints = asRecords(response.checkpoints);
36282
+ const checkpoints = asRecords2(response.checkpoints);
36092
36283
  const sections = [
36093
36284
  theme.fg("accent", `${checkpoints.length} checkpoint${checkpoints.length === 1 ? "" : "s"}`)
36094
36285
  ];
@@ -36097,8 +36288,8 @@ ${skipped.map((entry) => ` ↳ ${shortenPath2(asString(entry.file) ?? "(file)")
36097
36288
  return sections;
36098
36289
  }
36099
36290
  sections.push(checkpoints.map((checkpoint, index) => {
36100
- const name = asString(checkpoint.name) ?? `checkpoint-${index + 1}`;
36101
- const count = asNumber(checkpoint.file_count) ?? 0;
36291
+ const name = asString2(checkpoint.name) ?? `checkpoint-${index + 1}`;
36292
+ const count = asNumber2(checkpoint.file_count) ?? 0;
36102
36293
  const created = formatTimestamp(checkpoint.created_at) ?? "unknown time";
36103
36294
  return `${index + 1}. ${name} ${theme.fg("muted", `${count} file${count === 1 ? "" : "s"} · ${created}`)}`;
36104
36295
  }).join(`
@@ -36139,7 +36330,7 @@ function registerSafetyTool(pi, ctx) {
36139
36330
  previewReq.file = filePath;
36140
36331
  const preview = await callBridge(bridge, "undo_preview", previewReq, extCtx);
36141
36332
  for (const file2 of new Set(responsePaths(preview))) {
36142
- await assertExternalDirectoryPermission(extCtx, file2, "modify", {
36333
+ await assertExternalDirectoryPermission(extCtx, file2, {
36143
36334
  restrictToProjectRoot
36144
36335
  });
36145
36336
  }
@@ -36152,7 +36343,7 @@ function registerSafetyTool(pi, ctx) {
36152
36343
  if (checked.has(file2))
36153
36344
  continue;
36154
36345
  checked.add(file2);
36155
- await assertExternalDirectoryPermission(extCtx, file2, "modify", {
36346
+ await assertExternalDirectoryPermission(extCtx, file2, {
36156
36347
  restrictToProjectRoot
36157
36348
  });
36158
36349
  }
@@ -36161,7 +36352,7 @@ function registerSafetyTool(pi, ctx) {
36161
36352
  if (params.op === "restore" && params.name) {
36162
36353
  const preview = await callBridge(bridge, "checkpoint_paths", { name: params.name }, extCtx);
36163
36354
  for (const file2 of new Set(responsePaths(preview))) {
36164
- await assertExternalDirectoryPermission(extCtx, file2, "modify", {
36355
+ await assertExternalDirectoryPermission(extCtx, file2, {
36165
36356
  restrictToProjectRoot
36166
36357
  });
36167
36358
  }
@@ -36242,13 +36433,13 @@ var SearchParams2 = Type13.Object({
36242
36433
  }))
36243
36434
  });
36244
36435
  function buildSemanticSections(args, payload, theme) {
36245
- const response = asRecord2(payload);
36436
+ const response = asRecord3(payload);
36246
36437
  if (!response)
36247
36438
  return [theme.fg("muted", "No search result.")];
36248
- const status = asString(response.status) ?? "unknown";
36249
- const semanticStatus = asString(response.semantic_status) ?? status;
36250
- const interpretedAs = asString(response.interpreted_as) ?? "unknown";
36251
- const queryKind = asString(response.query_kind);
36439
+ const status = asString2(response.status) ?? "unknown";
36440
+ const semanticStatus = asString2(response.semantic_status) ?? status;
36441
+ const interpretedAs = asString2(response.interpreted_as) ?? "unknown";
36442
+ const queryKind = asString2(response.query_kind);
36252
36443
  const sections = [
36253
36444
  `${theme.fg(semanticStatus === "ready" ? "success" : "warning", `semantic: ${semanticStatus}`)} ${theme.fg("muted", `mode=${interpretedAs}${queryKind ? ` kind=${queryKind}` : ""} query=${JSON.stringify(args.query)} topK=${args.topK ?? 10}`)}`
36254
36445
  ];
@@ -36260,34 +36451,34 @@ function buildSemanticSections(args, payload, theme) {
36260
36451
  const honestyNote = semanticHonestyNote(response, theme);
36261
36452
  if (honestyNote)
36262
36453
  sections.push(honestyNote);
36263
- const results = asRecords(response.results);
36454
+ const results = asRecords2(response.results);
36264
36455
  if (status !== "ready" && results.length === 0) {
36265
- sections.push(asString(response.text) ?? theme.fg("muted", "Semantic index is not ready."));
36456
+ sections.push(asString2(response.text) ?? theme.fg("muted", "Semantic index is not ready."));
36266
36457
  return sections;
36267
36458
  }
36268
36459
  if (results.length === 0) {
36269
36460
  sections.push(theme.fg("muted", "No matches found."));
36270
36461
  return sections;
36271
36462
  }
36272
- const grouped = groupByFile(results, (result) => asString(result.file));
36463
+ const grouped = groupByFile(results, (result) => asString2(result.file));
36273
36464
  for (const [file2, fileResults] of grouped.entries()) {
36274
- const lines = [theme.fg("accent", shortenPath2(file2))];
36465
+ const lines = [theme.fg("accent", shortenPath3(file2))];
36275
36466
  fileResults.forEach((result) => {
36276
- if (asString(result.kind) === "GrepLine") {
36277
- const line = asNumber(result.line);
36278
- const column = asNumber(result.column);
36279
- const lineText = asString(result.line_text) ?? "";
36467
+ if (asString2(result.kind) === "GrepLine") {
36468
+ const line = asNumber2(result.line);
36469
+ const column = asNumber2(result.column);
36470
+ const lineText = asString2(result.line_text) ?? "";
36280
36471
  const location2 = line !== undefined ? `${line}${column !== undefined ? `:${column}` : ""}` : "?";
36281
36472
  lines.push(` ↳ ${theme.fg("muted", `line ${location2}`)} ${lineText}`);
36282
36473
  return;
36283
36474
  }
36284
- const score = asNumber(result.score);
36285
- const source = asString(result.source);
36286
- const kind = asString(result.kind) ?? "symbol";
36287
- const location = asString(result.location);
36475
+ const score = asNumber2(result.score);
36476
+ const source = asString2(result.source);
36477
+ const kind = asString2(result.kind) ?? "symbol";
36478
+ const location = asString2(result.location);
36288
36479
  if (source === "lexical") {
36289
36480
  lines.push(` ↳ ${theme.fg("muted", `[lexical match${score !== undefined ? ` — score ${score.toFixed(3)}` : ""}]`)}`);
36290
- const snippet2 = asString(result.snippet);
36481
+ const snippet2 = asString2(result.snippet);
36291
36482
  if (snippet2) {
36292
36483
  lines.push(...snippet2.split(`
36293
36484
  `).map((line) => ` ${line}`));
@@ -36295,16 +36486,16 @@ function buildSemanticSections(args, payload, theme) {
36295
36486
  return;
36296
36487
  }
36297
36488
  if (kind === "file_summary" || location === "[file summary]") {
36298
- const summary = asString(result.snippet) ?? asString(result.name) ?? "(no summary)";
36489
+ const summary = asString2(result.snippet) ?? asString2(result.name) ?? "(no summary)";
36299
36490
  lines.push(` ↳ ${summary} ${theme.fg("muted", `[file summary${score !== undefined ? ` score ${score.toFixed(3)}` : ""}]`)}`);
36300
36491
  return;
36301
36492
  }
36302
- const startLine = asNumber(result.start_line);
36303
- const endLine = asNumber(result.end_line);
36493
+ const startLine = asNumber2(result.start_line);
36494
+ const endLine = asNumber2(result.end_line);
36304
36495
  const range = startLine !== undefined ? `${startLine}${endLine && endLine !== startLine ? `-${endLine}` : ""}` : "?";
36305
- const name = asString(result.name) ?? "(unknown)";
36496
+ const name = asString2(result.name) ?? "(unknown)";
36306
36497
  lines.push(` ↳ ${name} ${theme.fg("muted", `[${kind}] lines ${range}${score !== undefined ? ` score ${score.toFixed(3)}` : ""}`)}`);
36307
- const snippet = asString(result.snippet);
36498
+ const snippet = asString2(result.snippet);
36308
36499
  if (snippet) {
36309
36500
  lines.push(...snippet.split(`
36310
36501
  `).map((line) => ` ${line}`));
@@ -36416,11 +36607,11 @@ function buildWorkflowHints(opts) {
36416
36607
  }
36417
36608
  if (hasBgBash) {
36418
36609
  sections.push([
36419
- `**Long-running commands** (builds, installs, full test suites): \`${bashName}({ background: true })\` returns a \`task_id\` immediately. Then do ONE of these:`,
36420
- "1. Keep working on something independent of the result.",
36421
- "2. End your turn a completion reminder with the result arrives automatically.",
36422
- `3. Need to react to a specific output line early? Register \`bash_watch({ task_id, pattern, background: true })\`, then end your turn — it pings you the moment the pattern appears.`,
36423
- `\`bash_status({ task_id })\` is for inspecting output AFTER the reminder arrives, or one quick look at a live task — never call it repeatedly to wait: the reminder already delivers the result, and each poll wastes a turn. Sync \`bash_watch\` (without \`background: true\`) blocks your turn and locks the user out reserve it for waits of a few seconds (a dev server printing its ready line), never for builds or test suites.`
36610
+ `**Long-running commands** (builds, installs, full test suites): \`${bashName}({ background: true })\` returns a \`task_id\` immediately.`,
36611
+ "1. Nothing else useful to do (a build/test/validation whose result is the next thing you need) → sync `bash_watch` to block until it exits (pass a longer timeout_ms for long commands; the user can interrupt).",
36612
+ "2. Useful parallel work available → end your turn; the completion reminder delivers the result. (Or spawn a subagent for the side work.)",
36613
+ "3. Want to react to a specific early output line async `bash_watch` (background:true + pattern).",
36614
+ "Never loop `bash_status` to wait — it's a one-shot inspector, not a wait primitive."
36424
36615
  ].join(`
36425
36616
  `));
36426
36617
  sections.push(`**PTY / interactive commands**: PTY mode is for interactive REPLs and terminal apps (python, node, bash itself, vim). Start with \`${bashName}({ command: "python", pty: true, background: true })\`, read the screen with \`bash_status({ task_id, output_mode: "screen" })\`, and send input with \`bash_write({ task_id, input: "..." })\`.`);
@@ -36535,7 +36726,7 @@ function isConfigureWarning(value) {
36535
36726
  if (!value || typeof value !== "object" || Array.isArray(value))
36536
36727
  return false;
36537
36728
  const warning = value;
36538
- return (warning.kind === "formatter_not_installed" || warning.kind === "checker_not_installed" || warning.kind === "lsp_binary_missing") && typeof warning.hint === "string";
36729
+ return (warning.kind === "formatter_not_installed" || warning.kind === "checker_not_installed" || warning.kind === "lsp_binary_missing" || warning.kind === "config_parse_failed") && typeof warning.hint === "string";
36539
36730
  }
36540
36731
  function coerceConfigureWarnings(warnings) {
36541
36732
  return warnings.filter(isConfigureWarning);
@@ -36545,6 +36736,18 @@ function drainPendingEagerWarnings(projectRoot) {
36545
36736
  pendingEagerWarnings.delete(projectRoot);
36546
36737
  return pending;
36547
36738
  }
36739
+ function enqueueConfigParseWarnings(projectRoot, errors3) {
36740
+ if (!projectRoot || errors3.length === 0)
36741
+ return;
36742
+ const pending = pendingEagerWarnings.get(projectRoot) ?? [];
36743
+ for (const entry of errors3) {
36744
+ const hint = formatConfigParseFailureMessage(entry.path, entry.message);
36745
+ if (!pending.some((item) => item.kind === "config_parse_failed" && item.hint === hint)) {
36746
+ pending.push({ kind: "config_parse_failed", hint });
36747
+ }
36748
+ }
36749
+ pendingEagerWarnings.set(projectRoot, pending);
36750
+ }
36548
36751
  function shouldPrepareOnnxRuntime(config2) {
36549
36752
  const isFastembedSemanticBackend = (config2.semantic?.backend ?? "fastembed") === "fastembed";
36550
36753
  return config2.semantic_search === true && isFastembedSemanticBackend;
@@ -36653,6 +36856,7 @@ async function src_default(pi) {
36653
36856
  }
36654
36857
  await ensureStorageMigrated({ harness: "pi", binaryPath, logger: bridgeLogger });
36655
36858
  const config2 = loadAftConfig(process.cwd());
36859
+ enqueueConfigParseWarnings(process.cwd(), getConfigLoadErrors());
36656
36860
  const storageDir = resolveCortexKitStorageRoot();
36657
36861
  let onnxRuntimePromise = null;
36658
36862
  if (shouldPrepareOnnxRuntime(config2)) {
@@ -36661,30 +36865,7 @@ async function src_default(pi) {
36661
36865
  return null;
36662
36866
  });
36663
36867
  }
36664
- const configOverrides = {};
36665
- if (config2.format_on_edit !== undefined)
36666
- configOverrides.format_on_edit = config2.format_on_edit;
36667
- if (config2.formatter_timeout_secs !== undefined)
36668
- configOverrides.formatter_timeout_secs = config2.formatter_timeout_secs;
36669
- if (config2.validate_on_edit !== undefined)
36670
- configOverrides.validate_on_edit = config2.validate_on_edit;
36671
- if (config2.formatter !== undefined)
36672
- configOverrides.formatter = config2.formatter;
36673
- if (config2.checker !== undefined)
36674
- configOverrides.checker = config2.checker;
36675
- configOverrides.restrict_to_project_root = config2.restrict_to_project_root ?? false;
36676
- if (config2.search_index !== undefined)
36677
- configOverrides.search_index = config2.search_index;
36678
- if (config2.semantic_search !== undefined)
36679
- configOverrides.semantic_search = config2.semantic_search;
36680
- Object.assign(configOverrides, resolveExperimentalConfigForConfigure(config2));
36681
- Object.assign(configOverrides, resolveLspConfigForConfigure(config2));
36682
- if (config2.semantic !== undefined)
36683
- configOverrides.semantic = config2.semantic;
36684
- if (config2.inspect !== undefined)
36685
- configOverrides.inspect = config2.inspect;
36686
- if (config2.max_callgraph_files !== undefined)
36687
- configOverrides.max_callgraph_files = config2.max_callgraph_files;
36868
+ const configOverrides = resolveProjectOverridesForConfigure(config2);
36688
36869
  if (config2.url_fetch_allow_private !== undefined)
36689
36870
  configOverrides.url_fetch_allow_private = config2.url_fetch_allow_private;
36690
36871
  configOverrides.storage_dir = storageDir;
@@ -36892,12 +37073,25 @@ ${lines}
36892
37073
  return { content, details: event.details, isError: event.isError };
36893
37074
  });
36894
37075
  pi.on("turn_end", async (_event, extCtx) => {
37076
+ const sessionID = resolveSessionId(extCtx);
36895
37077
  await handleTurnEndBgCompletions({
36896
37078
  ctx,
36897
37079
  directory: extCtx.cwd,
36898
- sessionID: resolveSessionId(extCtx),
37080
+ sessionID,
36899
37081
  runtime: pi
36900
37082
  });
37083
+ const bridge = pool.getActiveBridgeForRoot(extCtx.cwd);
37084
+ if (bridge && sessionID) {
37085
+ handleConfigureWarningsForSession({
37086
+ projectRoot: extCtx.cwd,
37087
+ sessionId: sessionID,
37088
+ client: pi,
37089
+ bridge,
37090
+ warnings: [],
37091
+ storageDir,
37092
+ pluginVersion: PLUGIN_VERSION
37093
+ });
37094
+ }
36901
37095
  });
36902
37096
  pi.on("input", (_event, extCtx) => {
36903
37097
  signalSyncWatchAbort(resolveSessionId(extCtx));
@@ -36926,6 +37120,7 @@ ${lines}
36926
37120
  log2(`AFT extension ready (surface=${config2.tool_surface ?? "recommended"})`);
36927
37121
  }
36928
37122
  var __test__2 = {
37123
+ enqueueConfigParseWarnings,
36929
37124
  bridgeDirectoryFromCallback,
36930
37125
  resolveToolSurface,
36931
37126
  handleConfigureWarningsForSession,