@cortexkit/aft-pi 0.61.0 → 0.62.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -8783,7 +8783,7 @@ function resolvePluginLogPath(context = {}) {
8783
8783
  var BASH_HOST_FALLBACK_BANNER = "[AFT host fallback - module transport down; no rewrites/compression/background]";
8784
8784
  var BASH_HOST_FALLBACK_MAX_OUTPUT_BYTES = 100 * 1024;
8785
8785
  var BASH_HOST_FALLBACK_MAX_TIMEOUT_MS = 10 * 60 * 1000;
8786
- var BASH_HOST_FALLBACK_REFUSAL = "AFT transport is down; only foreground execution is available in host fallback";
8786
+ var BASH_HOST_FALLBACK_REFUSAL = "AFT module connection is down";
8787
8787
  var BASH_RUNON_DESCRIPTION = 'Run on a remote build server: `linux` (2 vCPUs), or `linux,Nc` for N vCPUs such as `linux,8c`; add ",net" for outbound internet (offline by default). `windows` runs the command in Git Bash on a Windows build machine (no vCPU or network options).';
8788
8788
  var BASH_RUNON_GUIDANCE = 'When remote runs are available, put `runon: "linux"` on build and test lines (cargo, bun test), including chains and pipes. Keep git, gh, interactive and file-editing commands local, and keep a line local if it needs macOS (Seatbelt, codesign, launchd, TCC, AppKit) or runs binaries built on this machine: a remote build leaves no binaries or target/ output in the local worktree. Add `,Nc` to request N vCPUs (`linux,4c`); without it the job gets 2 vCPUs. The job sees exactly the CPUs it gets, so ask for what the command uses: 2c (the default) for a single test binary or script, 4c for `cargo check`/clippy on one crate, 8c for a workspace test suite, and 16c for a large release build.';
8789
8789
  function pathKeyForPlatform(env, platform) {
@@ -10877,7 +10877,7 @@ function isEmptyParam(value) {
10877
10877
  return false;
10878
10878
  }
10879
10879
  function coerceJsonCollectionParam(value, field) {
10880
- const shape = field === "targets" ? "an array of {path, symbol}" : "a string or array of strings";
10880
+ const shape = field !== "sections" ? "an array of {target, symbols}" : "a string or array of strings";
10881
10881
  if (typeof value === "string") {
10882
10882
  const trimmed = value.trim();
10883
10883
  if (trimmed.startsWith("[") || trimmed.startsWith("{")) {
@@ -10886,13 +10886,13 @@ function coerceJsonCollectionParam(value, field) {
10886
10886
  } catch {
10887
10887
  throw new Error(`${field} must be ${shape} (got a string that is not valid JSON)`);
10888
10888
  }
10889
- } else if (field === "targets") {
10889
+ } else if (field !== "sections") {
10890
10890
  throw new Error(`${field} must be ${shape} (got a string that is not valid JSON)`);
10891
10891
  }
10892
10892
  }
10893
10893
  if (value === undefined || value === null)
10894
10894
  return value;
10895
- if (field === "targets") {
10895
+ if (field !== "sections") {
10896
10896
  if (Array.isArray(value) || typeof value === "object")
10897
10897
  return value;
10898
10898
  } else if (typeof value === "string" || Array.isArray(value) && value.every((entry) => typeof entry === "string")) {
@@ -10915,7 +10915,7 @@ function usableZoomTargets(targets) {
10915
10915
  if (typeof entry !== "object")
10916
10916
  return true;
10917
10917
  const record = entry;
10918
- return !(isBlankParam(record.path) && isBlankParam(record.filePath) && isBlankParam(record.symbol));
10918
+ return !(isBlankParam(record.target) && isBlankParam(record.url) && isBlankParam(record.symbols) && isBlankParam(record.path) && isBlankParam(record.filePath) && isBlankParam(record.symbol));
10919
10919
  });
10920
10920
  }
10921
10921
  function isFindReplaceOnlyEdit(args) {
@@ -11090,6 +11090,7 @@ function aftLiveConfigKeys(resolveBash) {
11090
11090
  return [
11091
11091
  pathKey("configure_warnings_delivery"),
11092
11092
  pathKey("restrict_to_project_root"),
11093
+ pathKey("revision_reads"),
11093
11094
  pathKey("inspect.diagnostics_timeout_ms"),
11094
11095
  pathKey("inspect.tier2_idle_minutes"),
11095
11096
  pathKey("lsp.idle_minutes"),
@@ -11107,7 +11108,8 @@ function aftLiveConfigKeys(resolveBash) {
11107
11108
  bashKey("runon_enabled", resolveBash),
11108
11109
  bashKey("subagent_background", resolveBash),
11109
11110
  bashKey("watch_sync_max_ms", resolveBash),
11110
- bashKey("worker_wait_max_ms", resolveBash)
11111
+ bashKey("worker_wait_max_ms", resolveBash),
11112
+ bashKey("env", resolveBash)
11111
11113
  ];
11112
11114
  }
11113
11115
  function sameValue(a, b) {
@@ -15035,6 +15037,47 @@ function isSocketDeathError(error, depth = 0) {
15035
15037
  }
15036
15038
  return isConsumerReconnectTransient(error) || isSubcClientClosedError(error);
15037
15039
  }
15040
+ var REQUEST_CONNECTION_LOSS_CODES = new Set(["ECONNABORTED", "ECONNRESET", "EPIPE"]);
15041
+ function hasRequestConnectionLossCode(error) {
15042
+ if (typeof error !== "object" || error === null || !("code" in error))
15043
+ return false;
15044
+ return typeof error.code === "string" && REQUEST_CONNECTION_LOSS_CODES.has(error.code);
15045
+ }
15046
+ function hasRequestConnectionLossEvidence(error, depth = 0) {
15047
+ if (error instanceof SocketClosedError)
15048
+ return true;
15049
+ if (error instanceof SubcError && error.closeReason === "connection_lost")
15050
+ return true;
15051
+ if (hasRequestConnectionLossCode(error))
15052
+ return true;
15053
+ if (!(error instanceof SubcCallError) || error.kind !== "outcome_unknown" || depth >= 4 || error.cause === undefined || error.cause === error) {
15054
+ return false;
15055
+ }
15056
+ return hasRequestConnectionLossEvidence(error.cause, depth + 1);
15057
+ }
15058
+ function connectionClosedAfterDispatch(error) {
15059
+ if (error instanceof SubcCallError && error.kind === "not_sent")
15060
+ return error;
15061
+ if (typeof error === "object" && error !== null && "kind" in error && error.kind === "not_sent") {
15062
+ return error;
15063
+ }
15064
+ if (!hasRequestConnectionLossEvidence(error))
15065
+ return error;
15066
+ if (error instanceof SubcCallError)
15067
+ return error;
15068
+ if (!(error instanceof Error))
15069
+ return error;
15070
+ return Object.assign(error, { kind: "outcome_unknown" });
15071
+ }
15072
+ var dispatchedRequestDeadlines = new WeakSet;
15073
+ function noteDispatchedRequestDeadline(error) {
15074
+ if (error instanceof SubcError && error.code === REQUEST_DEADLINE_CODE) {
15075
+ dispatchedRequestDeadlines.add(error);
15076
+ }
15077
+ }
15078
+ function isDispatchedRequestDeadlineError(error) {
15079
+ return typeof error === "object" && error !== null && dispatchedRequestDeadlines.has(error);
15080
+ }
15038
15081
  function isUnansweredCallError(error) {
15039
15082
  if (isSocketDeathError(error))
15040
15083
  return false;
@@ -15641,7 +15684,24 @@ class SubcTransport {
15641
15684
  return {};
15642
15685
  const preview = options.preview;
15643
15686
  const timeoutMs = options.transportTimeoutMs ?? options.timeoutMs;
15644
- const onProgress = options.onProgress;
15687
+ const callback = options.onProgress;
15688
+ const onProgress = callback ? (body) => {
15689
+ let frame;
15690
+ try {
15691
+ frame = JSON.parse(new TextDecoder().decode(body));
15692
+ } catch {
15693
+ return;
15694
+ }
15695
+ if (typeof frame !== "object" || frame === null)
15696
+ return;
15697
+ const progress = frame;
15698
+ if (progress.type !== "progress" || typeof progress.chunk !== "string")
15699
+ return;
15700
+ callback({
15701
+ kind: progress.kind === "stderr" ? "stderr" : "stdout",
15702
+ text: progress.chunk
15703
+ });
15704
+ } : undefined;
15645
15705
  return { preview, timeoutMs, onProgress, abortSignal: options.abortSignal };
15646
15706
  }
15647
15707
  }
@@ -15653,7 +15713,7 @@ class SubcTransportPool {
15653
15713
  consumerIdentity;
15654
15714
  hostScopes;
15655
15715
  hostScopeCache = new Map;
15656
- hostScopeRefusals = new Set;
15716
+ hostScopeRefusals = new Map;
15657
15717
  hostScopeOutcomes = new Map;
15658
15718
  connectFn;
15659
15719
  onBgEventsNudge;
@@ -16540,7 +16600,7 @@ class SubcTransportPool {
16540
16600
  this.resetRouteReopenBackoff();
16541
16601
  return reply;
16542
16602
  } catch (requestError) {
16543
- const error = frameReadTimeoutError(routeClosedMidCallError(requestError, routeAndEntry.entry, record));
16603
+ const error = frameReadTimeoutError(routeClosedMidCallError(connectionClosedAfterDispatch(requestError), routeAndEntry.entry, record));
16544
16604
  if (this.isReapInduced(record))
16545
16605
  throw this.annotateReapError(error, record);
16546
16606
  const ownsRoute = this.isCurrentSession(key, record) && this.client === client;
@@ -16574,6 +16634,7 @@ class SubcTransportPool {
16574
16634
  continue;
16575
16635
  }
16576
16636
  handleRequestFailure(error, routeAndEntry.entry);
16637
+ noteDispatchedRequestDeadline(error);
16577
16638
  throw error;
16578
16639
  }
16579
16640
  }
@@ -16707,20 +16768,38 @@ class SubcTransportPool {
16707
16768
  return { route: await opening, entry };
16708
16769
  }
16709
16770
  hostScope(client, identity) {
16710
- if (this.hostScopeRefusals.has(identity.session))
16711
- return Promise.resolve(undefined);
16771
+ let lookupReason = "cache_miss";
16772
+ const refused = this.hostScopeRefusals.get(identity.session);
16773
+ if (refused) {
16774
+ const previous = this.hostScopeCache.get(identity.session);
16775
+ if (previous)
16776
+ this.cancelHostScopeRetry(previous);
16777
+ this.hostScopeCache.delete(identity.session);
16778
+ this.hostScopeRefusals.delete(identity.session);
16779
+ lookupReason = `previous_bind_refusal code=${refused}`;
16780
+ }
16712
16781
  let state = this.hostScopeCache.get(identity.session);
16713
16782
  if (!state) {
16714
16783
  state = { retryAt: 0, retryMs: HOST_SCOPE_RETRY_FLOOR_MS };
16715
16784
  this.hostScopeCache.set(identity.session, state);
16716
16785
  }
16717
16786
  if (state.answer) {
16718
- return Promise.resolve(state.answer.result === "scope" && !state.scopeBindPending ? state.answer.scope : undefined);
16787
+ const reason = state.scopeBindPending ? "scope_sync_pending" : `cached_${state.answer.result}`;
16788
+ log(`subc head scope: session=${identity.session} lookup=skipped reason=${reason}`);
16789
+ return Promise.resolve({
16790
+ scope: state.answer.result === "scope" && !state.scopeBindPending ? state.answer.scope : undefined,
16791
+ reason
16792
+ });
16719
16793
  }
16720
- if (state.pending)
16794
+ if (state.pending) {
16795
+ log(`subc head scope: session=${identity.session} lookup=skipped reason=inflight_lookup`);
16721
16796
  return state.pending;
16722
- if (Date.now() < state.retryAt)
16723
- return Promise.resolve(undefined);
16797
+ }
16798
+ if (Date.now() < state.retryAt) {
16799
+ log(`subc head scope: session=${identity.session} lookup=skipped reason=backoff retry_in=${state.retryAt - Date.now()}ms`);
16800
+ return Promise.resolve({ reason: state.unscopedReason ?? "lookup_backoff" });
16801
+ }
16802
+ log(`subc head scope: session=${identity.session} lookup=started reason=${lookupReason}`);
16724
16803
  const current = state;
16725
16804
  clearTimeout(current.retryTimer);
16726
16805
  current.retryTimer = undefined;
@@ -16728,24 +16807,32 @@ class SubcTransportPool {
16728
16807
  current.lookupAbort = abort;
16729
16808
  const lookup = this.readHostScope(client, identity, abort.signal).then((outcome) => {
16730
16809
  if (this.hostScopeCache.get(identity.session) !== current)
16731
- return;
16810
+ return { reason: "lookup_invalidated" };
16732
16811
  current.pending = undefined;
16733
16812
  current.lookupAbort = undefined;
16734
16813
  if (abort.signal.aborted)
16735
- return;
16814
+ return { reason: "lookup_cancelled" };
16736
16815
  this.logHostScopeOutcome(identity.session, outcome);
16816
+ const reason = outcome.result === "error" ? `lookup_error code=${outcome.code}` : `lookup_${outcome.result}`;
16737
16817
  if (outcome.result === "scope" || outcome.result === "no_scope") {
16738
16818
  current.answer = outcome;
16739
16819
  current.retryAt = 0;
16740
16820
  current.retryMs = HOST_SCOPE_RETRY_FLOOR_MS;
16741
- if (outcome.result === "scope")
16742
- this.promoteHostScope(identity.session);
16821
+ if (outcome.result === "scope") {
16822
+ current.promoteAfterBind = Boolean(refused);
16823
+ if (!current.promoteAfterBind)
16824
+ this.promoteHostScope(identity.session);
16825
+ }
16743
16826
  } else {
16827
+ current.unscopedReason = reason;
16744
16828
  current.retryAt = Date.now() + current.retryMs;
16745
16829
  current.retryMs = Math.min(current.retryMs * 2, HOST_SCOPE_RETRY_CAP_MS);
16746
16830
  this.scheduleHostScopeRetry(identity.session, current);
16747
16831
  }
16748
- return outcome.result === "scope" ? outcome.scope : undefined;
16832
+ return {
16833
+ scope: outcome.result === "scope" ? outcome.scope : undefined,
16834
+ reason
16835
+ };
16749
16836
  });
16750
16837
  current.pending = lookup;
16751
16838
  return lookup;
@@ -16792,7 +16879,7 @@ class SubcTransportPool {
16792
16879
  retry = this.hostScope(client, record.identity);
16793
16880
  }
16794
16881
  retry.catch((error) => {
16795
- warn(`subc head scope: background retry failed code=${error instanceof Error ? error.name : "Error"}`);
16882
+ warn(`subc head scope: session=${session} background retry failed code=${error instanceof Error ? error.name : "Error"}`);
16796
16883
  });
16797
16884
  }, Math.max(0, state.retryAt - Date.now()));
16798
16885
  state.retryTimer.unref();
@@ -16808,8 +16895,8 @@ class SubcTransportPool {
16808
16895
  }
16809
16896
  refuseHostScopeBind(session, state, code) {
16810
16897
  if (!this.hostScopeRefusals.has(session)) {
16811
- this.hostScopeRefusals.add(session);
16812
- warn(`subc head scope: session=${session} scoped bind refused code=${code}; binding unscoped; retry stopped`);
16898
+ this.hostScopeRefusals.set(session, code);
16899
+ warn(`subc head scope: session=${session} scoped bind refused code=${code}; retry stopped until next bind lookup`);
16813
16900
  }
16814
16901
  if (state) {
16815
16902
  state.scopeBindPending = false;
@@ -16818,11 +16905,12 @@ class SubcTransportPool {
16818
16905
  }
16819
16906
  async retryHostScopeBind(client, identity, state, scope) {
16820
16907
  try {
16821
- const route = await client.routeOpen({ kind: "tool_provider", module_id: AFT_MODULE_ID }, identity, { consumerIdentity: this.consumerIdentity, scope });
16908
+ const route = await this.bindHostToolRoute(client, identity, "scope_probe", "scope_sync_retry", scope);
16822
16909
  safeCloseRoute(client, route);
16823
16910
  if (this.hostScopeCache.get(identity.session) !== state || !state.scopeBindPending || this.shuttingDown || client !== this.client || !this.unscopedHostRecord(identity.session))
16824
16911
  return;
16825
16912
  state.scopeBindPending = false;
16913
+ state.promoteAfterBind = false;
16826
16914
  state.retryAt = 0;
16827
16915
  state.retryMs = HOST_SCOPE_RETRY_FLOOR_MS;
16828
16916
  log(`subc head scope: session=${identity.session} scoped bind recovered ref=${scope.ref.slice(0, 8)} epoch=${scope.scopeEpoch}`);
@@ -16948,24 +17036,39 @@ class SubcTransportPool {
16948
17036
  signal.removeEventListener("abort", cancel);
16949
17037
  }
16950
17038
  }
16951
- openHostToolRoute(client, identity, record, entry) {
17039
+ openHostToolRoute(client, identity, record, entry, channel = "tool") {
16952
17040
  if (!this.hostScopes || !["opencode", "pi"].includes(identity.harness) || identity.session === DEFAULT_SESSION_ID) {
16953
- return client.routeOpen({ kind: "tool_provider", module_id: AFT_MODULE_ID }, identity, {
16954
- consumerIdentity: this.consumerIdentity
16955
- });
17041
+ const reason = !this.hostScopes ? "host_scopes_disabled" : identity.session === DEFAULT_SESSION_ID ? "default_session" : "unsupported_harness";
17042
+ log(`subc head scope: session=${identity.session} lookup=skipped reason=${reason}`);
17043
+ return this.bindHostToolRoute(client, identity, channel, reason);
16956
17044
  }
16957
- return this.openScopedHostToolRoute(client, identity, record, entry);
17045
+ return this.openScopedHostToolRoute(client, identity, record, entry, channel);
16958
17046
  }
16959
- async openScopedHostToolRoute(client, identity, record, entry) {
16960
- const scope = await this.hostScope(client, identity);
17047
+ bindHostToolRoute(client, identity, channel, reason, scope) {
17048
+ log(`subc head scope: session=${identity.session} bind=${scope ? "scoped" : "unscoped"} channel=${channel} root=${identity.project_root} reason=${reason}` + (scope ? ` ref=${scope.ref.slice(0, 8)} epoch=${scope.scopeEpoch}` : ""));
17049
+ return client.routeOpen({ kind: "tool_provider", module_id: AFT_MODULE_ID }, identity, {
17050
+ consumerIdentity: this.consumerIdentity,
17051
+ ...scope ? { scope } : {}
17052
+ });
17053
+ }
17054
+ async openScopedHostToolRoute(client, identity, record, entry, channel = "tool") {
17055
+ const decision = await this.hostScope(client, identity);
17056
+ const scope = decision.scope;
17057
+ let reason = decision.reason;
16961
17058
  this.assertRecordLive(record);
16962
17059
  if (scope) {
16963
17060
  try {
16964
- const route = await client.routeOpen({ kind: "tool_provider", module_id: AFT_MODULE_ID }, identity, { consumerIdentity: this.consumerIdentity, scope });
17061
+ const route = await this.bindHostToolRoute(client, identity, channel, reason, scope);
16965
17062
  if (entry)
16966
17063
  entry.hostScope = scope;
17064
+ const state = this.hostScopeCache.get(identity.session);
17065
+ if (state?.promoteAfterBind && state.answer?.result === "scope" && state.answer.scope === scope) {
17066
+ state.promoteAfterBind = false;
17067
+ this.promoteHostScope(identity.session);
17068
+ }
16967
17069
  return route;
16968
17070
  } catch (error) {
17071
+ log(`subc head scope: session=${identity.session} bind=failed channel=${channel} reason=scoped_open_error code=${error instanceof SubcError ? error.code ?? error.name : "Error"}`);
16969
17072
  if (!(error instanceof SubcError) || !error.code || isSocketDeathError(error))
16970
17073
  throw error;
16971
17074
  if (isRouteOpenReloadWindowError(error))
@@ -16974,18 +17077,21 @@ class SubcTransportPool {
16974
17077
  const state = this.hostScopeCache.get(identity.session);
16975
17078
  if (state && isHostScopeSyncRefusal(error.code)) {
16976
17079
  this.deferHostScopeBind(identity.session, state, error.code);
17080
+ reason = `scope_sync_pending code=${error.code}`;
16977
17081
  } else {
16978
17082
  this.refuseHostScopeBind(identity.session, state, error.code);
17083
+ if (decision.reason === "cached_scope") {
17084
+ return this.openScopedHostToolRoute(client, identity, record, entry, channel);
17085
+ }
17086
+ reason = `scoped_bind_refused code=${error.code}`;
16979
17087
  }
16980
17088
  }
16981
17089
  }
16982
17090
  this.assertRecordLive(record);
16983
- const route = await client.routeOpen({ kind: "tool_provider", module_id: AFT_MODULE_ID }, identity, {
16984
- consumerIdentity: this.consumerIdentity
16985
- });
17091
+ const route = await this.bindHostToolRoute(client, identity, channel, reason);
16986
17092
  if (this.hostScopeCache.get(identity.session)?.answer?.result === "scope" && !this.hostScopeCache.get(identity.session)?.scopeBindPending && !this.hostScopeRefusals.has(identity.session)) {
16987
17093
  safeCloseRoute(client, route);
16988
- return this.openScopedHostToolRoute(client, identity, record, entry);
17094
+ return this.openScopedHostToolRoute(client, identity, record, entry, channel);
16989
17095
  }
16990
17096
  return route;
16991
17097
  }
@@ -17082,7 +17188,7 @@ class SubcTransportPool {
17082
17188
  };
17083
17189
  sub = new BgSubscription(identity, () => this.ensureClient(), (client, cause) => this.dropClient(client, cause), (client) => {
17084
17190
  binding.hostScope = undefined;
17085
- return this.openHostToolRoute(client, identity, record, binding);
17191
+ return this.openHostToolRoute(client, identity, record, binding, "bg_events");
17086
17192
  }, onNudge, this.bgBackoffSleep, () => this.rootCanAttach(record.canonicalRoot), (error) => {
17087
17193
  if (!isAbsentRootRouteError(error))
17088
17194
  return false;
@@ -17375,11 +17481,54 @@ function shellTimeoutDisposition(outcome) {
17375
17481
  var SUBC_MODULE_RESTART_DISPOSITION = "The AFT daemon module restarted while this call was in flight, so its outcome is UNKNOWN: it may or may not have executed. Verify actual state before re-running, and never blind-retry a mutation.";
17376
17482
  var BRIDGE_TRANSPORT_UNKNOWN_OUTCOME_DISPOSITION = "The standalone AFT transport failed after this call may have been sent, so its outcome is UNKNOWN: it may or may not have executed. Verify actual state before re-running, and never blind-retry a mutation.";
17377
17483
  var SUBC_ROUTE_CLOSED_MID_CALL_DISPOSITION = "The plugin closed this call's route while the call was in flight, so its outcome is UNKNOWN: it may or may not have executed. Verify actual state before re-running, and never blind-retry a mutation.";
17484
+ var SUBC_CONNECTION_CLOSED_MID_CALL_DISPOSITION = "The AFT module connection closed while this request was in flight, so its outcome is UNKNOWN: it may or may not have executed. Verify actual state before retrying, and never blindly retry a mutation.";
17485
+ function bashConnectionClosedDisposition(taskId) {
17486
+ const taskFollowup = taskId ? `Use bash_status or bash_watch with task ID ${taskId} to check it.` : "If a task ID is available, use it with bash_status or bash_watch; otherwise wait for the completion notice.";
17487
+ return `The command may have run or may still be running. AFT keeps commands running when the connection drops; a completion notice will follow if it ran. ${taskFollowup} Do not rerun it.`;
17488
+ }
17489
+
17490
+ class AftModuleUnavailableError extends SubcCallError {
17491
+ constructor(message, cause) {
17492
+ super("not_sent", message, "aft_module_unavailable", cause);
17493
+ this.name = "AftModuleUnavailableError";
17494
+ }
17495
+ }
17496
+ function refuseAftModuleOptionWithoutFallback(error, option, refusalPrefix) {
17497
+ return new AftModuleUnavailableError(`${refusalPrefix}; ${option} has no host fallback. Nothing was started, so it is safe to retry after the AFT module connection is restored.`, error);
17498
+ }
17378
17499
  var BASH_FRAME_READ_TIMEOUT_DISPOSITION = "The command's state is UNKNOWN: the transport timed out while reading the response frame, so it may or may not have executed. Check bash_status if a task ID could have been assigned, and do not re-run side-effecting commands blindly.";
17379
17500
  var SUBC_FRAME_READ_TIMEOUT_DISPOSITION = "The transport timed out while reading the response frame for this call, so its outcome is UNKNOWN: it may or may not have executed. Verify actual state before re-running, and never blind-retry a mutation.";
17380
17501
  function isRouteClosedMidCallError(error) {
17381
17502
  return error instanceof SubcRouteClosedMidCallError || isLocalRouteCloseRejection(error);
17382
17503
  }
17504
+ function isSubcConnectionClosedMidCallError(error) {
17505
+ if (!(error instanceof Error))
17506
+ return false;
17507
+ const fields = error;
17508
+ if (fields.kind !== "outcome_unknown")
17509
+ return false;
17510
+ if (error instanceof SocketClosedError)
17511
+ return true;
17512
+ if (error instanceof SubcError)
17513
+ return error.closeReason === "connection_lost";
17514
+ if (error instanceof SubcCallError) {
17515
+ return error.closeReason === "connection_lost" || hasConnectionLossCause(error);
17516
+ }
17517
+ return hasConnectionLossCause(error);
17518
+ }
17519
+ var CONNECTION_LOSS_ERROR_CODES = new Set(["ECONNABORTED", "ECONNRESET", "EPIPE"]);
17520
+ function hasConnectionLossCause(error, depth = 0) {
17521
+ if (error instanceof SocketClosedError)
17522
+ return true;
17523
+ if (error instanceof SubcError && error.closeReason === "connection_lost")
17524
+ return true;
17525
+ if (typeof error === "object" && error !== null && "code" in error && typeof error.code === "string" && CONNECTION_LOSS_ERROR_CODES.has(error.code)) {
17526
+ return true;
17527
+ }
17528
+ if (!(error instanceof Error) || depth >= 4 || error.cause === undefined)
17529
+ return false;
17530
+ return hasConnectionLossCause(error.cause, depth + 1);
17531
+ }
17383
17532
  function isRouteGoodbyeError(error) {
17384
17533
  if (!(error instanceof SubcError))
17385
17534
  return false;
@@ -17434,6 +17583,8 @@ function classifyBashHostFallbackError(error) {
17434
17583
  if (!(error instanceof Error) || hasEngineResponse(error) || isRouteGoodbyeError(error)) {
17435
17584
  return;
17436
17585
  }
17586
+ if (isSubcConnectionClosedMidCallError(error))
17587
+ return;
17437
17588
  if (error instanceof BridgeTransportUnknownOutcomeError)
17438
17589
  return;
17439
17590
  if (isRouteClosedMidCallError(error))
@@ -17460,6 +17611,8 @@ function isBashTransportDeadError(error) {
17460
17611
  function adaptToolError(command, error) {
17461
17612
  if (!(error instanceof Error))
17462
17613
  return error;
17614
+ if (error instanceof AftModuleUnavailableError)
17615
+ return error;
17463
17616
  if (isSubcFrameReadTimeoutError(error)) {
17464
17617
  const typedError = error instanceof SubcFrameReadTimeoutError ? error : new SubcFrameReadTimeoutError(error);
17465
17618
  const disposition = command === "bash" ? BASH_FRAME_READ_TIMEOUT_DISPOSITION : SUBC_FRAME_READ_TIMEOUT_DISPOSITION;
@@ -17474,6 +17627,13 @@ function adaptToolError(command, error) {
17474
17627
  error.message = error.message ? `${error.message} ${BRIDGE_TRANSPORT_UNKNOWN_OUTCOME_DISPOSITION}` : BRIDGE_TRANSPORT_UNKNOWN_OUTCOME_DISPOSITION;
17475
17628
  return error;
17476
17629
  }
17630
+ if (isSubcConnectionClosedMidCallError(error)) {
17631
+ const disposition = command === "bash" ? bashConnectionClosedDisposition(taskIdFromError(error)) : SUBC_CONNECTION_CLOSED_MID_CALL_DISPOSITION;
17632
+ if (error.message.includes(disposition))
17633
+ return error;
17634
+ error.message = error.message ? `${error.message} ${disposition}` : disposition;
17635
+ return error;
17636
+ }
17477
17637
  if (isRouteGoodbyeError(error)) {
17478
17638
  if (error.message.includes(SUBC_MODULE_RESTART_DISPOSITION))
17479
17639
  return error;
@@ -17486,6 +17646,13 @@ function adaptToolError(command, error) {
17486
17646
  error.message = error.message ? `${error.message} ${SUBC_ROUTE_CLOSED_MID_CALL_DISPOSITION}` : SUBC_ROUTE_CLOSED_MID_CALL_DISPOSITION;
17487
17647
  return error;
17488
17648
  }
17649
+ if (command === "bash" && isDispatchedRequestDeadlineError(error)) {
17650
+ const disposition = bashConnectionClosedDisposition(taskIdFromError(error));
17651
+ if (error.message.includes(disposition))
17652
+ return error;
17653
+ error.message = error.message ? `${error.message} ${disposition}` : disposition;
17654
+ return error;
17655
+ }
17489
17656
  if ((command === "bash" || command === "powershell") && isBridgeTransportTimeout(error)) {
17490
17657
  const outcome = error.shellOutcome;
17491
17658
  const disposition = shellTimeoutDisposition(outcome);
@@ -17494,6 +17661,9 @@ function adaptToolError(command, error) {
17494
17661
  error.message = error.message ? `${error.message} ${disposition}` : disposition;
17495
17662
  return error;
17496
17663
  }
17664
+ if (command !== "bash" && isBashTransportDeadError(error)) {
17665
+ return new AftModuleUnavailableError("The AFT module connection is down or unavailable; this AFT-backed option has no host fallback. Nothing was started, so it is safe to retry after the connection is restored.", error);
17666
+ }
17497
17667
  if (command !== "bash" || !isBashTransportDeadError(error))
17498
17668
  return error;
17499
17669
  if (error.message.includes(BASH_TRANSPORT_DISPOSITION))
@@ -17501,6 +17671,15 @@ function adaptToolError(command, error) {
17501
17671
  error.message = error.message ? `${error.message} ${BASH_TRANSPORT_DISPOSITION}` : BASH_TRANSPORT_DISPOSITION;
17502
17672
  return error;
17503
17673
  }
17674
+ function taskIdFromError(error) {
17675
+ for (let current = error, depth = 0;current instanceof Error && depth < 5; current = current.cause, depth += 1) {
17676
+ const fields = current;
17677
+ const taskId = fields.taskId ?? fields.task_id;
17678
+ if (typeof taskId === "string" && taskId.length > 0)
17679
+ return taskId;
17680
+ }
17681
+ return;
17682
+ }
17504
17683
  // ../aft-bridge/dist/feature-config.js
17505
17684
  import { createHash as createHash3 } from "node:crypto";
17506
17685
  var CANONICAL_TOOLS = [
@@ -22521,39 +22700,70 @@ function validateOptionalCanonicalPath(record, property) {
22521
22700
  }
22522
22701
  pathValue(record, property);
22523
22702
  }
22524
- function normalizeZoomTargets(record) {
22525
- if (!hasOwn(record, "targets"))
22526
- return;
22527
- const targets = record.targets;
22528
- const normalizeTarget = (target, index) => {
22529
- if (!target || typeof target !== "object" || Array.isArray(target)) {
22530
- throw new InvalidRequestError(`'targets[${index}].path' must be a non-empty string`);
22531
- }
22532
- const source = target;
22533
- const blank = (value) => value === undefined || value === null || typeof value === "string" && !value.trim();
22534
- const emptyTarget = blank(source.symbol) && blank(source.path) && blank(source.filePath);
22535
- if (emptyTarget)
22536
- return { ...source };
22537
- const normalized = { ...source };
22538
- try {
22539
- normalizeAliasPair(normalized, "path", "filePath", false);
22540
- } catch (error) {
22541
- if (error instanceof InvalidRequestError) {
22542
- throw new InvalidRequestError(error.message.replace("'filePath'", `'targets[${index}].filePath'`).replace("'path'", `'targets[${index}].path'`));
22543
- }
22544
- throw error;
22703
+ function normalizeZoomLocation(record, prefix = "") {
22704
+ let first;
22705
+ for (const key of ["target", "path", "filePath", "url"]) {
22706
+ const value = record[key];
22707
+ if (value === undefined || value === null || typeof value === "string" && !value.trim()) {
22708
+ delete record[key];
22709
+ continue;
22545
22710
  }
22546
- return normalized;
22547
- };
22548
- if (Array.isArray(targets)) {
22549
- if (targets.length === 0)
22550
- return;
22551
- record.targets = targets.map(normalizeTarget);
22552
- return;
22553
- }
22554
- if (targets && typeof targets === "object") {
22555
- record.targets = normalizeTarget(targets, 0);
22711
+ const location = pathValue(record, key);
22712
+ if (first && first.value !== location) {
22713
+ throw new InvalidRequestError(`Conflicting '${prefix}${first.key}' location '${first.value}' and '${prefix}${key}' '${location}': provide the same location or only one target`);
22714
+ }
22715
+ first ??= { key, value: location };
22556
22716
  }
22717
+ for (const alias of ["path", "filePath", "url"])
22718
+ delete record[alias];
22719
+ if (first)
22720
+ record.target = first.value;
22721
+ }
22722
+ function normalizeZoomTargets(record) {
22723
+ const normalizeEntry = (value, prefix) => {
22724
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
22725
+ throw new InvalidRequestError(`'${prefix}target' must be a non-empty string`);
22726
+ }
22727
+ const entry = { ...value };
22728
+ normalizeZoomLocation(entry, prefix);
22729
+ if (hasOwn(entry, "symbol")) {
22730
+ if (!isBlankParam(entry.symbols) && JSON.stringify(entry.symbols) !== JSON.stringify(entry.symbol)) {
22731
+ throw new InvalidRequestError(`Conflicting '${prefix}symbols' and '${prefix}symbol': provide the same symbols or only one spelling`);
22732
+ }
22733
+ if (isBlankParam(entry.symbols))
22734
+ entry.symbols = entry.symbol;
22735
+ delete entry.symbol;
22736
+ }
22737
+ entry.target ??= "";
22738
+ entry.symbols ??= "";
22739
+ return entry;
22740
+ };
22741
+ const batch = [];
22742
+ if (hasOwn(record, "batch")) {
22743
+ const entries = coerceJsonCollectionParam(record.batch, "batch");
22744
+ if (!Array.isArray(entries) || entries.length === 0) {
22745
+ throw new InvalidRequestError("'batch' must be a non-empty array of {target, symbols}");
22746
+ }
22747
+ batch.push(...entries.map((entry, index) => normalizeEntry(entry, `batch[${index}].`)));
22748
+ }
22749
+ normalizeZoomLocation(record);
22750
+ if (!isBlankParam(record.target) || !isBlankParam(record.symbols)) {
22751
+ const entry = {};
22752
+ if (record.target !== undefined)
22753
+ entry.target = record.target;
22754
+ if (record.symbols !== undefined)
22755
+ entry.symbols = record.symbols;
22756
+ batch.unshift(normalizeEntry(entry, ""));
22757
+ }
22758
+ if (hasOwn(record, "targets")) {
22759
+ const targets = coerceJsonCollectionParam(record.targets, "targets");
22760
+ batch.push(...usableZoomTargets(targets).map((entry, index) => normalizeEntry(entry, `targets[${index}].`)));
22761
+ }
22762
+ for (const key of ["target", "symbols", "targets"])
22763
+ delete record[key];
22764
+ if (!batch.length)
22765
+ throw new InvalidRequestError("'batch' must be a non-empty array of {target, symbols}");
22766
+ record.batch = batch;
22557
22767
  }
22558
22768
  function bareToolName(toolName) {
22559
22769
  const bare = toolName.startsWith("aft_") ? toolName.slice(4) : toolName;
@@ -22580,7 +22790,6 @@ function prepareCanonicalPathArguments(toolName, rawArguments) {
22580
22790
  normalizeAliasPair(record, "path", "filePath", true);
22581
22791
  break;
22582
22792
  case "zoom":
22583
- normalizeAliasPair(record, "path", "filePath", false);
22584
22793
  normalizeZoomTargets(record);
22585
22794
  break;
22586
22795
  case "callgraph":
@@ -23001,6 +23210,19 @@ function copyOwnProperties(source) {
23001
23210
  copy[key] = source[key];
23002
23211
  return copy;
23003
23212
  }
23213
+ // ../aft-bridge/dist/revision-reads.js
23214
+ var REVISION_REF_DESCRIPTION = "Git revision to read the file at instead of the working tree: a branch, tag, full or short commit SHA, or a form like HEAD~2. Content comes from git's object store; the working tree, index and HEAD are not touched.";
23215
+ var REVISION_REF_TOOL_NOTE = "Pass `ref` to read the file as it is at a git revision (branch, tag, SHA, HEAD~2); the reply's first line names the commit it resolved to.";
23216
+ function revisionReadsOffered(config) {
23217
+ return config.revision_reads === true;
23218
+ }
23219
+ function revisionRefArg(value) {
23220
+ if (value === undefined || value === null)
23221
+ return;
23222
+ if (typeof value === "string" && value.trim().length === 0)
23223
+ return;
23224
+ return value;
23225
+ }
23004
23226
  // ../aft-bridge/dist/semantic-status.js
23005
23227
  var MISSING_ONNX_RUNTIME_MARKER = "ONNX Runtime not found.";
23006
23228
  var MISSING_ONNX_RUNTIME_LABEL = "unavailable — ONNX Runtime missing (npx @cortexkit/aft doctor --fix)";
@@ -24142,7 +24364,7 @@ import {
24142
24364
  // package.json
24143
24365
  var package_default = {
24144
24366
  name: "@cortexkit/aft-pi",
24145
- version: "0.61.0",
24367
+ version: "0.62.0",
24146
24368
  type: "module",
24147
24369
  description: "Pi coding agent extension for Agent File Tools (AFT) — tree-sitter and LSP-powered code analysis",
24148
24370
  main: "dist/index.js",
@@ -24166,18 +24388,18 @@ var package_default = {
24166
24388
  "test:e2e": "bun test src/__tests__/e2e/"
24167
24389
  },
24168
24390
  dependencies: {
24169
- "@cortexkit/aft-bridge": "0.61.0",
24391
+ "@cortexkit/aft-bridge": "0.62.0",
24170
24392
  "comment-json": "^5.0.0",
24171
24393
  diff: "^8.0.4",
24172
24394
  zod: "^4.4.3"
24173
24395
  },
24174
24396
  optionalDependencies: {
24175
- "@cortexkit/aft-darwin-arm64": "0.61.0",
24176
- "@cortexkit/aft-darwin-x64": "0.61.0",
24177
- "@cortexkit/aft-linux-arm64": "0.61.0",
24178
- "@cortexkit/aft-linux-x64": "0.61.0",
24179
- "@cortexkit/aft-win32-arm64": "0.61.0",
24180
- "@cortexkit/aft-win32-x64": "0.61.0"
24397
+ "@cortexkit/aft-darwin-arm64": "0.62.0",
24398
+ "@cortexkit/aft-darwin-x64": "0.62.0",
24399
+ "@cortexkit/aft-linux-arm64": "0.62.0",
24400
+ "@cortexkit/aft-linux-x64": "0.62.0",
24401
+ "@cortexkit/aft-win32-arm64": "0.62.0",
24402
+ "@cortexkit/aft-win32-x64": "0.62.0"
24181
24403
  },
24182
24404
  devDependencies: {
24183
24405
  "@earendil-works/pi-coding-agent": "^0.80.2",
@@ -29280,7 +29502,8 @@ function resolveBashConfig(config) {
29280
29502
  foreground_wait_window_ms: foregroundWaitWindowMs,
29281
29503
  watch_sync_max_ms: watchSyncMaxMs,
29282
29504
  worker_wait_max_ms: (typeof top === "object" && top !== null ? top.worker_wait_max_ms : undefined) ?? DEFAULT_WORKER_WAIT_MAX_MS,
29283
- powershell_tool: typeof top === "object" && top !== null ? top.powershell_tool ?? false : false
29505
+ powershell_tool: typeof top === "object" && top !== null ? top.powershell_tool ?? false : false,
29506
+ env: typeof top === "object" && top !== null ? { ...top.env ?? {} } : {}
29284
29507
  };
29285
29508
  if (top === false)
29286
29509
  return base;
@@ -29417,6 +29640,41 @@ var ExperimentalConfigSchema = object({
29417
29640
  }).optional(),
29418
29641
  lsp_ty: boolean2().optional()
29419
29642
  });
29643
+ var BLOCKED_BASH_ENV_NAMES = new Set([
29644
+ "LD_PRELOAD",
29645
+ "LD_LIBRARY_PATH",
29646
+ "LD_AUDIT",
29647
+ "DYLD_INSERT_LIBRARIES",
29648
+ "DYLD_LIBRARY_PATH",
29649
+ "DYLD_FALLBACK_LIBRARY_PATH",
29650
+ "BASH_ENV",
29651
+ "ENV",
29652
+ "IFS",
29653
+ "PATH",
29654
+ "SHELLOPTS",
29655
+ "BASHOPTS",
29656
+ "PROMPT_COMMAND",
29657
+ "PS4",
29658
+ "PYTHONSTARTUP",
29659
+ "PYTHONPATH",
29660
+ "PYTHONHOME",
29661
+ "PERL5OPT",
29662
+ "PERL5LIB",
29663
+ "PERLLIB",
29664
+ "PERL5DB",
29665
+ "RUBYOPT",
29666
+ "RUBYLIB",
29667
+ "NODE_OPTIONS",
29668
+ "GCONV_PATH",
29669
+ "LOCPATH",
29670
+ "NLSPATH",
29671
+ "HOSTALIASES",
29672
+ "RESOLV_HOST_CONF"
29673
+ ]);
29674
+ function isBlockedBashEnvKey(key) {
29675
+ const normalized = process.platform === "win32" ? key.toUpperCase() : key;
29676
+ return BLOCKED_BASH_ENV_NAMES.has(normalized) || normalized.startsWith("LD_") || normalized.startsWith("DYLD_") || normalized.startsWith("BASH_FUNC_");
29677
+ }
29420
29678
  var BashFeaturesSchema = object({
29421
29679
  enabled: boolean2().optional(),
29422
29680
  rewrite: boolean2().optional(),
@@ -29436,7 +29694,18 @@ var BashFeaturesSchema = object({
29436
29694
  }).optional(),
29437
29695
  linux_scope: boolean2().optional(),
29438
29696
  disclaim_privacy: boolean2().optional(),
29439
- powershell_tool: boolean2().optional()
29697
+ powershell_tool: boolean2().optional(),
29698
+ env: record(string2(), string2()).superRefine((env, context) => {
29699
+ for (const key of Object.keys(env)) {
29700
+ if (isBlockedBashEnvKey(key)) {
29701
+ context.addIssue({
29702
+ code: "custom",
29703
+ path: [key],
29704
+ message: `bash.env key ${key} is blocked because it can alter startup or protected process state`
29705
+ });
29706
+ }
29707
+ }
29708
+ }).optional()
29440
29709
  });
29441
29710
  var BashConfigSchema = union([boolean2(), BashFeaturesSchema]);
29442
29711
  var SandboxConfigSchema = object({
@@ -29515,6 +29784,7 @@ var AftConfigFieldsSchema = object({
29515
29784
  $schema: string2().optional(),
29516
29785
  edit_mode: _enum(["default", "hashline"]).optional(),
29517
29786
  format_on_edit: boolean2().optional(),
29787
+ revision_reads: boolean2().optional(),
29518
29788
  formatter_timeout_secs: number2().int().min(1).max(600).optional(),
29519
29789
  validate_on_edit: _enum(["syntax", "full"]).optional(),
29520
29790
  formatter: record(string2(), FormatterEnum).optional(),
@@ -29858,6 +30128,7 @@ function getProjectLspStrippedKeys(lsp) {
29858
30128
  var PROJECT_SAFE_TOP_LEVEL_FIELDS = new Set([
29859
30129
  "edit_mode",
29860
30130
  "format_on_edit",
30131
+ "revision_reads",
29861
30132
  "validate_on_edit",
29862
30133
  "configure_warnings_delivery",
29863
30134
  "views",
@@ -29910,6 +30181,8 @@ function getStrippedTopLevelKeys(override, base) {
29910
30181
  }
29911
30182
  if (typeof override.bash === "object" && override.bash.disclaim_privacy === false)
29912
30183
  stripped.push("bash.disclaim_privacy");
30184
+ if (typeof override.bash === "object" && override.bash.env !== undefined)
30185
+ stripped.push("bash.env");
29913
30186
  if (override.sandbox?.write_allow !== undefined)
29914
30187
  stripped.push("sandbox.write_allow");
29915
30188
  if (override.subc !== undefined)
@@ -29957,8 +30230,10 @@ function mergeConfigs(base, override) {
29957
30230
  const projectBash = typeof override.bash === "object" ? { ...override.bash } : override.bash;
29958
30231
  if (typeof projectBash === "object" && projectBash.disclaim_privacy !== true)
29959
30232
  delete projectBash.disclaim_privacy;
29960
- if (typeof projectBash === "object")
30233
+ if (typeof projectBash === "object") {
29961
30234
  delete projectBash.runon_enabled;
30235
+ delete projectBash.env;
30236
+ }
29962
30237
  const bash = mergeBashConfig(base.bash, projectBash);
29963
30238
  const inspect = mergeInspectConfig(base.inspect, override.inspect);
29964
30239
  const worktree = mergeWorktreeConfig(base.worktree, override.worktree);
@@ -30424,6 +30699,10 @@ async function callToolCallForSession(bridge, name, rawArgs, sessionId, extCtx,
30424
30699
  await forwardPendingBashWaitDetach(pool, bridge, sessionId, name === "bash");
30425
30700
  response = await bridge.toolCall(sessionId, name, rawArgs, Object.keys(sendOptions).length > 0 ? sendOptions : undefined);
30426
30701
  } catch (error) {
30702
+ const noFallbackOption = name === "bash" ? rawArgs.runon !== undefined ? "runon" : rawArgs.pty === true ? "pty:true" : rawArgs.background === true ? "background:true" : undefined : undefined;
30703
+ if (noFallbackOption && isBashTransportDeadError(error)) {
30704
+ throw refuseAftModuleOptionWithoutFallback(error, noFallbackOption, BASH_HOST_FALLBACK_REFUSAL);
30705
+ }
30427
30706
  throw adaptToolError(name, error);
30428
30707
  }
30429
30708
  ingestBgCompletions(sessionId, response.bg_completions);
@@ -32605,6 +32884,22 @@ async function assertExternalReadPermission(ctx, extCtx, target, restrictToProje
32605
32884
  } catch {}
32606
32885
  await assertExternalDirectoryPermission(extCtx, absoluteTarget, { restrictToProjectRoot });
32607
32886
  }
32887
+ var RevisionRefParam = {
32888
+ ref: Type3.Optional(Type3.String({ description: REVISION_REF_DESCRIPTION }))
32889
+ };
32890
+ function withRevisionRefParam(params, config) {
32891
+ if (!revisionReadsOffered(config))
32892
+ return params;
32893
+ return Type3.Object({ ...params.properties, ...RevisionRefParam });
32894
+ }
32895
+ function withRevisionRefNote(description, config, extra = "") {
32896
+ if (!revisionReadsOffered(config))
32897
+ return description;
32898
+ const note = `${REVISION_REF_TOOL_NOTE}${extra ? ` ${extra}` : ""}`;
32899
+ return `${description}
32900
+
32901
+ ${note}`;
32902
+ }
32608
32903
  var ReadParams = Type3.Object({
32609
32904
  path: Type3.String({
32610
32905
  description: "Path to the file or directory to read (absolute or relative to project root)"
@@ -32753,10 +33048,14 @@ function registerHoistedTools(pi, ctx, surface) {
32753
33048
  pi.registerTool(withPathAliasPreparation({
32754
33049
  name: readName,
32755
33050
  label: readName,
32756
- description: readDescription(resolveGithubConfig(ctx.config).read),
33051
+ get description() {
33052
+ return withRevisionRefNote(readDescription(resolveGithubConfig(ctx.config).read), ctx.config);
33053
+ },
32757
33054
  promptSnippet: "Read files or directories (supports offset/limit for either)",
32758
33055
  promptGuidelines: [`Use ${readName} to examine files instead of cat or sed.`],
32759
- parameters: ReadParams,
33056
+ get parameters() {
33057
+ return withRevisionRefParam(ReadParams, ctx.config);
33058
+ },
32760
33059
  async execute(_toolCallId, params, _signal, _onUpdate, extCtx) {
32761
33060
  const bridge = bridgeFor(ctx, extCtx.cwd);
32762
33061
  const pathArg = readPathArg(params);
@@ -32778,6 +33077,9 @@ function registerHoistedTools(pi, ctx, surface) {
32778
33077
  rawArgs.offset = offset;
32779
33078
  if (limit !== undefined)
32780
33079
  rawArgs.limit = limit;
33080
+ const ref = revisionRefArg(params.ref);
33081
+ if (ref !== undefined)
33082
+ rawArgs.ref = ref;
32781
33083
  const visionCapability = visionCapabilityForPiModel(extCtx);
32782
33084
  if (visionCapability !== undefined)
32783
33085
  rawArgs.vision_capability = visionCapability;
@@ -33885,17 +34187,22 @@ DO NOT use bash for code search or code exploration. If you are about to run gre
33885
34187
  }, isPowerShell ? "powershell" : "bash");
33886
34188
  } catch (error) {
33887
34189
  const fallbackCause = classifyBashHostFallbackError(error);
33888
- if (isPowerShell || !bashCfg.host_fallback || fallbackCause === undefined)
34190
+ if (fallbackCause === undefined)
33889
34191
  throw error;
34192
+ if (isPowerShell) {
34193
+ throw refuseAftModuleOptionWithoutFallback(error, "PowerShell", BASH_HOST_FALLBACK_REFUSAL);
34194
+ }
33890
34195
  if (!backgroundDisabled && rawRequestedBackground) {
33891
- throw new Error(`${BASH_HOST_FALLBACK_REFUSAL}; background:true is unsupported.`);
34196
+ throw refuseAftModuleOptionWithoutFallback(error, "background:true", BASH_HOST_FALLBACK_REFUSAL);
33892
34197
  }
33893
34198
  if (requestedPty) {
33894
- throw new Error(`${BASH_HOST_FALLBACK_REFUSAL}; pty:true is unsupported.`);
34199
+ throw refuseAftModuleOptionWithoutFallback(error, "pty:true", BASH_HOST_FALLBACK_REFUSAL);
33895
34200
  }
33896
34201
  if (params.runon !== undefined) {
33897
- throw new Error(`${BASH_HOST_FALLBACK_REFUSAL}; runon is unsupported, and the command was not run locally.`);
34202
+ throw refuseAftModuleOptionWithoutFallback(error, "runon", BASH_HOST_FALLBACK_REFUSAL);
33898
34203
  }
34204
+ if (!bashCfg.host_fallback)
34205
+ throw error;
33899
34206
  if (!extCtx.hasUI || typeof extCtx.ui?.confirm !== "function") {
33900
34207
  throw new BridgeError("Permission denied: host fallback execution requires an interactive UI.", "permission_denied");
33901
34208
  }
@@ -35418,24 +35725,22 @@ var OutlineParams = Type11.Object({
35418
35725
  description: "Directory outline only: include test files. Defaults to false; tests are hidden."
35419
35726
  }))
35420
35727
  });
35421
- var ZoomTarget = Type11.Object({
35422
- path: Type11.String({ description: "Path to file (absolute or project-relative)" }),
35423
- symbol: Type11.String({ description: "Symbol name in that file" })
35728
+ var ZoomEntry = Type11.Object({
35729
+ target: Type11.String({
35730
+ description: "File path (absolute or project-relative) or HTTP/HTTPS URL, auto-detected; issue://N or pr://N when GitHub reads are enabled"
35731
+ }),
35732
+ symbols: Type11.Union([Type11.String(), Type11.Array(Type11.String())], {
35733
+ description: "Symbol name for code or heading text for Markdown/HTML; a string or array of strings"
35734
+ })
35424
35735
  });
35425
35736
  var ZoomParams = Type11.Object({
35426
- path: Type11.Optional(Type11.String({ description: "Path to file (absolute or project-relative)" })),
35427
- url: Type11.Optional(Type11.String({
35428
- description: "HTTP/HTTPS URL of an HTML or Markdown document to fetch and zoom into"
35429
- })),
35430
- symbols: Type11.Optional(Type11.Union([Type11.String(), Type11.Array(Type11.String())], {
35431
- description: "Symbol name for code, or heading text for Markdown/HTML. Pass a string or array for batched lookups in the same file/URL."
35432
- })),
35433
- targets: Type11.Optional(Type11.Union([ZoomTarget, Type11.Array(ZoomTarget)], {
35434
- description: "Cross-file batch: `{ path, symbol }` or an array of them. May be combined with path/url + symbols; all lookups are answered in one batch."
35435
- })),
35436
- contextLines: optionalInt(1, Number.MAX_SAFE_INTEGER, "Lines of context before/after (default: 3)"),
35737
+ batch: Type11.Array(ZoomEntry, {
35738
+ minItems: 1,
35739
+ description: "One or more lookups; entries may mix file paths and URLs"
35740
+ }),
35741
+ contextLines: optionalInt(1, Number.MAX_SAFE_INTEGER, "Lines of context before/after each symbol (default: 3)"),
35437
35742
  callgraph: Type11.Optional(Type11.Boolean({
35438
- description: "Include call-graph annotations (calls-out / called-by within the same file). Default false; off keeps zoom output minimal."
35743
+ description: "Include call-graph annotations for every entry (default: false)"
35439
35744
  }))
35440
35745
  });
35441
35746
  function isUrl(s) {
@@ -35453,12 +35758,27 @@ async function assertReadPathPermissions(extCtx, ctx, paths) {
35453
35758
  }
35454
35759
  function zoomTargetLabel(args) {
35455
35760
  const safeArgs = asRecordOrEmpty(args);
35456
- return asString2(safeArgs.path) ?? asString2(safeArgs.url) ?? "(no target)";
35761
+ if (Array.isArray(safeArgs.batch)) {
35762
+ if (safeArgs.batch.length > 1)
35763
+ return `${safeArgs.batch.length} locations`;
35764
+ return asString2(asRecord3(safeArgs.batch[0])?.target) ?? "(no target)";
35765
+ }
35766
+ return asString2(safeArgs.target) ?? asString2(safeArgs.path) ?? asString2(safeArgs.url) ?? "(no target)";
35457
35767
  }
35458
35768
  function zoomExtraCallSites(call) {
35459
35769
  const extraCount = call.extra_count;
35460
35770
  return typeof extraCount === "number" && Number.isInteger(extraCount) && extraCount > 0 ? ` +${extraCount}` : "";
35461
35771
  }
35772
+ var ZOOM_CALL_ANNOTATION_LIMIT = 15;
35773
+ function renderZoomAnnotationSection(label, calls, theme) {
35774
+ const rendered = calls.slice(0, ZOOM_CALL_ANNOTATION_LIMIT).map((call) => ` ↳ ${asString2(call.name) ?? "(unknown)"}${typeof call.line === "number" ? `:${call.line}` : ""}${zoomExtraCallSites(call)}`);
35775
+ if (calls.length > ZOOM_CALL_ANNOTATION_LIMIT) {
35776
+ rendered.push(` +${calls.length - ZOOM_CALL_ANNOTATION_LIMIT} more`);
35777
+ }
35778
+ return `${theme.fg("muted", label)}
35779
+ ${rendered.join(`
35780
+ `)}`;
35781
+ }
35462
35782
  function buildOutlineSections(text, theme) {
35463
35783
  const trimmed = text.trim();
35464
35784
  if (!trimmed)
@@ -35488,12 +35808,10 @@ function renderZoomRecord(record, targetLabel, theme) {
35488
35808
  const callsOut = annotations ? asRecords2(annotations.calls_out) : [];
35489
35809
  const calledBy = annotations ? asRecords2(annotations.called_by) : [];
35490
35810
  if (callsOut.length > 0) {
35491
- lines.push(`${theme.fg("muted", "calls out")}`, callsOut.map((call) => ` ↳ ${asString2(call.name) ?? "(unknown)"}${typeof call.line === "number" ? `:${call.line}` : ""}${zoomExtraCallSites(call)}`).join(`
35492
- `));
35811
+ lines.push(renderZoomAnnotationSection("calls out", callsOut, theme));
35493
35812
  }
35494
35813
  if (calledBy.length > 0) {
35495
- lines.push(`${theme.fg("muted", "called by")}`, calledBy.map((call) => ` ↳ ${asString2(call.name) ?? "(unknown)"}${typeof call.line === "number" ? `:${call.line}` : ""}${zoomExtraCallSites(call)}`).join(`
35496
- `));
35814
+ lines.push(renderZoomAnnotationSection("called_by (this file)", calledBy, theme));
35497
35815
  }
35498
35816
  return lines.join(`
35499
35817
  `);
@@ -35562,9 +35880,10 @@ function renderOutlineResult(result, theme, context, options = { expanded: true
35562
35880
  }
35563
35881
  function renderZoomCall(args, theme, context) {
35564
35882
  const safeArgs = asRecordOrEmpty(args);
35565
- const hasExplicitTarget = typeof safeArgs.path === "string" || typeof safeArgs.url === "string";
35566
- const symbols = safeArgs.symbols;
35567
- const targets = safeArgs.targets;
35883
+ const entries = Array.isArray(safeArgs.batch) ? safeArgs.batch : [];
35884
+ const hasExplicitTarget = entries.length > 0 || typeof safeArgs.target === "string" || typeof safeArgs.path === "string" || typeof safeArgs.url === "string";
35885
+ const symbols = entries.length === 1 ? asRecord3(entries[0])?.symbols : safeArgs.symbols;
35886
+ const targets = entries.length > 1 ? entries : safeArgs.targets;
35568
35887
  let summary;
35569
35888
  if (typeof symbols === "string") {
35570
35889
  summary = theme.fg("toolOutput", symbols);
@@ -35608,12 +35927,8 @@ function registerReadingTools(pi, ctx, surface) {
35608
35927
  const zoomEnabled = toolEnabled(ctx.config, "aft_zoom");
35609
35928
  const ghReadEnabled = resolveGithubConfig(ctx.config).read;
35610
35929
  const githubOutlineDescription = whenGhReadEnabled(ghReadEnabled, "GitHub issues and pull requests can be outlined with `issue://NUMBER` or `pr://NUMBER` (including `OWNER/REPO` forms).");
35611
- const githubZoomDescription = whenGhReadEnabled(ghReadEnabled, "GitHub issue and pull-request discussion ordinals can be zoomed with `path: issue://…` or `path: pr://…` and `symbols`.");
35612
- if (surface.outline) {
35613
- const outlineTool = withPathAliasPreparation({
35614
- name: "aft_outline",
35615
- label: "outline",
35616
- description: `Structural outline of source code, documentation files, or remote URLs. For code, returns symbols (functions, classes, types) with line ranges. For Markdown and HTML, returns heading hierarchy. Use this to explore structure before reading specific sections with ${zoomEnabled ? "aft_zoom" : "read"}. With \`files: true\`, the outline is breadth-first with directory rollups; drill in by outlining a subdirectory. Rows show language, symbol count, and line count.
35930
+ const githubZoomDescription = whenGhReadEnabled(ghReadEnabled, "Batch targets may use `issue://NUMBER` or `pr://NUMBER` (including `OWNER/REPO` forms) to inspect GitHub discussion ordinals.");
35931
+ const outlineDescription = `Structural outline of source code, documentation files, or remote URLs. For code, returns symbols (functions, classes, types) with line ranges. For Markdown and HTML, returns heading hierarchy. Use this to explore structure before reading specific sections with ${zoomEnabled ? "aft_zoom" : "read"}. With \`files: true\`, the outline is breadth-first with directory rollups; drill in by outlining a subdirectory. Rows show language, symbol count, and line count.
35617
35932
 
35618
35933
  ${outlineFocusSteer(ctx.config)}
35619
35934
 
@@ -35625,18 +35940,32 @@ Pass a single \`target\`:
35625
35940
 
35626
35941
  ${githubOutlineDescription}` : ""}
35627
35942
 
35628
- When a list is cut, the reply ends with \`shown N of M <unit> (<reason>) · narrow: <knobs>\`; absence of that line means the list is complete.`,
35629
- parameters: OutlineParams,
35943
+ When a list is cut, the reply ends with \`shown N of M <unit> (<reason>) · narrow: <knobs>\`; absence of that line means the list is complete.`;
35944
+ const zoomDescription = 'Inspect code symbols or documentation sections. Pass lookups in `batch: [{ target, symbols }]`; a single lookup is a one-entry batch. Mix files and URLs in one batch, for example `batch: [{ target: "src/app.ts", symbols: "run" }, { target: "https://example.com/docs", symbols: "Overview" }]`. Each target is auto-detected as a file path or an HTTP/HTTPS URL. `symbols` is a string or string array. Markdown and HTML lookups return the section under the heading. Top-level `contextLines` and `callgraph` apply to every entry; callgraph adds calls-out and called-by annotations. Each failed lookup reports its own error line.' + (githubZoomDescription ? `
35945
+
35946
+ ${githubZoomDescription}` : "");
35947
+ if (surface.outline) {
35948
+ const outlineTool = withPathAliasPreparation({
35949
+ name: "aft_outline",
35950
+ label: "outline",
35951
+ get description() {
35952
+ return withRevisionRefNote(outlineDescription, ctx.config, "Only a single file can be outlined at a revision.");
35953
+ },
35954
+ get parameters() {
35955
+ return withRevisionRefParam(OutlineParams, ctx.config);
35956
+ },
35630
35957
  async execute(_toolCallId, params, _signal, _onUpdate, extCtx) {
35631
35958
  const bridge = bridgeFor(ctx, extCtx.cwd);
35632
35959
  const target = coerceTargetParam(params.target);
35633
35960
  const filesMode = coerceBoolean(params.files);
35634
35961
  const hasIncludeTests = !isEmptyParam(params.includeTests);
35635
35962
  const includeTests = coerceBoolean(params.includeTests);
35963
+ const ref = revisionRefArg(params.ref);
35636
35964
  const rawArgs = {
35637
35965
  target,
35638
35966
  ...filesMode ? { files: true } : {},
35639
- ...hasIncludeTests ? { includeTests } : {}
35967
+ ...hasIncludeTests ? { includeTests } : {},
35968
+ ...ref !== undefined ? { ref } : {}
35640
35969
  };
35641
35970
  if (Array.isArray(target)) {
35642
35971
  if (target.length === 0) {
@@ -35682,81 +36011,33 @@ When a list is cut, the reply ends with \`shown N of M <unit> (<reason>) · narr
35682
36011
  pi.registerTool(withPathAliasPreparation({
35683
36012
  name: "aft_zoom",
35684
36013
  label: "zoom",
35685
- description: "Inspect code symbols or documentation sections. For code, returns the full source of a symbol. Pass `callgraph: true` to also include call-graph annotations (calls-out / called-by within the same file). For Markdown and HTML, returns the section content under the given heading.\n\nUse `{ path, symbols }` or `{ url, symbols }` for one file/URL. `symbols` can be a string or array (one or many lookups in the same file/URL). Use `targets` for cross-file batches: `{ path, symbol }` or an array of them. Sending both merges them into one batch; a lookup that fails reports its own error line." + (githubZoomDescription ? `
35686
-
35687
- ${githubZoomDescription}` : ""),
35688
- parameters: ZoomParams,
36014
+ get description() {
36015
+ return withRevisionRefNote(zoomDescription, ctx.config, "Every file of the call is read at that revision; URLs do not have revisions.");
36016
+ },
36017
+ get parameters() {
36018
+ return withRevisionRefParam(ZoomParams, ctx.config);
36019
+ },
35689
36020
  async execute(_toolCallId, params, _signal, _onUpdate, extCtx) {
35690
36021
  const bridge = bridgeFor(ctx, extCtx.cwd);
35691
- const targetsInput = coerceJsonCollectionParam(params.targets, "targets");
35692
- const targetEntries = usableZoomTargets(targetsInput);
35693
- const hasPath = !isBlankParam(params.path);
35694
- const hasUrl = !isBlankParam(params.url);
35695
- const hasTargets = targetEntries.length > 0;
35696
- const hasSymbols = !isBlankParam(params.symbols);
35697
- const wantCallgraph = coerceBoolean(params.callgraph);
36022
+ if (!Array.isArray(params.batch) || params.batch.length === 0)
36023
+ throw new Error("'batch' must be a non-empty array of {target, symbols}");
36024
+ const ref = revisionRefArg(params.ref);
36025
+ if (ref !== undefined && params.batch.some((entry) => isUrl(entry.target)))
36026
+ throw new Error("'ref' cannot be used with a URL target: a URL has no revision");
36027
+ const paths = params.batch.map((entry) => entry.target).filter((target) => target && !isGithubPathArg(target) && !isUrl(target));
36028
+ const resolved = await Promise.all(paths.map((target) => resolvePathArg(extCtx.cwd, target)));
36029
+ await assertReadPathPermissions(extCtx, ctx, resolved);
36030
+ const rawArgs = { batch: params.batch };
35698
36031
  const contextLines = coerceOptionalInt(params.contextLines, "contextLines", 1, Number.MAX_SAFE_INTEGER);
35699
- if (hasTargets) {
35700
- if (hasPath && hasUrl) {
35701
- throw new Error("Provide exactly ONE of 'path' or 'url' — not both");
35702
- }
35703
- const targets = targetEntries.map((entry, i) => {
35704
- if (!entry || typeof entry !== "object" || Array.isArray(entry)) {
35705
- throw new Error(`targets[${i}].path must be a non-empty string`);
35706
- }
35707
- const record = entry;
35708
- const targetPath = isBlankParam(record.path) ? record.filePath : record.path;
35709
- return {
35710
- path: typeof targetPath === "string" ? targetPath : "",
35711
- symbol: typeof record.symbol === "string" ? record.symbol : ""
35712
- };
35713
- });
35714
- const localPaths = targets.map((target) => target.path).filter((path) => path.trim().length > 0 && !isGithubPathArg(path));
35715
- if (hasPath && !isGithubPathArg(params.path))
35716
- localPaths.push(params.path);
35717
- const resolvedTargets = await Promise.all(localPaths.map((path) => resolvePathArg(extCtx.cwd, path)));
35718
- await assertReadPathPermissions(extCtx, ctx, resolvedTargets);
35719
- const rawArgs = {
35720
- targets: targets.map((target) => ({ filePath: target.path, symbol: target.symbol }))
35721
- };
35722
- if (hasPath)
35723
- rawArgs.filePath = params.path;
35724
- else if (hasUrl)
35725
- rawArgs.url = params.url;
35726
- if (hasSymbols)
35727
- rawArgs.symbols = params.symbols;
35728
- if (contextLines !== undefined)
35729
- rawArgs.contextLines = contextLines;
35730
- if (wantCallgraph)
35731
- rawArgs.callgraph = true;
35732
- const response = await callToolCall(bridge, "zoom", rawArgs, extCtx);
35733
- if (response.success === false) {
35734
- throw new Error(response.text || response.message || "zoom failed");
35735
- }
35736
- return textResult(response.text, response);
35737
- }
35738
- if (!hasPath && !hasUrl) {
35739
- throw new Error("Provide exactly one of 'path', 'url', or 'targets'");
35740
- }
35741
- if (hasPath && hasUrl) {
35742
- throw new Error("Provide exactly ONE of 'path' or 'url' — not both");
35743
- }
35744
- const githubPath = hasPath && isGithubPathArg(params.path) || hasUrl && isGithubPathArg(params.url);
35745
- if (!hasUrl && !githubPath) {
35746
- const file = await resolvePathArg(extCtx.cwd, params.path);
35747
- await assertReadPathPermissions(extCtx, ctx, file);
35748
- }
35749
- const rawArgs = hasUrl ? { url: params.url } : { filePath: params.path };
35750
- if (hasSymbols)
35751
- rawArgs.symbols = params.symbols;
35752
36032
  if (contextLines !== undefined)
35753
36033
  rawArgs.contextLines = contextLines;
35754
- if (wantCallgraph)
36034
+ if (coerceBoolean(params.callgraph))
35755
36035
  rawArgs.callgraph = true;
36036
+ if (ref !== undefined)
36037
+ rawArgs.ref = ref;
35756
36038
  const response = await callToolCall(bridge, "zoom", rawArgs, extCtx);
35757
- if (response.success === false) {
36039
+ if (response.success === false)
35758
36040
  throw new Error(response.text || response.message || "zoom failed");
35759
- }
35760
36041
  return textResult(response.text, response);
35761
36042
  },
35762
36043
  renderCall(args, theme, context) {
@@ -35776,11 +36057,11 @@ function responsePaths(response) {
35776
36057
  return Array.isArray(response.paths) ? response.paths.filter((path) => typeof path === "string" && path.length > 0) : [];
35777
36058
  }
35778
36059
  var SafetyParams = Type12.Object({
35779
- op: StringEnum4(["undo", "history", "checkpoint", "restore", "list"], {
36060
+ op: StringEnum4(["undo", "history", "checkpoint", "restore", "list", "purge"], {
35780
36061
  description: "Safety operation"
35781
36062
  }),
35782
36063
  path: Type12.Optional(Type12.String({
35783
- description: "File path (required for history, optional for undo, optional for checkpoint — snapshots only that file when files is omitted, optional for restore — restores only that file). Absolute or relative to project root."
36064
+ description: "File path (required for history, optional for undo, optional for checkpoint — snapshots only that file when files is omitted, optional for restore — restores only that file, required for purge — a file or directory). Absolute or relative to project root."
35784
36065
  })),
35785
36066
  name: Type12.Optional(Type12.String({ description: "Checkpoint name (required for checkpoint, restore)" })),
35786
36067
  files: Type12.Optional(Type12.Array(Type12.String(), {
@@ -35866,6 +36147,21 @@ ${skipped.map((entry) => ` ↳ ${shortenHomePath(asString2(entry.file) ?? "(fil
35866
36147
  sections.push(theme.fg("muted", durability));
35867
36148
  return sections;
35868
36149
  }
36150
+ if (op === "purge") {
36151
+ const pruned = asRecords2(response.checkpoints_pruned).map((entry) => asString2(entry.name)).filter((name) => Boolean(name));
36152
+ const dropped = Array.isArray(response.checkpoints_dropped) ? response.checkpoints_dropped.filter((name) => typeof name === "string") : [];
36153
+ const failures = asRecords2(response.failures);
36154
+ return [
36155
+ `${theme.fg("success", "purged")} ${theme.fg("accent", shortenHomePath(asString2(response.path) ?? path ?? "(path)"))}`,
36156
+ `${theme.fg("muted", "undo backups removed")} ${asNumber2(response.undo_entries_removed) ?? 0}`,
36157
+ pruned.length > 0 ? `${theme.fg("muted", "checkpoints pruned")} ${pruned.join(", ")}` : "",
36158
+ dropped.length > 0 ? `${theme.fg("muted", "checkpoints dropped")} ${dropped.join(", ")}` : "",
36159
+ failures.length > 0 ? `${theme.fg("warning", "not removed")}
36160
+ ${failures.map((entry) => ` ↳ ${asString2(entry.target) ?? "(unknown)"}: ${asString2(entry.error) ?? "unknown error"}`).join(`
36161
+ `)}` : "",
36162
+ theme.fg("warning", "This cannot be undone.")
36163
+ ].filter((section) => section.length > 0);
36164
+ }
35869
36165
  const knownDetails = [path, name].filter((value) => Boolean(value));
35870
36166
  return [theme.fg("muted", "Safety result available."), ...knownDetails];
35871
36167
  }
@@ -35895,10 +36191,10 @@ function registerSafetyTool(pi, ctx) {
35895
36191
  pi.registerTool(withPathAliasPreparation({
35896
36192
  name: "aft_safety",
35897
36193
  label: "safety",
35898
- description: "File safety and recovery operations. Ops: `undo` (omit path to undo the entire last tool call; pass path to pop latest snapshot for one file — irreversible), `history` (list snapshots for a file), `checkpoint` (save named snapshot), `restore` (restore named checkpoint; optional path/files scopes it), `list` (list checkpoints). Per-file undo stack is capped at 20.",
36194
+ description: "`undo` reverts a tool call (omit path) or latest file snapshot (irreversible); `history` lists snapshots; `checkpoint` saves named snapshots on disk across restarts, expiring after 14 days (contents may include secrets); `restore` restores a checkpoint, optionally scoped by path/files; `list` lists checkpoints; `purge` permanently deletes this session's undo backups and checkpoint copies of a file or directory `path` (cannot be undone). Undo keeps 20 snapshots/file (oldest evicted).",
35899
36195
  parameters: SafetyParams,
35900
36196
  async execute(_toolCallId, params, _signal, _onUpdate, extCtx) {
35901
- if (params.op === "history" && !params.path) {
36197
+ if ((params.op === "history" || params.op === "purge") && !params.path) {
35902
36198
  throw new Error(`op='${params.op}' requires 'path'`);
35903
36199
  }
35904
36200
  if ((params.op === "checkpoint" || params.op === "restore") && !params.name) {
@@ -35942,6 +36238,16 @@ function registerSafetyTool(pi, ctx) {
35942
36238
  });
35943
36239
  }
35944
36240
  }
36241
+ if (params.op === "purge" && params.path) {
36242
+ const purgeTarget = await resolvePathArg(extCtx.cwd, params.path);
36243
+ await assertExternalDirectoryPermission(extCtx, purgeTarget, { restrictToProjectRoot });
36244
+ const preview = await callBridge(bridge, "safety_purge_paths", { file: purgeTarget }, extCtx);
36245
+ for (const file of new Set(responsePaths(preview))) {
36246
+ await assertExternalDirectoryPermission(extCtx, file, {
36247
+ restrictToProjectRoot
36248
+ });
36249
+ }
36250
+ }
35945
36251
  const rawArgs = { op: params.op };
35946
36252
  if (filePath)
35947
36253
  rawArgs.filePath = filePath;
@@ -36043,7 +36349,7 @@ var SearchParams2 = Type13.Object({
36043
36349
  description: "Include test files (*.test.*, *_test.rs, __tests__/, …) plus test-support, fixture, mock, snapshot, and corpus files. Defaults to false."
36044
36350
  })),
36045
36351
  path: Type13.Optional(Type13.String({
36046
- description: "Only set this to search a different Git project (absolute or ~ path). Omit it for the current configured workspace, including non-Git workspace roots; this is not a subdirectory filter. Unindexed external projects use a bounded lexical fallback."
36352
+ description: "Only set this to search a different project or plain folder (absolute or ~ path). Omit it for the current configured workspace, including non-Git workspace roots; this is not a subdirectory filter. Unindexed external folders use a bounded lexical fallback; filesystem roots, home and its ancestors are refused."
36047
36353
  }))
36048
36354
  });
36049
36355
  function buildSemanticSections(args, payload, theme) {
@@ -36335,6 +36641,17 @@ function registerPiToolSurface(pi, ctx, loadedSurface, harness) {
36335
36641
  if (surface.delete || surface.move)
36336
36642
  registerFsTools(boundPi, ctx, surface);
36337
36643
  }
36644
+ function refreshPiRevisionReadTools(pi, ctx, loadedSurface) {
36645
+ const surface = withLiveRestriction({
36646
+ ...loadedSurface,
36647
+ hoistWrite: false,
36648
+ hoistEdit: false,
36649
+ hoistGrep: false
36650
+ }, ctx);
36651
+ const boundPi = bindToolRegistrationFunnel(pi, ctx);
36652
+ registerHoistedTools(boundPi, ctx, surface);
36653
+ registerReadingTools(boundPi, ctx, surface);
36654
+ }
36338
36655
 
36339
36656
  // src/config-error-state.ts
36340
36657
  var CONFIG_ERROR_STATUS_KEY = "aft";
@@ -36459,7 +36776,12 @@ function startPiLiveConfigReload(options) {
36459
36776
  securityKeys: aftLiveSecurityKeys(PI_LIVE_CONFIG_KEYS),
36460
36777
  keys: PI_LIVE_CONFIG_KEYS,
36461
36778
  getConfig: options.getConfig,
36462
- setConfig: options.setConfig,
36779
+ setConfig: (next) => {
36780
+ const revisionReadsChanged = options.getConfig().revision_reads !== next.revision_reads;
36781
+ options.setConfig(next);
36782
+ if (revisionReadsChanged)
36783
+ options.refreshRevisionReadTools?.();
36784
+ },
36463
36785
  log: (message) => log2(`${message} (${options.directory})`),
36464
36786
  reportError: (message) => {
36465
36787
  error2(message);
@@ -36582,7 +36904,7 @@ function buildWorkflowHints(opts) {
36582
36904
  `));
36583
36905
  }
36584
36906
  if (hasOutline && hasZoom) {
36585
- sections.push(`**Web/URL access**: \`aft_outline({ target: "<url>" })\` first for structure, then \`aft_zoom({ url: "<url>", symbols: "<heading>" })\` for the specific section.`);
36907
+ sections.push(`**Web/URL access**: \`aft_outline({ target: "<url>" })\` first for structure, then \`aft_zoom({ batch: [{ target: "<url>", symbols: "<heading>" }] })\` for the specific section.`);
36586
36908
  }
36587
36909
  if (hasOutline && (hasGrep || hasSearch) && (hasZoom || hasRead)) {
36588
36910
  const searchName = hasSearch ? "aft_search" : grepName;
@@ -36748,12 +37070,12 @@ var PLUGIN_VERSION = (() => {
36748
37070
  return "0.0.0";
36749
37071
  }
36750
37072
  })();
36751
- var ANNOUNCEMENT_VERSION = "0.61.0";
37073
+ var ANNOUNCEMENT_VERSION = "0.62.0";
36752
37074
  var ANNOUNCEMENT_FEATURES = [
36753
- "Shell sandboxing has per-role profiles: `sandbox.profile` (`off`, `relaxed`, `strict`) replaces `sandbox.enabled`, and `npx @cortexkit/aft doctor --fix` migrates your config.",
36754
- "Edits keep files' permissions and ownership, replacing them atomically when that is safe, and `apply_patch` rejects malformed or ambiguous patches before writing anything.",
36755
- "Much lighter background work: less CPU, disk and memory use, and no indexing of your home folder or drive roots.",
36756
- "Long shell waits keep their replies, remote `runon` jobs survive build-server restarts, and many reported issues are fixed."
37075
+ "Linux dev containers work again: a project at a bind-mount target such as `/workspace` is no longer refused as too broad.",
37076
+ "Read a file as it is at any git revision with the opt-in `ref` argument on `read`, `aft_outline` and `aft_zoom` (set `revision_reads: true`).",
37077
+ "`aft_safety` has a `purge` op that permanently removes a file from this session's undo backups and checkpoints, such as a secret written by mistake.",
37078
+ "`aft_zoom` takes one `batch` of lookups mixing files and URLs, and many reported issues are fixed."
36757
37079
  ];
36758
37080
  var ANNOUNCEMENT_FOOTER = "Join us on Discord: https://discord.gg/DSa65w8wuf";
36759
37081
  var pendingEagerWarnings = new Map;
@@ -37025,7 +37347,8 @@ async function src_default(pi) {
37025
37347
  registerBashTool(pi, ctx, surface.semantic, "bash", false, "bash");
37026
37348
  }
37027
37349
  },
37028
- notify: (message) => deliverConfigMigrationWarnings([message])
37350
+ notify: (message) => deliverConfigMigrationWarnings([message]),
37351
+ refreshRevisionReadTools: () => refreshPiRevisionReadTools(pi, ctx, surface)
37029
37352
  });
37030
37353
  const startLspDiscovery = () => {
37031
37354
  try {