@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
@@ -9574,29 +9574,84 @@ function isTerminalStatus(status) {
9574
9574
  function sleep(ms) {
9575
9575
  return new Promise((resolve) => setTimeout(resolve, ms));
9576
9576
  }
9577
- function monotonicNowMs() {
9578
- return performance.now();
9579
- }
9580
9577
  // ../aft-bridge/dist/bash-hints.js
9581
9578
  import * as fs from "node:fs";
9582
9579
  import * as os from "node:os";
9583
9580
  import * as path from "node:path";
9584
9581
  var DEFAULT_PRIMARY_WATCH_TIMEOUT_MS = 30000;
9585
- 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).";
9582
+ var MAX_WATCH_TIMEOUT_MS = 1800000;
9583
+ var LONGEST_TIMER_DELAY_MS = 2147483647;
9584
+ 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";
9585
+ 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.";
9586
9586
  function resolveWatchTimeoutMs(requestedMs, role, capMs) {
9587
- const fallback = role === "worker" ? capMs : DEFAULT_PRIMARY_WATCH_TIMEOUT_MS;
9588
- return Math.min(requestedMs ?? fallback, capMs);
9587
+ if (role === "worker")
9588
+ return requestedMs;
9589
+ return Math.min(requestedMs ?? DEFAULT_PRIMARY_WATCH_TIMEOUT_MS, capMs);
9590
+ }
9591
+ function maxWatchTimeoutMs(role, capMs) {
9592
+ return role === "worker" ? MAX_WATCH_TIMEOUT_MS : capMs;
9593
+ }
9594
+ function watchPollDelayMs(elapsedMs) {
9595
+ if (elapsedMs < 5000)
9596
+ return 100;
9597
+ if (elapsedMs < 30000)
9598
+ return 250;
9599
+ if (elapsedMs < 120000)
9600
+ return 500;
9601
+ return 1000;
9602
+ }
9603
+ var watchClock = {
9604
+ now: () => performance.now(),
9605
+ sleep: abortableSleep
9606
+ };
9607
+ function abortableSleep(ms, signal) {
9608
+ if (signal?.aborted)
9609
+ return Promise.resolve();
9610
+ return new Promise((resolve) => {
9611
+ const done = () => {
9612
+ clearTimeout(timer);
9613
+ signal?.removeEventListener("abort", done);
9614
+ resolve();
9615
+ };
9616
+ const timer = setTimeout(done, ms);
9617
+ signal?.addEventListener("abort", done, { once: true });
9618
+ });
9589
9619
  }
9590
9620
  function formatWatchWaited(elapsedMs, limitMs, capMs) {
9591
- const limit = limitMs >= capMs ? `limit ${limitMs}ms, the bash.watch_sync_max_ms cap` : `limit ${limitMs}ms`;
9621
+ let limit;
9622
+ if (limitMs === undefined)
9623
+ limit = "no limit: waits until the command finishes";
9624
+ else if (capMs !== undefined && limitMs >= capMs)
9625
+ limit = `limit ${limitMs}ms, the bash.watch_sync_max_ms cap`;
9626
+ else
9627
+ limit = `limit ${limitMs}ms`;
9592
9628
  return `Waited ${Math.round(elapsedMs)}ms (${limit})`;
9593
9629
  }
9594
- function watchTimeoutSteer(role, capMs, timeoutParam = "timeoutMs") {
9630
+ function watchTimeoutSteer(role, timeoutParam = "timeoutMs") {
9595
9631
  if (role === "worker") {
9596
- 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.`;
9632
+ 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.`;
9597
9633
  }
9598
9634
  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.";
9599
9635
  }
9636
+ var WATCH_UNAVAILABLE_GIVE_UP_MS = 120000;
9637
+ var WORKER_KEEP_WAITING = "Call bash_watch again to keep waiting; don't report a result until the command finishes.";
9638
+ function interruptedWatchTail(role, primaryTail) {
9639
+ return role === "worker" ? `The task is still running. ${WORKER_KEEP_WAITING}` : primaryTail;
9640
+ }
9641
+ function workerBackgroundTaskNote(taskId, taskIdArg = "taskId") {
9642
+ return `
9643
+
9644
+ 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.`;
9645
+ }
9646
+ function runningTaskStatusHint(role) {
9647
+ return role === "worker" ? "To wait for it, call bash_watch; don't poll." : "A completion reminder will be delivered automatically; don't poll.";
9648
+ }
9649
+ function watchUnavailableSteer(role) {
9650
+ const unknown = "the bridge was busy, so task state is unknown.";
9651
+ if (role === "worker")
9652
+ return `${unknown} ${WORKER_KEEP_WAITING}`;
9653
+ 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.`;
9654
+ }
9600
9655
  var CONFLICT_HINT = `
9601
9656
 
9602
9657
  [Hint] Use aft_conflicts to see all conflict regions across files in a single call.`;
@@ -10455,6 +10510,9 @@ function isPassiveCommand(command) {
10455
10510
  return PASSIVE_COMMANDS.has(command);
10456
10511
  }
10457
10512
 
10513
+ // ../aft-bridge/dist/transport.js
10514
+ var WORKER_SESSION_FIELD = "worker_session";
10515
+
10458
10516
  // ../aft-bridge/dist/bridge.js
10459
10517
  var DEFAULT_BRIDGE_TIMEOUT_MS = 30000;
10460
10518
  var BRIDGE_HANG_TIMEOUT_THRESHOLD = 2;
@@ -10873,9 +10931,11 @@ class BinaryBridge {
10873
10931
  params.edit_slot_survives = editSlotSurvives;
10874
10932
  this.logHashlineRegistrationCarrier("tool_call", sessionId, editSlotSurvives);
10875
10933
  }
10876
- const { preview, ...sendOptions } = options ?? {};
10934
+ const { preview, workerSession, ...sendOptions } = options ?? {};
10877
10935
  if (preview === true)
10878
10936
  params.preview = true;
10937
+ if (workerSession === true)
10938
+ params[WORKER_SESSION_FIELD] = true;
10879
10939
  return await this.send("tool_call", params, Object.keys(sendOptions).length > 0 ? sendOptions : undefined);
10880
10940
  }
10881
10941
  async sendWithVersionMismatchRetry(command, params, options, canRetryAfterVersionSwap) {
@@ -10944,7 +11004,7 @@ class BinaryBridge {
10944
11004
  if (command === "cancel_request" || Object.hasOwn(params, "command") || Object.hasOwn(params, "method")) {
10945
11005
  const nested = { ...params };
10946
11006
  const reserved = {};
10947
- for (const key of ["session_id", "lsp_hints"]) {
11007
+ for (const key of ["session_id", "lsp_hints", WORKER_SESSION_FIELD]) {
10948
11008
  if (Object.hasOwn(nested, key)) {
10949
11009
  reserved[key] = nested[key];
10950
11010
  delete nested[key];
@@ -11722,6 +11782,35 @@ function coerceJsonCollectionParam(value, field) {
11722
11782
  }
11723
11783
  throw new Error(`${field} must be ${shape} (got an invalid type)`);
11724
11784
  }
11785
+ function isBlankParam(value) {
11786
+ if (typeof value === "string")
11787
+ return value.trim().length === 0;
11788
+ return isEmptyParam(value);
11789
+ }
11790
+ function usableZoomTargets(targets) {
11791
+ if (isEmptyParam(targets))
11792
+ return [];
11793
+ const entries = Array.isArray(targets) ? targets : [targets];
11794
+ return entries.filter((entry) => {
11795
+ if (entry === null || entry === undefined)
11796
+ return false;
11797
+ if (typeof entry !== "object")
11798
+ return true;
11799
+ const record = entry;
11800
+ return !(isBlankParam(record.path) && isBlankParam(record.filePath) && isBlankParam(record.symbol));
11801
+ });
11802
+ }
11803
+ function isFindReplaceOnlyEdit(args) {
11804
+ const absent = (value) => value === undefined || value === null || value === "";
11805
+ const absentLine = (value) => absent(value) || value === 0;
11806
+ const edits = args.edits;
11807
+ return Array.isArray(edits) && edits.length > 0 && absent(args.appendContent) && absent(args.symbol) && absent(args.content) && edits.every((entry) => {
11808
+ if (entry === null || typeof entry !== "object")
11809
+ return false;
11810
+ const item = entry;
11811
+ return typeof item.oldString === "string" && (absent(item.newString) || typeof item.newString === "string") && absentLine(item.startLine) && absentLine(item.endLine) && absent(item.content);
11812
+ });
11813
+ }
11725
11814
  // ../aft-bridge/dist/config-error.js
11726
11815
  import { homedir as homedir5 } from "node:os";
11727
11816
  import { isAbsolute as isAbsolute2, join as join6 } from "node:path";
@@ -14984,6 +15073,13 @@ class SubcTransportShuttingDownError extends SubcCallError {
14984
15073
  this.name = "SubcTransportShuttingDownError";
14985
15074
  }
14986
15075
  }
15076
+
15077
+ class ReconnectKeyChangedError extends Error {
15078
+ constructor() {
15079
+ super("the subc daemon restarted and its connection key changed; the reconnect did not complete within the deadline, retry");
15080
+ this.name = "ReconnectKeyChangedError";
15081
+ }
15082
+ }
14987
15083
  function isSubcClientClosedError(error) {
14988
15084
  return error instanceof SubcError && error.message === "client closed";
14989
15085
  }
@@ -15486,6 +15582,8 @@ class SubcTransport {
15486
15582
  body.edit_slot_survives = editSlotSurvives;
15487
15583
  if (preview === true)
15488
15584
  body.preview = true;
15585
+ if (options?.workerSession === true)
15586
+ body[WORKER_SESSION_FIELD] = true;
15489
15587
  const reply = await this.pool.routeRequest(this.identityFor(sessionId), body, timeoutMs, onProgress, this.generation, abortSignal);
15490
15588
  return reliftReply(reply);
15491
15589
  }
@@ -15496,7 +15594,10 @@ class SubcTransport {
15496
15594
  }
15497
15595
  const { timeoutMs, onProgress, abortSignal } = this.splitOptions(options);
15498
15596
  const session = typeof params.session_id === "string" ? params.session_id : undefined;
15499
- const body = { name: command, arguments: params };
15597
+ const { [WORKER_SESSION_FIELD]: workerSession, ...args } = params;
15598
+ const body = { name: command, arguments: args };
15599
+ if (workerSession === true)
15600
+ body[WORKER_SESSION_FIELD] = true;
15500
15601
  const editSlotSurvives = this.pool.getEditSlotSurvives();
15501
15602
  if (editSlotSurvives !== undefined)
15502
15603
  body.edit_slot_survives = editSlotSurvives;
@@ -15533,6 +15634,7 @@ class SubcTransportPool {
15533
15634
  lifecycleRegistration = null;
15534
15635
  outerFacadeEvictor;
15535
15636
  client = null;
15637
+ hadWorkingClient = false;
15536
15638
  connecting = null;
15537
15639
  routeReopenRetryDelayMs = RECONNECT_RETRY_FLOOR_MS;
15538
15640
  routeReopenRetry = null;
@@ -15977,6 +16079,10 @@ class SubcTransportPool {
15977
16079
  }
15978
16080
  async routeRequest(identity, body, timeoutMs, onProgress, expectedGeneration, abortSignal) {
15979
16081
  const callStartedAt = performance.now();
16082
+ const callDeadlineMs = timeoutMs ?? SUBC_DEFAULT_REQUEST_TIMEOUT_MS;
16083
+ let scheduledDelayMs = 0;
16084
+ const spentMs = () => Math.max(performance.now() - callStartedAt, scheduledDelayMs);
16085
+ const remainingMs = () => callDeadlineMs - spentMs();
15980
16086
  const root = asCanonicalRootPath(identity.project_root);
15981
16087
  let generation = expectedGeneration;
15982
16088
  if (this.lifecycleEnabled()) {
@@ -15992,7 +16098,49 @@ class SubcTransportPool {
15992
16098
  try {
15993
16099
  let client;
15994
16100
  try {
15995
- client = await this.ensureClient();
16101
+ let keyChanged;
16102
+ while (true) {
16103
+ const remaining = remainingMs();
16104
+ if (remaining <= 0)
16105
+ throw keyChanged ?? callDeadlinePassedError(undefined, callDeadlineMs);
16106
+ let timer;
16107
+ try {
16108
+ const acquired = await Promise.race([
16109
+ this.ensureClient(),
16110
+ new Promise((resolve) => {
16111
+ timer = setTimeout(() => resolve(CALL_DEADLINE_EXPIRED), remaining);
16112
+ })
16113
+ ]);
16114
+ if (acquired === CALL_DEADLINE_EXPIRED) {
16115
+ throw keyChanged ?? callDeadlinePassedError(undefined, callDeadlineMs);
16116
+ }
16117
+ client = acquired;
16118
+ break;
16119
+ } catch (error) {
16120
+ if (!(error instanceof ReconnectKeyChangedError))
16121
+ throw error;
16122
+ keyChanged = error;
16123
+ if (spentMs() + this.nextRouteReopenDelayMs() >= callDeadlineMs)
16124
+ throw error;
16125
+ } finally {
16126
+ clearTimeout(timer);
16127
+ }
16128
+ const { delayMs, wait } = this.waitForRouteReopenBackoff();
16129
+ scheduledDelayMs = spentMs() + delayMs;
16130
+ let retryTimer;
16131
+ try {
16132
+ const waited = await Promise.race([
16133
+ wait,
16134
+ new Promise((resolve) => {
16135
+ retryTimer = setTimeout(() => resolve(CALL_DEADLINE_EXPIRED), callDeadlineMs - (performance.now() - callStartedAt));
16136
+ })
16137
+ ]);
16138
+ if (waited === CALL_DEADLINE_EXPIRED)
16139
+ throw keyChanged;
16140
+ } finally {
16141
+ clearTimeout(retryTimer);
16142
+ }
16143
+ }
15996
16144
  this.assertRecordLive(record);
15997
16145
  } catch (error) {
15998
16146
  throw this.annotateReapError(error, record);
@@ -16027,12 +16175,8 @@ class SubcTransportPool {
16027
16175
  throw error;
16028
16176
  }
16029
16177
  };
16030
- const callDeadlineMs = timeoutMs ?? SUBC_DEFAULT_REQUEST_TIMEOUT_MS;
16031
16178
  const reloadWaitBudgetMs = Math.min(callDeadlineMs, ROUTE_OPEN_RELOAD_WAIT_CEILING_MS);
16032
- let scheduledDelayMs = 0;
16033
16179
  let lastRefusal;
16034
- const spentMs = () => Math.max(performance.now() - callStartedAt, scheduledDelayMs);
16035
- const remainingMs = () => callDeadlineMs - spentMs();
16036
16180
  const openRouteWithinDeadline = async () => {
16037
16181
  const remaining = remainingMs();
16038
16182
  if (remaining <= 0)
@@ -16235,10 +16379,14 @@ class SubcTransportPool {
16235
16379
  throw new SubcTransportShuttingDownError;
16236
16380
  }
16237
16381
  this.client = client;
16382
+ this.hadWorkingClient = true;
16238
16383
  this.transportFailures = 0;
16239
16384
  return client;
16240
16385
  }).catch((error) => {
16241
16386
  this.connecting = null;
16387
+ if (this.hadWorkingClient && error instanceof AuthError) {
16388
+ throw new ReconnectKeyChangedError;
16389
+ }
16242
16390
  throw error;
16243
16391
  });
16244
16392
  return this.connecting;
@@ -17930,6 +18078,7 @@ async function acquireBridge(locationDirectory, options, dependencies = {}) {
17930
18078
  let owner = state.standalone;
17931
18079
  if (!owner) {
17932
18080
  const configByRoot = new Map;
18081
+ const versionMismatchHandlers = new Map;
17933
18082
  const configuredLoader = options.poolOptions.projectConfigLoader;
17934
18083
  const pool = await createPool({
17935
18084
  ...options,
@@ -17939,21 +18088,29 @@ async function acquireBridge(locationDirectory, options, dependencies = {}) {
17939
18088
  projectConfigLoader: (projectRoot) => ({
17940
18089
  ...configuredLoader?.(projectRoot) ?? {},
17941
18090
  ...configByRoot.get(canonicalizeProjectRoot(projectRoot)) ?? {}
17942
- })
18091
+ }),
18092
+ onVersionMismatch: (binaryVersion, minVersion) => [...versionMismatchHandlers.values()].at(-1)?.(binaryVersion, minVersion) ?? null
17943
18093
  }
17944
18094
  });
17945
- owner = { pool, configByRoot, referencesByRoot: new Map, references: 0 };
18095
+ owner = {
18096
+ pool,
18097
+ configByRoot,
18098
+ versionMismatchHandlers,
18099
+ referencesByRoot: new Map,
18100
+ references: 0
18101
+ };
17946
18102
  state.standalone = owner;
17947
18103
  }
17948
18104
  owner.references += 1;
17949
18105
  owner.referencesByRoot.set(directory, (owner.referencesByRoot.get(directory) ?? 0) + 1);
17950
18106
  owner.configByRoot.set(directory, { ...options.configOverrides });
17951
18107
  const acquiredOwner = owner;
17952
- return new LocationBridgeLease(directory, mode, owner.pool, async () => {
18108
+ const lease = new LocationBridgeLease(directory, mode, owner.pool, async () => {
17953
18109
  await withLock(directory, () => withLock(OWNER_LOCK, async () => {
17954
18110
  const current = lifecycleState().standalone;
17955
18111
  if (current !== acquiredOwner)
17956
18112
  return;
18113
+ current.versionMismatchHandlers?.delete(lease);
17957
18114
  current.references -= 1;
17958
18115
  const rootReferences = (current.referencesByRoot.get(directory) ?? 1) - 1;
17959
18116
  if (rootReferences > 0)
@@ -17968,6 +18125,10 @@ async function acquireBridge(locationDirectory, options, dependencies = {}) {
17968
18125
  await current.pool.shutdown();
17969
18126
  }));
17970
18127
  });
18128
+ const onVersionMismatch = options.poolOptions.onVersionMismatch;
18129
+ if (onVersionMismatch)
18130
+ owner.versionMismatchHandlers?.set(lease, onVersionMismatch);
18131
+ return lease;
17971
18132
  });
17972
18133
  });
17973
18134
  }
@@ -17988,6 +18149,14 @@ var STANDALONE_DETACH_KEYWORDS_PATTERN = new RegExp(STANDALONE_DETACH_KEYWORD_SO
17988
18149
  function containsStandaloneDetachKeyword(messageText) {
17989
18150
  return STANDALONE_DETACH_KEYWORD_PATTERN.test(messageText);
17990
18151
  }
18152
+ function standaloneDetachKeywordRanges(messageText) {
18153
+ const ranges = [];
18154
+ for (const match of messageText.matchAll(STANDALONE_DETACH_KEYWORDS_PATTERN)) {
18155
+ const start = (match.index ?? 0) + (match[1]?.length ?? 0);
18156
+ ranges.push([start, start + BASH_WAIT_DETACH_MAGIC_KEYWORD.length]);
18157
+ }
18158
+ return ranges;
18159
+ }
17991
18160
  function stripStandaloneDetachKeywords(messageText) {
17992
18161
  return messageText.replace(STANDALONE_DETACH_KEYWORDS_PATTERN, "$1");
17993
18162
  }
@@ -19379,31 +19548,36 @@ var ORT_PLATFORM_MAP = {
19379
19548
  arm64: {
19380
19549
  assetName: `onnxruntime-osx-arm64-${ORT_VERSION}`,
19381
19550
  libName: "libonnxruntime.dylib",
19382
- archiveType: "tgz"
19551
+ archiveType: "tgz",
19552
+ librarySha256: "872533f130f1839a5bc01788ddb4f75c83a189763441ba1178788ed965449289"
19383
19553
  }
19384
19554
  },
19385
19555
  linux: {
19386
19556
  x64: {
19387
19557
  assetName: `onnxruntime-linux-x64-${ORT_VERSION}`,
19388
19558
  libName: "libonnxruntime.so",
19389
- archiveType: "tgz"
19559
+ archiveType: "tgz",
19560
+ librarySha256: "d132535d051344ff5c64c9c200004150559049a81ed330eb4422c1962fb6b7e4"
19390
19561
  },
19391
19562
  arm64: {
19392
19563
  assetName: `onnxruntime-linux-aarch64-${ORT_VERSION}`,
19393
19564
  libName: "libonnxruntime.so",
19394
- archiveType: "tgz"
19565
+ archiveType: "tgz",
19566
+ librarySha256: "52b2a0e75e79468404284fec38ad5ee1a7a996232274f5e1b84f3e793fb07554"
19395
19567
  }
19396
19568
  },
19397
19569
  win32: {
19398
19570
  x64: {
19399
19571
  assetName: `onnxruntime-win-x64-${ORT_VERSION}`,
19400
19572
  libName: "onnxruntime.dll",
19401
- archiveType: "zip"
19573
+ archiveType: "zip",
19574
+ librarySha256: "b95efb2113b603bbbf3f191061c5516a871ed546893c820e4f3b7b6c358dbf2a"
19402
19575
  },
19403
19576
  arm64: {
19404
19577
  assetName: `onnxruntime-win-arm64-${ORT_VERSION}`,
19405
19578
  libName: "onnxruntime.dll",
19406
- archiveType: "zip"
19579
+ archiveType: "zip",
19580
+ librarySha256: "724d81ac50b11bfaa01ab3ce01b99fb6734a4b762259a8be4395ca662ce99fe6"
19407
19581
  }
19408
19582
  }
19409
19583
  };
@@ -19893,6 +20067,9 @@ async function downloadOnnxRuntime(info, targetDir) {
19893
20067
  copyOnnxLibraries(info, extractedDir, stagedInstallDir, realFiles, symlinks);
19894
20068
  const libPath = join14(stagedInstallDir, info.libName);
19895
20069
  const libHash = sha256File2(libPath);
20070
+ if (libHash !== info.librarySha256) {
20071
+ throw new Error(`onnx runtime library hash mismatch at ${libPath}: sha256 ${libHash}, ` + `expected the pinned ${info.librarySha256} for ${info.assetName}`);
20072
+ }
19896
20073
  writeOnnxInstalledMeta(stagedInstallDir, ORT_VERSION, libHash, archiveSha256);
19897
20074
  publishOnnxRuntime(stagedInstallDir, targetDir);
19898
20075
  rmSync3(tmpDir, { recursive: true, force: true });
@@ -20235,12 +20412,13 @@ function normalizeZoomTargets(record) {
20235
20412
  throw new InvalidRequestError(`'targets[${index}].path' must be a non-empty string`);
20236
20413
  }
20237
20414
  const source = target;
20238
- const emptyTarget = source.symbol === "" && (hasOwn(source, "path") && source.path === "" || hasOwn(source, "filePath") && source.filePath === "");
20415
+ const blank = (value) => value === undefined || value === null || typeof value === "string" && !value.trim();
20416
+ const emptyTarget = blank(source.symbol) && blank(source.path) && blank(source.filePath);
20239
20417
  if (emptyTarget)
20240
20418
  return { ...source };
20241
20419
  const normalized = { ...source };
20242
20420
  try {
20243
- normalizeAliasPair(normalized, "path", "filePath", true);
20421
+ normalizeAliasPair(normalized, "path", "filePath", false);
20244
20422
  } catch (error) {
20245
20423
  if (error instanceof InvalidRequestError) {
20246
20424
  throw new InvalidRequestError(error.message.replace("'filePath'", `'targets[${index}].filePath'`).replace("'path'", `'targets[${index}].path'`));
@@ -25686,6 +25864,10 @@ var SubcConfigSchema = object({
25686
25864
  connection_file: string2().optional(),
25687
25865
  client_reaper: boolean2().optional()
25688
25866
  });
25867
+ var OpenCodeHostConfigSchema = object({
25868
+ server_url: string2().optional(),
25869
+ server_password_env: string2().optional()
25870
+ });
25689
25871
  var GhShimConfigSchema = object({
25690
25872
  binary_path: string2().trim().refine(isAbsolute7, "gh_shim.binary_path must be absolute").optional()
25691
25873
  });
@@ -25775,6 +25957,7 @@ var AftConfigFieldsSchema = object({
25775
25957
  auto_update: boolean2().optional(),
25776
25958
  bridge: BridgeConfigSchema.optional(),
25777
25959
  subc: SubcConfigSchema.optional(),
25960
+ opencode: OpenCodeHostConfigSchema.optional(),
25778
25961
  github: GithubConfigSchema.optional(),
25779
25962
  gh_shim: GhShimConfigSchema.optional(),
25780
25963
  git: GitConfigSchema.optional(),
@@ -26364,6 +26547,8 @@ function getStrippedTopLevelKeys(override) {
26364
26547
  stripped.push("sandbox.write_allow");
26365
26548
  if (override.subc !== undefined)
26366
26549
  stripped.push("subc");
26550
+ if (override.opencode !== undefined)
26551
+ stripped.push("opencode");
26367
26552
  if (override.github !== undefined)
26368
26553
  stripped.push("github");
26369
26554
  if (override.gh_shim !== undefined)
@@ -40223,6 +40408,14 @@ function formatDuration(completion) {
40223
40408
  }
40224
40409
 
40225
40410
  // src/shared/session-directory.ts
40411
+ import { Effect } from "effect";
40412
+ function isV2PluginContext(client) {
40413
+ if (!client || typeof client !== "object")
40414
+ return false;
40415
+ const candidate = client;
40416
+ return typeof candidate.location?.directory === "string" && typeof candidate.session?.get === "function";
40417
+ }
40418
+ var V2_SESSION_LOOKUP_TIMEOUT_MS = 3000;
40226
40419
  var CACHE_MAX_ENTRIES = 200;
40227
40420
  var cache = new Map;
40228
40421
  async function getSessionDirectory(client, sessionId, fallbackDirectory) {
@@ -40234,6 +40427,8 @@ async function getSessionDirectory(client, sessionId, fallbackDirectory) {
40234
40427
  cache.set(sessionId, cached);
40235
40428
  return cached.directory;
40236
40429
  }
40430
+ if (isV2PluginContext(client))
40431
+ return lookupV2SessionDirectory(client, sessionId);
40237
40432
  const c = client;
40238
40433
  const sessionApi = c?.session;
40239
40434
  if (!sessionApi || typeof sessionApi.get !== "function") {
@@ -40254,6 +40449,25 @@ async function getSessionDirectory(client, sessionId, fallbackDirectory) {
40254
40449
  setCache(sessionId, dir);
40255
40450
  return dir;
40256
40451
  }
40452
+ async function lookupV2SessionDirectory(context, sessionId) {
40453
+ let dir = null;
40454
+ let failure;
40455
+ try {
40456
+ const session = await Effect.runPromise(Effect.timeout(context.session.get({ sessionID: sessionId }), V2_SESSION_LOOKUP_TIMEOUT_MS));
40457
+ const directory = session?.location?.directory;
40458
+ if (typeof directory === "string" && directory.length > 0)
40459
+ dir = directory;
40460
+ else
40461
+ failure = "the session record has no location.directory";
40462
+ } catch (err) {
40463
+ failure = err instanceof Error ? err.message || err.name : String(err);
40464
+ }
40465
+ if (failure !== undefined) {
40466
+ sessionWarn(sessionId, `[aft-plugin] OpenCode 2 session lookup failed (${failure}); tool paths and bash use the Location directory ${context.location.directory} for this session`);
40467
+ }
40468
+ setCache(sessionId, dir);
40469
+ return dir;
40470
+ }
40257
40471
  function setCache(sessionId, directory) {
40258
40472
  if (cache.has(sessionId))
40259
40473
  cache.delete(sessionId);
@@ -40312,6 +40526,108 @@ async function verifySessionDirectory(client, sessionId) {
40312
40526
  return dir;
40313
40527
  }
40314
40528
 
40529
+ // src/shared/subagent-detect.ts
40530
+ import { Effect as Effect2 } from "effect";
40531
+ var CACHE_MAX_ENTRIES2 = 200;
40532
+ var LOOKUP_TIMEOUT_MS = 1000;
40533
+ var FAILED_LOOKUP_RETRY_MS = 30000;
40534
+ var cache2 = new Map;
40535
+ var inflight = new Map;
40536
+ var now = () => Date.now();
40537
+ async function resolveIsSubagent(client, sessionId, _fallbackDirectory) {
40538
+ if (!sessionId) {
40539
+ sessionLog(undefined, "[subagent-detect] no sessionId provided → primary");
40540
+ return false;
40541
+ }
40542
+ const cached = cache2.get(sessionId);
40543
+ if (cached && (cached.retryAfter === undefined || now() < cached.retryAfter)) {
40544
+ cache2.delete(sessionId);
40545
+ cache2.set(sessionId, cached);
40546
+ return cached.isSubagent;
40547
+ }
40548
+ const pending = inflight.get(sessionId);
40549
+ if (pending)
40550
+ return pending;
40551
+ const lookup = boundedLookup(client, sessionId).finally(() => {
40552
+ inflight.delete(sessionId);
40553
+ });
40554
+ inflight.set(sessionId, lookup);
40555
+ return lookup;
40556
+ }
40557
+ async function boundedLookup(client, sessionId) {
40558
+ let timer;
40559
+ const timedOut = new Promise((resolve) => {
40560
+ timer = setTimeout(() => resolve("timeout"), LOOKUP_TIMEOUT_MS);
40561
+ });
40562
+ let answer;
40563
+ try {
40564
+ answer = await Promise.race([lookupSession(client, sessionId), timedOut]);
40565
+ } finally {
40566
+ clearTimeout(timer);
40567
+ }
40568
+ if (typeof answer === "boolean") {
40569
+ setCache2(sessionId, answer);
40570
+ return answer;
40571
+ }
40572
+ if (answer === "timeout") {
40573
+ sessionWarn(sessionId, `[subagent-detect] session lookup did not answer within ${LOOKUP_TIMEOUT_MS}ms → primary for now`);
40574
+ }
40575
+ setCache2(sessionId, false, now() + FAILED_LOOKUP_RETRY_MS);
40576
+ return false;
40577
+ }
40578
+ async function lookupSession(client, sessionId) {
40579
+ if (isV2PluginContext2(client))
40580
+ return resolveV2(client, sessionId);
40581
+ const c = client;
40582
+ const sessionApi = c?.session;
40583
+ if (!sessionApi || typeof sessionApi.get !== "function") {
40584
+ sessionLog(sessionId, `[subagent-detect] client.session.get unavailable (client=${typeof client}, session=${typeof sessionApi}, get=${typeof sessionApi?.get}) → caching as primary`);
40585
+ return false;
40586
+ }
40587
+ sessionLog(sessionId, `[subagent-detect] cache miss, calling client.session.get(id=${sessionId})`);
40588
+ try {
40589
+ const result = await sessionApi.get({
40590
+ path: { id: sessionId }
40591
+ });
40592
+ const session = result?.data ?? result;
40593
+ const parentIdRaw = session?.parentID;
40594
+ const isSubagent = session !== undefined && typeof session.parentID === "string" && session.parentID.length > 0;
40595
+ sessionLog(sessionId, `[subagent-detect] SDK returned session=${session !== undefined ? "present" : "undefined"}, parentID=${JSON.stringify(parentIdRaw)} → isSubagent=${isSubagent}`);
40596
+ return isSubagent;
40597
+ } catch (err) {
40598
+ sessionWarn(sessionId, `[subagent-detect] SDK lookup failed: ${err instanceof Error ? err.message : String(err)}`);
40599
+ return "failed";
40600
+ }
40601
+ }
40602
+ function isV2PluginContext2(client) {
40603
+ if (!client || typeof client !== "object")
40604
+ return false;
40605
+ const candidate = client;
40606
+ return typeof candidate.location?.directory === "string" && typeof candidate.session?.get === "function";
40607
+ }
40608
+ async function resolveV2(context, sessionId) {
40609
+ let session;
40610
+ try {
40611
+ session = await Effect2.runPromise(context.session.get({ sessionID: sessionId }));
40612
+ } catch (err) {
40613
+ sessionWarn(sessionId, `[subagent-detect] OpenCode 2 session lookup failed: ${err instanceof Error ? err.message : String(err)}`);
40614
+ return "failed";
40615
+ }
40616
+ const isSubagent = typeof session?.parentID === "string" && session.parentID.length > 0;
40617
+ sessionLog(sessionId, `[subagent-detect] OpenCode 2 session parentID=${JSON.stringify(session?.parentID)} → isSubagent=${isSubagent}`);
40618
+ return isSubagent;
40619
+ }
40620
+ function setCache2(sessionId, isSubagent, retryAfter) {
40621
+ if (cache2.has(sessionId))
40622
+ cache2.delete(sessionId);
40623
+ cache2.set(sessionId, retryAfter === undefined ? { isSubagent } : { isSubagent, retryAfter });
40624
+ if (cache2.size > CACHE_MAX_ENTRIES2) {
40625
+ const oldest = cache2.keys().next().value;
40626
+ if (oldest !== undefined)
40627
+ cache2.delete(oldest);
40628
+ }
40629
+ }
40630
+
40315
40631
  // src/tool-perf.ts
40316
40632
  import { AsyncLocalStorage } from "node:async_hooks";
40317
40633
  var perfStore = new AsyncLocalStorage;
@@ -40387,7 +40703,7 @@ function projectRootFor(runtime) {
40387
40703
  if (typeof cached === "string" && cached.length > 0) {
40388
40704
  return canonicalizeDirectory(cached);
40389
40705
  }
40390
- const raw = runtime.worktree ?? runtime.directory;
40706
+ const raw = runtime.directoryIsSessionRoot ? runtime.directory : runtime.worktree ?? runtime.directory;
40391
40707
  return canonicalizeDirectory(raw);
40392
40708
  }
40393
40709
  async function resolveProjectRoot(ctx, runtime) {
@@ -40423,6 +40739,11 @@ function bridgeFor(ctx, runtime) {
40423
40739
  }
40424
40740
  return ctx.pool.getBridge(projectRoot);
40425
40741
  }
40742
+ async function isWorkerRuntime(ctx, runtime) {
40743
+ if (!runtime.sessionID)
40744
+ return false;
40745
+ return resolveIsSubagent(ctx.client, runtime.sessionID, runtime.directory);
40746
+ }
40426
40747
  async function callBridge(ctx, runtime, command, params = {}, options) {
40427
40748
  if (runtime.sessionID && getSessionDirectoryCached(runtime.sessionID) === undefined) {
40428
40749
  await getSessionDirectory(ctx.client, runtime.sessionID, runtime.directory);
@@ -40431,6 +40752,9 @@ async function callBridge(ctx, runtime, command, params = {}, options) {
40431
40752
  if (runtime.sessionID) {
40432
40753
  merged.session_id = runtime.sessionID;
40433
40754
  }
40755
+ if (await isWorkerRuntime(ctx, runtime)) {
40756
+ merged[WORKER_SESSION_FIELD] = true;
40757
+ }
40434
40758
  const timeoutMs = timeoutForCommand(command);
40435
40759
  const sendOptions = {
40436
40760
  ...timeoutMs !== undefined ? { timeoutMs } : {},
@@ -40457,10 +40781,12 @@ async function callToolCall(ctx, runtime, name, rawArgs = {}, options) {
40457
40781
  await getSessionDirectory(ctx.client, runtime.sessionID, runtime.directory);
40458
40782
  }
40459
40783
  const timeoutMs = timeoutForCommand(name);
40784
+ const workerSession = await isWorkerRuntime(ctx, runtime);
40460
40785
  const sendOptions = {
40461
40786
  ...timeoutMs !== undefined ? { timeoutMs } : {},
40462
40787
  configureWarningClient: ctx.client,
40463
40788
  ...runtime.effectAbort ? { abortSignal: runtime.effectAbort } : {},
40789
+ ...workerSession ? { workerSession: true } : {},
40464
40790
  ...options
40465
40791
  };
40466
40792
  markBridgeStart();
@@ -41011,7 +41337,8 @@ var NPM_LSP_TABLE = [
41011
41337
  npm: "typescript",
41012
41338
  binary: "tsserver",
41013
41339
  extensions: ["ts", "tsx", "js", "jsx", "mjs", "cjs", "mts", "cts"],
41014
- rootMarkers: ["tsconfig.json", "jsconfig.json", "package.json"]
41340
+ rootMarkers: ["tsconfig.json", "jsconfig.json", "package.json"],
41341
+ supportedMajor: 5
41015
41342
  },
41016
41343
  {
41017
41344
  id: "python",
@@ -41085,6 +41412,15 @@ var NPM_LSP_TABLE = [
41085
41412
  rootMarkers: ["biome.json", "biome.jsonc"]
41086
41413
  }
41087
41414
  ];
41415
+ function majorVersionOf(version) {
41416
+ const match = /^v?(\d+)(?:\.|$)/.exec(version.trim());
41417
+ return match ? Number(match[1]) : null;
41418
+ }
41419
+ function isVersionSupported(spec, version) {
41420
+ if (spec.supportedMajor === undefined)
41421
+ return true;
41422
+ return majorVersionOf(version) === spec.supportedMajor;
41423
+ }
41088
41424
 
41089
41425
  // src/lsp-project-relevance.ts
41090
41426
  import { existsSync as existsSync14, readdirSync as readdirSync4, readFileSync as readFileSync12 } from "node:fs";
@@ -41181,10 +41517,11 @@ function extensionOf(fileName) {
41181
41517
 
41182
41518
  // src/lsp-registry-probe.ts
41183
41519
  var NPM_REGISTRY_BASE = "https://registry.npmjs.org";
41184
- function pickEligibleVersion(response, graceDays, now = Date.now()) {
41520
+ function pickEligibleVersion(response, graceDays, now = Date.now(), accept = () => true) {
41185
41521
  const times = response.time || {};
41186
41522
  const cutoff = now - graceDays * 24 * 60 * 60 * 1000;
41187
41523
  const candidates = [];
41524
+ let releaseCount = 0;
41188
41525
  for (const [version, publishedAt] of Object.entries(times)) {
41189
41526
  if (version === "created" || version === "modified")
41190
41527
  continue;
@@ -41195,6 +41532,9 @@ function pickEligibleVersion(response, graceDays, now = Date.now()) {
41195
41532
  const ts = Date.parse(publishedAt);
41196
41533
  if (Number.isNaN(ts))
41197
41534
  continue;
41535
+ releaseCount += 1;
41536
+ if (!accept(version))
41537
+ continue;
41198
41538
  candidates.push({ version, publishedAt, ts });
41199
41539
  }
41200
41540
  candidates.sort((a, b) => b.ts - a.ts);
@@ -41203,10 +41543,11 @@ function pickEligibleVersion(response, graceDays, now = Date.now()) {
41203
41543
  return {
41204
41544
  version: eligible[0]?.version ?? null,
41205
41545
  blockedByGrace,
41546
+ outsideSupportedRange: releaseCount > 0 && candidates.length === 0,
41206
41547
  eligible: eligible.map(({ version, publishedAt }) => ({ version, publishedAt }))
41207
41548
  };
41208
41549
  }
41209
- async function probeRegistry(npmPackage, graceDays, fetchImpl = fetch) {
41550
+ async function probeRegistry(npmPackage, graceDays, fetchImpl = fetch, accept) {
41210
41551
  const encoded = encodeURIComponent(npmPackage).replace(/^%40/, "@");
41211
41552
  const url = `${NPM_REGISTRY_BASE}/${encoded}`;
41212
41553
  try {
@@ -41219,7 +41560,7 @@ async function probeRegistry(npmPackage, graceDays, fetchImpl = fetch) {
41219
41560
  return null;
41220
41561
  }
41221
41562
  const json = await res.json();
41222
- return pickEligibleVersion(json, graceDays);
41563
+ return pickEligibleVersion(json, graceDays, Date.now(), accept);
41223
41564
  } catch (err) {
41224
41565
  warn2(`[lsp] registry probe failed for ${npmPackage}: ${err}`);
41225
41566
  return null;
@@ -41270,11 +41611,11 @@ async function resolveTargetVersion(spec, config, fetchImpl = fetch) {
41270
41611
  }
41271
41612
  const cached = readVersionCheck(spec.npm);
41272
41613
  const weeklyMs = config.graceDays * 24 * 60 * 60 * 1000;
41273
- const cachedSafe = isSafeVersion(cached?.latest_eligible ?? null);
41614
+ const cachedSafe = isSafeVersion(cached?.latest_eligible ?? null) && isVersionSupported(spec, cached?.latest_eligible);
41274
41615
  if (cached && !shouldRecheckVersion(cached, weeklyMs) && cachedSafe) {
41275
41616
  return { version: cached.latest_eligible, pinned: false, probe: null };
41276
41617
  }
41277
- const probe = await probeRegistry(spec.npm, config.graceDays, fetchImpl);
41618
+ const probe = await probeRegistry(spec.npm, config.graceDays, fetchImpl, (candidate) => isVersionSupported(spec, candidate));
41278
41619
  if (!probe) {
41279
41620
  return {
41280
41621
  version: cachedSafe ? cached?.latest_eligible ?? null : null,
@@ -41409,11 +41750,16 @@ async function ensureServerInstalled(spec, config, fetchImpl, signal) {
41409
41750
  const { version, probe } = await resolveTargetVersion(spec, config, fetchImpl);
41410
41751
  if (!version) {
41411
41752
  const installed = isInstalled(spec.npm, spec.binary);
41412
- if (installed) {
41753
+ if (installed && installedVersionAllowed(spec, config)) {
41413
41754
  warn2(`[lsp] no eligible version of ${spec.npm} (grace=${config.graceDays}d); keeping existing install`);
41414
41755
  return { started: false, reason: "kept existing install" };
41415
41756
  }
41416
- const blocked = probe?.blockedByGrace ? `all versions are within ${config.graceDays}-day grace window` : "registry probe failed";
41757
+ 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";
41758
+ if (installed) {
41759
+ 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}`;
41760
+ warn2(`[lsp] ${reason}`);
41761
+ return { started: false, reason };
41762
+ }
41417
41763
  warn2(`[lsp] skipping ${spec.npm}: ${blocked}`);
41418
41764
  return { started: false, reason: blocked };
41419
41765
  }
@@ -41538,6 +41884,15 @@ function validateCachedNpmInstall(spec) {
41538
41884
  }
41539
41885
  return true;
41540
41886
  }
41887
+ function installedVersionAllowed(spec, config) {
41888
+ const installed = readInstalledMeta(spec.npm)?.version;
41889
+ if (!installed)
41890
+ return spec.supportedMajor === undefined;
41891
+ return isVersionSupported(spec, installed) || config.versions[spec.npm] === installed;
41892
+ }
41893
+ function cachedInstallUsable(spec, config) {
41894
+ return isInstalled(spec.npm, spec.binary) && validateCachedNpmInstall(spec) && installedVersionAllowed(spec, config);
41895
+ }
41541
41896
  function runAutoInstall(projectRoot, config, fetchImpl = fetch) {
41542
41897
  const cachedBinDirs = [];
41543
41898
  const skipped = [];
@@ -41550,7 +41905,7 @@ function runAutoInstall(projectRoot, config, fetchImpl = fetch) {
41550
41905
  return projectExtensions;
41551
41906
  };
41552
41907
  for (const spec of NPM_LSP_TABLE) {
41553
- if (isInstalled(spec.npm, spec.binary) && validateCachedNpmInstall(spec)) {
41908
+ if (cachedInstallUsable(spec, config)) {
41554
41909
  cachedBinDirs.push(lspBinDir(spec.npm));
41555
41910
  }
41556
41911
  if (config.disabled.has(spec.id) || spec.id === "typescript-sdk" && config.disabled.has("typescript")) {
@@ -41590,7 +41945,7 @@ function runAutoInstall(projectRoot, config, fetchImpl = fetch) {
41590
41945
  },
41591
41946
  skipped,
41592
41947
  installsComplete: Promise.all(installPromises).then(() => {}),
41593
- getCachedBinDirs: () => NPM_LSP_TABLE.filter((spec) => isInstalled(spec.npm, spec.binary) && validateCachedNpmInstall(spec)).map((spec) => lspBinDir(spec.npm))
41948
+ getCachedBinDirs: () => NPM_LSP_TABLE.filter((spec) => cachedInstallUsable(spec, config)).map((spec) => lspBinDir(spec.npm))
41594
41949
  };
41595
41950
  }
41596
41951
  async function pushLspPathsAfterAutoInstall(pool, projectRoot, cachedBinDirs) {
@@ -43141,391 +43496,8 @@ function astTools(ctx) {
43141
43496
  };
43142
43497
  }
43143
43498
 
43144
- // src/tools/conflicts.ts
43145
- var z3 = tool.schema;
43146
- function conflictTools(ctx) {
43147
- return {
43148
- aft_conflicts: {
43149
- 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).",
43150
- args: {
43151
- 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()
43152
- },
43153
- execute: async (args, context) => {
43154
- const rawArgs = {};
43155
- if (!isEmptyParam(args?.path)) {
43156
- const expanded = expandTilde2(String(args.path));
43157
- const projectRoot = await resolveProjectRoot(ctx, context);
43158
- const resolved = resolvePathFromProjectRoot(projectRoot, expanded);
43159
- const denied = await assertExternalDirectoryPermission(ctx, context, resolved, {
43160
- kind: "directory"
43161
- });
43162
- if (denied)
43163
- return permissionDeniedResponse(denied);
43164
- rawArgs.path = resolved;
43165
- }
43166
- const response = await callToolCall(ctx, context, "conflicts", rawArgs);
43167
- if (response.success === false) {
43168
- throw new Error(response.message || "git_conflicts failed");
43169
- }
43170
- return response.text;
43171
- }
43172
- }
43173
- };
43174
- }
43175
-
43176
- // src/tools/definitions/v2.ts
43177
- import { Effect } from "effect";
43178
- var V2_TOOL_NAME = /^[A-Za-z][A-Za-z0-9_-]{0,63}$/;
43179
- var V2_PATH_HEADER_TOOLS = new Set(["read", "write", "edit"]);
43180
- var V2_BASH_TOOLS = new Set(["bash"]);
43181
- var V2_SHELL_PERMISSION = "shell";
43182
- var ROOT_COMBINATORS = ["anyOf", "oneOf", "allOf"];
43183
- function failure(error) {
43184
- if (error instanceof Error)
43185
- return error;
43186
- const detail = (() => {
43187
- try {
43188
- return JSON.stringify(error) ?? String(error);
43189
- } catch {
43190
- return String(error);
43191
- }
43192
- })();
43193
- return new Error(`V2 tool execution rejected with a non-Error value: ${detail}`);
43194
- }
43195
- function isPlainObject3(value) {
43196
- if (typeof value !== "object" || value === null)
43197
- return false;
43198
- const prototype = Object.getPrototypeOf(value);
43199
- return prototype === Object.prototype || prototype === null;
43200
- }
43201
- function jsonSafeMetadata(value) {
43202
- if (Array.isArray(value)) {
43203
- return value.map((entry) => entry === undefined ? null : jsonSafeMetadata(entry));
43204
- }
43205
- if (!isPlainObject3(value))
43206
- return value;
43207
- const cleaned = {};
43208
- for (const [key, entry] of Object.entries(value)) {
43209
- if (entry === undefined)
43210
- continue;
43211
- cleaned[key] = jsonSafeMetadata(entry);
43212
- }
43213
- return cleaned;
43214
- }
43215
- function resultContent(result) {
43216
- if (typeof result === "string")
43217
- return { content: result };
43218
- const attachments = (result.attachments ?? []).map((attachment) => ({
43219
- type: "file",
43220
- uri: attachment.url,
43221
- mime: attachment.mime,
43222
- ...attachment.filename ? { name: attachment.filename } : {}
43223
- }));
43224
- const content = attachments.length ? [{ type: "text", text: result.output }, ...attachments] : result.output;
43225
- const metadata = jsonSafeMetadata({
43226
- ...result.metadata ?? {},
43227
- ...result.title ? { title: result.title } : {}
43228
- });
43229
- return {
43230
- content,
43231
- ...Object.keys(metadata).length ? { metadata } : {}
43232
- };
43233
- }
43234
- function failureEnvelopeMessage(result) {
43235
- const output = typeof result === "string" ? result : result.output;
43236
- if (typeof output !== "string")
43237
- return;
43238
- const trimmed = output.trim();
43239
- if (!trimmed.startsWith("{") || !trimmed.includes('"success"'))
43240
- return;
43241
- let parsed;
43242
- try {
43243
- parsed = JSON.parse(trimmed);
43244
- } catch {
43245
- return;
43246
- }
43247
- if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
43248
- return;
43249
- const envelope = parsed;
43250
- if (envelope.success !== false)
43251
- return;
43252
- for (const field of [envelope.message, envelope.error, envelope.code]) {
43253
- if (typeof field === "string" && field.length > 0)
43254
- return field;
43255
- }
43256
- return "The tool call failed.";
43257
- }
43258
- function bareToolName3(name) {
43259
- return name.startsWith("aft_") ? name.slice(4) : name;
43260
- }
43261
- function projectArguments(name, definition) {
43262
- if (!V2_PATH_HEADER_TOOLS.has(bareToolName3(name)) || !("filePath" in definition.args)) {
43263
- return { ...definition.args };
43264
- }
43265
- const { filePath, ...rest } = definition.args;
43266
- return { path: filePath, ...rest };
43267
- }
43268
- function executionArguments(name, input) {
43269
- if (!V2_PATH_HEADER_TOOLS.has(bareToolName3(name)) || !("path" in input))
43270
- return input;
43271
- const { path, ...rest } = input;
43272
- return { filePath: path, ...rest };
43273
- }
43274
- function hostPermission(name) {
43275
- const bare = bareToolName3(name);
43276
- if (new Set(["read", "glob", "grep"]).has(bare))
43277
- return bare;
43278
- if (V2_BASH_TOOLS.has(name))
43279
- return V2_SHELL_PERMISSION;
43280
- if (new Set([
43281
- "write",
43282
- "edit",
43283
- "apply_patch",
43284
- "delete",
43285
- "move",
43286
- "ast_grep_replace",
43287
- "import",
43288
- "safety"
43289
- ]).has(bare)) {
43290
- return "edit";
43291
- }
43292
- return;
43293
- }
43294
- function assertV2Contract(name, input) {
43295
- if (!V2_TOOL_NAME.test(name)) {
43296
- throw new Error(`Invalid V2 tool name ${JSON.stringify(name)}; expected ${V2_TOOL_NAME.source}`);
43297
- }
43298
- const schema = tool.schema.toJSONSchema(input, { io: "input" });
43299
- if (schema.type !== "object") {
43300
- throw new Error(`V2 tool ${name} must have an object-rooted input schema`);
43301
- }
43302
- for (const keyword of ROOT_COMBINATORS) {
43303
- if (keyword in schema) {
43304
- throw new Error(`V2 tool ${name} input schema cannot use root ${keyword}`);
43305
- }
43306
- }
43307
- }
43308
- function v2PermissionRequest(name, request) {
43309
- if (!V2_BASH_TOOLS.has(name) || request.permission !== "bash")
43310
- return request;
43311
- return { ...request, permission: V2_SHELL_PERMISSION };
43312
- }
43313
- function runtimeFor(name, location, context, signal, consumers) {
43314
- const directory = location.directory;
43315
- const worktree = location.project?.canonical ?? location.project?.directory ?? directory;
43316
- return {
43317
- sessionID: context.sessionID,
43318
- messageID: context.messageID,
43319
- agent: context.agent,
43320
- directory,
43321
- worktree,
43322
- abort: signal,
43323
- effectAbort: signal,
43324
- metadata: (update) => {
43325
- Effect.runPromise(context.progress(jsonSafeMetadata({
43326
- ...update.metadata ?? {},
43327
- ...update.title ? { title: update.title } : {}
43328
- })));
43329
- },
43330
- ask: (asked) => {
43331
- const request = v2PermissionRequest(name, asked);
43332
- if (consumers.requestPermission)
43333
- return consumers.requestPermission(request, context);
43334
- 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.`));
43335
- },
43336
- progress: context.progress
43337
- };
43338
- }
43339
- function projectV2Tool(name, definition, location, consumers = {}) {
43340
- const input = tool.schema.object(projectArguments(name, definition));
43341
- assertV2Contract(name, input);
43342
- const sharedOptions = definition.options;
43343
- const permission = hostPermission(name);
43344
- return {
43345
- name,
43346
- description: definition.description,
43347
- input,
43348
- options: {
43349
- ...sharedOptions,
43350
- ...permission ? { permission } : {},
43351
- codemode: false
43352
- },
43353
- execute: (rawInput, context) => Effect.tryPromise({
43354
- try: async (signal) => {
43355
- const runtime = runtimeFor(name, location, context, signal, consumers);
43356
- const input = executionArguments(name, rawInput);
43357
- const result = consumers.executeBash && V2_BASH_TOOLS.has(name) ? await consumers.executeBash({
43358
- name,
43359
- input,
43360
- context: runtime,
43361
- definition
43362
- }) : await definition.execute(input, runtime);
43363
- const refusal = failureEnvelopeMessage(result);
43364
- if (refusal)
43365
- throw new Error(refusal);
43366
- return resultContent(result);
43367
- },
43368
- catch: failure
43369
- })
43370
- };
43371
- }
43372
-
43373
- // src/tools/hoisted.ts
43374
- import { stat as stat2 } from "node:fs/promises";
43375
- import * as path4 from "node:path";
43376
-
43377
- // src/shared/read-vision.ts
43378
- var VISION_HOST_TIMEOUT_MS = 1500;
43379
- var states = new WeakMap;
43380
- function stateFor2(client) {
43381
- let state = states.get(client);
43382
- if (!state) {
43383
- state = { models: new Map, history: new Map, capabilities: new Map };
43384
- states.set(client, state);
43385
- }
43386
- return state;
43387
- }
43388
- function rememberReadModel(client, sessionID, model) {
43389
- stateFor2(client).models.set(sessionID, model);
43390
- }
43391
- async function bounded(call) {
43392
- let timer;
43393
- try {
43394
- return await Promise.race([
43395
- Promise.resolve().then(call),
43396
- new Promise((resolve) => {
43397
- timer = setTimeout(() => resolve(undefined), VISION_HOST_TIMEOUT_MS);
43398
- })
43399
- ]);
43400
- } catch {
43401
- return;
43402
- } finally {
43403
- clearTimeout(timer);
43404
- }
43405
- }
43406
- function record3(value) {
43407
- return typeof value === "object" && value !== null && !Array.isArray(value) ? value : undefined;
43408
- }
43409
- async function currentSessionVisionCapability(client, sessionID) {
43410
- if (!sessionID)
43411
- return;
43412
- const state = stateFor2(client);
43413
- let model = state.models.get(sessionID);
43414
- if (!model) {
43415
- let pending = state.history.get(sessionID);
43416
- if (!pending) {
43417
- pending = bounded(() => resolvePromptContext(client, sessionID)).then((context) => context?.model);
43418
- state.history.set(sessionID, pending);
43419
- pending.finally(() => state.history.delete(sessionID));
43420
- }
43421
- const historical = await pending;
43422
- model = state.models.get(sessionID) ?? historical;
43423
- }
43424
- if (!model)
43425
- return;
43426
- const currentModel = model;
43427
- const key = JSON.stringify([model.providerID, model.modelID]);
43428
- let capability = state.capabilities.get(key);
43429
- if (!capability) {
43430
- capability = (async () => {
43431
- const api = client.provider;
43432
- if (!api?.list)
43433
- return;
43434
- const list = api.list.bind(api);
43435
- const listed = record3(await bounded(list));
43436
- const catalog = record3(listed?.data) ?? listed;
43437
- const providers = catalog?.all ?? catalog?.providers;
43438
- if (!Array.isArray(providers))
43439
- return;
43440
- const provider = providers.map(record3).find((entry) => entry?.id === currentModel.providerID);
43441
- const models = provider?.models;
43442
- const entry = record3(Array.isArray(models) ? models.find((entry) => record3(entry)?.id === currentModel.modelID) : record3(models)?.[currentModel.modelID]);
43443
- const inputs = record3(entry?.modalities)?.input;
43444
- return Array.isArray(inputs) ? inputs.includes("image") : typeof entry?.attachment === "boolean" ? entry.attachment : undefined;
43445
- })();
43446
- state.capabilities.set(key, capability);
43447
- capability.then((value) => {
43448
- if (value === undefined)
43449
- state.capabilities.delete(key);
43450
- });
43451
- }
43452
- return capability;
43453
- }
43454
-
43455
- // src/shared/subagent-detect.ts
43456
- import { Effect as Effect2 } from "effect";
43457
- var CACHE_MAX_ENTRIES2 = 200;
43458
- var cache2 = new Map;
43459
- async function resolveIsSubagent(client, sessionId, _fallbackDirectory) {
43460
- if (!sessionId) {
43461
- sessionLog(undefined, "[subagent-detect] no sessionId provided → primary");
43462
- return false;
43463
- }
43464
- const cached = cache2.get(sessionId);
43465
- if (cached) {
43466
- cache2.delete(sessionId);
43467
- cache2.set(sessionId, cached);
43468
- return cached.isSubagent;
43469
- }
43470
- if (isV2PluginContext(client))
43471
- return resolveV2(client, sessionId);
43472
- const c = client;
43473
- const sessionApi = c?.session;
43474
- if (!sessionApi || typeof sessionApi.get !== "function") {
43475
- sessionLog(sessionId, `[subagent-detect] client.session.get unavailable (client=${typeof client}, session=${typeof sessionApi}, get=${typeof sessionApi?.get}) → caching as primary`);
43476
- setCache2(sessionId, false);
43477
- return false;
43478
- }
43479
- sessionLog(sessionId, `[subagent-detect] cache miss, calling client.session.get(id=${sessionId})`);
43480
- let isSubagent = false;
43481
- let parentIdRaw;
43482
- try {
43483
- const result = await sessionApi.get({
43484
- path: { id: sessionId }
43485
- });
43486
- const session = result?.data ?? result;
43487
- parentIdRaw = session?.parentID;
43488
- isSubagent = session !== undefined && typeof session.parentID === "string" && session.parentID.length > 0;
43489
- sessionLog(sessionId, `[subagent-detect] SDK returned session=${session !== undefined ? "present" : "undefined"}, parentID=${JSON.stringify(parentIdRaw)} → isSubagent=${isSubagent}`);
43490
- } catch (err) {
43491
- sessionWarn(sessionId, `[subagent-detect] SDK lookup failed: ${err instanceof Error ? err.message : String(err)}`);
43492
- return false;
43493
- }
43494
- setCache2(sessionId, isSubagent);
43495
- return isSubagent;
43496
- }
43497
- function isV2PluginContext(client) {
43498
- if (!client || typeof client !== "object")
43499
- return false;
43500
- const candidate = client;
43501
- return typeof candidate.location?.directory === "string" && typeof candidate.session?.get === "function";
43502
- }
43503
- async function resolveV2(context, sessionId) {
43504
- let session;
43505
- try {
43506
- session = await Effect2.runPromise(context.session.get({ sessionID: sessionId }));
43507
- } catch (err) {
43508
- sessionWarn(sessionId, `[subagent-detect] OpenCode 2 session lookup failed: ${err instanceof Error ? err.message : String(err)}`);
43509
- return false;
43510
- }
43511
- const isSubagent = typeof session?.parentID === "string" && session.parentID.length > 0;
43512
- sessionLog(sessionId, `[subagent-detect] OpenCode 2 session parentID=${JSON.stringify(session?.parentID)} → isSubagent=${isSubagent}`);
43513
- setCache2(sessionId, isSubagent);
43514
- return isSubagent;
43515
- }
43516
- function setCache2(sessionId, isSubagent) {
43517
- if (cache2.has(sessionId))
43518
- cache2.delete(sessionId);
43519
- cache2.set(sessionId, { isSubagent, recordedAt: Date.now() });
43520
- if (cache2.size > CACHE_MAX_ENTRIES2) {
43521
- const oldest = cache2.keys().next().value;
43522
- if (oldest !== undefined)
43523
- cache2.delete(oldest);
43524
- }
43525
- }
43526
-
43527
43499
  // src/tools/bash.ts
43528
- var z4 = tool.schema;
43500
+ var z3 = tool.schema;
43529
43501
  var METADATA_PREVIEW_LIMIT = 30 * 1024;
43530
43502
  var DEFAULT_HARD_TIMEOUT_MS = 30 * 60 * 1000;
43531
43503
  var BASH_TRANSPORT_MARGIN_MS = 1e4;
@@ -43541,7 +43513,9 @@ function resolveForegroundWaitMs(configured) {
43541
43513
  }
43542
43514
  return configured;
43543
43515
  }
43544
- function orchestratedTransportTimeoutMs(blockToCompletion, wait, effectiveTimeout, foregroundWaitMs) {
43516
+ function orchestratedTransportTimeoutMs(blockToCompletion, wait, effectiveTimeout, foregroundWaitMs, noHardKill = false) {
43517
+ if (noHardKill)
43518
+ return LONGEST_TIMER_DELAY_MS;
43545
43519
  const waitBudget = blockToCompletion || wait ? effectiveTimeout ?? DEFAULT_HARD_TIMEOUT_MS : foregroundWaitMs;
43546
43520
  return waitBudget + BASH_TRANSPORT_MARGIN_MS;
43547
43521
  }
@@ -43618,13 +43592,38 @@ function listenForForegroundAbort(ctx, runtime, enabled) {
43618
43592
  function userMessageDetachDescription(detachOnUserMessage) {
43619
43593
  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)`.";
43620
43594
  }
43621
- function backgroundWaitDescription(watchToolRegistered) {
43622
- 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.";
43595
+ function bashCompanionRegistered(config, companion) {
43596
+ return resolveBashConfig(config).background && toolEnabled(config, companion);
43623
43597
  }
43624
- function bashToolDescription(aftSearchRegistered, compressionOn, backgroundOn, detachOnUserMessage = true, zoomEnabled = true, watchToolRegistered = true) {
43625
- 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`;
43598
+ function backgroundWaitDescription(watchToolRegistered, statusRegistered) {
43599
+ if (watchToolRegistered) {
43600
+ const noPolling = statusRegistered ? ", and never loop bash_status to wait" : "";
43601
+ 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}.`;
43602
+ }
43603
+ 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.";
43604
+ }
43605
+ function ptyDriveClause(statusRegistered, writeRegistered) {
43606
+ if (statusRegistered && writeRegistered) {
43607
+ return ', and is driven with bash_status({ outputMode: "screen" }) plus bash_write';
43608
+ }
43609
+ if (statusRegistered)
43610
+ return ', and its screen is read with bash_status({ outputMode: "screen" })';
43611
+ if (writeRegistered)
43612
+ return ", and is driven with bash_write";
43613
+ return "";
43614
+ }
43615
+ function bashToolDescription(aftSearchRegistered, compressionOn, backgroundOn, detachOnUserMessage = true, zoomEnabled = true, watchToolRegistered = true, surface = {}) {
43616
+ const outline = surface.outline !== false;
43617
+ const status = surface.status !== false;
43618
+ const write = surface.write !== false;
43619
+ const steerTools = [
43620
+ aftSearchRegistered ? "aft_search (concepts, identifiers, regex, literals)" : "the grep tool",
43621
+ "read",
43622
+ ...outline ? ["aft_outline"] : []
43623
+ ].join(", ");
43624
+ const searchSteer = `use ${steerTools}${zoomEnabled ? ", or aft_zoom" : ""} instead`;
43626
43625
  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." : "";
43627
- 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).";
43626
+ 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).";
43628
43627
  return `Execute shell commands.${compression}${tasks}
43629
43628
 
43630
43629
  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.`;
@@ -43701,30 +43700,63 @@ async function withPermissionLoop(ctx, runtime, params, bridgeCall, options) {
43701
43700
  }
43702
43701
  throw new Error("bash permission retry failed: too many rounds");
43703
43702
  }
43703
+ function ptyDriveParamSentence(statusRegistered, writeRegistered) {
43704
+ const writeInput = 'its input accepts either a string OR an array like [ "iHello", { key: "esc" }, ":wq", { key: "enter" } ] for atomic text+key sequences.';
43705
+ if (statusRegistered && writeRegistered) {
43706
+ return ` Inspect with bash_status({ taskId, outputMode: "screen" }) and drive interactively with bash_write — ${writeInput}`;
43707
+ }
43708
+ if (statusRegistered)
43709
+ return ' Inspect with bash_status({ taskId, outputMode: "screen" }).';
43710
+ if (writeRegistered)
43711
+ return ` Drive interactively with bash_write — ${writeInput}`;
43712
+ return "";
43713
+ }
43714
+ function nativeSandboxEnabled(config) {
43715
+ return config.sandbox?.enabled === true;
43716
+ }
43704
43717
  function createBashTool(ctx, aftSearchRegisteredOverride) {
43705
43718
  const initialBashCfg = resolveBashConfig(ctx.config);
43719
+ const statusRegistered = bashCompanionRegistered(ctx.config, "bash_status");
43720
+ const writeRegistered = bashCompanionRegistered(ctx.config, "bash_write");
43721
+ const taskControls = [
43722
+ ...statusRegistered ? ["bash_status"] : [],
43723
+ ...bashCompanionRegistered(ctx.config, "bash_kill") ? ["bash_kill"] : []
43724
+ ];
43725
+ const waitArg = initialBashCfg.background ? {
43726
+ 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.`)
43727
+ } : {};
43728
+ const sandboxArg = nativeSandboxEnabled(ctx.config) ? {
43729
+ 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.")
43730
+ } : {};
43731
+ const compressedArg = initialBashCfg.compress ? {
43732
+ compressed: z3.boolean().optional().describe("When true or omitted, return compressed output with noisy terminal control sequences reduced. Set to false for raw output.")
43733
+ } : {};
43706
43734
  const backgroundFlagArg = initialBashCfg.background ? {
43707
- 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.")
43735
+ 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.`)
43708
43736
  } : {};
43709
43737
  const ptyArgs = initialBashCfg.background ? {
43710
- 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.'),
43738
+ 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)}`),
43711
43739
  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."),
43712
43740
  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.")
43713
43741
  } : {};
43714
43742
  const args = {
43715
- command: z4.string().describe("Shell command to execute. Supports pipes, redirection, and normal shell syntax."),
43743
+ command: z3.string().describe("Shell command to execute. Supports pipes, redirection, and normal shell syntax."),
43716
43744
  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."),
43717
- 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."),
43718
- description: z4.string().optional().describe("Short 5-10 word human-readable summary shown in OpenCode UI metadata instead of raw shell syntax."),
43719
- 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.`),
43720
- 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."),
43745
+ 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."),
43746
+ description: z3.string().optional().describe("Short 5-10 word human-readable summary shown in OpenCode UI metadata instead of raw shell syntax."),
43747
+ ...waitArg,
43748
+ ...sandboxArg,
43721
43749
  ...backgroundFlagArg,
43722
- compressed: z4.boolean().optional().describe("When true or omitted, return compressed output with noisy terminal control sequences reduced. Set to false for raw output."),
43750
+ ...compressedArg,
43723
43751
  ...ptyArgs
43724
43752
  };
43725
43753
  let hostFallbackActive = false;
43726
43754
  return {
43727
- description: bashToolDescription(false, initialBashCfg.compress, initialBashCfg.background, true, toolEnabled(ctx.config, "aft_zoom")),
43755
+ description: bashToolDescription(false, initialBashCfg.compress, initialBashCfg.background, true, toolEnabled(ctx.config, "aft_zoom"), bashCompanionRegistered(ctx.config, "bash_watch"), {
43756
+ outline: toolEnabled(ctx.config, "aft_outline"),
43757
+ status: statusRegistered,
43758
+ write: writeRegistered
43759
+ }),
43728
43760
  args,
43729
43761
  execute: async (args, context) => {
43730
43762
  const bashCfg = resolveBashConfig(ctx.config);
@@ -43738,7 +43770,7 @@ function createBashTool(ctx, aftSearchRegisteredOverride) {
43738
43770
  const cwd = args.workdir ?? context.directory;
43739
43771
  const isSubagent = await resolveIsSubagent(ctx.client, context.sessionID, context.directory);
43740
43772
  const backgroundDisabled = !bashCfg.background;
43741
- const requestedWait = coerceBoolean(args.wait);
43773
+ const requestedWait = !backgroundDisabled && coerceBoolean(args.wait);
43742
43774
  const rawRequestedPty = coerceBoolean(args.pty);
43743
43775
  const rawRequestedBackground = coerceBoolean(args.background);
43744
43776
  if (requestedWait && rawRequestedPty) {
@@ -43749,14 +43781,15 @@ function createBashTool(ctx, aftSearchRegisteredOverride) {
43749
43781
  }
43750
43782
  const requestedPty = !backgroundDisabled && rawRequestedPty;
43751
43783
  const requestedBackground = !backgroundDisabled && (rawRequestedBackground || requestedPty);
43752
- if (requestedPty && isSubagent) {
43753
- throw new Error("PTY mode is not available in subagent sessions; subagents cannot drive interactive terminals.");
43754
- }
43755
43784
  const allowSubagentBg = bashCfg.subagent_background;
43756
43785
  const subagentForcedForeground = isSubagent && !allowSubagentBg;
43786
+ if (requestedPty && subagentForcedForeground) {
43787
+ throw new Error("pty:true is unavailable in this subagent session because bash.subagent_background is false; run the command without pty.");
43788
+ }
43757
43789
  const blockToCompletion = subagentForcedForeground || backgroundDisabled || requestedWait;
43758
43790
  const effectiveBackground = blockToCompletion ? false : requestedBackground;
43759
43791
  const rawTimeout = coerceOptionalInt(args.timeout, "timeout", 1, Number.MAX_SAFE_INTEGER);
43792
+ const workerUnboundedWait = isSubagent && requestedWait && rawTimeout === undefined;
43760
43793
  const ptyRows = coerceOptionalInt(args.ptyRows, "ptyRows", 1, 60);
43761
43794
  const ptyCols = coerceOptionalInt(args.ptyCols, "ptyCols", 1, 140);
43762
43795
  const compressed = coerceBoolean(args.compressed, true);
@@ -43787,7 +43820,7 @@ function createBashTool(ctx, aftSearchRegisteredOverride) {
43787
43820
  wait: requestedWait,
43788
43821
  sandbox: args.sandbox
43789
43822
  }, callBashBridge, {
43790
- transportTimeoutMs: orchestratedTransportTimeoutMs(blockToCompletion, requestedWait, rawTimeout, foregroundWaitMs),
43823
+ transportTimeoutMs: orchestratedTransportTimeoutMs(blockToCompletion, requestedWait, rawTimeout, foregroundWaitMs, workerUnboundedWait),
43791
43824
  onProgress: ({ text }) => {
43792
43825
  accumulatedOutput = preview(accumulatedOutput + text);
43793
43826
  metadata?.({ output: accumulatedOutput, description });
@@ -43797,10 +43830,10 @@ function createBashTool(ctx, aftSearchRegisteredOverride) {
43797
43830
  const fallbackCause = classifyBashHostFallbackError(error);
43798
43831
  if (!bashCfg.host_fallback || fallbackCause === undefined)
43799
43832
  throw error;
43800
- if (rawRequestedBackground) {
43833
+ if (!backgroundDisabled && rawRequestedBackground) {
43801
43834
  throw new Error(`${BASH_HOST_FALLBACK_REFUSAL}; background:true is unsupported.`);
43802
43835
  }
43803
- if (rawRequestedPty) {
43836
+ if (requestedPty) {
43804
43837
  throw new Error(`${BASH_HOST_FALLBACK_REFUSAL}; pty:true is unsupported.`);
43805
43838
  }
43806
43839
  const projectRoot = projectRootFor(context);
@@ -43834,7 +43867,7 @@ function createBashTool(ctx, aftSearchRegisteredOverride) {
43834
43867
  trackBgTask(context.sessionID, taskId);
43835
43868
  let rendered = data.output ?? "";
43836
43869
  if (isSubagent && allowSubagentBg) {
43837
- rendered += subagentGuidance(taskId, bashCfg.watch_sync_max_ms);
43870
+ rendered += workerBackgroundTaskNote(taskId);
43838
43871
  }
43839
43872
  const metadataPayload = { description, output: rendered, status: "running", taskId };
43840
43873
  metadata?.(metadataPayload);
@@ -43853,17 +43886,19 @@ function createBashTool(ctx, aftSearchRegisteredOverride) {
43853
43886
  };
43854
43887
  }
43855
43888
  function createBashStatusTool(ctx) {
43889
+ const waitSteer = bashCompanionRegistered(ctx.config, "bash_watch") ? " To wait, use bash_watch." : "";
43856
43890
  return {
43857
- 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.",
43891
+ 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}`,
43858
43892
  args: {
43859
- taskId: z4.string().describe("Background task ID returned by bash({ background: true }), e.g. bash-6b454047a1c39ded."),
43860
- 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.")
43893
+ taskId: z3.string().describe("Background task ID returned by bash({ background: true }), e.g. bash-6b454047a1c39ded."),
43894
+ 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.")
43861
43895
  },
43862
43896
  execute: async (args, context) => {
43863
43897
  const taskId = args.taskId;
43864
43898
  const outputMode = args.outputMode;
43865
43899
  const data = await bashStatusSnapshot(ctx, context, taskId, outputMode);
43866
- return await formatBashStatusText(context, taskId, data, outputMode);
43900
+ const isSubagent = await resolveIsSubagent(ctx.client, context.sessionID, context.directory);
43901
+ return await formatBashStatusText(context, taskId, data, outputMode, isSubagent ? "worker" : "primary");
43867
43902
  }
43868
43903
  };
43869
43904
  }
@@ -43871,7 +43906,7 @@ function createBashKillTool(ctx) {
43871
43906
  return {
43872
43907
  description: "Terminate a running background bash task spawned with bash({ background: true }). Returns confirmation of kill or an error if the task already finished.",
43873
43908
  args: {
43874
- taskId: z4.string().describe("Background task ID returned by bash({ background: true }), e.g. bash-6b454047a1c39ded.")
43909
+ taskId: z3.string().describe("Background task ID returned by bash({ background: true }), e.g. bash-6b454047a1c39ded.")
43875
43910
  },
43876
43911
  execute: async (args, context) => {
43877
43912
  const data = await callBashBridge(ctx, context, "bash_kill", {
@@ -43894,7 +43929,7 @@ async function bashStatusSnapshot(ctx, runtime, taskId, outputMode, options) {
43894
43929
  }
43895
43930
  return data;
43896
43931
  }
43897
- async function formatBashStatusText(runtime, taskId, data, requestedOutputMode) {
43932
+ async function formatBashStatusText(runtime, taskId, data, requestedOutputMode, role) {
43898
43933
  const status = data.status;
43899
43934
  const exit = typeof data.exit_code === "number" ? ` (exit ${data.exit_code})` : "";
43900
43935
  const dur = typeof data.duration_ms === "number" ? ` ${Math.round(data.duration_ms / 1000)}s` : "";
@@ -43912,7 +43947,7 @@ ${preview}`;
43912
43947
  }
43913
43948
  if (status === "running") {
43914
43949
  text += `
43915
- A completion reminder will be delivered automatically; don't poll.`;
43950
+ ${runningTaskStatusHint(role)}`;
43916
43951
  }
43917
43952
  }
43918
43953
  return text;
@@ -43941,11 +43976,6 @@ PTY task is still running. Use bash_status({ taskId: "${taskId}", outputMode: "s
43941
43976
  function preview(output) {
43942
43977
  return output.length <= METADATA_PREVIEW_LIMIT ? output : output.slice(-METADATA_PREVIEW_LIMIT);
43943
43978
  }
43944
- function subagentGuidance(taskId, watchSyncMaxMs) {
43945
- return `
43946
-
43947
- 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.`;
43948
- }
43949
43979
  function foregroundMetadata(description, data, rendered) {
43950
43980
  const outputPath = data.output_path;
43951
43981
  const truncated = typeof data.truncated === "boolean" ? data.truncated : data.output_truncated;
@@ -43966,24 +43996,355 @@ function shortenCommand(command) {
43966
43996
  return collapsed.length <= 80 ? collapsed : `${collapsed.slice(0, 77)}...`;
43967
43997
  }
43968
43998
 
43999
+ // src/tools/conflicts.ts
44000
+ var z4 = tool.schema;
44001
+ function conflictTools(ctx) {
44002
+ return {
44003
+ aft_conflicts: {
44004
+ 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).",
44005
+ args: {
44006
+ 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()
44007
+ },
44008
+ execute: async (args, context) => {
44009
+ const rawArgs = {};
44010
+ if (!isEmptyParam(args?.path)) {
44011
+ const expanded = expandTilde2(String(args.path));
44012
+ const projectRoot = await resolveProjectRoot(ctx, context);
44013
+ const resolved = resolvePathFromProjectRoot(projectRoot, expanded);
44014
+ const denied = await assertExternalDirectoryPermission(ctx, context, resolved, {
44015
+ kind: "directory"
44016
+ });
44017
+ if (denied)
44018
+ return permissionDeniedResponse(denied);
44019
+ rawArgs.path = resolved;
44020
+ }
44021
+ const response = await callToolCall(ctx, context, "conflicts", rawArgs);
44022
+ if (response.success === false) {
44023
+ throw new Error(response.message || "git_conflicts failed");
44024
+ }
44025
+ return response.text;
44026
+ }
44027
+ }
44028
+ };
44029
+ }
44030
+
44031
+ // src/tools/definitions/v2.ts
44032
+ import { Effect as Effect3 } from "effect";
44033
+ var V2_TOOL_NAME = /^[A-Za-z][A-Za-z0-9_-]{0,63}$/;
44034
+ var V2_PATH_HEADER_TOOLS = new Set(["read", "write", "edit"]);
44035
+ var V2_BASH_TOOLS = new Set(["bash"]);
44036
+ var V2_SHELL_PERMISSION = "shell";
44037
+ var ROOT_COMBINATORS = ["anyOf", "oneOf", "allOf"];
44038
+ function failure(error) {
44039
+ if (error instanceof Error)
44040
+ return error;
44041
+ const detail = (() => {
44042
+ try {
44043
+ return JSON.stringify(error) ?? String(error);
44044
+ } catch {
44045
+ return String(error);
44046
+ }
44047
+ })();
44048
+ return new Error(`V2 tool execution rejected with a non-Error value: ${detail}`);
44049
+ }
44050
+ function isPlainObject3(value) {
44051
+ if (typeof value !== "object" || value === null)
44052
+ return false;
44053
+ const prototype = Object.getPrototypeOf(value);
44054
+ return prototype === Object.prototype || prototype === null;
44055
+ }
44056
+ function jsonSafeMetadata(value) {
44057
+ if (Array.isArray(value)) {
44058
+ return value.map((entry) => entry === undefined ? null : jsonSafeMetadata(entry));
44059
+ }
44060
+ if (!isPlainObject3(value))
44061
+ return value;
44062
+ const cleaned = {};
44063
+ for (const [key, entry] of Object.entries(value)) {
44064
+ if (entry === undefined)
44065
+ continue;
44066
+ cleaned[key] = jsonSafeMetadata(entry);
44067
+ }
44068
+ return cleaned;
44069
+ }
44070
+ function resultContent(result) {
44071
+ if (typeof result === "string")
44072
+ return { content: result };
44073
+ const attachments = (result.attachments ?? []).map((attachment) => ({
44074
+ type: "file",
44075
+ uri: attachment.url,
44076
+ mime: attachment.mime,
44077
+ ...attachment.filename ? { name: attachment.filename } : {}
44078
+ }));
44079
+ const content = attachments.length ? [{ type: "text", text: result.output }, ...attachments] : result.output;
44080
+ const metadata = jsonSafeMetadata({
44081
+ ...result.metadata ?? {},
44082
+ ...result.title ? { title: result.title } : {}
44083
+ });
44084
+ return {
44085
+ content,
44086
+ ...Object.keys(metadata).length ? { metadata } : {}
44087
+ };
44088
+ }
44089
+ function failureEnvelopeMessage(result) {
44090
+ const output = typeof result === "string" ? result : result.output;
44091
+ if (typeof output !== "string")
44092
+ return;
44093
+ const trimmed = output.trim();
44094
+ if (!trimmed.startsWith("{") || !trimmed.includes('"success"'))
44095
+ return;
44096
+ let parsed;
44097
+ try {
44098
+ parsed = JSON.parse(trimmed);
44099
+ } catch {
44100
+ return;
44101
+ }
44102
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
44103
+ return;
44104
+ const envelope = parsed;
44105
+ if (envelope.success !== false)
44106
+ return;
44107
+ for (const field of [envelope.message, envelope.error, envelope.code]) {
44108
+ if (typeof field === "string" && field.length > 0)
44109
+ return field;
44110
+ }
44111
+ return "The tool call failed.";
44112
+ }
44113
+ function bareToolName3(name) {
44114
+ return name.startsWith("aft_") ? name.slice(4) : name;
44115
+ }
44116
+ function projectArguments(name, definition) {
44117
+ if (!V2_PATH_HEADER_TOOLS.has(bareToolName3(name)) || !("filePath" in definition.args)) {
44118
+ return { ...definition.args };
44119
+ }
44120
+ const { filePath, ...rest } = definition.args;
44121
+ return { path: filePath, ...rest };
44122
+ }
44123
+ function executionArguments(name, input) {
44124
+ if (!V2_PATH_HEADER_TOOLS.has(bareToolName3(name)) || !("path" in input))
44125
+ return input;
44126
+ const { path, ...rest } = input;
44127
+ return { filePath: path, ...rest };
44128
+ }
44129
+ function hostPermission(name) {
44130
+ const bare = bareToolName3(name);
44131
+ if (new Set(["read", "glob", "grep"]).has(bare))
44132
+ return bare;
44133
+ if (V2_BASH_TOOLS.has(name))
44134
+ return V2_SHELL_PERMISSION;
44135
+ if (new Set([
44136
+ "write",
44137
+ "edit",
44138
+ "apply_patch",
44139
+ "delete",
44140
+ "move",
44141
+ "ast_grep_replace",
44142
+ "import",
44143
+ "safety"
44144
+ ]).has(bare)) {
44145
+ return "edit";
44146
+ }
44147
+ return;
44148
+ }
44149
+ function assertV2Contract(name, input) {
44150
+ if (!V2_TOOL_NAME.test(name)) {
44151
+ throw new Error(`Invalid V2 tool name ${JSON.stringify(name)}; expected ${V2_TOOL_NAME.source}`);
44152
+ }
44153
+ const schema = tool.schema.toJSONSchema(input, { io: "input" });
44154
+ if (schema.type !== "object") {
44155
+ throw new Error(`V2 tool ${name} must have an object-rooted input schema`);
44156
+ }
44157
+ for (const keyword of ROOT_COMBINATORS) {
44158
+ if (keyword in schema) {
44159
+ throw new Error(`V2 tool ${name} input schema cannot use root ${keyword}`);
44160
+ }
44161
+ }
44162
+ }
44163
+ function v2PermissionRequest(name, request) {
44164
+ if (!V2_BASH_TOOLS.has(name) || request.permission !== "bash")
44165
+ return request;
44166
+ return { ...request, permission: V2_SHELL_PERMISSION };
44167
+ }
44168
+ function runtimeFor(name, location, context, signal, consumers) {
44169
+ const directory = location.directory;
44170
+ const worktree = location.project?.canonical ?? location.project?.directory ?? directory;
44171
+ return {
44172
+ sessionID: context.sessionID,
44173
+ messageID: context.messageID,
44174
+ agent: context.agent,
44175
+ directory,
44176
+ directoryIsSessionRoot: true,
44177
+ worktree,
44178
+ abort: signal,
44179
+ effectAbort: signal,
44180
+ metadata: (update) => {
44181
+ Effect3.runPromise(context.progress(jsonSafeMetadata({
44182
+ ...update.metadata ?? {},
44183
+ ...update.title ? { title: update.title } : {}
44184
+ })));
44185
+ },
44186
+ ask: (asked) => {
44187
+ const request = v2PermissionRequest(name, asked);
44188
+ if (consumers.requestPermission)
44189
+ return consumers.requestPermission(request, context);
44190
+ 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.`));
44191
+ },
44192
+ progress: context.progress
44193
+ };
44194
+ }
44195
+ function projectV2Tool(name, definition, location, consumers = {}) {
44196
+ const input = tool.schema.object(projectArguments(name, definition));
44197
+ assertV2Contract(name, input);
44198
+ const sharedOptions = definition.options;
44199
+ const permission = hostPermission(name);
44200
+ return {
44201
+ name,
44202
+ description: definition.description,
44203
+ input,
44204
+ options: {
44205
+ ...sharedOptions,
44206
+ ...permission ? { permission } : {},
44207
+ codemode: false
44208
+ },
44209
+ execute: (rawInput, context) => Effect3.tryPromise({
44210
+ try: async (signal) => {
44211
+ const runtime = runtimeFor(name, location, context, signal, consumers);
44212
+ const input = executionArguments(name, rawInput);
44213
+ const result = consumers.executeBash && V2_BASH_TOOLS.has(name) ? await consumers.executeBash({
44214
+ name,
44215
+ input,
44216
+ context: runtime,
44217
+ definition
44218
+ }) : await definition.execute(input, runtime);
44219
+ const refusal = failureEnvelopeMessage(result);
44220
+ if (refusal)
44221
+ throw new Error(refusal);
44222
+ return resultContent(result);
44223
+ },
44224
+ catch: failure
44225
+ })
44226
+ };
44227
+ }
44228
+
44229
+ // src/tools/hoisted.ts
44230
+ import { stat as stat2 } from "node:fs/promises";
44231
+ import * as path4 from "node:path";
44232
+
44233
+ // src/hashline-tag-sources.ts
44234
+ function hashlineTagSourceSentence(bashRewritesMintTags, isRegistered = () => true) {
44235
+ const rewrites = bashRewritesMintTags ? " (and accepted AFT `cat`/`head`/`tail` rewrites)" : "";
44236
+ const lookAlikes = [
44237
+ ...["aft_zoom", "aft_outline"].filter(isRegistered),
44238
+ "grep",
44239
+ ...isRegistered("aft_search") ? ["aft_search"] : []
44240
+ ].map((name) => `\`${name}\``);
44241
+ lookAlikes.push("conflict snippets");
44242
+ return `Only \`read\`${rewrites} mint hashline tags. ${joinList(lookAlikes)} do not. After navigation, call \`read\` on every file and range the patch addresses.`;
44243
+ }
44244
+ function joinList(items) {
44245
+ if (items.length <= 2)
44246
+ return items.join(" and ");
44247
+ return `${items.slice(0, -1).join(", ")}, and ${items[items.length - 1]}`;
44248
+ }
44249
+
44250
+ // src/shared/read-vision.ts
44251
+ var VISION_HOST_TIMEOUT_MS = 1500;
44252
+ var states = new WeakMap;
44253
+ function stateFor2(client) {
44254
+ let state = states.get(client);
44255
+ if (!state) {
44256
+ state = { models: new Map, history: new Map, capabilities: new Map };
44257
+ states.set(client, state);
44258
+ }
44259
+ return state;
44260
+ }
44261
+ function rememberReadModel(client, sessionID, model) {
44262
+ stateFor2(client).models.set(sessionID, model);
44263
+ }
44264
+ async function bounded(call) {
44265
+ let timer;
44266
+ try {
44267
+ return await Promise.race([
44268
+ Promise.resolve().then(call),
44269
+ new Promise((resolve) => {
44270
+ timer = setTimeout(() => resolve(undefined), VISION_HOST_TIMEOUT_MS);
44271
+ })
44272
+ ]);
44273
+ } catch {
44274
+ return;
44275
+ } finally {
44276
+ clearTimeout(timer);
44277
+ }
44278
+ }
44279
+ function record3(value) {
44280
+ return typeof value === "object" && value !== null && !Array.isArray(value) ? value : undefined;
44281
+ }
44282
+ async function currentSessionVisionCapability(client, sessionID) {
44283
+ if (!sessionID)
44284
+ return;
44285
+ const state = stateFor2(client);
44286
+ let model = state.models.get(sessionID);
44287
+ if (!model) {
44288
+ let pending = state.history.get(sessionID);
44289
+ if (!pending) {
44290
+ pending = bounded(() => resolvePromptContext(client, sessionID)).then((context) => context?.model);
44291
+ state.history.set(sessionID, pending);
44292
+ pending.finally(() => state.history.delete(sessionID));
44293
+ }
44294
+ const historical = await pending;
44295
+ model = state.models.get(sessionID) ?? historical;
44296
+ }
44297
+ if (!model)
44298
+ return;
44299
+ const currentModel = model;
44300
+ const key = JSON.stringify([model.providerID, model.modelID]);
44301
+ let capability = state.capabilities.get(key);
44302
+ if (!capability) {
44303
+ capability = (async () => {
44304
+ const api = client.provider;
44305
+ if (!api?.list)
44306
+ return;
44307
+ const list = api.list.bind(api);
44308
+ const listed = record3(await bounded(list));
44309
+ const catalog = record3(listed?.data) ?? listed;
44310
+ const providers = catalog?.all ?? catalog?.providers;
44311
+ if (!Array.isArray(providers))
44312
+ return;
44313
+ const provider = providers.map(record3).find((entry) => entry?.id === currentModel.providerID);
44314
+ const models = provider?.models;
44315
+ const entry = record3(Array.isArray(models) ? models.find((entry) => record3(entry)?.id === currentModel.modelID) : record3(models)?.[currentModel.modelID]);
44316
+ const inputs = record3(entry?.modalities)?.input;
44317
+ return Array.isArray(inputs) ? inputs.includes("image") : typeof entry?.attachment === "boolean" ? entry.attachment : undefined;
44318
+ })();
44319
+ state.capabilities.set(key, capability);
44320
+ capability.then((value) => {
44321
+ if (value === undefined)
44322
+ state.capabilities.delete(key);
44323
+ });
44324
+ }
44325
+ return capability;
44326
+ }
44327
+
43969
44328
  // src/tools/bash_watch.ts
43970
44329
  var z5 = tool.schema;
43971
- var BASH_WAIT_POLL_INTERVAL_MS = 100;
43972
44330
  var REGEX_WAIT_SCAN_WINDOW_BYTES = 64 * 1024;
43973
44331
  function unavailableSnapshot() {
43974
44332
  return { status: "unknown" };
43975
44333
  }
43976
- function coerceConfiguredWatchTimeout(value, cap) {
44334
+ function coerceConfiguredWatchTimeout(value, role, cap) {
43977
44335
  try {
43978
- return coerceOptionalInt(value, "timeoutMs", 1, cap);
44336
+ return coerceOptionalInt(value, "timeoutMs", 1, maxWatchTimeoutMs(role, cap));
43979
44337
  } catch (error) {
43980
44338
  const message = error instanceof Error ? error.message : String(error);
44339
+ if (role === "worker")
44340
+ throw error;
43981
44341
  throw new Error(`${message} (bash.watch_sync_max_ms)`);
43982
44342
  }
43983
44343
  }
43984
44344
  function createBashWatchTool(ctx) {
44345
+ const noPolling = bashCompanionRegistered(ctx.config, "bash_status") ? " Never loop bash_status to wait." : "";
43985
44346
  return {
43986
- 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.",
44347
+ 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}`,
43987
44348
  args: {
43988
44349
  taskId: z5.string().describe("Background task ID returned by bash({ background: true })."),
43989
44350
  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."),
@@ -44034,11 +44395,11 @@ A notification will fire when the pattern matches or the task exits.`;
44034
44395
  }
44035
44396
  const syncWaitCap = bashCfg.watch_sync_max_ms;
44036
44397
  const role = isSubagent ? "worker" : "primary";
44037
- const effectiveWaitMs = subagentForcedSync ? syncWaitCap : resolveWatchTimeoutMs(coerceConfiguredWatchTimeout(args.timeoutMs, syncWaitCap), role, syncWaitCap);
44398
+ const effectiveWaitMs = subagentForcedSync ? undefined : resolveWatchTimeoutMs(coerceConfiguredWatchTimeout(args.timeoutMs, role, syncWaitCap), role, syncWaitCap);
44038
44399
  const data = await waitForBashStatus(ctx, context, taskId, undefined, waitFor, effectiveWaitMs);
44039
44400
  const waited = data.waited;
44040
44401
  if (waited?.reason === "user_message") {
44041
- const convertedText = await convertToAsyncWatchOnAbort(ctx, context, taskId, waitFor, coerceBoolean(args.once, true), waited.elapsed_ms);
44402
+ const convertedText = await convertToAsyncWatchOnAbort(ctx, context, taskId, waitFor, coerceBoolean(args.once, true), waited.elapsed_ms, role);
44042
44403
  const metadata = context.metadata;
44043
44404
  metadata?.({ taskId, status: data.status, waited, convertedToAsync: true });
44044
44405
  return convertedText;
@@ -44050,10 +44411,11 @@ A notification will fire when the pattern matches or the task exits.`;
44050
44411
  }
44051
44412
  };
44052
44413
  }
44053
- async function convertToAsyncWatchOnAbort(ctx, context, taskId, waitFor, once, elapsedMs) {
44414
+ async function convertToAsyncWatchOnAbort(ctx, context, taskId, waitFor, once, elapsedMs, role) {
44054
44415
  const interrupted = `Sync watch for task ${taskId} was interrupted because you sent a message after ${elapsedMs}ms of waiting. `;
44416
+ const reminderFallback = interruptedWatchTail(role, `The task is still running in the background. A completion reminder will be ` + `delivered automatically when the task exits.`);
44055
44417
  if (!waitFor) {
44056
- 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.`;
44418
+ 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.`);
44057
44419
  }
44058
44420
  const notifyParams = {
44059
44421
  task_id: taskId,
@@ -44068,12 +44430,12 @@ async function convertToAsyncWatchOnAbort(ctx, context, taskId, waitFor, once, e
44068
44430
  const registered = await callBashBridge(ctx, context, "bash_notify", notifyParams);
44069
44431
  if (registered.success === false) {
44070
44432
  unmarkExplicitControl(context.sessionID, taskId);
44071
- 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.`;
44433
+ return interrupted + `Auto-registering an async watch failed (${String(registered.message ?? "unknown error")}). ` + reminderFallback;
44072
44434
  }
44073
- 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.`;
44435
+ 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.`);
44074
44436
  } catch (err) {
44075
44437
  unmarkExplicitControl(context.sessionID, taskId);
44076
- 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.`;
44438
+ return interrupted + `Auto-registering an async watch failed (${err instanceof Error ? err.message : String(err)}). ` + reminderFallback;
44077
44439
  }
44078
44440
  }
44079
44441
  function formatWatchResultText(taskId, data, waited, role, syncWaitCap) {
@@ -44082,17 +44444,20 @@ function formatWatchResultText(taskId, data, waited, role, syncWaitCap) {
44082
44444
  const dur = typeof data.duration_ms === "number" ? ` ${Math.round(data.duration_ms / 1000)}s` : "";
44083
44445
  let text = `Task ${taskId}: ${status}${exit}${dur}`;
44084
44446
  if (waited) {
44085
- const waitedText = formatWatchWaited(waited.elapsed_ms, waited.limit_ms, syncWaitCap);
44447
+ const waitedText = formatWatchWaited(waited.elapsed_ms, waited.limit_ms, role === "primary" ? syncWaitCap : undefined);
44086
44448
  if (waited.reason === "matched") {
44087
44449
  const stream = waited.match_stream ? ` in ${waited.match_stream}` : "";
44088
44450
  text += `
44089
44451
  ${waitedText}; matched ${JSON.stringify(waited.match ?? "")}${stream} at offset ${waited.match_offset ?? 0}.`;
44090
44452
  } else if (waited.reason === "timeout") {
44091
44453
  text += `
44092
- ${waitedText}; timeout reached without match. ${watchTimeoutSteer(role, syncWaitCap)}`;
44454
+ ${waitedText}; timeout reached without match. ${watchTimeoutSteer(role)}`;
44093
44455
  } else if (waited.reason === "unavailable") {
44094
44456
  text += `
44095
- ${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.`;
44457
+ ${waitedText}; ${watchUnavailableSteer(role)}`;
44458
+ } else if (waited.reason === "aborted") {
44459
+ text += `
44460
+ ${waitedText}; the watch was cancelled. The task keeps running.`;
44096
44461
  } else {
44097
44462
  const stat = String(data.status ?? "unknown");
44098
44463
  const e = typeof data.exit_code === "number" ? `, exit ${data.exit_code}` : "";
@@ -44119,9 +44484,17 @@ async function bashStatusSnapshot2(ctx, runtime, taskId, outputMode, cursor, opt
44119
44484
  return data;
44120
44485
  }
44121
44486
  async function waitForBashStatus(ctx, runtime, taskId, outputMode, waitFor, effectiveWaitMs) {
44122
- const startedAt = monotonicNowMs();
44123
- const deadline = startedAt + effectiveWaitMs;
44124
- const elapsedMs = () => Math.round(monotonicNowMs() - startedAt);
44487
+ const startedAt = watchClock.now();
44488
+ const deadline = effectiveWaitMs === undefined ? Number.POSITIVE_INFINITY : startedAt + effectiveWaitMs;
44489
+ const elapsedMs = () => Math.round(watchClock.now() - startedAt);
44490
+ const abortSignal = runtime.abort;
44491
+ const pause = () => watchClock.sleep(Math.min(watchPollDelayMs(elapsedMs()), Math.max(0, deadline - watchClock.now())), abortSignal);
44492
+ const waited = (reason, extra = {}) => ({
44493
+ reason,
44494
+ elapsed_ms: elapsedMs(),
44495
+ ...effectiveWaitMs === undefined ? {} : { limit_ms: effectiveWaitMs },
44496
+ ...extra
44497
+ });
44125
44498
  let spillCursor = { output: 0, stderr: 0 };
44126
44499
  const scanState = {
44127
44500
  output: { text: "", baseOffset: 0 },
@@ -44135,31 +44508,29 @@ async function waitForBashStatus(ctx, runtime, taskId, outputMode, waitFor, effe
44135
44508
  markTaskWaiting(runtime.sessionID, taskId);
44136
44509
  let sawTerminal = false;
44137
44510
  let lastData;
44511
+ let busySince;
44138
44512
  try {
44139
44513
  while (true) {
44514
+ if (abortSignal?.aborted) {
44515
+ return withWaited(lastData ?? unavailableSnapshot(), waited("aborted"));
44516
+ }
44140
44517
  let data;
44141
44518
  try {
44142
44519
  data = await bashStatusSnapshot2(ctx, runtime, taskId, outputMode, waitFor ? spillCursor : undefined, bridgeOptions);
44143
44520
  } catch (err) {
44144
44521
  if (!isBridgeTransportTimeout(err))
44145
44522
  throw err;
44523
+ busySince ??= watchClock.now();
44146
44524
  if (isSyncWatchAborted(runtime.sessionID)) {
44147
- return withWaited(lastData ?? unavailableSnapshot(), {
44148
- reason: "user_message",
44149
- elapsed_ms: elapsedMs(),
44150
- limit_ms: effectiveWaitMs
44151
- });
44525
+ return withWaited(lastData ?? unavailableSnapshot(), waited("user_message"));
44152
44526
  }
44153
- if (monotonicNowMs() >= deadline) {
44154
- return withWaited(lastData ?? unavailableSnapshot(), {
44155
- reason: "unavailable",
44156
- elapsed_ms: elapsedMs(),
44157
- limit_ms: effectiveWaitMs
44158
- });
44527
+ if (watchClock.now() >= deadline || watchClock.now() - busySince >= WATCH_UNAVAILABLE_GIVE_UP_MS) {
44528
+ return withWaited(lastData ?? unavailableSnapshot(), waited("unavailable"));
44159
44529
  }
44160
- await sleep(Math.min(BASH_WAIT_POLL_INTERVAL_MS, Math.max(0, deadline - monotonicNowMs())));
44530
+ await pause();
44161
44531
  continue;
44162
44532
  }
44533
+ busySince = undefined;
44163
44534
  lastData = data;
44164
44535
  const terminal = isTerminalStatus(data.status);
44165
44536
  if (waitFor) {
@@ -44184,14 +44555,11 @@ async function waitForBashStatus(ctx, runtime, taskId, outputMode, waitFor, effe
44184
44555
  await markBgCompletionDelivered({ ctx, directory: projectRootFor(runtime), sessionID: runtime.sessionID }, taskId);
44185
44556
  }
44186
44557
  const matchStream = data.mode === "pty" ? undefined : chunk.stream === "output" ? "stdout" : "stderr";
44187
- return withWaited(data, {
44188
- reason: "matched",
44189
- elapsed_ms: elapsedMs(),
44190
- limit_ms: effectiveWaitMs,
44558
+ return withWaited(data, waited("matched", {
44191
44559
  match: match.text,
44192
44560
  match_offset: state.baseOffset + match.byteOffset,
44193
44561
  match_stream: matchStream
44194
- });
44562
+ }));
44195
44563
  }
44196
44564
  if (waitFor.kind === "substring") {
44197
44565
  const trimmed = trimWaitScanBuffer(state.text, state.baseOffset, waitFor);
@@ -44205,27 +44573,15 @@ async function waitForBashStatus(ctx, runtime, taskId, outputMode, waitFor, effe
44205
44573
  sawTerminal = true;
44206
44574
  consumeBgCompletion(runtime.sessionID, taskId);
44207
44575
  await markBgCompletionDelivered({ ctx, directory: projectRootFor(runtime), sessionID: runtime.sessionID }, taskId);
44208
- return withWaited(data, {
44209
- reason: "exited",
44210
- elapsed_ms: elapsedMs(),
44211
- limit_ms: effectiveWaitMs
44212
- });
44576
+ return withWaited(data, waited("exited"));
44213
44577
  }
44214
44578
  if (isSyncWatchAborted(runtime.sessionID)) {
44215
- return withWaited(data, {
44216
- reason: "user_message",
44217
- elapsed_ms: elapsedMs(),
44218
- limit_ms: effectiveWaitMs
44219
- });
44579
+ return withWaited(data, waited("user_message"));
44220
44580
  }
44221
- if (monotonicNowMs() >= deadline) {
44222
- return withWaited(data, {
44223
- reason: "timeout",
44224
- elapsed_ms: elapsedMs(),
44225
- limit_ms: effectiveWaitMs
44226
- });
44581
+ if (watchClock.now() >= deadline) {
44582
+ return withWaited(data, waited("timeout"));
44227
44583
  }
44228
- await sleep(Math.min(BASH_WAIT_POLL_INTERVAL_MS, Math.max(0, deadline - monotonicNowMs())));
44584
+ await pause();
44229
44585
  }
44230
44586
  } finally {
44231
44587
  if (!sawTerminal)
@@ -44335,8 +44691,9 @@ function withWaited(data, waited) {
44335
44691
  // src/tools/bash_write.ts
44336
44692
  var z6 = tool.schema;
44337
44693
  function createBashWriteTool(ctx) {
44694
+ const modeCheck = bashCompanionRegistered(ctx.config, "bash_status") ? '; check bash_status reports mode: "pty" first.' : ".";
44338
44695
  return {
44339
- 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).",
44696
+ 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).",
44340
44697
  args: {
44341
44698
  taskId: z6.string().describe("Background PTY task ID returned by bash({ pty: true, background: true })."),
44342
44699
  input: z6.union([
@@ -44604,25 +44961,19 @@ function createReadTool(ctx) {
44604
44961
  const failure = classifyPermissionError(error);
44605
44962
  return permissionDeniedResponse(failure.kind === "rule_denied" ? permissionRuleDenial("read") : failure.message);
44606
44963
  }
44607
- const rawStartLine = coerceOptionalInt(args.startLine, "startLine", 1, Number.MAX_SAFE_INTEGER);
44608
- const rawEndLine = coerceOptionalInt(args.endLine, "endLine", 1, Number.MAX_SAFE_INTEGER);
44609
- const rawLimit = coerceOptionalInt(args.limit, "limit", 1, Number.MAX_SAFE_INTEGER);
44610
- const rawOffset = coerceOptionalInt(args.offset, "offset", 1, Number.MAX_SAFE_INTEGER);
44611
- let startLine = rawStartLine;
44612
- let endLine = rawEndLine;
44613
- if (startLine === undefined && rawOffset !== undefined) {
44614
- startLine = rawOffset;
44615
- if (rawLimit !== undefined) {
44616
- endLine = rawOffset + rawLimit - 1;
44617
- }
44618
- }
44964
+ const startLine = coerceOptionalInt(args.startLine, "startLine", 1, Number.MAX_SAFE_INTEGER);
44965
+ const endLine = coerceOptionalInt(args.endLine, "endLine", 1, Number.MAX_SAFE_INTEGER);
44966
+ const limit = coerceOptionalInt(args.limit, "limit", 1, Number.MAX_SAFE_INTEGER);
44967
+ const offset = coerceOptionalInt(args.offset, "offset", 1, Number.MAX_SAFE_INTEGER);
44619
44968
  const rawArgs = { filePath: file };
44620
44969
  if (startLine !== undefined)
44621
44970
  rawArgs.startLine = startLine;
44622
44971
  if (endLine !== undefined)
44623
44972
  rawArgs.endLine = endLine;
44624
- if (rawLimit !== undefined && rawOffset === undefined)
44625
- rawArgs.limit = rawLimit;
44973
+ if (offset !== undefined)
44974
+ rawArgs.offset = offset;
44975
+ if (limit !== undefined)
44976
+ rawArgs.limit = limit;
44626
44977
  const mediaCandidate = isGithubResourcePath(file) || /\.(png|jpe?g|gif|webp|pdf)$/i.test(filePath) && await stat2(filePath).then((info) => info.isFile(), () => false);
44627
44978
  const visionCapability = mediaCandidate ? await measurePreStage("capability", () => currentSessionVisionCapability(ctx.client, context.sessionID)) : undefined;
44628
44979
  if (visionCapability !== undefined)
@@ -44678,7 +45029,7 @@ function createWriteTool(ctx, editToolName = "edit") {
44678
45029
  description: getWriteDescription(ctx, editToolName),
44679
45030
  args: {
44680
45031
  filePath: z7.string().describe("Path to the file to write (absolute or relative to project root)"),
44681
- content: z7.string().describe("The full content to write to the file")
45032
+ content: z7.string().describe("The full content to write to the file; for issue://N / pr://N, the comment body to post")
44682
45033
  },
44683
45034
  execute: async (args, context) => {
44684
45035
  const argsRecord = args;
@@ -44795,7 +45146,7 @@ ${backupBehavior}
44795
45146
  - Symbol replace includes decorators, attributes, and doc comments in range
44796
45147
  - Response is a compact server-rendered summary; before/after diff details are attached as UI metadata when available.`;
44797
45148
  }
44798
- var HASHLINE_EDIT_DESCRIPTION = [
45149
+ var HASHLINE_EDIT_GRAMMAR = [
44799
45150
  "Apply a hashline patch. Arguments are exactly `{patch}` where `patch` is a non-empty string. Server-owned preview control is outside this schema.",
44800
45151
  "",
44801
45152
  "Quick reference:",
@@ -44803,13 +45154,18 @@ var HASHLINE_EDIT_DESCRIPTION = [
44803
45154
  "- Same canonical path: multiple sections compose in patch order against pre-request coordinates.",
44804
45155
  "- 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.",
44805
45156
  "- 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 `-`.",
44806
- "- 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.",
44807
- "- 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."
45157
+ "- 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."
44808
45158
  ].join(`
44809
45159
  `);
45160
+ function hashlineEditDescription(config) {
45161
+ const bashRewrites = toolEnabled(config, "bash") && resolveBashConfig(config).rewrite;
45162
+ const tagSources = hashlineTagSourceSentence(bashRewrites, (name) => toolEnabled(config, name));
45163
+ return `${HASHLINE_EDIT_GRAMMAR}
45164
+ - ${tagSources}`;
45165
+ }
44810
45166
  function createHashlineEditTool(ctx) {
44811
45167
  return {
44812
- description: HASHLINE_EDIT_DESCRIPTION,
45168
+ description: hashlineEditDescription(ctx.config),
44813
45169
  args: {
44814
45170
  patch: z7.string().min(1).describe("Hashline patch text with one or more [path#TAG] sections and PUT/CUT/REM/MV operations")
44815
45171
  },
@@ -44884,8 +45240,7 @@ function createEditTool(ctx, writeToolName = "write") {
44884
45240
  throw new Error("'path' parameter is required");
44885
45241
  if (isGithubResourcePath(file)) {
44886
45242
  const edits = argsRecord.edits;
44887
- 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);
44888
- if (!onlyFindReplace) {
45243
+ if (!isFindReplaceOnlyEdit(argsRecord)) {
44889
45244
  throw new Error("edit: GitHub resources support only edits[] find/replace entries with oldString and optional newString");
44890
45245
  }
44891
45246
  const rawArgs = { path: file, edits };
@@ -45172,6 +45527,12 @@ function createMoveTool(ctx) {
45172
45527
  };
45173
45528
  }
45174
45529
  function hoistedTools(ctx) {
45530
+ const backgroundCompanions = resolveBashConfig(ctx.config).background ? {
45531
+ bash_status: createBashStatusTool(ctx),
45532
+ bash_write: createBashWriteTool(ctx),
45533
+ bash_watch: createBashWatchTool(ctx),
45534
+ bash_kill: createBashKillTool(ctx)
45535
+ } : {};
45175
45536
  const tools = {
45176
45537
  read: createReadTool(ctx),
45177
45538
  write: createWriteTool(ctx, "edit"),
@@ -45180,10 +45541,7 @@ function hoistedTools(ctx) {
45180
45541
  aft_delete: createDeleteTool(ctx),
45181
45542
  aft_move: createMoveTool(ctx),
45182
45543
  bash: createBashTool(ctx),
45183
- bash_status: createBashStatusTool(ctx),
45184
- bash_write: createBashWriteTool(ctx),
45185
- bash_watch: createBashWatchTool(ctx),
45186
- bash_kill: createBashKillTool(ctx)
45544
+ ...backgroundCompanions
45187
45545
  };
45188
45546
  return prepareToolMap(tools, { hashlineEffective: ctx.hashlineEffective });
45189
45547
  }
@@ -45579,6 +45937,9 @@ function navigationTools(ctx) {
45579
45937
 
45580
45938
  // src/tools/reading.ts
45581
45939
  var z11 = tool.schema;
45940
+ function isGithubPathArg(value) {
45941
+ return typeof value === "string" && (value.startsWith("issue://") || value.startsWith("pr://"));
45942
+ }
45582
45943
  function buildZoomTitle(args) {
45583
45944
  const targets = args.targets;
45584
45945
  if (!isEmptyParam(targets)) {
@@ -45602,6 +45963,13 @@ function buildZoomTitle(args) {
45602
45963
  }
45603
45964
  return path || "(no target)";
45604
45965
  }
45966
+ function outlineFocusSteer(config) {
45967
+ const zoomEnabled = toolEnabled(config, "aft_zoom");
45968
+ const reader = zoomEnabled ? "aft_zoom" : "read";
45969
+ const locate = toolEnabled(config, "aft_search") ? `aft_search + ${reader}` : reader;
45970
+ 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." : "";
45971
+ 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}`;
45972
+ }
45605
45973
  function readingTools(ctx) {
45606
45974
  const zoomEnabled = toolEnabled(ctx.config, "aft_zoom");
45607
45975
  const ghReadEnabled = resolveGithubConfig(ctx.config).read;
@@ -45609,9 +45977,9 @@ function readingTools(ctx) {
45609
45977
  const githubZoomDescription = whenGhReadEnabled(ghReadEnabled, "GitHub issue and pull-request discussion ordinals can be zoomed with `path: issue://…` or `path: pr://…` and `symbols`.");
45610
45978
  const tools = prepareToolMap({
45611
45979
  aft_outline: {
45612
- 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.
45980
+ 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) + `
45613
45981
 
45614
- `) + "Pass a single `target`:\n" + ` • file path → outline that file (with signatures)
45982
+ ` + "Pass a single `target`:\n" + ` • file path → outline that file (with signatures)
45615
45983
  ` + ` • directory path → outline source files under it
45616
45984
  ` + ` • URL (http:// or https://) → fetch and outline a remote HTML/Markdown document
45617
45985
  ` + " • array of paths → outline multiple files in one call; with files:true, every path must be a directory" + (githubOutlineDescription ? `
@@ -45660,7 +46028,7 @@ ${githubOutlineDescription}` : "") + "\n\nWhen a list is cut, the reply ends wit
45660
46028
  }
45661
46029
  },
45662
46030
  aft_zoom: {
45663
- 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 ? `
46031
+ 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 ? `
45664
46032
 
45665
46033
  ${githubZoomDescription}` : ""),
45666
46034
  args: {
@@ -45676,37 +46044,22 @@ ${githubZoomDescription}` : ""),
45676
46044
  path: z11.string().describe("Path to file (absolute or relative to project root)"),
45677
46045
  symbol: z11.string().describe("Symbol name in that file")
45678
46046
  }))
45679
- ]).optional().describe("Cross-file batch: `{ path, symbol }` or an array of them. Mutually exclusive with path/url/symbols."),
46047
+ ]).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."),
45680
46048
  contextLines: optionalInt(1, Number.MAX_SAFE_INTEGER).describe("Lines of context before/after the symbol (default: 3)"),
45681
46049
  callgraph: z11.boolean().optional().describe("Include call-graph annotations (calls-out / called-by within the same file). Default false; off keeps zoom output minimal.")
45682
46050
  },
45683
46051
  execute: async (args, context) => {
45684
- const hasTargetsProvided = (t) => {
45685
- if (isEmptyParam(t))
45686
- return false;
45687
- const entryEmpty = (entry) => {
45688
- if (!entry || typeof entry !== "object")
45689
- return true;
45690
- const fp = entry.path;
45691
- const sym = entry.symbol;
45692
- const fpEmpty = typeof fp !== "string" || fp.length === 0;
45693
- const symEmpty = typeof sym !== "string" || sym.length === 0;
45694
- return fpEmpty && symEmpty;
45695
- };
45696
- if (Array.isArray(t))
45697
- return !t.every(entryEmpty);
45698
- return !entryEmpty(t);
45699
- };
45700
46052
  const targetsInput = coerceJsonCollectionParam(args.targets, "targets");
45701
- const hasFilePath = !isEmptyParam(args.path);
45702
- const hasUrl = !isEmptyParam(args.url);
45703
- const hasTargets = hasTargetsProvided(targetsInput);
45704
- const hasSymbols = !isEmptyParam(args.symbols);
46053
+ const targetEntries = usableZoomTargets(targetsInput);
46054
+ const hasFilePath = !isBlankParam(args.path);
46055
+ const hasUrl = !isBlankParam(args.url);
46056
+ const hasTargets = targetEntries.length > 0;
46057
+ const hasSymbols = !isBlankParam(args.symbols);
45705
46058
  const wantCallgraph = coerceBoolean(args.callgraph);
45706
46059
  const contextLines = coerceOptionalInt(args.contextLines, "contextLines", 1, Number.MAX_SAFE_INTEGER);
45707
46060
  const zoomTitle = buildZoomTitle({
45708
46061
  ...args,
45709
- targets: targetsInput
46062
+ targets: hasTargets ? targetEntries : undefined
45710
46063
  });
45711
46064
  const zoomDisplay = { title: zoomTitle };
45712
46065
  if (hasFilePath)
@@ -45717,7 +46070,7 @@ ${githubZoomDescription}` : ""),
45717
46070
  zoomDisplay.symbols = typeof args.symbols === "string" ? args.symbols : JSON.stringify(args.symbols);
45718
46071
  }
45719
46072
  if (hasTargets)
45720
- zoomDisplay.targets = JSON.stringify(targetsInput);
46073
+ zoomDisplay.targets = JSON.stringify(targetEntries);
45721
46074
  if (contextLines !== undefined)
45722
46075
  zoomDisplay.contextLines = contextLines;
45723
46076
  if (wantCallgraph)
@@ -45728,23 +46081,24 @@ ${githubZoomDescription}` : ""),
45728
46081
  metadata: zoomDisplay
45729
46082
  });
45730
46083
  if (hasTargets) {
45731
- if (hasFilePath || hasUrl || hasSymbols) {
45732
- throw new Error("'targets' is mutually exclusive with 'path', 'url', and 'symbols'");
46084
+ if (hasFilePath && hasUrl) {
46085
+ throw new Error("Provide exactly ONE of 'path' or 'url' — not both");
45733
46086
  }
45734
- const targets = Array.isArray(targetsInput) ? targetsInput : [targetsInput];
45735
- if (targets.length === 0) {
45736
- throw new Error("'targets' must be a non-empty object or array");
45737
- }
45738
- for (const [i, entry] of targets.entries()) {
45739
- const targetPath = entry?.path;
45740
- if (typeof targetPath !== "string" || targetPath.length === 0) {
46087
+ const targets = targetEntries.map((entry, i) => {
46088
+ if (!entry || typeof entry !== "object" || Array.isArray(entry)) {
45741
46089
  throw new Error(`targets[${i}].path must be a non-empty string`);
45742
46090
  }
45743
- if (typeof entry.symbol !== "string" || entry.symbol.length === 0) {
45744
- throw new Error(`targets[${i}].symbol must be a non-empty string`);
45745
- }
45746
- }
45747
- const resolvedTargets = await Promise.all(targets.map((t) => resolvePathArg(ctx, context, t.path)));
46091
+ const record = entry;
46092
+ const targetPath = isBlankParam(record.path) ? record.filePath : record.path;
46093
+ return {
46094
+ path: typeof targetPath === "string" ? targetPath : "",
46095
+ symbol: typeof record.symbol === "string" ? record.symbol : ""
46096
+ };
46097
+ });
46098
+ const localPaths = targets.map((target) => target.path).filter((path) => path.trim().length > 0 && !isGithubPathArg(path));
46099
+ if (hasFilePath && !isGithubPathArg(args.path))
46100
+ localPaths.push(args.path);
46101
+ const resolvedTargets = await Promise.all(localPaths.map((path) => resolvePathArg(ctx, context, path)));
45748
46102
  const permissionDenied = await assertPathExternalPermissions(ctx, context, resolvedTargets);
45749
46103
  if (permissionDenied)
45750
46104
  return permissionDeniedResponse(permissionDenied);
@@ -45754,6 +46108,12 @@ ${githubZoomDescription}` : ""),
45754
46108
  symbol: target.symbol
45755
46109
  }))
45756
46110
  };
46111
+ if (hasFilePath)
46112
+ rawArgs.filePath = args.path;
46113
+ else if (hasUrl)
46114
+ rawArgs.url = args.url;
46115
+ if (hasSymbols)
46116
+ rawArgs.symbols = args.symbols;
45757
46117
  if (contextLines !== undefined)
45758
46118
  rawArgs.contextLines = contextLines;
45759
46119
  if (wantCallgraph)
@@ -45770,7 +46130,7 @@ ${githubZoomDescription}` : ""),
45770
46130
  if (hasFilePath && hasUrl) {
45771
46131
  throw new Error("Provide exactly ONE of 'path' or 'url' — not both");
45772
46132
  }
45773
- const githubPath = hasFilePath && (String(args.path).startsWith("issue://") || String(args.path).startsWith("pr://")) || hasUrl && (String(args.url).startsWith("issue://") || String(args.url).startsWith("pr://"));
46133
+ const githubPath = hasFilePath && isGithubPathArg(args.path) || hasUrl && isGithubPathArg(args.url);
45774
46134
  if (!hasUrl && !githubPath) {
45775
46135
  const file = await resolvePathArg(ctx, context, args.path);
45776
46136
  const permissionDenied = await assertPathExternalPermissions(ctx, context, file);
@@ -45861,7 +46221,7 @@ function safetyTools(ctx) {
45861
46221
  ` + "Use checkpoint before risky multi-file changes. Use undo for quick single-file rollback.",
45862
46222
  args: {
45863
46223
  op: z12.enum(["undo", "history", "checkpoint", "restore", "list"]).describe("Safety operation"),
45864
- 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"),
46224
+ 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"),
45865
46225
  name: z12.string().optional().describe("Checkpoint name (required for checkpoint, restore)"),
45866
46226
  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)")
45867
46227
  },
@@ -46297,8 +46657,20 @@ function buildAftToolDefinitions(ctx, config, onUnknownDisabled) {
46297
46657
  const unknown = unknownDisabledTools(disabled);
46298
46658
  if (unknown.length > 0)
46299
46659
  onUnknownDisabled?.(unknown);
46660
+ describeBashForRegisteredSurface(allTools, config);
46300
46661
  return allTools;
46301
46662
  }
46663
+ function describeBashForRegisteredSurface(allTools, config) {
46664
+ const bash = allTools.bash;
46665
+ if (!bash)
46666
+ return;
46667
+ const bashCfg = resolveBashConfig(config);
46668
+ bash.description = bashToolDescription("aft_search" in allTools, bashCfg.compress, bashCfg.background, bashCfg.detach_on_user_message, "aft_zoom" in allTools, "bash_watch" in allTools, {
46669
+ outline: "aft_outline" in allTools,
46670
+ status: "bash_status" in allTools,
46671
+ write: "bash_write" in allTools
46672
+ });
46673
+ }
46302
46674
  function registerAftTools(context, location, definitions, consumers = {}) {
46303
46675
  const projected = Object.entries(definitions).map(([name, definition]) => projectV2Tool(name, definition, location, consumers));
46304
46676
  return context.tool.transform((editor) => {
@@ -49009,7 +49381,10 @@ function registerShutdownCleanup(fn) {
49009
49381
 
49010
49382
  // src/workflow-hints.ts
49011
49383
  var HEADING = "## IMPORTANT NOTICE about your tools";
49012
- 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.";
49384
+ function hashlineTagSourceHint(bashRewritesMintTags, isRegistered = () => true) {
49385
+ return `**Hashline edit tags**: ${hashlineTagSourceSentence(bashRewritesMintTags, isRegistered)}`;
49386
+ }
49387
+ var HASHLINE_TAG_SOURCE_HINT = hashlineTagSourceHint(true);
49013
49388
  function buildWorkflowHints(opts) {
49014
49389
  const sections = [];
49015
49390
  const grepName = "grep";
@@ -49026,6 +49401,8 @@ function buildWorkflowHints(opts) {
49026
49401
  const hasInspect = !opts.disabledTools.has("aft_inspect");
49027
49402
  const hasBash = !opts.disabledTools.has(bashName);
49028
49403
  const hasBgBash = hasBash && opts.bashBackgroundEnabled && !opts.disabledTools.has(bashStatusName);
49404
+ const hasWatch = !opts.disabledTools.has("bash_watch");
49405
+ const hasWrite = !opts.disabledTools.has(bashWriteName);
49029
49406
  if (hasBash && opts.bashCompressionEnabled) {
49030
49407
  sections.push([
49031
49408
  "**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:",
@@ -49067,17 +49444,19 @@ function buildWorkflowHints(opts) {
49067
49444
  ].join(`
49068
49445
  `));
49069
49446
  }
49070
- if (hasBash && hasBgBash) {
49447
+ if (hasBgBash && hasWatch) {
49071
49448
  sections.push([
49072
49449
  `**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.`,
49073
49450
  "- `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.",
49074
49451
  "- `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."
49075
49452
  ].join(`
49076
49453
  `));
49454
+ }
49455
+ if (hasBgBash && hasWrite) {
49077
49456
  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: "..." })\`.`);
49078
49457
  }
49079
49458
  if (opts.hashlineEffective === true) {
49080
- sections.push(HASHLINE_TAG_SOURCE_HINT);
49459
+ sections.push(hashlineTagSourceHint(hasBash && opts.bashRewriteEnabled !== false, (name) => !opts.disabledTools.has(name)));
49081
49460
  }
49082
49461
  if (sections.length === 0) {
49083
49462
  return null;
@@ -49092,13 +49471,32 @@ ${sections.join(`
49092
49471
  `)}`;
49093
49472
  }
49094
49473
  function buildHintsFromConfig(config, disabledTools, hashlineEffective = false) {
49474
+ const bashCfg = resolveBashConfig(config);
49095
49475
  return buildWorkflowHints({
49096
- bashBackgroundEnabled: resolveBashConfig(config).background,
49097
- bashCompressionEnabled: resolveBashConfig(config).compress,
49476
+ bashBackgroundEnabled: bashCfg.background,
49477
+ bashCompressionEnabled: bashCfg.compress,
49478
+ bashRewriteEnabled: bashCfg.rewrite,
49098
49479
  disabledTools,
49099
49480
  hashlineEffective
49100
49481
  });
49101
49482
  }
49483
+ var WORKFLOW_HINT_TOOL_NAMES = [
49484
+ "aft_outline",
49485
+ "aft_zoom",
49486
+ "aft_search",
49487
+ "aft_callgraph",
49488
+ "aft_inspect",
49489
+ "grep",
49490
+ "read",
49491
+ "bash",
49492
+ "bash_status",
49493
+ "bash_watch",
49494
+ "bash_write"
49495
+ ];
49496
+ function buildHintsForRegisteredTools(config, registeredTools, hashlineEffective = false) {
49497
+ const absent = new Set(WORKFLOW_HINT_TOOL_NAMES.filter((name) => !registeredTools.has(name)));
49498
+ return buildHintsFromConfig(config, absent, hashlineEffective);
49499
+ }
49102
49500
  function appendHintsToSystem(system, hintsBlock) {
49103
49501
  if (!hintsBlock)
49104
49502
  return;
@@ -49469,36 +49867,13 @@ Install: ${getManualInstallHint()}`);
49469
49867
  signal: autoUpdateAbort.signal,
49470
49868
  storageDir: ctx.storageDir
49471
49869
  });
49472
- const HINTS_TOOL_NAMES = [
49473
- "aft_outline",
49474
- "aft_zoom",
49475
- "aft_search",
49476
- "aft_callgraph",
49477
- "aft_inspect",
49478
- "grep",
49479
- "read",
49480
- "bash",
49481
- "bash_status"
49482
- ];
49483
49870
  const registeredTools = new Set(Object.keys(allTools));
49484
49871
  const { hashlineEditRegistered, aftSearchRegistered } = applyToolSurfaceOverrides(pool, aftConfig, registeredTools);
49485
49872
  ctx.hashlineEffective = hashlineEditRegistered;
49486
49873
  const hashlineDowngrade = reportHashlineDowngrade(aftConfig, registeredTools, (message) => deliverConfigMigrationWarnings(registrationRoot, [message]));
49487
49874
  log2(`hashline activation decision requested=${aftConfig.edit_mode === "hashline"} ` + `edit_slot_survives=${hashlineEditRegistered} effective=${ctx.hashlineEffective}` + (hashlineDowngrade ? ` downgraded=${hashlineDowngrade.code}` : ""));
49488
49875
  ctx.aftSearchRegistered = aftSearchRegistered;
49489
- for (const name of ["bash"]) {
49490
- const def = allTools[name];
49491
- if (def) {
49492
- const bashCfg = resolveBashConfig(aftConfig);
49493
- def.description = bashToolDescription(aftSearchRegistered, bashCfg.compress, bashCfg.background, bashCfg.detach_on_user_message);
49494
- }
49495
- }
49496
- const hintsAbsentTools = new Set;
49497
- for (const name of HINTS_TOOL_NAMES) {
49498
- if (!registeredTools.has(name))
49499
- hintsAbsentTools.add(name);
49500
- }
49501
- const hintsBlock = buildHintsFromConfig(aftConfig, hintsAbsentTools, hashlineEditRegistered);
49876
+ const hintsBlock = buildHintsForRegisteredTools(aftConfig, registeredTools, hashlineEditRegistered);
49502
49877
  if (hintsBlock) {
49503
49878
  log2(`Workflow hints injected (${hintsBlock.length} chars)`);
49504
49879
  }
@@ -49644,10 +50019,10 @@ Install: ${getManualInstallHint()}`);
49644
50019
  var src_default = plugin;
49645
50020
 
49646
50021
  // src/entry/server-runtime.mjs
49647
- import { Effect as Effect7 } from "effect";
50022
+ import { Effect as Effect9 } from "effect";
49648
50023
 
49649
50024
  // src/rpc/register.ts
49650
- import { Effect as Effect3 } from "effect";
50025
+ import { Effect as Effect4 } from "effect";
49651
50026
 
49652
50027
  // src/tui/aft-rpc.ts
49653
50028
  import { Schema } from "effect";
@@ -49726,7 +50101,7 @@ function isStatusSubscribable(bridge) {
49726
50101
  return typeof bridge.subscribeStatus === "function";
49727
50102
  }
49728
50103
  function locationRoot(location) {
49729
- return location.project?.canonical ?? location.project?.directory ?? location.directory;
50104
+ return location.directory;
49730
50105
  }
49731
50106
  function placeholderStatus() {
49732
50107
  return {
@@ -49743,7 +50118,7 @@ function restoreOwnProperty(target, key, descriptor) {
49743
50118
  Reflect.deleteProperty(target, key);
49744
50119
  }
49745
50120
  function registerAftRpc(context, location, pool) {
49746
- return Effect3.gen(function* () {
50121
+ return Effect4.gen(function* () {
49747
50122
  const root = locationRoot(location);
49748
50123
  const bridgeUnsubscribes = new Map;
49749
50124
  let disposed = false;
@@ -49751,7 +50126,7 @@ function registerAftRpc(context, location, pool) {
49751
50126
  const emit = async (name, payload) => {
49752
50127
  if (disposed)
49753
50128
  return;
49754
- await Effect3.runPromise(registration.events.emit(name, payload));
50129
+ await Effect4.runPromise(registration.events.emit(name, payload));
49755
50130
  };
49756
50131
  const publishStatus = (snapshot) => {
49757
50132
  const sessionID = sessionFromSnapshot(snapshot);
@@ -49765,21 +50140,21 @@ function registerAftRpc(context, location, pool) {
49765
50140
  return;
49766
50141
  bridgeUnsubscribes.set(bridge, bridge.subscribeStatus(publishStatus));
49767
50142
  };
49768
- const getStatus = (input) => Effect3.suspend(() => {
50143
+ const getStatus = (input) => Effect4.suspend(() => {
49769
50144
  const sessionID = input.sessionID || "rpc";
49770
50145
  const bridge = pool.getActiveBridgeForRoot(root);
49771
50146
  if (!bridge)
49772
- return Effect3.succeed(placeholderStatus());
50147
+ return Effect4.succeed(placeholderStatus());
49773
50148
  observeBridge(bridge);
49774
50149
  const cached = bridge.getCachedStatus();
49775
50150
  const cachedSessionID = cached ? sessionFromSnapshot(cached) : undefined;
49776
50151
  if (cached && cachedSessionID === sessionID) {
49777
- return Effect3.succeed({ success: true, ...cached });
50152
+ return Effect4.succeed({ success: true, ...cached });
49778
50153
  }
49779
- return Effect3.tryPromise({
50154
+ return Effect4.tryPromise({
49780
50155
  try: (signal) => bridge.send("status", { session_id: sessionID }, { abortSignal: signal }),
49781
50156
  catch: (error) => error
49782
- }).pipe(Effect3.tap((response) => Effect3.sync(() => {
50157
+ }).pipe(Effect4.tap((response) => Effect4.sync(() => {
49783
50158
  if (response.success !== false)
49784
50159
  bridge.cacheStatusSnapshot(response);
49785
50160
  })));
@@ -49840,21 +50215,21 @@ function registerAftRpc(context, location, pool) {
49840
50215
  bridgeUnsubscribes.clear();
49841
50216
  restoreOwnProperty(pool, "getBridge", ownGetBridge);
49842
50217
  restoreOwnProperty(pool, "toolCall", ownToolCall);
49843
- await Effect3.runPromise(registration.dispose);
50218
+ await Effect4.runPromise(registration.dispose);
49844
50219
  }
49845
50220
  };
49846
50221
  });
49847
50222
  }
49848
50223
  function registerAftConfigErrorRpc(context, message) {
49849
- return Effect3.gen(function* () {
50224
+ return Effect4.gen(function* () {
49850
50225
  let disposed = false;
49851
50226
  const registration = yield* context.rpc.register(AftRpc, {
49852
- getStatus: () => Effect3.succeed(configErrorStatusSnapshot(message))
50227
+ getStatus: () => Effect4.succeed(configErrorStatusSnapshot(message))
49853
50228
  });
49854
50229
  const emit = async (name, payload) => {
49855
50230
  if (disposed)
49856
50231
  return;
49857
- await Effect3.runPromise(registration.events.emit(name, payload));
50232
+ await Effect4.runPromise(registration.events.emit(name, payload));
49858
50233
  };
49859
50234
  return {
49860
50235
  emitStatusInvalidated: (payload = {}) => emit("statusInvalidated", payload),
@@ -49864,14 +50239,14 @@ function registerAftConfigErrorRpc(context, message) {
49864
50239
  if (disposed)
49865
50240
  return;
49866
50241
  disposed = true;
49867
- await Effect3.runPromise(registration.dispose);
50242
+ await Effect4.runPromise(registration.dispose);
49868
50243
  }
49869
50244
  };
49870
50245
  });
49871
50246
  }
49872
50247
 
49873
50248
  // src/permissions/v2.ts
49874
- import { Effect as Effect4 } from "effect";
50249
+ import { Effect as Effect5 } from "effect";
49875
50250
 
49876
50251
  class PermissionDeniedError extends Error {
49877
50252
  name = "PermissionDeniedError";
@@ -49927,7 +50302,7 @@ function detail(error) {
49927
50302
  return error instanceof Error ? error.message : String(error);
49928
50303
  }
49929
50304
  async function runHostEffect(effect) {
49930
- return await Effect4.runPromise(effect);
50305
+ return await Effect5.runPromise(effect);
49931
50306
  }
49932
50307
  async function sessionRecord(host, sessionID) {
49933
50308
  if (!sessionID)
@@ -49968,7 +50343,7 @@ function deniedMessage(request, decision) {
49968
50343
  }
49969
50344
  function promptUnavailableMessage(request, decision, observed) {
49970
50345
  const resource = decision.resource ?? "*";
49971
- return `The ${JSON.stringify(request.permission)} operation needs interactive approval: this ` + `session's permission rules resolve it to "ask" for ${JSON.stringify(resource)}, and AFT ` + `could not reach the OpenCode service to raise a prompt (${observed}). Start OpenCode's ` + "background service and retry, or run it without a prompt by adding " + `{"action": ${JSON.stringify(request.permission)}, "resource": ${JSON.stringify(resource)}, ` + '"effect": "allow"} to the "permissions" list of the agent or session in your OpenCode config.';
50346
+ return `The ${JSON.stringify(request.permission)} operation needs interactive approval: this ` + `session's permission rules resolve it to "ask" for ${JSON.stringify(resource)}, and AFT ` + `could not reach the OpenCode service to raise a prompt (${observed}). Once the server is ` + "reachable, retry; or run it without a prompt by adding " + `{"action": ${JSON.stringify(request.permission)}, "resource": ${JSON.stringify(resource)}, ` + '"effect": "allow"} to the "permissions" list of the agent or session in your OpenCode config.';
49972
50347
  }
49973
50348
  function promptUnansweredMessage(request, decision, observed) {
49974
50349
  const resource = decision.resource ?? "*";
@@ -50176,6 +50551,9 @@ async function requestPermission(host, request, context, prompt, limits = {}) {
50176
50551
  }
50177
50552
 
50178
50553
  // src/permissions/v2-service.ts
50554
+ import { readdir, readFile as readFile3, readlink } from "node:fs/promises";
50555
+ import { homedir as homedir15 } from "node:os";
50556
+ import { join as join26 } from "node:path";
50179
50557
  init_promise();
50180
50558
 
50181
50559
  // ../../node_modules/.bun/@opencode+client@2.0.11+bdc4bbd92cece36b/node_modules/@opencode/client/dist/chunks/service-txckps9j.js
@@ -50183,26 +50561,6 @@ var stderrLimit = 8 * 1024;
50183
50561
 
50184
50562
  // ../../node_modules/.bun/@opencode+client@2.0.11+bdc4bbd92cece36b/node_modules/@opencode/client/dist/chunks/service-xgnj2w4v.js
50185
50563
  var timings = new WeakMap;
50186
- var defaultEnsureTiming2 = {
50187
- pollInterval: 25,
50188
- requestTimeout: 2000,
50189
- spawnDelay: 5000,
50190
- maxSpawnDelay: 30000,
50191
- promiseTimeout: 120000,
50192
- stopPollInterval: 50,
50193
- stopPollAttempts: 100
50194
- };
50195
-
50196
- // ../../node_modules/.bun/@opencode+client@2.0.11+bdc4bbd92cece36b/node_modules/@opencode/client/dist/chunks/service-sxt88q69.js
50197
- function matchesVersion2(version, options) {
50198
- if (options.version === undefined)
50199
- return true;
50200
- if (version === undefined)
50201
- return false;
50202
- if (typeof options.version === "function")
50203
- return options.version(version);
50204
- return version === options.version;
50205
- }
50206
50564
 
50207
50565
  // ../../node_modules/.bun/@opencode+client@2.0.11+bdc4bbd92cece36b/node_modules/@opencode/client/dist/chunks/service-rg0h87gt.js
50208
50566
  init_service_9rqn6x4v();
@@ -50312,22 +50670,6 @@ function isHandoff(value) {
50312
50670
  return typeof value === "object" && value !== null && "directory" in value && typeof value.directory === "string" && "instanceID" in value && typeof value.instanceID === "string" && "ticket" in value && typeof value.ticket === "string" && "expiresAt" in value && typeof value.expiresAt === "number" && Number.isFinite(value.expiresAt);
50313
50671
  }
50314
50672
  // ../../node_modules/.bun/@opencode+client@2.0.11+bdc4bbd92cece36b/node_modules/@opencode/client/dist/promise/service.js
50315
- import { readFile as readFile3, rm as rm3 } from "node:fs/promises";
50316
- import { homedir as homedir15 } from "node:os";
50317
- import { join as join26 } from "node:path";
50318
- async function discover(options = {}) {
50319
- const found = (await registered(options.file)).service;
50320
- if (found?.state !== "ready")
50321
- return;
50322
- if (!found.compatible)
50323
- return;
50324
- if (!matchesVersion2(found.version, options))
50325
- return;
50326
- return found.endpoint;
50327
- }
50328
- function fallback() {
50329
- return join26(process.env["XDG_STATE_HOME"] ?? join26(homedir15(), ".local", "state"), "opencode", "service.json");
50330
- }
50331
50673
  function headers(endpoint) {
50332
50674
  if (endpoint.auth === undefined)
50333
50675
  return;
@@ -50335,113 +50677,385 @@ function headers(endpoint) {
50335
50677
  authorization: "Basic " + Buffer.from(endpoint.auth.username + ":" + endpoint.auth.password).toString("base64")
50336
50678
  };
50337
50679
  }
50338
- async function read2(file) {
50339
- const text = await readFile3(file ?? fallback(), "utf8").catch(() => {
50340
- return;
50341
- });
50342
- if (text === undefined)
50680
+
50681
+ // src/permissions/v2-service.ts
50682
+ var OPENCODE_BASIC_AUTH_USERNAME = "opencode";
50683
+ var DEFAULT_PROBE_TIMEOUT_MS2 = 2000;
50684
+ var LISTEN_LOOKUP_TIMEOUT_MS = 5000;
50685
+ var HOW_TO_FIX = `set "opencode.server_url" in your user AFT config to this server's URL, with ` + '"opencode.server_password_env" naming the environment variable that holds its password ' + "(for OpenChamber, the host and port given as OPENCODE_HOST / OPENCODE_PORT), or start " + "OpenCode with OPENCODE_SERVER_PASSWORD set so AFT can authenticate to it";
50686
+ function detail2(error) {
50687
+ return error instanceof Error ? error.message : String(error);
50688
+ }
50689
+ function promptClient(endpoint) {
50690
+ const client = exports_client.make({ baseUrl: endpoint.url, headers: headers(endpoint) });
50691
+ return {
50692
+ permission: {
50693
+ create: (input) => client.permission.create(input)
50694
+ },
50695
+ event: {
50696
+ subscribe: async () => ({ stream: client.event.subscribe() })
50697
+ }
50698
+ };
50699
+ }
50700
+ function basicAuth(password) {
50701
+ if (!password)
50343
50702
  return;
50703
+ return { type: "basic", username: OPENCODE_BASIC_AUTH_USERNAME, password };
50704
+ }
50705
+ function ownServerPassword(env) {
50706
+ return env.OPENCODE_PASSWORD || env.OPENCODE_SERVER_PASSWORD || undefined;
50707
+ }
50708
+ async function probe(server, endpoint, timeoutMs) {
50709
+ let response;
50344
50710
  try {
50345
- return JSON.parse(text);
50711
+ response = await server.fetch(`${endpoint.url.replace(/\/+$/, "")}/api/info`, {
50712
+ headers: headers(endpoint) ?? {},
50713
+ signal: AbortSignal.timeout(timeoutMs)
50714
+ });
50715
+ } catch (error) {
50716
+ return { kind: "failed", detail: detail2(error) };
50717
+ }
50718
+ if (response.status === 401 || response.status === 403) {
50719
+ await response.body?.cancel().catch(() => {});
50720
+ return { kind: "unauthorized", status: response.status };
50721
+ }
50722
+ if (!response.ok && response.status !== 503) {
50723
+ await response.body?.cancel().catch(() => {});
50724
+ return { kind: "failed", detail: `it answered HTTP ${response.status}` };
50725
+ }
50726
+ let body;
50727
+ try {
50728
+ body = await response.json();
50346
50729
  } catch {
50347
- return;
50730
+ body = undefined;
50348
50731
  }
50732
+ const pid = body?.pid;
50733
+ if (typeof pid === "number" && Number.isInteger(pid))
50734
+ return { kind: "answered", pid };
50735
+ return response.ok ? { kind: "answered" } : { kind: "failed", detail: `it answered HTTP ${response.status}` };
50349
50736
  }
50350
- async function probeResult(info, timeout = defaultEnsureTiming2.requestTimeout) {
50737
+ async function configuredServer(settings, server, timeoutMs) {
50738
+ const raw = settings.server_url.trim();
50739
+ let url;
50740
+ try {
50741
+ url = new URL(raw);
50742
+ } catch {
50743
+ return {
50744
+ reason: "server_url_invalid",
50745
+ unavailable: `"opencode.server_url" ${JSON.stringify(raw)} in your AFT config is not a URL; set it to the server's base URL, for example "http://127.0.0.1:4096"`
50746
+ };
50747
+ }
50748
+ if (url.protocol !== "http:" && url.protocol !== "https:" || url.username || url.password) {
50749
+ return {
50750
+ reason: "server_url_invalid",
50751
+ unavailable: `"opencode.server_url" ${JSON.stringify(raw)} must be an http(s) URL without credentials; ` + 'put the password in an environment variable and name it in "opencode.server_password_env"'
50752
+ };
50753
+ }
50754
+ const variable = settings.server_password_env?.trim();
50755
+ let password;
50756
+ if (variable) {
50757
+ password = server.env[variable] || undefined;
50758
+ if (!password) {
50759
+ return {
50760
+ reason: "server_url_invalid",
50761
+ unavailable: `the environment variable ${variable} named by "opencode.server_password_env" is not ` + "set in the OpenCode process; set it to the server's password and restart OpenCode"
50762
+ };
50763
+ }
50764
+ } else {
50765
+ password = ownServerPassword(server.env);
50766
+ }
50351
50767
  const endpoint = {
50352
- url: info.url,
50353
- auth: info.password === undefined ? undefined : { type: "basic", username: "opencode", password: info.password }
50354
- };
50355
- const signal = AbortSignal.timeout(timeout);
50356
- const result = await fetch(new URL("/api/info", info.url), { headers: headers(endpoint), signal }).then(async (response) => ({
50357
- response,
50358
- body: response.status === 404 ? undefined : await response.json()
50359
- })).then((value) => ({ value }), (cause) => ({ cause }));
50360
- if ("cause" in result)
50361
- return { service: undefined, timedOut: signal.aborted };
50362
- const response = result.value.response;
50363
- if (response.status === 404)
50768
+ url: url.href.replace(/\/+$/, ""),
50769
+ auth: basicAuth(password)
50770
+ };
50771
+ const result = await probe(server, endpoint, timeoutMs);
50772
+ if (result.kind === "unauthorized") {
50364
50773
  return {
50365
- service: {
50366
- info,
50367
- endpoint,
50368
- version: info.version,
50369
- state: "ready",
50370
- compatible: false
50371
- },
50372
- timedOut: false
50774
+ reason: "server_url_unreachable",
50775
+ unavailable: `the server at "opencode.server_url" ${endpoint.url} refused AFT's credentials ` + `(HTTP ${result.status}); set "opencode.server_password_env" to the name of the ` + "environment variable holding that server's password"
50373
50776
  };
50374
- const serverInfo = decodeInfo(result.value.body);
50375
- if (serverInfo !== undefined) {
50376
- if (serverInfo.pid !== info.pid)
50377
- return { service: undefined, timedOut: false };
50378
- if (info.version !== undefined && serverInfo.version !== info.version)
50379
- return { service: undefined, timedOut: false };
50777
+ }
50778
+ if (result.kind === "failed") {
50380
50779
  return {
50381
- service: {
50382
- info,
50383
- endpoint,
50384
- version: serverInfo.version,
50385
- state: response.ok ? "ready" : response.status === 500 ? "failed" : "waiting",
50386
- compatible: true
50387
- },
50388
- timedOut: false
50780
+ reason: "server_url_unreachable",
50781
+ unavailable: `the server at "opencode.server_url" ${endpoint.url} did not answer (${result.detail})`
50389
50782
  };
50390
50783
  }
50391
- return { service: undefined, timedOut: false };
50784
+ if (result.pid !== undefined && result.pid !== server.pid) {
50785
+ warn2(`[permissions] opencode.server_url ${endpoint.url} is served by process ${result.pid}, not ` + `by this OpenCode process (${server.pid}); permission prompts are raised there`);
50786
+ }
50787
+ return { client: promptClient(endpoint) };
50392
50788
  }
50393
- function decodeInfo(input) {
50394
- if (typeof input !== "object" || input === null)
50395
- return;
50396
- if (!("version" in input) || typeof input.version !== "string")
50397
- return;
50398
- if (!("pid" in input) || typeof input.pid !== "number" || !Number.isInteger(input.pid) || input.pid < 0)
50789
+ function flagValue(argv, name) {
50790
+ let value;
50791
+ for (let index = 0;index < argv.length; index += 1) {
50792
+ const arg = argv[index];
50793
+ if (arg === `--${name}`)
50794
+ value = argv[index + 1];
50795
+ else if (arg?.startsWith(`--${name}=`))
50796
+ value = arg.slice(name.length + 3);
50797
+ }
50798
+ return value;
50799
+ }
50800
+ function ownListenAddress(argv) {
50801
+ const rawHost = flagValue(argv, "hostname")?.trim();
50802
+ let hostname = rawHost ? rawHost.replace(/^\[(.*)\]$/, "$1") : "127.0.0.1";
50803
+ if (hostname === "0.0.0.0")
50804
+ hostname = "127.0.0.1";
50805
+ if (hostname === "::")
50806
+ hostname = "::1";
50807
+ const rawPort = flagValue(argv, "port")?.trim();
50808
+ const parsed = rawPort !== undefined && /^\d+$/.test(rawPort) ? Number(rawPort) : undefined;
50809
+ const port = parsed !== undefined && parsed <= 65535 ? parsed : undefined;
50810
+ return { hostname, port, stdio: argv.includes("--stdio") };
50811
+ }
50812
+ function serverURL(hostname, port) {
50813
+ return `http://${hostname.includes(":") ? `[${hostname}]` : hostname}:${port}`;
50814
+ }
50815
+ async function ownRegistration(server, timeoutMs, observed) {
50816
+ let registrations;
50817
+ try {
50818
+ registrations = await server.registrations();
50819
+ } catch (error) {
50820
+ observed.push(`reading OpenCode service registrations failed: ${detail2(error)}`);
50399
50821
  return;
50400
- return { version: input.version, pid: input.pid };
50822
+ }
50823
+ for (const registration of registrations) {
50824
+ let info;
50825
+ try {
50826
+ info = JSON.parse(registration.text);
50827
+ } catch {
50828
+ continue;
50829
+ }
50830
+ if (typeof info?.url !== "string" || typeof info.pid !== "number")
50831
+ continue;
50832
+ if (info.pid !== server.pid) {
50833
+ observed.push(`the service registered in ${registration.path} is process ${info.pid}, not this one (${server.pid}), so it was not used`);
50834
+ continue;
50835
+ }
50836
+ const endpoint = {
50837
+ url: info.url.replace(/\/+$/, ""),
50838
+ auth: basicAuth(typeof info.password === "string" ? info.password : undefined)
50839
+ };
50840
+ const result = await probe(server, endpoint, timeoutMs);
50841
+ if (result.kind === "answered" && result.pid === server.pid) {
50842
+ return { client: promptClient(endpoint) };
50843
+ }
50844
+ observed.push(`this process's own registration ${registration.path} did not verify at ${endpoint.url}`);
50845
+ }
50846
+ return;
50401
50847
  }
50402
- async function registered(file, timeout) {
50403
- const info = await read2(file);
50404
- if (info === undefined)
50405
- return { info: undefined, service: undefined, timedOut: false };
50406
- return { info, ...await probeResult(info, timeout) };
50848
+ function reachableHost(address) {
50849
+ const host = address.replace(/^\[(.*)\]$/, "$1");
50850
+ if (host === "*" || host === "0.0.0.0" || host === "")
50851
+ return "127.0.0.1";
50852
+ if (/^[0:]+$/.test(host))
50853
+ return "::1";
50854
+ return host;
50855
+ }
50856
+ async function ownCandidates(server, listen, observed) {
50857
+ const declared = listen.port !== undefined && listen.port !== 0 ? listen.port : undefined;
50858
+ let sockets;
50859
+ try {
50860
+ sockets = await server.listeningSockets();
50861
+ } catch (error) {
50862
+ const failure = `looking up the ports this process (${server.pid}) listens on failed: ${detail2(error)}`;
50863
+ if (declared === undefined) {
50864
+ observed.push(`${failure}, and its command line names no --port`);
50865
+ return;
50866
+ }
50867
+ return [serverURL(listen.hostname, declared)];
50868
+ }
50869
+ const own = declared === undefined ? sockets : sockets.filter((socket) => socket.port === declared);
50870
+ if (own.length === 0) {
50871
+ const found = sockets.map((socket) => socket.port).join(", ") || "none";
50872
+ observed.push(declared === undefined ? `this process (${server.pid}) listens on no TCP port` : `--port ${declared} is not one of the ports this process (${server.pid}) listens on (${found})`);
50873
+ return;
50874
+ }
50875
+ return [...new Set(own.map((socket) => serverURL(reachableHost(socket.address), socket.port)))];
50876
+ }
50877
+ async function ownServer(server, options) {
50878
+ const observed = [];
50879
+ const registered = await ownRegistration(server, options.timeoutMs, observed);
50880
+ if (registered)
50881
+ return registered;
50882
+ const unaddressable = () => ({
50883
+ reason: "own_server_unaddressable",
50884
+ unavailable: `AFT could not find the OpenCode server it runs in (${observed.join("; ")}); to fix it, ${HOW_TO_FIX}`
50885
+ });
50886
+ const listen = ownListenAddress(server.argv);
50887
+ const password = ownServerPassword(server.env);
50888
+ if (listen.stdio) {
50889
+ observed.push("this is a private server (opencode --standalone) whose password is not readable by plugins");
50890
+ return unaddressable();
50891
+ }
50892
+ if (!password) {
50893
+ observed.push("this server was started without OPENCODE_SERVER_PASSWORD, so it generated a password AFT cannot read");
50894
+ return unaddressable();
50895
+ }
50896
+ const candidates = await ownCandidates(server, listen, observed);
50897
+ if (!candidates)
50898
+ return unaddressable();
50899
+ let refused = 0;
50900
+ let others = 0;
50901
+ for (const url of candidates) {
50902
+ const endpoint = { url, auth: basicAuth(password) };
50903
+ const result = await probe(server, endpoint, options.timeoutMs);
50904
+ if (result.kind === "answered" && result.pid === server.pid) {
50905
+ return { client: promptClient(endpoint) };
50906
+ }
50907
+ if (result.kind === "unauthorized")
50908
+ refused += 1;
50909
+ if (result.kind === "answered")
50910
+ others += 1;
50911
+ }
50912
+ observed.push(`none of ${candidates.join(", ")} answered as this process (${server.pid})` + (refused > 0 ? `; ${refused} refused the password from OPENCODE_SERVER_PASSWORD` : "") + (others > 0 ? `; ${others} belonged to another process` : ""));
50913
+ return unaddressable();
50914
+ }
50915
+ async function discoverPromptServer(settings, server = currentServerProcess(), options = {}) {
50916
+ const resolved = {
50917
+ timeoutMs: options.timeoutMs ?? DEFAULT_PROBE_TIMEOUT_MS2
50918
+ };
50919
+ const serverUrl = settings?.server_url;
50920
+ if (serverUrl !== undefined && serverUrl.trim() !== "") {
50921
+ return await configuredServer({ ...settings, server_url: serverUrl }, server, resolved.timeoutMs);
50922
+ }
50923
+ return await ownServer(server, resolved);
50407
50924
  }
50408
-
50409
- // src/permissions/v2-service.ts
50410
- function detail2(error) {
50411
- return error instanceof Error ? error.message : String(error);
50925
+ function registrationDirectory(env) {
50926
+ return join26(env.XDG_STATE_HOME || join26(homedir15(), ".local", "state"), "opencode");
50412
50927
  }
50413
- function promptClient(endpoint) {
50414
- const client = exports_client.make({ baseUrl: endpoint.url, headers: headers(endpoint) });
50928
+ function currentServerProcess() {
50929
+ const env = globalThis.process.env;
50415
50930
  return {
50416
- permission: {
50417
- create: (input) => client.permission.create(input)
50931
+ pid: globalThis.process.pid,
50932
+ argv: globalThis.process.argv,
50933
+ env,
50934
+ async registrations() {
50935
+ const directory = registrationDirectory(env);
50936
+ const names = await readdir(directory).catch(() => []);
50937
+ const files = names.filter((name) => /^service(-[^/\\]+)?\.json$/.test(name)).sort();
50938
+ const read = await Promise.all(files.map(async (name) => {
50939
+ const path = join26(directory, name);
50940
+ const text = await readFile3(path, "utf8").catch(() => {
50941
+ return;
50942
+ });
50943
+ return text === undefined ? undefined : { path, text };
50944
+ }));
50945
+ return read.filter((entry) => entry !== undefined);
50418
50946
  },
50419
- event: {
50420
- subscribe: async () => ({ stream: client.event.subscribe() })
50421
- }
50947
+ listeningSockets: () => listeningSocketsOf(globalThis.process.pid),
50948
+ fetch: (url, init) => fetch(url, init)
50422
50949
  };
50423
50950
  }
50424
- async function discoverPromptClient() {
50425
- let endpoint;
50951
+ function runCommand(file, args) {
50952
+ return new Promise((resolve, reject) => {
50953
+ execFile(file, [...args], { timeout: LISTEN_LOOKUP_TIMEOUT_MS, maxBuffer: 8 * 1024 * 1024, encoding: "utf8" }, (error, stdout) => {
50954
+ const code = error?.code;
50955
+ if (error && !(code === 1 && file === "lsof"))
50956
+ reject(error);
50957
+ else
50958
+ resolve(String(stdout ?? ""));
50959
+ });
50960
+ });
50961
+ }
50962
+ function splitHostPort(value) {
50963
+ const separator = value.lastIndexOf(":");
50964
+ if (separator < 0)
50965
+ return;
50966
+ const port = Number(value.slice(separator + 1));
50967
+ if (!Number.isInteger(port) || port <= 0 || port > 65535)
50968
+ return;
50969
+ return { address: value.slice(0, separator).replace(/^\[(.*)\]$/, "$1"), port };
50970
+ }
50971
+ function parseLsofListening(output) {
50972
+ return output.split(/\r?\n/).filter((line) => line.startsWith("n")).map((line) => splitHostPort(line.slice(1))).filter((socket) => socket !== undefined);
50973
+ }
50974
+ function parseNetstatListening(output, pid) {
50975
+ const sockets = [];
50976
+ for (const line of output.split(/\r?\n/)) {
50977
+ const columns = line.trim().split(/\s+/);
50978
+ if (columns.length < 5 || !columns[0]?.toUpperCase().startsWith("TCP"))
50979
+ continue;
50980
+ if (Number(columns[columns.length - 1]) !== pid)
50981
+ continue;
50982
+ if (!/:0$/.test(columns[2] ?? ""))
50983
+ continue;
50984
+ const socket = splitHostPort(columns[1] ?? "");
50985
+ if (socket)
50986
+ sockets.push(socket);
50987
+ }
50988
+ return sockets;
50989
+ }
50990
+ function procAddress(hex) {
50991
+ const bytes = [];
50992
+ for (let word = 0;word < hex.length; word += 8) {
50993
+ const chunk = hex.slice(word, word + 8);
50994
+ for (let index = 6;index >= 0; index -= 2) {
50995
+ bytes.push(Number.parseInt(chunk.slice(index, index + 2), 16));
50996
+ }
50997
+ }
50998
+ if (bytes.length === 4)
50999
+ return bytes.join(".");
51000
+ const groups = [];
51001
+ for (let index = 0;index < bytes.length; index += 2) {
51002
+ groups.push(((bytes[index] ?? 0) << 8 | (bytes[index + 1] ?? 0)).toString(16));
51003
+ }
51004
+ return groups.join(":");
51005
+ }
51006
+ function parseProcNetListening(table, inodes) {
51007
+ const sockets = [];
51008
+ for (const line of table.split(/\r?\n/).slice(1)) {
51009
+ const columns = line.trim().split(/\s+/);
51010
+ if (columns[3] !== "0A" || !inodes.has(columns[9] ?? ""))
51011
+ continue;
51012
+ const [address, port] = (columns[1] ?? "").split(":");
51013
+ if (!address || !port)
51014
+ continue;
51015
+ sockets.push({ address: procAddress(address), port: Number.parseInt(port, 16) });
51016
+ }
51017
+ return sockets;
51018
+ }
51019
+ async function procListening(pid) {
51020
+ const descriptors = await readdir(`/proc/${pid}/fd`);
51021
+ const inodes = new Set;
51022
+ await Promise.all(descriptors.map(async (fd) => {
51023
+ const target = await readlink(`/proc/${pid}/fd/${fd}`).catch(() => "");
51024
+ const inode = /^socket:\[(\d+)\]$/.exec(target)?.[1];
51025
+ if (inode)
51026
+ inodes.add(inode);
51027
+ }));
51028
+ const tables = await Promise.all(["/proc/net/tcp", "/proc/net/tcp6"].map((path) => readFile3(path, "utf8").catch(() => "")));
51029
+ return tables.flatMap((table) => parseProcNetListening(table, inodes));
51030
+ }
51031
+ async function listeningSocketsOf(pid) {
51032
+ if (globalThis.process.platform === "win32") {
51033
+ return parseNetstatListening(await runCommand("netstat", ["-ano", "-p", "TCP"]), pid).concat(parseNetstatListening(await runCommand("netstat", ["-ano", "-p", "TCPv6"]), pid));
51034
+ }
50426
51035
  try {
50427
- endpoint = await discover();
51036
+ return parseLsofListening(await runCommand("lsof", ["-nP", "-a", "-p", String(pid), "-iTCP", "-sTCP:LISTEN", "-Fn"]));
50428
51037
  } catch (error) {
50429
- return { unavailable: `looking up the local OpenCode service failed: ${detail2(error)}` };
50430
- }
50431
- if (!endpoint) {
50432
- return { unavailable: "no healthy, compatible local OpenCode service is registered" };
51038
+ const missing = error.code === "ENOENT";
51039
+ if (!missing || globalThis.process.platform !== "linux")
51040
+ throw error;
51041
+ return await procListening(pid);
50433
51042
  }
50434
- return { client: promptClient(endpoint) };
50435
51043
  }
50436
- function createV2PromptChannel(discovery = discoverPromptClient) {
51044
+ function createV2PromptChannel(discovery = () => discoverPromptServer(undefined)) {
50437
51045
  let cached;
51046
+ let failureLogged = false;
50438
51047
  return {
50439
51048
  async client() {
50440
51049
  if (cached)
50441
51050
  return { client: cached };
50442
51051
  const result = await discovery();
50443
- if (result.client)
51052
+ if (result.client) {
50444
51053
  cached = result.client;
51054
+ log2("[permissions] OpenCode server for permission prompts found");
51055
+ } else if (!failureLogged) {
51056
+ failureLogged = true;
51057
+ warn2(`[permissions] permission prompts are unavailable${result.reason ? ` (${result.reason})` : ""}: ${result.unavailable}`);
51058
+ }
50445
51059
  return result;
50446
51060
  }
50447
51061
  };
@@ -50461,9 +51075,149 @@ function hoistedV2ToolConsumers(host, prompt = createV2PromptChannel()) {
50461
51075
  requestPermission: (request, context) => requestPermission(host, request, context, prompt)
50462
51076
  };
50463
51077
  }
51078
+ function v2PromptChannelFor(settings) {
51079
+ return createV2PromptChannel(() => discoverPromptServer(settings));
51080
+ }
50464
51081
 
50465
- // src/wakes/runtime-consumer.ts
51082
+ // src/v2-prompt-detach.ts
51083
+ import { Effect as Effect7 } from "effect";
51084
+
51085
+ // src/wakes/session-delivery.ts
50466
51086
  import { Effect as Effect6 } from "effect";
51087
+ var AFT_PROMPT_SOURCE = "aft";
51088
+ function isAftOriginatedPrompt(metadata) {
51089
+ return typeof metadata === "object" && metadata !== null && metadata.source === AFT_PROMPT_SOURCE;
51090
+ }
51091
+
51092
+ class V2SessionDelivery {
51093
+ session;
51094
+ admittedCompletions = new Set;
51095
+ disposed = false;
51096
+ constructor(session) {
51097
+ this.session = session;
51098
+ }
51099
+ completion(input) {
51100
+ return Effect6.suspend(() => {
51101
+ if (this.disposed)
51102
+ return Effect6.succeed(false);
51103
+ const keys = [...new Set(input.taskIDs)].map((taskID) => `${input.sessionID}\x00${taskID}`);
51104
+ if (keys.length === 0 || keys.every((key) => this.admittedCompletions.has(key))) {
51105
+ return Effect6.succeed(false);
51106
+ }
51107
+ for (const key of keys)
51108
+ this.admittedCompletions.add(key);
51109
+ return this.session.prompt({
51110
+ sessionID: input.sessionID,
51111
+ text: input.text,
51112
+ delivery: "steer",
51113
+ metadata: {
51114
+ ...input.metadata,
51115
+ source: AFT_PROMPT_SOURCE,
51116
+ task_ids: [...new Set(input.taskIDs)]
51117
+ }
51118
+ }).pipe(Effect6.map(() => true));
51119
+ });
51120
+ }
51121
+ status(input) {
51122
+ return Effect6.suspend(() => {
51123
+ if (this.disposed)
51124
+ return Effect6.succeed(undefined);
51125
+ return this.session.synthetic({
51126
+ sessionID: input.sessionID,
51127
+ text: input.text,
51128
+ description: input.description,
51129
+ metadata: input.metadata,
51130
+ resume: false
51131
+ }).pipe(Effect6.asVoid);
51132
+ });
51133
+ }
51134
+ dispose() {
51135
+ if (this.disposed)
51136
+ return;
51137
+ this.disposed = true;
51138
+ this.admittedCompletions.clear();
51139
+ }
51140
+ }
51141
+
51142
+ // src/v2-prompt-detach.ts
51143
+ function mapOffset(offset, edits) {
51144
+ let shift = 0;
51145
+ for (const edit of edits) {
51146
+ if (offset <= edit.start)
51147
+ break;
51148
+ if (offset >= edit.end) {
51149
+ shift += edit.length - (edit.end - edit.start);
51150
+ continue;
51151
+ }
51152
+ return edit.start + shift + Math.min(offset - edit.start, edit.length);
51153
+ }
51154
+ return offset + shift;
51155
+ }
51156
+ function offsetMapper(original, finalText) {
51157
+ const tokenEdits = standaloneDetachKeywordRanges(original).map(([start, end]) => ({
51158
+ start,
51159
+ end,
51160
+ length: 0
51161
+ }));
51162
+ const withoutTokens = stripStandaloneDetachKeywords(original);
51163
+ const whitespaceEdits = [...withoutTokens.matchAll(/[ \t]{2,}/g)].map((match) => ({
51164
+ start: match.index ?? 0,
51165
+ end: (match.index ?? 0) + match[0].length,
51166
+ length: 1
51167
+ }));
51168
+ return (offset) => Math.min(mapOffset(mapOffset(offset, tokenEdits), whitespaceEdits), finalText.length);
51169
+ }
51170
+ function mentionCarriers(prompt) {
51171
+ return [...prompt.files ?? [], ...prompt.agents ?? [], ...prompt.skills ?? []].filter((item) => typeof item?.mention?.start === "number" && typeof item.mention.end === "number");
51172
+ }
51173
+ function interruptBashWaitsForV2Prompt(runtime, event) {
51174
+ const prompt = event.prompt;
51175
+ const original = prompt.text;
51176
+ const synthetic = isAftOriginatedPrompt(event.metadata);
51177
+ const output = { parts: [{ type: "text", text: original, synthetic }] };
51178
+ const interrupted = interruptBashWaitsForChatMessage(runtime.pool, runtime.getConfig(), runtime.projectRoot, event.sessionID, output);
51179
+ const finalText = output.parts[0]?.text ?? original;
51180
+ if (synthetic || finalText === original)
51181
+ return interrupted;
51182
+ const map = offsetMapper(original, finalText);
51183
+ const moved = mentionCarriers(prompt).map((item) => {
51184
+ const mention = item.mention;
51185
+ return { item, mention: { ...mention, start: map(mention.start), end: map(mention.end) } };
51186
+ });
51187
+ prompt.text = finalText;
51188
+ for (const { item, mention } of moved)
51189
+ item.mention = mention;
51190
+ return interrupted;
51191
+ }
51192
+ function failureReason(error) {
51193
+ return error instanceof Error ? error.message : String(error);
51194
+ }
51195
+ function createV2PromptDetachHook(runtime) {
51196
+ const loggedReasons = new Set;
51197
+ return (event) => Effect7.sync(() => {
51198
+ try {
51199
+ interruptBashWaitsForV2Prompt(runtime, event);
51200
+ } catch (error) {
51201
+ const reason = failureReason(error);
51202
+ if (loggedReasons.has(reason))
51203
+ return;
51204
+ loggedReasons.add(reason);
51205
+ warn2(`[bash_wait_detach] prompt hook failed for session ${String(event?.sessionID)}; the prompt passes through unchanged: ${reason}`);
51206
+ }
51207
+ });
51208
+ }
51209
+ function registerV2PromptDetachHook(context, runtime) {
51210
+ const session = context?.session;
51211
+ const hook = session?.hook;
51212
+ if (typeof hook !== "function") {
51213
+ warn2("[bash_wait_detach] host has no session prompt hook; a new message cannot detach a waiting bash here");
51214
+ return Effect7.succeed(false);
51215
+ }
51216
+ return hook.call(session, "prompt", createV2PromptDetachHook(runtime)).pipe(Effect7.as(true));
51217
+ }
51218
+
51219
+ // src/wakes/runtime-consumer.ts
51220
+ import { Effect as Effect8 } from "effect";
50467
51221
 
50468
51222
  // src/cancellation/effect-abort.ts
50469
51223
  function forwardEffectAbort(signal, send) {
@@ -50511,96 +51265,64 @@ async function executeV2Bash(execution) {
50511
51265
  }
50512
51266
  }
50513
51267
 
50514
- // src/wakes/session-delivery.ts
50515
- import { Effect as Effect5 } from "effect";
50516
-
50517
- class V2SessionDelivery {
50518
- session;
50519
- admittedCompletions = new Set;
50520
- disposed = false;
50521
- constructor(session) {
50522
- this.session = session;
50523
- }
50524
- completion(input) {
50525
- return Effect5.suspend(() => {
50526
- if (this.disposed)
50527
- return Effect5.succeed(false);
50528
- const keys = [...new Set(input.taskIDs)].map((taskID) => `${input.sessionID}\x00${taskID}`);
50529
- if (keys.length === 0 || keys.every((key) => this.admittedCompletions.has(key))) {
50530
- return Effect5.succeed(false);
50531
- }
50532
- for (const key of keys)
50533
- this.admittedCompletions.add(key);
50534
- return this.session.prompt({
50535
- sessionID: input.sessionID,
50536
- text: input.text,
50537
- delivery: "steer",
50538
- metadata: {
50539
- ...input.metadata,
50540
- task_ids: [...new Set(input.taskIDs)]
50541
- }
50542
- }).pipe(Effect5.map(() => true));
50543
- });
50544
- }
50545
- status(input) {
50546
- return Effect5.suspend(() => {
50547
- if (this.disposed)
50548
- return Effect5.succeed(undefined);
50549
- return this.session.synthetic({
50550
- sessionID: input.sessionID,
50551
- text: input.text,
50552
- description: input.description,
50553
- metadata: input.metadata,
50554
- resume: false
50555
- }).pipe(Effect5.asVoid);
50556
- });
50557
- }
50558
- dispose() {
50559
- if (this.disposed)
50560
- return;
50561
- this.disposed = true;
50562
- this.admittedCompletions.clear();
50563
- }
50564
- }
50565
-
50566
51268
  // src/wakes/runtime-consumer.ts
50567
- var sessionRoutes = new Map;
50568
- async function routeCompletion(completion, bridge) {
50569
- const route = sessionRoutes.get(completion.session_id);
50570
- if (!route)
50571
- return;
50572
- const admitted = await Effect6.runPromise(route.delivery.completion({
50573
- sessionID: completion.session_id,
50574
- taskIDs: [completion.task_id],
50575
- text: formatSystemReminder([completion]),
50576
- metadata: {
50577
- source: "aft",
50578
- kind: "bash_completion",
50579
- task_ids: [completion.task_id],
50580
- status: completion.status
50581
- }
50582
- }));
50583
- if (!admitted)
51269
+ var WAKE_ROUTE_STATE = Symbol.for("@cortexkit/aft-opencode/wake-routes/v1");
51270
+ function wakeRouteState() {
51271
+ const host = globalThis;
51272
+ return host[WAKE_ROUTE_STATE] ??= {
51273
+ routes: new Map,
51274
+ unrouted: new Map,
51275
+ warnedUnrouted: new Set
51276
+ };
51277
+ }
51278
+ var MAX_UNROUTED_COMPLETIONS = 256;
51279
+ function holdUnrouted(completion, bridge) {
51280
+ const { unrouted } = wakeRouteState();
51281
+ let held = 0;
51282
+ for (const tasks of unrouted.values())
51283
+ held += tasks.size;
51284
+ const sessionTasks = unrouted.get(completion.session_id);
51285
+ if (!sessionTasks?.has(completion.task_id) && held >= MAX_UNROUTED_COMPLETIONS) {
51286
+ const oldest = unrouted.keys().next();
51287
+ if (!oldest.done)
51288
+ unrouted.delete(oldest.value);
51289
+ }
51290
+ const tasks = unrouted.get(completion.session_id) ?? new Map;
51291
+ tasks.set(completion.task_id, { completion, bridge });
51292
+ unrouted.set(completion.session_id, tasks);
51293
+ }
51294
+ function warnUnroutedOnce(sessionID, message) {
51295
+ const { warnedUnrouted } = wakeRouteState();
51296
+ if (warnedUnrouted.has(sessionID))
50584
51297
  return;
50585
- await bridge.send("bash_ack_completions", {
50586
- session_id: completion.session_id,
50587
- task_ids: [completion.task_id]
50588
- });
51298
+ warnedUnrouted.add(sessionID);
51299
+ warn2(message);
51300
+ }
51301
+ async function routeCompletion(completion, bridge) {
51302
+ const route = wakeRouteState().routes.get(completion.session_id);
51303
+ if (route)
51304
+ return route.completion(completion, bridge);
51305
+ holdUnrouted(completion, bridge);
51306
+ warnUnroutedOnce(completion.session_id, `[bash_completion] no OpenCode Location has session ${completion.session_id} registered; completion of task ${completion.task_id} is held unacknowledged until the session registers`);
50589
51307
  }
50590
51308
  async function routeLongRunning(reminder, _bridge) {
50591
- const route = sessionRoutes.get(reminder.session_id);
50592
- if (!route)
51309
+ const route = wakeRouteState().routes.get(reminder.session_id);
51310
+ if (route)
51311
+ return route.longRunning(reminder);
51312
+ warnUnroutedOnce(reminder.session_id, `[bash_long_running] no OpenCode Location has session ${reminder.session_id} registered; long-running reminder for task ${reminder.task_id} was not delivered`);
51313
+ }
51314
+ function deliverUnrouted(sessionID, route) {
51315
+ const state = wakeRouteState();
51316
+ state.warnedUnrouted.delete(sessionID);
51317
+ const held = state.unrouted.get(sessionID);
51318
+ if (!held)
50593
51319
  return;
50594
- await Effect6.runPromise(route.delivery.status({
50595
- sessionID: reminder.session_id,
50596
- text: formatLongRunningReminder([reminder]),
50597
- description: "Background bash status",
50598
- metadata: {
50599
- source: "aft",
50600
- kind: "bash_long_running",
50601
- task_id: reminder.task_id
50602
- }
50603
- }));
51320
+ state.unrouted.delete(sessionID);
51321
+ for (const { completion, bridge } of held.values()) {
51322
+ route.completion(completion, bridge).catch((error) => {
51323
+ warn2(`[bash_completion] late delivery of task ${completion.task_id} to session ${sessionID} failed: ${error instanceof Error ? error.message : String(error)}`);
51324
+ });
51325
+ }
50604
51326
  }
50605
51327
  var sharedBridgeOptions = {
50606
51328
  onBashCompletion: routeCompletion,
@@ -50608,12 +51330,47 @@ var sharedBridgeOptions = {
50608
51330
  };
50609
51331
  function createV2RuntimeConsumer(context) {
50610
51332
  const delivery = new V2SessionDelivery(context.session);
50611
- const route = { delivery, sessions: new Set };
51333
+ const sessions = new Set;
51334
+ const route = {
51335
+ completion: async (completion, bridge) => {
51336
+ const admitted = await Effect8.runPromise(delivery.completion({
51337
+ sessionID: completion.session_id,
51338
+ taskIDs: [completion.task_id],
51339
+ text: formatSystemReminder([completion]),
51340
+ metadata: {
51341
+ source: "aft",
51342
+ kind: "bash_completion",
51343
+ task_ids: [completion.task_id],
51344
+ status: completion.status
51345
+ }
51346
+ }));
51347
+ if (!admitted)
51348
+ return;
51349
+ await bridge.send("bash_ack_completions", {
51350
+ session_id: completion.session_id,
51351
+ task_ids: [completion.task_id]
51352
+ });
51353
+ },
51354
+ longRunning: (reminder) => Effect8.runPromise(delivery.status({
51355
+ sessionID: reminder.session_id,
51356
+ text: formatLongRunningReminder([reminder]),
51357
+ description: "Background bash status",
51358
+ metadata: {
51359
+ source: "aft",
51360
+ kind: "bash_long_running",
51361
+ task_id: reminder.task_id
51362
+ }
51363
+ }))
51364
+ };
50612
51365
  const registerSession = (sessionID) => {
50613
51366
  if (!sessionID)
50614
51367
  return;
50615
- route.sessions.add(sessionID);
50616
- sessionRoutes.set(sessionID, route);
51368
+ sessions.add(sessionID);
51369
+ const { routes } = wakeRouteState();
51370
+ if (routes.get(sessionID) === route)
51371
+ return;
51372
+ routes.set(sessionID, route);
51373
+ deliverUnrouted(sessionID, route);
50617
51374
  };
50618
51375
  return {
50619
51376
  bridgeOptions: sharedBridgeOptions,
@@ -50623,11 +51380,12 @@ function createV2RuntimeConsumer(context) {
50623
51380
  },
50624
51381
  dispose: () => {
50625
51382
  delivery.dispose();
50626
- for (const sessionID of route.sessions) {
50627
- if (sessionRoutes.get(sessionID) === route)
50628
- sessionRoutes.delete(sessionID);
51383
+ const { routes } = wakeRouteState();
51384
+ for (const sessionID of sessions) {
51385
+ if (routes.get(sessionID) === route)
51386
+ routes.delete(sessionID);
50629
51387
  }
50630
- route.sessions.clear();
51388
+ sessions.clear();
50631
51389
  }
50632
51390
  };
50633
51391
  }
@@ -50639,8 +51397,9 @@ var defaults = {
50639
51397
  registerTools: registerAftTools,
50640
51398
  registerRpc: registerAftRpc,
50641
51399
  registerConfigErrorRpc: registerAftConfigErrorRpc,
50642
- toolConsumers: (context) => ({
50643
- ...hoistedV2ToolConsumers(context),
51400
+ registerPromptHook: registerV2PromptDetachHook,
51401
+ toolConsumers: (context, config) => ({
51402
+ ...hoistedV2ToolConsumers(context, v2PromptChannelFor(config?.opencode)),
50644
51403
  ...createV2RuntimeConsumer(context)
50645
51404
  }),
50646
51405
  acquireBridge,
@@ -50666,7 +51425,7 @@ async function bootLocation(context, location, dependencies) {
50666
51425
  const pluginVersion = dependencies.resolveVersion();
50667
51426
  const environment = await prepareBridgeEnvironment({ configRoot: directory, lspDirectory: directory, config, pluginVersion, notify }, dependencies);
50668
51427
  const canonicalDirectory = location.project?.canonical ?? directory;
50669
- const consumers = dependencies.toolConsumers(context);
51428
+ const consumers = dependencies.toolConsumers(context, config);
50670
51429
  const isProjectEnabled = createProjectAcceptance(directory, dependencies);
50671
51430
  const pool = await dependencies.acquireBridge(canonicalDirectory, {
50672
51431
  harness: "opencode",
@@ -50709,7 +51468,7 @@ async function bootLocation(context, location, dependencies) {
50709
51468
  },
50710
51469
  notify
50711
51470
  });
50712
- return { consumers, pool, tools, liveConfigReload };
51471
+ return { consumers, pool, tools, liveConfigReload, toolContext, directory };
50713
51472
  }
50714
51473
  var runningByDirectory = new Map;
50715
51474
  var startsInProcess = 0;
@@ -50736,27 +51495,32 @@ function makeServerEffect(overrides = {}) {
50736
51495
  const location = context.location;
50737
51496
  if (typeof location?.directory !== "string") {
50738
51497
  debug2("V2 effect skipped: host context has no location");
50739
- return Effect7.void;
51498
+ return Effect9.void;
50740
51499
  }
50741
- return Effect7.gen(function* () {
51500
+ return Effect9.gen(function* () {
50742
51501
  const logRuntimeStop = logRuntimeStart(location);
50743
- yield* Effect7.addFinalizer(() => Effect7.sync(logRuntimeStop));
50744
- const runtime = yield* Effect7.promise(() => bootLocation(context, location, dependencies));
51502
+ yield* Effect9.addFinalizer(() => Effect9.sync(logRuntimeStop));
51503
+ const runtime = yield* Effect9.promise(() => bootLocation(context, location, dependencies));
50745
51504
  if (!runtime)
50746
51505
  return;
50747
51506
  if (runtime.configError !== undefined) {
50748
51507
  const rpc = yield* dependencies.registerConfigErrorRpc(context, runtime.configError);
50749
- yield* Effect7.addFinalizer(() => Effect7.promise(() => rpc.dispose()));
51508
+ yield* Effect9.addFinalizer(() => Effect9.promise(() => rpc.dispose()));
50750
51509
  yield* dependencies.registerTools(context, location, runtime.tools, runtime.consumers);
50751
51510
  return;
50752
51511
  }
50753
- yield* Effect7.addFinalizer(() => Effect7.promise(async () => {
51512
+ yield* Effect9.addFinalizer(() => Effect9.promise(async () => {
50754
51513
  runtime.liveConfigReload?.stop();
50755
51514
  runtime.consumers.dispose?.();
50756
51515
  await dependencies.releaseBridge(runtime.pool);
50757
51516
  }));
50758
51517
  const rpc = yield* dependencies.registerRpc(context, location, runtime.pool);
50759
- yield* Effect7.addFinalizer(() => Effect7.promise(() => rpc.dispose()));
51518
+ yield* Effect9.addFinalizer(() => Effect9.promise(() => rpc.dispose()));
51519
+ yield* dependencies.registerPromptHook(context, {
51520
+ pool: runtime.pool,
51521
+ projectRoot: runtime.directory,
51522
+ getConfig: () => runtime.toolContext.config
51523
+ });
50760
51524
  yield* dependencies.registerTools(context, location, runtime.tools, runtime.consumers);
50761
51525
  });
50762
51526
  };