@cortexkit/aft-pi 0.58.1 → 0.58.2

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
@@ -7935,32 +7935,79 @@ function error(message, meta) {
7935
7935
  function isTerminalStatus(status) {
7936
7936
  return status === "completed" || status === "failed" || status === "killed" || status === "timed_out" || status === "fate_unknown";
7937
7937
  }
7938
- function sleep(ms) {
7939
- return new Promise((resolve) => setTimeout(resolve, ms));
7940
- }
7941
- function monotonicNowMs() {
7942
- return performance.now();
7943
- }
7944
7938
  // ../aft-bridge/dist/bash-hints.js
7945
7939
  import * as fs from "node:fs";
7946
7940
  import * as os from "node:os";
7947
7941
  import * as path from "node:path";
7948
7942
  var DEFAULT_PRIMARY_WATCH_TIMEOUT_MS = 30000;
7949
- var WATCH_TIMEOUT_PARAM_DESCRIPTION = "Sync-only timeout in milliseconds. Default 30000 (the configured maximum for delegated sessions); max `bash.watch_sync_max_ms` (120000 by default).";
7943
+ var MAX_WATCH_TIMEOUT_MS = 1800000;
7944
+ var LONGEST_TIMER_DELAY_MS = 2147483647;
7945
+ var WATCH_SYNC_DEFAULTS_DESCRIPTION = "Sync waits default to 30s in a main session (max `bash.watch_sync_max_ms`, 120s by default); in a delegated session a sync wait without a timeout waits until the command finishes";
7946
+ var WATCH_TIMEOUT_PARAM_DESCRIPTION = "Sync-only timeout in milliseconds. In a main session: default 30000, max `bash.watch_sync_max_ms` (120000 by default). In a delegated session: omit it to wait until the command finishes; a value you pass is used as given.";
7950
7947
  function resolveWatchTimeoutMs(requestedMs, role, capMs) {
7951
- const fallback = role === "worker" ? capMs : DEFAULT_PRIMARY_WATCH_TIMEOUT_MS;
7952
- return Math.min(requestedMs ?? fallback, capMs);
7948
+ if (role === "worker")
7949
+ return requestedMs;
7950
+ return Math.min(requestedMs ?? DEFAULT_PRIMARY_WATCH_TIMEOUT_MS, capMs);
7951
+ }
7952
+ function maxWatchTimeoutMs(role, capMs) {
7953
+ return role === "worker" ? MAX_WATCH_TIMEOUT_MS : capMs;
7954
+ }
7955
+ function watchPollDelayMs(elapsedMs) {
7956
+ if (elapsedMs < 5000)
7957
+ return 100;
7958
+ if (elapsedMs < 30000)
7959
+ return 250;
7960
+ if (elapsedMs < 120000)
7961
+ return 500;
7962
+ return 1000;
7963
+ }
7964
+ var watchClock = {
7965
+ now: () => performance.now(),
7966
+ sleep: abortableSleep
7967
+ };
7968
+ function abortableSleep(ms, signal) {
7969
+ if (signal?.aborted)
7970
+ return Promise.resolve();
7971
+ return new Promise((resolve) => {
7972
+ const done = () => {
7973
+ clearTimeout(timer);
7974
+ signal?.removeEventListener("abort", done);
7975
+ resolve();
7976
+ };
7977
+ const timer = setTimeout(done, ms);
7978
+ signal?.addEventListener("abort", done, { once: true });
7979
+ });
7953
7980
  }
7954
7981
  function formatWatchWaited(elapsedMs, limitMs, capMs) {
7955
- const limit = limitMs >= capMs ? `limit ${limitMs}ms, the bash.watch_sync_max_ms cap` : `limit ${limitMs}ms`;
7982
+ let limit;
7983
+ if (limitMs === undefined)
7984
+ limit = "no limit: waits until the command finishes";
7985
+ else if (capMs !== undefined && limitMs >= capMs)
7986
+ limit = `limit ${limitMs}ms, the bash.watch_sync_max_ms cap`;
7987
+ else
7988
+ limit = `limit ${limitMs}ms`;
7956
7989
  return `Waited ${Math.round(elapsedMs)}ms (${limit})`;
7957
7990
  }
7958
- function watchTimeoutSteer(role, capMs, timeoutParam = "timeoutMs") {
7991
+ function watchTimeoutSteer(role, timeoutParam = "timeoutMs") {
7959
7992
  if (role === "worker") {
7960
- return `The command is still running; this is not a failure. Watch again (${timeoutParam} up to ${capMs}) and don't report a result until it finishes.`;
7993
+ return `The command is still running; this is not a failure. Watch again and don't report a result until it finishes; a bash_watch without ${timeoutParam} waits until the command finishes.`;
7961
7994
  }
7962
7995
  return "The command is still running; this is not a failure. Watch again, do other work, or end your turn: the completion reminder wakes you.";
7963
7996
  }
7997
+ var WATCH_UNAVAILABLE_GIVE_UP_MS = 120000;
7998
+ var WORKER_KEEP_WAITING = "Call bash_watch again to keep waiting; don't report a result until the command finishes.";
7999
+ function interruptedWatchTail(role, primaryTail) {
8000
+ return role === "worker" ? `The task is still running. ${WORKER_KEEP_WAITING}` : primaryTail;
8001
+ }
8002
+ function runningTaskStatusHint(role) {
8003
+ return role === "worker" ? "To wait for it, call bash_watch; don't poll." : "A completion reminder will be delivered automatically; don't poll.";
8004
+ }
8005
+ function watchUnavailableSteer(role) {
8006
+ const unknown = "the bridge was busy, so task state is unknown.";
8007
+ if (role === "worker")
8008
+ return `${unknown} ${WORKER_KEEP_WAITING}`;
8009
+ return `${unknown} Do not poll; let the task's completion notification wake the session, or use one bash_status snapshot on the next normal tool call.`;
8010
+ }
7964
8011
  var CONFLICT_HINT = `
7965
8012
 
7966
8013
  [Hint] Use aft_conflicts to see all conflict regions across files in a single call.`;
@@ -8819,6 +8866,9 @@ function isPassiveCommand(command) {
8819
8866
  return PASSIVE_COMMANDS.has(command);
8820
8867
  }
8821
8868
 
8869
+ // ../aft-bridge/dist/transport.js
8870
+ var WORKER_SESSION_FIELD = "worker_session";
8871
+
8822
8872
  // ../aft-bridge/dist/bridge.js
8823
8873
  var DEFAULT_BRIDGE_TIMEOUT_MS = 30000;
8824
8874
  var BRIDGE_HANG_TIMEOUT_THRESHOLD = 2;
@@ -9237,9 +9287,11 @@ class BinaryBridge {
9237
9287
  params.edit_slot_survives = editSlotSurvives;
9238
9288
  this.logHashlineRegistrationCarrier("tool_call", sessionId, editSlotSurvives);
9239
9289
  }
9240
- const { preview, ...sendOptions } = options ?? {};
9290
+ const { preview, workerSession, ...sendOptions } = options ?? {};
9241
9291
  if (preview === true)
9242
9292
  params.preview = true;
9293
+ if (workerSession === true)
9294
+ params[WORKER_SESSION_FIELD] = true;
9243
9295
  return await this.send("tool_call", params, Object.keys(sendOptions).length > 0 ? sendOptions : undefined);
9244
9296
  }
9245
9297
  async sendWithVersionMismatchRetry(command, params, options, canRetryAfterVersionSwap) {
@@ -9308,7 +9360,7 @@ class BinaryBridge {
9308
9360
  if (command === "cancel_request" || Object.hasOwn(params, "command") || Object.hasOwn(params, "method")) {
9309
9361
  const nested = { ...params };
9310
9362
  const reserved = {};
9311
- for (const key of ["session_id", "lsp_hints"]) {
9363
+ for (const key of ["session_id", "lsp_hints", WORKER_SESSION_FIELD]) {
9312
9364
  if (Object.hasOwn(nested, key)) {
9313
9365
  reserved[key] = nested[key];
9314
9366
  delete nested[key];
@@ -10091,7 +10143,8 @@ function renderCallersGroupLines(group, theme) {
10091
10143
  const bySymbolProvenance = new Map;
10092
10144
  for (const caller of callers) {
10093
10145
  const symbol = asString(caller.symbol) ?? "(unknown)";
10094
- const provenanceKey = asString(caller.resolved_by) === "name_match" ? `${symbol}\x00name_match` : `${symbol}\x00exact`;
10146
+ const via = asString(caller.via) ?? "";
10147
+ const provenanceKey = asString(caller.resolved_by) === "name_match" ? `${symbol}\x00${via}\x00name_match` : `${symbol}\x00${via}\x00exact`;
10095
10148
  const line = asNumber(caller.line);
10096
10149
  const bucket = bySymbolProvenance.get(provenanceKey) ?? [];
10097
10150
  if (line !== undefined)
@@ -10100,12 +10153,13 @@ function renderCallersGroupLines(group, theme) {
10100
10153
  }
10101
10154
  const keys = [...bySymbolProvenance.keys()].sort((a, b) => a.localeCompare(b));
10102
10155
  for (const key of keys) {
10103
- const symbol = key.split("\x00")[0] ?? "(unknown)";
10156
+ const [symbol = "(unknown)", via = ""] = key.split("\x00");
10104
10157
  const isNameMatch = key.endsWith("\x00name_match");
10105
10158
  const lineNums = (bySymbolProvenance.get(key) ?? []).sort((a, b) => a - b);
10106
10159
  const linePart = lineNums.length > 0 ? lineNums.map(String).join(", ") : "?";
10107
10160
  const marker = isNameMatch ? ` ${theme.fg("warning", "~")}` : "";
10108
- lines.push(` ↳ ${symbol}:${linePart}${marker}`);
10161
+ const viaPart = via ? ` via ${via}` : "";
10162
+ lines.push(` ↳ ${symbol}:${linePart}${marker}${viaPart}`);
10109
10163
  }
10110
10164
  return lines;
10111
10165
  }
@@ -10139,6 +10193,9 @@ function formatCallgraphSections(op, response, theme = PLAIN_CALLGRAPH_THEME, op
10139
10193
  sections.push(renderCallersGroupLines(group, theme).join(`
10140
10194
  `));
10141
10195
  });
10196
+ const macroNote = asString(asRecord(record.macro_note)?.message);
10197
+ if (macroNote)
10198
+ sections.push(theme.fg("warning", macroNote));
10142
10199
  return sections;
10143
10200
  }
10144
10201
  if (op === "trace_to_symbol") {
@@ -10352,6 +10409,35 @@ function coerceJsonCollectionParam(value, field) {
10352
10409
  }
10353
10410
  throw new Error(`${field} must be ${shape} (got an invalid type)`);
10354
10411
  }
10412
+ function isBlankParam(value) {
10413
+ if (typeof value === "string")
10414
+ return value.trim().length === 0;
10415
+ return isEmptyParam(value);
10416
+ }
10417
+ function usableZoomTargets(targets) {
10418
+ if (isEmptyParam(targets))
10419
+ return [];
10420
+ const entries = Array.isArray(targets) ? targets : [targets];
10421
+ return entries.filter((entry) => {
10422
+ if (entry === null || entry === undefined)
10423
+ return false;
10424
+ if (typeof entry !== "object")
10425
+ return true;
10426
+ const record = entry;
10427
+ return !(isBlankParam(record.path) && isBlankParam(record.filePath) && isBlankParam(record.symbol));
10428
+ });
10429
+ }
10430
+ function isFindReplaceOnlyEdit(args) {
10431
+ const absent = (value) => value === undefined || value === null || value === "";
10432
+ const absentLine = (value) => absent(value) || value === 0;
10433
+ const edits = args.edits;
10434
+ return Array.isArray(edits) && edits.length > 0 && absent(args.appendContent) && absent(args.symbol) && absent(args.content) && edits.every((entry) => {
10435
+ if (entry === null || typeof entry !== "object")
10436
+ return false;
10437
+ const item = entry;
10438
+ return typeof item.oldString === "string" && (absent(item.newString) || typeof item.newString === "string") && absentLine(item.startLine) && absentLine(item.endLine) && absent(item.content);
10439
+ });
10440
+ }
10355
10441
  // ../aft-bridge/dist/config-error.js
10356
10442
  import { homedir as homedir6 } from "node:os";
10357
10443
  import { isAbsolute as isAbsolute2, join as join6 } from "node:path";
@@ -13656,6 +13742,13 @@ class SubcTransportShuttingDownError extends SubcCallError {
13656
13742
  this.name = "SubcTransportShuttingDownError";
13657
13743
  }
13658
13744
  }
13745
+
13746
+ class ReconnectKeyChangedError extends Error {
13747
+ constructor() {
13748
+ super("the subc daemon restarted and its connection key changed; the reconnect did not complete within the deadline, retry");
13749
+ this.name = "ReconnectKeyChangedError";
13750
+ }
13751
+ }
13659
13752
  function isSubcClientClosedError(error) {
13660
13753
  return error instanceof SubcError && error.message === "client closed";
13661
13754
  }
@@ -14158,6 +14251,8 @@ class SubcTransport {
14158
14251
  body.edit_slot_survives = editSlotSurvives;
14159
14252
  if (preview === true)
14160
14253
  body.preview = true;
14254
+ if (options?.workerSession === true)
14255
+ body[WORKER_SESSION_FIELD] = true;
14161
14256
  const reply = await this.pool.routeRequest(this.identityFor(sessionId), body, timeoutMs, onProgress, this.generation, abortSignal);
14162
14257
  return reliftReply(reply);
14163
14258
  }
@@ -14168,7 +14263,10 @@ class SubcTransport {
14168
14263
  }
14169
14264
  const { timeoutMs, onProgress, abortSignal } = this.splitOptions(options);
14170
14265
  const session = typeof params.session_id === "string" ? params.session_id : undefined;
14171
- const body = { name: command, arguments: params };
14266
+ const { [WORKER_SESSION_FIELD]: workerSession, ...args } = params;
14267
+ const body = { name: command, arguments: args };
14268
+ if (workerSession === true)
14269
+ body[WORKER_SESSION_FIELD] = true;
14172
14270
  const editSlotSurvives = this.pool.getEditSlotSurvives();
14173
14271
  if (editSlotSurvives !== undefined)
14174
14272
  body.edit_slot_survives = editSlotSurvives;
@@ -14205,6 +14303,7 @@ class SubcTransportPool {
14205
14303
  lifecycleRegistration = null;
14206
14304
  outerFacadeEvictor;
14207
14305
  client = null;
14306
+ hadWorkingClient = false;
14208
14307
  connecting = null;
14209
14308
  routeReopenRetryDelayMs = RECONNECT_RETRY_FLOOR_MS;
14210
14309
  routeReopenRetry = null;
@@ -14649,6 +14748,10 @@ class SubcTransportPool {
14649
14748
  }
14650
14749
  async routeRequest(identity, body, timeoutMs, onProgress, expectedGeneration, abortSignal) {
14651
14750
  const callStartedAt = performance.now();
14751
+ const callDeadlineMs = timeoutMs ?? SUBC_DEFAULT_REQUEST_TIMEOUT_MS;
14752
+ let scheduledDelayMs = 0;
14753
+ const spentMs = () => Math.max(performance.now() - callStartedAt, scheduledDelayMs);
14754
+ const remainingMs = () => callDeadlineMs - spentMs();
14652
14755
  const root = asCanonicalRootPath(identity.project_root);
14653
14756
  let generation = expectedGeneration;
14654
14757
  if (this.lifecycleEnabled()) {
@@ -14664,7 +14767,49 @@ class SubcTransportPool {
14664
14767
  try {
14665
14768
  let client;
14666
14769
  try {
14667
- client = await this.ensureClient();
14770
+ let keyChanged;
14771
+ while (true) {
14772
+ const remaining = remainingMs();
14773
+ if (remaining <= 0)
14774
+ throw keyChanged ?? callDeadlinePassedError(undefined, callDeadlineMs);
14775
+ let timer;
14776
+ try {
14777
+ const acquired = await Promise.race([
14778
+ this.ensureClient(),
14779
+ new Promise((resolve) => {
14780
+ timer = setTimeout(() => resolve(CALL_DEADLINE_EXPIRED), remaining);
14781
+ })
14782
+ ]);
14783
+ if (acquired === CALL_DEADLINE_EXPIRED) {
14784
+ throw keyChanged ?? callDeadlinePassedError(undefined, callDeadlineMs);
14785
+ }
14786
+ client = acquired;
14787
+ break;
14788
+ } catch (error) {
14789
+ if (!(error instanceof ReconnectKeyChangedError))
14790
+ throw error;
14791
+ keyChanged = error;
14792
+ if (spentMs() + this.nextRouteReopenDelayMs() >= callDeadlineMs)
14793
+ throw error;
14794
+ } finally {
14795
+ clearTimeout(timer);
14796
+ }
14797
+ const { delayMs, wait } = this.waitForRouteReopenBackoff();
14798
+ scheduledDelayMs = spentMs() + delayMs;
14799
+ let retryTimer;
14800
+ try {
14801
+ const waited = await Promise.race([
14802
+ wait,
14803
+ new Promise((resolve) => {
14804
+ retryTimer = setTimeout(() => resolve(CALL_DEADLINE_EXPIRED), callDeadlineMs - (performance.now() - callStartedAt));
14805
+ })
14806
+ ]);
14807
+ if (waited === CALL_DEADLINE_EXPIRED)
14808
+ throw keyChanged;
14809
+ } finally {
14810
+ clearTimeout(retryTimer);
14811
+ }
14812
+ }
14668
14813
  this.assertRecordLive(record);
14669
14814
  } catch (error) {
14670
14815
  throw this.annotateReapError(error, record);
@@ -14699,12 +14844,8 @@ class SubcTransportPool {
14699
14844
  throw error;
14700
14845
  }
14701
14846
  };
14702
- const callDeadlineMs = timeoutMs ?? SUBC_DEFAULT_REQUEST_TIMEOUT_MS;
14703
14847
  const reloadWaitBudgetMs = Math.min(callDeadlineMs, ROUTE_OPEN_RELOAD_WAIT_CEILING_MS);
14704
- let scheduledDelayMs = 0;
14705
14848
  let lastRefusal;
14706
- const spentMs = () => Math.max(performance.now() - callStartedAt, scheduledDelayMs);
14707
- const remainingMs = () => callDeadlineMs - spentMs();
14708
14849
  const openRouteWithinDeadline = async () => {
14709
14850
  const remaining = remainingMs();
14710
14851
  if (remaining <= 0)
@@ -14907,10 +15048,14 @@ class SubcTransportPool {
14907
15048
  throw new SubcTransportShuttingDownError;
14908
15049
  }
14909
15050
  this.client = client;
15051
+ this.hadWorkingClient = true;
14910
15052
  this.transportFailures = 0;
14911
15053
  return client;
14912
15054
  }).catch((error) => {
14913
15055
  this.connecting = null;
15056
+ if (this.hadWorkingClient && error instanceof AuthError) {
15057
+ throw new ReconnectKeyChangedError;
15058
+ }
14914
15059
  throw error;
14915
15060
  });
14916
15061
  return this.connecting;
@@ -17903,31 +18048,36 @@ var ORT_PLATFORM_MAP = {
17903
18048
  arm64: {
17904
18049
  assetName: `onnxruntime-osx-arm64-${ORT_VERSION}`,
17905
18050
  libName: "libonnxruntime.dylib",
17906
- archiveType: "tgz"
18051
+ archiveType: "tgz",
18052
+ librarySha256: "872533f130f1839a5bc01788ddb4f75c83a189763441ba1178788ed965449289"
17907
18053
  }
17908
18054
  },
17909
18055
  linux: {
17910
18056
  x64: {
17911
18057
  assetName: `onnxruntime-linux-x64-${ORT_VERSION}`,
17912
18058
  libName: "libonnxruntime.so",
17913
- archiveType: "tgz"
18059
+ archiveType: "tgz",
18060
+ librarySha256: "d132535d051344ff5c64c9c200004150559049a81ed330eb4422c1962fb6b7e4"
17914
18061
  },
17915
18062
  arm64: {
17916
18063
  assetName: `onnxruntime-linux-aarch64-${ORT_VERSION}`,
17917
18064
  libName: "libonnxruntime.so",
17918
- archiveType: "tgz"
18065
+ archiveType: "tgz",
18066
+ librarySha256: "52b2a0e75e79468404284fec38ad5ee1a7a996232274f5e1b84f3e793fb07554"
17919
18067
  }
17920
18068
  },
17921
18069
  win32: {
17922
18070
  x64: {
17923
18071
  assetName: `onnxruntime-win-x64-${ORT_VERSION}`,
17924
18072
  libName: "onnxruntime.dll",
17925
- archiveType: "zip"
18073
+ archiveType: "zip",
18074
+ librarySha256: "b95efb2113b603bbbf3f191061c5516a871ed546893c820e4f3b7b6c358dbf2a"
17926
18075
  },
17927
18076
  arm64: {
17928
18077
  assetName: `onnxruntime-win-arm64-${ORT_VERSION}`,
17929
18078
  libName: "onnxruntime.dll",
17930
- archiveType: "zip"
18079
+ archiveType: "zip",
18080
+ librarySha256: "724d81ac50b11bfaa01ab3ce01b99fb6734a4b762259a8be4395ca662ce99fe6"
17931
18081
  }
17932
18082
  }
17933
18083
  };
@@ -18414,6 +18564,9 @@ async function downloadOnnxRuntime(info, targetDir) {
18414
18564
  copyOnnxLibraries(info, extractedDir, stagedInstallDir, realFiles, symlinks);
18415
18565
  const libPath = join14(stagedInstallDir, info.libName);
18416
18566
  const libHash = sha256File2(libPath);
18567
+ if (libHash !== info.librarySha256) {
18568
+ throw new Error(`onnx runtime library hash mismatch at ${libPath}: sha256 ${libHash}, ` + `expected the pinned ${info.librarySha256} for ${info.assetName}`);
18569
+ }
18417
18570
  writeOnnxInstalledMeta(stagedInstallDir, ORT_VERSION, libHash, archiveSha256);
18418
18571
  publishOnnxRuntime(stagedInstallDir, targetDir);
18419
18572
  rmSync3(tmpDir, { recursive: true, force: true });
@@ -18756,12 +18909,13 @@ function normalizeZoomTargets(record) {
18756
18909
  throw new InvalidRequestError(`'targets[${index}].path' must be a non-empty string`);
18757
18910
  }
18758
18911
  const source = target;
18759
- const emptyTarget = source.symbol === "" && (hasOwn(source, "path") && source.path === "" || hasOwn(source, "filePath") && source.filePath === "");
18912
+ const blank = (value) => value === undefined || value === null || typeof value === "string" && !value.trim();
18913
+ const emptyTarget = blank(source.symbol) && blank(source.path) && blank(source.filePath);
18760
18914
  if (emptyTarget)
18761
18915
  return { ...source };
18762
18916
  const normalized = { ...source };
18763
18917
  try {
18764
- normalizeAliasPair(normalized, "path", "filePath", true);
18918
+ normalizeAliasPair(normalized, "path", "filePath", false);
18765
18919
  } catch (error) {
18766
18920
  if (error instanceof InvalidRequestError) {
18767
18921
  throw new InvalidRequestError(error.message.replace("'filePath'", `'targets[${index}].filePath'`).replace("'path'", `'targets[${index}].path'`));
@@ -20342,7 +20496,7 @@ import {
20342
20496
  // package.json
20343
20497
  var package_default = {
20344
20498
  name: "@cortexkit/aft-pi",
20345
- version: "0.58.1",
20499
+ version: "0.58.2",
20346
20500
  type: "module",
20347
20501
  description: "Pi coding agent extension for Agent File Tools (AFT) — tree-sitter and LSP-powered code analysis",
20348
20502
  main: "dist/index.js",
@@ -20365,18 +20519,18 @@ var package_default = {
20365
20519
  "test:unit": "bun test src/__tests__ --path-ignore-patterns 'src/__tests__/e2e/**'"
20366
20520
  },
20367
20521
  dependencies: {
20368
- "@cortexkit/aft-bridge": "0.58.1",
20522
+ "@cortexkit/aft-bridge": "0.58.2",
20369
20523
  "comment-json": "^5.0.0",
20370
20524
  diff: "^8.0.4",
20371
20525
  zod: "^4.4.3"
20372
20526
  },
20373
20527
  optionalDependencies: {
20374
- "@cortexkit/aft-darwin-arm64": "0.58.1",
20375
- "@cortexkit/aft-darwin-x64": "0.58.1",
20376
- "@cortexkit/aft-linux-arm64": "0.58.1",
20377
- "@cortexkit/aft-linux-x64": "0.58.1",
20378
- "@cortexkit/aft-win32-arm64": "0.58.1",
20379
- "@cortexkit/aft-win32-x64": "0.58.1"
20528
+ "@cortexkit/aft-darwin-arm64": "0.58.2",
20529
+ "@cortexkit/aft-darwin-x64": "0.58.2",
20530
+ "@cortexkit/aft-linux-arm64": "0.58.2",
20531
+ "@cortexkit/aft-linux-x64": "0.58.2",
20532
+ "@cortexkit/aft-win32-arm64": "0.58.2",
20533
+ "@cortexkit/aft-win32-x64": "0.58.2"
20380
20534
  },
20381
20535
  devDependencies: {
20382
20536
  "@earendil-works/pi-coding-agent": "^0.80.2",
@@ -20656,6 +20810,24 @@ function appendDegradedStatus(lines, status, markdown) {
20656
20810
  // src/tools/_shared.ts
20657
20811
  import { existsSync as existsSync12 } from "node:fs";
20658
20812
  import { Type } from "typebox";
20813
+
20814
+ // src/session-kind.ts
20815
+ var MAGIC_CONTEXT_SUBAGENT_ENV = "MAGIC_CONTEXT_PI_SUBAGENT";
20816
+ function piWorkerKind(extCtx, env = process.env) {
20817
+ if (env[MAGIC_CONTEXT_SUBAGENT_ENV] === "1")
20818
+ return "delegated";
20819
+ if (extCtx?.hasUI === false)
20820
+ return "headless";
20821
+ return;
20822
+ }
20823
+ function isPiWorkerSession(extCtx, env = process.env) {
20824
+ return piWorkerKind(extCtx, env) !== undefined;
20825
+ }
20826
+ function skipsEagerStartup(env = process.env) {
20827
+ return piWorkerKind(undefined, env) === "delegated";
20828
+ }
20829
+
20830
+ // src/tools/_shared.ts
20659
20831
  var optionalInt = (min, max, description = "(integer)") => Type.Optional(Type.Union([Type.Integer({ minimum: min, maximum: max }), Type.String()], {
20660
20832
  description
20661
20833
  }));
@@ -20741,6 +20913,9 @@ async function callBridge(bridge, command, params = {}, extCtx, options) {
20741
20913
  if (sessionId) {
20742
20914
  merged.session_id = sessionId;
20743
20915
  }
20916
+ if (isPiWorkerSession(extCtx)) {
20917
+ merged[WORKER_SESSION_FIELD] = true;
20918
+ }
20744
20919
  const sendOptions = {
20745
20920
  ...timeoutMs !== undefined ? { timeoutMs } : {},
20746
20921
  configureWarningClient: extCtx,
@@ -20768,6 +20943,7 @@ async function callToolCallForSession(bridge, name, rawArgs, sessionId, extCtx,
20768
20943
  const sendOptions = {
20769
20944
  ...timeoutMs !== undefined ? { timeoutMs } : {},
20770
20945
  configureWarningClient: extCtx,
20946
+ ...isPiWorkerSession(extCtx) ? { workerSession: true } : {},
20771
20947
  ...options
20772
20948
  };
20773
20949
  let response;
@@ -25985,6 +26161,10 @@ var SubcConfigSchema = object({
25985
26161
  var GhShimConfigSchema = object({
25986
26162
  binary_path: string2().trim().refine(isAbsolute7, "gh_shim.binary_path must be absolute").optional()
25987
26163
  });
26164
+ var OpenCodeHostConfigSchema = object({
26165
+ server_url: string2().optional(),
26166
+ server_password_env: string2().optional()
26167
+ });
25988
26168
  var GithubConfigSchema = object({
25989
26169
  shim: boolean2().optional(),
25990
26170
  read: boolean2().optional(),
@@ -26060,6 +26240,7 @@ var AftConfigFieldsSchema = object({
26060
26240
  search: SearchConfigSchema.optional(),
26061
26241
  bridge: BridgeConfigSchema.optional(),
26062
26242
  subc: SubcConfigSchema.optional(),
26243
+ opencode: OpenCodeHostConfigSchema.optional(),
26063
26244
  github: GithubConfigSchema.optional(),
26064
26245
  gh_shim: GhShimConfigSchema.optional(),
26065
26246
  git: GitConfigSchema.optional()
@@ -26552,6 +26733,8 @@ function getStrippedTopLevelKeys(override) {
26552
26733
  stripped.push("sandbox.write_allow");
26553
26734
  if (override.subc !== undefined)
26554
26735
  stripped.push("subc");
26736
+ if (override.opencode !== undefined)
26737
+ stripped.push("opencode");
26555
26738
  if (override.github !== undefined)
26556
26739
  stripped.push("github");
26557
26740
  if (override.gh_shim !== undefined)
@@ -27159,7 +27342,8 @@ var NPM_LSP_TABLE = [
27159
27342
  npm: "typescript",
27160
27343
  binary: "tsserver",
27161
27344
  extensions: ["ts", "tsx", "js", "jsx", "mjs", "cjs", "mts", "cts"],
27162
- rootMarkers: ["tsconfig.json", "jsconfig.json", "package.json"]
27345
+ rootMarkers: ["tsconfig.json", "jsconfig.json", "package.json"],
27346
+ supportedMajor: 5
27163
27347
  },
27164
27348
  {
27165
27349
  id: "python",
@@ -27233,6 +27417,15 @@ var NPM_LSP_TABLE = [
27233
27417
  rootMarkers: ["biome.json", "biome.jsonc"]
27234
27418
  }
27235
27419
  ];
27420
+ function majorVersionOf(version) {
27421
+ const match = /^v?(\d+)(?:\.|$)/.exec(version.trim());
27422
+ return match ? Number(match[1]) : null;
27423
+ }
27424
+ function isVersionSupported(spec, version) {
27425
+ if (spec.supportedMajor === undefined)
27426
+ return true;
27427
+ return majorVersionOf(version) === spec.supportedMajor;
27428
+ }
27236
27429
 
27237
27430
  // src/lsp-project-relevance.ts
27238
27431
  import { existsSync as existsSync14, readdirSync as readdirSync4, readFileSync as readFileSync12 } from "node:fs";
@@ -27329,10 +27522,11 @@ function extensionOf(fileName) {
27329
27522
 
27330
27523
  // src/lsp-registry-probe.ts
27331
27524
  var NPM_REGISTRY_BASE = "https://registry.npmjs.org";
27332
- function pickEligibleVersion(response, graceDays, now = Date.now()) {
27525
+ function pickEligibleVersion(response, graceDays, now = Date.now(), accept = () => true) {
27333
27526
  const times = response.time || {};
27334
27527
  const cutoff = now - graceDays * 24 * 60 * 60 * 1000;
27335
27528
  const candidates = [];
27529
+ let releaseCount = 0;
27336
27530
  for (const [version, publishedAt] of Object.entries(times)) {
27337
27531
  if (version === "created" || version === "modified")
27338
27532
  continue;
@@ -27343,6 +27537,9 @@ function pickEligibleVersion(response, graceDays, now = Date.now()) {
27343
27537
  const ts = Date.parse(publishedAt);
27344
27538
  if (Number.isNaN(ts))
27345
27539
  continue;
27540
+ releaseCount += 1;
27541
+ if (!accept(version))
27542
+ continue;
27346
27543
  candidates.push({ version, publishedAt, ts });
27347
27544
  }
27348
27545
  candidates.sort((a, b) => b.ts - a.ts);
@@ -27351,10 +27548,11 @@ function pickEligibleVersion(response, graceDays, now = Date.now()) {
27351
27548
  return {
27352
27549
  version: eligible[0]?.version ?? null,
27353
27550
  blockedByGrace,
27551
+ outsideSupportedRange: releaseCount > 0 && candidates.length === 0,
27354
27552
  eligible: eligible.map(({ version, publishedAt }) => ({ version, publishedAt }))
27355
27553
  };
27356
27554
  }
27357
- async function probeRegistry(npmPackage, graceDays, fetchImpl = fetch) {
27555
+ async function probeRegistry(npmPackage, graceDays, fetchImpl = fetch, accept) {
27358
27556
  const encoded = encodeURIComponent(npmPackage).replace(/^%40/, "@");
27359
27557
  const url = `${NPM_REGISTRY_BASE}/${encoded}`;
27360
27558
  try {
@@ -27367,7 +27565,7 @@ async function probeRegistry(npmPackage, graceDays, fetchImpl = fetch) {
27367
27565
  return null;
27368
27566
  }
27369
27567
  const json = await res.json();
27370
- return pickEligibleVersion(json, graceDays);
27568
+ return pickEligibleVersion(json, graceDays, Date.now(), accept);
27371
27569
  } catch (err) {
27372
27570
  warn2(`[lsp] registry probe failed for ${npmPackage}: ${err}`);
27373
27571
  return null;
@@ -27418,11 +27616,11 @@ async function resolveTargetVersion(spec, config, fetchImpl = fetch) {
27418
27616
  }
27419
27617
  const cached = readVersionCheck(spec.npm);
27420
27618
  const weeklyMs = config.graceDays * 24 * 60 * 60 * 1000;
27421
- const cachedSafe = isSafeVersion(cached?.latest_eligible ?? null);
27619
+ const cachedSafe = isSafeVersion(cached?.latest_eligible ?? null) && isVersionSupported(spec, cached?.latest_eligible);
27422
27620
  if (cached && !shouldRecheckVersion(cached, weeklyMs) && cachedSafe) {
27423
27621
  return { version: cached.latest_eligible, pinned: false, probe: null };
27424
27622
  }
27425
- const probe = await probeRegistry(spec.npm, config.graceDays, fetchImpl);
27623
+ const probe = await probeRegistry(spec.npm, config.graceDays, fetchImpl, (candidate) => isVersionSupported(spec, candidate));
27426
27624
  if (!probe) {
27427
27625
  return {
27428
27626
  version: cachedSafe ? cached?.latest_eligible ?? null : null,
@@ -27557,11 +27755,16 @@ async function ensureServerInstalled(spec, config, fetchImpl, signal) {
27557
27755
  const { version, probe } = await resolveTargetVersion(spec, config, fetchImpl);
27558
27756
  if (!version) {
27559
27757
  const installed = isInstalled(spec.npm, spec.binary);
27560
- if (installed) {
27758
+ if (installed && installedVersionAllowed(spec, config)) {
27561
27759
  warn2(`[lsp] no eligible version of ${spec.npm} (grace=${config.graceDays}d); keeping existing install`);
27562
27760
  return { started: false, reason: "kept existing install" };
27563
27761
  }
27564
- const blocked = probe?.blockedByGrace ? `all versions are within ${config.graceDays}-day grace window` : "registry probe failed";
27762
+ const blocked = probe?.blockedByGrace ? `all versions are within ${config.graceDays}-day grace window` : probe?.outsideSupportedRange ? `no ${spec.supportedMajor}.x release on the registry` : "registry probe failed";
27763
+ if (installed) {
27764
+ const reason = `cached ${spec.npm} ${readInstalledMeta(spec.npm)?.version ?? "(unknown version)"} is outside the supported ${spec.supportedMajor}.x line and no replacement is available: ${blocked}`;
27765
+ warn2(`[lsp] ${reason}`);
27766
+ return { started: false, reason };
27767
+ }
27565
27768
  warn2(`[lsp] skipping ${spec.npm}: ${blocked}`);
27566
27769
  return { started: false, reason: blocked };
27567
27770
  }
@@ -27686,6 +27889,15 @@ function validateCachedNpmInstall(spec) {
27686
27889
  }
27687
27890
  return true;
27688
27891
  }
27892
+ function installedVersionAllowed(spec, config) {
27893
+ const installed = readInstalledMeta(spec.npm)?.version;
27894
+ if (!installed)
27895
+ return spec.supportedMajor === undefined;
27896
+ return isVersionSupported(spec, installed) || config.versions[spec.npm] === installed;
27897
+ }
27898
+ function cachedInstallUsable(spec, config) {
27899
+ return isInstalled(spec.npm, spec.binary) && validateCachedNpmInstall(spec) && installedVersionAllowed(spec, config);
27900
+ }
27689
27901
  function runAutoInstall(projectRoot, config, fetchImpl = fetch) {
27690
27902
  const cachedBinDirs = [];
27691
27903
  const skipped = [];
@@ -27698,7 +27910,7 @@ function runAutoInstall(projectRoot, config, fetchImpl = fetch) {
27698
27910
  return projectExtensions;
27699
27911
  };
27700
27912
  for (const spec of NPM_LSP_TABLE) {
27701
- if (isInstalled(spec.npm, spec.binary) && validateCachedNpmInstall(spec)) {
27913
+ if (cachedInstallUsable(spec, config)) {
27702
27914
  cachedBinDirs.push(lspBinDir(spec.npm));
27703
27915
  }
27704
27916
  if (config.disabled.has(spec.id) || spec.id === "typescript-sdk" && config.disabled.has("typescript")) {
@@ -27737,7 +27949,7 @@ function runAutoInstall(projectRoot, config, fetchImpl = fetch) {
27737
27949
  },
27738
27950
  skipped,
27739
27951
  installsComplete: Promise.all(installPromises).then(() => {}),
27740
- getCachedBinDirs: () => NPM_LSP_TABLE.filter((spec) => isInstalled(spec.npm, spec.binary) && validateCachedNpmInstall(spec)).map((spec) => lspBinDir(spec.npm))
27952
+ getCachedBinDirs: () => NPM_LSP_TABLE.filter((spec) => cachedInstallUsable(spec, config)).map((spec) => lspBinDir(spec.npm))
27741
27953
  };
27742
27954
  }
27743
27955
  async function pushLspPathsAfterAutoInstall(pool, projectRoot, cachedBinDirs) {
@@ -28868,6 +29080,23 @@ import {
28868
29080
  import { Container as Container2, Spacer as Spacer2, Text as Text2 } from "@earendil-works/pi-tui";
28869
29081
  import { Type as Type2 } from "typebox";
28870
29082
 
29083
+ // src/hashline-tag-sources.ts
29084
+ function hashlineTagSourceSentence(bashRewritesMintTags, isRegistered = () => true) {
29085
+ const rewrites = bashRewritesMintTags ? " (and accepted AFT `cat`/`head`/`tail` rewrites)" : "";
29086
+ const lookAlikes = [
29087
+ ...["aft_zoom", "aft_outline"].filter(isRegistered),
29088
+ "grep",
29089
+ ...isRegistered("aft_search") ? ["aft_search"] : []
29090
+ ].map((name) => `\`${name}\``);
29091
+ lookAlikes.push("conflict snippets");
29092
+ return `Only \`read\`${rewrites} mint hashline tags. ${joinList(lookAlikes)} do not. After navigation, call \`read\` on every file and range the patch addresses.`;
29093
+ }
29094
+ function joinList(items) {
29095
+ if (items.length <= 2)
29096
+ return items.join(" and ");
29097
+ return `${items.slice(0, -1).join(", ")}, and ${items[items.length - 1]}`;
29098
+ }
29099
+
28871
29100
  // src/tools/diff-format.ts
28872
29101
  import { diffLines } from "diff";
28873
29102
 
@@ -29322,7 +29551,9 @@ var WriteParams = Type2.Object({
29322
29551
  path: Type2.String({
29323
29552
  description: "Path to the file to write (absolute or relative to project root)"
29324
29553
  }),
29325
- content: Type2.String({ description: "Full file contents to write" })
29554
+ content: Type2.String({
29555
+ description: "Full file contents to write; for issue://N / pr://N, the comment body to post"
29556
+ })
29326
29557
  });
29327
29558
  var BatchEditParams = Type2.Object({
29328
29559
  oldString: Type2.Optional(Type2.String({ description: "Text to find for a batch find/replace edit" })),
@@ -29346,14 +29577,14 @@ var EditParams = Type2.Object({
29346
29577
  description: "Replacement content for symbol mode; only meaningful together with `symbol`. A call carrying one without the other is incomplete. For whole-file writes, use the `write` tool."
29347
29578
  })),
29348
29579
  appendContent: Type2.Optional(Type2.String({
29349
- description: "Append text to the end of the file (creates the file if missing, parent dirs auto-created). When set, other edit modes are ignored."
29580
+ description: "Append text to the end of the file (creates the file if missing, parent dirs auto-created). Mixing with other edit modes is rejected."
29350
29581
  })),
29351
29582
  edits: Type2.Optional(Type2.Array(BatchEditParams, {
29352
29583
  minItems: 1,
29353
29584
  description: "Batch edits — non-empty array of { oldString, newString }, { oldString, newString, replaceAll: true }, or { startLine, endLine, content } objects applied atomically."
29354
29585
  }))
29355
29586
  });
29356
- var HASHLINE_EDIT_DESCRIPTION = [
29587
+ var HASHLINE_EDIT_GRAMMAR = [
29357
29588
  "Apply a hashline patch. Arguments are exactly `{patch}` where `patch` is a non-empty string. Server-owned preview control is outside this schema.",
29358
29589
  "",
29359
29590
  "Quick reference:",
@@ -29361,10 +29592,15 @@ var HASHLINE_EDIT_DESCRIPTION = [
29361
29592
  "- Same canonical path: multiple sections compose in patch order against pre-request coordinates.",
29362
29593
  "- Addresses: `0` (BOF), `N` (one line), `N.=M` (range; `N..=M`/`N..M` also work), `<N`/`>N` (gap before/after), `N*`/`<N*`/`>N*` (block), and `$`/`$-K` (EOF-relative). A plain `N` PUT replaces; use `<N` or `>N` to insert.",
29363
29594
  "- PUT text: `PUT <address>:` followed by one or more `+` body rows (`+` alone is blank). A final patch newline is allowed. PUT without `:` copies `@name` (or the anonymous register) and takes no body; names use `@` plus ASCII letters, digits, `_`, or `-`.",
29364
- "- CUT: `CUT <address> [@name]`. REM: bare `REM` only, removing the whole file. MV: `MV <destination>` (one whitespace-free path, optional matching quotes), once and after any line operations. `*** Begin Patch`/`*** End Patch` is an optional envelope.",
29365
- "- Only `read` (and accepted AFT `cat`/`head`/`tail` rewrites) mint hashline tags. `aft_zoom`, `aft_outline`, `grep`, `aft_search`, and conflict snippets do not. After navigation, call `read` on every file and range the patch addresses."
29595
+ "- CUT: `CUT <address> [@name]`. REM: bare `REM` only, removing the whole file. MV: `MV <destination>` (one whitespace-free path, optional matching quotes), once and after any line operations. `*** Begin Patch`/`*** End Patch` is an optional envelope."
29366
29596
  ].join(`
29367
29597
  `);
29598
+ function hashlineEditDescription(config) {
29599
+ const bashRewrites = toolEnabled(config, "bash") && resolveBashConfig(config).rewrite;
29600
+ const tagSources = hashlineTagSourceSentence(bashRewrites, (name) => toolEnabled(config, name));
29601
+ return `${HASHLINE_EDIT_GRAMMAR}
29602
+ - ${tagSources}`;
29603
+ }
29368
29604
  var HashlineEditParams = Type2.Object({
29369
29605
  patch: Type2.String({
29370
29606
  minLength: 1,
@@ -29578,7 +29814,7 @@ PDFs aren't supported on the Pi harness yet.`, response);
29578
29814
  pi.registerTool({
29579
29815
  name: editName,
29580
29816
  label: editName,
29581
- description: HASHLINE_EDIT_DESCRIPTION,
29817
+ description: hashlineEditDescription(ctx.config),
29582
29818
  promptSnippet: `Apply a tagged hashline patch through the ${editName} tool`,
29583
29819
  promptGuidelines: [
29584
29820
  "Read a file to obtain its current hashline tag before editing it.",
@@ -29636,8 +29872,7 @@ PDFs aren't supported on the Pi harness yet.`, response);
29636
29872
  }
29637
29873
  if (isGithubResourcePath(filePathArg)) {
29638
29874
  const edits = params.edits;
29639
- const onlyFindReplace = Array.isArray(edits) && edits.length > 0 && params.appendContent === undefined && params.symbol === undefined && params.content === undefined && edits.every((entry) => typeof entry.oldString === "string" && (entry.newString === undefined || typeof entry.newString === "string") && entry.startLine === undefined && entry.endLine === undefined && entry.content === undefined);
29640
- if (!onlyFindReplace) {
29875
+ if (!isFindReplaceOnlyEdit(argsRecord)) {
29641
29876
  throw new Error("edit: GitHub resources support only edits[] find/replace entries with oldString and optional newString");
29642
29877
  }
29643
29878
  const bridge = bridgeFor(ctx, extCtx.cwd);
@@ -30165,34 +30400,17 @@ function registerAstTools(pi, ctx, surface) {
30165
30400
  // src/tools/bash.ts
30166
30401
  import { Container as Container3, Spacer as Spacer3, Text as Text3 } from "@earendil-works/pi-tui";
30167
30402
  import { Type as Type4 } from "typebox";
30168
-
30169
- // src/session-kind.ts
30170
- var MAGIC_CONTEXT_SUBAGENT_ENV = "MAGIC_CONTEXT_PI_SUBAGENT";
30171
- function piWorkerKind(extCtx, env = process.env) {
30172
- if (env[MAGIC_CONTEXT_SUBAGENT_ENV] === "1")
30173
- return "delegated";
30174
- if (extCtx?.hasUI === false)
30175
- return "headless";
30176
- return;
30177
- }
30178
- function isPiWorkerSession(extCtx, env = process.env) {
30179
- return piWorkerKind(extCtx, env) !== undefined;
30180
- }
30181
- function skipsEagerStartup(env = process.env) {
30182
- return piWorkerKind(undefined, env) === "delegated";
30183
- }
30184
-
30185
- // src/tools/bash.ts
30186
- var BASH_WAIT_POLL_INTERVAL_MS = 100;
30187
30403
  var REGEX_WAIT_SCAN_WINDOW_BYTES = 64 * 1024;
30188
30404
  function watchCallerRole(extCtx, env = process.env) {
30189
30405
  return isPiWorkerSession(extCtx, env) ? "worker" : "primary";
30190
30406
  }
30191
- function coerceConfiguredWatchTimeout(value, cap) {
30407
+ function coerceConfiguredWatchTimeout(value, role, cap) {
30192
30408
  try {
30193
- return coerceOptionalInt(value, "timeoutMs", 1, cap);
30409
+ return coerceOptionalInt(value, "timeoutMs", 1, maxWatchTimeoutMs(role, cap));
30194
30410
  } catch (error) {
30195
30411
  const message = error instanceof Error ? error.message : String(error);
30412
+ if (role === "worker")
30413
+ throw error;
30196
30414
  throw new Error(`${message} (bash.watch_sync_max_ms)`);
30197
30415
  }
30198
30416
  }
@@ -30208,7 +30426,9 @@ function resolveForegroundWaitMs(configured) {
30208
30426
  var BASH_TRANSPORT_TIMEOUT_MS2 = 30000;
30209
30427
  var DEFAULT_HARD_TIMEOUT_MS = 30 * 60 * 1000;
30210
30428
  var BASH_TRANSPORT_MARGIN_MS = 1e4;
30211
- function orchestratedTransportTimeoutMs(blockToCompletion, wait, effectiveTimeout, foregroundWaitMs) {
30429
+ function orchestratedTransportTimeoutMs(blockToCompletion, wait, effectiveTimeout, foregroundWaitMs, noHardKill = false) {
30430
+ if (noHardKill)
30431
+ return LONGEST_TIMER_DELAY_MS;
30212
30432
  const waitBudget = blockToCompletion || wait ? effectiveTimeout ?? DEFAULT_HARD_TIMEOUT_MS : foregroundWaitMs;
30213
30433
  return waitBudget + BASH_TRANSPORT_MARGIN_MS;
30214
30434
  }
@@ -30222,10 +30442,14 @@ var BashBaseParams = {
30222
30442
  })),
30223
30443
  description: Type4.Optional(Type4.String({
30224
30444
  description: "Human-readable description shown in UI logs. Helps users understand what the command does without reading shell syntax."
30225
- })),
30445
+ }))
30446
+ };
30447
+ var BashWaitParam = {
30226
30448
  wait: Type4.Optional(Type4.Boolean({
30227
30449
  description: "When true, run in the foreground without auto-promoting and wait until the command finishes or reaches its timeout; any new message detaches by default, while `bash.detach_on_user_message: false` keeps it blocking unless the message contains the literal `&detach`. The token is stripped before delivery; the rest of the message is preserved, and a token-only message becomes `(requested background detach)`. Use only when you know the result is required before doing anything else."
30228
- })),
30450
+ }))
30451
+ };
30452
+ var BashSandboxParam = {
30229
30453
  sandbox: Type4.Optional(Type4.Literal("host", {
30230
30454
  description: "Request one-command approval to run unsandboxed on the host; use only when native sandboxing blocks required work, and note that it is a no-op when sandboxing is disabled."
30231
30455
  }))
@@ -30249,16 +30473,65 @@ var BashPtyParams = {
30249
30473
  };
30250
30474
  var BashParams = Type4.Object({
30251
30475
  ...BashBaseParams,
30476
+ ...BashWaitParam,
30477
+ ...BashSandboxParam,
30252
30478
  ...BashBackgroundFlagParam,
30253
30479
  ...BashCompressionParam,
30254
30480
  ...BashPtyParams
30255
30481
  });
30256
- var BashForegroundOnlyParams = Type4.Object({
30257
- ...BashBaseParams,
30258
- ...BashCompressionParam
30259
- });
30260
- function bashParamsForConfig(backgroundEnabled) {
30261
- return backgroundEnabled ? BashParams : BashForegroundOnlyParams;
30482
+ function bashParamsForConfig(features, companions) {
30483
+ return Type4.Object({
30484
+ ...BashBaseParams,
30485
+ ...features.background ? BashWaitParam : {},
30486
+ ...features.sandbox ? BashSandboxParam : {},
30487
+ ...features.background ? {
30488
+ background: Type4.Optional(Type4.Boolean({ description: backgroundParamDescription(companions) }))
30489
+ } : {},
30490
+ ...features.compress ? BashCompressionParam : {},
30491
+ ...features.background ? {
30492
+ ...BashPtyParams,
30493
+ pty: Type4.Optional(Type4.Boolean({
30494
+ description: ptyParamDescription(companions, features.subagentBackground)
30495
+ }))
30496
+ } : {}
30497
+ });
30498
+ }
30499
+ function bashCompanionRegistered(config, companion) {
30500
+ return resolveBashConfig(config).background && toolEnabled(config, companion);
30501
+ }
30502
+ function registeredCompanions(config) {
30503
+ return {
30504
+ status: bashCompanionRegistered(config, "bash_status"),
30505
+ watch: bashCompanionRegistered(config, "bash_watch"),
30506
+ write: bashCompanionRegistered(config, "bash_write"),
30507
+ kill: bashCompanionRegistered(config, "bash_kill")
30508
+ };
30509
+ }
30510
+ function backgroundParamDescription(c) {
30511
+ let controls = "";
30512
+ if (c.watch) {
30513
+ controls += ` Use bash_watch to wait for completion or output patterns${c.status ? "; bash_status for a one-shot snapshot only." : "."}`;
30514
+ } else if (c.status) {
30515
+ controls += " Use bash_status for a one-shot snapshot.";
30516
+ }
30517
+ if (c.kill)
30518
+ controls += " Use bash_kill to terminate.";
30519
+ return `Spawn command in background and return immediately with a task_id.${controls} Ideal for long-running tasks like builds or dev servers.`;
30520
+ }
30521
+ function ptyParamDescription(c, subagentBackground) {
30522
+ let drive = "";
30523
+ if (c.status && c.write) {
30524
+ drive = ' Inspect with bash_status({ task_id, output_mode: "screen" }) and send input with bash_write.';
30525
+ } else if (c.status) {
30526
+ drive = ' Inspect with bash_status({ task_id, output_mode: "screen" }).';
30527
+ } else if (c.write) {
30528
+ drive = " Send input with bash_write.";
30529
+ }
30530
+ const workers = subagentBackground ? "" : " Unavailable in worker sessions because bash.subagent_background is false.";
30531
+ return `Spawn the command in a real PTY for interactive programs. Implies background: true automatically.${workers}${drive}`;
30532
+ }
30533
+ function nativeSandboxEnabled(config) {
30534
+ return config.sandbox?.enabled === true;
30262
30535
  }
30263
30536
  var BashTaskParams = Type4.Object({
30264
30537
  task_id: Type4.String({
@@ -30390,18 +30663,47 @@ function getBashSpawnHook(pi) {
30390
30663
  }
30391
30664
  return api.hooks?.bashSpawn;
30392
30665
  }
30666
+ function backgroundWaitSentence(c) {
30667
+ if (c.watch) {
30668
+ const noPolling = c.status ? ", and never loop `bash_status` to wait" : "";
30669
+ return `then \`bash_watch\` handles only a short remaining wait (in a main session a watch defaults to 30s, max bash.watch_sync_max_ms, 120s by default; in a delegated session a watch without a timeout waits until the command finishes); for anything longer end the turn and let the completion reminder wake you, or use bash({wait:true}) when the result is needed before anything else — never background a command and immediately \`bash_watch\` it (that wastes a turn for what foreground returns in one)${noPolling}.`;
30670
+ }
30671
+ return c.status ? "the task keeps running after the call returns, a completion reminder arrives when it exits, and `bash_status` reports its state and output. Use bash({wait:true}) instead when the result is needed before anything else." : "the task keeps running after the call returns and a completion reminder arrives when it exits. Use bash({wait:true}) instead when the result is needed before anything else.";
30672
+ }
30673
+ function ptyDriveClause(c) {
30674
+ if (c.status && c.write) {
30675
+ return ', and is driven with `bash_status({ output_mode: "screen" })` plus `bash_write`';
30676
+ }
30677
+ if (c.status)
30678
+ return ', and its screen is read with `bash_status({ output_mode: "screen" })`';
30679
+ if (c.write)
30680
+ return ", and is driven with `bash_write`";
30681
+ return "";
30682
+ }
30393
30683
  function registerBashTool(pi, ctx, aftSearchRegistered = false, registeredName = "bash", registerCompanions = true, shell = "bash") {
30394
30684
  const isPowerShell = shell === "powershell";
30395
30685
  const spawnHook = isPowerShell ? undefined : getBashSpawnHook(pi);
30396
30686
  const readToolName = "read";
30397
30687
  const grepToolName = "grep";
30398
30688
  const zoomSteer = toolEnabled(ctx.config, "aft_zoom") ? ", or `aft_zoom`" : "";
30399
- const searchSteer = aftSearchRegistered ? `use \`aft_search\` (concepts, identifiers, regex, literals), \`${readToolName}\`, \`aft_outline\`${zoomSteer} instead` : `use the \`${grepToolName}\` tool, \`${readToolName}\`, \`aft_outline\`${zoomSteer} instead`;
30689
+ const steerTools = [
30690
+ aftSearchRegistered ? "`aft_search` (concepts, identifiers, regex, literals)" : `the \`${grepToolName}\` tool`,
30691
+ `\`${readToolName}\``,
30692
+ ...toolEnabled(ctx.config, "aft_outline") ? ["`aft_outline`"] : []
30693
+ ].join(", ");
30694
+ const searchSteer = `use ${steerTools}${zoomSteer} instead`;
30695
+ const companions = registeredCompanions(ctx.config);
30400
30696
  const bashCfg = resolveBashConfig(ctx.config);
30401
30697
  let hostFallbackActive = false;
30402
30698
  const compressionSentence = bashCfg.compress ? " Output is compressed by default; pass `compressed: false` for raw output. Piped commands run verbatim and show the pipeline's output; for AFT's test/build summary, run the runner without `| head`, `| tail`, or `| grep`." : "";
30699
+ const supported = bashCfg.background ? [
30700
+ "workdir",
30701
+ "background tasks",
30702
+ ...bashCfg.compress ? ["compressed output"] : [],
30703
+ "PTY mode"
30704
+ ].join(", ") : bashCfg.compress ? "workdir and compressed output" : "workdir";
30403
30705
  const detachSentence = bashCfg.detach_on_user_message ? "Any new message detaches this wait. Set `bash.detach_on_user_message: false` to keep it blocking; even then, a message containing the literal `&detach` forces detachment, and the token is stripped before delivery; the rest of the message is preserved, while a token-only message becomes `(requested background detach)`." : "Because `bash.detach_on_user_message` is false, a new message leaves this wait blocking; include the literal `&detach` anywhere to force detachment, and the token is stripped before delivery; the rest of the message is preserved, while a token-only message becomes `(requested background detach)`.";
30404
- const tasksSentence = bashCfg.background ? ` Commands run in the foreground and return inline; \`wait: true\` blocks until a long command finishes instead of auto-promoting; ${detachSentence} Use it when you need the result before doing anything else; keep it off otherwise so auto-promote can remind you while you work. Use \`background: true\` yourself ONLY when you have other useful work to do while it runs; then \`bash_watch\` handles only a short remaining wait (default 30s, max bash.watch_sync_max_ms, 120s by default); for anything longer end the turn and let the completion reminder wake you, or use bash({wait:true}) when the result is needed before anything else — never background a command and immediately \`bash_watch\` it (that wastes a turn for what foreground returns in one), and never loop \`bash_status\` to wait. A \`nohup … &\` launch still holds the call if the child keeps stdout/stderr; redirect both or use background:true. \`pty: true\` runs interactive programs (REPLs, TUIs), implies background, and is driven with \`bash_status({ output_mode: "screen" })\` plus \`bash_write\`.` : " Commands run in the foreground to completion; `timeout` is the hard kill cap (default 30 minutes).";
30706
+ const tasksSentence = bashCfg.background ? ` Commands run in the foreground and return inline; \`wait: true\` blocks until a long command finishes instead of auto-promoting; ${detachSentence} Use it when you need the result before doing anything else; keep it off otherwise so auto-promote can remind you while you work. Use \`background: true\` yourself ONLY when you have other useful work to do while it runs; ${backgroundWaitSentence(companions)} A \`nohup … &\` launch still holds the call if the child keeps stdout/stderr; redirect both or use background:true. \`pty: true\` runs interactive programs (REPLs, TUIs), implies background${ptyDriveClause(companions)}.` : " Commands run in the foreground to completion; `timeout` is the hard kill cap (default 30 minutes).";
30405
30707
  pi.registerTool({
30406
30708
  name: registeredName,
30407
30709
  label: registeredName,
@@ -30410,13 +30712,18 @@ function registerBashTool(pi, ctx, aftSearchRegistered = false, registeredName =
30410
30712
  PowerShell syntax is not analyzed as POSIX shell. Each command requires explicit approval so syntax AFT cannot safely interpret is never auto-allowed.` : `Execute shell commands.${compressionSentence}${tasksSentence}
30411
30713
 
30412
30714
  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}. 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.`,
30413
- promptSnippet: isPowerShell ? "Run PowerShell commands (timeout in milliseconds; supports workdir, background tasks, compressed output, PTY mode)" : bashCfg.background ? "Run shell commands (timeout in milliseconds; supports workdir, background tasks, compressed output, PTY mode)" : "Run shell commands (timeout in milliseconds; supports workdir and compressed output)",
30715
+ promptSnippet: isPowerShell ? `Run PowerShell commands (timeout in milliseconds; supports ${supported})` : `Run shell commands (timeout in milliseconds; supports ${supported})`,
30414
30716
  promptGuidelines: isPowerShell ? ["Use PowerShell syntax. Every command requires explicit approval."] : [
30415
30717
  `DO NOT use bash for code search or exploration — ${searchSteer}.`,
30416
- "Set compressed: false when you need ANSI color codes in the output.",
30718
+ ...bashCfg.compress ? ["Set compressed: false when you need ANSI color codes in the output."] : [],
30417
30719
  "Piped commands run verbatim and show the pipeline's output; run test/build tools without pipes when you need AFT's summary."
30418
30720
  ],
30419
- parameters: bashParamsForConfig(bashCfg.background),
30721
+ parameters: bashParamsForConfig({
30722
+ background: bashCfg.background,
30723
+ compress: bashCfg.compress,
30724
+ sandbox: nativeSandboxEnabled(ctx.config),
30725
+ subagentBackground: bashCfg.subagent_background
30726
+ }, companions),
30420
30727
  async execute(_toolCallId, params, signal, onUpdate, extCtx) {
30421
30728
  const bridge = bridgeFor(ctx, extCtx.cwd);
30422
30729
  const bashCfg = resolveBashConfig(ctx.config);
@@ -30426,7 +30733,7 @@ DO NOT use bash for code search or code exploration. If you are about to run gre
30426
30733
  const ptyRows = backgroundDisabled ? undefined : coerceOptionalInt(params.ptyRows, "ptyRows", 1, 60);
30427
30734
  const ptyCols = backgroundDisabled ? undefined : coerceOptionalInt(params.ptyCols, "ptyCols", 1, 140);
30428
30735
  const compressed = coerceBoolean(params.compressed, true);
30429
- const requestedWait = coerceBoolean(params.wait);
30736
+ const requestedWait = !backgroundDisabled && coerceBoolean(params.wait);
30430
30737
  const rawRequestedPty = coerceBoolean(params.pty);
30431
30738
  const rawRequestedBackground = coerceBoolean(params.background);
30432
30739
  if (requestedWait && rawRequestedPty) {
@@ -30442,6 +30749,8 @@ DO NOT use bash for code search or code exploration. If you are about to run gre
30442
30749
  }
30443
30750
  const blockToCompletion = backgroundDisabled || requestedWait || workerForcedForeground;
30444
30751
  const effectiveBackground = !blockToCompletion && (rawRequestedBackground || requestedPty);
30752
+ const isWorker = isPiWorkerSession(extCtx);
30753
+ const workerUnboundedWait = isWorker && requestedWait && timeout === undefined;
30445
30754
  let spawnContext = {
30446
30755
  command: params.command,
30447
30756
  cwd: params.workdir
@@ -30476,7 +30785,7 @@ DO NOT use bash for code search or code exploration. If you are about to run gre
30476
30785
  sandbox: params.sandbox,
30477
30786
  ...isPowerShell ? { shell: "powershell" } : {}
30478
30787
  }, extCtx, {
30479
- transportTimeoutMs: orchestratedTransportTimeoutMs(blockToCompletion, requestedWait, timeout, foregroundWaitMs),
30788
+ transportTimeoutMs: orchestratedTransportTimeoutMs(blockToCompletion, requestedWait, timeout, foregroundWaitMs, workerUnboundedWait),
30480
30789
  onProgress: ({ text }) => {
30481
30790
  streamed += text;
30482
30791
  const displayText = truncateToVisualLines(streamed, 100);
@@ -30487,10 +30796,10 @@ DO NOT use bash for code search or code exploration. If you are about to run gre
30487
30796
  const fallbackCause = classifyBashHostFallbackError(error);
30488
30797
  if (isPowerShell || !bashCfg.host_fallback || fallbackCause === undefined)
30489
30798
  throw error;
30490
- if (rawRequestedBackground) {
30799
+ if (!backgroundDisabled && rawRequestedBackground) {
30491
30800
  throw new Error(`${BASH_HOST_FALLBACK_REFUSAL}; background:true is unsupported.`);
30492
30801
  }
30493
- if (rawRequestedPty) {
30802
+ if (requestedPty) {
30494
30803
  throw new Error(`${BASH_HOST_FALLBACK_REFUSAL}; pty:true is unsupported.`);
30495
30804
  }
30496
30805
  if (!extCtx.hasUI || typeof extCtx.ui?.confirm !== "function") {
@@ -30545,6 +30854,8 @@ DO NOT use bash for code search or code exploration. If you are about to run gre
30545
30854
  registerBashCompanionTools(pi, ctx);
30546
30855
  }
30547
30856
  function registerBashCompanionTools(pi, ctx, enabled = { bashStatus: true, bashWatch: true, bashWrite: true, bashKill: true }) {
30857
+ if (!resolveBashConfig(ctx.config).background)
30858
+ return;
30548
30859
  if (enabled.bashStatus) {
30549
30860
  pi.registerTool(createBashStatusTool(ctx));
30550
30861
  }
@@ -30565,7 +30876,7 @@ function createBashStatusTool(ctx) {
30565
30876
  return {
30566
30877
  name: "bash_status",
30567
30878
  label: "bash_status",
30568
- description: "Read-only snapshot of a background bash task. Returns immediately. Never waits. One look to check on a task is fine — never loop it to wait for completion. To wait, use bash_watch.",
30879
+ description: `Read-only snapshot of a background bash task. Returns immediately. Never waits. One look to check on a task is fine — never loop it to wait for completion.${bashCompanionRegistered(ctx.config, "bash_watch") ? " To wait, use bash_watch." : ""}`,
30569
30880
  promptSnippet: "Inspect a background bash task by task_id",
30570
30881
  parameters: BashStatusParams,
30571
30882
  async execute(_toolCallId, params, _signal, _onUpdate, extCtx) {
@@ -30583,10 +30894,10 @@ function createBashWatchTool(ctx) {
30583
30894
  return {
30584
30895
  name: "bash_watch",
30585
30896
  label: "bash_watch",
30586
- description: "Watch a background bash task. Sync waits are for a short remaining wait on a task (default 30s, max `bash.watch_sync_max_ms`, 120s by default); for anything longer end the turn on `bash({background:true})` and let the completion reminder wake you, or use `bash({wait:true})` when the result is needed before anything else. 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.",
30897
+ description: `Watch a background bash task. ${WATCH_SYNC_DEFAULTS_DESCRIPTION}. In a main session sync waits are for a short remaining wait on a task; for anything longer end the turn on \`bash({background:true})\` and let the completion reminder wake you, or use \`bash({wait:true})\` when the result is needed before anything else. 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.${bashCompanionRegistered(ctx.config, "bash_status") ? " Never loop bash_status to wait." : ""}`,
30587
30898
  promptSnippet: "Wait for or watch a background bash task",
30588
30899
  parameters: BashWatchParams,
30589
- async execute(_toolCallId, params, _signal, _onUpdate, extCtx) {
30900
+ async execute(_toolCallId, params, signal, _onUpdate, extCtx) {
30590
30901
  const bridge = bridgeFor(ctx, extCtx.cwd);
30591
30902
  const waitFor = parseWaitPattern(params.pattern);
30592
30903
  const bashCfg = resolveBashConfig(ctx.config);
@@ -30623,10 +30934,10 @@ A notification will fire when the pattern matches or the task exits.`, watchDeta
30623
30934
  }
30624
30935
  const syncWaitCap = bashCfg.watch_sync_max_ms;
30625
30936
  const role = watchCallerRole(extCtx);
30626
- const effectiveWaitMs = workerForcedSync ? syncWaitCap : resolveWatchTimeoutMs(coerceConfiguredWatchTimeout(params.timeout_ms, syncWaitCap), role, syncWaitCap);
30627
- const data = await waitForBashStatus(ctx, bridge, extCtx, params.task_id, undefined, waitFor, true, effectiveWaitMs);
30937
+ const effectiveWaitMs = workerForcedSync ? undefined : resolveWatchTimeoutMs(coerceConfiguredWatchTimeout(params.timeout_ms, role, syncWaitCap), role, syncWaitCap);
30938
+ const data = await waitForBashStatus(ctx, bridge, extCtx, params.task_id, undefined, waitFor, true, effectiveWaitMs, signal);
30628
30939
  if (data.waited?.reason === "user_message") {
30629
- const convertedText = await convertToAsyncWatchOnAbort(bridge, extCtx, params.task_id, waitFor, coerceBoolean(params.once, true), data.waited.elapsed_ms);
30940
+ const convertedText = await convertToAsyncWatchOnAbort(bridge, extCtx, params.task_id, waitFor, coerceBoolean(params.once, true), data.waited.elapsed_ms, role);
30630
30941
  return textResult(convertedText, { waited: data.waited });
30631
30942
  }
30632
30943
  const text = await formatBashStatus(extCtx, params.task_id, data, undefined, { role, capMs: syncWaitCap });
@@ -30634,10 +30945,11 @@ A notification will fire when the pattern matches or the task exits.`, watchDeta
30634
30945
  }
30635
30946
  };
30636
30947
  }
30637
- async function convertToAsyncWatchOnAbort(bridge, extCtx, taskId, waitFor, once, elapsedMs) {
30948
+ async function convertToAsyncWatchOnAbort(bridge, extCtx, taskId, waitFor, once, elapsedMs, role) {
30638
30949
  const interrupted = `Sync watch for task ${taskId} was interrupted because you sent a message after ${elapsedMs}ms of waiting. `;
30950
+ const reminderFallback = interruptedWatchTail(role, `The task is still running in the background. A completion reminder will be ` + `delivered automatically when the task exits.`);
30639
30951
  if (!waitFor) {
30640
- return interrupted + `The task is still running in the background. A completion reminder will be ` + `delivered automatically when the task exits; don't poll bash_status.`;
30952
+ return interrupted + interruptedWatchTail(role, `The task is still running in the background. A completion reminder will be ` + `delivered automatically when the task exits; don't poll bash_status.`);
30641
30953
  }
30642
30954
  const notifyParams = {
30643
30955
  task_id: taskId,
@@ -30653,19 +30965,19 @@ async function convertToAsyncWatchOnAbort(bridge, extCtx, taskId, waitFor, once,
30653
30965
  const registered = await callBashBridge(bridge, "bash_notify", notifyParams, extCtx);
30654
30966
  if (registered.success === false) {
30655
30967
  unmarkExplicitControl(sessionId, taskId);
30656
- return interrupted + `Auto-registering an async watch failed (${String(registered.message ?? "unknown error")}). ` + `The task is still running in the background. A completion reminder will be ` + `delivered automatically when the task exits.`;
30968
+ return interrupted + `Auto-registering an async watch failed (${String(registered.message ?? "unknown error")}). ` + reminderFallback;
30657
30969
  }
30658
- return interrupted + `The wait has been converted to an async watch (${registered.watch_id}). ` + `A notification will fire when the pattern matches or the task exits.`;
30970
+ return interrupted + `The wait has been converted to an async watch (${registered.watch_id}). ` + interruptedWatchTail(role, `A notification will fire when the pattern matches or the task exits.`);
30659
30971
  } catch (err) {
30660
30972
  unmarkExplicitControl(sessionId, taskId);
30661
- return interrupted + `Auto-registering an async watch failed (${err instanceof Error ? err.message : String(err)}). ` + `The task is still running in the background. A completion reminder will be ` + `delivered automatically when the task exits.`;
30973
+ return interrupted + `Auto-registering an async watch failed (${err instanceof Error ? err.message : String(err)}). ` + reminderFallback;
30662
30974
  }
30663
30975
  }
30664
30976
  function createBashWriteTool(ctx) {
30665
30977
  return {
30666
30978
  name: "bash_write",
30667
30979
  label: "bash_write",
30668
- description: 'Write input bytes to a running PTY bash task. PTY-only; check bash_status reports mode: "pty" first. ' + 'Input is either a string (verbatim bytes) or an array mixing strings and { key: "esc" | "enter" | "up" | "ctrl-c" | ... } objects ' + 'for atomic text+key sequences such as [ "iHello", { key: "esc" }, ":wq", { key: "enter" } ]. ' + "Named keys cover enter/return/tab/space/backspace/esc/escape, arrows, home/end/page-up/page-down/delete/insert, f1..f12, and ctrl-a..ctrl-z. " + "Maximum 1 MiB per call (post-expansion).",
30980
+ description: `Write input bytes to a running PTY bash task. PTY-only${bashCompanionRegistered(ctx.config, "bash_status") ? '; check bash_status reports mode: "pty" first.' : "."} ` + 'Input is either a string (verbatim bytes) or an array mixing strings and { key: "esc" | "enter" | "up" | "ctrl-c" | ... } objects ' + 'for atomic text+key sequences such as [ "iHello", { key: "esc" }, ":wq", { key: "enter" } ]. ' + "Named keys cover enter/return/tab/space/backspace/esc/escape, arrows, home/end/page-up/page-down/delete/insert, f1..f12, and ctrl-a..ctrl-z. " + "Maximum 1 MiB per call (post-expansion).",
30669
30981
  promptSnippet: "Write keystrokes/input to a PTY bash task",
30670
30982
  parameters: BashWriteParams,
30671
30983
  async execute(_toolCallId, params, _signal, _onUpdate, extCtx) {
@@ -30729,10 +31041,17 @@ async function bashStatusSnapshot(bridge, extCtx, taskId, outputMode, options, c
30729
31041
  stderr_offset: cursor?.stderr
30730
31042
  }, extCtx, options);
30731
31043
  }
30732
- async function waitForBashStatus(ctx, bridge, extCtx, taskId, outputMode, waitFor, waitForExit, effectiveWaitMs) {
30733
- const startedAt = monotonicNowMs();
30734
- const deadline = startedAt + effectiveWaitMs;
30735
- const elapsedMs = () => Math.round(monotonicNowMs() - startedAt);
31044
+ async function waitForBashStatus(ctx, bridge, extCtx, taskId, outputMode, waitFor, waitForExit, effectiveWaitMs, abortSignal) {
31045
+ const startedAt = watchClock.now();
31046
+ const deadline = effectiveWaitMs === undefined ? Number.POSITIVE_INFINITY : startedAt + effectiveWaitMs;
31047
+ const elapsedMs = () => Math.round(watchClock.now() - startedAt);
31048
+ const pause = () => watchClock.sleep(Math.min(watchPollDelayMs(elapsedMs()), Math.max(0, deadline - watchClock.now())), abortSignal);
31049
+ const waited = (reason, extra = {}) => ({
31050
+ reason,
31051
+ elapsed_ms: elapsedMs(),
31052
+ ...effectiveWaitMs === undefined ? {} : { limit_ms: effectiveWaitMs },
31053
+ ...extra
31054
+ });
30736
31055
  let spillCursor = { output: 0, stderr: 0 };
30737
31056
  const scanState = {
30738
31057
  output: { text: "", baseOffset: 0 },
@@ -30751,31 +31070,29 @@ async function waitForBashStatus(ctx, bridge, extCtx, taskId, outputMode, waitFo
30751
31070
  markTaskWaiting(sessionId, taskId);
30752
31071
  let sawTerminal = false;
30753
31072
  let lastData;
31073
+ let busySince;
30754
31074
  try {
30755
31075
  while (true) {
31076
+ if (abortSignal?.aborted) {
31077
+ return withWaited(lastData ?? unavailableSnapshot(), waited("aborted"));
31078
+ }
30756
31079
  let data;
30757
31080
  try {
30758
31081
  data = await bashStatusSnapshot(bridge, extCtx, taskId, outputMode, bridgeOptions, waitFor ? spillCursor : undefined);
30759
31082
  } catch (err) {
30760
31083
  if (!isBridgeTransportTimeout(err))
30761
31084
  throw err;
31085
+ busySince ??= watchClock.now();
30762
31086
  if (isSyncWatchAborted(sessionId)) {
30763
- return withWaited(lastData ?? unavailableSnapshot(), {
30764
- reason: "user_message",
30765
- elapsed_ms: elapsedMs(),
30766
- limit_ms: effectiveWaitMs
30767
- });
31087
+ return withWaited(lastData ?? unavailableSnapshot(), waited("user_message"));
30768
31088
  }
30769
- if (monotonicNowMs() >= deadline) {
30770
- return withWaited(lastData ?? unavailableSnapshot(), {
30771
- reason: "unavailable",
30772
- elapsed_ms: elapsedMs(),
30773
- limit_ms: effectiveWaitMs
30774
- });
31089
+ if (watchClock.now() >= deadline || watchClock.now() - busySince >= WATCH_UNAVAILABLE_GIVE_UP_MS) {
31090
+ return withWaited(lastData ?? unavailableSnapshot(), waited("unavailable"));
30775
31091
  }
30776
- await sleep(Math.min(BASH_WAIT_POLL_INTERVAL_MS, Math.max(0, deadline - monotonicNowMs())));
31092
+ await pause();
30777
31093
  continue;
30778
31094
  }
31095
+ busySince = undefined;
30779
31096
  lastData = data;
30780
31097
  const terminal = isTerminalStatus(data.status);
30781
31098
  if (waitFor) {
@@ -30800,14 +31117,11 @@ async function waitForBashStatus(ctx, bridge, extCtx, taskId, outputMode, waitFo
30800
31117
  await markBgCompletionDelivered({ ctx, directory: extCtx.cwd, sessionID: sessionId }, taskId);
30801
31118
  }
30802
31119
  const matchStream = data.mode === "pty" ? undefined : chunk.stream === "output" ? "stdout" : "stderr";
30803
- return withWaited(data, {
30804
- reason: "matched",
30805
- elapsed_ms: elapsedMs(),
30806
- limit_ms: effectiveWaitMs,
31120
+ return withWaited(data, waited("matched", {
30807
31121
  match: match.text,
30808
31122
  match_offset: state.baseOffset + match.byteOffset,
30809
31123
  match_stream: matchStream
30810
- });
31124
+ }));
30811
31125
  }
30812
31126
  if (waitFor.kind === "substring") {
30813
31127
  const trimmed = trimWaitScanBuffer(state.text, state.baseOffset, waitFor);
@@ -30823,27 +31137,15 @@ async function waitForBashStatus(ctx, bridge, extCtx, taskId, outputMode, waitFo
30823
31137
  consumeBgCompletion(sessionId, taskId);
30824
31138
  await markBgCompletionDelivered({ ctx, directory: extCtx.cwd, sessionID: sessionId }, taskId);
30825
31139
  }
30826
- return withWaited(data, {
30827
- reason: "exited",
30828
- elapsed_ms: elapsedMs(),
30829
- limit_ms: effectiveWaitMs
30830
- });
31140
+ return withWaited(data, waited("exited"));
30831
31141
  }
30832
31142
  if (isSyncWatchAborted(sessionId)) {
30833
- return withWaited(data, {
30834
- reason: "user_message",
30835
- elapsed_ms: elapsedMs(),
30836
- limit_ms: effectiveWaitMs
30837
- });
31143
+ return withWaited(data, waited("user_message"));
30838
31144
  }
30839
- if (monotonicNowMs() >= deadline) {
30840
- return withWaited(data, {
30841
- reason: "timeout",
30842
- elapsed_ms: elapsedMs(),
30843
- limit_ms: effectiveWaitMs
30844
- });
31145
+ if (watchClock.now() >= deadline) {
31146
+ return withWaited(data, waited("timeout"));
30845
31147
  }
30846
- await sleep(Math.min(BASH_WAIT_POLL_INTERVAL_MS, Math.max(0, deadline - monotonicNowMs())));
31148
+ await pause();
30847
31149
  }
30848
31150
  } finally {
30849
31151
  if (waitForExit && !sawTerminal)
@@ -30950,16 +31252,19 @@ function withWaited(data, waited) {
30950
31252
  return { ...data, waited };
30951
31253
  }
30952
31254
  function formatWaitSummary(waited, details, watchRole) {
30953
- const waitedText = formatWatchWaited(waited.elapsed_ms, waited.limit_ms, watchRole.capMs);
31255
+ const waitedText = formatWatchWaited(waited.elapsed_ms, waited.limit_ms, watchRole.role === "primary" ? watchRole.capMs : undefined);
30954
31256
  if (waited.reason === "matched") {
30955
31257
  const stream = waited.match_stream ? ` in ${waited.match_stream}` : "";
30956
31258
  return `${waitedText}; matched ${JSON.stringify(waited.match ?? "")}${stream} at offset ${waited.match_offset ?? 0}.`;
30957
31259
  }
30958
31260
  if (waited.reason === "timeout") {
30959
- return `${waitedText}; timeout reached without match. ${watchTimeoutSteer(watchRole.role, watchRole.capMs, "timeout_ms")}`;
31261
+ return `${waitedText}; timeout reached without match. ${watchTimeoutSteer(watchRole.role, "timeout_ms")}`;
30960
31262
  }
30961
31263
  if (waited.reason === "unavailable") {
30962
- return `${waitedText}; the bridge was busy, so task state is unknown. Do not poll; let the task's completion notification wake the session, or use one bash_status snapshot on the next normal tool call.`;
31264
+ return `${waitedText}; ${watchUnavailableSteer(watchRole.role)}`;
31265
+ }
31266
+ if (waited.reason === "aborted") {
31267
+ return `${waitedText}; the watch was cancelled. The task keeps running.`;
30963
31268
  }
30964
31269
  const exit = typeof details.exit_code === "number" ? `, exit ${details.exit_code}` : "";
30965
31270
  return `${waitedText}; task exited (${details.status}${exit}).`;
@@ -30981,10 +31286,10 @@ ${formatWaitSummary(details.waited, details, watchRole)}`;
30981
31286
  text += `
30982
31287
  ${details.output_preview}`;
30983
31288
  }
30984
- const workerWatchTimeout = watchRole.role === "worker" && details.waited?.reason === "timeout";
30985
- if (!isTerminalStatus(details.status) && !workerWatchTimeout) {
31289
+ const workerWatchReturned = watchRole.role === "worker" && details.waited !== undefined;
31290
+ if (!isTerminalStatus(details.status) && !workerWatchReturned) {
30986
31291
  text += `
30987
- A completion reminder will be delivered automatically; don't poll.`;
31292
+ ${runningTaskStatusHint(watchRole.role)}`;
30988
31293
  }
30989
31294
  }
30990
31295
  return text;
@@ -31928,6 +32233,9 @@ function registerNavigateTool(pi, ctx) {
31928
32233
 
31929
32234
  // src/tools/reading.ts
31930
32235
  import { Type as Type10 } from "typebox";
32236
+ function isGithubPathArg(value) {
32237
+ return typeof value === "string" && (value.startsWith("issue://") || value.startsWith("pr://"));
32238
+ }
31931
32239
  var OutlineParams = Type10.Object({
31932
32240
  target: Type10.Union([Type10.String(), Type10.Array(Type10.String())], {
31933
32241
  description: "What to outline: a file path, directory path, URL (http:// or https://), or array of file paths. The mode is auto-detected: URLs by `http://`/`https://` prefix, directories by stat, arrays as multi-file."
@@ -31952,7 +32260,7 @@ var ZoomParams = Type10.Object({
31952
32260
  description: "Symbol name for code, or heading text for Markdown/HTML. Pass a string or array for batched lookups in the same file/URL."
31953
32261
  })),
31954
32262
  targets: Type10.Optional(Type10.Union([ZoomTarget, Type10.Array(ZoomTarget)], {
31955
- description: "Cross-file batch: `{ path, symbol }` or an array of them. Mutually exclusive with path/url/symbols."
32263
+ 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."
31956
32264
  })),
31957
32265
  contextLines: optionalInt(1, Number.MAX_SAFE_INTEGER, "Lines of context before/after (default: 3)"),
31958
32266
  callgraph: Type10.Optional(Type10.Boolean({
@@ -32106,6 +32414,13 @@ function renderZoomResult(result, args, theme, context, options = { expanded: tr
32106
32414
  context
32107
32415
  });
32108
32416
  }
32417
+ function outlineFocusSteer(config) {
32418
+ const zoomEnabled = toolEnabled(config, "aft_zoom");
32419
+ const reader = zoomEnabled ? "aft_zoom" : "read";
32420
+ const locate = toolEnabled(config, "aft_search") ? `aft_search + ${reader}` : reader;
32421
+ const callgraph = zoomEnabled ? toolEnabled(config, "aft_callgraph") ? " aft_zoom with `callgraph:true` gives one-level forward calls-out; use aft_callgraph only for reverse callers or multi-level traces." : " aft_zoom with `callgraph:true` gives one-level forward calls-out." : "";
32422
+ return `For understanding a specific feature, prefer ${locate} on named symbols; use aft_outline on a whole directory only for high-level structure mapping.${callgraph}`;
32423
+ }
32109
32424
  function registerReadingTools(pi, ctx, surface) {
32110
32425
  const zoomEnabled = toolEnabled(ctx.config, "aft_zoom");
32111
32426
  const ghReadEnabled = resolveGithubConfig(ctx.config).read;
@@ -32117,7 +32432,7 @@ function registerReadingTools(pi, ctx, surface) {
32117
32432
  label: "outline",
32118
32433
  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.
32119
32434
 
32120
- ${zoomEnabled ? "For understanding a specific feature, prefer aft_search + aft_zoom on named symbols; use aft_outline on a whole directory only for high-level structure mapping. aft_zoom with `callgraph:true` gives one-level forward calls-out; use aft_callgraph only for reverse callers or multi-level traces." : "For understanding a specific feature, prefer aft_search + read on named symbols; use aft_outline on a whole directory only for high-level structure mapping."}
32435
+ ${outlineFocusSteer(ctx.config)}
32121
32436
 
32122
32437
  Pass a single \`target\`:
32123
32438
  • file path → outline that file (with signatures)
@@ -32184,56 +32499,49 @@ When a list is cut, the reply ends with \`shown N of M <unit> (<reason>) · narr
32184
32499
  pi.registerTool(withPathAliasPreparation({
32185
32500
  name: "aft_zoom",
32186
32501
  label: "zoom",
32187
- 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 exactly ONE mode: `{ path, symbols }`, `{ url, symbols }`, or `{ targets }`. `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." + (githubZoomDescription ? `
32502
+ 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 ? `
32188
32503
 
32189
32504
  ${githubZoomDescription}` : ""),
32190
32505
  parameters: ZoomParams,
32191
32506
  async execute(_toolCallId, params, _signal, _onUpdate, extCtx) {
32192
32507
  const bridge = bridgeFor(ctx, extCtx.cwd);
32193
- const hasTargetsProvided = (t) => {
32194
- if (isEmptyParam(t))
32195
- return false;
32196
- const entryEmpty = (entry) => {
32197
- if (!entry || typeof entry !== "object")
32198
- return true;
32199
- const fp = entry.path;
32200
- const sym = entry.symbol;
32201
- const fpEmpty = typeof fp !== "string" || fp.length === 0;
32202
- const symEmpty = typeof sym !== "string" || sym.length === 0;
32203
- return fpEmpty && symEmpty;
32204
- };
32205
- if (Array.isArray(t))
32206
- return !t.every(entryEmpty);
32207
- return !entryEmpty(t);
32208
- };
32209
32508
  const targetsInput = coerceJsonCollectionParam(params.targets, "targets");
32210
- const hasPath = !isEmptyParam(params.path);
32211
- const hasUrl = !isEmptyParam(params.url);
32212
- const hasTargets = hasTargetsProvided(targetsInput);
32213
- const hasSymbols = !isEmptyParam(params.symbols);
32509
+ const targetEntries = usableZoomTargets(targetsInput);
32510
+ const hasPath = !isBlankParam(params.path);
32511
+ const hasUrl = !isBlankParam(params.url);
32512
+ const hasTargets = targetEntries.length > 0;
32513
+ const hasSymbols = !isBlankParam(params.symbols);
32214
32514
  const wantCallgraph = coerceBoolean(params.callgraph);
32215
32515
  const contextLines = coerceOptionalInt(params.contextLines, "contextLines", 1, Number.MAX_SAFE_INTEGER);
32216
32516
  if (hasTargets) {
32217
- if (hasPath || hasUrl || hasSymbols) {
32218
- throw new Error("'targets' is mutually exclusive with 'path', 'url', and 'symbols'");
32517
+ if (hasPath && hasUrl) {
32518
+ throw new Error("Provide exactly ONE of 'path' or 'url' — not both");
32219
32519
  }
32220
- const targets = Array.isArray(targetsInput) ? targetsInput : [targetsInput];
32221
- if (targets.length === 0) {
32222
- throw new Error("'targets' must be a non-empty object or array");
32223
- }
32224
- for (const [i, entry] of targets.entries()) {
32225
- if (!entry || typeof entry.path !== "string" || entry.path.length === 0) {
32520
+ const targets = targetEntries.map((entry, i) => {
32521
+ if (!entry || typeof entry !== "object" || Array.isArray(entry)) {
32226
32522
  throw new Error(`targets[${i}].path must be a non-empty string`);
32227
32523
  }
32228
- if (typeof entry.symbol !== "string" || entry.symbol.length === 0) {
32229
- throw new Error(`targets[${i}].symbol must be a non-empty string`);
32230
- }
32231
- }
32232
- const resolvedTargets = await Promise.all(targets.map((t) => resolvePathArg(extCtx.cwd, t.path)));
32524
+ const record = entry;
32525
+ const targetPath = isBlankParam(record.path) ? record.filePath : record.path;
32526
+ return {
32527
+ path: typeof targetPath === "string" ? targetPath : "",
32528
+ symbol: typeof record.symbol === "string" ? record.symbol : ""
32529
+ };
32530
+ });
32531
+ const localPaths = targets.map((target) => target.path).filter((path) => path.trim().length > 0 && !isGithubPathArg(path));
32532
+ if (hasPath && !isGithubPathArg(params.path))
32533
+ localPaths.push(params.path);
32534
+ const resolvedTargets = await Promise.all(localPaths.map((path) => resolvePathArg(extCtx.cwd, path)));
32233
32535
  await assertReadPathPermissions(extCtx, ctx, resolvedTargets);
32234
32536
  const rawArgs = {
32235
32537
  targets: targets.map((target) => ({ filePath: target.path, symbol: target.symbol }))
32236
32538
  };
32539
+ if (hasPath)
32540
+ rawArgs.filePath = params.path;
32541
+ else if (hasUrl)
32542
+ rawArgs.url = params.url;
32543
+ if (hasSymbols)
32544
+ rawArgs.symbols = params.symbols;
32237
32545
  if (contextLines !== undefined)
32238
32546
  rawArgs.contextLines = contextLines;
32239
32547
  if (wantCallgraph)
@@ -32250,7 +32558,7 @@ ${githubZoomDescription}` : ""),
32250
32558
  if (hasPath && hasUrl) {
32251
32559
  throw new Error("Provide exactly ONE of 'path' or 'url' — not both");
32252
32560
  }
32253
- const githubPath = hasPath && (String(params.path).startsWith("issue://") || String(params.path).startsWith("pr://")) || hasUrl && (String(params.url).startsWith("issue://") || String(params.url).startsWith("pr://"));
32561
+ const githubPath = hasPath && isGithubPathArg(params.path) || hasUrl && isGithubPathArg(params.url);
32254
32562
  if (!hasUrl && !githubPath) {
32255
32563
  const file = await resolvePathArg(extCtx.cwd, params.path);
32256
32564
  await assertReadPathPermissions(extCtx, ctx, file);
@@ -32289,7 +32597,7 @@ var SafetyParams = Type11.Object({
32289
32597
  description: "Safety operation"
32290
32598
  }),
32291
32599
  path: Type11.Optional(Type11.String({
32292
- description: "File path (required for history, optional for undo, optional for restore — restores only that file). Absolute or relative to project root."
32600
+ 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."
32293
32601
  })),
32294
32602
  name: Type11.Optional(Type11.String({ description: "Checkpoint name (required for checkpoint, restore)" })),
32295
32603
  files: Type11.Optional(Type11.Array(Type11.String(), {
@@ -32722,6 +33030,8 @@ function withLiveRestriction(surface, ctx) {
32722
33030
  function resolvePiToolSurface(config, pi) {
32723
33031
  const disabled = new Set(resolvedDisabledTools(config));
32724
33032
  const ok = (name) => !disabled.has(name);
33033
+ const background = resolveBashConfig(config).background;
33034
+ const companion = (name) => background && ok(name);
32725
33035
  const powershellEnabled = (pi ? piPowerShellEnabledFromHost(pi) : undefined) ?? resolvePiPowerShellFallback(config);
32726
33036
  return {
32727
33037
  hoistBash: ok("bash"),
@@ -32743,10 +33053,10 @@ function resolvePiToolSurface(config, pi) {
32743
33053
  move: ok("aft_move"),
32744
33054
  astSearch: ok("ast_grep_search"),
32745
33055
  astReplace: ok("ast_grep_replace"),
32746
- bashStatus: ok("bash_status"),
32747
- bashWatch: ok("bash_watch"),
32748
- bashWrite: ok("bash_write"),
32749
- bashKill: ok("bash_kill")
33056
+ bashStatus: companion("bash_status"),
33057
+ bashWatch: companion("bash_watch"),
33058
+ bashWrite: companion("bash_write"),
33059
+ bashKill: companion("bash_kill")
32750
33060
  };
32751
33061
  }
32752
33062
  var FUNNEL_BOUND = Symbol.for("aft.pi.registration_funnel_bound");
@@ -33016,7 +33326,10 @@ function registerShutdownCleanup(fn) {
33016
33326
 
33017
33327
  // src/workflow-hints.ts
33018
33328
  var HEADING = "## IMPORTANT NOTICE about your tools";
33019
- var HASHLINE_TAG_SOURCE_HINT = "**Hashline edit tags**: Only `read` (and accepted AFT `cat`/`head`/`tail` rewrites) mint hashline tags. `aft_zoom`, `aft_outline`, `grep`, `aft_search`, and conflict snippets do not. After navigation, call `read` on every file and range the patch addresses.";
33329
+ function hashlineTagSourceHint(bashRewritesMintTags, isRegistered = () => true) {
33330
+ return `**Hashline edit tags**: ${hashlineTagSourceSentence(bashRewritesMintTags, isRegistered)}`;
33331
+ }
33332
+ var HASHLINE_TAG_SOURCE_HINT = hashlineTagSourceHint(true);
33020
33333
  function buildWorkflowHints(opts) {
33021
33334
  const sections = [];
33022
33335
  const grepName = "grep";
@@ -33031,6 +33344,8 @@ function buildWorkflowHints(opts) {
33031
33344
  const hasInspect = !opts.absentTools.has("aft_inspect");
33032
33345
  const hasBash = !opts.absentTools.has(bashName);
33033
33346
  const hasBgBash = opts.bashBackgroundEnabled && hasBash && !opts.absentTools.has("bash_status");
33347
+ const hasWatch = !opts.absentTools.has("bash_watch");
33348
+ const hasWrite = !opts.absentTools.has("bash_write");
33034
33349
  if (hasBash && opts.bashCompressionEnabled) {
33035
33350
  sections.push([
33036
33351
  "**Test/build output**: bash output is auto-compressed for non-piped commands. Piped commands run verbatim and show the pipeline's output. For AFT's test/build summary, run the runner without filters:",
@@ -33072,17 +33387,19 @@ function buildWorkflowHints(opts) {
33072
33387
  ].join(`
33073
33388
  `));
33074
33389
  }
33075
- if (hasBgBash) {
33390
+ if (hasBgBash && hasWatch) {
33076
33391
  sections.push([
33077
33392
  `**Long-running commands** (builds, installs, full test suites): run them in the FOREGROUND — use \`${bashName}({ command, wait: true })\` when you know it is long and need the result before anything else; if you send a new message, the wait detaches to background; otherwise omit \`wait\` so auto-promote can hand you a reminder while you work.`,
33078
33393
  "- `background: true` is ONLY for when you have OTHER useful work to do while it runs: start it, do the other work, and the completion reminder delivers the result (or spawn a subagent for the side work). Do NOT background a command and then immediately `bash_watch` it — that spends a whole extra turn waiting for something foreground returns in one.",
33079
33394
  "- `bash_watch` is for blocking on an ALREADY-backgrounded task once you've run out of parallel work (sync — the user can interrupt), or reacting to a specific early output line (async: background:true + pattern). Never loop `bash_status` to wait — it's a one-shot inspector."
33080
33395
  ].join(`
33081
33396
  `));
33397
+ }
33398
+ if (hasBgBash && hasWrite) {
33082
33399
  sections.push(`**PTY / interactive commands**: PTY mode is for interactive REPLs and terminal apps (python, node, bash itself, vim). Start with \`${bashName}({ command: "python", pty: true, background: true })\`, read the screen with \`bash_status({ task_id, output_mode: "screen" })\`, and send input with \`bash_write({ task_id, input: "..." })\`.`);
33083
33400
  }
33084
33401
  if (opts.hashlineEffective === true) {
33085
- sections.push(HASHLINE_TAG_SOURCE_HINT);
33402
+ sections.push(hashlineTagSourceHint(hasBash && opts.bashRewriteEnabled !== false, (name) => !opts.absentTools.has(name)));
33086
33403
  }
33087
33404
  if (sections.length === 0) {
33088
33405
  return null;
@@ -33097,9 +33414,11 @@ ${sections.join(`
33097
33414
  `)}`;
33098
33415
  }
33099
33416
  function buildHintsFromConfig(config, absentTools, hashlineEffective = false) {
33417
+ const bashCfg = resolveBashConfig(config);
33100
33418
  return buildWorkflowHints({
33101
- bashBackgroundEnabled: resolveBashConfig(config).background,
33102
- bashCompressionEnabled: resolveBashConfig(config).compress,
33419
+ bashBackgroundEnabled: bashCfg.background,
33420
+ bashCompressionEnabled: bashCfg.compress,
33421
+ bashRewriteEnabled: bashCfg.rewrite,
33103
33422
  absentTools,
33104
33423
  hashlineEffective
33105
33424
  });
@@ -33126,6 +33445,10 @@ function registerWorkflowHints(pi, config, surface) {
33126
33445
  absent.add("bash");
33127
33446
  if (!surface.bashStatus)
33128
33447
  absent.add("bash_status");
33448
+ if (!surface.bashWatch)
33449
+ absent.add("bash_watch");
33450
+ if (!surface.bashWrite)
33451
+ absent.add("bash_write");
33129
33452
  const hintsBlock = buildHintsFromConfig(config, absent, piHashlineEffective(config, surface));
33130
33453
  if (!hintsBlock)
33131
33454
  return;