@cortexkit/aft-opencode 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
@@ -12101,6 +12101,21 @@ class BridgeReplacedDuringVersionCheck extends Error {
12101
12101
  }
12102
12102
  }
12103
12103
 
12104
+ class BridgeTransportTimeoutError extends Error {
12105
+ command;
12106
+ timeoutMs;
12107
+ code = "transport_timeout";
12108
+ constructor(command, timeoutMs, message) {
12109
+ super(message);
12110
+ this.command = command;
12111
+ this.timeoutMs = timeoutMs;
12112
+ this.name = "BridgeTransportTimeoutError";
12113
+ }
12114
+ }
12115
+ function isBridgeTransportTimeout(err) {
12116
+ return err instanceof Error && err.code === "transport_timeout";
12117
+ }
12118
+
12104
12119
  class BinaryBridge {
12105
12120
  static RESTART_RESET_MS = 5 * 60 * 1000;
12106
12121
  static STDERR_TAIL_MAX = 20;
@@ -12328,7 +12343,7 @@ class BinaryBridge {
12328
12343
  } else {
12329
12344
  this.warnVia(timeoutMsg2);
12330
12345
  }
12331
- entry.reject(new Error(`${this.errorPrefix} Request "${command}" (id=${id}) timed out after ${effectiveTimeoutMs}ms`));
12346
+ entry.reject(new BridgeTransportTimeoutError(command, effectiveTimeoutMs, `${this.errorPrefix} Request "${command}" (id=${id}) timed out after ${effectiveTimeoutMs}ms`));
12332
12347
  return;
12333
12348
  }
12334
12349
  const childActiveSinceRequest = this.lastChildActivityAt > requestSentAt;
@@ -12864,6 +12879,220 @@ class BinaryBridge {
12864
12879
  }
12865
12880
  }
12866
12881
  }
12882
+ // ../aft-bridge/dist/callgraph-format.js
12883
+ import { homedir as homedir3 } from "node:os";
12884
+ var PLAIN_CALLGRAPH_THEME = {
12885
+ fg: (_role, text) => text
12886
+ };
12887
+ function asRecord(value) {
12888
+ if (!value || typeof value !== "object" || Array.isArray(value))
12889
+ return;
12890
+ return value;
12891
+ }
12892
+ function asRecords(value) {
12893
+ return Array.isArray(value) ? value.map(asRecord).filter(Boolean) : [];
12894
+ }
12895
+ function asString(value) {
12896
+ return typeof value === "string" ? value : undefined;
12897
+ }
12898
+ function asNumber(value) {
12899
+ return typeof value === "number" && Number.isFinite(value) ? value : undefined;
12900
+ }
12901
+ function asBoolean(value) {
12902
+ return typeof value === "boolean" ? value : undefined;
12903
+ }
12904
+ function shortenPath(path2) {
12905
+ const home = homedir3();
12906
+ if (path2.startsWith(home))
12907
+ return `~${path2.slice(home.length)}`;
12908
+ return path2;
12909
+ }
12910
+ function joinNonEmpty(parts, separator = " · ") {
12911
+ return parts.filter((part) => Boolean(part && part.length > 0)).join(separator);
12912
+ }
12913
+ function treeLine(depth, text) {
12914
+ return `${" ".repeat(depth)}${depth === 0 ? "" : "↳ "}${text}`;
12915
+ }
12916
+ function nameMatchEdgeMarker(record, theme) {
12917
+ return asString(record.resolved_by) === "name_match" ? ` ${theme.fg("warning", "~")}` : "";
12918
+ }
12919
+ function renderCallTreeNode(node, depth, lines, theme) {
12920
+ const name = asString(node.name) ?? "(unknown)";
12921
+ const file = shortenPath(asString(node.file) ?? "(unknown file)");
12922
+ const line = asNumber(node.line);
12923
+ const unresolved = node.resolved === false ? ` ${theme.fg("warning", "[unresolved]")}` : "";
12924
+ const nameMatch = nameMatchEdgeMarker(node, theme);
12925
+ const location = line !== undefined ? `[${file}:${line}]` : `[${file}]`;
12926
+ lines.push(treeLine(depth, `${name} ${location}${unresolved}${nameMatch}`));
12927
+ asRecords(node.children).forEach((child) => {
12928
+ renderCallTreeNode(child, depth + 1, lines, theme);
12929
+ });
12930
+ }
12931
+ function depthWarning(response, theme, depthField = "depth_limited", truncatedField = "truncated") {
12932
+ const limited = asBoolean(response[depthField]);
12933
+ const truncated = asNumber(response[truncatedField]) ?? 0;
12934
+ if (!limited && truncated === 0)
12935
+ return "";
12936
+ const detail = truncated > 0 ? `, ${truncated} truncated` : "";
12937
+ return theme.fg("warning", `(depth limited${detail})`);
12938
+ }
12939
+ function renderTracePath(path2, index, lines, theme) {
12940
+ lines.push(`Path ${index + 1}`);
12941
+ asRecords(path2.hops).forEach((hop, hopIndex) => {
12942
+ const symbol = asString(hop.symbol) ?? "(unknown)";
12943
+ const file = shortenPath(asString(hop.file) ?? "(unknown file)");
12944
+ const line = asNumber(hop.line);
12945
+ const entry = hop.is_entry_point === true ? " [entry]" : "";
12946
+ const nameMatch = nameMatchEdgeMarker(hop, theme);
12947
+ lines.push(treeLine(hopIndex + 1, `${symbol}${entry} ${line !== undefined ? `[${file}:${line}]` : `[${file}]`}${nameMatch}`));
12948
+ });
12949
+ }
12950
+ function renderCallersGroupLines(group, theme) {
12951
+ const file = shortenPath(asString(group.file) ?? "(unknown file)");
12952
+ const lines = [theme.fg("accent", file)];
12953
+ const callers = asRecords(group.callers);
12954
+ const bySymbolProvenance = new Map;
12955
+ for (const caller of callers) {
12956
+ const symbol = asString(caller.symbol) ?? "(unknown)";
12957
+ const provenanceKey = asString(caller.resolved_by) === "name_match" ? `${symbol}\x00name_match` : `${symbol}\x00exact`;
12958
+ const line = asNumber(caller.line);
12959
+ const bucket = bySymbolProvenance.get(provenanceKey) ?? [];
12960
+ if (line !== undefined)
12961
+ bucket.push(line);
12962
+ bySymbolProvenance.set(provenanceKey, bucket);
12963
+ }
12964
+ const keys = [...bySymbolProvenance.keys()].sort((a, b) => a.localeCompare(b));
12965
+ for (const key of keys) {
12966
+ const symbol = key.split("\x00")[0] ?? "(unknown)";
12967
+ const isNameMatch = key.endsWith("\x00name_match");
12968
+ const lineNums = (bySymbolProvenance.get(key) ?? []).sort((a, b) => a - b);
12969
+ const linePart = lineNums.length > 0 ? lineNums.map(String).join(", ") : "?";
12970
+ const marker = isNameMatch ? ` ${theme.fg("warning", "~")}` : "";
12971
+ lines.push(` ↳ ${symbol}:${linePart}${marker}`);
12972
+ }
12973
+ return lines;
12974
+ }
12975
+ function formatCallgraphSections(op, response, theme = PLAIN_CALLGRAPH_THEME) {
12976
+ const record = asRecord(response);
12977
+ if (!record)
12978
+ return [theme.fg("muted", "No navigation result.")];
12979
+ if (op === "call_tree") {
12980
+ const lines = [];
12981
+ renderCallTreeNode(record, 0, lines, theme);
12982
+ const warning = depthWarning(record, theme);
12983
+ if (warning)
12984
+ lines.push(warning);
12985
+ return lines.length > 0 ? lines : [theme.fg("muted", "No call tree available.")];
12986
+ }
12987
+ if (op === "callers") {
12988
+ const groups = asRecords(record.callers);
12989
+ const warning = depthWarning(record, theme);
12990
+ const total = asNumber(record.total_callers) ?? 0;
12991
+ const sections2 = [
12992
+ joinNonEmpty([
12993
+ theme.fg("success", `${total} caller${total === 1 ? "" : "s"}`),
12994
+ theme.fg("muted", `${groups.length} file group${groups.length === 1 ? "" : "s"}`),
12995
+ warning
12996
+ ])
12997
+ ];
12998
+ groups.forEach((group) => {
12999
+ sections2.push(renderCallersGroupLines(group, theme).join(`
13000
+ `));
13001
+ });
13002
+ return sections2;
13003
+ }
13004
+ if (op === "trace_to_symbol") {
13005
+ const path2 = asRecords(record.path);
13006
+ const complete = asBoolean(record.complete);
13007
+ const reason = asString(record.reason);
13008
+ if (path2.length === 0) {
13009
+ const prefix = complete === false ? theme.fg("warning", "No complete path") : theme.fg("muted", "No path");
13010
+ return [`${prefix}${reason ? ` (${reason})` : ""}`];
13011
+ }
13012
+ const lines = [theme.fg("success", `${path2.length} hop${path2.length === 1 ? "" : "s"}`)];
13013
+ path2.forEach((hop, index) => {
13014
+ const symbol = asString(hop.symbol) ?? "(unknown)";
13015
+ const file = shortenPath(asString(hop.file) ?? "(unknown file)");
13016
+ const line = asNumber(hop.line);
13017
+ const nameMatch = nameMatchEdgeMarker(hop, theme);
13018
+ lines.push(treeLine(index + 1, `${symbol} ${line !== undefined ? `[${file}:${line}]` : `[${file}]`}${nameMatch}`));
13019
+ });
13020
+ return lines;
13021
+ }
13022
+ if (op === "trace_to") {
13023
+ const paths = asRecords(record.paths);
13024
+ const warning = depthWarning(record, theme, "max_depth_reached", "truncated_paths");
13025
+ const totalPaths = asNumber(record.total_paths) ?? paths.length;
13026
+ const entryPoints = asNumber(record.entry_points_found) ?? 0;
13027
+ const sections2 = [
13028
+ joinNonEmpty([
13029
+ theme.fg("success", `${totalPaths} path${totalPaths === 1 ? "" : "s"}`),
13030
+ theme.fg("muted", `${entryPoints} entry point${entryPoints === 1 ? "" : "s"}`),
13031
+ warning
13032
+ ])
13033
+ ];
13034
+ if (paths.length === 0)
13035
+ sections2.push(theme.fg("muted", "No entry paths found."));
13036
+ paths.forEach((path2, index) => {
13037
+ const lines = [];
13038
+ renderTracePath(path2, index, lines, theme);
13039
+ sections2.push(lines.join(`
13040
+ `));
13041
+ });
13042
+ return sections2;
13043
+ }
13044
+ if (op === "impact") {
13045
+ const callers = asRecords(record.callers);
13046
+ const warning = depthWarning(record, theme);
13047
+ const totalAffected = asNumber(record.total_affected) ?? callers.length;
13048
+ const affectedFiles = asNumber(record.affected_files) ?? 0;
13049
+ const sections2 = [
13050
+ joinNonEmpty([
13051
+ theme.fg("warning", `${totalAffected} affected call site${totalAffected === 1 ? "" : "s"}`),
13052
+ theme.fg("muted", `${affectedFiles} file${affectedFiles === 1 ? "" : "s"}`),
13053
+ warning
13054
+ ])
13055
+ ];
13056
+ if (callers.length === 0)
13057
+ sections2.push(theme.fg("muted", "No impacted callers found."));
13058
+ callers.forEach((caller) => {
13059
+ const file = shortenPath(asString(caller.caller_file) ?? "(unknown file)");
13060
+ const symbol = asString(caller.caller_symbol) ?? "(unknown)";
13061
+ const line = asNumber(caller.line) ?? 0;
13062
+ const entry = caller.is_entry_point === true ? ` ${theme.fg("warning", "[entry]")}` : "";
13063
+ const nameMatch = nameMatchEdgeMarker(caller, theme);
13064
+ const expression = asString(caller.call_expression);
13065
+ const params = Array.isArray(caller.parameters) ? caller.parameters.map(String).join(", ") : "";
13066
+ sections2.push([
13067
+ `${theme.fg("accent", file)}:${line}`,
13068
+ ` ↳ ${symbol}${entry}${nameMatch}`,
13069
+ expression ? ` ${theme.fg("muted", expression)}` : undefined,
13070
+ params ? ` ${theme.fg("muted", `params: ${params}`)}` : undefined
13071
+ ].filter(Boolean).join(`
13072
+ `));
13073
+ });
13074
+ return sections2;
13075
+ }
13076
+ const hops = asRecords(record.hops);
13077
+ const sections = [
13078
+ joinNonEmpty([
13079
+ theme.fg("success", `${hops.length} hop${hops.length === 1 ? "" : "s"}`),
13080
+ asBoolean(record.depth_limited) ? theme.fg("warning", "(depth limited)") : undefined
13081
+ ])
13082
+ ];
13083
+ if (hops.length === 0)
13084
+ sections.push(theme.fg("muted", "No data-flow hops found."));
13085
+ hops.forEach((hop, index) => {
13086
+ const file = shortenPath(asString(hop.file) ?? "(unknown file)");
13087
+ const symbol = asString(hop.symbol) ?? "(unknown)";
13088
+ const variable = asString(hop.variable) ?? "(unknown)";
13089
+ const line = asNumber(hop.line) ?? 0;
13090
+ const approximate = hop.approximate === true ? ` ${theme.fg("warning", "[approx]")}` : "";
13091
+ const nameMatch = nameMatchEdgeMarker(hop, theme);
13092
+ sections.push(treeLine(index, `${variable} ${theme.fg("muted", `${asString(hop.flow_type) ?? "flow"}`)} ${symbol} [${file}:${line}]${approximate}${nameMatch}`));
13093
+ });
13094
+ return sections;
13095
+ }
12867
13096
  // ../aft-bridge/dist/coerce.js
12868
13097
  function coerceStringArray(value) {
12869
13098
  if (Array.isArray(value)) {
@@ -12900,7 +13129,7 @@ function isEmptyParam(value) {
12900
13129
  import { spawnSync } from "node:child_process";
12901
13130
  import { createHash, randomUUID } from "node:crypto";
12902
13131
  import { chmodSync, closeSync, copyFileSync, createWriteStream, existsSync, mkdirSync, openSync, readFileSync, renameSync, rmSync, statSync, unlinkSync, writeSync } from "node:fs";
12903
- import { homedir as homedir3 } from "node:os";
13132
+ import { homedir as homedir4 } from "node:os";
12904
13133
  import { join as join3 } from "node:path";
12905
13134
  import { Readable } from "node:stream";
12906
13135
  import { pipeline } from "node:stream/promises";
@@ -12958,10 +13187,10 @@ function isExpectedCachedBinary(binaryPath, tag) {
12958
13187
  function getCacheDir() {
12959
13188
  if (process.platform === "win32") {
12960
13189
  const localAppData = process.env.LOCALAPPDATA || process.env.APPDATA;
12961
- const base2 = localAppData || join3(homedir3(), "AppData", "Local");
13190
+ const base2 = localAppData || join3(homedir4(), "AppData", "Local");
12962
13191
  return join3(base2, "aft", "bin");
12963
13192
  }
12964
- const base = process.env.XDG_CACHE_HOME || join3(homedir3(), ".cache");
13193
+ const base = process.env.XDG_CACHE_HOME || join3(homedir4(), ".cache");
12965
13194
  return join3(base, "aft", "bin");
12966
13195
  }
12967
13196
  function getBinaryName() {
@@ -13203,7 +13432,7 @@ function formatEditSummary(data) {
13203
13432
  if (data.created === true) {
13204
13433
  let s2 = `Created file (${counts}).`;
13205
13434
  if (data.formatted)
13206
- s2 += " Auto-formatted.";
13435
+ s2 += formatAutoFormattedSuffix(data);
13207
13436
  return s2;
13208
13437
  }
13209
13438
  let detail = counts;
@@ -13214,9 +13443,21 @@ function formatEditSummary(data) {
13214
13443
  }
13215
13444
  let s = `Edited (${detail}).`;
13216
13445
  if (data.formatted)
13217
- s += " Auto-formatted.";
13446
+ s += formatAutoFormattedSuffix(data);
13218
13447
  return s;
13219
13448
  }
13449
+ function formatAutoFormattedSuffix(data) {
13450
+ const reflowText = data.reformatted?.text;
13451
+ if (typeof reflowText === "string" && reflowText.length > 0) {
13452
+ return `
13453
+ Auto-formatted — the formatter reflowed your edit. On disk now:
13454
+ ${reflowText}`;
13455
+ }
13456
+ if (data.reformatted?.extensive === true) {
13457
+ return " Auto-formatted — extensive reflow; re-read the file before your next anchored edit.";
13458
+ }
13459
+ return " Auto-formatted.";
13460
+ }
13220
13461
  // ../aft-bridge/dist/jsonc.js
13221
13462
  function stripJsoncSymbols(value) {
13222
13463
  if (Array.isArray(value)) {
@@ -13234,7 +13475,7 @@ function stripJsoncSymbols(value) {
13234
13475
  // ../aft-bridge/dist/migration.js
13235
13476
  import { spawnSync as spawnSync2 } from "node:child_process";
13236
13477
  import { existsSync as existsSync4, mkdirSync as mkdirSync4 } from "node:fs";
13237
- import { homedir as homedir5, tmpdir } from "node:os";
13478
+ import { homedir as homedir6, tmpdir } from "node:os";
13238
13479
  import { dirname as dirname2, join as join6 } from "node:path";
13239
13480
 
13240
13481
  // ../aft-bridge/dist/paths.js
@@ -13291,7 +13532,7 @@ function markAnnouncementSeen(storageRoot, harness, currentVersion) {
13291
13532
  import { execSync } from "node:child_process";
13292
13533
  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";
13293
13534
  import { createRequire as createRequire2 } from "node:module";
13294
- import { homedir as homedir4 } from "node:os";
13535
+ import { homedir as homedir5 } from "node:os";
13295
13536
  import { join as join5 } from "node:path";
13296
13537
  var ensureBinaryForResolver = ensureBinary;
13297
13538
  function copyToVersionedCache(npmBinaryPath, knownVersion) {
@@ -13333,7 +13574,7 @@ function normalizeBareVersion(version) {
13333
13574
  return version.startsWith("v") ? version.slice(1) : version;
13334
13575
  }
13335
13576
  function homeDirFromEnv(env) {
13336
- return (process.platform === "win32" ? env.USERPROFILE || env.HOME : env.HOME) || homedir4();
13577
+ return (process.platform === "win32" ? env.USERPROFILE || env.HOME : env.HOME) || homedir5();
13337
13578
  }
13338
13579
  function cacheDirFromEnv(env) {
13339
13580
  if (process.platform === "win32") {
@@ -13515,8 +13756,8 @@ function dataHome() {
13515
13756
  }
13516
13757
  function homeDir() {
13517
13758
  if (process.platform === "win32")
13518
- return process.env.USERPROFILE || process.env.HOME || homedir5();
13519
- return process.env.HOME || homedir5();
13759
+ return process.env.USERPROFILE || process.env.HOME || homedir6();
13760
+ return process.env.HOME || homedir6();
13520
13761
  }
13521
13762
  function resolveLegacyStorageRoot(harness) {
13522
13763
  if (harness === "pi")
@@ -13606,13 +13847,13 @@ async function ensureStorageMigrated(opts) {
13606
13847
  }
13607
13848
  // ../aft-bridge/dist/npm-resolver.js
13608
13849
  import { readdirSync, statSync as statSync2 } from "node:fs";
13609
- import { homedir as homedir6 } from "node:os";
13850
+ import { homedir as homedir7 } from "node:os";
13610
13851
  import { delimiter, dirname as dirname3, isAbsolute as isAbsolute2, join as join7 } from "node:path";
13611
13852
  function defaultDeps() {
13612
13853
  return {
13613
13854
  platform: process.platform,
13614
13855
  env: process.env,
13615
- home: homedir6(),
13856
+ home: homedir7(),
13616
13857
  execPath: process.execPath
13617
13858
  };
13618
13859
  }
@@ -15379,9 +15620,6 @@ function write(level, message, data, sessionId) {
15379
15620
  function log2(message, data) {
15380
15621
  write("INFO", message, data);
15381
15622
  }
15382
- function debug(message, data) {
15383
- write("DEBUG", message, data);
15384
- }
15385
15623
  function warn2(message, data) {
15386
15624
  write("WARN", message, data);
15387
15625
  }
@@ -15391,9 +15629,6 @@ function error2(message, data) {
15391
15629
  function sessionLog(sessionId, message, data) {
15392
15630
  write("INFO", message, data, sessionId);
15393
15631
  }
15394
- function sessionDebug(sessionId, message, data) {
15395
- write("DEBUG", message, data, sessionId);
15396
- }
15397
15632
  function sessionWarn(sessionId, message, data) {
15398
15633
  write("WARN", message, data, sessionId);
15399
15634
  }
@@ -15487,84 +15722,6 @@ async function resolvePromptContext(client, sessionId) {
15487
15722
  return result;
15488
15723
  }
15489
15724
 
15490
- // src/shared/live-server-client.ts
15491
- import { createOpencodeClient } from "@opencode-ai/sdk";
15492
- var clientCache = new Map;
15493
- function cacheKey(serverUrl, directory) {
15494
- return `${serverUrl}|${directory}`;
15495
- }
15496
- function normalizeServerUrl(serverUrl) {
15497
- try {
15498
- return new URL(serverUrl).toString();
15499
- } catch {
15500
- return serverUrl;
15501
- }
15502
- }
15503
- function serverAuthHeaders() {
15504
- const password = process.env.OPENCODE_SERVER_PASSWORD;
15505
- if (!password)
15506
- return;
15507
- const username = process.env.OPENCODE_SERVER_USERNAME ?? "opencode";
15508
- return {
15509
- Authorization: `Basic ${Buffer.from(`${username}:${password}`).toString("base64")}`
15510
- };
15511
- }
15512
- function getLiveServerClient(serverUrl, directory) {
15513
- const key = cacheKey(serverUrl, directory);
15514
- const cached = clientCache.get(key);
15515
- if (cached)
15516
- return cached;
15517
- const client = createOpencodeClient({
15518
- baseUrl: serverUrl,
15519
- directory,
15520
- headers: serverAuthHeaders(),
15521
- fetch: globalThis.fetch
15522
- });
15523
- clientCache.set(key, client);
15524
- return client;
15525
- }
15526
- var liveServerWakeAvailableByServerUrl = new Map;
15527
- var legacyLiveServerWakeAvailable = false;
15528
- async function probeServerReachable(serverUrl, timeoutMs = 1500) {
15529
- if (!serverUrl)
15530
- return false;
15531
- const normalizedServerUrl = normalizeServerUrl(serverUrl);
15532
- const controller = new AbortController;
15533
- const timer = setTimeout(() => controller.abort(), timeoutMs);
15534
- let reachable = false;
15535
- try {
15536
- const probeUrl = new URL("/session", serverUrl).toString();
15537
- const res = await globalThis.fetch(probeUrl, {
15538
- method: "GET",
15539
- headers: serverAuthHeaders(),
15540
- signal: controller.signal
15541
- });
15542
- reachable = res.ok || res.status === 401 || res.status === 403;
15543
- } catch {
15544
- reachable = false;
15545
- } finally {
15546
- clearTimeout(timer);
15547
- liveServerWakeAvailableByServerUrl.set(normalizedServerUrl, reachable);
15548
- }
15549
- return reachable;
15550
- }
15551
- function setLiveServerWakeAvailable(serverUrlOrAvailable, available) {
15552
- if (typeof serverUrlOrAvailable === "boolean") {
15553
- legacyLiveServerWakeAvailable = serverUrlOrAvailable;
15554
- return;
15555
- }
15556
- if (!serverUrlOrAvailable) {
15557
- legacyLiveServerWakeAvailable = available ?? false;
15558
- return;
15559
- }
15560
- liveServerWakeAvailableByServerUrl.set(normalizeServerUrl(serverUrlOrAvailable), available ?? false);
15561
- }
15562
- function useLiveServerWake(serverUrl) {
15563
- if (!serverUrl)
15564
- return legacyLiveServerWakeAvailable;
15565
- return liveServerWakeAvailableByServerUrl.get(normalizeServerUrl(serverUrl)) ?? false;
15566
- }
15567
-
15568
15725
  // src/bg-notifications.ts
15569
15726
  function hashReminder(text) {
15570
15727
  return createHash3("sha256").update(text).digest("hex").slice(0, 16);
@@ -15791,15 +15948,15 @@ async function triggerWakeIfPending(drainContext, skipDrain, includeDeferredComp
15791
15948
  directory: drainContext.directory,
15792
15949
  attempt: state.wakeRetryAttempts + 1
15793
15950
  });
15794
- throw new Error("no wake transport available: live-server unreachable and input.client absent");
15951
+ throw new Error("in-process wake client unavailable: input.client absent");
15795
15952
  }
15796
15953
  return drainContext.client;
15797
15954
  };
15798
- const sendPrompt = async (client, clientPath) => {
15799
- if (typeof client.session?.promptAsync !== "function") {
15800
- throw new Error(`wake client.session.promptAsync is unavailable (path=${clientPath})`);
15955
+ const sendPrompt = async (client2) => {
15956
+ if (typeof client2.session?.promptAsync !== "function") {
15957
+ throw new Error("wake client.session.promptAsync is unavailable");
15801
15958
  }
15802
- const promptContext = await resolvePromptContext(client, drainContext.sessionID);
15959
+ const promptContext = await resolvePromptContext(client2, drainContext.sessionID);
15803
15960
  const body = {
15804
15961
  noReply: false,
15805
15962
  parts: [{ type: "text", text: reminder, synthetic: true }]
@@ -15822,7 +15979,6 @@ async function triggerWakeIfPending(drainContext, skipDrain, includeDeferredComp
15822
15979
  directory: drainContext.directory,
15823
15980
  reminder_sha256: hashReminder(reminder),
15824
15981
  reminder_chars: reminder.length,
15825
- wake_client_path: clientPath,
15826
15982
  prompt_context: promptContext ? {
15827
15983
  agent: promptContext.agent,
15828
15984
  model: promptContext.model ? {
@@ -15837,18 +15993,16 @@ async function triggerWakeIfPending(drainContext, skipDrain, includeDeferredComp
15837
15993
  ...wakeMeta
15838
15994
  });
15839
15995
  try {
15840
- await client.session.promptAsync({
15996
+ await client2.session.promptAsync({
15841
15997
  path: { id: drainContext.sessionID },
15842
15998
  body
15843
15999
  });
15844
16000
  } catch (err) {
15845
- const logPromptError = clientPath === "live-server" ? sessionDebug : sessionWarn;
15846
- logPromptError(drainContext.sessionID, `${LOG_PREFIX} wake promptAsync error`, {
16001
+ sessionWarn(drainContext.sessionID, `${LOG_PREFIX} wake promptAsync error`, {
15847
16002
  event: "bash_completion_wake_prompt_async_error",
15848
16003
  delivery_id: deliveryID2,
15849
16004
  attempt: state.wakeRetryAttempts + 1,
15850
16005
  task_ids: taskIDs,
15851
- wake_client_path: clientPath,
15852
16006
  error: err instanceof Error ? err.message : String(err)
15853
16007
  });
15854
16008
  throw err;
@@ -15857,38 +16011,12 @@ async function triggerWakeIfPending(drainContext, skipDrain, includeDeferredComp
15857
16011
  event: "bash_completion_wake_prompt_async_ok",
15858
16012
  delivery_id: deliveryID2,
15859
16013
  attempt: state.wakeRetryAttempts + 1,
15860
- task_ids: taskIDs,
15861
- wake_client_path: clientPath
16014
+ task_ids: taskIDs
15862
16015
  });
15863
16016
  return deliveryID2;
15864
16017
  };
15865
- if (useLiveServerWake(drainContext.serverUrl) && drainContext.serverUrl) {
15866
- try {
15867
- const liveClient = getLiveServerClient(drainContext.serverUrl, drainContext.directory);
15868
- const deliveryID2 = await sendPrompt(liveClient, "live-server");
15869
- await ackCompletions(drainContext, deliveredCompletions, deliveryID2);
15870
- return;
15871
- } catch (err) {
15872
- setLiveServerWakeAvailable(drainContext.serverUrl, false);
15873
- sessionDebug(drainContext.sessionID, `${LOG_PREFIX} live-server wake failed; falling back`, {
15874
- event: "bash_completion_wake_live_server_fallback",
15875
- task_ids: taskIDs,
15876
- directory: drainContext.directory,
15877
- server_url: drainContext.serverUrl,
15878
- attempt: state.wakeRetryAttempts + 1,
15879
- error: err instanceof Error ? err.message : String(err)
15880
- });
15881
- const fallbackClient2 = getInProcessClient();
15882
- const deliveryID2 = await sendPrompt(fallbackClient2, "in-process-fallback");
15883
- state.retryDelayMs = null;
15884
- state.wakeRetryAttempts = 0;
15885
- state.wakeHardStopped = false;
15886
- await ackCompletions(drainContext, deliveredCompletions, deliveryID2);
15887
- return;
15888
- }
15889
- }
15890
- const fallbackClient = getInProcessClient();
15891
- const deliveryID = await sendPrompt(fallbackClient, "in-process-fallback");
16018
+ const client = getInProcessClient();
16019
+ const deliveryID = await sendPrompt(client);
15892
16020
  await ackCompletions(drainContext, deliveredCompletions, deliveryID);
15893
16021
  }, (err, hardStopped) => {
15894
16022
  sessionWarn(drainContext.sessionID, hardStopped ? `${LOG_PREFIX} wake send failed ${MAX_WAKE_SEND_ATTEMPTS} times; stopping retries: ${err instanceof Error ? err.message : String(err)}` : `${LOG_PREFIX} wake send failed: ${err instanceof Error ? err.message : String(err)}`);
@@ -16271,7 +16399,7 @@ function formatDuration(completion) {
16271
16399
 
16272
16400
  // src/config.ts
16273
16401
  import { existsSync as existsSync6, readFileSync as readFileSync4, renameSync as renameSync4, unlinkSync as unlinkSync4, writeFileSync as writeFileSync3 } from "node:fs";
16274
- import { homedir as homedir7 } from "node:os";
16402
+ import { homedir as homedir8 } from "node:os";
16275
16403
  import { join as join10 } from "node:path";
16276
16404
  var import_comment_json = __toESM(require_src2(), 1);
16277
16405
 
@@ -29922,6 +30050,8 @@ var AftConfigSchema = exports_external.object({
29922
30050
  restrict_to_project_root: exports_external.boolean().optional(),
29923
30051
  search_index: exports_external.boolean().optional(),
29924
30052
  semantic_search: exports_external.boolean().optional(),
30053
+ callgraph_store: exports_external.boolean().optional(),
30054
+ callgraph_chunk_size: exports_external.number().optional(),
29925
30055
  inspect: InspectConfigSchema.optional(),
29926
30056
  bash: BashConfigSchema.optional(),
29927
30057
  experimental: ExperimentalConfigSchema.optional(),
@@ -30000,6 +30130,10 @@ function resolveProjectOverridesForConfigure(config2) {
30000
30130
  overrides.search_index = config2.search_index;
30001
30131
  if (config2.semantic_search !== undefined)
30002
30132
  overrides.semantic_search = config2.semantic_search;
30133
+ if (config2.callgraph_store !== undefined)
30134
+ overrides.callgraph_store = config2.callgraph_store;
30135
+ if (config2.callgraph_chunk_size !== undefined)
30136
+ overrides.callgraph_chunk_size = config2.callgraph_chunk_size;
30003
30137
  Object.assign(overrides, resolveExperimentalConfigForConfigure(config2));
30004
30138
  Object.assign(overrides, resolveLspConfigForConfigure(config2));
30005
30139
  if (config2.semantic !== undefined)
@@ -30274,6 +30408,17 @@ function parseConfigPartially(rawConfig) {
30274
30408
  }
30275
30409
  return partialConfig;
30276
30410
  }
30411
+ var configLoadErrors = [];
30412
+ function getConfigLoadErrors() {
30413
+ return configLoadErrors;
30414
+ }
30415
+ function formatConfigParseFailureMessage(configPath, errorMessage) {
30416
+ return `AFT config at ${configPath} failed to parse and was ignored (running on defaults): ${errorMessage}. ` + "Fix the syntax or run `npx @cortexkit/aft doctor`.";
30417
+ }
30418
+ function recordConfigParseFailure(configPath, errorMessage) {
30419
+ configLoadErrors.push({ path: configPath, message: errorMessage });
30420
+ warn2(formatConfigParseFailureMessage(configPath, errorMessage));
30421
+ }
30277
30422
  function loadConfigFromPath(configPath) {
30278
30423
  try {
30279
30424
  if (!existsSync6(configPath)) {
@@ -30294,6 +30439,7 @@ function loadConfigFromPath(configPath) {
30294
30439
  } catch (err) {
30295
30440
  const errorMsg = err instanceof Error ? err.message : String(err);
30296
30441
  error2(`Error loading config from ${configPath}: ${errorMsg}`);
30442
+ recordConfigParseFailure(configPath, errorMsg);
30297
30443
  return null;
30298
30444
  }
30299
30445
  }
@@ -30417,6 +30563,8 @@ var PROJECT_SAFE_TOP_LEVEL_FIELDS = new Set([
30417
30563
  "configure_warnings_delivery",
30418
30564
  "search_index",
30419
30565
  "semantic_search",
30566
+ "callgraph_store",
30567
+ "callgraph_chunk_size",
30420
30568
  "inspect",
30421
30569
  "experimental",
30422
30570
  "bash"
@@ -30484,10 +30632,11 @@ function getOpenCodeConfigDir() {
30484
30632
  if (envDir) {
30485
30633
  return envDir;
30486
30634
  }
30487
- const xdgConfig = process.env.XDG_CONFIG_HOME || join10(homedir7(), ".config");
30635
+ const xdgConfig = process.env.XDG_CONFIG_HOME || join10(homedir8(), ".config");
30488
30636
  return join10(xdgConfig, "opencode");
30489
30637
  }
30490
30638
  function loadAftConfig(projectDirectory) {
30639
+ configLoadErrors = [];
30491
30640
  const configDir = getOpenCodeConfigDir();
30492
30641
  const userBasePath = join10(configDir, "aft");
30493
30642
  migrateAftConfigFile(`${userBasePath}.jsonc`);
@@ -30520,7 +30669,7 @@ function loadAftConfig(projectDirectory) {
30520
30669
 
30521
30670
  // src/notifications.ts
30522
30671
  import { existsSync as existsSync7, readFileSync as readFileSync5 } from "node:fs";
30523
- import { homedir as homedir8, platform } from "node:os";
30672
+ import { homedir as homedir9, platform } from "node:os";
30524
30673
  import { join as join11 } from "node:path";
30525
30674
  function isTuiMode() {
30526
30675
  return process.env.OPENCODE_CLIENT === "cli";
@@ -30542,7 +30691,7 @@ var WARNING_MARKER = `${AFT_MARKER} ⚠️`;
30542
30691
  var STATUS_MARKER = `${AFT_MARKER} ✅`;
30543
30692
  function getDesktopStatePath() {
30544
30693
  const os3 = platform();
30545
- const home = homedir8();
30694
+ const home = homedir9();
30546
30695
  if (os3 === "darwin") {
30547
30696
  return join11(home, "Library", "Application Support", "ai.opencode.desktop", "opencode.global.dat");
30548
30697
  }
@@ -30821,6 +30970,8 @@ function warningTitle(warning) {
30821
30970
  return "Checker is not installed";
30822
30971
  case "lsp_binary_missing":
30823
30972
  return "LSP binary is missing";
30973
+ case "config_parse_failed":
30974
+ return "Config failed to parse";
30824
30975
  }
30825
30976
  }
30826
30977
  function formatConfigureWarningLine(warning) {
@@ -30942,12 +31093,36 @@ async function cleanupWarnings(opts) {
30942
31093
 
30943
31094
  // src/configure-warnings.ts
30944
31095
  var pendingEagerWarnings = new Map;
31096
+ var pendingConfigParseWarnings = new Map;
30945
31097
  var pendingBySession = new Map;
30946
31098
  function isConfigureWarning(value) {
30947
31099
  if (!value || typeof value !== "object" || Array.isArray(value))
30948
31100
  return false;
30949
31101
  const warning = value;
30950
- return (warning.kind === "formatter_not_installed" || warning.kind === "checker_not_installed" || warning.kind === "lsp_binary_missing") && typeof warning.hint === "string";
31102
+ 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";
31103
+ }
31104
+ function configParseWarningsFromErrors(errors3) {
31105
+ return errors3.map((entry) => ({
31106
+ kind: "config_parse_failed",
31107
+ hint: formatConfigParseFailureMessage(entry.path, entry.message)
31108
+ }));
31109
+ }
31110
+ function enqueueConfigParseWarnings(projectRoot, errors3) {
31111
+ if (!projectRoot || errors3.length === 0)
31112
+ return;
31113
+ const incoming = configParseWarningsFromErrors(errors3);
31114
+ const existing = pendingConfigParseWarnings.get(projectRoot) ?? [];
31115
+ for (const warning of incoming) {
31116
+ if (!existing.some((item) => item.hint === warning.hint)) {
31117
+ existing.push(warning);
31118
+ }
31119
+ }
31120
+ pendingConfigParseWarnings.set(projectRoot, existing);
31121
+ }
31122
+ function drainPendingConfigParseWarnings(projectRoot) {
31123
+ const pending = pendingConfigParseWarnings.get(projectRoot) ?? [];
31124
+ pendingConfigParseWarnings.delete(projectRoot);
31125
+ return pending;
30951
31126
  }
30952
31127
  function coerceConfigureWarnings(warnings) {
30953
31128
  return warnings.filter(isConfigureWarning);
@@ -30958,7 +31133,10 @@ function drainPendingEagerWarnings(projectRoot) {
30958
31133
  return pending;
30959
31134
  }
30960
31135
  function enqueueConfigureWarningsForSession(context) {
30961
- const validWarnings = coerceConfigureWarnings(context.warnings);
31136
+ const validWarnings = [
31137
+ ...drainPendingConfigParseWarnings(context.projectRoot),
31138
+ ...coerceConfigureWarnings(context.warnings)
31139
+ ];
30962
31140
  if (!context.sessionId) {
30963
31141
  if (validWarnings.length === 0)
30964
31142
  return;
@@ -31028,27 +31206,27 @@ var import_comment_json3 = __toESM(require_src2(), 1);
31028
31206
  // src/hooks/auto-update-checker/checker.ts
31029
31207
  var import_comment_json2 = __toESM(require_src2(), 1);
31030
31208
  import { existsSync as existsSync8, readFileSync as readFileSync6, statSync as statSync4, writeFileSync as writeFileSync4 } from "node:fs";
31031
- import { homedir as homedir10 } from "node:os";
31209
+ import { homedir as homedir11 } from "node:os";
31032
31210
  import { dirname as dirname5, isAbsolute as isAbsolute4, join as join13, resolve as resolve3 } from "node:path";
31033
31211
  import { fileURLToPath } from "node:url";
31034
31212
 
31035
31213
  // src/hooks/auto-update-checker/constants.ts
31036
- import { homedir as homedir9, platform as platform2 } from "node:os";
31214
+ import { homedir as homedir10, platform as platform2 } from "node:os";
31037
31215
  import { join as join12 } from "node:path";
31038
31216
  var PACKAGE_NAME = "@cortexkit/aft-opencode";
31039
31217
  var NPM_REGISTRY_URL = "https://registry.npmjs.org";
31040
31218
  var NPM_FETCH_TIMEOUT = 1e4;
31041
31219
  function getOpenCodeCacheRoot() {
31042
31220
  if (platform2() === "win32") {
31043
- return join12(process.env.LOCALAPPDATA ?? homedir9(), "opencode");
31221
+ return join12(process.env.LOCALAPPDATA ?? homedir10(), "opencode");
31044
31222
  }
31045
- return join12(homedir9(), ".cache", "opencode");
31223
+ return join12(homedir10(), ".cache", "opencode");
31046
31224
  }
31047
31225
  function getOpenCodeConfigRoot() {
31048
31226
  if (platform2() === "win32") {
31049
- return join12(process.env.APPDATA ?? join12(homedir9(), "AppData", "Roaming"), "opencode");
31227
+ return join12(process.env.APPDATA ?? join12(homedir10(), "AppData", "Roaming"), "opencode");
31050
31228
  }
31051
- return join12(process.env.XDG_CONFIG_HOME ?? join12(homedir9(), ".config"), "opencode");
31229
+ return join12(process.env.XDG_CONFIG_HOME ?? join12(homedir10(), ".config"), "opencode");
31052
31230
  }
31053
31231
  var CACHE_DIR = join12(getOpenCodeCacheRoot(), "packages");
31054
31232
  var USER_OPENCODE_CONFIG = join12(getOpenCodeConfigRoot(), "opencode.json");
@@ -31227,7 +31405,7 @@ function getCachedVersion(spec) {
31227
31405
  getCurrentRuntimePackageJsonPath(),
31228
31406
  spec ? getSpecCachePackageJsonPath(spec) : null,
31229
31407
  getSpecCachePackageJsonPath(`${PACKAGE_NAME}@latest`),
31230
- join13(homedir10(), ".cache", "opencode", "node_modules", PACKAGE_NAME, "package.json")
31408
+ join13(homedir11(), ".cache", "opencode", "node_modules", PACKAGE_NAME, "package.json")
31231
31409
  ].filter(isString);
31232
31410
  for (const packageJsonPath of candidates) {
31233
31411
  try {
@@ -31730,7 +31908,7 @@ import {
31730
31908
  unlinkSync as unlinkSync5,
31731
31909
  writeFileSync as writeFileSync7
31732
31910
  } from "node:fs";
31733
- import { homedir as homedir11 } from "node:os";
31911
+ import { homedir as homedir12 } from "node:os";
31734
31912
  import { join as join15 } from "node:path";
31735
31913
  function aftCacheBase() {
31736
31914
  const override = process.env.AFT_CACHE_DIR;
@@ -31738,10 +31916,10 @@ function aftCacheBase() {
31738
31916
  return override;
31739
31917
  if (process.platform === "win32") {
31740
31918
  const localAppData = process.env.LOCALAPPDATA || process.env.APPDATA;
31741
- const base2 = localAppData || join15(homedir11(), "AppData", "Local");
31919
+ const base2 = localAppData || join15(homedir12(), "AppData", "Local");
31742
31920
  return join15(base2, "aft");
31743
31921
  }
31744
- const base = process.env.XDG_CACHE_HOME || join15(homedir11(), ".cache");
31922
+ const base = process.env.XDG_CACHE_HOME || join15(homedir12(), ".cache");
31745
31923
  return join15(base, "aft");
31746
31924
  }
31747
31925
  function lspCacheRoot() {
@@ -33416,6 +33594,34 @@ function normalizeToolMap(tools) {
33416
33594
  }
33417
33595
  return tools;
33418
33596
  }
33597
+ // src/shared/ignored-message.ts
33598
+ async function sendIgnoredMessage2(client, sessionID, text) {
33599
+ const typedClient = client;
33600
+ let agent;
33601
+ try {
33602
+ const ctx = await resolvePromptContext(client, sessionID);
33603
+ agent = ctx?.agent;
33604
+ } catch {
33605
+ agent = undefined;
33606
+ }
33607
+ const body = {
33608
+ noReply: true,
33609
+ parts: [{ type: "text", text, ignored: true }]
33610
+ };
33611
+ if (agent)
33612
+ body.agent = agent;
33613
+ const promptInput = { path: { id: sessionID }, body };
33614
+ if (typeof typedClient.session?.prompt === "function") {
33615
+ await Promise.resolve(typedClient.session.prompt(promptInput));
33616
+ return;
33617
+ }
33618
+ if (typeof typedClient.session?.promptAsync === "function") {
33619
+ await typedClient.session.promptAsync(promptInput);
33620
+ return;
33621
+ }
33622
+ throw new Error("[aft-plugin] client.session.prompt is unavailable");
33623
+ }
33624
+
33419
33625
  // src/shared/pty-cache.ts
33420
33626
  var import_headless = __toESM(require_xterm_headless(), 1);
33421
33627
  import * as fs2 from "node:fs/promises";
@@ -33824,7 +34030,7 @@ async function verifySessionDirectory(client, sessionId) {
33824
34030
  }
33825
34031
 
33826
34032
  // src/shared/status.ts
33827
- function asRecord(value) {
34033
+ function asRecord2(value) {
33828
34034
  return typeof value === "object" && value !== null ? value : {};
33829
34035
  }
33830
34036
  function readString(value, fallback = "") {
@@ -33843,7 +34049,7 @@ function readOptionalNumber(value) {
33843
34049
  return typeof value === "number" && Number.isFinite(value) ? value : null;
33844
34050
  }
33845
34051
  function readCompressionAggregate(value) {
33846
- const aggregate = asRecord(value);
34052
+ const aggregate = asRecord2(value);
33847
34053
  return {
33848
34054
  events: readNumber(aggregate.events),
33849
34055
  original_tokens: readNumber(aggregate.original_tokens),
@@ -33854,7 +34060,7 @@ function readCompressionAggregate(value) {
33854
34060
  function readCompression(value) {
33855
34061
  if (typeof value !== "object" || value === null)
33856
34062
  return;
33857
- const compression = asRecord(value);
34063
+ const compression = asRecord2(value);
33858
34064
  return {
33859
34065
  project: readCompressionAggregate(compression.project),
33860
34066
  session: readCompressionAggregate(compression.session)
@@ -33863,7 +34069,7 @@ function readCompression(value) {
33863
34069
  function readStatusBar(value) {
33864
34070
  if (typeof value !== "object" || value === null)
33865
34071
  return;
33866
- const bar = asRecord(value);
34072
+ const bar = asRecord2(value);
33867
34073
  return {
33868
34074
  errors: readNumber(bar.errors),
33869
34075
  warnings: readNumber(bar.warnings),
@@ -33907,16 +34113,16 @@ function formatBytes(bytes) {
33907
34113
  return `${value.toFixed(decimals)} ${units[unitIndex]}`;
33908
34114
  }
33909
34115
  function coerceAftStatus(response) {
33910
- const features = asRecord(response.features);
33911
- const searchIndex = asRecord(response.search_index);
33912
- const semanticIndex = asRecord(response.semantic_index);
34116
+ const features = asRecord2(response.features);
34117
+ const searchIndex = asRecord2(response.search_index);
34118
+ const semanticIndex = asRecord2(response.semantic_index);
33913
34119
  const semanticConfig = {
33914
- ...asRecord(response.semantic),
33915
- ...asRecord(response.semantic_config)
34120
+ ...asRecord2(response.semantic),
34121
+ ...asRecord2(response.semantic_config)
33916
34122
  };
33917
- const disk = asRecord(response.disk);
33918
- const symbolCache = asRecord(response.symbol_cache);
33919
- const session = asRecord(response.session);
34123
+ const disk = asRecord2(response.disk);
34124
+ const symbolCache = asRecord2(response.symbol_cache);
34125
+ const session = asRecord2(response.session);
33920
34126
  return {
33921
34127
  version: readString(response.version, "unknown"),
33922
34128
  project_root: readNullableString(response.project_root),
@@ -34038,7 +34244,7 @@ import { existsSync as existsSync15, mkdirSync as mkdirSync11, readFileSync as r
34038
34244
  import { dirname as dirname10, join as join22 } from "node:path";
34039
34245
 
34040
34246
  // src/shared/opencode-config-dir.ts
34041
- import { homedir as homedir12 } from "node:os";
34247
+ import { homedir as homedir13 } from "node:os";
34042
34248
  import { join as join21, resolve as resolve5 } from "node:path";
34043
34249
  function getCliConfigDir() {
34044
34250
  const envConfigDir = process.env.OPENCODE_CONFIG_DIR?.trim();
@@ -34046,9 +34252,9 @@ function getCliConfigDir() {
34046
34252
  return resolve5(envConfigDir);
34047
34253
  }
34048
34254
  if (process.platform === "win32") {
34049
- return join21(homedir12(), ".config", "opencode");
34255
+ return join21(homedir13(), ".config", "opencode");
34050
34256
  }
34051
- return join21(process.env.XDG_CONFIG_HOME || join21(homedir12(), ".config"), "opencode");
34257
+ return join21(process.env.XDG_CONFIG_HOME || join21(homedir13(), ".config"), "opencode");
34052
34258
  }
34053
34259
  function getOpenCodeConfigDir2(_options) {
34054
34260
  return getCliConfigDir();
@@ -34356,6 +34562,27 @@ async function callBashBridge(ctx, runtime, command, params = {}, options) {
34356
34562
  import * as fs4 from "node:fs";
34357
34563
  import * as path4 from "node:path";
34358
34564
  var UNSUPPORTED_ASK_HOST = "AFT requires OpenCode 1.15.5 or newer for permission asks; please upgrade OpenCode";
34565
+ var RESTRICT_NOTICE_THROTTLE_MS = 5 * 60 * 1000;
34566
+ var restrictNoticeLastSentAt = new Map;
34567
+ function restrictNoticeWording(target) {
34568
+ return `AFT blocked access to a path outside the project:
34569
+ ${target}
34570
+ ` + "`restrict_to_project_root` is enabled (full isolation), so AFT does not access paths " + "outside the project root. To allow external paths, set `restrict_to_project_root: false` " + "in your aft.jsonc.";
34571
+ }
34572
+ function notifyRestrictBlocked(ctx, context, target) {
34573
+ const sessionID = context.sessionID;
34574
+ if (!sessionID)
34575
+ return;
34576
+ const now = Date.now();
34577
+ const last = restrictNoticeLastSentAt.get(sessionID);
34578
+ if (last !== undefined && now - last < RESTRICT_NOTICE_THROTTLE_MS)
34579
+ return;
34580
+ restrictNoticeLastSentAt.set(sessionID, now);
34581
+ sendIgnoredMessage2(ctx.client, sessionID, restrictNoticeWording(target)).catch(() => {});
34582
+ }
34583
+ function restrictDenialMessage(target) {
34584
+ return `Blocked: '${target}' 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.";
34585
+ }
34359
34586
  async function runAsk(maybe) {
34360
34587
  await maybe;
34361
34588
  }
@@ -34435,11 +34662,9 @@ function normalizePathPattern(p) {
34435
34662
  const dir = /^[A-Za-z]:$/.test(match[1]) ? `${match[1]}\\` : match[1];
34436
34663
  return path4.join(normalizePath(dir), match[2]);
34437
34664
  }
34438
- async function assertExternalDirectoryPermission(context, target, options) {
34665
+ async function assertExternalDirectoryPermission(ctx, context, target, options) {
34439
34666
  if (!target)
34440
34667
  return;
34441
- if (typeof context.ask !== "function")
34442
- return UNSUPPORTED_ASK_HOST;
34443
34668
  const resolved = resolveAbsolutePath(context, target);
34444
34669
  const absoluteTarget = normalizePath(resolved);
34445
34670
  const root = projectRootFor(context);
@@ -34451,6 +34676,12 @@ async function assertExternalDirectoryPermission(context, target, options) {
34451
34676
  if (worktree && worktree !== "/" && worktree !== directory && containsPath(worktree, absoluteTarget)) {
34452
34677
  return;
34453
34678
  }
34679
+ if (ctx.config.restrict_to_project_root === true) {
34680
+ notifyRestrictBlocked(ctx, context, absoluteTarget);
34681
+ return restrictDenialMessage(absoluteTarget);
34682
+ }
34683
+ if (typeof context.ask !== "function")
34684
+ return UNSUPPORTED_ASK_HOST;
34454
34685
  const kind = options?.kind ?? "file";
34455
34686
  const parentDir = kind === "directory" ? absoluteTarget : path4.dirname(absoluteTarget);
34456
34687
  const rawGlob = process.platform === "win32" ? normalizePathPattern(path4.join(parentDir, "*")) : path4.join(parentDir, "*").replaceAll("\\", "/");
@@ -34547,12 +34778,12 @@ async function resolveAstPaths(ctx, context, paths) {
34547
34778
  const resolved = await Promise.all(paths.filter((p) => typeof p === "string" && p.length > 0).map((p) => resolvePathArg(ctx, context, p)));
34548
34779
  return resolved.length > 0 ? resolved : undefined;
34549
34780
  }
34550
- async function checkAstPathsPermission(context, paths) {
34781
+ async function checkAstPathsPermission(ctx, context, paths) {
34551
34782
  if (paths === undefined)
34552
34783
  return;
34553
34784
  const uniquePaths = Array.from(new Set(paths));
34554
34785
  for (const p of uniquePaths) {
34555
- const denial = await assertExternalDirectoryPermission(context, p, { kind: "directory" });
34786
+ const denial = await assertExternalDirectoryPermission(ctx, context, p, { kind: "directory" });
34556
34787
  if (denial)
34557
34788
  return denial;
34558
34789
  }
@@ -34586,7 +34817,7 @@ function astTools(ctx) {
34586
34817
  },
34587
34818
  execute: async (args, context) => {
34588
34819
  const paths = await resolveAstPaths(ctx, context, args.paths);
34589
- const externalDenied = await checkAstPathsPermission(context, paths);
34820
+ const externalDenied = await checkAstPathsPermission(ctx, context, paths);
34590
34821
  if (externalDenied)
34591
34822
  return permissionDeniedResponse(externalDenied);
34592
34823
  const params = {
@@ -34684,13 +34915,13 @@ ${hint}`;
34684
34915
  execute: async (args, context) => {
34685
34916
  const isDryRun = args.dryRun === true;
34686
34917
  const paths = await resolveAstPaths(ctx, context, args.paths);
34687
- const externalDenied = await checkAstPathsPermission(context, paths);
34918
+ const externalDenied = await checkAstPathsPermission(ctx, context, paths);
34688
34919
  if (externalDenied)
34689
34920
  return permissionDeniedResponse(externalDenied);
34690
34921
  if (!isDryRun) {
34691
34922
  if (!Array.isArray(args.paths)) {
34692
34923
  const targetPath = await resolvePathArg(ctx, context, ".");
34693
- const denial = await assertExternalDirectoryPermission(context, targetPath, {
34924
+ const denial = await assertExternalDirectoryPermission(ctx, context, targetPath, {
34694
34925
  kind: "directory"
34695
34926
  });
34696
34927
  if (denial)
@@ -34888,18 +35119,43 @@ function resolveForegroundWaitMs(configured) {
34888
35119
  function bashToolDescription(aftSearchRegistered, compressionOn, backgroundOn) {
34889
35120
  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";
34890
35121
  const compression = compressionOn ? " Output is compressed by default; pass compressed: false for raw output." : "";
34891
- const tasks = backgroundOn ? ' Pass background: true to run in the background and get a taskId for bash_status/bash_kill. Pass pty: true for interactive programs (REPLs, TUIs) and drive them with bash_status({ outputMode: "screen" }) plus bash_write (pty implies background automatically).' : " Commands that outlive the foreground wait window are promoted to background tasks; inspect them with bash_status({ taskId }) or terminate with bash_kill.";
34892
- return `Execute shell commands.${compression}${tasks} Use bash_watch to wait for output patterns or exit events.
35122
+ const tasks = backgroundOn ? ' Pass background: true to run in the background and get a taskId for bash_status/bash_watch/bash_kill. Pass pty: true for interactive programs (REPLs, TUIs) and drive them with bash_status({ outputMode: "screen" }) plus bash_write (pty implies background automatically). 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).";
35123
+ return `Execute shell commands.${compression}${tasks}
34893
35124
 
34894
35125
  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}.`;
34895
35126
  }
35127
+ function pushUnique(target, values) {
35128
+ for (const value of values) {
35129
+ if (!target.includes(value))
35130
+ target.push(value);
35131
+ }
35132
+ }
35133
+ function groupBashPermissionAsks(asks) {
35134
+ const grouped = [];
35135
+ let bashAsk;
35136
+ for (const ask of asks) {
35137
+ if (ask.kind === "bash") {
35138
+ if (!bashAsk) {
35139
+ bashAsk = { kind: "bash", patterns: [], always: [] };
35140
+ grouped.push(bashAsk);
35141
+ }
35142
+ pushUnique(bashAsk.patterns, ask.patterns);
35143
+ pushUnique(bashAsk.always, ask.always);
35144
+ continue;
35145
+ }
35146
+ grouped.push(ask);
35147
+ }
35148
+ return grouped;
35149
+ }
35150
+ function permissionsGrantedForRetry(asks) {
35151
+ return asks.flatMap((ask) => ask.always.length > 0 ? ask.always : ask.patterns);
35152
+ }
34896
35153
  async function withPermissionLoop(ctx, runtime, params, bridgeCall, options) {
34897
35154
  const first = await bridgeCall(ctx, runtime, "bash", params, options);
34898
35155
  if (first.success !== false || first.code !== "permission_required")
34899
35156
  return first;
34900
35157
  const asks = Array.isArray(first.asks) ? first.asks : [];
34901
- const permissionsGranted = [];
34902
- for (const ask of asks) {
35158
+ for (const ask of groupBashPermissionAsks(asks)) {
34903
35159
  const permission = ask.kind === "external_directory" ? "external_directory" : "bash";
34904
35160
  await runAsk(runtime.ask({
34905
35161
  permission,
@@ -34907,54 +35163,60 @@ async function withPermissionLoop(ctx, runtime, params, bridgeCall, options) {
34907
35163
  always: ask.always,
34908
35164
  metadata: {}
34909
35165
  }));
34910
- permissionsGranted.push(...ask.always.length > 0 ? ask.always : ask.patterns);
34911
35166
  }
34912
- const second = await bridgeCall(ctx, runtime, "bash", { ...params, permissions_granted: permissionsGranted }, options);
35167
+ const second = await bridgeCall(ctx, runtime, "bash", { ...params, permissions_granted: permissionsGrantedForRetry(asks) }, options);
34913
35168
  if (second.success === false && second.code === "permission_required") {
34914
35169
  throw new Error("bash permission retry failed");
34915
35170
  }
34916
35171
  return second;
34917
35172
  }
34918
35173
  function createBashTool(ctx, aftSearchRegisteredOverride) {
35174
+ const initialBashCfg = resolveBashConfig(ctx.config);
35175
+ const backgroundFlagArg = initialBashCfg.background ? {
35176
+ background: z4.boolean().optional().describe("When true, spawn the command in the background and return a taskId for bash_status/bash_kill instead of waiting for completion. Defaults to false.")
35177
+ } : {};
35178
+ const ptyArgs = initialBashCfg.background ? {
35179
+ pty: z4.boolean().optional().describe('When true, spawn the command in a real PTY for interactive programs (python/node/bash REPLs, vim). Implies background: true automatically. Unavailable in subagent sessions. Inspect with bash_status({ taskId, outputMode: "screen" }) and drive interactively with bash_write — its input accepts either a string OR an array like [ "iHello", { key: "esc" }, ":wq", { key: "enter" } ] for atomic text+key sequences.'),
35180
+ ptyRows: optionalInt(1, 60).describe("PTY terminal height in rows — ignored when pty is false. Defaults to 24 when pty: true. Minimum 1, maximum 60."),
35181
+ ptyCols: optionalInt(1, 140).describe("PTY terminal width in columns — ignored when pty is false. Defaults to 80 when pty: true. Minimum 1, maximum 140.")
35182
+ } : {};
35183
+ const args = {
35184
+ command: z4.string().describe("Shell command to execute. Supports pipes, redirection, and normal shell syntax."),
35185
+ timeout: optionalInt(1, Number.MAX_SAFE_INTEGER).describe(initialBashCfg.background ? "Hard kill cap in milliseconds (positive integer). When omitted, the task can run up to 30 minutes. Foreground bash returns inline if the command finishes within ~8s (configurable via bash.foreground_wait_window_ms); otherwise it's automatically promoted to background and a completion reminder is delivered when the task actually finishes." : "Hard kill cap in milliseconds (positive integer). When omitted, the foreground command can run up to 30 minutes and returns inline when it finishes."),
35186
+ workdir: z4.string().optional().describe("Working directory for command execution. Relative paths resolve through the bridge; defaults to the current tool context/project root when omitted."),
35187
+ description: z4.string().optional().describe("Short 5-10 word human-readable summary shown in OpenCode UI metadata instead of raw shell syntax."),
35188
+ ...backgroundFlagArg,
35189
+ compressed: z4.boolean().optional().describe("When true or omitted, return compressed output with noisy terminal control sequences reduced. Set to false for raw output."),
35190
+ ...ptyArgs
35191
+ };
34919
35192
  return {
34920
- description: (() => {
34921
- const cfg = resolveBashConfig(ctx.config);
34922
- return bashToolDescription(false, cfg.compress, cfg.background);
34923
- })(),
34924
- args: {
34925
- command: z4.string().describe("Shell command to execute. Supports pipes, redirection, and normal shell syntax."),
34926
- timeout: optionalInt(1, Number.MAX_SAFE_INTEGER).describe("Hard kill cap in milliseconds (positive integer). When omitted, the task can run up to 30 minutes. Foreground bash returns inline if the command finishes within ~8s (configurable via bash.foreground_wait_window_ms); otherwise it's automatically promoted to background and a completion reminder is delivered when the task actually finishes."),
34927
- workdir: z4.string().optional().describe("Working directory for command execution. Relative paths resolve through the bridge; defaults to the current tool context/project root when omitted."),
34928
- description: z4.string().optional().describe("Short 5-10 word human-readable summary shown in OpenCode UI metadata instead of raw shell syntax."),
34929
- background: z4.boolean().optional().describe("When true, spawn the command in the background and return a taskId for bash_status/bash_kill instead of waiting for completion. Defaults to false."),
34930
- compressed: z4.boolean().optional().describe("When true or omitted, return compressed output with noisy terminal control sequences reduced. Set to false for raw output."),
34931
- pty: z4.boolean().optional().describe('When true, spawn the command in a real PTY for interactive programs (python/node/bash REPLs, vim). Implies background: true automatically. Unavailable in subagent sessions. Inspect with bash_status({ taskId, outputMode: "screen" }) and drive interactively with bash_write — its input accepts either a string OR an array like [ "iHello", { key: "esc" }, ":wq", { key: "enter" } ] for atomic text+key sequences.'),
34932
- ptyRows: optionalInt(1, 60).describe("PTY terminal height in rows — ignored when pty is false. Defaults to 24 when pty: true. Minimum 1, maximum 60."),
34933
- ptyCols: optionalInt(1, 140).describe("PTY terminal width in columns — ignored when pty is false. Defaults to 80 when pty: true. Minimum 1, maximum 140.")
34934
- },
34935
- execute: async (args, context) => {
35193
+ description: bashToolDescription(false, initialBashCfg.compress, initialBashCfg.background),
35194
+ args,
35195
+ execute: async (args2, context) => {
34936
35196
  const bashCfg = resolveBashConfig(ctx.config);
34937
35197
  const ctxAftSearchRegistered = ctx.aftSearchRegistered === true;
34938
35198
  const aftSearchRegistered = aftSearchRegisteredOverride ?? ctxAftSearchRegistered;
34939
35199
  let accumulatedOutput = "";
34940
- const description = args.description;
35200
+ const description = args2.description;
34941
35201
  const metadata = context.metadata;
34942
- const rawCommand = args.command;
34943
- const compressionEnabled = bashCfg.compress && args.compressed !== false;
35202
+ const rawCommand = args2.command;
35203
+ const compressionEnabled = bashCfg.compress && args2.compressed !== false;
34944
35204
  const pipeStrip = maybeStripCompressorPipe(rawCommand, compressionEnabled);
34945
35205
  const command = pipeStrip.command;
34946
- const cwd = args.workdir ?? context.directory;
35206
+ const cwd = args2.workdir ?? context.directory;
34947
35207
  const isSubagent = await resolveIsSubagent(ctx.client, context.sessionID, context.directory);
34948
- const requestedPty = args.pty === true;
34949
- const requestedBackground = args.background === true || requestedPty;
35208
+ const backgroundDisabled = !bashCfg.background;
35209
+ const requestedPty = !backgroundDisabled && args2.pty === true;
35210
+ const requestedBackground = !backgroundDisabled && (args2.background === true || requestedPty);
34950
35211
  if (requestedPty && isSubagent) {
34951
35212
  throw new Error("PTY mode is not available in subagent sessions; subagents cannot drive interactive terminals.");
34952
35213
  }
34953
35214
  const allowSubagentBg = bashCfg.subagent_background;
34954
35215
  const subagentForcedForeground = isSubagent && !allowSubagentBg;
34955
- const effectiveBackground = subagentForcedForeground ? false : requestedBackground;
34956
- const rawTimeout = args.timeout;
34957
- const effectiveTimeout = effectiveBackground ? rawTimeout : resolveBashKillTimeout(rawTimeout, bashCfg.foreground_wait_window_ms);
35216
+ const blockToCompletion = subagentForcedForeground || backgroundDisabled;
35217
+ const effectiveBackground = blockToCompletion ? false : requestedBackground;
35218
+ const rawTimeout = args2.timeout;
35219
+ const effectiveTimeout = effectiveBackground || backgroundDisabled ? rawTimeout : resolveBashKillTimeout(rawTimeout, bashCfg.foreground_wait_window_ms);
34958
35220
  if (subagentForcedForeground && requestedBackground) {
34959
35221
  sessionLog(context.sessionID, "[bash] subagent + background:true → converting to foreground (subagent would lose task_id)");
34960
35222
  }
@@ -34962,15 +35224,15 @@ function createBashTool(ctx, aftSearchRegisteredOverride) {
34962
35224
  const data = await withPermissionLoop(ctx, context, {
34963
35225
  command,
34964
35226
  timeout: effectiveTimeout,
34965
- workdir: args.workdir,
35227
+ workdir: args2.workdir,
34966
35228
  env: shellEnv?.env ?? {},
34967
35229
  description,
34968
35230
  background: effectiveBackground,
34969
35231
  notify_on_completion: effectiveBackground,
34970
- compressed: args.compressed,
35232
+ compressed: args2.compressed,
34971
35233
  pty: requestedPty,
34972
- pty_rows: args.ptyRows,
34973
- pty_cols: args.ptyCols,
35234
+ pty_rows: args2.ptyRows,
35235
+ pty_cols: args2.ptyCols,
34974
35236
  permissions_requested: true
34975
35237
  }, callBashBridge, {
34976
35238
  onProgress: ({ text }) => {
@@ -34995,7 +35257,7 @@ function createBashTool(ctx, aftSearchRegisteredOverride) {
34995
35257
  return { output: startedLine, title: uiTitle, metadata: metadataPayload2 };
34996
35258
  }
34997
35259
  const foregroundWaitMs = resolveForegroundWaitMs(bashCfg.foreground_wait_window_ms);
34998
- const waitTimeoutMs = subagentForcedForeground ? effectiveTimeout ?? DEFAULT_HARD_TIMEOUT_MS : effectiveTimeout !== undefined ? Math.min(effectiveTimeout, foregroundWaitMs) : foregroundWaitMs;
35260
+ const waitTimeoutMs = blockToCompletion ? effectiveTimeout ?? DEFAULT_HARD_TIMEOUT_MS : effectiveTimeout !== undefined ? Math.min(effectiveTimeout, foregroundWaitMs) : foregroundWaitMs;
34999
35261
  const startedAt = Date.now();
35000
35262
  while (true) {
35001
35263
  const status = await callBashBridge(ctx, context, "bash_status", { task_id: taskId });
@@ -35009,7 +35271,7 @@ function createBashTool(ctx, aftSearchRegisteredOverride) {
35009
35271
  return { output: rendered2, title: uiTitle, metadata: metadataPayload2 };
35010
35272
  }
35011
35273
  if (Date.now() - startedAt >= waitTimeoutMs) {
35012
- if (subagentForcedForeground) {
35274
+ if (blockToCompletion) {
35013
35275
  await sleep(FOREGROUND_POLL_INTERVAL_MS);
35014
35276
  continue;
35015
35277
  }
@@ -35069,7 +35331,7 @@ function createBashTool(ctx, aftSearchRegisteredOverride) {
35069
35331
  }
35070
35332
  function createBashStatusTool(ctx) {
35071
35333
  return {
35072
- description: "Read-only snapshot of a background or PTY bash task's current state and output. Returns immediately. Never waits. Use bash_watch to block on or register for pattern matches and exit events.",
35334
+ description: "Read-only snapshot of a background or PTY bash task's current state and output. 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.",
35073
35335
  args: {
35074
35336
  taskId: z4.string().describe("Background task ID returned by bash({ background: true }), e.g. bash-6b454047."),
35075
35337
  outputMode: z4.enum(["screen", "raw", "both"]).optional().describe("PTY output rendering mode. Defaults to screen for PTY tasks and preserves existing behavior for piped tasks when omitted.")
@@ -35221,7 +35483,7 @@ function conflictTools(ctx) {
35221
35483
  const expanded = expandTilde2(String(args.path));
35222
35484
  const projectRoot = await resolveProjectRoot(ctx, context);
35223
35485
  const resolved = resolvePathFromProjectRoot(projectRoot, expanded);
35224
- const denied = await assertExternalDirectoryPermission(context, resolved, {
35486
+ const denied = await assertExternalDirectoryPermission(ctx, context, resolved, {
35225
35487
  kind: "directory"
35226
35488
  });
35227
35489
  if (denied)
@@ -35547,16 +35809,19 @@ import { tool as tool6 } from "@opencode-ai/plugin";
35547
35809
  var z6 = tool6.schema;
35548
35810
  var BASH_WAIT_POLL_INTERVAL_MS = 100;
35549
35811
  var DEFAULT_BASH_STATUS_WAIT_TIMEOUT_MS = 30000;
35550
- var MAX_BASH_STATUS_WAIT_TIMEOUT_MS = 300000;
35812
+ var MAX_BASH_STATUS_WAIT_TIMEOUT_MS = 30 * 60 * 1000;
35551
35813
  var REGEX_WAIT_SCAN_WINDOW_BYTES = 64 * 1024;
35814
+ function unavailableSnapshot() {
35815
+ return { status: "unknown" };
35816
+ }
35552
35817
  function createBashWatchTool(ctx) {
35553
35818
  return {
35554
- 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.",
35819
+ 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 timeoutMs 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.",
35555
35820
  args: {
35556
35821
  taskId: z6.string().describe("Background task ID returned by bash({ background: true })."),
35557
35822
  pattern: z6.union([z6.string(), z6.object({ regex: z6.string() })]).optional().describe("Substring or regex pattern. Optional in sync mode; required with background:true. Sync substring watches keep only the overlap tail needed for boundary matches; sync regex watches use a 64 KB rolling output window."),
35558
35823
  background: z6.boolean().optional().describe("When true, register an async watch and return immediately. Defaults to false (sync wait)."),
35559
- timeoutMs: optionalInt(1, MAX_BASH_STATUS_WAIT_TIMEOUT_MS).describe("Sync-only timeout in milliseconds. Default 30000, max 300000."),
35824
+ timeoutMs: optionalInt(1, MAX_BASH_STATUS_WAIT_TIMEOUT_MS).describe("Sync-only timeout in milliseconds. Default 30000, max 1800000 (30 min)."),
35560
35825
  once: z6.boolean().optional().describe("Async-only. Defaults true; false keeps the watch sticky until task exit.")
35561
35826
  },
35562
35827
  execute: async (args, context) => {
@@ -35653,6 +35918,9 @@ Waited ${waited.elapsed_ms}ms; matched ${JSON.stringify(waited.match ?? "")} at
35653
35918
  } else if (waited.reason === "timeout") {
35654
35919
  text += `
35655
35920
  Waited ${waited.elapsed_ms}ms; timeout reached without match.`;
35921
+ } else if (waited.reason === "unavailable") {
35922
+ text += `
35923
+ 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 }).`;
35656
35924
  } else {
35657
35925
  const stat = String(data.status ?? "unknown");
35658
35926
  const e = typeof data.exit_code === "number" ? `, exit ${data.exit_code}` : "";
@@ -35686,9 +35954,31 @@ async function waitForBashStatus(ctx, runtime, taskId, outputMode, waitFor, effe
35686
35954
  clearSyncWatchAbort(runtime.sessionID);
35687
35955
  markTaskWaiting(runtime.sessionID, taskId);
35688
35956
  let sawTerminal = false;
35957
+ let lastData;
35689
35958
  try {
35690
35959
  while (true) {
35691
- const data = await bashStatusSnapshot2(ctx, runtime, taskId, outputMode, bridgeOptions);
35960
+ let data;
35961
+ try {
35962
+ data = await bashStatusSnapshot2(ctx, runtime, taskId, outputMode, bridgeOptions);
35963
+ } catch (err) {
35964
+ if (!isBridgeTransportTimeout(err))
35965
+ throw err;
35966
+ if (isSyncWatchAborted(runtime.sessionID)) {
35967
+ return withWaited(lastData ?? unavailableSnapshot(), {
35968
+ reason: "user_message",
35969
+ elapsed_ms: Date.now() - startedAt
35970
+ });
35971
+ }
35972
+ if (Date.now() >= deadline) {
35973
+ return withWaited(lastData ?? unavailableSnapshot(), {
35974
+ reason: "unavailable",
35975
+ elapsed_ms: Date.now() - startedAt
35976
+ });
35977
+ }
35978
+ await sleep(Math.min(BASH_WAIT_POLL_INTERVAL_MS, Math.max(0, deadline - Date.now())));
35979
+ continue;
35980
+ }
35981
+ lastData = data;
35692
35982
  const terminal = isTerminalStatus(data.status);
35693
35983
  if (waitFor) {
35694
35984
  const scan = await readNewTaskOutput(runtime, taskId, data, spillCursor);
@@ -36222,7 +36512,7 @@ function createReadTool(ctx) {
36222
36512
  const projectRoot = await resolveProjectRoot(ctx, context);
36223
36513
  const filePath = resolvePathFromProjectRoot(projectRoot, file2);
36224
36514
  {
36225
- const denial = await assertExternalDirectoryPermission(context, filePath);
36515
+ const denial = await assertExternalDirectoryPermission(ctx, context, filePath);
36226
36516
  if (denial)
36227
36517
  return permissionDeniedResponse(denial);
36228
36518
  }
@@ -36333,7 +36623,7 @@ function createWriteTool(ctx, editToolName = "edit") {
36333
36623
  const filePath = resolvePathFromProjectRoot(projectRoot, file2);
36334
36624
  const relPath = path5.relative(projectRoot, filePath);
36335
36625
  {
36336
- const denial2 = await assertExternalDirectoryPermission(context, filePath);
36626
+ const denial2 = await assertExternalDirectoryPermission(ctx, context, filePath);
36337
36627
  if (denial2)
36338
36628
  return permissionDeniedResponse(denial2);
36339
36629
  }
@@ -36484,7 +36774,7 @@ function createEditTool(ctx, writeToolName = "write") {
36484
36774
  const filePath = resolvePathFromProjectRoot(projectRoot, file2);
36485
36775
  const relPath = path5.relative(projectRoot, filePath);
36486
36776
  {
36487
- const denial2 = await assertExternalDirectoryPermission(context, filePath);
36777
+ const denial2 = await assertExternalDirectoryPermission(ctx, context, filePath);
36488
36778
  if (denial2)
36489
36779
  return permissionDeniedResponse(denial2);
36490
36780
  }
@@ -36720,7 +37010,7 @@ function createApplyPatchTool(ctx) {
36720
37010
  if (asked.has(filePath))
36721
37011
  continue;
36722
37012
  asked.add(filePath);
36723
- const denial2 = await assertExternalDirectoryPermission(context, filePath);
37013
+ const denial2 = await assertExternalDirectoryPermission(ctx, context, filePath);
36724
37014
  if (denial2)
36725
37015
  return permissionDeniedResponse(denial2);
36726
37016
  }
@@ -37031,7 +37321,7 @@ function createDeleteTool(ctx) {
37031
37321
  if (asked.has(filePath))
37032
37322
  continue;
37033
37323
  asked.add(filePath);
37034
- const denial = await assertExternalDirectoryPermission(context, filePath);
37324
+ const denial = await assertExternalDirectoryPermission(ctx, context, filePath);
37035
37325
  if (denial)
37036
37326
  return permissionDeniedResponse(denial);
37037
37327
  }
@@ -37079,13 +37369,13 @@ function createMoveTool(ctx) {
37079
37369
  const filePath = resolvePathFromProjectRoot(projectRoot, args.filePath);
37080
37370
  const destPath = resolvePathFromProjectRoot(projectRoot, args.destination);
37081
37371
  {
37082
- const sourceDenial = await assertExternalDirectoryPermission(context, filePath, {
37372
+ const sourceDenial = await assertExternalDirectoryPermission(ctx, context, filePath, {
37083
37373
  kind: "file"
37084
37374
  });
37085
37375
  if (sourceDenial)
37086
37376
  return permissionDeniedResponse(sourceDenial);
37087
37377
  if (destPath !== filePath) {
37088
- const destDenial = await assertExternalDirectoryPermission(context, destPath);
37378
+ const destDenial = await assertExternalDirectoryPermission(ctx, context, destPath);
37089
37379
  if (destDenial)
37090
37380
  return permissionDeniedResponse(destDenial);
37091
37381
  }
@@ -37116,12 +37406,15 @@ function hoistedTools(ctx) {
37116
37406
  aft_delete: createDeleteTool(ctx),
37117
37407
  aft_move: createMoveTool(ctx)
37118
37408
  };
37119
- if (resolveBashConfig(ctx.config).enabled) {
37409
+ const bashCfg = resolveBashConfig(ctx.config);
37410
+ if (bashCfg.enabled) {
37120
37411
  tools.bash = createBashTool(ctx);
37121
- tools.bash_status = createBashStatusTool(ctx);
37122
- tools.bash_write = createBashWriteTool(ctx);
37123
- tools.bash_watch = createBashWatchTool(ctx);
37124
- tools.bash_kill = createBashKillTool(ctx);
37412
+ if (bashCfg.background) {
37413
+ tools.bash_status = createBashStatusTool(ctx);
37414
+ tools.bash_write = createBashWriteTool(ctx);
37415
+ tools.bash_watch = createBashWatchTool(ctx);
37416
+ tools.bash_kill = createBashKillTool(ctx);
37417
+ }
37125
37418
  }
37126
37419
  return tools;
37127
37420
  }
@@ -37144,7 +37437,7 @@ function aftPrefixedTools(ctx) {
37144
37437
  const filePath = resolvePathFromProjectRoot(projectRoot, file2);
37145
37438
  const relPath = path5.relative(projectRoot, filePath);
37146
37439
  {
37147
- const denial2 = await assertExternalDirectoryPermission(context, filePath);
37440
+ const denial2 = await assertExternalDirectoryPermission(ctx, context, filePath);
37148
37441
  if (denial2)
37149
37442
  return permissionDeniedResponse(denial2);
37150
37443
  }
@@ -37175,12 +37468,15 @@ function aftPrefixedTools(ctx) {
37175
37468
  aft_delete: createDeleteTool(ctx),
37176
37469
  aft_move: createMoveTool(ctx)
37177
37470
  };
37178
- if (resolveBashConfig(ctx.config).enabled) {
37471
+ const bashCfg = resolveBashConfig(ctx.config);
37472
+ if (bashCfg.enabled) {
37179
37473
  tools.aft_bash = createBashTool(ctx);
37180
- tools.bash_status = createBashStatusTool(ctx);
37181
- tools.bash_write = createBashWriteTool(ctx);
37182
- tools.bash_watch = createBashWatchTool(ctx);
37183
- tools.bash_kill = createBashKillTool(ctx);
37474
+ if (bashCfg.background) {
37475
+ tools.bash_status = createBashStatusTool(ctx);
37476
+ tools.bash_write = createBashWriteTool(ctx);
37477
+ tools.bash_watch = createBashWatchTool(ctx);
37478
+ tools.bash_kill = createBashKillTool(ctx);
37479
+ }
37184
37480
  }
37185
37481
  return tools;
37186
37482
  }
@@ -37218,7 +37514,7 @@ function importTools(ctx) {
37218
37514
  }
37219
37515
  const filePath = await resolvePathArg(ctx, context, args.filePath);
37220
37516
  {
37221
- const denial = await assertExternalDirectoryPermission(context, filePath);
37517
+ const denial = await assertExternalDirectoryPermission(ctx, context, filePath);
37222
37518
  if (denial)
37223
37519
  return permissionDeniedResponse(denial);
37224
37520
  }
@@ -37264,15 +37560,15 @@ function importTools(ctx) {
37264
37560
  // src/tools/inspect.ts
37265
37561
  import { tool as tool10 } from "@opencode-ai/plugin";
37266
37562
  var z10 = tool10.schema;
37267
- function asRecord2(value) {
37563
+ function asRecord3(value) {
37268
37564
  if (!value || typeof value !== "object" || Array.isArray(value))
37269
37565
  return;
37270
37566
  return value;
37271
37567
  }
37272
- function asNumber(value) {
37568
+ function asNumber2(value) {
37273
37569
  return typeof value === "number" && Number.isFinite(value) ? value : undefined;
37274
37570
  }
37275
- function asString(value) {
37571
+ function asString2(value) {
37276
37572
  return typeof value === "string" ? value : undefined;
37277
37573
  }
37278
37574
  function asStringArray(value) {
@@ -37289,16 +37585,16 @@ function diagnosticsServerSummary(section) {
37289
37585
  return parts.length > 0 ? parts.join("; ") : "none reported";
37290
37586
  }
37291
37587
  function formatDiagnosticsSummary(summary) {
37292
- const section = asRecord2(summary?.diagnostics);
37588
+ const section = asRecord3(summary?.diagnostics);
37293
37589
  if (!section)
37294
37590
  return;
37295
- const errors3 = asNumber(section.errors);
37296
- const warnings = asNumber(section.warnings);
37297
- const info = asNumber(section.info);
37298
- const hints = asNumber(section.hints);
37591
+ const errors3 = asNumber2(section.errors);
37592
+ const warnings = asNumber2(section.warnings);
37593
+ const info = asNumber2(section.info);
37594
+ const hints = asNumber2(section.hints);
37299
37595
  const hasCounts = [errors3, warnings, info, hints].some((value) => value !== undefined);
37300
37596
  const counts = `${errors3 ?? 0} errors, ${warnings ?? 0} warnings, ${info ?? 0} info, ${hints ?? 0} hints`;
37301
- const status = asString(section.status);
37597
+ const status = asString2(section.status);
37302
37598
  if (status === "pending") {
37303
37599
  return hasCounts ? `diagnostics: ${counts} so far — still pending (servers: ${diagnosticsServerSummary(section)})` : `diagnostics: pending (servers: ${diagnosticsServerSummary(section)})`;
37304
37600
  }
@@ -37311,9 +37607,9 @@ function formatDiagnosticsSummary(summary) {
37311
37607
  return;
37312
37608
  }
37313
37609
  function formatDiagnosticLocation(diagnostic) {
37314
- const file2 = asString(diagnostic.file) ?? "(unknown file)";
37315
- const line = asNumber(diagnostic.line);
37316
- const column = asNumber(diagnostic.column);
37610
+ const file2 = asString2(diagnostic.file) ?? "(unknown file)";
37611
+ const line = asNumber2(diagnostic.line);
37612
+ const column = asNumber2(diagnostic.column);
37317
37613
  if (line === undefined)
37318
37614
  return file2;
37319
37615
  if (column === undefined)
@@ -37321,21 +37617,21 @@ function formatDiagnosticLocation(diagnostic) {
37321
37617
  return `${file2}:${line}:${column}`;
37322
37618
  }
37323
37619
  function formatDiagnosticsDetails(details) {
37324
- const diagnostics = Array.isArray(details?.diagnostics) ? details.diagnostics.map(asRecord2).filter(Boolean) : [];
37620
+ const diagnostics = Array.isArray(details?.diagnostics) ? details.diagnostics.map(asRecord3).filter(Boolean) : [];
37325
37621
  return diagnostics.map((diagnostic) => {
37326
- const severity = asString(diagnostic.severity) ?? "information";
37327
- const message = asString(diagnostic.message) ?? "(no message)";
37328
- const source = asString(diagnostic.source);
37622
+ const severity = asString2(diagnostic.severity) ?? "information";
37623
+ const message = asString2(diagnostic.message) ?? "(no message)";
37624
+ const source = asString2(diagnostic.source);
37329
37625
  const suffix = source ? ` [${source}]` : "";
37330
37626
  return `${formatDiagnosticLocation(diagnostic)} ${severity} ${message}${suffix}`;
37331
37627
  });
37332
37628
  }
37333
37629
  function renderInspectDiagnostics(response) {
37334
37630
  const lines = [];
37335
- const summaryLine = formatDiagnosticsSummary(asRecord2(response.summary));
37631
+ const summaryLine = formatDiagnosticsSummary(asRecord3(response.summary));
37336
37632
  if (summaryLine)
37337
37633
  lines.push(summaryLine);
37338
- const detailLines = formatDiagnosticsDetails(asRecord2(response.details));
37634
+ const detailLines = formatDiagnosticsDetails(asRecord3(response.details));
37339
37635
  if (detailLines.length > 0) {
37340
37636
  lines.push("diagnostics details:", ...detailLines.map((line) => `- ${line}`));
37341
37637
  }
@@ -37368,7 +37664,7 @@ async function resolveAndGateScope(ctx, context, scope) {
37368
37664
  if (checked.has(target))
37369
37665
  continue;
37370
37666
  checked.add(target);
37371
- const denial = await assertExternalDirectoryPermission(context, target);
37667
+ const denial = await assertExternalDirectoryPermission(ctx, context, target);
37372
37668
  if (denial)
37373
37669
  return { scope: undefined, denial };
37374
37670
  }
@@ -37465,6 +37761,7 @@ function navigationTools(ctx) {
37465
37761
 
37466
37762
  ` + `All ops require both 'filePath' and 'symbol'. 'expression' is additionally required for trace_data; 'toSymbol' for trace_to_symbol.
37467
37763
 
37764
+ ` + `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.
37468
37765
  `,
37469
37766
  args: {
37470
37767
  op: z11.enum(["call_tree", "callers", "trace_to", "trace_to_symbol", "impact", "trace_data"]).describe("Navigation operation"),
@@ -37495,7 +37792,7 @@ function navigationTools(ctx) {
37495
37792
  if (checked.has(target))
37496
37793
  continue;
37497
37794
  checked.add(target);
37498
- const denial = await assertExternalDirectoryPermission(context, target);
37795
+ const denial = await assertExternalDirectoryPermission(ctx, context, target);
37499
37796
  if (denial)
37500
37797
  return permissionDeniedResponse(denial);
37501
37798
  }
@@ -37520,7 +37817,8 @@ function navigationTools(ctx) {
37520
37817
  }
37521
37818
  throw new Error(message);
37522
37819
  }
37523
- return JSON.stringify(response);
37820
+ return formatCallgraphSections(args.op, response).join(`
37821
+ `);
37524
37822
  }
37525
37823
  }
37526
37824
  };
@@ -37571,7 +37869,7 @@ function readingTools(ctx) {
37571
37869
  throw new Error("'target' must be a non-empty string or array of strings");
37572
37870
  }
37573
37871
  const resolvedTargets = await Promise.all(target.map((entry) => resolvePathArg(ctx, context, entry)));
37574
- const permissionDenied3 = await assertPathExternalPermissions(context, resolvedTargets, "directory");
37872
+ const permissionDenied3 = await assertPathExternalPermissions(ctx, context, resolvedTargets, "directory");
37575
37873
  if (permissionDenied3)
37576
37874
  return permissionDeniedResponse(permissionDenied3);
37577
37875
  const response3 = await callBridge(ctx, context, "outline", {
@@ -37593,7 +37891,7 @@ function readingTools(ctx) {
37593
37891
  const st = await stat(resolvedPath);
37594
37892
  isDirectory2 = st.isDirectory();
37595
37893
  } catch {}
37596
- const permissionDenied2 = await assertPathExternalPermissions(context, resolvedPath, isDirectory2 ? "directory" : "file");
37894
+ const permissionDenied2 = await assertPathExternalPermissions(ctx, context, resolvedPath, isDirectory2 ? "directory" : "file");
37597
37895
  if (permissionDenied2)
37598
37896
  return permissionDeniedResponse(permissionDenied2);
37599
37897
  const params = isDirectory2 ? { directory: resolvedPath, files: true } : { file: resolvedPath, files: true };
@@ -37612,7 +37910,7 @@ function readingTools(ctx) {
37612
37910
  }
37613
37911
  if (isArray) {
37614
37912
  const resolvedTargets = await Promise.all(target.map((entry) => resolvePathArg(ctx, context, entry)));
37615
- const permissionDenied2 = await assertPathExternalPermissions(context, resolvedTargets);
37913
+ const permissionDenied2 = await assertPathExternalPermissions(ctx, context, resolvedTargets);
37616
37914
  if (permissionDenied2)
37617
37915
  return permissionDeniedResponse(permissionDenied2);
37618
37916
  const response2 = await callBridge(ctx, context, "outline", {
@@ -37633,7 +37931,7 @@ function readingTools(ctx) {
37633
37931
  const st = await stat(resolvedTarget);
37634
37932
  isDirectory = st.isDirectory();
37635
37933
  } catch {}
37636
- const permissionDenied = await assertPathExternalPermissions(context, resolvedTarget, isDirectory ? "directory" : "file");
37934
+ const permissionDenied = await assertPathExternalPermissions(ctx, context, resolvedTarget, isDirectory ? "directory" : "file");
37637
37935
  if (permissionDenied)
37638
37936
  return permissionDeniedResponse(permissionDenied);
37639
37937
  if (isDirectory) {
@@ -37728,7 +38026,7 @@ function readingTools(ctx) {
37728
38026
  }
37729
38027
  }
37730
38028
  const resolvedTargets = await Promise.all(targets.map((t) => resolvePathArg(ctx, context, t.filePath)));
37731
- const permissionDenied = await assertPathExternalPermissions(context, resolvedTargets);
38029
+ const permissionDenied = await assertPathExternalPermissions(ctx, context, resolvedTargets);
37732
38030
  if (permissionDenied)
37733
38031
  return permissionDeniedResponse(permissionDenied);
37734
38032
  const responses = await Promise.all(targets.map((t, index) => {
@@ -37760,7 +38058,7 @@ function readingTools(ctx) {
37760
38058
  }
37761
38059
  const file2 = hasUrl ? args.url : await resolvePathArg(ctx, context, args.filePath);
37762
38060
  if (!hasUrl) {
37763
- const permissionDenied = await assertPathExternalPermissions(context, file2);
38061
+ const permissionDenied = await assertPathExternalPermissions(ctx, context, file2);
37764
38062
  if (permissionDenied)
37765
38063
  return permissionDeniedResponse(permissionDenied);
37766
38064
  }
@@ -37832,7 +38130,7 @@ function formatZoomBatchResult(targetLabel, symbols, responses) {
37832
38130
  `) };
37833
38131
  }
37834
38132
  var MAX_UNCHECKED_FILES_IN_FOOTER = 10;
37835
- async function assertPathExternalPermissions(context, target, kind = "file") {
38133
+ async function assertPathExternalPermissions(ctx, context, target, kind = "file") {
37836
38134
  const targets = Array.isArray(target) ? target : [target];
37837
38135
  const checked = new Set;
37838
38136
  for (const resolvedPath of targets) {
@@ -37842,7 +38140,7 @@ async function assertPathExternalPermissions(context, target, kind = "file") {
37842
38140
  if (checked.has(key))
37843
38141
  continue;
37844
38142
  checked.add(key);
37845
- const denial = await assertExternalDirectoryPermission(context, resolvedPath, { kind });
38143
+ const denial = await assertExternalDirectoryPermission(ctx, context, resolvedPath, { kind });
37846
38144
  if (denial)
37847
38145
  return denial;
37848
38146
  }
@@ -38008,7 +38306,7 @@ function refactoringTools(ctx) {
38008
38306
  if (asked.has(affectedPath))
38009
38307
  continue;
38010
38308
  asked.add(affectedPath);
38011
- const denial = await assertExternalDirectoryPermission(context, affectedPath);
38309
+ const denial = await assertExternalDirectoryPermission(ctx, context, affectedPath);
38012
38310
  if (denial)
38013
38311
  return permissionDeniedResponse(denial);
38014
38312
  }
@@ -38116,7 +38414,7 @@ function safetyTools(ctx) {
38116
38414
  }
38117
38415
  const previewPaths = Array.from(new Set(responsePaths(preview2)));
38118
38416
  for (const filePath2 of previewPaths) {
38119
- const denial = await assertExternalDirectoryPermission(context, filePath2);
38417
+ const denial = await assertExternalDirectoryPermission(ctx, context, filePath2);
38120
38418
  if (denial)
38121
38419
  return permissionDeniedResponse(denial);
38122
38420
  }
@@ -38140,7 +38438,7 @@ function safetyTools(ctx) {
38140
38438
  if (uniqueParents.has(parent))
38141
38439
  continue;
38142
38440
  uniqueParents.add(parent);
38143
- const denial = await assertExternalDirectoryPermission(context, abs, {
38441
+ const denial = await assertExternalDirectoryPermission(ctx, context, abs, {
38144
38442
  kind: "file"
38145
38443
  });
38146
38444
  if (denial)
@@ -38154,7 +38452,7 @@ function safetyTools(ctx) {
38154
38452
  throw new Error(bridgeErrorMessage(preview2, "checkpoint path preview failed"));
38155
38453
  }
38156
38454
  for (const filePath of new Set(responsePaths(preview2))) {
38157
- const denial = await assertExternalDirectoryPermission(context, filePath);
38455
+ const denial = await assertExternalDirectoryPermission(ctx, context, filePath);
38158
38456
  if (denial)
38159
38457
  return permissionDeniedResponse(denial);
38160
38458
  }
@@ -38349,7 +38647,7 @@ function searchTools(ctx) {
38349
38647
  return permissionDeniedResponse(grepDenied);
38350
38648
  if (pathSplit) {
38351
38649
  for (const target of searchPathTargets(projectRoot, pathSplit, "file")) {
38352
- const externalDenied = await assertExternalDirectoryPermission(context, target.target, {
38650
+ const externalDenied = await assertExternalDirectoryPermission(ctx, context, target.target, {
38353
38651
  kind: target.kind
38354
38652
  });
38355
38653
  if (externalDenied)
@@ -38402,7 +38700,7 @@ function searchTools(ctx) {
38402
38700
  return permissionDeniedResponse(globDenied);
38403
38701
  if (pathSplit) {
38404
38702
  for (const target of searchPathTargets(projectRoot, pathSplit, "directory")) {
38405
- const externalDenied = await assertExternalDirectoryPermission(context, target.target, {
38703
+ const externalDenied = await assertExternalDirectoryPermission(ctx, context, target.target, {
38406
38704
  kind: target.kind
38407
38705
  });
38408
38706
  if (externalDenied)
@@ -38568,11 +38866,11 @@ function buildWorkflowHints(opts) {
38568
38866
  }
38569
38867
  if (hasBash && hasBgBash) {
38570
38868
  sections.push([
38571
- `**Long-running commands** (builds, installs, full test suites): \`${bashName}({ background: true })\` returns a \`taskId\` immediately. Then do ONE of these:`,
38572
- "1. Keep working on something independent of the result.",
38573
- "2. End your turn a completion reminder with the result arrives automatically.",
38574
- `3. Need to react to a specific output line early? Register \`bash_watch({ taskId, pattern, background: true })\`, then end your turn — it pings you the moment the pattern appears.`,
38575
- `\`${bashStatusName}({ taskId })\` 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.`
38869
+ `**Long-running commands** (builds, installs, full test suites): \`${bashName}({ background: true })\` returns a \`taskId\` immediately.`,
38870
+ "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 timeoutMs for long commands; the user can interrupt).",
38871
+ "2. Useful parallel work available → end your turn; the completion reminder delivers the result. (Or spawn a subagent for the side work.)",
38872
+ "3. Want to react to a specific early output line async `bash_watch` (background:true + pattern).",
38873
+ "Never loop `bash_status` to wait — it's a one-shot inspector, not a wait primitive."
38576
38874
  ].join(`
38577
38875
  `));
38578
38876
  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 \`${bashStatusName}({ taskId, outputMode: "screen" })\`, and send input with \`${bashWriteName}({ taskId, input: "..." })\`.`);
@@ -38610,32 +38908,6 @@ function isTuiMode2() {
38610
38908
  function throwSentinel(command) {
38611
38909
  throw new Error(`${SENTINEL_PREFIX}${command.toUpperCase().replace(/-/g, "_")}_HANDLED__`);
38612
38910
  }
38613
- async function sendIgnoredMessage2(client, sessionID, text) {
38614
- const typedClient = client;
38615
- let agent;
38616
- try {
38617
- const ctx = await resolvePromptContext(client, sessionID);
38618
- agent = ctx?.agent;
38619
- } catch {
38620
- agent = undefined;
38621
- }
38622
- const body = {
38623
- noReply: true,
38624
- parts: [{ type: "text", text, ignored: true }]
38625
- };
38626
- if (agent)
38627
- body.agent = agent;
38628
- const promptInput = { path: { id: sessionID }, body };
38629
- if (typeof typedClient.session?.prompt === "function") {
38630
- await Promise.resolve(typedClient.session.prompt(promptInput));
38631
- return;
38632
- }
38633
- if (typeof typedClient.session?.promptAsync === "function") {
38634
- await typedClient.session.promptAsync(promptInput);
38635
- return;
38636
- }
38637
- throw new Error("[aft-plugin] client.session.prompt is unavailable");
38638
- }
38639
38911
  var PLUGIN_VERSION = (() => {
38640
38912
  try {
38641
38913
  const req = createRequire3(import.meta.url);
@@ -38657,6 +38929,7 @@ async function initializePluginForDirectory(input) {
38657
38929
  const binaryPath = await findBinary(PLUGIN_VERSION);
38658
38930
  await ensureStorageMigrated({ harness: "opencode", binaryPath, logger: bridgeLogger });
38659
38931
  const aftConfig = loadAftConfig(input.directory);
38932
+ enqueueConfigParseWarnings(input.directory, getConfigLoadErrors());
38660
38933
  const autoUpdateAbort = new AbortController;
38661
38934
  const configOverrides = {
38662
38935
  ...resolveProjectOverridesForConfigure(aftConfig),
@@ -38747,6 +39020,7 @@ ${lines}
38747
39020
  projectConfigLoader: (projectRoot) => {
38748
39021
  try {
38749
39022
  const projectConfig = loadAftConfig(projectRoot);
39023
+ enqueueConfigParseWarnings(projectRoot, getConfigLoadErrors());
38750
39024
  return resolveProjectOverridesForConfigure(projectConfig);
38751
39025
  } catch (err) {
38752
39026
  warn2(`loadAftConfig(${projectRoot}) failed; falling back to plugin-init config: ${err instanceof Error ? err.message : String(err)}`);
@@ -38805,8 +39079,7 @@ ${lines}
38805
39079
  ctx,
38806
39080
  directory: sessionDir,
38807
39081
  sessionID: completion.session_id,
38808
- client: input.client,
38809
- serverUrl: input.serverUrl?.toString()
39082
+ client: input.client
38810
39083
  }, completion);
38811
39084
  },
38812
39085
  onBashLongRunning: (reminder, bridge) => {
@@ -38815,8 +39088,7 @@ ${lines}
38815
39088
  ctx,
38816
39089
  directory: sessionDir,
38817
39090
  sessionID: reminder.session_id,
38818
- client: input.client,
38819
- serverUrl: input.serverUrl?.toString()
39091
+ client: input.client
38820
39092
  }, reminder);
38821
39093
  },
38822
39094
  onBashPatternMatch: (frame, bridge) => {
@@ -38825,8 +39097,7 @@ ${lines}
38825
39097
  ctx,
38826
39098
  directory: sessionDir,
38827
39099
  sessionID: frame.session_id,
38828
- client: input.client,
38829
- serverUrl: input.serverUrl?.toString()
39100
+ client: input.client
38830
39101
  }, frame);
38831
39102
  }
38832
39103
  };
@@ -38839,16 +39110,6 @@ ${lines}
38839
39110
  config: aftConfig,
38840
39111
  storageDir: configOverrides.storage_dir
38841
39112
  };
38842
- probeServerReachable(input.serverUrl?.toString()).then((reachable) => {
38843
- setLiveServerWakeAvailable(reachable);
38844
- if (reachable) {
38845
- log2("Live OpenCode HTTP listener reachable; bg-notifications wake path = live-server (anomalyco/opencode#28202 workaround active).");
38846
- } else {
38847
- debug("Live OpenCode HTTP listener unreachable; bg-notifications wake path = in-process-fallback. Wakes will still arrive but the upstream duplicate-runner bug (anomalyco/opencode#28202) is not worked around. Launch with `opencode --port 0` in TUI mode to activate the workaround.");
38848
- }
38849
- }).catch(() => {
38850
- setLiveServerWakeAvailable(false);
38851
- });
38852
39113
  if (onnxRuntimePromise) {
38853
39114
  onnxRuntimePromise.then((ortDylibDir) => {
38854
39115
  if (ortDylibDir) {
@@ -39083,9 +39344,24 @@ Install: ${getManualInstallHint()}`).catch(() => {});
39083
39344
  ctx,
39084
39345
  directory: sessionDir,
39085
39346
  sessionID,
39086
- client: input.client,
39087
- serverUrl: input.serverUrl?.toString()
39347
+ client: input.client
39088
39348
  });
39349
+ const configParseWarnings = drainPendingConfigParseWarnings(sessionDir);
39350
+ if (configParseWarnings.length > 0) {
39351
+ const bridge = pool.getActiveBridgeForRoot(sessionDir) ?? pool.getBridge(sessionDir);
39352
+ enqueueConfigureWarningsForSession({
39353
+ projectRoot: sessionDir,
39354
+ sessionId: sessionID,
39355
+ client: input.client,
39356
+ bridge,
39357
+ warnings: configParseWarnings,
39358
+ fallbackClient: input.client,
39359
+ storageDir: configOverrides.storage_dir,
39360
+ pluginVersion: PLUGIN_VERSION,
39361
+ serverUrl: input.serverUrl?.toString(),
39362
+ delivery: aftConfig.configure_warnings_delivery ?? "toast"
39363
+ });
39364
+ }
39089
39365
  await flushConfigureWarningsOnIdle(sessionID);
39090
39366
  },
39091
39367
  "chat.message": async (messageInput) => {