@cortexkit/aft-pi 0.36.1 → 0.37.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -11678,6 +11678,9 @@ function error(message, meta) {
11678
11678
  var CONFLICT_HINT = `
11679
11679
 
11680
11680
  [Hint] Use aft_conflicts to see all conflict regions across files in a single call.`;
11681
+ var GREP_SEARCH_AFT_SEARCH_HINT = "DO NOT search code by running grep/rg in bash — it is unindexed, unranked, and serial. Use the `aft_search` tool instead (it auto-routes concepts, identifiers, regex, and literals).";
11682
+ var GREP_SEARCH_GREP_HINT = "DO NOT search code by running grep/rg in bash — it is unindexed, unranked, and serial. Use the `grep` tool instead (indexed and ranked).";
11683
+ var GREP_SEARCH_HINT_PREFIX = "DO NOT search code by running grep/rg in bash —";
11681
11684
  function maybeAppendConflictsHint(output) {
11682
11685
  if (!output.includes("Automatic merge failed; fix conflicts"))
11683
11686
  return output;
@@ -11685,6 +11688,146 @@ function maybeAppendConflictsHint(output) {
11685
11688
  return output;
11686
11689
  return output + CONFLICT_HINT;
11687
11690
  }
11691
+ function commandLeadsWithCodeSearch(command) {
11692
+ const trimmed = command.trim();
11693
+ if (!trimmed)
11694
+ return false;
11695
+ const afterCd = peelLeadingCdAnd(trimmed);
11696
+ if (afterCd === null)
11697
+ return false;
11698
+ const firstStage = firstPipelineStage(afterCd);
11699
+ if (firstStage === null)
11700
+ return false;
11701
+ const firstToken = readShellToken(firstStage, skipSpaces(firstStage, 0));
11702
+ if (firstToken === null)
11703
+ return false;
11704
+ return firstToken.token === "grep" || firstToken.token === "rg";
11705
+ }
11706
+ function maybeAppendGrepSearchHint(output, command, aftSearchRegistered) {
11707
+ if (output === "")
11708
+ return output;
11709
+ if (!commandLeadsWithCodeSearch(command))
11710
+ return output;
11711
+ if (output.includes(GREP_SEARCH_HINT_PREFIX))
11712
+ return output;
11713
+ const hint = aftSearchRegistered ? GREP_SEARCH_AFT_SEARCH_HINT : GREP_SEARCH_GREP_HINT;
11714
+ return `${output}
11715
+
11716
+ ${hint}`;
11717
+ }
11718
+ function peelLeadingCdAnd(command) {
11719
+ const first = readShellToken(command, skipSpaces(command, 0));
11720
+ if (first === null)
11721
+ return null;
11722
+ if (first.token !== "cd")
11723
+ return command;
11724
+ const dir = readShellToken(command, skipSpaces(command, first.end));
11725
+ if (dir === null)
11726
+ return null;
11727
+ if (!dir.token)
11728
+ return command;
11729
+ const afterDir = skipSpaces(command, dir.end);
11730
+ if (!command.startsWith("&&", afterDir))
11731
+ return command;
11732
+ return command.slice(afterDir + 2).trim();
11733
+ }
11734
+ function firstPipelineStage(command) {
11735
+ let quote = "none";
11736
+ let firstPipeIndex;
11737
+ for (let index = 0;index < command.length; index++) {
11738
+ const ch = command[index];
11739
+ if (quote === "single") {
11740
+ if (ch === "'")
11741
+ quote = "none";
11742
+ continue;
11743
+ }
11744
+ if (quote === "double") {
11745
+ if (ch === '"') {
11746
+ quote = "none";
11747
+ } else if (ch === "\\") {
11748
+ index++;
11749
+ } else if (ch === "`") {
11750
+ return null;
11751
+ }
11752
+ continue;
11753
+ }
11754
+ if (ch === "'") {
11755
+ quote = "single";
11756
+ } else if (ch === '"') {
11757
+ quote = "double";
11758
+ } else if (ch === "\\") {
11759
+ index++;
11760
+ } else if (ch === "`") {
11761
+ return null;
11762
+ } else if (ch === "|") {
11763
+ if (command[index + 1] === "|") {
11764
+ index++;
11765
+ } else if (firstPipeIndex === undefined) {
11766
+ firstPipeIndex = index;
11767
+ }
11768
+ }
11769
+ }
11770
+ if (quote !== "none")
11771
+ return null;
11772
+ return command.slice(0, firstPipeIndex ?? command.length).trim();
11773
+ }
11774
+ function readShellToken(command, start) {
11775
+ let quote = "none";
11776
+ let token = "";
11777
+ let index = start;
11778
+ for (;index < command.length; index++) {
11779
+ const ch = command[index];
11780
+ if (quote === "single") {
11781
+ if (ch === "'") {
11782
+ quote = "none";
11783
+ } else {
11784
+ token += ch;
11785
+ }
11786
+ continue;
11787
+ }
11788
+ if (quote === "double") {
11789
+ if (ch === '"') {
11790
+ quote = "none";
11791
+ } else if (ch === "\\") {
11792
+ index++;
11793
+ token += command[index] ?? "\\";
11794
+ } else if (ch === "`") {
11795
+ return null;
11796
+ } else {
11797
+ token += ch;
11798
+ }
11799
+ continue;
11800
+ }
11801
+ if (/\s/.test(ch))
11802
+ break;
11803
+ if (isTokenBoundary(ch))
11804
+ break;
11805
+ if (ch === "'") {
11806
+ quote = "single";
11807
+ } else if (ch === '"') {
11808
+ quote = "double";
11809
+ } else if (ch === "\\") {
11810
+ index++;
11811
+ token += command[index] ?? "\\";
11812
+ } else if (ch === "`") {
11813
+ return null;
11814
+ } else {
11815
+ token += ch;
11816
+ }
11817
+ }
11818
+ if (quote !== "none")
11819
+ return null;
11820
+ return { token, end: index };
11821
+ }
11822
+ function isTokenBoundary(ch) {
11823
+ return ch === "|" || ch === ";" || ch === "&" || ch === "<" || ch === ">";
11824
+ }
11825
+ function skipSpaces(input, start) {
11826
+ let index = start;
11827
+ while (index < input.length && /\s/.test(input[index]))
11828
+ index++;
11829
+ return index;
11830
+ }
11688
11831
  // ../aft-bridge/dist/bash-timeout.js
11689
11832
  function resolveBashKillTimeout(modelTimeout, foregroundWaitMs) {
11690
11833
  if (modelTimeout !== undefined && modelTimeout >= foregroundWaitMs) {
@@ -11698,6 +11841,21 @@ import { homedir } from "node:os";
11698
11841
  import { join } from "node:path";
11699
11842
  import { StringDecoder } from "node:string_decoder";
11700
11843
 
11844
+ // ../aft-bridge/dist/command-timeouts.js
11845
+ var LONG_RUNNING_COMMAND_TIMEOUT_MS = {
11846
+ callers: 60000,
11847
+ trace_to: 60000,
11848
+ trace_to_symbol: 60000,
11849
+ trace_data: 60000,
11850
+ impact: 60000,
11851
+ grep: 60000,
11852
+ glob: 60000,
11853
+ semantic_search: 60000
11854
+ };
11855
+ function timeoutForCommand(command) {
11856
+ return LONG_RUNNING_COMMAND_TIMEOUT_MS[command];
11857
+ }
11858
+
11701
11859
  // ../aft-bridge/dist/status-bar.js
11702
11860
  var STATUS_BAR_HEARTBEAT_CALLS = 15;
11703
11861
  function createStatusBarEmitState() {
@@ -11745,6 +11903,27 @@ var DEFAULT_BRIDGE_TIMEOUT_MS = 30000;
11745
11903
  var BRIDGE_HANG_TIMEOUT_THRESHOLD = 2;
11746
11904
  var SEMANTIC_TIMEOUT_SAFETY_MARGIN_MS = 5000;
11747
11905
  var MAX_STDOUT_BUFFER = 64 * 1024 * 1024;
11906
+ var STDOUT_BUFFER_COMPACT_THRESHOLD = 64 * 1024;
11907
+ var TERMINAL_BASH_STATUSES = new Set([
11908
+ "completed",
11909
+ "failed",
11910
+ "killed",
11911
+ "timed_out",
11912
+ "cancelled",
11913
+ "timeout"
11914
+ ]);
11915
+ function isTerminalBashStatus(status) {
11916
+ return typeof status === "string" && TERMINAL_BASH_STATUSES.has(status);
11917
+ }
11918
+ function bashTaskIdFrom(response) {
11919
+ const snakeCase = response.task_id;
11920
+ if (typeof snakeCase === "string" && snakeCase.length > 0)
11921
+ return snakeCase;
11922
+ const camelCase = response.taskId;
11923
+ if (typeof camelCase === "string" && camelCase.length > 0)
11924
+ return camelCase;
11925
+ return;
11926
+ }
11748
11927
  function tagStderrLine(line) {
11749
11928
  return /^\[aft(-\w+)?\] /.test(line) ? line : `[aft] ${line}`;
11750
11929
  }
@@ -11799,11 +11978,12 @@ function clampSemanticTimeout(configOverrides, bridgeTimeoutMs) {
11799
11978
  if (typeof timeoutMs !== "number" || !Number.isFinite(timeoutMs)) {
11800
11979
  return configOverrides;
11801
11980
  }
11802
- const maxSemanticTimeoutMs = bridgeTimeoutMs > SEMANTIC_TIMEOUT_SAFETY_MARGIN_MS ? bridgeTimeoutMs - SEMANTIC_TIMEOUT_SAFETY_MARGIN_MS : Math.max(1, bridgeTimeoutMs - 1);
11981
+ const semanticTransportBudgetMs = Math.max(bridgeTimeoutMs, LONG_RUNNING_COMMAND_TIMEOUT_MS.semantic_search ?? 0);
11982
+ const maxSemanticTimeoutMs = semanticTransportBudgetMs > SEMANTIC_TIMEOUT_SAFETY_MARGIN_MS ? semanticTransportBudgetMs - SEMANTIC_TIMEOUT_SAFETY_MARGIN_MS : Math.max(1, semanticTransportBudgetMs - 1);
11803
11983
  if (timeoutMs <= maxSemanticTimeoutMs) {
11804
11984
  return configOverrides;
11805
11985
  }
11806
- warn(`semantic.timeout_ms=${timeoutMs} exceeds bridge timeout budget; clamping to ${maxSemanticTimeoutMs}ms (bridge timeout: ${bridgeTimeoutMs}ms)`);
11986
+ warn(`semantic.timeout_ms=${timeoutMs} exceeds the semantic transport budget; clamping to ${maxSemanticTimeoutMs}ms (budget: ${semanticTransportBudgetMs}ms)`);
11807
11987
  return {
11808
11988
  ...configOverrides,
11809
11989
  semantic: {
@@ -11829,8 +12009,10 @@ class BinaryBridge {
11829
12009
  cwd;
11830
12010
  process = null;
11831
12011
  pending = new Map;
12012
+ outstandingBackgroundTaskIds = new Set;
11832
12013
  nextId = 1;
11833
12014
  stdoutBuffer = "";
12015
+ stdoutReadOffset = 0;
11834
12016
  stderrBuffer = "";
11835
12017
  stderrTail = [];
11836
12018
  _restartCount = 0;
@@ -11940,6 +12122,9 @@ class BinaryBridge {
11940
12122
  hasPendingRequests() {
11941
12123
  return this.pending.size > 0;
11942
12124
  }
12125
+ hasOutstandingBackgroundTasks() {
12126
+ return this.outstandingBackgroundTaskIds.size > 0;
12127
+ }
11943
12128
  getCwd() {
11944
12129
  return this.cwd;
11945
12130
  }
@@ -12061,7 +12246,7 @@ class BinaryBridge {
12061
12246
  entry.reject(new Error(`${this.errorPrefix} Request "${command}" (id=${id}) timed out after ${effectiveTimeoutMs}ms`));
12062
12247
  this.handleTimeout(requestSessionId);
12063
12248
  }, effectiveTimeoutMs);
12064
- this.pending.set(id, { resolve, reject, timer, onProgress: options?.onProgress });
12249
+ this.pending.set(id, { resolve, reject, timer, onProgress: options?.onProgress, command });
12065
12250
  if (!this.process?.stdin?.writable) {
12066
12251
  this.pending.delete(id);
12067
12252
  clearTimeout(timer);
@@ -12322,6 +12507,7 @@ class BinaryBridge {
12322
12507
  });
12323
12508
  this.process = child;
12324
12509
  this.stdoutBuffer = "";
12510
+ this.stdoutReadOffset = 0;
12325
12511
  this.stderrBuffer = "";
12326
12512
  this.lastChildActivityAt = 0;
12327
12513
  this.consecutiveRequestTimeouts = 0;
@@ -12366,24 +12552,41 @@ class BinaryBridge {
12366
12552
  ${tail}`;
12367
12553
  }
12368
12554
  onStdoutData(data) {
12555
+ if (this.stdoutReadOffset > STDOUT_BUFFER_COMPACT_THRESHOLD) {
12556
+ this.compactStdoutBuffer();
12557
+ }
12369
12558
  this.stdoutBuffer += data;
12370
- if (this.stdoutBuffer.length > MAX_STDOUT_BUFFER) {
12559
+ if (this.stdoutBuffer.length - this.stdoutReadOffset > MAX_STDOUT_BUFFER) {
12371
12560
  this.handleCrash(new Error(`aft bridge stdout buffer exceeded ${MAX_STDOUT_BUFFER} bytes — killing bridge`));
12372
12561
  return;
12373
12562
  }
12374
12563
  let newlineIdx;
12375
12564
  while ((newlineIdx = this.stdoutBuffer.indexOf(`
12376
- `)) !== -1) {
12377
- const line = this.stdoutBuffer.slice(0, newlineIdx).trim();
12378
- this.stdoutBuffer = this.stdoutBuffer.slice(newlineIdx + 1);
12379
- if (!line)
12380
- continue;
12381
- this.processStdoutLine(line);
12565
+ `, this.stdoutReadOffset)) !== -1) {
12566
+ const line = this.stdoutBuffer.slice(this.stdoutReadOffset, newlineIdx).trim();
12567
+ this.stdoutReadOffset = newlineIdx + 1;
12568
+ if (line) {
12569
+ this.processStdoutLine(line);
12570
+ }
12571
+ if (this.stdoutReadOffset > STDOUT_BUFFER_COMPACT_THRESHOLD && this.stdoutReadOffset > this.stdoutBuffer.length / 2) {
12572
+ this.compactStdoutBuffer();
12573
+ }
12382
12574
  }
12575
+ if (this.stdoutReadOffset === this.stdoutBuffer.length) {
12576
+ this.stdoutBuffer = "";
12577
+ this.stdoutReadOffset = 0;
12578
+ }
12579
+ }
12580
+ compactStdoutBuffer() {
12581
+ if (this.stdoutReadOffset === 0)
12582
+ return;
12583
+ this.stdoutBuffer = this.stdoutBuffer.slice(this.stdoutReadOffset);
12584
+ this.stdoutReadOffset = 0;
12383
12585
  }
12384
12586
  flushStdoutBuffer() {
12385
- const line = this.stdoutBuffer.trim();
12587
+ const line = this.stdoutBuffer.slice(this.stdoutReadOffset).trim();
12386
12588
  this.stdoutBuffer = "";
12589
+ this.stdoutReadOffset = 0;
12387
12590
  if (!line)
12388
12591
  return;
12389
12592
  this.processStdoutLine(line);
@@ -12416,6 +12619,9 @@ class BinaryBridge {
12416
12619
  return;
12417
12620
  }
12418
12621
  if (response.type === "bash_completed") {
12622
+ const taskId = bashTaskIdFrom(response);
12623
+ if (taskId)
12624
+ this.outstandingBackgroundTaskIds.delete(taskId);
12419
12625
  this.onBashCompletion?.(response, this);
12420
12626
  return;
12421
12627
  }
@@ -12446,6 +12652,7 @@ class BinaryBridge {
12446
12652
  clearTimeout(entry.timer);
12447
12653
  this.consecutiveRequestTimeouts = 0;
12448
12654
  this.scheduleRestartCountReset();
12655
+ this.accountForBashTaskResponse(entry.command, response);
12449
12656
  this.captureStatusBar(response);
12450
12657
  entry.resolve(response);
12451
12658
  } else if (typeof response.type === "string") {
@@ -12455,6 +12662,18 @@ class BinaryBridge {
12455
12662
  this.warnVia(`Failed to parse stdout line: ${line}`);
12456
12663
  }
12457
12664
  }
12665
+ accountForBashTaskResponse(command, response) {
12666
+ const taskId = bashTaskIdFrom(response);
12667
+ if (!taskId)
12668
+ return;
12669
+ if (isTerminalBashStatus(response.status)) {
12670
+ this.outstandingBackgroundTaskIds.delete(taskId);
12671
+ return;
12672
+ }
12673
+ if (command === "bash" && response.success !== false) {
12674
+ this.outstandingBackgroundTaskIds.add(taskId);
12675
+ }
12676
+ }
12458
12677
  captureStatusBar(response) {
12459
12678
  const parsed = parseStatusBarCounts(response.status_bar);
12460
12679
  if (parsed)
@@ -12466,6 +12685,7 @@ class BinaryBridge {
12466
12685
  handleTimeout(triggeringSessionId) {
12467
12686
  this.consecutiveRequestTimeouts = 0;
12468
12687
  this.rejectAllPending(new Error(`${this.errorPrefix} bridge killed during sibling timeout — request aborted`));
12688
+ this.outstandingBackgroundTaskIds.clear();
12469
12689
  if (this.process) {
12470
12690
  this.process.kill("SIGKILL");
12471
12691
  this.process = null;
@@ -12495,6 +12715,7 @@ class BinaryBridge {
12495
12715
  }
12496
12716
  this.clearRestartResetTimer();
12497
12717
  this.configured = false;
12718
+ this.outstandingBackgroundTaskIds.clear();
12498
12719
  const tail = this.formatStderrTail();
12499
12720
  if (tail) {
12500
12721
  this.errorVia(`Binary crashed (restarts: ${this._restartCount})${cause ? `: ${cause.message}` : ""}.${tail}`);
@@ -14220,14 +14441,27 @@ function isCompressorHandledRunner(stage) {
14220
14441
  if (tokens.some((token) => token === "&&" || token === "||" || token.includes(";"))) {
14221
14442
  return false;
14222
14443
  }
14223
- const first = runnerName(tokens[0]);
14224
- const second = tokens[1];
14225
- const third = tokens[2];
14226
- const rest = tokens.slice(1);
14444
+ let tokenOffset = 0;
14445
+ while (tokenOffset < tokens.length && isEnvAssignment(tokens[tokenOffset])) {
14446
+ tokenOffset++;
14447
+ }
14448
+ const first = runnerName(tokens[tokenOffset]);
14449
+ const runnerArgs = tokens.slice(tokenOffset + 1);
14450
+ const second = runnerArgs[0];
14451
+ const third = runnerArgs[1];
14452
+ const rest = runnerArgs;
14227
14453
  if (!first)
14228
14454
  return false;
14229
- if (first === "bun")
14230
- return second === "test" || second === "run" && startsWithTest(third);
14455
+ if (first === "bun") {
14456
+ let args = rest;
14457
+ if (args[0] === "--cwd")
14458
+ args = args.slice(2);
14459
+ else if (args[0]?.startsWith("--cwd="))
14460
+ args = args.slice(1);
14461
+ const sub = args[0];
14462
+ const subNext = args[1];
14463
+ return sub === "test" || sub === "run" && startsWithTest(subNext);
14464
+ }
14231
14465
  if (first === "npm" || first === "pnpm") {
14232
14466
  return second === "test" || second === "run" && startsWithTest(third);
14233
14467
  }
@@ -14250,14 +14484,20 @@ function isCompressorHandledRunner(stage) {
14250
14484
  return hasBuildTask(rest, ["test", "check", "build", "assemble", "clean"]);
14251
14485
  }
14252
14486
  if (first === "mvn" || first === "mvnw") {
14253
- return hasBuildTask(rest, ["test", "verify", "package", "install"]);
14487
+ return hasBuildTask(rest, ["test", "verify", "package", "install", "clean"]);
14254
14488
  }
14255
14489
  if (first === "dotnet")
14256
14490
  return ["test", "build"].includes(second ?? "");
14257
14491
  if (first === "rspec")
14258
14492
  return true;
14259
- if (first === "rake")
14260
- return second === "test" || second === "spec";
14493
+ if (first === "rake") {
14494
+ const positionals = rest.filter((a) => !a.startsWith("-"));
14495
+ if (positionals.length === 0)
14496
+ return false;
14497
+ if (positionals.some((a) => a.includes("/") || a.includes(".") || a.includes("=")))
14498
+ return false;
14499
+ return positionals.some((a) => a === "test" || a === "spec");
14500
+ }
14261
14501
  if (first === "phpunit" || first === "pest")
14262
14502
  return true;
14263
14503
  if (first === "xcodebuild")
@@ -14280,6 +14520,11 @@ function isCompressorHandledRunner(stage) {
14280
14520
  "nox"
14281
14521
  ].includes(first);
14282
14522
  }
14523
+ function isEnvAssignment(token) {
14524
+ if (!token)
14525
+ return false;
14526
+ return /^[a-zA-Z_][a-zA-Z0-9_]*=/.test(token);
14527
+ }
14283
14528
  function runnerName(token) {
14284
14529
  if (!token)
14285
14530
  return "";
@@ -14536,7 +14781,7 @@ function tokenizeStage(stage) {
14536
14781
  // ../aft-bridge/dist/pool.js
14537
14782
  import { realpathSync as realpathSync2 } from "node:fs";
14538
14783
  import { homedir as homedir6 } from "node:os";
14539
- var DEFAULT_IDLE_TIMEOUT_MS = Infinity;
14784
+ var DEFAULT_IDLE_TIMEOUT_MS = 30 * 60 * 1000;
14540
14785
  var DEFAULT_MAX_POOL_SIZE = 8;
14541
14786
  var CLEANUP_INTERVAL_MS = 60 * 1000;
14542
14787
  function canonicalHomeDir() {
@@ -14631,7 +14876,7 @@ class BridgePool {
14631
14876
  cleanup() {
14632
14877
  const now = Date.now();
14633
14878
  for (const [dir, entry] of this.bridges) {
14634
- if (entry.bridge.hasPendingRequests())
14879
+ if (entry.bridge.hasPendingRequests() || entry.bridge.hasOutstandingBackgroundTasks())
14635
14880
  continue;
14636
14881
  if (now - entry.lastUsed > this.idleTimeoutMs) {
14637
14882
  entry.bridge.shutdown().catch((err) => this.error("cleanup shutdown failed:", err));
@@ -14639,7 +14884,7 @@ class BridgePool {
14639
14884
  }
14640
14885
  }
14641
14886
  for (const bridge of this.staleBridges) {
14642
- if (bridge.hasPendingRequests())
14887
+ if (bridge.hasPendingRequests() || bridge.hasOutstandingBackgroundTasks())
14643
14888
  continue;
14644
14889
  bridge.shutdown().catch((err) => this.error("stale cleanup shutdown failed:", err));
14645
14890
  this.staleBridges.delete(bridge);
@@ -14649,7 +14894,7 @@ class BridgePool {
14649
14894
  let oldestDir = null;
14650
14895
  let oldestTime = Infinity;
14651
14896
  for (const [dir, entry] of this.bridges) {
14652
- if (entry.bridge.hasPendingRequests())
14897
+ if (entry.bridge.hasPendingRequests() || entry.bridge.hasOutstandingBackgroundTasks())
14653
14898
  continue;
14654
14899
  if (entry.lastUsed < oldestTime) {
14655
14900
  oldestTime = entry.lastUsed;
@@ -15455,7 +15700,7 @@ import {
15455
15700
  // package.json
15456
15701
  var package_default = {
15457
15702
  name: "@cortexkit/aft-pi",
15458
- version: "0.36.1",
15703
+ version: "0.37.1",
15459
15704
  type: "module",
15460
15705
  description: "Pi coding agent extension for Agent File Tools (AFT) — tree-sitter and LSP-powered code analysis",
15461
15706
  main: "dist/index.js",
@@ -15478,7 +15723,7 @@ var package_default = {
15478
15723
  "test:unit": "bun test src/__tests__ --path-ignore-patterns 'src/__tests__/e2e/**'"
15479
15724
  },
15480
15725
  dependencies: {
15481
- "@cortexkit/aft-bridge": "0.36.1",
15726
+ "@cortexkit/aft-bridge": "0.37.1",
15482
15727
  "@xterm/headless": "^5.5.0",
15483
15728
  "comment-json": "^5.0.0",
15484
15729
  diff: "^8.0.4",
@@ -15486,12 +15731,12 @@ var package_default = {
15486
15731
  zod: "^4.1.8"
15487
15732
  },
15488
15733
  optionalDependencies: {
15489
- "@cortexkit/aft-darwin-arm64": "0.36.1",
15490
- "@cortexkit/aft-darwin-x64": "0.36.1",
15491
- "@cortexkit/aft-linux-arm64": "0.36.1",
15492
- "@cortexkit/aft-linux-x64": "0.36.1",
15493
- "@cortexkit/aft-win32-arm64": "0.36.1",
15494
- "@cortexkit/aft-win32-x64": "0.36.1"
15734
+ "@cortexkit/aft-darwin-arm64": "0.37.1",
15735
+ "@cortexkit/aft-darwin-x64": "0.37.1",
15736
+ "@cortexkit/aft-linux-arm64": "0.37.1",
15737
+ "@cortexkit/aft-linux-x64": "0.37.1",
15738
+ "@cortexkit/aft-win32-arm64": "0.37.1",
15739
+ "@cortexkit/aft-win32-x64": "0.37.1"
15495
15740
  },
15496
15741
  devDependencies: {
15497
15742
  "@earendil-works/pi-coding-agent": "*",
@@ -15749,19 +15994,6 @@ function isEmptyParam(value) {
15749
15994
  return Object.keys(value).length === 0;
15750
15995
  return false;
15751
15996
  }
15752
- var LONG_RUNNING_COMMAND_TIMEOUT_MS = {
15753
- callers: 60000,
15754
- trace_to: 60000,
15755
- trace_to_symbol: 60000,
15756
- trace_data: 60000,
15757
- impact: 60000,
15758
- grep: 60000,
15759
- glob: 60000,
15760
- semantic_search: 60000
15761
- };
15762
- function timeoutForCommand(command) {
15763
- return LONG_RUNNING_COMMAND_TIMEOUT_MS[command];
15764
- }
15765
15997
  function asPlainObject(value) {
15766
15998
  if (!value || typeof value !== "object" || Array.isArray(value))
15767
15999
  return;
@@ -32282,6 +32514,21 @@ function registerShutdownCleanup(fn) {
32282
32514
  };
32283
32515
  }
32284
32516
 
32517
+ // src/sync-watch-abort.ts
32518
+ var abortFlags = new Map;
32519
+ function signalSyncWatchAbort(sessionID) {
32520
+ const key = sessionID || "__default__";
32521
+ abortFlags.set(key, true);
32522
+ }
32523
+ function clearSyncWatchAbort(sessionID) {
32524
+ const key = sessionID || "__default__";
32525
+ abortFlags.delete(key);
32526
+ }
32527
+ function isSyncWatchAborted(sessionID) {
32528
+ const key = sessionID || "__default__";
32529
+ return abortFlags.get(key) === true;
32530
+ }
32531
+
32285
32532
  // src/tools/ast.ts
32286
32533
  import { StringEnum } from "@earendil-works/pi-ai";
32287
32534
  import { Type as Type3 } from "typebox";
@@ -32399,7 +32646,6 @@ function formatDiffForPi(oldContent, newContent, contextLines = DEFAULT_CONTEXT_
32399
32646
  }
32400
32647
 
32401
32648
  // src/tools/hoisted.ts
32402
- var DIAGNOSTICS_PARAM_DESCRIPTION = "When true, wait up to 3 seconds for fresh LSP diagnostics on the edited file and include them in the result. Defaults to the configured `lsp.diagnostics_on_edit` value (false unless configured); per-call true/false overrides. Use aft_inspect to check diagnostics across a batch of edits or before tests/commits.";
32403
32649
  function diagnosticsOnEditDefault(ctx) {
32404
32650
  return ctx.config.lsp?.diagnostics_on_edit ?? false;
32405
32651
  }
@@ -32469,8 +32715,7 @@ var WriteParams = Type2.Object({
32469
32715
  filePath: Type2.String({
32470
32716
  description: "Path to the file to write (absolute or relative to project root)"
32471
32717
  }),
32472
- content: Type2.String({ description: "Full file contents to write" }),
32473
- diagnostics: Type2.Optional(Type2.Boolean({ description: DIAGNOSTICS_PARAM_DESCRIPTION }))
32718
+ content: Type2.String({ description: "Full file contents to write" })
32474
32719
  });
32475
32720
  var EditParams = Type2.Object({
32476
32721
  filePath: Type2.String({
@@ -32482,8 +32727,7 @@ var EditParams = Type2.Object({
32482
32727
  occurrence: optionalInt(0, Number.MAX_SAFE_INTEGER),
32483
32728
  appendContent: Type2.Optional(Type2.String({
32484
32729
  description: "Append text to the end of the file (creates the file if missing, parent dirs auto-created). When set, oldString/newString are ignored."
32485
- })),
32486
- diagnostics: Type2.Optional(Type2.Boolean({ description: DIAGNOSTICS_PARAM_DESCRIPTION }))
32730
+ }))
32487
32731
  });
32488
32732
  var GrepParams = Type2.Object({
32489
32733
  pattern: Type2.String({ description: "Regex pattern to search for" }),
@@ -32491,8 +32735,7 @@ var GrepParams = Type2.Object({
32491
32735
  description: "Path scope (file or directory; absolute or relative to project root)"
32492
32736
  })),
32493
32737
  include: Type2.Optional(Type2.String({ description: "Glob filter for included files (e.g. '*.ts,*.tsx')" })),
32494
- caseSensitive: Type2.Optional(Type2.Boolean({ description: "Case-sensitive matching" })),
32495
- contextLines: optionalInt(1, Number.MAX_SAFE_INTEGER)
32738
+ caseSensitive: Type2.Optional(Type2.Boolean({ description: "Case-sensitive matching" }))
32496
32739
  });
32497
32740
  function registerHoistedTools(pi, ctx, surface) {
32498
32741
  if (surface.hoistRead) {
@@ -32507,7 +32750,9 @@ function registerHoistedTools(pi, ctx, surface) {
32507
32750
  const bridge = bridgeFor(ctx, extCtx.cwd);
32508
32751
  const offset = coerceOptionalInt(params.offset, "offset", 1, Number.MAX_SAFE_INTEGER);
32509
32752
  const limit = coerceOptionalInt(params.limit, "limit", 1, Number.MAX_SAFE_INTEGER);
32510
- const req = { file: params.path };
32753
+ const req = {
32754
+ file: await resolvePathArg(extCtx.cwd, params.path)
32755
+ };
32511
32756
  if (offset !== undefined) {
32512
32757
  req.start_line = offset;
32513
32758
  if (limit !== undefined) {
@@ -32534,19 +32779,20 @@ function registerHoistedTools(pi, ctx, surface) {
32534
32779
  pi.registerTool({
32535
32780
  name: "write",
32536
32781
  label: "write",
32537
- description: "Write a file atomically with per-file backup and optional auto-format. Parent directories are created automatically. Overwrites existing files. Uses `filePath` (not `path`). Edits return as soon as the write completes unless `lsp.diagnostics_on_edit` or a per-call `diagnostics: true` requests legacy sync-wait behavior. Call `aft_inspect` afterward to check diagnostics across a batch of edits.",
32538
- promptSnippet: "Create or overwrite files (uses filePath; auto-formats; diagnostics follow lsp.diagnostics_on_edit unless overridden)",
32782
+ description: "Write content to a file, creating it and parent directories automatically. Existing files are backed up before overwriting (undo via aft_safety) and auto-formatted when the project has a formatter configured. Uses `filePath` (not `path`). For partial edits, use the `edit` tool.",
32783
+ promptSnippet: "Create or overwrite files (uses filePath; auto-formats)",
32539
32784
  promptGuidelines: ["Use write only for new files or complete rewrites."],
32540
32785
  parameters: WriteParams,
32541
32786
  async execute(_toolCallId, params, _signal, _onUpdate, extCtx) {
32542
- await assertExternalDirectoryPermission(extCtx, params.filePath, "modify", {
32787
+ const filePath = await resolvePathArg(extCtx.cwd, params.filePath);
32788
+ await assertExternalDirectoryPermission(extCtx, filePath, "modify", {
32543
32789
  restrictToProjectRoot: surface.restrictToProjectRoot
32544
32790
  });
32545
32791
  const bridge = bridgeFor(ctx, extCtx.cwd);
32546
32792
  const response = await callBridge(bridge, "write", {
32547
- file: params.filePath,
32793
+ file: filePath,
32548
32794
  content: params.content,
32549
- diagnostics: params.diagnostics ?? diagnosticsOnEditDefault(ctx),
32795
+ diagnostics: diagnosticsOnEditDefault(ctx),
32550
32796
  include_diff_content: true
32551
32797
  }, extCtx);
32552
32798
  return buildMutationResult(response);
@@ -32563,8 +32809,8 @@ function registerHoistedTools(pi, ctx, surface) {
32563
32809
  pi.registerTool({
32564
32810
  name: "edit",
32565
32811
  label: "edit",
32566
- description: "Find-and-replace edit with progressive fuzzy matching (handles whitespace and Unicode drift). Uses `filePath`, `oldString`, `newString`. Errors on multiple matches — use `occurrence` to pick one, or `replaceAll: true`. Edits return as soon as the write completes unless `lsp.diagnostics_on_edit` or a per-call `diagnostics: true` requests legacy sync-wait behavior. Call `aft_inspect` afterward to check diagnostics across a batch of edits.",
32567
- promptSnippet: "Targeted find-and-replace (uses filePath/oldString/newString; occurrence or replaceAll for disambiguation; fuzzy whitespace matching). Pass appendContent to append to a file (creates if missing). Diagnostics follow lsp.diagnostics_on_edit unless overridden.",
32812
+ description: "Find-and-replace edit with progressive fuzzy matching (handles whitespace and Unicode drift). Uses `filePath`, `oldString`, `newString`. Errors on multiple matches — use `occurrence` to pick one, or `replaceAll: true`.",
32813
+ promptSnippet: "Targeted find-and-replace (uses filePath/oldString/newString; occurrence or replaceAll for disambiguation; fuzzy whitespace matching). Pass appendContent to append to a file (creates if missing).",
32568
32814
  promptGuidelines: [
32569
32815
  "Prefer edit over write when changing part of an existing file.",
32570
32816
  "Include enough surrounding context in oldString to make the match unique, or set replaceAll/occurrence explicitly.",
@@ -32572,26 +32818,27 @@ function registerHoistedTools(pi, ctx, surface) {
32572
32818
  ],
32573
32819
  parameters: EditParams,
32574
32820
  async execute(_toolCallId, params, _signal, _onUpdate, extCtx) {
32575
- await assertExternalDirectoryPermission(extCtx, params.filePath, "modify", {
32821
+ const filePath = await resolvePathArg(extCtx.cwd, params.filePath);
32822
+ await assertExternalDirectoryPermission(extCtx, filePath, "modify", {
32576
32823
  restrictToProjectRoot: surface.restrictToProjectRoot
32577
32824
  });
32578
32825
  const bridge = bridgeFor(ctx, extCtx.cwd);
32579
32826
  if (typeof params.appendContent === "string") {
32580
32827
  const req2 = {
32581
32828
  op: "append",
32582
- file: params.filePath,
32829
+ file: filePath,
32583
32830
  append_content: params.appendContent,
32584
- diagnostics: params.diagnostics ?? diagnosticsOnEditDefault(ctx),
32831
+ diagnostics: diagnosticsOnEditDefault(ctx),
32585
32832
  include_diff_content: true
32586
32833
  };
32587
32834
  const response2 = await callBridge(bridge, "edit_match", req2, extCtx);
32588
32835
  return buildMutationResult(response2);
32589
32836
  }
32590
32837
  const req = {
32591
- file: params.filePath,
32838
+ file: filePath,
32592
32839
  match: params.oldString ?? "",
32593
32840
  replacement: params.newString ?? "",
32594
- diagnostics: params.diagnostics ?? diagnosticsOnEditDefault(ctx),
32841
+ diagnostics: diagnosticsOnEditDefault(ctx),
32595
32842
  include_diff_content: true
32596
32843
  };
32597
32844
  if (params.replaceAll === true)
@@ -32631,9 +32878,6 @@ function registerHoistedTools(pi, ctx, surface) {
32631
32878
  req.include = splitIncludeGlobs(params.include);
32632
32879
  if (params.caseSensitive !== undefined)
32633
32880
  req.case_sensitive = params.caseSensitive;
32634
- const contextLines = coerceOptionalInt(params.contextLines, "contextLines", 1, Number.MAX_SAFE_INTEGER);
32635
- if (contextLines !== undefined)
32636
- req.context_lines = contextLines;
32637
32881
  const response = await callBridge(bridge, "grep", req, extCtx);
32638
32882
  const text = response.text ?? "";
32639
32883
  return textResult(text);
@@ -33194,7 +33438,7 @@ function registerAstTools(pi, ctx, surface) {
33194
33438
  if (!isEmptyParam(params.globs))
33195
33439
  req.globs = params.globs;
33196
33440
  if (params.contextLines !== undefined)
33197
- req.context_lines = params.contextLines;
33441
+ req.context = params.contextLines;
33198
33442
  const response = await callBridge(bridge, "ast_search", req, extCtx);
33199
33443
  return textResult(response.text ?? JSON.stringify(response));
33200
33444
  },
@@ -33348,22 +33592,28 @@ function getBashSpawnHook(pi) {
33348
33592
  }
33349
33593
  return api2.hooks?.bashSpawn;
33350
33594
  }
33351
- function registerBashTool(pi, ctx) {
33595
+ function registerBashTool(pi, ctx, aftSearchRegistered = false) {
33352
33596
  const spawnHook = getBashSpawnHook(pi);
33597
+ const searchSteer = aftSearchRegistered ? "use `aft_search` (concepts, identifiers, regex, literals), `read`, `aft_outline`, or `aft_zoom` instead" : "use the `grep` tool, `read`, `aft_outline`, or `aft_zoom` instead";
33598
+ const bashCfg = resolveBashConfig(ctx.config);
33599
+ const compressionSentence = bashCfg.compress ? " Output is compressed by default; pass `compressed: false` for raw output." : "";
33600
+ const tasksSentence = bashCfg.background ? ' Pass `background: true` to run in the background and get a task_id for `bash_status`/`bash_kill`. Pass `pty: true` for interactive programs (REPLs, TUIs) and drive them with `bash_status({ output_mode: "screen" })` plus `bash_write`.' : " Commands that outlive the foreground wait window are promoted to background tasks; inspect them with `bash_status({ task_id })` or terminate with `bash_kill`.";
33353
33601
  pi.registerTool({
33354
33602
  name: "bash",
33355
33603
  label: "bash",
33356
- description: 'Execute shell commands through AFT\'s Rust bash handler. By default, output is compressed. Pass `compressed: false` for raw output. Pass `background: true` to spawn in the background and get a task_id for `bash_status`/`bash_kill`. Pass `pty: true` with `background: true` for interactive programs and drive them with `bash_status({ output_mode: "screen" })` plus `bash_write`.',
33604
+ description: `Execute shell commands.${compressionSentence}${tasksSentence}
33605
+
33606
+ 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}.`,
33357
33607
  promptSnippet: "Run shell commands (timeout in milliseconds; supports workdir, background tasks, compressed output, PTY mode)",
33358
33608
  promptGuidelines: [
33359
- "Use bash only when a dedicated AFT tool is not a better fit.",
33609
+ `DO NOT use bash for code search or exploration ${searchSteer}.`,
33360
33610
  "Set compressed: false when you need ANSI color codes in the output."
33361
33611
  ],
33362
33612
  parameters: BashParams,
33363
33613
  async execute(_toolCallId, params, _signal, onUpdate, extCtx) {
33364
33614
  const bridge = bridgeFor(ctx, extCtx.cwd);
33365
- const bashCfg = resolveBashConfig(ctx.config);
33366
- const foregroundWaitMs = resolveForegroundWaitMs(bashCfg.foreground_wait_window_ms);
33615
+ const bashCfg2 = resolveBashConfig(ctx.config);
33616
+ const foregroundWaitMs = resolveForegroundWaitMs(bashCfg2.foreground_wait_window_ms);
33367
33617
  const timeout = coerceOptionalInt(params.timeout, "timeout", 1, Number.MAX_SAFE_INTEGER);
33368
33618
  const ptyRows = coerceOptionalInt(params.ptyRows, "ptyRows", 1, 60);
33369
33619
  const ptyCols = coerceOptionalInt(params.ptyCols, "ptyCols", 1, 140);
@@ -33380,7 +33630,7 @@ function registerBashTool(pi, ctx) {
33380
33630
  throw new Error(`BashSpawnHook failed: ${hookErr instanceof Error ? hookErr.message : String(hookErr)}`);
33381
33631
  }
33382
33632
  }
33383
- const compressionEnabled = bashCfg.compress && params.compressed !== false;
33633
+ const compressionEnabled = bashCfg2.compress && params.compressed !== false;
33384
33634
  const pipeStrip = maybeStripCompressorPipe(spawnContext.command, compressionEnabled);
33385
33635
  const bridgeCommand = pipeStrip.command;
33386
33636
  let streamed = "";
@@ -33425,7 +33675,7 @@ function registerBashTool(pi, ctx) {
33425
33675
  throw new Error(status.message ?? "bash_status failed");
33426
33676
  }
33427
33677
  if (isTerminalStatus(status.status)) {
33428
- return bashResult(appendPipeStripNote(withBashHints(formatForegroundResult(status)), pipeStrip.note), {
33678
+ return bashResult(appendPipeStripNote(withBashHints(formatForegroundResult(status), bridgeCommand, aftSearchRegistered), pipeStrip.note), {
33429
33679
  exit_code: status.exit_code,
33430
33680
  duration_ms: status.duration_ms,
33431
33681
  truncated: status.output_truncated,
@@ -33452,7 +33702,7 @@ function registerBashTool(pi, ctx) {
33452
33702
  task_id: taskId
33453
33703
  };
33454
33704
  const output = response.output ?? "";
33455
- return bashResult(appendPipeStripNote(withBashHints(output), pipeStrip.note), details);
33705
+ return bashResult(appendPipeStripNote(withBashHints(output, bridgeCommand, aftSearchRegistered), pipeStrip.note), details);
33456
33706
  },
33457
33707
  renderCall(args, theme, context) {
33458
33708
  return renderBashCall(args?.command, args?.description, theme, context);
@@ -33484,8 +33734,8 @@ function formatPromotionMessage(taskId, timeout, waitWindowMs) {
33484
33734
  function formatSeconds(ms) {
33485
33735
  return `${Number((ms / 1000).toFixed(1))}s`;
33486
33736
  }
33487
- function withBashHints(output) {
33488
- return maybeAppendConflictsHint(output);
33737
+ function withBashHints(output, command, aftSearchRegistered) {
33738
+ return maybeAppendGrepSearchHint(maybeAppendConflictsHint(output), command, aftSearchRegistered);
33489
33739
  }
33490
33740
  function formatForegroundResult(data) {
33491
33741
  const output = data.output_preview ?? "";
@@ -33567,11 +33817,41 @@ function createBashWatchTool(ctx) {
33567
33817
  A notification will fire when the pattern matches or the task exits.`, watchDetails);
33568
33818
  }
33569
33819
  const data = await waitForBashStatus(ctx, bridge, extCtx, params.task_id, undefined, waitFor, true, Math.min(coerceOptionalInt(params.timeout_ms, "timeout_ms", 1, MAX_BASH_STATUS_WAIT_TIMEOUT_MS) ?? DEFAULT_BASH_STATUS_WAIT_TIMEOUT_MS, MAX_BASH_STATUS_WAIT_TIMEOUT_MS));
33820
+ if (data.waited?.reason === "user_message") {
33821
+ const convertedText = await convertToAsyncWatchOnAbort(bridge, extCtx, params.task_id, waitFor, params.once !== false);
33822
+ return textResult(convertedText, { waited: data.waited });
33823
+ }
33570
33824
  const text = await formatBashStatus(extCtx, params.task_id, data, undefined);
33571
33825
  return textResult(text, data);
33572
33826
  }
33573
33827
  };
33574
33828
  }
33829
+ async function convertToAsyncWatchOnAbort(bridge, extCtx, taskId, waitFor, once) {
33830
+ if (!waitFor) {
33831
+ return `Sync watch for task ${taskId} was interrupted because you sent a message. ` + `The task is still running in the background. A completion reminder will be ` + `delivered automatically when the task exits; don't poll bash_status.`;
33832
+ }
33833
+ const notifyParams = {
33834
+ task_id: taskId,
33835
+ once
33836
+ };
33837
+ if (waitFor.kind === "regex")
33838
+ notifyParams.regex = waitFor.source;
33839
+ else
33840
+ notifyParams.pattern = waitFor.value;
33841
+ const sessionId = resolveSessionId(extCtx);
33842
+ markExplicitControl(sessionId, taskId, false);
33843
+ try {
33844
+ const registered = await callBashBridge(bridge, "bash_notify", notifyParams, extCtx);
33845
+ if (registered.success === false) {
33846
+ unmarkExplicitControl(sessionId, taskId);
33847
+ return `Sync watch for task ${taskId} was interrupted because you sent a message. ` + `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.`;
33848
+ }
33849
+ return `Sync watch for task ${taskId} was interrupted because you sent a message. ` + `The wait has been converted to an async watch (${registered.watch_id}). ` + `A notification will fire when the pattern matches or the task exits.`;
33850
+ } catch (err) {
33851
+ unmarkExplicitControl(sessionId, taskId);
33852
+ return `Sync watch for task ${taskId} was interrupted because you sent a message. ` + `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.`;
33853
+ }
33854
+ }
33575
33855
  function createBashWriteTool(ctx) {
33576
33856
  return {
33577
33857
  name: "bash_write",
@@ -33647,6 +33927,7 @@ async function waitForBashStatus(ctx, bridge, extCtx, taskId, outputMode, waitFo
33647
33927
  transportTimeoutMs: BASH_TRANSPORT_TIMEOUT_MS
33648
33928
  };
33649
33929
  const sessionId = resolveSessionId(extCtx);
33930
+ clearSyncWatchAbort(sessionId);
33650
33931
  if (waitForExit)
33651
33932
  markTaskWaiting(sessionId, taskId);
33652
33933
  let sawTerminal = false;
@@ -33685,6 +33966,9 @@ async function waitForBashStatus(ctx, bridge, extCtx, taskId, outputMode, waitFo
33685
33966
  }
33686
33967
  return withWaited(data, { reason: "exited", elapsed_ms: Date.now() - startedAt });
33687
33968
  }
33969
+ if (isSyncWatchAborted(sessionId)) {
33970
+ return withWaited(data, { reason: "user_message", elapsed_ms: Date.now() - startedAt });
33971
+ }
33688
33972
  if (Date.now() >= deadline) {
33689
33973
  return withWaited(data, { reason: "timeout", elapsed_ms: Date.now() - startedAt });
33690
33974
  }
@@ -33953,8 +34237,12 @@ function registerConflictsTool(pi, ctx) {
33953
34237
  const bridge = bridgeFor(ctx, extCtx.cwd);
33954
34238
  const reqParams = {};
33955
34239
  const path2 = params?.path;
33956
- if (typeof path2 === "string" && path2.trim() !== "")
33957
- reqParams.path = path2;
34240
+ if (typeof path2 === "string" && path2.trim() !== "") {
34241
+ await assertExternalDirectoryPermission(extCtx, path2, "inspect", {
34242
+ restrictToProjectRoot: ctx.config.restrict_to_project_root ?? false
34243
+ });
34244
+ reqParams.path = await resolvePathArg(extCtx.cwd, path2);
34245
+ }
33958
34246
  const response = await callBridge(bridge, "git_conflicts", reqParams, extCtx);
33959
34247
  return textResult(response.text ?? JSON.stringify(response, null, 2));
33960
34248
  },
@@ -35247,7 +35535,7 @@ function registerRefactorTool(pi, ctx) {
35247
35535
  pi.registerTool({
35248
35536
  name: "aft_refactor",
35249
35537
  label: "refactor",
35250
- description: "Workspace-wide refactoring that updates imports and references across files. `move` relocates a top-level symbol; `extract` pulls a line range into a new function; `inline` replaces a call site. Use aft_safety checkpoint/undo before risky refactors.",
35538
+ description: "Workspace-wide refactoring that updates imports and references across files. `move` relocates a top-level symbol (not nested functions or class methods) to another file, rewriting imports workspace-wide; a checkpoint is created first. To move/rename a whole file, use aft_move. `extract` pulls a line range into a new function (TS/JS/TSX, Python). `inline` replaces a call with the function's body.",
35251
35539
  parameters: RefactorParams,
35252
35540
  async execute(_toolCallId, params, _signal, _onUpdate, extCtx) {
35253
35541
  const commandMap = {
@@ -35626,21 +35914,9 @@ function registerSemanticTool(pi, ctx) {
35626
35914
  name: "aft_search",
35627
35915
  label: "search",
35628
35916
  description: [
35629
- "Find code with unified semantic, lexical, literal, and regex search. Returns ranked symbol/file results or exact matching lines, with routing metadata.",
35917
+ "Search code with one tool: concepts, identifiers, error strings, regex, literals, and filenames are auto-routed to the right engine and returned ranked. Use it for any code search — including when you only know what the code does, not what it's named ('where is rate limiting handled', 'retry logic', '^export', 'Cargo.lock').",
35630
35918
  "",
35631
- "When to reach for it:",
35632
- "- Exploring an unfamiliar area: 'where is rate limiting handled', 'how does auth flow work'",
35633
- "- Concept doesn't appear as a literal string: 'retry logic', 'cache invalidation', 'graceful shutdown'",
35634
- "- Filename-shaped concepts: 'the bridge spawn helper', 'the session detection module'",
35635
- "- Regex-shaped or exact text queries when you want AFT to classify and route automatically",
35636
- "- You know roughly what the function does but not what it's named",
35637
- "",
35638
- "When NOT to use:",
35639
- "- You need exhaustive literal enumeration → use grep directly",
35640
- "- You want the file/module structure → use aft_outline",
35641
- "- You're following a call chain → use aft_callgraph",
35642
- "",
35643
- "Set hint to 'regex', 'literal', 'semantic', or 'auto' to override or document routing intent."
35919
+ "Set hint to 'regex', 'literal', or 'semantic' to force a lane."
35644
35920
  ].join(`
35645
35921
  `),
35646
35922
  parameters: SearchParams2,
@@ -35671,138 +35947,8 @@ ${extra}`;
35671
35947
  });
35672
35948
  }
35673
35949
 
35674
- // src/tools/structure.ts
35675
- import { StringEnum as StringEnum6 } from "@earendil-works/pi-ai";
35676
- import { Type as Type14 } from "typebox";
35677
- var TransformParams = Type14.Object({
35678
- op: StringEnum6(["add_member", "add_derive", "wrap_try_catch", "add_decorator", "add_struct_tags"], { description: "Transformation operation" }),
35679
- filePath: Type14.String({
35680
- description: "Path to the source file (absolute or relative to project root)"
35681
- }),
35682
- container: Type14.Optional(Type14.String({ description: "Class/struct/impl name for add_member" })),
35683
- code: Type14.Optional(Type14.String({ description: "Member code to insert (add_member)" })),
35684
- target: Type14.Optional(Type14.String({ description: "Target symbol name" })),
35685
- derives: Type14.Optional(Type14.Array(Type14.String(), { description: "Derive macro names (add_derive)" })),
35686
- catchBody: Type14.Optional(Type14.String({ description: "Catch block body (wrap_try_catch, default: 'throw error;')" })),
35687
- decorator: Type14.Optional(Type14.String({ description: "Decorator text without @ (add_decorator)" })),
35688
- field: Type14.Optional(Type14.String({ description: "Struct field name (add_struct_tags)" })),
35689
- tag: Type14.Optional(Type14.String({ description: "Tag key (add_struct_tags)" })),
35690
- value: Type14.Optional(Type14.String({ description: "Tag value (add_struct_tags)" })),
35691
- position: Type14.Optional(Type14.String({
35692
- description: "Position hint: 'first', 'last' (default), 'before:name', 'after:name' for add_member"
35693
- })),
35694
- validate: Type14.Optional(StringEnum6(["syntax", "full"], {
35695
- description: "Validation level (default: syntax)"
35696
- }))
35697
- });
35698
- function buildTransformSections(args, payload, theme) {
35699
- const response = asRecord2(payload);
35700
- if (!response)
35701
- return [theme.fg("muted", "No transform result.")];
35702
- const target = asString(response.target) ?? asString(response.scope) ?? args.target ?? args.container ?? args.field ?? args.filePath;
35703
- return [
35704
- `${theme.fg("success", "transformed")} ${theme.fg("accent", args.op)}`,
35705
- `${theme.fg("muted", "file")} ${theme.fg("accent", asString(response.file) ?? args.filePath)}`,
35706
- target ? `${theme.fg("muted", "target")} ${target}` : theme.fg("muted", "No target metadata.")
35707
- ];
35708
- }
35709
- function renderTransformCall(args, theme, context) {
35710
- const target = args.target ?? args.container ?? args.field;
35711
- const summary = [
35712
- theme.fg("accent", args.op),
35713
- accentPath(theme, args.filePath),
35714
- target ? theme.fg("toolOutput", target) : undefined
35715
- ].filter(Boolean).join(" ");
35716
- return renderToolCall("transform", summary, theme, context);
35717
- }
35718
- function renderTransformResult(result, args, theme, context) {
35719
- if (context.isError)
35720
- return renderErrorResult(result, "transform failed", theme, context);
35721
- return renderSections(buildTransformSections(args, extractStructuredPayload(result), theme), context);
35722
- }
35723
- function registerStructureTool(pi, ctx) {
35724
- pi.registerTool({
35725
- name: "aft_transform",
35726
- label: "transform",
35727
- description: "Scope-aware structural code transformations with correct indentation. Use aft_safety checkpoint/undo before risky transforms.",
35728
- parameters: TransformParams,
35729
- async execute(_toolCallId, params, _signal, _onUpdate, extCtx) {
35730
- validateTransformParams(params);
35731
- const filePath = await resolvePathArg(extCtx.cwd, params.filePath);
35732
- await assertExternalDirectoryPermission(extCtx, filePath, "modify", {
35733
- restrictToProjectRoot: ctx.config.restrict_to_project_root ?? false
35734
- });
35735
- const bridge = bridgeFor(ctx, extCtx.cwd);
35736
- const req = { file: filePath };
35737
- if (params.container !== undefined)
35738
- req.scope = params.container;
35739
- if (params.code !== undefined)
35740
- req.code = params.code;
35741
- if (params.target !== undefined)
35742
- req.target = params.target;
35743
- if (params.derives !== undefined)
35744
- req.derives = params.derives;
35745
- if (params.catchBody !== undefined)
35746
- req.catch_body = params.catchBody;
35747
- if (params.decorator !== undefined)
35748
- req.decorator = params.decorator;
35749
- if (params.field !== undefined)
35750
- req.field = params.field;
35751
- if (params.tag !== undefined)
35752
- req.tag = params.tag;
35753
- if (params.value !== undefined)
35754
- req.value = params.value;
35755
- if (params.position !== undefined)
35756
- req.position = params.position;
35757
- if (params.validate !== undefined)
35758
- req.validate = params.validate;
35759
- const response = await callBridge(bridge, params.op, req, extCtx);
35760
- return textResult(JSON.stringify(response, null, 2));
35761
- },
35762
- renderCall(args, theme, context) {
35763
- return renderTransformCall(args, theme, context);
35764
- },
35765
- renderResult(result, _options, theme, context) {
35766
- return renderTransformResult(result, context.args, theme, context);
35767
- }
35768
- });
35769
- }
35770
- function validateTransformParams(params) {
35771
- const op = params.op;
35772
- if (op === "add_member") {
35773
- if (isEmptyParam(params.container)) {
35774
- throw new Error("'container' is required for 'add_member' op");
35775
- }
35776
- if (isEmptyParam(params.code)) {
35777
- throw new Error("'code' is required for 'add_member' op");
35778
- }
35779
- }
35780
- if (op === "add_derive" || op === "wrap_try_catch" || op === "add_decorator" || op === "add_struct_tags") {
35781
- if (isEmptyParam(params.target)) {
35782
- throw new Error(`'target' is required for '${op}' op`);
35783
- }
35784
- }
35785
- if (op === "add_derive" && isEmptyParam(params.derives)) {
35786
- throw new Error("'derives' array is required for 'add_derive' op");
35787
- }
35788
- if (op === "add_decorator" && isEmptyParam(params.decorator)) {
35789
- throw new Error("'decorator' is required for 'add_decorator' op");
35790
- }
35791
- if (op === "add_struct_tags") {
35792
- if (isEmptyParam(params.field)) {
35793
- throw new Error("'field' is required for 'add_struct_tags' op");
35794
- }
35795
- if (isEmptyParam(params.tag)) {
35796
- throw new Error("'tag' is required for 'add_struct_tags' op");
35797
- }
35798
- if (isEmptyParam(params.value)) {
35799
- throw new Error("'value' is required for 'add_struct_tags' op");
35800
- }
35801
- }
35802
- }
35803
-
35804
35950
  // src/workflow-hints.ts
35805
- var HEADING = "## Prefer AFT tools for token efficiency";
35951
+ var HEADING = "## IMPORTANT NOTICE about your tools";
35806
35952
  function buildWorkflowHints(opts) {
35807
35953
  const sections = [];
35808
35954
  const grepName = opts.hoistBuiltins ? "grep" : "aft_grep";
@@ -35816,14 +35962,28 @@ function buildWorkflowHints(opts) {
35816
35962
  const hasBash = !opts.absentTools.has(bashName);
35817
35963
  const hasBgBash = opts.bashBackgroundEnabled && hasBash && !opts.absentTools.has("bash_status");
35818
35964
  if (hasBash && opts.bashCompressionEnabled) {
35819
- sections.push("When AFT bash output compression is on, do NOT pipe test/build commands through grep/head/tail (e.g. `bun test | grep fail`) to summarize. The compressor already keeps failures and the summary; piping hides failures. Run the command bare.");
35965
+ sections.push([
35966
+ "**Test/build output**: bash output is auto-compressed — failures and the summary are always kept. DO NOT pipe test/build commands through filters to summarize; that hides failures:",
35967
+ "- `bun test | grep fail` → run `bun test`",
35968
+ "- `cargo test 2>&1 | tail -20` → run `cargo test`",
35969
+ "- `npm run build | head -50` → run `npm run build`"
35970
+ ].join(`
35971
+ `));
35820
35972
  }
35821
35973
  if (hasOutline && hasZoom) {
35822
35974
  sections.push(`**Web/URL access**: \`aft_outline({ target: "<url>" })\` first for structure, then \`aft_zoom({ url: "<url>", symbols: "<heading>" })\` for the specific section.`);
35823
35975
  }
35824
35976
  if (hasOutline && hasZoom && (hasGrep || hasSearch)) {
35977
+ const searchName = hasSearch ? "aft_search" : grepName;
35825
35978
  const locate = hasSearch ? '`aft_search` is the primary code-search tool: one call auto-routes concepts, identifiers, regex, error strings, and literals (pass `hint: "regex"`/`"literal"`/`"semantic"` to force a lane).' : `\`${grepName}\` (the tool — indexed and ranked) locates code.`;
35826
- sections.push(`**Code exploration**: fire independent lookups in ONE parallel tool-call wave — do NOT serialize them. ${locate} Then \`aft_outline\` for structure → \`aft_zoom\` for symbol(s). DO NOT run \`grep\`/\`rg\`/\`find\` through \`bash\` to locate code — the bash path is unindexed, unranked, serial, and routinely surfaces the wrong hit. Keep \`bash\` for shell facts (git state, file metadata, processes).`);
35979
+ const readName = opts.hoistBuiltins ? "read" : "aft_read";
35980
+ sections.push([
35981
+ `**Code exploration**: ${locate} Then \`aft_outline\` for structure → \`aft_zoom\` for symbol(s). DO NOT run \`grep\`/\`rg\`/\`find\`/\`sed\`/\`cat\` through \`bash\` to locate or read code — the bash path is unindexed, unranked, serial, and routinely surfaces the wrong hit. Keep \`bash\` for shell facts (git state, file metadata, processes). Reflex translations:`,
35982
+ `- \`grep -rn "handleAuth" src/\` in bash → \`${searchName}({ query: "handleAuth" })\``,
35983
+ `- \`find . -name "*.ts" | xargs grep watcher\` in bash → \`${searchName}({ query: "watcher invalidation" })\` (concepts work too)`,
35984
+ `- \`sed -n '100,160p' app.ts\` / \`cat app.ts\` in bash → \`${readName}({ filePath: "app.ts", startLine: 100, endLine: 160 })\``
35985
+ ].join(`
35986
+ `));
35827
35987
  }
35828
35988
  if (hasInspect) {
35829
35989
  sections.push("**Codebase health & diagnostics**: AFT does not surface compile/type errors automatically after edits — pull them with `aft_inspect`. Run it after a batch of edits and before you run tests or commit, when starting in unfamiliar code, or before a refactor/review. One call summarizes diagnostics (compile/type errors), TODOs, metrics, dead code, unused exports, and duplicates; pass `sections` for focused drill-down and `scope` to actively pull diagnostics for a specific file or directory. Its diagnostics are a fast checkpoint, not the authority — a clean `tsc` / `cargo check` / `pyright` run is the real gate. Treat `stale_categories` as a genuine stale-cache signal while an async Tier 2 refresh catches up.");
@@ -35841,13 +36001,22 @@ function buildWorkflowHints(opts) {
35841
36001
  `));
35842
36002
  }
35843
36003
  if (hasBgBash) {
35844
- sections.push(`**Long-running commands** (builds, installs, full test suites): \`${bashName}({ background: true })\` returns immediately with a \`task_id\`, then **end your turn** — a completion reminder arrives automatically when it finishes. Do not poll \`bash_status({ task_id })\`, and do not sync-wait with \`bash_watch\` for a long task: blocking freezes your turn and locks the user out until it ends. For an early non-blocking ping on a specific output line, register an async watch \`bash_watch({ task_id, pattern, background: true })\`. \`bash_watch\` synchronous mode is only for short bounded waits (seconds, e.g. a dev server printing a readiness line), never for multi-minute jobs.`);
36004
+ sections.push([
36005
+ `**Long-running commands** (builds, installs, full test suites): \`${bashName}({ background: true })\` returns a \`task_id\` immediately. Then do ONE of these:`,
36006
+ "1. Keep working on something independent of the result.",
36007
+ "2. End your turn — a completion reminder with the result arrives automatically.",
36008
+ `3. Need to react to a specific output line early? Register \`bash_watch({ task_id, pattern, background: true })\`, then end your turn — it pings you the moment the pattern appears.`,
36009
+ `\`bash_status({ task_id })\` is for inspecting output AFTER the reminder arrives, or one quick look at a live task — never call it repeatedly to wait: the reminder already delivers the result, and each poll wastes a turn. Sync \`bash_watch\` (without \`background: true\`) blocks your turn and locks the user out — reserve it for waits of a few seconds (a dev server printing its ready line), never for builds or test suites.`
36010
+ ].join(`
36011
+ `));
35845
36012
  sections.push(`**PTY / interactive commands**: PTY mode is for interactive REPLs and terminal apps (python, node, bash itself, vim). Start with \`${bashName}({ command: "python", pty: true, background: true })\`, read the screen with \`bash_status({ task_id, output_mode: "screen" })\`, and send input with \`bash_write({ task_id, input: "..." })\`.`);
35846
36013
  }
35847
36014
  if (sections.length === 0) {
35848
36015
  return null;
35849
36016
  }
35850
- sections.unshift("**Parallel tool calls**: when several read-only operations are independent, emit them in ONE response instead of serializing file reads, structure and symbol lookups, code search, diagnostics, and git status/diff/log. Sequence only when a call depends on a prior result or when a command mutates state.");
36017
+ sections.unshift(`You are equipped with a non-standard tool set: indexed code search, symbol-level reading, structural editing, and code analysis that are faster, more precise, and far cheaper in tokens than stitching together command-line utilities in bash. Always reach for these tools first.
36018
+
36019
+ **Parallel tool calls**: when several read-only operations are independent, emit them in ONE response instead of serializing — file reads, structure and symbol lookups, code search, diagnostics, and git status/diff/log. Sequence only when a call depends on a prior result or when a command mutates state.`);
35851
36020
  return `${HEADING}
35852
36021
 
35853
36022
  ${sections.join(`
@@ -35939,21 +36108,15 @@ var PLUGIN_VERSION = (() => {
35939
36108
  return "0.0.0";
35940
36109
  }
35941
36110
  })();
35942
- var ANNOUNCEMENT_VERSION = "0.36.0";
36111
+ var ANNOUNCEMENT_VERSION = "0.37.0";
35943
36112
  var ANNOUNCEMENT_FEATURES = [
35944
- "Persisted call graph: dead-code analysis runs on large repositories again (the file-count cap is gone), `aft_callgraph` queries resolve from disk, and method-call edges are more accurate across more languages.",
35945
- "Bash output stops hiding failures: piping a test/build through `grep`/`head` with compression on now keeps the failures and summary instead of stripping them.",
35946
- "Code search steering: guidance and hints now point to `aft_search` and parallel `aft_*` tools instead of `grep`/`find` in bash.",
35947
- "Fixes: a small `timeout` no longer kills a foreground `bash` command (#102), and `aft_delete`/`aft_safety checkpoint` no longer crash on a mistyped `files` argument."
36113
+ "Much lower background CPU: idle bridges shut down after 30 minutes, deleted project roots stop the watcher instead of spinning it, and background scans reuse warm caches on a bounded thread pool.",
36114
+ "`aft_transform` removed: usage data showed agents never called it `edit` covers everything it did, and every request now carries a smaller tool surface.",
36115
+ "Leaner tools: `bash`, `write`, `edit`, `apply_patch`, `aft_search`, and `aft_refactor` descriptions trimmed and config-aware; system-prompt guidance rewritten around what to do instead of what to avoid.",
36116
+ "Background bash reminders are right-sized: failures keep head + tail context, successes show a short tail, and compressors never print a success summary for a non-zero exit."
35948
36117
  ];
35949
36118
  var ANNOUNCEMENT_FOOTER = "Join us on Discord: https://discord.gg/DSa65w8wuf";
35950
- var ALL_ONLY_TOOLS = new Set([
35951
- "aft_callgraph",
35952
- "aft_delete",
35953
- "aft_move",
35954
- "aft_transform",
35955
- "aft_refactor"
35956
- ]);
36119
+ var ALL_ONLY_TOOLS = new Set(["aft_callgraph", "aft_delete", "aft_move", "aft_refactor"]);
35957
36120
  var pendingEagerWarnings = new Map;
35958
36121
  function isConfigureWarning(value) {
35959
36122
  if (!value || typeof value !== "object" || Array.isArray(value))
@@ -36031,7 +36194,6 @@ function resolveToolSurface(config2) {
36031
36194
  move: false,
36032
36195
  astSearch: false,
36033
36196
  astReplace: false,
36034
- structure: false,
36035
36197
  refactor: false
36036
36198
  };
36037
36199
  }
@@ -36054,7 +36216,6 @@ function resolveToolSurface(config2) {
36054
36216
  move: false,
36055
36217
  astSearch: ok("ast_grep_search"),
36056
36218
  astReplace: ok("ast_grep_replace"),
36057
- structure: false,
36058
36219
  refactor: false
36059
36220
  };
36060
36221
  if (surface === "all") {
@@ -36063,7 +36224,6 @@ function resolveToolSurface(config2) {
36063
36224
  navigate: allOnly("aft_callgraph"),
36064
36225
  delete: allOnly("aft_delete"),
36065
36226
  move: allOnly("aft_move"),
36066
- structure: allOnly("aft_transform"),
36067
36227
  refactor: allOnly("aft_refactor")
36068
36228
  };
36069
36229
  }
@@ -36269,7 +36429,7 @@ ${lines}
36269
36429
  }
36270
36430
  const surface = resolveToolSurface(config2);
36271
36431
  if (surface.hoistBash && resolveBashConfig(config2).enabled) {
36272
- registerBashTool(pi, ctx);
36432
+ registerBashTool(pi, ctx, surface.semantic);
36273
36433
  }
36274
36434
  registerHoistedTools(pi, ctx, surface);
36275
36435
  if (surface.outline || surface.zoom) {
@@ -36299,9 +36459,6 @@ ${lines}
36299
36459
  if (surface.delete || surface.move) {
36300
36460
  registerFsTools(pi, ctx, surface);
36301
36461
  }
36302
- if (surface.structure) {
36303
- registerStructureTool(pi, ctx);
36304
- }
36305
36462
  if (surface.refactor) {
36306
36463
  registerRefactorTool(pi, ctx);
36307
36464
  }
@@ -36328,6 +36485,9 @@ ${lines}
36328
36485
  runtime: pi
36329
36486
  });
36330
36487
  });
36488
+ pi.on("input", (_event, extCtx) => {
36489
+ signalSyncWatchAbort(resolveSessionId(extCtx));
36490
+ });
36331
36491
  const unregisterShutdownCleanup = registerShutdownCleanup(async () => {
36332
36492
  try {
36333
36493
  await Promise.allSettled([abortInFlightAutoInstalls(), abortInFlightGithubInstalls()]);