@cortexkit/aft-opencode 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.
Files changed (54) hide show
  1. package/dist/config-live-reload.d.ts +8 -0
  2. package/dist/config-live-reload.d.ts.map +1 -1
  3. package/dist/config.d.ts +13 -0
  4. package/dist/config.d.ts.map +1 -1
  5. package/dist/entry/server.js +1603 -839
  6. package/dist/hashline-tag-sources.d.ts +18 -0
  7. package/dist/hashline-tag-sources.d.ts.map +1 -0
  8. package/dist/index.d.ts.map +1 -1
  9. package/dist/index.js +980 -605
  10. package/dist/lsp-auto-install.d.ts +30 -0
  11. package/dist/lsp-auto-install.d.ts.map +1 -1
  12. package/dist/lsp-github-table.d.ts +4 -3
  13. package/dist/lsp-github-table.d.ts.map +1 -1
  14. package/dist/lsp-npm-table.d.ts +10 -0
  15. package/dist/lsp-npm-table.d.ts.map +1 -1
  16. package/dist/lsp-registry-probe.d.ts +8 -4
  17. package/dist/lsp-registry-probe.d.ts.map +1 -1
  18. package/dist/permissions/v2-service.d.ts +110 -5
  19. package/dist/permissions/v2-service.d.ts.map +1 -1
  20. package/dist/rpc/register.d.ts +0 -4
  21. package/dist/rpc/register.d.ts.map +1 -1
  22. package/dist/shared/session-directory.d.ts +7 -0
  23. package/dist/shared/session-directory.d.ts.map +1 -1
  24. package/dist/shared/subagent-detect.d.ts +11 -4
  25. package/dist/shared/subagent-detect.d.ts.map +1 -1
  26. package/dist/tool-registration.d.ts +8 -5
  27. package/dist/tool-registration.d.ts.map +1 -1
  28. package/dist/tools/_shared.d.ts +16 -15
  29. package/dist/tools/_shared.d.ts.map +1 -1
  30. package/dist/tools/bash.d.ts +24 -1
  31. package/dist/tools/bash.d.ts.map +1 -1
  32. package/dist/tools/bash_watch.d.ts +4 -4
  33. package/dist/tools/bash_watch.d.ts.map +1 -1
  34. package/dist/tools/bash_write.d.ts.map +1 -1
  35. package/dist/tools/definitions/v2.d.ts +2 -0
  36. package/dist/tools/definitions/v2.d.ts.map +1 -1
  37. package/dist/tools/hoisted/v2.d.ts +8 -1
  38. package/dist/tools/hoisted/v2.d.ts.map +1 -1
  39. package/dist/tools/hoisted.d.ts +9 -7
  40. package/dist/tools/hoisted.d.ts.map +1 -1
  41. package/dist/tools/reading.d.ts +0 -3
  42. package/dist/tools/reading.d.ts.map +1 -1
  43. package/dist/v2-prompt-detach.d.ts +69 -0
  44. package/dist/v2-prompt-detach.d.ts.map +1 -0
  45. package/dist/wakes/runtime-consumer.d.ts +2 -0
  46. package/dist/wakes/runtime-consumer.d.ts.map +1 -1
  47. package/dist/wakes/session-delivery.d.ts +9 -0
  48. package/dist/wakes/session-delivery.d.ts.map +1 -1
  49. package/dist/workflow-hints.d.ts +20 -7
  50. package/dist/workflow-hints.d.ts.map +1 -1
  51. package/package.json +8 -8
  52. package/src/entry/server-runtime.mjs +21 -5
  53. package/src/shared/session-directory.ts +92 -0
  54. package/src/shared/subagent-detect.ts +89 -28
package/dist/index.js CHANGED
@@ -7878,29 +7878,84 @@ function isTerminalStatus(status) {
7878
7878
  function sleep(ms) {
7879
7879
  return new Promise((resolve) => setTimeout(resolve, ms));
7880
7880
  }
7881
- function monotonicNowMs() {
7882
- return performance.now();
7883
- }
7884
7881
  // ../aft-bridge/dist/bash-hints.js
7885
7882
  import * as fs from "node:fs";
7886
7883
  import * as os from "node:os";
7887
7884
  import * as path from "node:path";
7888
7885
  var DEFAULT_PRIMARY_WATCH_TIMEOUT_MS = 30000;
7889
- 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).";
7886
+ var MAX_WATCH_TIMEOUT_MS = 1800000;
7887
+ var LONGEST_TIMER_DELAY_MS = 2147483647;
7888
+ 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";
7889
+ 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.";
7890
7890
  function resolveWatchTimeoutMs(requestedMs, role, capMs) {
7891
- const fallback = role === "worker" ? capMs : DEFAULT_PRIMARY_WATCH_TIMEOUT_MS;
7892
- return Math.min(requestedMs ?? fallback, capMs);
7891
+ if (role === "worker")
7892
+ return requestedMs;
7893
+ return Math.min(requestedMs ?? DEFAULT_PRIMARY_WATCH_TIMEOUT_MS, capMs);
7894
+ }
7895
+ function maxWatchTimeoutMs(role, capMs) {
7896
+ return role === "worker" ? MAX_WATCH_TIMEOUT_MS : capMs;
7897
+ }
7898
+ function watchPollDelayMs(elapsedMs) {
7899
+ if (elapsedMs < 5000)
7900
+ return 100;
7901
+ if (elapsedMs < 30000)
7902
+ return 250;
7903
+ if (elapsedMs < 120000)
7904
+ return 500;
7905
+ return 1000;
7906
+ }
7907
+ var watchClock = {
7908
+ now: () => performance.now(),
7909
+ sleep: abortableSleep
7910
+ };
7911
+ function abortableSleep(ms, signal) {
7912
+ if (signal?.aborted)
7913
+ return Promise.resolve();
7914
+ return new Promise((resolve) => {
7915
+ const done = () => {
7916
+ clearTimeout(timer);
7917
+ signal?.removeEventListener("abort", done);
7918
+ resolve();
7919
+ };
7920
+ const timer = setTimeout(done, ms);
7921
+ signal?.addEventListener("abort", done, { once: true });
7922
+ });
7893
7923
  }
7894
7924
  function formatWatchWaited(elapsedMs, limitMs, capMs) {
7895
- const limit = limitMs >= capMs ? `limit ${limitMs}ms, the bash.watch_sync_max_ms cap` : `limit ${limitMs}ms`;
7925
+ let limit;
7926
+ if (limitMs === undefined)
7927
+ limit = "no limit: waits until the command finishes";
7928
+ else if (capMs !== undefined && limitMs >= capMs)
7929
+ limit = `limit ${limitMs}ms, the bash.watch_sync_max_ms cap`;
7930
+ else
7931
+ limit = `limit ${limitMs}ms`;
7896
7932
  return `Waited ${Math.round(elapsedMs)}ms (${limit})`;
7897
7933
  }
7898
- function watchTimeoutSteer(role, capMs, timeoutParam = "timeoutMs") {
7934
+ function watchTimeoutSteer(role, timeoutParam = "timeoutMs") {
7899
7935
  if (role === "worker") {
7900
- 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.`;
7936
+ 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.`;
7901
7937
  }
7902
7938
  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.";
7903
7939
  }
7940
+ var WATCH_UNAVAILABLE_GIVE_UP_MS = 120000;
7941
+ var WORKER_KEEP_WAITING = "Call bash_watch again to keep waiting; don't report a result until the command finishes.";
7942
+ function interruptedWatchTail(role, primaryTail) {
7943
+ return role === "worker" ? `The task is still running. ${WORKER_KEEP_WAITING}` : primaryTail;
7944
+ }
7945
+ function workerBackgroundTaskNote(taskId, taskIdArg = "taskId") {
7946
+ return `
7947
+
7948
+ NOTE (subagent session): Continue with other work if you have it. If you don't, call bash_watch({ ${taskIdArg}: "${taskId}" }) to wait for completion before returning to the parent; without a timeout it waits until the command finishes. Subagents don't survive turn-end and won't be woken when the command finishes.`;
7949
+ }
7950
+ function runningTaskStatusHint(role) {
7951
+ return role === "worker" ? "To wait for it, call bash_watch; don't poll." : "A completion reminder will be delivered automatically; don't poll.";
7952
+ }
7953
+ function watchUnavailableSteer(role) {
7954
+ const unknown = "the bridge was busy, so task state is unknown.";
7955
+ if (role === "worker")
7956
+ return `${unknown} ${WORKER_KEEP_WAITING}`;
7957
+ 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.`;
7958
+ }
7904
7959
  var CONFLICT_HINT = `
7905
7960
 
7906
7961
  [Hint] Use aft_conflicts to see all conflict regions across files in a single call.`;
@@ -8759,6 +8814,9 @@ function isPassiveCommand(command) {
8759
8814
  return PASSIVE_COMMANDS.has(command);
8760
8815
  }
8761
8816
 
8817
+ // ../aft-bridge/dist/transport.js
8818
+ var WORKER_SESSION_FIELD = "worker_session";
8819
+
8762
8820
  // ../aft-bridge/dist/bridge.js
8763
8821
  var DEFAULT_BRIDGE_TIMEOUT_MS = 30000;
8764
8822
  var BRIDGE_HANG_TIMEOUT_THRESHOLD = 2;
@@ -9177,9 +9235,11 @@ class BinaryBridge {
9177
9235
  params.edit_slot_survives = editSlotSurvives;
9178
9236
  this.logHashlineRegistrationCarrier("tool_call", sessionId, editSlotSurvives);
9179
9237
  }
9180
- const { preview, ...sendOptions } = options ?? {};
9238
+ const { preview, workerSession, ...sendOptions } = options ?? {};
9181
9239
  if (preview === true)
9182
9240
  params.preview = true;
9241
+ if (workerSession === true)
9242
+ params[WORKER_SESSION_FIELD] = true;
9183
9243
  return await this.send("tool_call", params, Object.keys(sendOptions).length > 0 ? sendOptions : undefined);
9184
9244
  }
9185
9245
  async sendWithVersionMismatchRetry(command, params, options, canRetryAfterVersionSwap) {
@@ -9248,7 +9308,7 @@ class BinaryBridge {
9248
9308
  if (command === "cancel_request" || Object.hasOwn(params, "command") || Object.hasOwn(params, "method")) {
9249
9309
  const nested = { ...params };
9250
9310
  const reserved = {};
9251
- for (const key of ["session_id", "lsp_hints"]) {
9311
+ for (const key of ["session_id", "lsp_hints", WORKER_SESSION_FIELD]) {
9252
9312
  if (Object.hasOwn(nested, key)) {
9253
9313
  reserved[key] = nested[key];
9254
9314
  delete nested[key];
@@ -10026,6 +10086,35 @@ function coerceJsonCollectionParam(value, field) {
10026
10086
  }
10027
10087
  throw new Error(`${field} must be ${shape} (got an invalid type)`);
10028
10088
  }
10089
+ function isBlankParam(value) {
10090
+ if (typeof value === "string")
10091
+ return value.trim().length === 0;
10092
+ return isEmptyParam(value);
10093
+ }
10094
+ function usableZoomTargets(targets) {
10095
+ if (isEmptyParam(targets))
10096
+ return [];
10097
+ const entries = Array.isArray(targets) ? targets : [targets];
10098
+ return entries.filter((entry) => {
10099
+ if (entry === null || entry === undefined)
10100
+ return false;
10101
+ if (typeof entry !== "object")
10102
+ return true;
10103
+ const record = entry;
10104
+ return !(isBlankParam(record.path) && isBlankParam(record.filePath) && isBlankParam(record.symbol));
10105
+ });
10106
+ }
10107
+ function isFindReplaceOnlyEdit(args) {
10108
+ const absent = (value) => value === undefined || value === null || value === "";
10109
+ const absentLine = (value) => absent(value) || value === 0;
10110
+ const edits = args.edits;
10111
+ return Array.isArray(edits) && edits.length > 0 && absent(args.appendContent) && absent(args.symbol) && absent(args.content) && edits.every((entry) => {
10112
+ if (entry === null || typeof entry !== "object")
10113
+ return false;
10114
+ const item = entry;
10115
+ return typeof item.oldString === "string" && (absent(item.newString) || typeof item.newString === "string") && absentLine(item.startLine) && absentLine(item.endLine) && absent(item.content);
10116
+ });
10117
+ }
10029
10118
  // ../aft-bridge/dist/config-error.js
10030
10119
  import { homedir as homedir5 } from "node:os";
10031
10120
  import { isAbsolute as isAbsolute2, join as join6 } from "node:path";
@@ -13288,6 +13377,13 @@ class SubcTransportShuttingDownError extends SubcCallError {
13288
13377
  this.name = "SubcTransportShuttingDownError";
13289
13378
  }
13290
13379
  }
13380
+
13381
+ class ReconnectKeyChangedError extends Error {
13382
+ constructor() {
13383
+ super("the subc daemon restarted and its connection key changed; the reconnect did not complete within the deadline, retry");
13384
+ this.name = "ReconnectKeyChangedError";
13385
+ }
13386
+ }
13291
13387
  function isSubcClientClosedError(error) {
13292
13388
  return error instanceof SubcError && error.message === "client closed";
13293
13389
  }
@@ -13790,6 +13886,8 @@ class SubcTransport {
13790
13886
  body.edit_slot_survives = editSlotSurvives;
13791
13887
  if (preview === true)
13792
13888
  body.preview = true;
13889
+ if (options?.workerSession === true)
13890
+ body[WORKER_SESSION_FIELD] = true;
13793
13891
  const reply = await this.pool.routeRequest(this.identityFor(sessionId), body, timeoutMs, onProgress, this.generation, abortSignal);
13794
13892
  return reliftReply(reply);
13795
13893
  }
@@ -13800,7 +13898,10 @@ class SubcTransport {
13800
13898
  }
13801
13899
  const { timeoutMs, onProgress, abortSignal } = this.splitOptions(options);
13802
13900
  const session = typeof params.session_id === "string" ? params.session_id : undefined;
13803
- const body = { name: command, arguments: params };
13901
+ const { [WORKER_SESSION_FIELD]: workerSession, ...args } = params;
13902
+ const body = { name: command, arguments: args };
13903
+ if (workerSession === true)
13904
+ body[WORKER_SESSION_FIELD] = true;
13804
13905
  const editSlotSurvives = this.pool.getEditSlotSurvives();
13805
13906
  if (editSlotSurvives !== undefined)
13806
13907
  body.edit_slot_survives = editSlotSurvives;
@@ -13837,6 +13938,7 @@ class SubcTransportPool {
13837
13938
  lifecycleRegistration = null;
13838
13939
  outerFacadeEvictor;
13839
13940
  client = null;
13941
+ hadWorkingClient = false;
13840
13942
  connecting = null;
13841
13943
  routeReopenRetryDelayMs = RECONNECT_RETRY_FLOOR_MS;
13842
13944
  routeReopenRetry = null;
@@ -14281,6 +14383,10 @@ class SubcTransportPool {
14281
14383
  }
14282
14384
  async routeRequest(identity, body, timeoutMs, onProgress, expectedGeneration, abortSignal) {
14283
14385
  const callStartedAt = performance.now();
14386
+ const callDeadlineMs = timeoutMs ?? SUBC_DEFAULT_REQUEST_TIMEOUT_MS;
14387
+ let scheduledDelayMs = 0;
14388
+ const spentMs = () => Math.max(performance.now() - callStartedAt, scheduledDelayMs);
14389
+ const remainingMs = () => callDeadlineMs - spentMs();
14284
14390
  const root = asCanonicalRootPath(identity.project_root);
14285
14391
  let generation = expectedGeneration;
14286
14392
  if (this.lifecycleEnabled()) {
@@ -14296,7 +14402,49 @@ class SubcTransportPool {
14296
14402
  try {
14297
14403
  let client;
14298
14404
  try {
14299
- client = await this.ensureClient();
14405
+ let keyChanged;
14406
+ while (true) {
14407
+ const remaining = remainingMs();
14408
+ if (remaining <= 0)
14409
+ throw keyChanged ?? callDeadlinePassedError(undefined, callDeadlineMs);
14410
+ let timer;
14411
+ try {
14412
+ const acquired = await Promise.race([
14413
+ this.ensureClient(),
14414
+ new Promise((resolve) => {
14415
+ timer = setTimeout(() => resolve(CALL_DEADLINE_EXPIRED), remaining);
14416
+ })
14417
+ ]);
14418
+ if (acquired === CALL_DEADLINE_EXPIRED) {
14419
+ throw keyChanged ?? callDeadlinePassedError(undefined, callDeadlineMs);
14420
+ }
14421
+ client = acquired;
14422
+ break;
14423
+ } catch (error) {
14424
+ if (!(error instanceof ReconnectKeyChangedError))
14425
+ throw error;
14426
+ keyChanged = error;
14427
+ if (spentMs() + this.nextRouteReopenDelayMs() >= callDeadlineMs)
14428
+ throw error;
14429
+ } finally {
14430
+ clearTimeout(timer);
14431
+ }
14432
+ const { delayMs, wait } = this.waitForRouteReopenBackoff();
14433
+ scheduledDelayMs = spentMs() + delayMs;
14434
+ let retryTimer;
14435
+ try {
14436
+ const waited = await Promise.race([
14437
+ wait,
14438
+ new Promise((resolve) => {
14439
+ retryTimer = setTimeout(() => resolve(CALL_DEADLINE_EXPIRED), callDeadlineMs - (performance.now() - callStartedAt));
14440
+ })
14441
+ ]);
14442
+ if (waited === CALL_DEADLINE_EXPIRED)
14443
+ throw keyChanged;
14444
+ } finally {
14445
+ clearTimeout(retryTimer);
14446
+ }
14447
+ }
14300
14448
  this.assertRecordLive(record);
14301
14449
  } catch (error) {
14302
14450
  throw this.annotateReapError(error, record);
@@ -14331,12 +14479,8 @@ class SubcTransportPool {
14331
14479
  throw error;
14332
14480
  }
14333
14481
  };
14334
- const callDeadlineMs = timeoutMs ?? SUBC_DEFAULT_REQUEST_TIMEOUT_MS;
14335
14482
  const reloadWaitBudgetMs = Math.min(callDeadlineMs, ROUTE_OPEN_RELOAD_WAIT_CEILING_MS);
14336
- let scheduledDelayMs = 0;
14337
14483
  let lastRefusal;
14338
- const spentMs = () => Math.max(performance.now() - callStartedAt, scheduledDelayMs);
14339
- const remainingMs = () => callDeadlineMs - spentMs();
14340
14484
  const openRouteWithinDeadline = async () => {
14341
14485
  const remaining = remainingMs();
14342
14486
  if (remaining <= 0)
@@ -14539,10 +14683,14 @@ class SubcTransportPool {
14539
14683
  throw new SubcTransportShuttingDownError;
14540
14684
  }
14541
14685
  this.client = client;
14686
+ this.hadWorkingClient = true;
14542
14687
  this.transportFailures = 0;
14543
14688
  return client;
14544
14689
  }).catch((error) => {
14545
14690
  this.connecting = null;
14691
+ if (this.hadWorkingClient && error instanceof AuthError) {
14692
+ throw new ReconnectKeyChangedError;
14693
+ }
14546
14694
  throw error;
14547
14695
  });
14548
14696
  return this.connecting;
@@ -16234,6 +16382,7 @@ async function acquireBridge(locationDirectory, options, dependencies = {}) {
16234
16382
  let owner = state.standalone;
16235
16383
  if (!owner) {
16236
16384
  const configByRoot = new Map;
16385
+ const versionMismatchHandlers = new Map;
16237
16386
  const configuredLoader = options.poolOptions.projectConfigLoader;
16238
16387
  const pool = await createPool({
16239
16388
  ...options,
@@ -16243,21 +16392,29 @@ async function acquireBridge(locationDirectory, options, dependencies = {}) {
16243
16392
  projectConfigLoader: (projectRoot) => ({
16244
16393
  ...configuredLoader?.(projectRoot) ?? {},
16245
16394
  ...configByRoot.get(canonicalizeProjectRoot(projectRoot)) ?? {}
16246
- })
16395
+ }),
16396
+ onVersionMismatch: (binaryVersion, minVersion) => [...versionMismatchHandlers.values()].at(-1)?.(binaryVersion, minVersion) ?? null
16247
16397
  }
16248
16398
  });
16249
- owner = { pool, configByRoot, referencesByRoot: new Map, references: 0 };
16399
+ owner = {
16400
+ pool,
16401
+ configByRoot,
16402
+ versionMismatchHandlers,
16403
+ referencesByRoot: new Map,
16404
+ references: 0
16405
+ };
16250
16406
  state.standalone = owner;
16251
16407
  }
16252
16408
  owner.references += 1;
16253
16409
  owner.referencesByRoot.set(directory, (owner.referencesByRoot.get(directory) ?? 0) + 1);
16254
16410
  owner.configByRoot.set(directory, { ...options.configOverrides });
16255
16411
  const acquiredOwner = owner;
16256
- return new LocationBridgeLease(directory, mode, owner.pool, async () => {
16412
+ const lease = new LocationBridgeLease(directory, mode, owner.pool, async () => {
16257
16413
  await withLock(directory, () => withLock(OWNER_LOCK, async () => {
16258
16414
  const current = lifecycleState().standalone;
16259
16415
  if (current !== acquiredOwner)
16260
16416
  return;
16417
+ current.versionMismatchHandlers?.delete(lease);
16261
16418
  current.references -= 1;
16262
16419
  const rootReferences = (current.referencesByRoot.get(directory) ?? 1) - 1;
16263
16420
  if (rootReferences > 0)
@@ -16272,6 +16429,10 @@ async function acquireBridge(locationDirectory, options, dependencies = {}) {
16272
16429
  await current.pool.shutdown();
16273
16430
  }));
16274
16431
  });
16432
+ const onVersionMismatch = options.poolOptions.onVersionMismatch;
16433
+ if (onVersionMismatch)
16434
+ owner.versionMismatchHandlers?.set(lease, onVersionMismatch);
16435
+ return lease;
16275
16436
  });
16276
16437
  });
16277
16438
  }
@@ -16292,6 +16453,14 @@ var STANDALONE_DETACH_KEYWORDS_PATTERN = new RegExp(STANDALONE_DETACH_KEYWORD_SO
16292
16453
  function containsStandaloneDetachKeyword(messageText) {
16293
16454
  return STANDALONE_DETACH_KEYWORD_PATTERN.test(messageText);
16294
16455
  }
16456
+ function standaloneDetachKeywordRanges(messageText) {
16457
+ const ranges = [];
16458
+ for (const match of messageText.matchAll(STANDALONE_DETACH_KEYWORDS_PATTERN)) {
16459
+ const start = (match.index ?? 0) + (match[1]?.length ?? 0);
16460
+ ranges.push([start, start + BASH_WAIT_DETACH_MAGIC_KEYWORD.length]);
16461
+ }
16462
+ return ranges;
16463
+ }
16295
16464
  function stripStandaloneDetachKeywords(messageText) {
16296
16465
  return messageText.replace(STANDALONE_DETACH_KEYWORDS_PATTERN, "$1");
16297
16466
  }
@@ -17683,31 +17852,36 @@ var ORT_PLATFORM_MAP = {
17683
17852
  arm64: {
17684
17853
  assetName: `onnxruntime-osx-arm64-${ORT_VERSION}`,
17685
17854
  libName: "libonnxruntime.dylib",
17686
- archiveType: "tgz"
17855
+ archiveType: "tgz",
17856
+ librarySha256: "872533f130f1839a5bc01788ddb4f75c83a189763441ba1178788ed965449289"
17687
17857
  }
17688
17858
  },
17689
17859
  linux: {
17690
17860
  x64: {
17691
17861
  assetName: `onnxruntime-linux-x64-${ORT_VERSION}`,
17692
17862
  libName: "libonnxruntime.so",
17693
- archiveType: "tgz"
17863
+ archiveType: "tgz",
17864
+ librarySha256: "d132535d051344ff5c64c9c200004150559049a81ed330eb4422c1962fb6b7e4"
17694
17865
  },
17695
17866
  arm64: {
17696
17867
  assetName: `onnxruntime-linux-aarch64-${ORT_VERSION}`,
17697
17868
  libName: "libonnxruntime.so",
17698
- archiveType: "tgz"
17869
+ archiveType: "tgz",
17870
+ librarySha256: "52b2a0e75e79468404284fec38ad5ee1a7a996232274f5e1b84f3e793fb07554"
17699
17871
  }
17700
17872
  },
17701
17873
  win32: {
17702
17874
  x64: {
17703
17875
  assetName: `onnxruntime-win-x64-${ORT_VERSION}`,
17704
17876
  libName: "onnxruntime.dll",
17705
- archiveType: "zip"
17877
+ archiveType: "zip",
17878
+ librarySha256: "b95efb2113b603bbbf3f191061c5516a871ed546893c820e4f3b7b6c358dbf2a"
17706
17879
  },
17707
17880
  arm64: {
17708
17881
  assetName: `onnxruntime-win-arm64-${ORT_VERSION}`,
17709
17882
  libName: "onnxruntime.dll",
17710
- archiveType: "zip"
17883
+ archiveType: "zip",
17884
+ librarySha256: "724d81ac50b11bfaa01ab3ce01b99fb6734a4b762259a8be4395ca662ce99fe6"
17711
17885
  }
17712
17886
  }
17713
17887
  };
@@ -18197,6 +18371,9 @@ async function downloadOnnxRuntime(info, targetDir) {
18197
18371
  copyOnnxLibraries(info, extractedDir, stagedInstallDir, realFiles, symlinks);
18198
18372
  const libPath = join14(stagedInstallDir, info.libName);
18199
18373
  const libHash = sha256File2(libPath);
18374
+ if (libHash !== info.librarySha256) {
18375
+ throw new Error(`onnx runtime library hash mismatch at ${libPath}: sha256 ${libHash}, ` + `expected the pinned ${info.librarySha256} for ${info.assetName}`);
18376
+ }
18200
18377
  writeOnnxInstalledMeta(stagedInstallDir, ORT_VERSION, libHash, archiveSha256);
18201
18378
  publishOnnxRuntime(stagedInstallDir, targetDir);
18202
18379
  rmSync3(tmpDir, { recursive: true, force: true });
@@ -18539,12 +18716,13 @@ function normalizeZoomTargets(record) {
18539
18716
  throw new InvalidRequestError(`'targets[${index}].path' must be a non-empty string`);
18540
18717
  }
18541
18718
  const source = target;
18542
- const emptyTarget = source.symbol === "" && (hasOwn(source, "path") && source.path === "" || hasOwn(source, "filePath") && source.filePath === "");
18719
+ const blank = (value) => value === undefined || value === null || typeof value === "string" && !value.trim();
18720
+ const emptyTarget = blank(source.symbol) && blank(source.path) && blank(source.filePath);
18543
18721
  if (emptyTarget)
18544
18722
  return { ...source };
18545
18723
  const normalized = { ...source };
18546
18724
  try {
18547
- normalizeAliasPair(normalized, "path", "filePath", true);
18725
+ normalizeAliasPair(normalized, "path", "filePath", false);
18548
18726
  } catch (error) {
18549
18727
  if (error instanceof InvalidRequestError) {
18550
18728
  throw new InvalidRequestError(error.message.replace("'filePath'", `'targets[${index}].filePath'`).replace("'path'", `'targets[${index}].path'`));
@@ -23990,6 +24168,10 @@ var SubcConfigSchema = object({
23990
24168
  connection_file: string2().optional(),
23991
24169
  client_reaper: boolean2().optional()
23992
24170
  });
24171
+ var OpenCodeHostConfigSchema = object({
24172
+ server_url: string2().optional(),
24173
+ server_password_env: string2().optional()
24174
+ });
23993
24175
  var GhShimConfigSchema = object({
23994
24176
  binary_path: string2().trim().refine(isAbsolute7, "gh_shim.binary_path must be absolute").optional()
23995
24177
  });
@@ -24079,6 +24261,7 @@ var AftConfigFieldsSchema = object({
24079
24261
  auto_update: boolean2().optional(),
24080
24262
  bridge: BridgeConfigSchema.optional(),
24081
24263
  subc: SubcConfigSchema.optional(),
24264
+ opencode: OpenCodeHostConfigSchema.optional(),
24082
24265
  github: GithubConfigSchema.optional(),
24083
24266
  gh_shim: GhShimConfigSchema.optional(),
24084
24267
  git: GitConfigSchema.optional(),
@@ -24668,6 +24851,8 @@ function getStrippedTopLevelKeys(override) {
24668
24851
  stripped.push("sandbox.write_allow");
24669
24852
  if (override.subc !== undefined)
24670
24853
  stripped.push("subc");
24854
+ if (override.opencode !== undefined)
24855
+ stripped.push("opencode");
24671
24856
  if (override.github !== undefined)
24672
24857
  stripped.push("github");
24673
24858
  if (override.gh_shim !== undefined)
@@ -38527,6 +38712,14 @@ function formatDuration(completion) {
38527
38712
  }
38528
38713
 
38529
38714
  // src/shared/session-directory.ts
38715
+ import { Effect } from "effect";
38716
+ function isV2PluginContext(client) {
38717
+ if (!client || typeof client !== "object")
38718
+ return false;
38719
+ const candidate = client;
38720
+ return typeof candidate.location?.directory === "string" && typeof candidate.session?.get === "function";
38721
+ }
38722
+ var V2_SESSION_LOOKUP_TIMEOUT_MS = 3000;
38530
38723
  var CACHE_MAX_ENTRIES = 200;
38531
38724
  var cache = new Map;
38532
38725
  async function getSessionDirectory(client, sessionId, fallbackDirectory) {
@@ -38538,6 +38731,8 @@ async function getSessionDirectory(client, sessionId, fallbackDirectory) {
38538
38731
  cache.set(sessionId, cached);
38539
38732
  return cached.directory;
38540
38733
  }
38734
+ if (isV2PluginContext(client))
38735
+ return lookupV2SessionDirectory(client, sessionId);
38541
38736
  const c = client;
38542
38737
  const sessionApi = c?.session;
38543
38738
  if (!sessionApi || typeof sessionApi.get !== "function") {
@@ -38558,6 +38753,25 @@ async function getSessionDirectory(client, sessionId, fallbackDirectory) {
38558
38753
  setCache(sessionId, dir);
38559
38754
  return dir;
38560
38755
  }
38756
+ async function lookupV2SessionDirectory(context, sessionId) {
38757
+ let dir = null;
38758
+ let failure;
38759
+ try {
38760
+ const session = await Effect.runPromise(Effect.timeout(context.session.get({ sessionID: sessionId }), V2_SESSION_LOOKUP_TIMEOUT_MS));
38761
+ const directory = session?.location?.directory;
38762
+ if (typeof directory === "string" && directory.length > 0)
38763
+ dir = directory;
38764
+ else
38765
+ failure = "the session record has no location.directory";
38766
+ } catch (err) {
38767
+ failure = err instanceof Error ? err.message || err.name : String(err);
38768
+ }
38769
+ if (failure !== undefined) {
38770
+ sessionWarn(sessionId, `[aft-plugin] OpenCode 2 session lookup failed (${failure}); tool paths and bash use the Location directory ${context.location.directory} for this session`);
38771
+ }
38772
+ setCache(sessionId, dir);
38773
+ return dir;
38774
+ }
38561
38775
  function setCache(sessionId, directory) {
38562
38776
  if (cache.has(sessionId))
38563
38777
  cache.delete(sessionId);
@@ -38616,6 +38830,108 @@ async function verifySessionDirectory(client, sessionId) {
38616
38830
  return dir;
38617
38831
  }
38618
38832
 
38833
+ // src/shared/subagent-detect.ts
38834
+ import { Effect as Effect2 } from "effect";
38835
+ var CACHE_MAX_ENTRIES2 = 200;
38836
+ var LOOKUP_TIMEOUT_MS = 1000;
38837
+ var FAILED_LOOKUP_RETRY_MS = 30000;
38838
+ var cache2 = new Map;
38839
+ var inflight = new Map;
38840
+ var now = () => Date.now();
38841
+ async function resolveIsSubagent(client, sessionId, _fallbackDirectory) {
38842
+ if (!sessionId) {
38843
+ sessionLog(undefined, "[subagent-detect] no sessionId provided → primary");
38844
+ return false;
38845
+ }
38846
+ const cached = cache2.get(sessionId);
38847
+ if (cached && (cached.retryAfter === undefined || now() < cached.retryAfter)) {
38848
+ cache2.delete(sessionId);
38849
+ cache2.set(sessionId, cached);
38850
+ return cached.isSubagent;
38851
+ }
38852
+ const pending = inflight.get(sessionId);
38853
+ if (pending)
38854
+ return pending;
38855
+ const lookup = boundedLookup(client, sessionId).finally(() => {
38856
+ inflight.delete(sessionId);
38857
+ });
38858
+ inflight.set(sessionId, lookup);
38859
+ return lookup;
38860
+ }
38861
+ async function boundedLookup(client, sessionId) {
38862
+ let timer;
38863
+ const timedOut = new Promise((resolve) => {
38864
+ timer = setTimeout(() => resolve("timeout"), LOOKUP_TIMEOUT_MS);
38865
+ });
38866
+ let answer;
38867
+ try {
38868
+ answer = await Promise.race([lookupSession(client, sessionId), timedOut]);
38869
+ } finally {
38870
+ clearTimeout(timer);
38871
+ }
38872
+ if (typeof answer === "boolean") {
38873
+ setCache2(sessionId, answer);
38874
+ return answer;
38875
+ }
38876
+ if (answer === "timeout") {
38877
+ sessionWarn(sessionId, `[subagent-detect] session lookup did not answer within ${LOOKUP_TIMEOUT_MS}ms → primary for now`);
38878
+ }
38879
+ setCache2(sessionId, false, now() + FAILED_LOOKUP_RETRY_MS);
38880
+ return false;
38881
+ }
38882
+ async function lookupSession(client, sessionId) {
38883
+ if (isV2PluginContext2(client))
38884
+ return resolveV2(client, sessionId);
38885
+ const c = client;
38886
+ const sessionApi = c?.session;
38887
+ if (!sessionApi || typeof sessionApi.get !== "function") {
38888
+ sessionLog(sessionId, `[subagent-detect] client.session.get unavailable (client=${typeof client}, session=${typeof sessionApi}, get=${typeof sessionApi?.get}) → caching as primary`);
38889
+ return false;
38890
+ }
38891
+ sessionLog(sessionId, `[subagent-detect] cache miss, calling client.session.get(id=${sessionId})`);
38892
+ try {
38893
+ const result = await sessionApi.get({
38894
+ path: { id: sessionId }
38895
+ });
38896
+ const session = result?.data ?? result;
38897
+ const parentIdRaw = session?.parentID;
38898
+ const isSubagent = session !== undefined && typeof session.parentID === "string" && session.parentID.length > 0;
38899
+ sessionLog(sessionId, `[subagent-detect] SDK returned session=${session !== undefined ? "present" : "undefined"}, parentID=${JSON.stringify(parentIdRaw)} → isSubagent=${isSubagent}`);
38900
+ return isSubagent;
38901
+ } catch (err) {
38902
+ sessionWarn(sessionId, `[subagent-detect] SDK lookup failed: ${err instanceof Error ? err.message : String(err)}`);
38903
+ return "failed";
38904
+ }
38905
+ }
38906
+ function isV2PluginContext2(client) {
38907
+ if (!client || typeof client !== "object")
38908
+ return false;
38909
+ const candidate = client;
38910
+ return typeof candidate.location?.directory === "string" && typeof candidate.session?.get === "function";
38911
+ }
38912
+ async function resolveV2(context, sessionId) {
38913
+ let session;
38914
+ try {
38915
+ session = await Effect2.runPromise(context.session.get({ sessionID: sessionId }));
38916
+ } catch (err) {
38917
+ sessionWarn(sessionId, `[subagent-detect] OpenCode 2 session lookup failed: ${err instanceof Error ? err.message : String(err)}`);
38918
+ return "failed";
38919
+ }
38920
+ const isSubagent = typeof session?.parentID === "string" && session.parentID.length > 0;
38921
+ sessionLog(sessionId, `[subagent-detect] OpenCode 2 session parentID=${JSON.stringify(session?.parentID)} → isSubagent=${isSubagent}`);
38922
+ return isSubagent;
38923
+ }
38924
+ function setCache2(sessionId, isSubagent, retryAfter) {
38925
+ if (cache2.has(sessionId))
38926
+ cache2.delete(sessionId);
38927
+ cache2.set(sessionId, retryAfter === undefined ? { isSubagent } : { isSubagent, retryAfter });
38928
+ if (cache2.size > CACHE_MAX_ENTRIES2) {
38929
+ const oldest = cache2.keys().next().value;
38930
+ if (oldest !== undefined)
38931
+ cache2.delete(oldest);
38932
+ }
38933
+ }
38934
+
38619
38935
  // src/tool-perf.ts
38620
38936
  import { AsyncLocalStorage } from "node:async_hooks";
38621
38937
  var perfStore = new AsyncLocalStorage;
@@ -38691,7 +39007,7 @@ function projectRootFor(runtime) {
38691
39007
  if (typeof cached === "string" && cached.length > 0) {
38692
39008
  return canonicalizeDirectory(cached);
38693
39009
  }
38694
- const raw = runtime.worktree ?? runtime.directory;
39010
+ const raw = runtime.directoryIsSessionRoot ? runtime.directory : runtime.worktree ?? runtime.directory;
38695
39011
  return canonicalizeDirectory(raw);
38696
39012
  }
38697
39013
  async function resolveProjectRoot(ctx, runtime) {
@@ -38727,6 +39043,11 @@ function bridgeFor(ctx, runtime) {
38727
39043
  }
38728
39044
  return ctx.pool.getBridge(projectRoot);
38729
39045
  }
39046
+ async function isWorkerRuntime(ctx, runtime) {
39047
+ if (!runtime.sessionID)
39048
+ return false;
39049
+ return resolveIsSubagent(ctx.client, runtime.sessionID, runtime.directory);
39050
+ }
38730
39051
  async function callBridge(ctx, runtime, command, params = {}, options) {
38731
39052
  if (runtime.sessionID && getSessionDirectoryCached(runtime.sessionID) === undefined) {
38732
39053
  await getSessionDirectory(ctx.client, runtime.sessionID, runtime.directory);
@@ -38735,6 +39056,9 @@ async function callBridge(ctx, runtime, command, params = {}, options) {
38735
39056
  if (runtime.sessionID) {
38736
39057
  merged.session_id = runtime.sessionID;
38737
39058
  }
39059
+ if (await isWorkerRuntime(ctx, runtime)) {
39060
+ merged[WORKER_SESSION_FIELD] = true;
39061
+ }
38738
39062
  const timeoutMs = timeoutForCommand(command);
38739
39063
  const sendOptions = {
38740
39064
  ...timeoutMs !== undefined ? { timeoutMs } : {},
@@ -38761,10 +39085,12 @@ async function callToolCall(ctx, runtime, name, rawArgs = {}, options) {
38761
39085
  await getSessionDirectory(ctx.client, runtime.sessionID, runtime.directory);
38762
39086
  }
38763
39087
  const timeoutMs = timeoutForCommand(name);
39088
+ const workerSession = await isWorkerRuntime(ctx, runtime);
38764
39089
  const sendOptions = {
38765
39090
  ...timeoutMs !== undefined ? { timeoutMs } : {},
38766
39091
  configureWarningClient: ctx.client,
38767
39092
  ...runtime.effectAbort ? { abortSignal: runtime.effectAbort } : {},
39093
+ ...workerSession ? { workerSession: true } : {},
38768
39094
  ...options
38769
39095
  };
38770
39096
  markBridgeStart();
@@ -39315,7 +39641,8 @@ var NPM_LSP_TABLE = [
39315
39641
  npm: "typescript",
39316
39642
  binary: "tsserver",
39317
39643
  extensions: ["ts", "tsx", "js", "jsx", "mjs", "cjs", "mts", "cts"],
39318
- rootMarkers: ["tsconfig.json", "jsconfig.json", "package.json"]
39644
+ rootMarkers: ["tsconfig.json", "jsconfig.json", "package.json"],
39645
+ supportedMajor: 5
39319
39646
  },
39320
39647
  {
39321
39648
  id: "python",
@@ -39389,6 +39716,15 @@ var NPM_LSP_TABLE = [
39389
39716
  rootMarkers: ["biome.json", "biome.jsonc"]
39390
39717
  }
39391
39718
  ];
39719
+ function majorVersionOf(version) {
39720
+ const match = /^v?(\d+)(?:\.|$)/.exec(version.trim());
39721
+ return match ? Number(match[1]) : null;
39722
+ }
39723
+ function isVersionSupported(spec, version) {
39724
+ if (spec.supportedMajor === undefined)
39725
+ return true;
39726
+ return majorVersionOf(version) === spec.supportedMajor;
39727
+ }
39392
39728
 
39393
39729
  // src/lsp-project-relevance.ts
39394
39730
  import { existsSync as existsSync14, readdirSync as readdirSync4, readFileSync as readFileSync12 } from "node:fs";
@@ -39485,10 +39821,11 @@ function extensionOf(fileName) {
39485
39821
 
39486
39822
  // src/lsp-registry-probe.ts
39487
39823
  var NPM_REGISTRY_BASE = "https://registry.npmjs.org";
39488
- function pickEligibleVersion(response, graceDays, now = Date.now()) {
39824
+ function pickEligibleVersion(response, graceDays, now = Date.now(), accept = () => true) {
39489
39825
  const times = response.time || {};
39490
39826
  const cutoff = now - graceDays * 24 * 60 * 60 * 1000;
39491
39827
  const candidates = [];
39828
+ let releaseCount = 0;
39492
39829
  for (const [version, publishedAt] of Object.entries(times)) {
39493
39830
  if (version === "created" || version === "modified")
39494
39831
  continue;
@@ -39499,6 +39836,9 @@ function pickEligibleVersion(response, graceDays, now = Date.now()) {
39499
39836
  const ts = Date.parse(publishedAt);
39500
39837
  if (Number.isNaN(ts))
39501
39838
  continue;
39839
+ releaseCount += 1;
39840
+ if (!accept(version))
39841
+ continue;
39502
39842
  candidates.push({ version, publishedAt, ts });
39503
39843
  }
39504
39844
  candidates.sort((a, b) => b.ts - a.ts);
@@ -39507,10 +39847,11 @@ function pickEligibleVersion(response, graceDays, now = Date.now()) {
39507
39847
  return {
39508
39848
  version: eligible[0]?.version ?? null,
39509
39849
  blockedByGrace,
39850
+ outsideSupportedRange: releaseCount > 0 && candidates.length === 0,
39510
39851
  eligible: eligible.map(({ version, publishedAt }) => ({ version, publishedAt }))
39511
39852
  };
39512
39853
  }
39513
- async function probeRegistry(npmPackage, graceDays, fetchImpl = fetch) {
39854
+ async function probeRegistry(npmPackage, graceDays, fetchImpl = fetch, accept) {
39514
39855
  const encoded = encodeURIComponent(npmPackage).replace(/^%40/, "@");
39515
39856
  const url = `${NPM_REGISTRY_BASE}/${encoded}`;
39516
39857
  try {
@@ -39523,7 +39864,7 @@ async function probeRegistry(npmPackage, graceDays, fetchImpl = fetch) {
39523
39864
  return null;
39524
39865
  }
39525
39866
  const json = await res.json();
39526
- return pickEligibleVersion(json, graceDays);
39867
+ return pickEligibleVersion(json, graceDays, Date.now(), accept);
39527
39868
  } catch (err) {
39528
39869
  warn2(`[lsp] registry probe failed for ${npmPackage}: ${err}`);
39529
39870
  return null;
@@ -39574,11 +39915,11 @@ async function resolveTargetVersion(spec, config, fetchImpl = fetch) {
39574
39915
  }
39575
39916
  const cached = readVersionCheck(spec.npm);
39576
39917
  const weeklyMs = config.graceDays * 24 * 60 * 60 * 1000;
39577
- const cachedSafe = isSafeVersion(cached?.latest_eligible ?? null);
39918
+ const cachedSafe = isSafeVersion(cached?.latest_eligible ?? null) && isVersionSupported(spec, cached?.latest_eligible);
39578
39919
  if (cached && !shouldRecheckVersion(cached, weeklyMs) && cachedSafe) {
39579
39920
  return { version: cached.latest_eligible, pinned: false, probe: null };
39580
39921
  }
39581
- const probe = await probeRegistry(spec.npm, config.graceDays, fetchImpl);
39922
+ const probe = await probeRegistry(spec.npm, config.graceDays, fetchImpl, (candidate) => isVersionSupported(spec, candidate));
39582
39923
  if (!probe) {
39583
39924
  return {
39584
39925
  version: cachedSafe ? cached?.latest_eligible ?? null : null,
@@ -39713,11 +40054,16 @@ async function ensureServerInstalled(spec, config, fetchImpl, signal) {
39713
40054
  const { version, probe } = await resolveTargetVersion(spec, config, fetchImpl);
39714
40055
  if (!version) {
39715
40056
  const installed = isInstalled(spec.npm, spec.binary);
39716
- if (installed) {
40057
+ if (installed && installedVersionAllowed(spec, config)) {
39717
40058
  warn2(`[lsp] no eligible version of ${spec.npm} (grace=${config.graceDays}d); keeping existing install`);
39718
40059
  return { started: false, reason: "kept existing install" };
39719
40060
  }
39720
- const blocked = probe?.blockedByGrace ? `all versions are within ${config.graceDays}-day grace window` : "registry probe failed";
40061
+ 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";
40062
+ if (installed) {
40063
+ 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}`;
40064
+ warn2(`[lsp] ${reason}`);
40065
+ return { started: false, reason };
40066
+ }
39721
40067
  warn2(`[lsp] skipping ${spec.npm}: ${blocked}`);
39722
40068
  return { started: false, reason: blocked };
39723
40069
  }
@@ -39842,6 +40188,15 @@ function validateCachedNpmInstall(spec) {
39842
40188
  }
39843
40189
  return true;
39844
40190
  }
40191
+ function installedVersionAllowed(spec, config) {
40192
+ const installed = readInstalledMeta(spec.npm)?.version;
40193
+ if (!installed)
40194
+ return spec.supportedMajor === undefined;
40195
+ return isVersionSupported(spec, installed) || config.versions[spec.npm] === installed;
40196
+ }
40197
+ function cachedInstallUsable(spec, config) {
40198
+ return isInstalled(spec.npm, spec.binary) && validateCachedNpmInstall(spec) && installedVersionAllowed(spec, config);
40199
+ }
39845
40200
  function runAutoInstall(projectRoot, config, fetchImpl = fetch) {
39846
40201
  const cachedBinDirs = [];
39847
40202
  const skipped = [];
@@ -39854,7 +40209,7 @@ function runAutoInstall(projectRoot, config, fetchImpl = fetch) {
39854
40209
  return projectExtensions;
39855
40210
  };
39856
40211
  for (const spec of NPM_LSP_TABLE) {
39857
- if (isInstalled(spec.npm, spec.binary) && validateCachedNpmInstall(spec)) {
40212
+ if (cachedInstallUsable(spec, config)) {
39858
40213
  cachedBinDirs.push(lspBinDir(spec.npm));
39859
40214
  }
39860
40215
  if (config.disabled.has(spec.id) || spec.id === "typescript-sdk" && config.disabled.has("typescript")) {
@@ -39894,7 +40249,7 @@ function runAutoInstall(projectRoot, config, fetchImpl = fetch) {
39894
40249
  },
39895
40250
  skipped,
39896
40251
  installsComplete: Promise.all(installPromises).then(() => {}),
39897
- getCachedBinDirs: () => NPM_LSP_TABLE.filter((spec) => isInstalled(spec.npm, spec.binary) && validateCachedNpmInstall(spec)).map((spec) => lspBinDir(spec.npm))
40252
+ getCachedBinDirs: () => NPM_LSP_TABLE.filter((spec) => cachedInstallUsable(spec, config)).map((spec) => lspBinDir(spec.npm))
39898
40253
  };
39899
40254
  }
39900
40255
  async function pushLspPathsAfterAutoInstall(pool, projectRoot, cachedBinDirs) {
@@ -41445,391 +41800,8 @@ function astTools(ctx) {
41445
41800
  };
41446
41801
  }
41447
41802
 
41448
- // src/tools/conflicts.ts
41449
- var z3 = tool.schema;
41450
- function conflictTools(ctx) {
41451
- return {
41452
- aft_conflicts: {
41453
- description: "Show all git merge conflicts across the repository — returns line-numbered conflict regions with context for every conflicted file in a single call. Conflicts are discovered from the git repository's top level. By default it inspects the session's project repository; pass `path` to inspect a different repository or git worktree (e.g. where a rebase/merge is running).",
41454
- args: {
41455
- path: z3.string().describe("Optional path inside the git repository or worktree to inspect (absolute or relative to project root). Conflicts are discovered from that repository's top level. Defaults to the session project root.").optional()
41456
- },
41457
- execute: async (args, context) => {
41458
- const rawArgs = {};
41459
- if (!isEmptyParam(args?.path)) {
41460
- const expanded = expandTilde2(String(args.path));
41461
- const projectRoot = await resolveProjectRoot(ctx, context);
41462
- const resolved = resolvePathFromProjectRoot(projectRoot, expanded);
41463
- const denied = await assertExternalDirectoryPermission(ctx, context, resolved, {
41464
- kind: "directory"
41465
- });
41466
- if (denied)
41467
- return permissionDeniedResponse(denied);
41468
- rawArgs.path = resolved;
41469
- }
41470
- const response = await callToolCall(ctx, context, "conflicts", rawArgs);
41471
- if (response.success === false) {
41472
- throw new Error(response.message || "git_conflicts failed");
41473
- }
41474
- return response.text;
41475
- }
41476
- }
41477
- };
41478
- }
41479
-
41480
- // src/tools/definitions/v2.ts
41481
- import { Effect } from "effect";
41482
- var V2_TOOL_NAME = /^[A-Za-z][A-Za-z0-9_-]{0,63}$/;
41483
- var V2_PATH_HEADER_TOOLS = new Set(["read", "write", "edit"]);
41484
- var V2_BASH_TOOLS = new Set(["bash"]);
41485
- var V2_SHELL_PERMISSION = "shell";
41486
- var ROOT_COMBINATORS = ["anyOf", "oneOf", "allOf"];
41487
- function failure(error) {
41488
- if (error instanceof Error)
41489
- return error;
41490
- const detail = (() => {
41491
- try {
41492
- return JSON.stringify(error) ?? String(error);
41493
- } catch {
41494
- return String(error);
41495
- }
41496
- })();
41497
- return new Error(`V2 tool execution rejected with a non-Error value: ${detail}`);
41498
- }
41499
- function isPlainObject3(value) {
41500
- if (typeof value !== "object" || value === null)
41501
- return false;
41502
- const prototype = Object.getPrototypeOf(value);
41503
- return prototype === Object.prototype || prototype === null;
41504
- }
41505
- function jsonSafeMetadata(value) {
41506
- if (Array.isArray(value)) {
41507
- return value.map((entry) => entry === undefined ? null : jsonSafeMetadata(entry));
41508
- }
41509
- if (!isPlainObject3(value))
41510
- return value;
41511
- const cleaned = {};
41512
- for (const [key, entry] of Object.entries(value)) {
41513
- if (entry === undefined)
41514
- continue;
41515
- cleaned[key] = jsonSafeMetadata(entry);
41516
- }
41517
- return cleaned;
41518
- }
41519
- function resultContent(result) {
41520
- if (typeof result === "string")
41521
- return { content: result };
41522
- const attachments = (result.attachments ?? []).map((attachment) => ({
41523
- type: "file",
41524
- uri: attachment.url,
41525
- mime: attachment.mime,
41526
- ...attachment.filename ? { name: attachment.filename } : {}
41527
- }));
41528
- const content = attachments.length ? [{ type: "text", text: result.output }, ...attachments] : result.output;
41529
- const metadata = jsonSafeMetadata({
41530
- ...result.metadata ?? {},
41531
- ...result.title ? { title: result.title } : {}
41532
- });
41533
- return {
41534
- content,
41535
- ...Object.keys(metadata).length ? { metadata } : {}
41536
- };
41537
- }
41538
- function failureEnvelopeMessage(result) {
41539
- const output = typeof result === "string" ? result : result.output;
41540
- if (typeof output !== "string")
41541
- return;
41542
- const trimmed = output.trim();
41543
- if (!trimmed.startsWith("{") || !trimmed.includes('"success"'))
41544
- return;
41545
- let parsed;
41546
- try {
41547
- parsed = JSON.parse(trimmed);
41548
- } catch {
41549
- return;
41550
- }
41551
- if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
41552
- return;
41553
- const envelope = parsed;
41554
- if (envelope.success !== false)
41555
- return;
41556
- for (const field of [envelope.message, envelope.error, envelope.code]) {
41557
- if (typeof field === "string" && field.length > 0)
41558
- return field;
41559
- }
41560
- return "The tool call failed.";
41561
- }
41562
- function bareToolName3(name) {
41563
- return name.startsWith("aft_") ? name.slice(4) : name;
41564
- }
41565
- function projectArguments(name, definition) {
41566
- if (!V2_PATH_HEADER_TOOLS.has(bareToolName3(name)) || !("filePath" in definition.args)) {
41567
- return { ...definition.args };
41568
- }
41569
- const { filePath, ...rest } = definition.args;
41570
- return { path: filePath, ...rest };
41571
- }
41572
- function executionArguments(name, input) {
41573
- if (!V2_PATH_HEADER_TOOLS.has(bareToolName3(name)) || !("path" in input))
41574
- return input;
41575
- const { path, ...rest } = input;
41576
- return { filePath: path, ...rest };
41577
- }
41578
- function hostPermission(name) {
41579
- const bare = bareToolName3(name);
41580
- if (new Set(["read", "glob", "grep"]).has(bare))
41581
- return bare;
41582
- if (V2_BASH_TOOLS.has(name))
41583
- return V2_SHELL_PERMISSION;
41584
- if (new Set([
41585
- "write",
41586
- "edit",
41587
- "apply_patch",
41588
- "delete",
41589
- "move",
41590
- "ast_grep_replace",
41591
- "import",
41592
- "safety"
41593
- ]).has(bare)) {
41594
- return "edit";
41595
- }
41596
- return;
41597
- }
41598
- function assertV2Contract(name, input) {
41599
- if (!V2_TOOL_NAME.test(name)) {
41600
- throw new Error(`Invalid V2 tool name ${JSON.stringify(name)}; expected ${V2_TOOL_NAME.source}`);
41601
- }
41602
- const schema = tool.schema.toJSONSchema(input, { io: "input" });
41603
- if (schema.type !== "object") {
41604
- throw new Error(`V2 tool ${name} must have an object-rooted input schema`);
41605
- }
41606
- for (const keyword of ROOT_COMBINATORS) {
41607
- if (keyword in schema) {
41608
- throw new Error(`V2 tool ${name} input schema cannot use root ${keyword}`);
41609
- }
41610
- }
41611
- }
41612
- function v2PermissionRequest(name, request) {
41613
- if (!V2_BASH_TOOLS.has(name) || request.permission !== "bash")
41614
- return request;
41615
- return { ...request, permission: V2_SHELL_PERMISSION };
41616
- }
41617
- function runtimeFor(name, location, context, signal, consumers) {
41618
- const directory = location.directory;
41619
- const worktree = location.project?.canonical ?? location.project?.directory ?? directory;
41620
- return {
41621
- sessionID: context.sessionID,
41622
- messageID: context.messageID,
41623
- agent: context.agent,
41624
- directory,
41625
- worktree,
41626
- abort: signal,
41627
- effectAbort: signal,
41628
- metadata: (update) => {
41629
- Effect.runPromise(context.progress(jsonSafeMetadata({
41630
- ...update.metadata ?? {},
41631
- ...update.title ? { title: update.title } : {}
41632
- })));
41633
- },
41634
- ask: (asked) => {
41635
- const request = v2PermissionRequest(name, asked);
41636
- if (consumers.requestPermission)
41637
- return consumers.requestPermission(request, context);
41638
- return Promise.reject(new Error(`The "${request.permission}" operation was refused because this AFT runtime has no permission evaluator bound, so the host's permission rules for it could not be consulted.`));
41639
- },
41640
- progress: context.progress
41641
- };
41642
- }
41643
- function projectV2Tool(name, definition, location, consumers = {}) {
41644
- const input = tool.schema.object(projectArguments(name, definition));
41645
- assertV2Contract(name, input);
41646
- const sharedOptions = definition.options;
41647
- const permission = hostPermission(name);
41648
- return {
41649
- name,
41650
- description: definition.description,
41651
- input,
41652
- options: {
41653
- ...sharedOptions,
41654
- ...permission ? { permission } : {},
41655
- codemode: false
41656
- },
41657
- execute: (rawInput, context) => Effect.tryPromise({
41658
- try: async (signal) => {
41659
- const runtime = runtimeFor(name, location, context, signal, consumers);
41660
- const input = executionArguments(name, rawInput);
41661
- const result = consumers.executeBash && V2_BASH_TOOLS.has(name) ? await consumers.executeBash({
41662
- name,
41663
- input,
41664
- context: runtime,
41665
- definition
41666
- }) : await definition.execute(input, runtime);
41667
- const refusal = failureEnvelopeMessage(result);
41668
- if (refusal)
41669
- throw new Error(refusal);
41670
- return resultContent(result);
41671
- },
41672
- catch: failure
41673
- })
41674
- };
41675
- }
41676
-
41677
- // src/tools/hoisted.ts
41678
- import { stat as stat2 } from "node:fs/promises";
41679
- import * as path4 from "node:path";
41680
-
41681
- // src/shared/read-vision.ts
41682
- var VISION_HOST_TIMEOUT_MS = 1500;
41683
- var states = new WeakMap;
41684
- function stateFor2(client) {
41685
- let state = states.get(client);
41686
- if (!state) {
41687
- state = { models: new Map, history: new Map, capabilities: new Map };
41688
- states.set(client, state);
41689
- }
41690
- return state;
41691
- }
41692
- function rememberReadModel(client, sessionID, model) {
41693
- stateFor2(client).models.set(sessionID, model);
41694
- }
41695
- async function bounded(call) {
41696
- let timer;
41697
- try {
41698
- return await Promise.race([
41699
- Promise.resolve().then(call),
41700
- new Promise((resolve) => {
41701
- timer = setTimeout(() => resolve(undefined), VISION_HOST_TIMEOUT_MS);
41702
- })
41703
- ]);
41704
- } catch {
41705
- return;
41706
- } finally {
41707
- clearTimeout(timer);
41708
- }
41709
- }
41710
- function record3(value) {
41711
- return typeof value === "object" && value !== null && !Array.isArray(value) ? value : undefined;
41712
- }
41713
- async function currentSessionVisionCapability(client, sessionID) {
41714
- if (!sessionID)
41715
- return;
41716
- const state = stateFor2(client);
41717
- let model = state.models.get(sessionID);
41718
- if (!model) {
41719
- let pending = state.history.get(sessionID);
41720
- if (!pending) {
41721
- pending = bounded(() => resolvePromptContext(client, sessionID)).then((context) => context?.model);
41722
- state.history.set(sessionID, pending);
41723
- pending.finally(() => state.history.delete(sessionID));
41724
- }
41725
- const historical = await pending;
41726
- model = state.models.get(sessionID) ?? historical;
41727
- }
41728
- if (!model)
41729
- return;
41730
- const currentModel = model;
41731
- const key = JSON.stringify([model.providerID, model.modelID]);
41732
- let capability = state.capabilities.get(key);
41733
- if (!capability) {
41734
- capability = (async () => {
41735
- const api = client.provider;
41736
- if (!api?.list)
41737
- return;
41738
- const list = api.list.bind(api);
41739
- const listed = record3(await bounded(list));
41740
- const catalog = record3(listed?.data) ?? listed;
41741
- const providers = catalog?.all ?? catalog?.providers;
41742
- if (!Array.isArray(providers))
41743
- return;
41744
- const provider = providers.map(record3).find((entry) => entry?.id === currentModel.providerID);
41745
- const models = provider?.models;
41746
- const entry = record3(Array.isArray(models) ? models.find((entry) => record3(entry)?.id === currentModel.modelID) : record3(models)?.[currentModel.modelID]);
41747
- const inputs = record3(entry?.modalities)?.input;
41748
- return Array.isArray(inputs) ? inputs.includes("image") : typeof entry?.attachment === "boolean" ? entry.attachment : undefined;
41749
- })();
41750
- state.capabilities.set(key, capability);
41751
- capability.then((value) => {
41752
- if (value === undefined)
41753
- state.capabilities.delete(key);
41754
- });
41755
- }
41756
- return capability;
41757
- }
41758
-
41759
- // src/shared/subagent-detect.ts
41760
- import { Effect as Effect2 } from "effect";
41761
- var CACHE_MAX_ENTRIES2 = 200;
41762
- var cache2 = new Map;
41763
- async function resolveIsSubagent(client, sessionId, _fallbackDirectory) {
41764
- if (!sessionId) {
41765
- sessionLog(undefined, "[subagent-detect] no sessionId provided → primary");
41766
- return false;
41767
- }
41768
- const cached = cache2.get(sessionId);
41769
- if (cached) {
41770
- cache2.delete(sessionId);
41771
- cache2.set(sessionId, cached);
41772
- return cached.isSubagent;
41773
- }
41774
- if (isV2PluginContext(client))
41775
- return resolveV2(client, sessionId);
41776
- const c = client;
41777
- const sessionApi = c?.session;
41778
- if (!sessionApi || typeof sessionApi.get !== "function") {
41779
- sessionLog(sessionId, `[subagent-detect] client.session.get unavailable (client=${typeof client}, session=${typeof sessionApi}, get=${typeof sessionApi?.get}) → caching as primary`);
41780
- setCache2(sessionId, false);
41781
- return false;
41782
- }
41783
- sessionLog(sessionId, `[subagent-detect] cache miss, calling client.session.get(id=${sessionId})`);
41784
- let isSubagent = false;
41785
- let parentIdRaw;
41786
- try {
41787
- const result = await sessionApi.get({
41788
- path: { id: sessionId }
41789
- });
41790
- const session = result?.data ?? result;
41791
- parentIdRaw = session?.parentID;
41792
- isSubagent = session !== undefined && typeof session.parentID === "string" && session.parentID.length > 0;
41793
- sessionLog(sessionId, `[subagent-detect] SDK returned session=${session !== undefined ? "present" : "undefined"}, parentID=${JSON.stringify(parentIdRaw)} → isSubagent=${isSubagent}`);
41794
- } catch (err) {
41795
- sessionWarn(sessionId, `[subagent-detect] SDK lookup failed: ${err instanceof Error ? err.message : String(err)}`);
41796
- return false;
41797
- }
41798
- setCache2(sessionId, isSubagent);
41799
- return isSubagent;
41800
- }
41801
- function isV2PluginContext(client) {
41802
- if (!client || typeof client !== "object")
41803
- return false;
41804
- const candidate = client;
41805
- return typeof candidate.location?.directory === "string" && typeof candidate.session?.get === "function";
41806
- }
41807
- async function resolveV2(context, sessionId) {
41808
- let session;
41809
- try {
41810
- session = await Effect2.runPromise(context.session.get({ sessionID: sessionId }));
41811
- } catch (err) {
41812
- sessionWarn(sessionId, `[subagent-detect] OpenCode 2 session lookup failed: ${err instanceof Error ? err.message : String(err)}`);
41813
- return false;
41814
- }
41815
- const isSubagent = typeof session?.parentID === "string" && session.parentID.length > 0;
41816
- sessionLog(sessionId, `[subagent-detect] OpenCode 2 session parentID=${JSON.stringify(session?.parentID)} → isSubagent=${isSubagent}`);
41817
- setCache2(sessionId, isSubagent);
41818
- return isSubagent;
41819
- }
41820
- function setCache2(sessionId, isSubagent) {
41821
- if (cache2.has(sessionId))
41822
- cache2.delete(sessionId);
41823
- cache2.set(sessionId, { isSubagent, recordedAt: Date.now() });
41824
- if (cache2.size > CACHE_MAX_ENTRIES2) {
41825
- const oldest = cache2.keys().next().value;
41826
- if (oldest !== undefined)
41827
- cache2.delete(oldest);
41828
- }
41829
- }
41830
-
41831
41803
  // src/tools/bash.ts
41832
- var z4 = tool.schema;
41804
+ var z3 = tool.schema;
41833
41805
  var METADATA_PREVIEW_LIMIT = 30 * 1024;
41834
41806
  var DEFAULT_HARD_TIMEOUT_MS = 30 * 60 * 1000;
41835
41807
  var BASH_TRANSPORT_MARGIN_MS = 1e4;
@@ -41845,7 +41817,9 @@ function resolveForegroundWaitMs(configured) {
41845
41817
  }
41846
41818
  return configured;
41847
41819
  }
41848
- function orchestratedTransportTimeoutMs(blockToCompletion, wait, effectiveTimeout, foregroundWaitMs) {
41820
+ function orchestratedTransportTimeoutMs(blockToCompletion, wait, effectiveTimeout, foregroundWaitMs, noHardKill = false) {
41821
+ if (noHardKill)
41822
+ return LONGEST_TIMER_DELAY_MS;
41849
41823
  const waitBudget = blockToCompletion || wait ? effectiveTimeout ?? DEFAULT_HARD_TIMEOUT_MS : foregroundWaitMs;
41850
41824
  return waitBudget + BASH_TRANSPORT_MARGIN_MS;
41851
41825
  }
@@ -41922,13 +41896,38 @@ function listenForForegroundAbort(ctx, runtime, enabled) {
41922
41896
  function userMessageDetachDescription(detachOnUserMessage) {
41923
41897
  return detachOnUserMessage ? "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 and the rest of the message is preserved; 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 and the rest of the message is preserved; a token-only message becomes `(requested background detach)`.";
41924
41898
  }
41925
- function backgroundWaitDescription(watchToolRegistered) {
41926
- return watchToolRegistered ? "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." : "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.";
41899
+ function bashCompanionRegistered(config, companion) {
41900
+ return resolveBashConfig(config).background && toolEnabled(config, companion);
41927
41901
  }
41928
- function bashToolDescription(aftSearchRegistered, compressionOn, backgroundOn, detachOnUserMessage = true, zoomEnabled = true, watchToolRegistered = true) {
41929
- const searchSteer = aftSearchRegistered ? `use aft_search (concepts, identifiers, regex, literals), read, aft_outline${zoomEnabled ? ", or aft_zoom" : ""} instead` : `use the grep tool, read, aft_outline${zoomEnabled ? ", or aft_zoom" : ""} instead`;
41902
+ function backgroundWaitDescription(watchToolRegistered, statusRegistered) {
41903
+ if (watchToolRegistered) {
41904
+ const noPolling = statusRegistered ? ", and never loop bash_status to wait" : "";
41905
+ 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}.`;
41906
+ }
41907
+ return statusRegistered ? "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.";
41908
+ }
41909
+ function ptyDriveClause(statusRegistered, writeRegistered) {
41910
+ if (statusRegistered && writeRegistered) {
41911
+ return ', and is driven with bash_status({ outputMode: "screen" }) plus bash_write';
41912
+ }
41913
+ if (statusRegistered)
41914
+ return ', and its screen is read with bash_status({ outputMode: "screen" })';
41915
+ if (writeRegistered)
41916
+ return ", and is driven with bash_write";
41917
+ return "";
41918
+ }
41919
+ function bashToolDescription(aftSearchRegistered, compressionOn, backgroundOn, detachOnUserMessage = true, zoomEnabled = true, watchToolRegistered = true, surface = {}) {
41920
+ const outline = surface.outline !== false;
41921
+ const status = surface.status !== false;
41922
+ const write = surface.write !== false;
41923
+ const steerTools = [
41924
+ aftSearchRegistered ? "aft_search (concepts, identifiers, regex, literals)" : "the grep tool",
41925
+ "read",
41926
+ ...outline ? ["aft_outline"] : []
41927
+ ].join(", ");
41928
+ const searchSteer = `use ${steerTools}${zoomEnabled ? ", or aft_zoom" : ""} instead`;
41930
41929
  const compression = compressionOn ? " 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. Pipeline-failure notes cover single top-level pipelines only; multi-statement commands (`a; b | c; d`) are not instrumented, so masked failures inside them still need explicit exit-code checks." : "";
41931
- const tasks = backgroundOn ? ` Commands run in the foreground and return inline; wait: true blocks until a long command finishes instead of auto-promoting; ${userMessageDetachDescription(detachOnUserMessage)} 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; ${backgroundWaitDescription(watchToolRegistered)} 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({ outputMode: "screen" }) plus bash_write.` : " Commands run in the foreground to completion; timeout is the hard kill cap (default 30 minutes).";
41930
+ const tasks = backgroundOn ? ` Commands run in the foreground and return inline; wait: true blocks until a long command finishes instead of auto-promoting; ${userMessageDetachDescription(detachOnUserMessage)} 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; ${backgroundWaitDescription(watchToolRegistered, status)} 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(status, write)}.` : " Commands run in the foreground to completion; timeout is the hard kill cap (default 30 minutes).";
41932
41931
  return `Execute shell commands.${compression}${tasks}
41933
41932
 
41934
41933
  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.`;
@@ -42005,30 +42004,63 @@ async function withPermissionLoop(ctx, runtime, params, bridgeCall, options) {
42005
42004
  }
42006
42005
  throw new Error("bash permission retry failed: too many rounds");
42007
42006
  }
42007
+ function ptyDriveParamSentence(statusRegistered, writeRegistered) {
42008
+ const writeInput = 'its input accepts either a string OR an array like [ "iHello", { key: "esc" }, ":wq", { key: "enter" } ] for atomic text+key sequences.';
42009
+ if (statusRegistered && writeRegistered) {
42010
+ return ` Inspect with bash_status({ taskId, outputMode: "screen" }) and drive interactively with bash_write — ${writeInput}`;
42011
+ }
42012
+ if (statusRegistered)
42013
+ return ' Inspect with bash_status({ taskId, outputMode: "screen" }).';
42014
+ if (writeRegistered)
42015
+ return ` Drive interactively with bash_write — ${writeInput}`;
42016
+ return "";
42017
+ }
42018
+ function nativeSandboxEnabled(config) {
42019
+ return config.sandbox?.enabled === true;
42020
+ }
42008
42021
  function createBashTool(ctx, aftSearchRegisteredOverride) {
42009
42022
  const initialBashCfg = resolveBashConfig(ctx.config);
42023
+ const statusRegistered = bashCompanionRegistered(ctx.config, "bash_status");
42024
+ const writeRegistered = bashCompanionRegistered(ctx.config, "bash_write");
42025
+ const taskControls = [
42026
+ ...statusRegistered ? ["bash_status"] : [],
42027
+ ...bashCompanionRegistered(ctx.config, "bash_kill") ? ["bash_kill"] : []
42028
+ ];
42029
+ const waitArg = initialBashCfg.background ? {
42030
+ wait: z3.boolean().optional().describe(`When true, run in the foreground without auto-promoting and wait until the command finishes or reaches its timeout; ${userMessageDetachDescription(initialBashCfg.detach_on_user_message)} Use only when you know the result is required before doing anything else.`)
42031
+ } : {};
42032
+ const sandboxArg = nativeSandboxEnabled(ctx.config) ? {
42033
+ sandbox: z3.literal("host").optional().describe("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.")
42034
+ } : {};
42035
+ const compressedArg = initialBashCfg.compress ? {
42036
+ compressed: z3.boolean().optional().describe("When true or omitted, return compressed output with noisy terminal control sequences reduced. Set to false for raw output.")
42037
+ } : {};
42010
42038
  const backgroundFlagArg = initialBashCfg.background ? {
42011
- background: z4.boolean().optional().describe("When true, spawn the command in the background and return a taskId for bash_status/bash_kill instead of waiting for completion. Defaults to false.")
42039
+ background: z3.boolean().optional().describe(`When true, spawn the command in the background and return a taskId${taskControls.length > 0 ? ` for ${taskControls.join("/")}` : ""} instead of waiting for completion. Defaults to false.`)
42012
42040
  } : {};
42013
42041
  const ptyArgs = initialBashCfg.background ? {
42014
- pty: z4.boolean().optional().describe('When true, spawn the command in a real PTY for interactive programs (python/node/bash REPLs, vim). Implies background: true automatically. Unavailable in subagent sessions. Inspect with bash_status({ taskId, outputMode: "screen" }) and drive interactively with bash_write — its input accepts either a string OR an array like [ "iHello", { key: "esc" }, ":wq", { key: "enter" } ] for atomic text+key sequences.'),
42042
+ pty: z3.boolean().optional().describe(`When true, spawn the command in a real PTY for interactive programs (python/node/bash REPLs, vim). Implies background: true automatically.${initialBashCfg.subagent_background ? "" : " Unavailable in subagent sessions because bash.subagent_background is false."}${ptyDriveParamSentence(statusRegistered, writeRegistered)}`),
42015
42043
  ptyRows: optionalInt(1, 60).describe("PTY terminal height in rows — ignored when pty is false. Defaults to 24 when pty: true. Minimum 1, maximum 60."),
42016
42044
  ptyCols: optionalInt(1, 140).describe("PTY terminal width in columns — ignored when pty is false. Defaults to 80 when pty: true. Minimum 1, maximum 140.")
42017
42045
  } : {};
42018
42046
  const args = {
42019
- command: z4.string().describe("Shell command to execute. Supports pipes, redirection, and normal shell syntax."),
42047
+ command: z3.string().describe("Shell command to execute. Supports pipes, redirection, and normal shell syntax."),
42020
42048
  timeout: optionalInt(1, Number.MAX_SAFE_INTEGER).describe(initialBashCfg.background ? "Hard kill cap in milliseconds (positive integer). In the default foreground mode when wait is false, a command that exceeds the configured wait window is promoted to background and gets a completion reminder when it exits; wait:true disables promotion and remains inline until completion or timeout." : "Hard kill cap in milliseconds (positive integer). When omitted, the foreground command can run up to 30 minutes and returns inline when it finishes."),
42021
- workdir: z4.string().optional().describe("Working directory for command execution. Relative paths resolve through the bridge; defaults to the current tool context/project root when omitted."),
42022
- description: z4.string().optional().describe("Short 5-10 word human-readable summary shown in OpenCode UI metadata instead of raw shell syntax."),
42023
- wait: z4.boolean().optional().describe(`When true, run in the foreground without auto-promoting and wait until the command finishes or reaches its timeout; ${userMessageDetachDescription(initialBashCfg.detach_on_user_message)} Use only when you know the result is required before doing anything else.`),
42024
- sandbox: z4.literal("host").optional().describe("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."),
42049
+ workdir: z3.string().optional().describe("Working directory for command execution. Relative paths resolve through the bridge; defaults to the current tool context/project root when omitted."),
42050
+ description: z3.string().optional().describe("Short 5-10 word human-readable summary shown in OpenCode UI metadata instead of raw shell syntax."),
42051
+ ...waitArg,
42052
+ ...sandboxArg,
42025
42053
  ...backgroundFlagArg,
42026
- compressed: z4.boolean().optional().describe("When true or omitted, return compressed output with noisy terminal control sequences reduced. Set to false for raw output."),
42054
+ ...compressedArg,
42027
42055
  ...ptyArgs
42028
42056
  };
42029
42057
  let hostFallbackActive = false;
42030
42058
  return {
42031
- description: bashToolDescription(false, initialBashCfg.compress, initialBashCfg.background, true, toolEnabled(ctx.config, "aft_zoom")),
42059
+ description: bashToolDescription(false, initialBashCfg.compress, initialBashCfg.background, true, toolEnabled(ctx.config, "aft_zoom"), bashCompanionRegistered(ctx.config, "bash_watch"), {
42060
+ outline: toolEnabled(ctx.config, "aft_outline"),
42061
+ status: statusRegistered,
42062
+ write: writeRegistered
42063
+ }),
42032
42064
  args,
42033
42065
  execute: async (args, context) => {
42034
42066
  const bashCfg = resolveBashConfig(ctx.config);
@@ -42042,7 +42074,7 @@ function createBashTool(ctx, aftSearchRegisteredOverride) {
42042
42074
  const cwd = args.workdir ?? context.directory;
42043
42075
  const isSubagent = await resolveIsSubagent(ctx.client, context.sessionID, context.directory);
42044
42076
  const backgroundDisabled = !bashCfg.background;
42045
- const requestedWait = coerceBoolean(args.wait);
42077
+ const requestedWait = !backgroundDisabled && coerceBoolean(args.wait);
42046
42078
  const rawRequestedPty = coerceBoolean(args.pty);
42047
42079
  const rawRequestedBackground = coerceBoolean(args.background);
42048
42080
  if (requestedWait && rawRequestedPty) {
@@ -42053,14 +42085,15 @@ function createBashTool(ctx, aftSearchRegisteredOverride) {
42053
42085
  }
42054
42086
  const requestedPty = !backgroundDisabled && rawRequestedPty;
42055
42087
  const requestedBackground = !backgroundDisabled && (rawRequestedBackground || requestedPty);
42056
- if (requestedPty && isSubagent) {
42057
- throw new Error("PTY mode is not available in subagent sessions; subagents cannot drive interactive terminals.");
42058
- }
42059
42088
  const allowSubagentBg = bashCfg.subagent_background;
42060
42089
  const subagentForcedForeground = isSubagent && !allowSubagentBg;
42090
+ if (requestedPty && subagentForcedForeground) {
42091
+ throw new Error("pty:true is unavailable in this subagent session because bash.subagent_background is false; run the command without pty.");
42092
+ }
42061
42093
  const blockToCompletion = subagentForcedForeground || backgroundDisabled || requestedWait;
42062
42094
  const effectiveBackground = blockToCompletion ? false : requestedBackground;
42063
42095
  const rawTimeout = coerceOptionalInt(args.timeout, "timeout", 1, Number.MAX_SAFE_INTEGER);
42096
+ const workerUnboundedWait = isSubagent && requestedWait && rawTimeout === undefined;
42064
42097
  const ptyRows = coerceOptionalInt(args.ptyRows, "ptyRows", 1, 60);
42065
42098
  const ptyCols = coerceOptionalInt(args.ptyCols, "ptyCols", 1, 140);
42066
42099
  const compressed = coerceBoolean(args.compressed, true);
@@ -42091,7 +42124,7 @@ function createBashTool(ctx, aftSearchRegisteredOverride) {
42091
42124
  wait: requestedWait,
42092
42125
  sandbox: args.sandbox
42093
42126
  }, callBashBridge, {
42094
- transportTimeoutMs: orchestratedTransportTimeoutMs(blockToCompletion, requestedWait, rawTimeout, foregroundWaitMs),
42127
+ transportTimeoutMs: orchestratedTransportTimeoutMs(blockToCompletion, requestedWait, rawTimeout, foregroundWaitMs, workerUnboundedWait),
42095
42128
  onProgress: ({ text }) => {
42096
42129
  accumulatedOutput = preview(accumulatedOutput + text);
42097
42130
  metadata?.({ output: accumulatedOutput, description });
@@ -42101,10 +42134,10 @@ function createBashTool(ctx, aftSearchRegisteredOverride) {
42101
42134
  const fallbackCause = classifyBashHostFallbackError(error);
42102
42135
  if (!bashCfg.host_fallback || fallbackCause === undefined)
42103
42136
  throw error;
42104
- if (rawRequestedBackground) {
42137
+ if (!backgroundDisabled && rawRequestedBackground) {
42105
42138
  throw new Error(`${BASH_HOST_FALLBACK_REFUSAL}; background:true is unsupported.`);
42106
42139
  }
42107
- if (rawRequestedPty) {
42140
+ if (requestedPty) {
42108
42141
  throw new Error(`${BASH_HOST_FALLBACK_REFUSAL}; pty:true is unsupported.`);
42109
42142
  }
42110
42143
  const projectRoot = projectRootFor(context);
@@ -42138,7 +42171,7 @@ function createBashTool(ctx, aftSearchRegisteredOverride) {
42138
42171
  trackBgTask(context.sessionID, taskId);
42139
42172
  let rendered = data.output ?? "";
42140
42173
  if (isSubagent && allowSubagentBg) {
42141
- rendered += subagentGuidance(taskId, bashCfg.watch_sync_max_ms);
42174
+ rendered += workerBackgroundTaskNote(taskId);
42142
42175
  }
42143
42176
  const metadataPayload = { description, output: rendered, status: "running", taskId };
42144
42177
  metadata?.(metadataPayload);
@@ -42157,17 +42190,19 @@ function createBashTool(ctx, aftSearchRegisteredOverride) {
42157
42190
  };
42158
42191
  }
42159
42192
  function createBashStatusTool(ctx) {
42193
+ const waitSteer = bashCompanionRegistered(ctx.config, "bash_watch") ? " To wait, use bash_watch." : "";
42160
42194
  return {
42161
- description: "Read-only snapshot of a background or PTY bash task's current state and output. Returns immediately. Never waits. One look to check on a task is fine — never loop it to wait for completion. To wait, use bash_watch.",
42195
+ description: `Read-only snapshot of a background or PTY bash task's current state and output. Returns immediately. Never waits. One look to check on a task is fine — never loop it to wait for completion.${waitSteer}`,
42162
42196
  args: {
42163
- taskId: z4.string().describe("Background task ID returned by bash({ background: true }), e.g. bash-6b454047a1c39ded."),
42164
- outputMode: z4.enum(["screen", "raw", "both"]).optional().describe("PTY output rendering mode. Defaults to screen for PTY tasks and preserves existing behavior for piped tasks when omitted.")
42197
+ taskId: z3.string().describe("Background task ID returned by bash({ background: true }), e.g. bash-6b454047a1c39ded."),
42198
+ outputMode: z3.enum(["screen", "raw", "both"]).optional().describe("PTY output rendering mode. Defaults to screen for PTY tasks and preserves existing behavior for piped tasks when omitted.")
42165
42199
  },
42166
42200
  execute: async (args, context) => {
42167
42201
  const taskId = args.taskId;
42168
42202
  const outputMode = args.outputMode;
42169
42203
  const data = await bashStatusSnapshot(ctx, context, taskId, outputMode);
42170
- return await formatBashStatusText(context, taskId, data, outputMode);
42204
+ const isSubagent = await resolveIsSubagent(ctx.client, context.sessionID, context.directory);
42205
+ return await formatBashStatusText(context, taskId, data, outputMode, isSubagent ? "worker" : "primary");
42171
42206
  }
42172
42207
  };
42173
42208
  }
@@ -42175,7 +42210,7 @@ function createBashKillTool(ctx) {
42175
42210
  return {
42176
42211
  description: "Terminate a running background bash task spawned with bash({ background: true }). Returns confirmation of kill or an error if the task already finished.",
42177
42212
  args: {
42178
- taskId: z4.string().describe("Background task ID returned by bash({ background: true }), e.g. bash-6b454047a1c39ded.")
42213
+ taskId: z3.string().describe("Background task ID returned by bash({ background: true }), e.g. bash-6b454047a1c39ded.")
42179
42214
  },
42180
42215
  execute: async (args, context) => {
42181
42216
  const data = await callBashBridge(ctx, context, "bash_kill", {
@@ -42198,7 +42233,7 @@ async function bashStatusSnapshot(ctx, runtime, taskId, outputMode, options) {
42198
42233
  }
42199
42234
  return data;
42200
42235
  }
42201
- async function formatBashStatusText(runtime, taskId, data, requestedOutputMode) {
42236
+ async function formatBashStatusText(runtime, taskId, data, requestedOutputMode, role) {
42202
42237
  const status = data.status;
42203
42238
  const exit = typeof data.exit_code === "number" ? ` (exit ${data.exit_code})` : "";
42204
42239
  const dur = typeof data.duration_ms === "number" ? ` ${Math.round(data.duration_ms / 1000)}s` : "";
@@ -42216,7 +42251,7 @@ ${preview}`;
42216
42251
  }
42217
42252
  if (status === "running") {
42218
42253
  text += `
42219
- A completion reminder will be delivered automatically; don't poll.`;
42254
+ ${runningTaskStatusHint(role)}`;
42220
42255
  }
42221
42256
  }
42222
42257
  return text;
@@ -42245,11 +42280,6 @@ PTY task is still running. Use bash_status({ taskId: "${taskId}", outputMode: "s
42245
42280
  function preview(output) {
42246
42281
  return output.length <= METADATA_PREVIEW_LIMIT ? output : output.slice(-METADATA_PREVIEW_LIMIT);
42247
42282
  }
42248
- function subagentGuidance(taskId, watchSyncMaxMs) {
42249
- return `
42250
-
42251
- NOTE (subagent session): Continue with other work if you have it. If you don't, call bash_watch({ taskId: "${taskId}" }) to wait for completion before returning to the parent; without timeoutMs it waits up to ${watchSyncMaxMs} ms, the maximum. Subagents don't survive turn-end and won't receive the completion reminder.`;
42252
- }
42253
42283
  function foregroundMetadata(description, data, rendered) {
42254
42284
  const outputPath = data.output_path;
42255
42285
  const truncated = typeof data.truncated === "boolean" ? data.truncated : data.output_truncated;
@@ -42270,24 +42300,355 @@ function shortenCommand(command) {
42270
42300
  return collapsed.length <= 80 ? collapsed : `${collapsed.slice(0, 77)}...`;
42271
42301
  }
42272
42302
 
42303
+ // src/tools/conflicts.ts
42304
+ var z4 = tool.schema;
42305
+ function conflictTools(ctx) {
42306
+ return {
42307
+ aft_conflicts: {
42308
+ description: "Show all git merge conflicts across the repository — returns line-numbered conflict regions with context for every conflicted file in a single call. Conflicts are discovered from the git repository's top level. By default it inspects the session's project repository; pass `path` to inspect a different repository or git worktree (e.g. where a rebase/merge is running).",
42309
+ args: {
42310
+ path: z4.string().describe("Optional path inside the git repository or worktree to inspect (absolute or relative to project root). Conflicts are discovered from that repository's top level. Defaults to the session project root.").optional()
42311
+ },
42312
+ execute: async (args, context) => {
42313
+ const rawArgs = {};
42314
+ if (!isEmptyParam(args?.path)) {
42315
+ const expanded = expandTilde2(String(args.path));
42316
+ const projectRoot = await resolveProjectRoot(ctx, context);
42317
+ const resolved = resolvePathFromProjectRoot(projectRoot, expanded);
42318
+ const denied = await assertExternalDirectoryPermission(ctx, context, resolved, {
42319
+ kind: "directory"
42320
+ });
42321
+ if (denied)
42322
+ return permissionDeniedResponse(denied);
42323
+ rawArgs.path = resolved;
42324
+ }
42325
+ const response = await callToolCall(ctx, context, "conflicts", rawArgs);
42326
+ if (response.success === false) {
42327
+ throw new Error(response.message || "git_conflicts failed");
42328
+ }
42329
+ return response.text;
42330
+ }
42331
+ }
42332
+ };
42333
+ }
42334
+
42335
+ // src/tools/definitions/v2.ts
42336
+ import { Effect as Effect3 } from "effect";
42337
+ var V2_TOOL_NAME = /^[A-Za-z][A-Za-z0-9_-]{0,63}$/;
42338
+ var V2_PATH_HEADER_TOOLS = new Set(["read", "write", "edit"]);
42339
+ var V2_BASH_TOOLS = new Set(["bash"]);
42340
+ var V2_SHELL_PERMISSION = "shell";
42341
+ var ROOT_COMBINATORS = ["anyOf", "oneOf", "allOf"];
42342
+ function failure(error) {
42343
+ if (error instanceof Error)
42344
+ return error;
42345
+ const detail = (() => {
42346
+ try {
42347
+ return JSON.stringify(error) ?? String(error);
42348
+ } catch {
42349
+ return String(error);
42350
+ }
42351
+ })();
42352
+ return new Error(`V2 tool execution rejected with a non-Error value: ${detail}`);
42353
+ }
42354
+ function isPlainObject3(value) {
42355
+ if (typeof value !== "object" || value === null)
42356
+ return false;
42357
+ const prototype = Object.getPrototypeOf(value);
42358
+ return prototype === Object.prototype || prototype === null;
42359
+ }
42360
+ function jsonSafeMetadata(value) {
42361
+ if (Array.isArray(value)) {
42362
+ return value.map((entry) => entry === undefined ? null : jsonSafeMetadata(entry));
42363
+ }
42364
+ if (!isPlainObject3(value))
42365
+ return value;
42366
+ const cleaned = {};
42367
+ for (const [key, entry] of Object.entries(value)) {
42368
+ if (entry === undefined)
42369
+ continue;
42370
+ cleaned[key] = jsonSafeMetadata(entry);
42371
+ }
42372
+ return cleaned;
42373
+ }
42374
+ function resultContent(result) {
42375
+ if (typeof result === "string")
42376
+ return { content: result };
42377
+ const attachments = (result.attachments ?? []).map((attachment) => ({
42378
+ type: "file",
42379
+ uri: attachment.url,
42380
+ mime: attachment.mime,
42381
+ ...attachment.filename ? { name: attachment.filename } : {}
42382
+ }));
42383
+ const content = attachments.length ? [{ type: "text", text: result.output }, ...attachments] : result.output;
42384
+ const metadata = jsonSafeMetadata({
42385
+ ...result.metadata ?? {},
42386
+ ...result.title ? { title: result.title } : {}
42387
+ });
42388
+ return {
42389
+ content,
42390
+ ...Object.keys(metadata).length ? { metadata } : {}
42391
+ };
42392
+ }
42393
+ function failureEnvelopeMessage(result) {
42394
+ const output = typeof result === "string" ? result : result.output;
42395
+ if (typeof output !== "string")
42396
+ return;
42397
+ const trimmed = output.trim();
42398
+ if (!trimmed.startsWith("{") || !trimmed.includes('"success"'))
42399
+ return;
42400
+ let parsed;
42401
+ try {
42402
+ parsed = JSON.parse(trimmed);
42403
+ } catch {
42404
+ return;
42405
+ }
42406
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
42407
+ return;
42408
+ const envelope = parsed;
42409
+ if (envelope.success !== false)
42410
+ return;
42411
+ for (const field of [envelope.message, envelope.error, envelope.code]) {
42412
+ if (typeof field === "string" && field.length > 0)
42413
+ return field;
42414
+ }
42415
+ return "The tool call failed.";
42416
+ }
42417
+ function bareToolName3(name) {
42418
+ return name.startsWith("aft_") ? name.slice(4) : name;
42419
+ }
42420
+ function projectArguments(name, definition) {
42421
+ if (!V2_PATH_HEADER_TOOLS.has(bareToolName3(name)) || !("filePath" in definition.args)) {
42422
+ return { ...definition.args };
42423
+ }
42424
+ const { filePath, ...rest } = definition.args;
42425
+ return { path: filePath, ...rest };
42426
+ }
42427
+ function executionArguments(name, input) {
42428
+ if (!V2_PATH_HEADER_TOOLS.has(bareToolName3(name)) || !("path" in input))
42429
+ return input;
42430
+ const { path, ...rest } = input;
42431
+ return { filePath: path, ...rest };
42432
+ }
42433
+ function hostPermission(name) {
42434
+ const bare = bareToolName3(name);
42435
+ if (new Set(["read", "glob", "grep"]).has(bare))
42436
+ return bare;
42437
+ if (V2_BASH_TOOLS.has(name))
42438
+ return V2_SHELL_PERMISSION;
42439
+ if (new Set([
42440
+ "write",
42441
+ "edit",
42442
+ "apply_patch",
42443
+ "delete",
42444
+ "move",
42445
+ "ast_grep_replace",
42446
+ "import",
42447
+ "safety"
42448
+ ]).has(bare)) {
42449
+ return "edit";
42450
+ }
42451
+ return;
42452
+ }
42453
+ function assertV2Contract(name, input) {
42454
+ if (!V2_TOOL_NAME.test(name)) {
42455
+ throw new Error(`Invalid V2 tool name ${JSON.stringify(name)}; expected ${V2_TOOL_NAME.source}`);
42456
+ }
42457
+ const schema = tool.schema.toJSONSchema(input, { io: "input" });
42458
+ if (schema.type !== "object") {
42459
+ throw new Error(`V2 tool ${name} must have an object-rooted input schema`);
42460
+ }
42461
+ for (const keyword of ROOT_COMBINATORS) {
42462
+ if (keyword in schema) {
42463
+ throw new Error(`V2 tool ${name} input schema cannot use root ${keyword}`);
42464
+ }
42465
+ }
42466
+ }
42467
+ function v2PermissionRequest(name, request) {
42468
+ if (!V2_BASH_TOOLS.has(name) || request.permission !== "bash")
42469
+ return request;
42470
+ return { ...request, permission: V2_SHELL_PERMISSION };
42471
+ }
42472
+ function runtimeFor(name, location, context, signal, consumers) {
42473
+ const directory = location.directory;
42474
+ const worktree = location.project?.canonical ?? location.project?.directory ?? directory;
42475
+ return {
42476
+ sessionID: context.sessionID,
42477
+ messageID: context.messageID,
42478
+ agent: context.agent,
42479
+ directory,
42480
+ directoryIsSessionRoot: true,
42481
+ worktree,
42482
+ abort: signal,
42483
+ effectAbort: signal,
42484
+ metadata: (update) => {
42485
+ Effect3.runPromise(context.progress(jsonSafeMetadata({
42486
+ ...update.metadata ?? {},
42487
+ ...update.title ? { title: update.title } : {}
42488
+ })));
42489
+ },
42490
+ ask: (asked) => {
42491
+ const request = v2PermissionRequest(name, asked);
42492
+ if (consumers.requestPermission)
42493
+ return consumers.requestPermission(request, context);
42494
+ return Promise.reject(new Error(`The "${request.permission}" operation was refused because this AFT runtime has no permission evaluator bound, so the host's permission rules for it could not be consulted.`));
42495
+ },
42496
+ progress: context.progress
42497
+ };
42498
+ }
42499
+ function projectV2Tool(name, definition, location, consumers = {}) {
42500
+ const input = tool.schema.object(projectArguments(name, definition));
42501
+ assertV2Contract(name, input);
42502
+ const sharedOptions = definition.options;
42503
+ const permission = hostPermission(name);
42504
+ return {
42505
+ name,
42506
+ description: definition.description,
42507
+ input,
42508
+ options: {
42509
+ ...sharedOptions,
42510
+ ...permission ? { permission } : {},
42511
+ codemode: false
42512
+ },
42513
+ execute: (rawInput, context) => Effect3.tryPromise({
42514
+ try: async (signal) => {
42515
+ const runtime = runtimeFor(name, location, context, signal, consumers);
42516
+ const input = executionArguments(name, rawInput);
42517
+ const result = consumers.executeBash && V2_BASH_TOOLS.has(name) ? await consumers.executeBash({
42518
+ name,
42519
+ input,
42520
+ context: runtime,
42521
+ definition
42522
+ }) : await definition.execute(input, runtime);
42523
+ const refusal = failureEnvelopeMessage(result);
42524
+ if (refusal)
42525
+ throw new Error(refusal);
42526
+ return resultContent(result);
42527
+ },
42528
+ catch: failure
42529
+ })
42530
+ };
42531
+ }
42532
+
42533
+ // src/tools/hoisted.ts
42534
+ import { stat as stat2 } from "node:fs/promises";
42535
+ import * as path4 from "node:path";
42536
+
42537
+ // src/hashline-tag-sources.ts
42538
+ function hashlineTagSourceSentence(bashRewritesMintTags, isRegistered = () => true) {
42539
+ const rewrites = bashRewritesMintTags ? " (and accepted AFT `cat`/`head`/`tail` rewrites)" : "";
42540
+ const lookAlikes = [
42541
+ ...["aft_zoom", "aft_outline"].filter(isRegistered),
42542
+ "grep",
42543
+ ...isRegistered("aft_search") ? ["aft_search"] : []
42544
+ ].map((name) => `\`${name}\``);
42545
+ lookAlikes.push("conflict snippets");
42546
+ return `Only \`read\`${rewrites} mint hashline tags. ${joinList(lookAlikes)} do not. After navigation, call \`read\` on every file and range the patch addresses.`;
42547
+ }
42548
+ function joinList(items) {
42549
+ if (items.length <= 2)
42550
+ return items.join(" and ");
42551
+ return `${items.slice(0, -1).join(", ")}, and ${items[items.length - 1]}`;
42552
+ }
42553
+
42554
+ // src/shared/read-vision.ts
42555
+ var VISION_HOST_TIMEOUT_MS = 1500;
42556
+ var states = new WeakMap;
42557
+ function stateFor2(client) {
42558
+ let state = states.get(client);
42559
+ if (!state) {
42560
+ state = { models: new Map, history: new Map, capabilities: new Map };
42561
+ states.set(client, state);
42562
+ }
42563
+ return state;
42564
+ }
42565
+ function rememberReadModel(client, sessionID, model) {
42566
+ stateFor2(client).models.set(sessionID, model);
42567
+ }
42568
+ async function bounded(call) {
42569
+ let timer;
42570
+ try {
42571
+ return await Promise.race([
42572
+ Promise.resolve().then(call),
42573
+ new Promise((resolve) => {
42574
+ timer = setTimeout(() => resolve(undefined), VISION_HOST_TIMEOUT_MS);
42575
+ })
42576
+ ]);
42577
+ } catch {
42578
+ return;
42579
+ } finally {
42580
+ clearTimeout(timer);
42581
+ }
42582
+ }
42583
+ function record3(value) {
42584
+ return typeof value === "object" && value !== null && !Array.isArray(value) ? value : undefined;
42585
+ }
42586
+ async function currentSessionVisionCapability(client, sessionID) {
42587
+ if (!sessionID)
42588
+ return;
42589
+ const state = stateFor2(client);
42590
+ let model = state.models.get(sessionID);
42591
+ if (!model) {
42592
+ let pending = state.history.get(sessionID);
42593
+ if (!pending) {
42594
+ pending = bounded(() => resolvePromptContext(client, sessionID)).then((context) => context?.model);
42595
+ state.history.set(sessionID, pending);
42596
+ pending.finally(() => state.history.delete(sessionID));
42597
+ }
42598
+ const historical = await pending;
42599
+ model = state.models.get(sessionID) ?? historical;
42600
+ }
42601
+ if (!model)
42602
+ return;
42603
+ const currentModel = model;
42604
+ const key = JSON.stringify([model.providerID, model.modelID]);
42605
+ let capability = state.capabilities.get(key);
42606
+ if (!capability) {
42607
+ capability = (async () => {
42608
+ const api = client.provider;
42609
+ if (!api?.list)
42610
+ return;
42611
+ const list = api.list.bind(api);
42612
+ const listed = record3(await bounded(list));
42613
+ const catalog = record3(listed?.data) ?? listed;
42614
+ const providers = catalog?.all ?? catalog?.providers;
42615
+ if (!Array.isArray(providers))
42616
+ return;
42617
+ const provider = providers.map(record3).find((entry) => entry?.id === currentModel.providerID);
42618
+ const models = provider?.models;
42619
+ const entry = record3(Array.isArray(models) ? models.find((entry) => record3(entry)?.id === currentModel.modelID) : record3(models)?.[currentModel.modelID]);
42620
+ const inputs = record3(entry?.modalities)?.input;
42621
+ return Array.isArray(inputs) ? inputs.includes("image") : typeof entry?.attachment === "boolean" ? entry.attachment : undefined;
42622
+ })();
42623
+ state.capabilities.set(key, capability);
42624
+ capability.then((value) => {
42625
+ if (value === undefined)
42626
+ state.capabilities.delete(key);
42627
+ });
42628
+ }
42629
+ return capability;
42630
+ }
42631
+
42273
42632
  // src/tools/bash_watch.ts
42274
42633
  var z5 = tool.schema;
42275
- var BASH_WAIT_POLL_INTERVAL_MS = 100;
42276
42634
  var REGEX_WAIT_SCAN_WINDOW_BYTES = 64 * 1024;
42277
42635
  function unavailableSnapshot() {
42278
42636
  return { status: "unknown" };
42279
42637
  }
42280
- function coerceConfiguredWatchTimeout(value, cap) {
42638
+ function coerceConfiguredWatchTimeout(value, role, cap) {
42281
42639
  try {
42282
- return coerceOptionalInt(value, "timeoutMs", 1, cap);
42640
+ return coerceOptionalInt(value, "timeoutMs", 1, maxWatchTimeoutMs(role, cap));
42283
42641
  } catch (error) {
42284
42642
  const message = error instanceof Error ? error.message : String(error);
42643
+ if (role === "worker")
42644
+ throw error;
42285
42645
  throw new Error(`${message} (bash.watch_sync_max_ms)`);
42286
42646
  }
42287
42647
  }
42288
42648
  function createBashWatchTool(ctx) {
42649
+ const noPolling = bashCompanionRegistered(ctx.config, "bash_status") ? " Never loop bash_status to wait." : "";
42289
42650
  return {
42290
- 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.",
42651
+ 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.${noPolling}`,
42291
42652
  args: {
42292
42653
  taskId: z5.string().describe("Background task ID returned by bash({ background: true })."),
42293
42654
  pattern: z5.union([z5.string(), z5.object({ regex: z5.string() })]).optional().describe("Substring or regex pattern. Optional in sync mode; required with background:true. Sync substring watches keep only the overlap tail needed for boundary matches; sync regex watches use a 64 KB rolling output window."),
@@ -42338,11 +42699,11 @@ A notification will fire when the pattern matches or the task exits.`;
42338
42699
  }
42339
42700
  const syncWaitCap = bashCfg.watch_sync_max_ms;
42340
42701
  const role = isSubagent ? "worker" : "primary";
42341
- const effectiveWaitMs = subagentForcedSync ? syncWaitCap : resolveWatchTimeoutMs(coerceConfiguredWatchTimeout(args.timeoutMs, syncWaitCap), role, syncWaitCap);
42702
+ const effectiveWaitMs = subagentForcedSync ? undefined : resolveWatchTimeoutMs(coerceConfiguredWatchTimeout(args.timeoutMs, role, syncWaitCap), role, syncWaitCap);
42342
42703
  const data = await waitForBashStatus(ctx, context, taskId, undefined, waitFor, effectiveWaitMs);
42343
42704
  const waited = data.waited;
42344
42705
  if (waited?.reason === "user_message") {
42345
- const convertedText = await convertToAsyncWatchOnAbort(ctx, context, taskId, waitFor, coerceBoolean(args.once, true), waited.elapsed_ms);
42706
+ const convertedText = await convertToAsyncWatchOnAbort(ctx, context, taskId, waitFor, coerceBoolean(args.once, true), waited.elapsed_ms, role);
42346
42707
  const metadata = context.metadata;
42347
42708
  metadata?.({ taskId, status: data.status, waited, convertedToAsync: true });
42348
42709
  return convertedText;
@@ -42354,10 +42715,11 @@ A notification will fire when the pattern matches or the task exits.`;
42354
42715
  }
42355
42716
  };
42356
42717
  }
42357
- async function convertToAsyncWatchOnAbort(ctx, context, taskId, waitFor, once, elapsedMs) {
42718
+ async function convertToAsyncWatchOnAbort(ctx, context, taskId, waitFor, once, elapsedMs, role) {
42358
42719
  const interrupted = `Sync watch for task ${taskId} was interrupted because you sent a message after ${elapsedMs}ms of waiting. `;
42720
+ const reminderFallback = interruptedWatchTail(role, `The task is still running in the background. A completion reminder will be ` + `delivered automatically when the task exits.`);
42359
42721
  if (!waitFor) {
42360
- 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.`;
42722
+ 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.`);
42361
42723
  }
42362
42724
  const notifyParams = {
42363
42725
  task_id: taskId,
@@ -42372,12 +42734,12 @@ async function convertToAsyncWatchOnAbort(ctx, context, taskId, waitFor, once, e
42372
42734
  const registered = await callBashBridge(ctx, context, "bash_notify", notifyParams);
42373
42735
  if (registered.success === false) {
42374
42736
  unmarkExplicitControl(context.sessionID, taskId);
42375
- 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.`;
42737
+ return interrupted + `Auto-registering an async watch failed (${String(registered.message ?? "unknown error")}). ` + reminderFallback;
42376
42738
  }
42377
- 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.`;
42739
+ 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.`);
42378
42740
  } catch (err) {
42379
42741
  unmarkExplicitControl(context.sessionID, taskId);
42380
- 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.`;
42742
+ return interrupted + `Auto-registering an async watch failed (${err instanceof Error ? err.message : String(err)}). ` + reminderFallback;
42381
42743
  }
42382
42744
  }
42383
42745
  function formatWatchResultText(taskId, data, waited, role, syncWaitCap) {
@@ -42386,17 +42748,20 @@ function formatWatchResultText(taskId, data, waited, role, syncWaitCap) {
42386
42748
  const dur = typeof data.duration_ms === "number" ? ` ${Math.round(data.duration_ms / 1000)}s` : "";
42387
42749
  let text = `Task ${taskId}: ${status}${exit}${dur}`;
42388
42750
  if (waited) {
42389
- const waitedText = formatWatchWaited(waited.elapsed_ms, waited.limit_ms, syncWaitCap);
42751
+ const waitedText = formatWatchWaited(waited.elapsed_ms, waited.limit_ms, role === "primary" ? syncWaitCap : undefined);
42390
42752
  if (waited.reason === "matched") {
42391
42753
  const stream = waited.match_stream ? ` in ${waited.match_stream}` : "";
42392
42754
  text += `
42393
42755
  ${waitedText}; matched ${JSON.stringify(waited.match ?? "")}${stream} at offset ${waited.match_offset ?? 0}.`;
42394
42756
  } else if (waited.reason === "timeout") {
42395
42757
  text += `
42396
- ${waitedText}; timeout reached without match. ${watchTimeoutSteer(role, syncWaitCap)}`;
42758
+ ${waitedText}; timeout reached without match. ${watchTimeoutSteer(role)}`;
42397
42759
  } else if (waited.reason === "unavailable") {
42398
42760
  text += `
42399
- ${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.`;
42761
+ ${waitedText}; ${watchUnavailableSteer(role)}`;
42762
+ } else if (waited.reason === "aborted") {
42763
+ text += `
42764
+ ${waitedText}; the watch was cancelled. The task keeps running.`;
42400
42765
  } else {
42401
42766
  const stat = String(data.status ?? "unknown");
42402
42767
  const e = typeof data.exit_code === "number" ? `, exit ${data.exit_code}` : "";
@@ -42423,9 +42788,17 @@ async function bashStatusSnapshot2(ctx, runtime, taskId, outputMode, cursor, opt
42423
42788
  return data;
42424
42789
  }
42425
42790
  async function waitForBashStatus(ctx, runtime, taskId, outputMode, waitFor, effectiveWaitMs) {
42426
- const startedAt = monotonicNowMs();
42427
- const deadline = startedAt + effectiveWaitMs;
42428
- const elapsedMs = () => Math.round(monotonicNowMs() - startedAt);
42791
+ const startedAt = watchClock.now();
42792
+ const deadline = effectiveWaitMs === undefined ? Number.POSITIVE_INFINITY : startedAt + effectiveWaitMs;
42793
+ const elapsedMs = () => Math.round(watchClock.now() - startedAt);
42794
+ const abortSignal = runtime.abort;
42795
+ const pause = () => watchClock.sleep(Math.min(watchPollDelayMs(elapsedMs()), Math.max(0, deadline - watchClock.now())), abortSignal);
42796
+ const waited = (reason, extra = {}) => ({
42797
+ reason,
42798
+ elapsed_ms: elapsedMs(),
42799
+ ...effectiveWaitMs === undefined ? {} : { limit_ms: effectiveWaitMs },
42800
+ ...extra
42801
+ });
42429
42802
  let spillCursor = { output: 0, stderr: 0 };
42430
42803
  const scanState = {
42431
42804
  output: { text: "", baseOffset: 0 },
@@ -42439,31 +42812,29 @@ async function waitForBashStatus(ctx, runtime, taskId, outputMode, waitFor, effe
42439
42812
  markTaskWaiting(runtime.sessionID, taskId);
42440
42813
  let sawTerminal = false;
42441
42814
  let lastData;
42815
+ let busySince;
42442
42816
  try {
42443
42817
  while (true) {
42818
+ if (abortSignal?.aborted) {
42819
+ return withWaited(lastData ?? unavailableSnapshot(), waited("aborted"));
42820
+ }
42444
42821
  let data;
42445
42822
  try {
42446
42823
  data = await bashStatusSnapshot2(ctx, runtime, taskId, outputMode, waitFor ? spillCursor : undefined, bridgeOptions);
42447
42824
  } catch (err) {
42448
42825
  if (!isBridgeTransportTimeout(err))
42449
42826
  throw err;
42827
+ busySince ??= watchClock.now();
42450
42828
  if (isSyncWatchAborted(runtime.sessionID)) {
42451
- return withWaited(lastData ?? unavailableSnapshot(), {
42452
- reason: "user_message",
42453
- elapsed_ms: elapsedMs(),
42454
- limit_ms: effectiveWaitMs
42455
- });
42829
+ return withWaited(lastData ?? unavailableSnapshot(), waited("user_message"));
42456
42830
  }
42457
- if (monotonicNowMs() >= deadline) {
42458
- return withWaited(lastData ?? unavailableSnapshot(), {
42459
- reason: "unavailable",
42460
- elapsed_ms: elapsedMs(),
42461
- limit_ms: effectiveWaitMs
42462
- });
42831
+ if (watchClock.now() >= deadline || watchClock.now() - busySince >= WATCH_UNAVAILABLE_GIVE_UP_MS) {
42832
+ return withWaited(lastData ?? unavailableSnapshot(), waited("unavailable"));
42463
42833
  }
42464
- await sleep(Math.min(BASH_WAIT_POLL_INTERVAL_MS, Math.max(0, deadline - monotonicNowMs())));
42834
+ await pause();
42465
42835
  continue;
42466
42836
  }
42837
+ busySince = undefined;
42467
42838
  lastData = data;
42468
42839
  const terminal = isTerminalStatus(data.status);
42469
42840
  if (waitFor) {
@@ -42488,14 +42859,11 @@ async function waitForBashStatus(ctx, runtime, taskId, outputMode, waitFor, effe
42488
42859
  await markBgCompletionDelivered({ ctx, directory: projectRootFor(runtime), sessionID: runtime.sessionID }, taskId);
42489
42860
  }
42490
42861
  const matchStream = data.mode === "pty" ? undefined : chunk.stream === "output" ? "stdout" : "stderr";
42491
- return withWaited(data, {
42492
- reason: "matched",
42493
- elapsed_ms: elapsedMs(),
42494
- limit_ms: effectiveWaitMs,
42862
+ return withWaited(data, waited("matched", {
42495
42863
  match: match.text,
42496
42864
  match_offset: state.baseOffset + match.byteOffset,
42497
42865
  match_stream: matchStream
42498
- });
42866
+ }));
42499
42867
  }
42500
42868
  if (waitFor.kind === "substring") {
42501
42869
  const trimmed = trimWaitScanBuffer(state.text, state.baseOffset, waitFor);
@@ -42509,27 +42877,15 @@ async function waitForBashStatus(ctx, runtime, taskId, outputMode, waitFor, effe
42509
42877
  sawTerminal = true;
42510
42878
  consumeBgCompletion(runtime.sessionID, taskId);
42511
42879
  await markBgCompletionDelivered({ ctx, directory: projectRootFor(runtime), sessionID: runtime.sessionID }, taskId);
42512
- return withWaited(data, {
42513
- reason: "exited",
42514
- elapsed_ms: elapsedMs(),
42515
- limit_ms: effectiveWaitMs
42516
- });
42880
+ return withWaited(data, waited("exited"));
42517
42881
  }
42518
42882
  if (isSyncWatchAborted(runtime.sessionID)) {
42519
- return withWaited(data, {
42520
- reason: "user_message",
42521
- elapsed_ms: elapsedMs(),
42522
- limit_ms: effectiveWaitMs
42523
- });
42883
+ return withWaited(data, waited("user_message"));
42524
42884
  }
42525
- if (monotonicNowMs() >= deadline) {
42526
- return withWaited(data, {
42527
- reason: "timeout",
42528
- elapsed_ms: elapsedMs(),
42529
- limit_ms: effectiveWaitMs
42530
- });
42885
+ if (watchClock.now() >= deadline) {
42886
+ return withWaited(data, waited("timeout"));
42531
42887
  }
42532
- await sleep(Math.min(BASH_WAIT_POLL_INTERVAL_MS, Math.max(0, deadline - monotonicNowMs())));
42888
+ await pause();
42533
42889
  }
42534
42890
  } finally {
42535
42891
  if (!sawTerminal)
@@ -42639,8 +42995,9 @@ function withWaited(data, waited) {
42639
42995
  // src/tools/bash_write.ts
42640
42996
  var z6 = tool.schema;
42641
42997
  function createBashWriteTool(ctx) {
42998
+ const modeCheck = bashCompanionRegistered(ctx.config, "bash_status") ? '; check bash_status reports mode: "pty" first.' : ".";
42642
42999
  return {
42643
- 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).",
43000
+ description: `Write input bytes to a running PTY bash task. PTY-only${modeCheck} ` + '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).",
42644
43001
  args: {
42645
43002
  taskId: z6.string().describe("Background PTY task ID returned by bash({ pty: true, background: true })."),
42646
43003
  input: z6.union([
@@ -42908,25 +43265,19 @@ function createReadTool(ctx) {
42908
43265
  const failure = classifyPermissionError(error);
42909
43266
  return permissionDeniedResponse(failure.kind === "rule_denied" ? permissionRuleDenial("read") : failure.message);
42910
43267
  }
42911
- const rawStartLine = coerceOptionalInt(args.startLine, "startLine", 1, Number.MAX_SAFE_INTEGER);
42912
- const rawEndLine = coerceOptionalInt(args.endLine, "endLine", 1, Number.MAX_SAFE_INTEGER);
42913
- const rawLimit = coerceOptionalInt(args.limit, "limit", 1, Number.MAX_SAFE_INTEGER);
42914
- const rawOffset = coerceOptionalInt(args.offset, "offset", 1, Number.MAX_SAFE_INTEGER);
42915
- let startLine = rawStartLine;
42916
- let endLine = rawEndLine;
42917
- if (startLine === undefined && rawOffset !== undefined) {
42918
- startLine = rawOffset;
42919
- if (rawLimit !== undefined) {
42920
- endLine = rawOffset + rawLimit - 1;
42921
- }
42922
- }
43268
+ const startLine = coerceOptionalInt(args.startLine, "startLine", 1, Number.MAX_SAFE_INTEGER);
43269
+ const endLine = coerceOptionalInt(args.endLine, "endLine", 1, Number.MAX_SAFE_INTEGER);
43270
+ const limit = coerceOptionalInt(args.limit, "limit", 1, Number.MAX_SAFE_INTEGER);
43271
+ const offset = coerceOptionalInt(args.offset, "offset", 1, Number.MAX_SAFE_INTEGER);
42923
43272
  const rawArgs = { filePath: file };
42924
43273
  if (startLine !== undefined)
42925
43274
  rawArgs.startLine = startLine;
42926
43275
  if (endLine !== undefined)
42927
43276
  rawArgs.endLine = endLine;
42928
- if (rawLimit !== undefined && rawOffset === undefined)
42929
- rawArgs.limit = rawLimit;
43277
+ if (offset !== undefined)
43278
+ rawArgs.offset = offset;
43279
+ if (limit !== undefined)
43280
+ rawArgs.limit = limit;
42930
43281
  const mediaCandidate = isGithubResourcePath(file) || /\.(png|jpe?g|gif|webp|pdf)$/i.test(filePath) && await stat2(filePath).then((info) => info.isFile(), () => false);
42931
43282
  const visionCapability = mediaCandidate ? await measurePreStage("capability", () => currentSessionVisionCapability(ctx.client, context.sessionID)) : undefined;
42932
43283
  if (visionCapability !== undefined)
@@ -42982,7 +43333,7 @@ function createWriteTool(ctx, editToolName = "edit") {
42982
43333
  description: getWriteDescription(ctx, editToolName),
42983
43334
  args: {
42984
43335
  filePath: z7.string().describe("Path to the file to write (absolute or relative to project root)"),
42985
- content: z7.string().describe("The full content to write to the file")
43336
+ content: z7.string().describe("The full content to write to the file; for issue://N / pr://N, the comment body to post")
42986
43337
  },
42987
43338
  execute: async (args, context) => {
42988
43339
  const argsRecord = args;
@@ -43099,7 +43450,7 @@ ${backupBehavior}
43099
43450
  - Symbol replace includes decorators, attributes, and doc comments in range
43100
43451
  - Response is a compact server-rendered summary; before/after diff details are attached as UI metadata when available.`;
43101
43452
  }
43102
- var HASHLINE_EDIT_DESCRIPTION = [
43453
+ var HASHLINE_EDIT_GRAMMAR = [
43103
43454
  "Apply a hashline patch. Arguments are exactly `{patch}` where `patch` is a non-empty string. Server-owned preview control is outside this schema.",
43104
43455
  "",
43105
43456
  "Quick reference:",
@@ -43107,13 +43458,18 @@ var HASHLINE_EDIT_DESCRIPTION = [
43107
43458
  "- Same canonical path: multiple sections compose in patch order against pre-request coordinates.",
43108
43459
  "- 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.",
43109
43460
  "- 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 `-`.",
43110
- "- 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.",
43111
- "- 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."
43461
+ "- 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."
43112
43462
  ].join(`
43113
43463
  `);
43464
+ function hashlineEditDescription(config) {
43465
+ const bashRewrites = toolEnabled(config, "bash") && resolveBashConfig(config).rewrite;
43466
+ const tagSources = hashlineTagSourceSentence(bashRewrites, (name) => toolEnabled(config, name));
43467
+ return `${HASHLINE_EDIT_GRAMMAR}
43468
+ - ${tagSources}`;
43469
+ }
43114
43470
  function createHashlineEditTool(ctx) {
43115
43471
  return {
43116
- description: HASHLINE_EDIT_DESCRIPTION,
43472
+ description: hashlineEditDescription(ctx.config),
43117
43473
  args: {
43118
43474
  patch: z7.string().min(1).describe("Hashline patch text with one or more [path#TAG] sections and PUT/CUT/REM/MV operations")
43119
43475
  },
@@ -43188,8 +43544,7 @@ function createEditTool(ctx, writeToolName = "write") {
43188
43544
  throw new Error("'path' parameter is required");
43189
43545
  if (isGithubResourcePath(file)) {
43190
43546
  const edits = argsRecord.edits;
43191
- const onlyFindReplace = Array.isArray(edits) && edits.length > 0 && argsRecord.appendContent === undefined && argsRecord.symbol === undefined && argsRecord.content === undefined && edits.every((entry) => entry !== null && typeof entry === "object" && typeof entry.oldString === "string" && (entry.newString === undefined || typeof entry.newString === "string") && entry.startLine === undefined && entry.endLine === undefined && entry.content === undefined);
43192
- if (!onlyFindReplace) {
43547
+ if (!isFindReplaceOnlyEdit(argsRecord)) {
43193
43548
  throw new Error("edit: GitHub resources support only edits[] find/replace entries with oldString and optional newString");
43194
43549
  }
43195
43550
  const rawArgs = { path: file, edits };
@@ -43476,6 +43831,12 @@ function createMoveTool(ctx) {
43476
43831
  };
43477
43832
  }
43478
43833
  function hoistedTools(ctx) {
43834
+ const backgroundCompanions = resolveBashConfig(ctx.config).background ? {
43835
+ bash_status: createBashStatusTool(ctx),
43836
+ bash_write: createBashWriteTool(ctx),
43837
+ bash_watch: createBashWatchTool(ctx),
43838
+ bash_kill: createBashKillTool(ctx)
43839
+ } : {};
43479
43840
  const tools = {
43480
43841
  read: createReadTool(ctx),
43481
43842
  write: createWriteTool(ctx, "edit"),
@@ -43484,10 +43845,7 @@ function hoistedTools(ctx) {
43484
43845
  aft_delete: createDeleteTool(ctx),
43485
43846
  aft_move: createMoveTool(ctx),
43486
43847
  bash: createBashTool(ctx),
43487
- bash_status: createBashStatusTool(ctx),
43488
- bash_write: createBashWriteTool(ctx),
43489
- bash_watch: createBashWatchTool(ctx),
43490
- bash_kill: createBashKillTool(ctx)
43848
+ ...backgroundCompanions
43491
43849
  };
43492
43850
  return prepareToolMap(tools, { hashlineEffective: ctx.hashlineEffective });
43493
43851
  }
@@ -43883,6 +44241,9 @@ function navigationTools(ctx) {
43883
44241
 
43884
44242
  // src/tools/reading.ts
43885
44243
  var z11 = tool.schema;
44244
+ function isGithubPathArg(value) {
44245
+ return typeof value === "string" && (value.startsWith("issue://") || value.startsWith("pr://"));
44246
+ }
43886
44247
  function buildZoomTitle(args) {
43887
44248
  const targets = args.targets;
43888
44249
  if (!isEmptyParam(targets)) {
@@ -43906,6 +44267,13 @@ function buildZoomTitle(args) {
43906
44267
  }
43907
44268
  return path || "(no target)";
43908
44269
  }
44270
+ function outlineFocusSteer(config) {
44271
+ const zoomEnabled = toolEnabled(config, "aft_zoom");
44272
+ const reader = zoomEnabled ? "aft_zoom" : "read";
44273
+ const locate = toolEnabled(config, "aft_search") ? `aft_search + ${reader}` : reader;
44274
+ 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." : "";
44275
+ 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}`;
44276
+ }
43909
44277
  function readingTools(ctx) {
43910
44278
  const zoomEnabled = toolEnabled(ctx.config, "aft_zoom");
43911
44279
  const ghReadEnabled = resolveGithubConfig(ctx.config).read;
@@ -43913,9 +44281,9 @@ function readingTools(ctx) {
43913
44281
  const githubZoomDescription = whenGhReadEnabled(ghReadEnabled, "GitHub issue and pull-request discussion ordinals can be zoomed with `path: issue://…` or `path: pr://…` and `symbols`.");
43914
44282
  const tools = prepareToolMap({
43915
44283
  aft_outline: {
43916
- 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.\n\n" + (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.\n\n" : `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.
44284
+ 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.\n\n" + outlineFocusSteer(ctx.config) + `
43917
44285
 
43918
- `) + "Pass a single `target`:\n" + ` • file path → outline that file (with signatures)
44286
+ ` + "Pass a single `target`:\n" + ` • file path → outline that file (with signatures)
43919
44287
  ` + ` • directory path → outline source files under it
43920
44288
  ` + ` • URL (http:// or https://) → fetch and outline a remote HTML/Markdown document
43921
44289
  ` + " • array of paths → outline multiple files in one call; with files:true, every path must be a directory" + (githubOutlineDescription ? `
@@ -43964,7 +44332,7 @@ ${githubOutlineDescription}` : "") + "\n\nWhen a list is cut, the reply ends wit
43964
44332
  }
43965
44333
  },
43966
44334
  aft_zoom: {
43967
- 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 ? `
44335
+ 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 ? `
43968
44336
 
43969
44337
  ${githubZoomDescription}` : ""),
43970
44338
  args: {
@@ -43980,37 +44348,22 @@ ${githubZoomDescription}` : ""),
43980
44348
  path: z11.string().describe("Path to file (absolute or relative to project root)"),
43981
44349
  symbol: z11.string().describe("Symbol name in that file")
43982
44350
  }))
43983
- ]).optional().describe("Cross-file batch: `{ path, symbol }` or an array of them. Mutually exclusive with path/url/symbols."),
44351
+ ]).optional().describe("Cross-file batch: `{ path, symbol }` or an array of them. May be combined with path/url + symbols; all lookups are answered in one batch."),
43984
44352
  contextLines: optionalInt(1, Number.MAX_SAFE_INTEGER).describe("Lines of context before/after the symbol (default: 3)"),
43985
44353
  callgraph: z11.boolean().optional().describe("Include call-graph annotations (calls-out / called-by within the same file). Default false; off keeps zoom output minimal.")
43986
44354
  },
43987
44355
  execute: async (args, context) => {
43988
- const hasTargetsProvided = (t) => {
43989
- if (isEmptyParam(t))
43990
- return false;
43991
- const entryEmpty = (entry) => {
43992
- if (!entry || typeof entry !== "object")
43993
- return true;
43994
- const fp = entry.path;
43995
- const sym = entry.symbol;
43996
- const fpEmpty = typeof fp !== "string" || fp.length === 0;
43997
- const symEmpty = typeof sym !== "string" || sym.length === 0;
43998
- return fpEmpty && symEmpty;
43999
- };
44000
- if (Array.isArray(t))
44001
- return !t.every(entryEmpty);
44002
- return !entryEmpty(t);
44003
- };
44004
44356
  const targetsInput = coerceJsonCollectionParam(args.targets, "targets");
44005
- const hasFilePath = !isEmptyParam(args.path);
44006
- const hasUrl = !isEmptyParam(args.url);
44007
- const hasTargets = hasTargetsProvided(targetsInput);
44008
- const hasSymbols = !isEmptyParam(args.symbols);
44357
+ const targetEntries = usableZoomTargets(targetsInput);
44358
+ const hasFilePath = !isBlankParam(args.path);
44359
+ const hasUrl = !isBlankParam(args.url);
44360
+ const hasTargets = targetEntries.length > 0;
44361
+ const hasSymbols = !isBlankParam(args.symbols);
44009
44362
  const wantCallgraph = coerceBoolean(args.callgraph);
44010
44363
  const contextLines = coerceOptionalInt(args.contextLines, "contextLines", 1, Number.MAX_SAFE_INTEGER);
44011
44364
  const zoomTitle = buildZoomTitle({
44012
44365
  ...args,
44013
- targets: targetsInput
44366
+ targets: hasTargets ? targetEntries : undefined
44014
44367
  });
44015
44368
  const zoomDisplay = { title: zoomTitle };
44016
44369
  if (hasFilePath)
@@ -44021,7 +44374,7 @@ ${githubZoomDescription}` : ""),
44021
44374
  zoomDisplay.symbols = typeof args.symbols === "string" ? args.symbols : JSON.stringify(args.symbols);
44022
44375
  }
44023
44376
  if (hasTargets)
44024
- zoomDisplay.targets = JSON.stringify(targetsInput);
44377
+ zoomDisplay.targets = JSON.stringify(targetEntries);
44025
44378
  if (contextLines !== undefined)
44026
44379
  zoomDisplay.contextLines = contextLines;
44027
44380
  if (wantCallgraph)
@@ -44032,23 +44385,24 @@ ${githubZoomDescription}` : ""),
44032
44385
  metadata: zoomDisplay
44033
44386
  });
44034
44387
  if (hasTargets) {
44035
- if (hasFilePath || hasUrl || hasSymbols) {
44036
- throw new Error("'targets' is mutually exclusive with 'path', 'url', and 'symbols'");
44037
- }
44038
- const targets = Array.isArray(targetsInput) ? targetsInput : [targetsInput];
44039
- if (targets.length === 0) {
44040
- throw new Error("'targets' must be a non-empty object or array");
44388
+ if (hasFilePath && hasUrl) {
44389
+ throw new Error("Provide exactly ONE of 'path' or 'url' — not both");
44041
44390
  }
44042
- for (const [i, entry] of targets.entries()) {
44043
- const targetPath = entry?.path;
44044
- if (typeof targetPath !== "string" || targetPath.length === 0) {
44391
+ const targets = targetEntries.map((entry, i) => {
44392
+ if (!entry || typeof entry !== "object" || Array.isArray(entry)) {
44045
44393
  throw new Error(`targets[${i}].path must be a non-empty string`);
44046
44394
  }
44047
- if (typeof entry.symbol !== "string" || entry.symbol.length === 0) {
44048
- throw new Error(`targets[${i}].symbol must be a non-empty string`);
44049
- }
44050
- }
44051
- const resolvedTargets = await Promise.all(targets.map((t) => resolvePathArg(ctx, context, t.path)));
44395
+ const record = entry;
44396
+ const targetPath = isBlankParam(record.path) ? record.filePath : record.path;
44397
+ return {
44398
+ path: typeof targetPath === "string" ? targetPath : "",
44399
+ symbol: typeof record.symbol === "string" ? record.symbol : ""
44400
+ };
44401
+ });
44402
+ const localPaths = targets.map((target) => target.path).filter((path) => path.trim().length > 0 && !isGithubPathArg(path));
44403
+ if (hasFilePath && !isGithubPathArg(args.path))
44404
+ localPaths.push(args.path);
44405
+ const resolvedTargets = await Promise.all(localPaths.map((path) => resolvePathArg(ctx, context, path)));
44052
44406
  const permissionDenied = await assertPathExternalPermissions(ctx, context, resolvedTargets);
44053
44407
  if (permissionDenied)
44054
44408
  return permissionDeniedResponse(permissionDenied);
@@ -44058,6 +44412,12 @@ ${githubZoomDescription}` : ""),
44058
44412
  symbol: target.symbol
44059
44413
  }))
44060
44414
  };
44415
+ if (hasFilePath)
44416
+ rawArgs.filePath = args.path;
44417
+ else if (hasUrl)
44418
+ rawArgs.url = args.url;
44419
+ if (hasSymbols)
44420
+ rawArgs.symbols = args.symbols;
44061
44421
  if (contextLines !== undefined)
44062
44422
  rawArgs.contextLines = contextLines;
44063
44423
  if (wantCallgraph)
@@ -44074,7 +44434,7 @@ ${githubZoomDescription}` : ""),
44074
44434
  if (hasFilePath && hasUrl) {
44075
44435
  throw new Error("Provide exactly ONE of 'path' or 'url' — not both");
44076
44436
  }
44077
- const githubPath = hasFilePath && (String(args.path).startsWith("issue://") || String(args.path).startsWith("pr://")) || hasUrl && (String(args.url).startsWith("issue://") || String(args.url).startsWith("pr://"));
44437
+ const githubPath = hasFilePath && isGithubPathArg(args.path) || hasUrl && isGithubPathArg(args.url);
44078
44438
  if (!hasUrl && !githubPath) {
44079
44439
  const file = await resolvePathArg(ctx, context, args.path);
44080
44440
  const permissionDenied = await assertPathExternalPermissions(ctx, context, file);
@@ -44165,7 +44525,7 @@ function safetyTools(ctx) {
44165
44525
  ` + "Use checkpoint before risky multi-file changes. Use undo for quick single-file rollback.",
44166
44526
  args: {
44167
44527
  op: z12.enum(["undo", "history", "checkpoint", "restore", "list"]).describe("Safety operation"),
44168
- path: z12.string().optional().describe("File path (required for history, optional for undo, optional for restore — restores only that file). Absolute or relative to project root"),
44528
+ path: z12.string().optional().describe("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"),
44169
44529
  name: z12.string().optional().describe("Checkpoint name (required for checkpoint, restore)"),
44170
44530
  files: z12.array(z12.string()).optional().describe("Specific files to include in checkpoint, or to restore from a checkpoint (optional; restore without path/files restores the whole checkpoint; explicit checkpoint files may be untracked or gitignored)")
44171
44531
  },
@@ -44601,8 +44961,20 @@ function buildAftToolDefinitions(ctx, config, onUnknownDisabled) {
44601
44961
  const unknown = unknownDisabledTools(disabled);
44602
44962
  if (unknown.length > 0)
44603
44963
  onUnknownDisabled?.(unknown);
44964
+ describeBashForRegisteredSurface(allTools, config);
44604
44965
  return allTools;
44605
44966
  }
44967
+ function describeBashForRegisteredSurface(allTools, config) {
44968
+ const bash = allTools.bash;
44969
+ if (!bash)
44970
+ return;
44971
+ const bashCfg = resolveBashConfig(config);
44972
+ bash.description = bashToolDescription("aft_search" in allTools, bashCfg.compress, bashCfg.background, bashCfg.detach_on_user_message, "aft_zoom" in allTools, "bash_watch" in allTools, {
44973
+ outline: "aft_outline" in allTools,
44974
+ status: "bash_status" in allTools,
44975
+ write: "bash_write" in allTools
44976
+ });
44977
+ }
44606
44978
  function registerAftTools(context, location, definitions, consumers = {}) {
44607
44979
  const projected = Object.entries(definitions).map(([name, definition]) => projectV2Tool(name, definition, location, consumers));
44608
44980
  return context.tool.transform((editor) => {
@@ -47313,7 +47685,10 @@ function registerShutdownCleanup(fn) {
47313
47685
 
47314
47686
  // src/workflow-hints.ts
47315
47687
  var HEADING = "## IMPORTANT NOTICE about your tools";
47316
- 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.";
47688
+ function hashlineTagSourceHint(bashRewritesMintTags, isRegistered = () => true) {
47689
+ return `**Hashline edit tags**: ${hashlineTagSourceSentence(bashRewritesMintTags, isRegistered)}`;
47690
+ }
47691
+ var HASHLINE_TAG_SOURCE_HINT = hashlineTagSourceHint(true);
47317
47692
  function buildWorkflowHints(opts) {
47318
47693
  const sections = [];
47319
47694
  const grepName = "grep";
@@ -47330,6 +47705,8 @@ function buildWorkflowHints(opts) {
47330
47705
  const hasInspect = !opts.disabledTools.has("aft_inspect");
47331
47706
  const hasBash = !opts.disabledTools.has(bashName);
47332
47707
  const hasBgBash = hasBash && opts.bashBackgroundEnabled && !opts.disabledTools.has(bashStatusName);
47708
+ const hasWatch = !opts.disabledTools.has("bash_watch");
47709
+ const hasWrite = !opts.disabledTools.has(bashWriteName);
47333
47710
  if (hasBash && opts.bashCompressionEnabled) {
47334
47711
  sections.push([
47335
47712
  "**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:",
@@ -47371,17 +47748,19 @@ function buildWorkflowHints(opts) {
47371
47748
  ].join(`
47372
47749
  `));
47373
47750
  }
47374
- if (hasBash && hasBgBash) {
47751
+ if (hasBgBash && hasWatch) {
47375
47752
  sections.push([
47376
47753
  `**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.`,
47377
47754
  "- `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.",
47378
47755
  "- `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."
47379
47756
  ].join(`
47380
47757
  `));
47758
+ }
47759
+ if (hasBgBash && hasWrite) {
47381
47760
  sections.push(`**PTY / interactive commands**: PTY mode is for interactive REPLs and terminal apps (python, node, bash itself, vim). Start with \`${bashName}({ command: "python", pty: true, background: true })\`, read the screen with \`${bashStatusName}({ taskId, outputMode: "screen" })\`, and send input with \`${bashWriteName}({ taskId, input: "..." })\`.`);
47382
47761
  }
47383
47762
  if (opts.hashlineEffective === true) {
47384
- sections.push(HASHLINE_TAG_SOURCE_HINT);
47763
+ sections.push(hashlineTagSourceHint(hasBash && opts.bashRewriteEnabled !== false, (name) => !opts.disabledTools.has(name)));
47385
47764
  }
47386
47765
  if (sections.length === 0) {
47387
47766
  return null;
@@ -47396,13 +47775,32 @@ ${sections.join(`
47396
47775
  `)}`;
47397
47776
  }
47398
47777
  function buildHintsFromConfig(config, disabledTools, hashlineEffective = false) {
47778
+ const bashCfg = resolveBashConfig(config);
47399
47779
  return buildWorkflowHints({
47400
- bashBackgroundEnabled: resolveBashConfig(config).background,
47401
- bashCompressionEnabled: resolveBashConfig(config).compress,
47780
+ bashBackgroundEnabled: bashCfg.background,
47781
+ bashCompressionEnabled: bashCfg.compress,
47782
+ bashRewriteEnabled: bashCfg.rewrite,
47402
47783
  disabledTools,
47403
47784
  hashlineEffective
47404
47785
  });
47405
47786
  }
47787
+ var WORKFLOW_HINT_TOOL_NAMES = [
47788
+ "aft_outline",
47789
+ "aft_zoom",
47790
+ "aft_search",
47791
+ "aft_callgraph",
47792
+ "aft_inspect",
47793
+ "grep",
47794
+ "read",
47795
+ "bash",
47796
+ "bash_status",
47797
+ "bash_watch",
47798
+ "bash_write"
47799
+ ];
47800
+ function buildHintsForRegisteredTools(config, registeredTools, hashlineEffective = false) {
47801
+ const absent = new Set(WORKFLOW_HINT_TOOL_NAMES.filter((name) => !registeredTools.has(name)));
47802
+ return buildHintsFromConfig(config, absent, hashlineEffective);
47803
+ }
47406
47804
  function appendHintsToSystem(system, hintsBlock) {
47407
47805
  if (!hintsBlock)
47408
47806
  return;
@@ -47773,36 +48171,13 @@ Install: ${getManualInstallHint()}`);
47773
48171
  signal: autoUpdateAbort.signal,
47774
48172
  storageDir: ctx.storageDir
47775
48173
  });
47776
- const HINTS_TOOL_NAMES = [
47777
- "aft_outline",
47778
- "aft_zoom",
47779
- "aft_search",
47780
- "aft_callgraph",
47781
- "aft_inspect",
47782
- "grep",
47783
- "read",
47784
- "bash",
47785
- "bash_status"
47786
- ];
47787
48174
  const registeredTools = new Set(Object.keys(allTools));
47788
48175
  const { hashlineEditRegistered, aftSearchRegistered } = applyToolSurfaceOverrides(pool, aftConfig, registeredTools);
47789
48176
  ctx.hashlineEffective = hashlineEditRegistered;
47790
48177
  const hashlineDowngrade = reportHashlineDowngrade(aftConfig, registeredTools, (message) => deliverConfigMigrationWarnings(registrationRoot, [message]));
47791
48178
  log2(`hashline activation decision requested=${aftConfig.edit_mode === "hashline"} ` + `edit_slot_survives=${hashlineEditRegistered} effective=${ctx.hashlineEffective}` + (hashlineDowngrade ? ` downgraded=${hashlineDowngrade.code}` : ""));
47792
48179
  ctx.aftSearchRegistered = aftSearchRegistered;
47793
- for (const name of ["bash"]) {
47794
- const def = allTools[name];
47795
- if (def) {
47796
- const bashCfg = resolveBashConfig(aftConfig);
47797
- def.description = bashToolDescription(aftSearchRegistered, bashCfg.compress, bashCfg.background, bashCfg.detach_on_user_message);
47798
- }
47799
- }
47800
- const hintsAbsentTools = new Set;
47801
- for (const name of HINTS_TOOL_NAMES) {
47802
- if (!registeredTools.has(name))
47803
- hintsAbsentTools.add(name);
47804
- }
47805
- const hintsBlock = buildHintsFromConfig(aftConfig, hintsAbsentTools, hashlineEditRegistered);
48180
+ const hintsBlock = buildHintsForRegisteredTools(aftConfig, registeredTools, hashlineEditRegistered);
47806
48181
  if (hintsBlock) {
47807
48182
  log2(`Workflow hints injected (${hintsBlock.length} chars)`);
47808
48183
  }