@cortexkit/aft-pi 0.36.0 → 0.37.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -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
+ }
12574
+ }
12575
+ if (this.stdoutReadOffset === this.stdoutBuffer.length) {
12576
+ this.stdoutBuffer = "";
12577
+ this.stdoutReadOffset = 0;
12382
12578
  }
12383
12579
  }
12580
+ compactStdoutBuffer() {
12581
+ if (this.stdoutReadOffset === 0)
12582
+ return;
12583
+ this.stdoutBuffer = this.stdoutBuffer.slice(this.stdoutReadOffset);
12584
+ this.stdoutReadOffset = 0;
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}`);
@@ -12857,6 +13078,11 @@ function formatEditSummary(data) {
12857
13078
  const n = data.files_modified;
12858
13079
  return `Applied edits to ${n} file${n === 1 ? "" : "s"}.`;
12859
13080
  }
13081
+ if (typeof data.total_files === "number") {
13082
+ const files = data.total_files;
13083
+ const reps = data.total_replacements ?? 0;
13084
+ return `Edited ${files} file${files === 1 ? "" : "s"} (${reps} replacement${reps === 1 ? "" : "s"}).`;
13085
+ }
12860
13086
  const additions = data.diff?.additions ?? 0;
12861
13087
  const deletions = data.diff?.deletions ?? 0;
12862
13088
  const counts = `+${additions}/-${deletions}`;
@@ -14071,7 +14297,23 @@ function isProcessAlive(pid) {
14071
14297
  }
14072
14298
  }
14073
14299
  // ../aft-bridge/dist/pipe-strip.js
14074
- var NOISE_FILTERS = new Set(["grep", "rg", "head", "tail", "cat", "less", "more"]);
14300
+ var NOISE_FILTERS = new Set([
14301
+ "grep",
14302
+ "rg",
14303
+ "head",
14304
+ "tail",
14305
+ "cat",
14306
+ "less",
14307
+ "more",
14308
+ "sed",
14309
+ "awk",
14310
+ "cut",
14311
+ "sort",
14312
+ "uniq",
14313
+ "tr",
14314
+ "column",
14315
+ "fold"
14316
+ ]);
14075
14317
  var GREP_GUARD_FLAGS = new Set([
14076
14318
  "c",
14077
14319
  "count",
@@ -14085,6 +14327,8 @@ var GREP_GUARD_FLAGS = new Set([
14085
14327
  function maybeStripCompressorPipe(command, compressionEnabled) {
14086
14328
  if (!compressionEnabled)
14087
14329
  return { command, stripped: false };
14330
+ if (containsUnsplittableConstruct(command))
14331
+ return { command, stripped: false };
14088
14332
  const chain = splitTopLevelAndChain(command);
14089
14333
  if (chain === null)
14090
14334
  return { command, stripped: false };
@@ -14098,7 +14342,7 @@ function maybeStripCompressorPipe(command, compressionEnabled) {
14098
14342
  return { command, stripped: false };
14099
14343
  const filterStages = stages.slice(1).map((stage) => stage.trim());
14100
14344
  for (const stage of filterStages) {
14101
- if (!isPlainNoiseFilter(stage))
14345
+ if (!filterStageIsSafeToDrop(stage))
14102
14346
  return { command, stripped: false };
14103
14347
  }
14104
14348
  const filters = filterStages.join(" | ");
@@ -14144,6 +14388,14 @@ function splitTopLevelAndChain(command) {
14144
14388
  return null;
14145
14389
  if (char === ";")
14146
14390
  return null;
14391
+ if (char === `
14392
+ ` || char === "\r")
14393
+ return null;
14394
+ if (char === "&") {
14395
+ const prev = command[index - 1];
14396
+ if (prev !== ">" && next !== ">")
14397
+ return null;
14398
+ }
14147
14399
  }
14148
14400
  segments.push(command.slice(start));
14149
14401
  return segments;
@@ -14186,45 +14438,262 @@ function isCompressorHandledRunner(stage) {
14186
14438
  const tokens = tokenizeStage(stage);
14187
14439
  if (tokens.length === 0)
14188
14440
  return false;
14189
- const [first, second, third] = tokens;
14190
- if (!first)
14191
- return false;
14192
14441
  if (tokens.some((token) => token === "&&" || token === "||" || token.includes(";"))) {
14193
14442
  return false;
14194
14443
  }
14444
+ const first = runnerName(tokens[0]);
14445
+ const second = tokens[1];
14446
+ const third = tokens[2];
14447
+ const rest = tokens.slice(1);
14448
+ if (!first)
14449
+ return false;
14195
14450
  if (first === "bun")
14196
14451
  return second === "test" || second === "run" && startsWithTest(third);
14197
- if (first === "cargo")
14198
- return ["test", "build", "check", "clippy"].includes(second ?? "");
14199
- if (first === "go")
14200
- return second === "test" || second === "build";
14201
- if (["npm", "pnpm"].includes(first)) {
14452
+ if (first === "npm" || first === "pnpm") {
14202
14453
  return second === "test" || second === "run" && startsWithTest(third);
14203
14454
  }
14204
- if (first === "yarn")
14205
- return second === "test";
14455
+ if (first === "yarn") {
14456
+ return startsWithTest(second) || second === "run" && startsWithTest(third);
14457
+ }
14458
+ if (first === "deno")
14459
+ return ["test", "lint", "check", "bench"].includes(second ?? "");
14460
+ if (first === "npx") {
14461
+ return ["tsc", "eslint", "vitest", "jest", "playwright", "biome"].includes(second ?? "");
14462
+ }
14206
14463
  if (first === "playwright")
14207
14464
  return second === "test";
14208
- if (first === "npx")
14209
- return ["tsc", "eslint", "vitest", "jest"].includes(second ?? "");
14210
- return ["vitest", "jest", "pytest", "tsc", "eslint", "biome", "ruff", "mypy"].includes(first);
14465
+ if (first === "cargo") {
14466
+ return ["test", "build", "check", "clippy", "nextest"].includes(second ?? "");
14467
+ }
14468
+ if (first === "go")
14469
+ return ["test", "build", "vet"].includes(second ?? "");
14470
+ if (first === "gradle" || first === "gradlew") {
14471
+ return hasBuildTask(rest, ["test", "check", "build", "assemble", "clean"]);
14472
+ }
14473
+ if (first === "mvn" || first === "mvnw") {
14474
+ return hasBuildTask(rest, ["test", "verify", "package", "install"]);
14475
+ }
14476
+ if (first === "dotnet")
14477
+ return ["test", "build"].includes(second ?? "");
14478
+ if (first === "rspec")
14479
+ return true;
14480
+ if (first === "rake")
14481
+ return second === "test" || second === "spec";
14482
+ if (first === "phpunit" || first === "pest")
14483
+ return true;
14484
+ if (first === "xcodebuild")
14485
+ return xcodebuildHasBuildAction(rest);
14486
+ if (first === "swift")
14487
+ return second === "test" || second === "build";
14488
+ if (first === "make" || first === "gmake") {
14489
+ return hasBuildTask(rest, ["test", "check", "lint", "clean"]);
14490
+ }
14491
+ return [
14492
+ "vitest",
14493
+ "jest",
14494
+ "pytest",
14495
+ "tsc",
14496
+ "eslint",
14497
+ "biome",
14498
+ "ruff",
14499
+ "mypy",
14500
+ "tox",
14501
+ "nox"
14502
+ ].includes(first);
14503
+ }
14504
+ function runnerName(token) {
14505
+ if (!token)
14506
+ return "";
14507
+ const slash = token.lastIndexOf("/");
14508
+ return slash === -1 ? token : token.slice(slash + 1);
14509
+ }
14510
+ function hasBuildTask(args, tasks) {
14511
+ const isAllowedTask = (arg) => tasks.some((task) => arg === task || arg.endsWith(`:${task}`));
14512
+ const isFlagOrProperty = (arg) => arg.startsWith("-") || arg.includes("=");
14513
+ let sawAllowed = false;
14514
+ for (const arg of args) {
14515
+ if (isFlagOrProperty(arg))
14516
+ continue;
14517
+ if (!isAllowedTask(arg))
14518
+ return false;
14519
+ sawAllowed = true;
14520
+ }
14521
+ return sawAllowed;
14211
14522
  }
14212
14523
  function startsWithTest(token) {
14213
14524
  return token?.startsWith("test") === true;
14214
14525
  }
14215
- function isPlainNoiseFilter(stage) {
14216
- const tokens = tokenizeStage(stage);
14217
- const head = tokens[0];
14526
+ var XCODEBUILD_VALUE_FLAGS = new Set([
14527
+ "-scheme",
14528
+ "-target",
14529
+ "-project",
14530
+ "-workspace",
14531
+ "-configuration",
14532
+ "-sdk",
14533
+ "-destination",
14534
+ "-arch",
14535
+ "-derivedDataPath",
14536
+ "-resultBundlePath",
14537
+ "-xcconfig",
14538
+ "-toolchain"
14539
+ ]);
14540
+ var XCODEBUILD_BUILD_ACTIONS = new Set([
14541
+ "build",
14542
+ "test",
14543
+ "build-for-testing",
14544
+ "test-without-building",
14545
+ "analyze"
14546
+ ]);
14547
+ function xcodebuildHasBuildAction(args) {
14548
+ for (let i = 0;i < args.length; i++) {
14549
+ const arg = args[i];
14550
+ if (arg.startsWith("-")) {
14551
+ if (XCODEBUILD_VALUE_FLAGS.has(arg))
14552
+ i++;
14553
+ continue;
14554
+ }
14555
+ if (XCODEBUILD_BUILD_ACTIONS.has(arg))
14556
+ return true;
14557
+ }
14558
+ return false;
14559
+ }
14560
+ function containsUnsplittableConstruct(command) {
14561
+ let quote = null;
14562
+ let escaped = false;
14563
+ for (let i = 0;i < command.length; i++) {
14564
+ const char = command[i];
14565
+ if (escaped) {
14566
+ escaped = false;
14567
+ continue;
14568
+ }
14569
+ if (char === "\\" && quote !== "'") {
14570
+ escaped = true;
14571
+ continue;
14572
+ }
14573
+ if (quote === "'") {
14574
+ if (char === "'")
14575
+ quote = null;
14576
+ continue;
14577
+ }
14578
+ if (quote === '"') {
14579
+ if (char === '"')
14580
+ quote = null;
14581
+ else if (char === "`")
14582
+ return true;
14583
+ else if (char === "$" && command[i + 1] === "(")
14584
+ return true;
14585
+ continue;
14586
+ }
14587
+ if (char === "'" || char === '"') {
14588
+ quote = char;
14589
+ continue;
14590
+ }
14591
+ if (char === "`")
14592
+ return true;
14593
+ if (char === "(" || char === ")")
14594
+ return true;
14595
+ }
14596
+ return false;
14597
+ }
14598
+ var READS_FILE_OPERAND = new Set(["cat", "tac", "nl", "less", "more"]);
14599
+ function filterStageIsSafeToDrop(stage) {
14600
+ const head = tokenizeStage(stage)[0];
14218
14601
  if (!head)
14219
14602
  return false;
14220
14603
  if (head === "wc")
14221
14604
  return false;
14222
14605
  if (!NOISE_FILTERS.has(head))
14223
14606
  return false;
14224
- if ((head === "grep" || head === "rg") && hasIntentChangingGrepFlag(tokens.slice(1)))
14607
+ if (/[<>]/.test(stage))
14608
+ return false;
14609
+ if (hasUnquotedBackground(stage))
14610
+ return false;
14611
+ const args = tokenizeStage(stage).slice(1);
14612
+ const hasFlag = (...names) => args.some((a) => names.some((n) => a === n || a.startsWith(`${n}=`)));
14613
+ if (head === "grep" || head === "rg") {
14614
+ if (hasIntentChangingGrepFlag(args))
14615
+ return false;
14616
+ if (countBareOperands(args) > 1)
14617
+ return false;
14618
+ return true;
14619
+ }
14620
+ if (head === "head" || head === "tail") {
14621
+ if (bareOperands(args).some((op) => !/^\d+$/.test(op)))
14622
+ return false;
14623
+ return true;
14624
+ }
14625
+ if (READS_FILE_OPERAND.has(head)) {
14626
+ if (countBareOperands(args) > 0)
14627
+ return false;
14628
+ return true;
14629
+ }
14630
+ if (head === "sed") {
14631
+ if (hasFlag("-i", "--in-place"))
14632
+ return false;
14633
+ if (countBareOperands(args) > 1)
14634
+ return false;
14635
+ return true;
14636
+ }
14637
+ if (head === "awk") {
14638
+ if (countBareOperands(args) > 1)
14639
+ return false;
14640
+ return true;
14641
+ }
14642
+ if (head === "sort" && hasFlag("-o", "--output"))
14643
+ return false;
14644
+ if (bareOperands(args).some((op) => op.includes("/") || op.includes(".")))
14225
14645
  return false;
14226
14646
  return true;
14227
14647
  }
14648
+ function bareOperands(args) {
14649
+ const out = [];
14650
+ let afterDoubleDash = false;
14651
+ for (const arg of args) {
14652
+ if (!afterDoubleDash && arg === "--") {
14653
+ afterDoubleDash = true;
14654
+ continue;
14655
+ }
14656
+ if (!afterDoubleDash && arg.startsWith("-") && arg !== "-")
14657
+ continue;
14658
+ out.push(arg);
14659
+ }
14660
+ return out;
14661
+ }
14662
+ function countBareOperands(args) {
14663
+ return bareOperands(args).length;
14664
+ }
14665
+ function hasUnquotedBackground(stage) {
14666
+ let quote = null;
14667
+ let escaped = false;
14668
+ for (let i = 0;i < stage.length; i++) {
14669
+ const char = stage[i];
14670
+ if (escaped) {
14671
+ escaped = false;
14672
+ continue;
14673
+ }
14674
+ if (char === "\\" && quote !== "'") {
14675
+ escaped = true;
14676
+ continue;
14677
+ }
14678
+ if (quote) {
14679
+ if (char === quote)
14680
+ quote = null;
14681
+ continue;
14682
+ }
14683
+ if (char === "'" || char === '"') {
14684
+ quote = char;
14685
+ continue;
14686
+ }
14687
+ if (char === "&") {
14688
+ const prev = stage[i - 1];
14689
+ const next = stage[i + 1];
14690
+ if (prev === "&" || next === "&" || prev === ">" || next === ">")
14691
+ continue;
14692
+ return true;
14693
+ }
14694
+ }
14695
+ return false;
14696
+ }
14228
14697
  function hasIntentChangingGrepFlag(args) {
14229
14698
  for (const arg of args) {
14230
14699
  if (arg === "--")
@@ -14288,7 +14757,7 @@ function tokenizeStage(stage) {
14288
14757
  // ../aft-bridge/dist/pool.js
14289
14758
  import { realpathSync as realpathSync2 } from "node:fs";
14290
14759
  import { homedir as homedir6 } from "node:os";
14291
- var DEFAULT_IDLE_TIMEOUT_MS = Infinity;
14760
+ var DEFAULT_IDLE_TIMEOUT_MS = 30 * 60 * 1000;
14292
14761
  var DEFAULT_MAX_POOL_SIZE = 8;
14293
14762
  var CLEANUP_INTERVAL_MS = 60 * 1000;
14294
14763
  function canonicalHomeDir() {
@@ -14383,7 +14852,7 @@ class BridgePool {
14383
14852
  cleanup() {
14384
14853
  const now = Date.now();
14385
14854
  for (const [dir, entry] of this.bridges) {
14386
- if (entry.bridge.hasPendingRequests())
14855
+ if (entry.bridge.hasPendingRequests() || entry.bridge.hasOutstandingBackgroundTasks())
14387
14856
  continue;
14388
14857
  if (now - entry.lastUsed > this.idleTimeoutMs) {
14389
14858
  entry.bridge.shutdown().catch((err) => this.error("cleanup shutdown failed:", err));
@@ -14391,7 +14860,7 @@ class BridgePool {
14391
14860
  }
14392
14861
  }
14393
14862
  for (const bridge of this.staleBridges) {
14394
- if (bridge.hasPendingRequests())
14863
+ if (bridge.hasPendingRequests() || bridge.hasOutstandingBackgroundTasks())
14395
14864
  continue;
14396
14865
  bridge.shutdown().catch((err) => this.error("stale cleanup shutdown failed:", err));
14397
14866
  this.staleBridges.delete(bridge);
@@ -14401,7 +14870,7 @@ class BridgePool {
14401
14870
  let oldestDir = null;
14402
14871
  let oldestTime = Infinity;
14403
14872
  for (const [dir, entry] of this.bridges) {
14404
- if (entry.bridge.hasPendingRequests())
14873
+ if (entry.bridge.hasPendingRequests() || entry.bridge.hasOutstandingBackgroundTasks())
14405
14874
  continue;
14406
14875
  if (entry.lastUsed < oldestTime) {
14407
14876
  oldestTime = entry.lastUsed;
@@ -15207,7 +15676,7 @@ import {
15207
15676
  // package.json
15208
15677
  var package_default = {
15209
15678
  name: "@cortexkit/aft-pi",
15210
- version: "0.36.0",
15679
+ version: "0.37.0",
15211
15680
  type: "module",
15212
15681
  description: "Pi coding agent extension for Agent File Tools (AFT) — tree-sitter and LSP-powered code analysis",
15213
15682
  main: "dist/index.js",
@@ -15230,7 +15699,7 @@ var package_default = {
15230
15699
  "test:unit": "bun test src/__tests__ --path-ignore-patterns 'src/__tests__/e2e/**'"
15231
15700
  },
15232
15701
  dependencies: {
15233
- "@cortexkit/aft-bridge": "0.36.0",
15702
+ "@cortexkit/aft-bridge": "0.37.0",
15234
15703
  "@xterm/headless": "^5.5.0",
15235
15704
  "comment-json": "^5.0.0",
15236
15705
  diff: "^8.0.4",
@@ -15238,12 +15707,12 @@ var package_default = {
15238
15707
  zod: "^4.1.8"
15239
15708
  },
15240
15709
  optionalDependencies: {
15241
- "@cortexkit/aft-darwin-arm64": "0.36.0",
15242
- "@cortexkit/aft-darwin-x64": "0.36.0",
15243
- "@cortexkit/aft-linux-arm64": "0.36.0",
15244
- "@cortexkit/aft-linux-x64": "0.36.0",
15245
- "@cortexkit/aft-win32-arm64": "0.36.0",
15246
- "@cortexkit/aft-win32-x64": "0.36.0"
15710
+ "@cortexkit/aft-darwin-arm64": "0.37.0",
15711
+ "@cortexkit/aft-darwin-x64": "0.37.0",
15712
+ "@cortexkit/aft-linux-arm64": "0.37.0",
15713
+ "@cortexkit/aft-linux-x64": "0.37.0",
15714
+ "@cortexkit/aft-win32-arm64": "0.37.0",
15715
+ "@cortexkit/aft-win32-x64": "0.37.0"
15247
15716
  },
15248
15717
  devDependencies: {
15249
15718
  "@earendil-works/pi-coding-agent": "*",
@@ -15501,19 +15970,6 @@ function isEmptyParam(value) {
15501
15970
  return Object.keys(value).length === 0;
15502
15971
  return false;
15503
15972
  }
15504
- var LONG_RUNNING_COMMAND_TIMEOUT_MS = {
15505
- callers: 60000,
15506
- trace_to: 60000,
15507
- trace_to_symbol: 60000,
15508
- trace_data: 60000,
15509
- impact: 60000,
15510
- grep: 60000,
15511
- glob: 60000,
15512
- semantic_search: 60000
15513
- };
15514
- function timeoutForCommand(command) {
15515
- return LONG_RUNNING_COMMAND_TIMEOUT_MS[command];
15516
- }
15517
15973
  function asPlainObject(value) {
15518
15974
  if (!value || typeof value !== "object" || Array.isArray(value))
15519
15975
  return;
@@ -31881,7 +32337,7 @@ function sendFeatureAnnouncement(version2, features, footer, storageDir) {
31881
32337
  // src/status-bar-inject.ts
31882
32338
  var DEFAULT_SESSION = "__default__";
31883
32339
  var emitStateBySession = new Map;
31884
- function statusBarBlockForSession(sessionID, counts) {
32340
+ function statusBarBlockForSession(sessionID, counts, force = false) {
31885
32341
  if (!counts)
31886
32342
  return;
31887
32343
  const key = sessionID ?? DEFAULT_SESSION;
@@ -31890,6 +32346,11 @@ function statusBarBlockForSession(sessionID, counts) {
31890
32346
  state = createStatusBarEmitState();
31891
32347
  emitStateBySession.set(key, state);
31892
32348
  }
32349
+ if (force) {
32350
+ state.last = counts;
32351
+ state.callsSinceEmit = 0;
32352
+ return formatStatusBar(counts);
32353
+ }
31893
32354
  return shouldEmitStatusBar(state, counts) ? formatStatusBar(counts) : undefined;
31894
32355
  }
31895
32356
 
@@ -32146,7 +32607,6 @@ function formatDiffForPi(oldContent, newContent, contextLines = DEFAULT_CONTEXT_
32146
32607
  }
32147
32608
 
32148
32609
  // src/tools/hoisted.ts
32149
- 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.";
32150
32610
  function diagnosticsOnEditDefault(ctx) {
32151
32611
  return ctx.config.lsp?.diagnostics_on_edit ?? false;
32152
32612
  }
@@ -32216,8 +32676,7 @@ var WriteParams = Type2.Object({
32216
32676
  filePath: Type2.String({
32217
32677
  description: "Path to the file to write (absolute or relative to project root)"
32218
32678
  }),
32219
- content: Type2.String({ description: "Full file contents to write" }),
32220
- diagnostics: Type2.Optional(Type2.Boolean({ description: DIAGNOSTICS_PARAM_DESCRIPTION }))
32679
+ content: Type2.String({ description: "Full file contents to write" })
32221
32680
  });
32222
32681
  var EditParams = Type2.Object({
32223
32682
  filePath: Type2.String({
@@ -32229,8 +32688,7 @@ var EditParams = Type2.Object({
32229
32688
  occurrence: optionalInt(0, Number.MAX_SAFE_INTEGER),
32230
32689
  appendContent: Type2.Optional(Type2.String({
32231
32690
  description: "Append text to the end of the file (creates the file if missing, parent dirs auto-created). When set, oldString/newString are ignored."
32232
- })),
32233
- diagnostics: Type2.Optional(Type2.Boolean({ description: DIAGNOSTICS_PARAM_DESCRIPTION }))
32691
+ }))
32234
32692
  });
32235
32693
  var GrepParams = Type2.Object({
32236
32694
  pattern: Type2.String({ description: "Regex pattern to search for" }),
@@ -32238,8 +32696,7 @@ var GrepParams = Type2.Object({
32238
32696
  description: "Path scope (file or directory; absolute or relative to project root)"
32239
32697
  })),
32240
32698
  include: Type2.Optional(Type2.String({ description: "Glob filter for included files (e.g. '*.ts,*.tsx')" })),
32241
- caseSensitive: Type2.Optional(Type2.Boolean({ description: "Case-sensitive matching" })),
32242
- contextLines: optionalInt(1, Number.MAX_SAFE_INTEGER)
32699
+ caseSensitive: Type2.Optional(Type2.Boolean({ description: "Case-sensitive matching" }))
32243
32700
  });
32244
32701
  function registerHoistedTools(pi, ctx, surface) {
32245
32702
  if (surface.hoistRead) {
@@ -32254,7 +32711,9 @@ function registerHoistedTools(pi, ctx, surface) {
32254
32711
  const bridge = bridgeFor(ctx, extCtx.cwd);
32255
32712
  const offset = coerceOptionalInt(params.offset, "offset", 1, Number.MAX_SAFE_INTEGER);
32256
32713
  const limit = coerceOptionalInt(params.limit, "limit", 1, Number.MAX_SAFE_INTEGER);
32257
- const req = { file: params.path };
32714
+ const req = {
32715
+ file: await resolvePathArg(extCtx.cwd, params.path)
32716
+ };
32258
32717
  if (offset !== undefined) {
32259
32718
  req.start_line = offset;
32260
32719
  if (limit !== undefined) {
@@ -32281,19 +32740,20 @@ function registerHoistedTools(pi, ctx, surface) {
32281
32740
  pi.registerTool({
32282
32741
  name: "write",
32283
32742
  label: "write",
32284
- 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.",
32285
- promptSnippet: "Create or overwrite files (uses filePath; auto-formats; diagnostics follow lsp.diagnostics_on_edit unless overridden)",
32743
+ 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.",
32744
+ promptSnippet: "Create or overwrite files (uses filePath; auto-formats)",
32286
32745
  promptGuidelines: ["Use write only for new files or complete rewrites."],
32287
32746
  parameters: WriteParams,
32288
32747
  async execute(_toolCallId, params, _signal, _onUpdate, extCtx) {
32289
- await assertExternalDirectoryPermission(extCtx, params.filePath, "modify", {
32748
+ const filePath = await resolvePathArg(extCtx.cwd, params.filePath);
32749
+ await assertExternalDirectoryPermission(extCtx, filePath, "modify", {
32290
32750
  restrictToProjectRoot: surface.restrictToProjectRoot
32291
32751
  });
32292
32752
  const bridge = bridgeFor(ctx, extCtx.cwd);
32293
32753
  const response = await callBridge(bridge, "write", {
32294
- file: params.filePath,
32754
+ file: filePath,
32295
32755
  content: params.content,
32296
- diagnostics: params.diagnostics ?? diagnosticsOnEditDefault(ctx),
32756
+ diagnostics: diagnosticsOnEditDefault(ctx),
32297
32757
  include_diff_content: true
32298
32758
  }, extCtx);
32299
32759
  return buildMutationResult(response);
@@ -32310,8 +32770,8 @@ function registerHoistedTools(pi, ctx, surface) {
32310
32770
  pi.registerTool({
32311
32771
  name: "edit",
32312
32772
  label: "edit",
32313
- 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.",
32314
- 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.",
32773
+ 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`.",
32774
+ 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).",
32315
32775
  promptGuidelines: [
32316
32776
  "Prefer edit over write when changing part of an existing file.",
32317
32777
  "Include enough surrounding context in oldString to make the match unique, or set replaceAll/occurrence explicitly.",
@@ -32319,26 +32779,27 @@ function registerHoistedTools(pi, ctx, surface) {
32319
32779
  ],
32320
32780
  parameters: EditParams,
32321
32781
  async execute(_toolCallId, params, _signal, _onUpdate, extCtx) {
32322
- await assertExternalDirectoryPermission(extCtx, params.filePath, "modify", {
32782
+ const filePath = await resolvePathArg(extCtx.cwd, params.filePath);
32783
+ await assertExternalDirectoryPermission(extCtx, filePath, "modify", {
32323
32784
  restrictToProjectRoot: surface.restrictToProjectRoot
32324
32785
  });
32325
32786
  const bridge = bridgeFor(ctx, extCtx.cwd);
32326
32787
  if (typeof params.appendContent === "string") {
32327
32788
  const req2 = {
32328
32789
  op: "append",
32329
- file: params.filePath,
32790
+ file: filePath,
32330
32791
  append_content: params.appendContent,
32331
- diagnostics: params.diagnostics ?? diagnosticsOnEditDefault(ctx),
32792
+ diagnostics: diagnosticsOnEditDefault(ctx),
32332
32793
  include_diff_content: true
32333
32794
  };
32334
32795
  const response2 = await callBridge(bridge, "edit_match", req2, extCtx);
32335
32796
  return buildMutationResult(response2);
32336
32797
  }
32337
32798
  const req = {
32338
- file: params.filePath,
32799
+ file: filePath,
32339
32800
  match: params.oldString ?? "",
32340
32801
  replacement: params.newString ?? "",
32341
- diagnostics: params.diagnostics ?? diagnosticsOnEditDefault(ctx),
32802
+ diagnostics: diagnosticsOnEditDefault(ctx),
32342
32803
  include_diff_content: true
32343
32804
  };
32344
32805
  if (params.replaceAll === true)
@@ -32378,9 +32839,6 @@ function registerHoistedTools(pi, ctx, surface) {
32378
32839
  req.include = splitIncludeGlobs(params.include);
32379
32840
  if (params.caseSensitive !== undefined)
32380
32841
  req.case_sensitive = params.caseSensitive;
32381
- const contextLines = coerceOptionalInt(params.contextLines, "contextLines", 1, Number.MAX_SAFE_INTEGER);
32382
- if (contextLines !== undefined)
32383
- req.context_lines = contextLines;
32384
32842
  const response = await callBridge(bridge, "grep", req, extCtx);
32385
32843
  const text = response.text ?? "";
32386
32844
  return textResult(text);
@@ -32941,7 +33399,7 @@ function registerAstTools(pi, ctx, surface) {
32941
33399
  if (!isEmptyParam(params.globs))
32942
33400
  req.globs = params.globs;
32943
33401
  if (params.contextLines !== undefined)
32944
- req.context_lines = params.contextLines;
33402
+ req.context = params.contextLines;
32945
33403
  const response = await callBridge(bridge, "ast_search", req, extCtx);
32946
33404
  return textResult(response.text ?? JSON.stringify(response));
32947
33405
  },
@@ -33095,22 +33553,28 @@ function getBashSpawnHook(pi) {
33095
33553
  }
33096
33554
  return api2.hooks?.bashSpawn;
33097
33555
  }
33098
- function registerBashTool(pi, ctx) {
33556
+ function registerBashTool(pi, ctx, aftSearchRegistered = false) {
33099
33557
  const spawnHook = getBashSpawnHook(pi);
33558
+ 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";
33559
+ const bashCfg = resolveBashConfig(ctx.config);
33560
+ const compressionSentence = bashCfg.compress ? " Output is compressed by default; pass `compressed: false` for raw output." : "";
33561
+ 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`.";
33100
33562
  pi.registerTool({
33101
33563
  name: "bash",
33102
33564
  label: "bash",
33103
- 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`.',
33565
+ description: `Execute shell commands.${compressionSentence}${tasksSentence}
33566
+
33567
+ 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}.`,
33104
33568
  promptSnippet: "Run shell commands (timeout in milliseconds; supports workdir, background tasks, compressed output, PTY mode)",
33105
33569
  promptGuidelines: [
33106
- "Use bash only when a dedicated AFT tool is not a better fit.",
33570
+ `DO NOT use bash for code search or exploration ${searchSteer}.`,
33107
33571
  "Set compressed: false when you need ANSI color codes in the output."
33108
33572
  ],
33109
33573
  parameters: BashParams,
33110
33574
  async execute(_toolCallId, params, _signal, onUpdate, extCtx) {
33111
33575
  const bridge = bridgeFor(ctx, extCtx.cwd);
33112
- const bashCfg = resolveBashConfig(ctx.config);
33113
- const foregroundWaitMs = resolveForegroundWaitMs(bashCfg.foreground_wait_window_ms);
33576
+ const bashCfg2 = resolveBashConfig(ctx.config);
33577
+ const foregroundWaitMs = resolveForegroundWaitMs(bashCfg2.foreground_wait_window_ms);
33114
33578
  const timeout = coerceOptionalInt(params.timeout, "timeout", 1, Number.MAX_SAFE_INTEGER);
33115
33579
  const ptyRows = coerceOptionalInt(params.ptyRows, "ptyRows", 1, 60);
33116
33580
  const ptyCols = coerceOptionalInt(params.ptyCols, "ptyCols", 1, 140);
@@ -33127,7 +33591,7 @@ function registerBashTool(pi, ctx) {
33127
33591
  throw new Error(`BashSpawnHook failed: ${hookErr instanceof Error ? hookErr.message : String(hookErr)}`);
33128
33592
  }
33129
33593
  }
33130
- const compressionEnabled = bashCfg.compress && params.compressed !== false;
33594
+ const compressionEnabled = bashCfg2.compress && params.compressed !== false;
33131
33595
  const pipeStrip = maybeStripCompressorPipe(spawnContext.command, compressionEnabled);
33132
33596
  const bridgeCommand = pipeStrip.command;
33133
33597
  let streamed = "";
@@ -33162,9 +33626,7 @@ function registerBashTool(pi, ctx) {
33162
33626
  if (response.status === "running" && taskId) {
33163
33627
  if (effectiveBackground) {
33164
33628
  trackBgTask(resolveSessionId(extCtx), taskId);
33165
- return bashResult(formatBackgroundLaunch(taskId, params.pty === true), {
33166
- task_id: taskId
33167
- });
33629
+ return bashResult(appendPipeStripNote(formatBackgroundLaunch(taskId, params.pty === true), pipeStrip.note), { task_id: taskId });
33168
33630
  }
33169
33631
  const waitTimeoutMs = effectiveTimeout !== undefined ? Math.min(effectiveTimeout, foregroundWaitMs) : foregroundWaitMs;
33170
33632
  const startedAt = Date.now();
@@ -33174,7 +33636,7 @@ function registerBashTool(pi, ctx) {
33174
33636
  throw new Error(status.message ?? "bash_status failed");
33175
33637
  }
33176
33638
  if (isTerminalStatus(status.status)) {
33177
- return bashResult(appendPipeStripNote(withBashHints(formatForegroundResult(status)), pipeStrip.note), {
33639
+ return bashResult(appendPipeStripNote(withBashHints(formatForegroundResult(status), bridgeCommand, aftSearchRegistered), pipeStrip.note), {
33178
33640
  exit_code: status.exit_code,
33179
33641
  duration_ms: status.duration_ms,
33180
33642
  truncated: status.output_truncated,
@@ -33188,9 +33650,7 @@ function registerBashTool(pi, ctx) {
33188
33650
  throw new Error(promoted.message ?? "bash_promote failed");
33189
33651
  }
33190
33652
  trackBgTask(resolveSessionId(extCtx), taskId);
33191
- return bashResult(formatPromotionMessage(taskId, effectiveTimeout, foregroundWaitMs), {
33192
- task_id: taskId
33193
- });
33653
+ return bashResult(appendPipeStripNote(formatPromotionMessage(taskId, effectiveTimeout, foregroundWaitMs), pipeStrip.note), { task_id: taskId });
33194
33654
  }
33195
33655
  await sleep(FOREGROUND_POLL_INTERVAL_MS);
33196
33656
  }
@@ -33203,7 +33663,7 @@ function registerBashTool(pi, ctx) {
33203
33663
  task_id: taskId
33204
33664
  };
33205
33665
  const output = response.output ?? "";
33206
- return bashResult(appendPipeStripNote(withBashHints(output), pipeStrip.note), details);
33666
+ return bashResult(appendPipeStripNote(withBashHints(output, bridgeCommand, aftSearchRegistered), pipeStrip.note), details);
33207
33667
  },
33208
33668
  renderCall(args, theme, context) {
33209
33669
  return renderBashCall(args?.command, args?.description, theme, context);
@@ -33235,8 +33695,8 @@ function formatPromotionMessage(taskId, timeout, waitWindowMs) {
33235
33695
  function formatSeconds(ms) {
33236
33696
  return `${Number((ms / 1000).toFixed(1))}s`;
33237
33697
  }
33238
- function withBashHints(output) {
33239
- return maybeAppendConflictsHint(output);
33698
+ function withBashHints(output, command, aftSearchRegistered) {
33699
+ return maybeAppendGrepSearchHint(maybeAppendConflictsHint(output), command, aftSearchRegistered);
33240
33700
  }
33241
33701
  function formatForegroundResult(data) {
33242
33702
  const output = data.output_preview ?? "";
@@ -33704,8 +34164,12 @@ function registerConflictsTool(pi, ctx) {
33704
34164
  const bridge = bridgeFor(ctx, extCtx.cwd);
33705
34165
  const reqParams = {};
33706
34166
  const path2 = params?.path;
33707
- if (typeof path2 === "string" && path2.trim() !== "")
33708
- reqParams.path = path2;
34167
+ if (typeof path2 === "string" && path2.trim() !== "") {
34168
+ await assertExternalDirectoryPermission(extCtx, path2, "inspect", {
34169
+ restrictToProjectRoot: ctx.config.restrict_to_project_root ?? false
34170
+ });
34171
+ reqParams.path = await resolvePathArg(extCtx.cwd, path2);
34172
+ }
33709
34173
  const response = await callBridge(bridge, "git_conflicts", reqParams, extCtx);
33710
34174
  return textResult(response.text ?? JSON.stringify(response, null, 2));
33711
34175
  },
@@ -34251,7 +34715,15 @@ function registerInspectTool(pi, ctx) {
34251
34715
  const topK = validateOptionalTopK(params.topK);
34252
34716
  const response = await callBridge(bridge, "inspect", { sections, scope, topK }, extCtx);
34253
34717
  runPendingTier2Categories(bridge, tier2RefreshCategories(response), extCtx);
34254
- return textResult(response.text ?? JSON.stringify(response, null, 2), response);
34718
+ const body = response.text;
34719
+ if (typeof body === "string") {
34720
+ const diagnostics = diagnosticsSummaryPart(asRecord2(response.summary));
34721
+ const text = diagnostics ? body ? `${body}
34722
+
34723
+ ${diagnostics}` : diagnostics : body;
34724
+ return textResult(text, response);
34725
+ }
34726
+ return textResult(JSON.stringify(response, null, 2), response);
34255
34727
  },
34256
34728
  renderCall(args, theme, context) {
34257
34729
  return renderInspectCall(args, theme, context);
@@ -34990,7 +35462,7 @@ function registerRefactorTool(pi, ctx) {
34990
35462
  pi.registerTool({
34991
35463
  name: "aft_refactor",
34992
35464
  label: "refactor",
34993
- 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.",
35465
+ 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.",
34994
35466
  parameters: RefactorParams,
34995
35467
  async execute(_toolCallId, params, _signal, _onUpdate, extCtx) {
34996
35468
  const commandMap = {
@@ -35369,21 +35841,9 @@ function registerSemanticTool(pi, ctx) {
35369
35841
  name: "aft_search",
35370
35842
  label: "search",
35371
35843
  description: [
35372
- "Find code with unified semantic, lexical, literal, and regex search. Returns ranked symbol/file results or exact matching lines, with routing metadata.",
35373
- "",
35374
- "When to reach for it:",
35375
- "- Exploring an unfamiliar area: 'where is rate limiting handled', 'how does auth flow work'",
35376
- "- Concept doesn't appear as a literal string: 'retry logic', 'cache invalidation', 'graceful shutdown'",
35377
- "- Filename-shaped concepts: 'the bridge spawn helper', 'the session detection module'",
35378
- "- Regex-shaped or exact text queries when you want AFT to classify and route automatically",
35379
- "- You know roughly what the function does but not what it's named",
35844
+ "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').",
35380
35845
  "",
35381
- "When NOT to use:",
35382
- "- You need exhaustive literal enumeration → use grep directly",
35383
- "- You want the file/module structure → use aft_outline",
35384
- "- You're following a call chain → use aft_callgraph",
35385
- "",
35386
- "Set hint to 'regex', 'literal', 'semantic', or 'auto' to override or document routing intent."
35846
+ "Set hint to 'regex', 'literal', or 'semantic' to force a lane."
35387
35847
  ].join(`
35388
35848
  `),
35389
35849
  parameters: SearchParams2,
@@ -35414,138 +35874,8 @@ ${extra}`;
35414
35874
  });
35415
35875
  }
35416
35876
 
35417
- // src/tools/structure.ts
35418
- import { StringEnum as StringEnum6 } from "@earendil-works/pi-ai";
35419
- import { Type as Type14 } from "typebox";
35420
- var TransformParams = Type14.Object({
35421
- op: StringEnum6(["add_member", "add_derive", "wrap_try_catch", "add_decorator", "add_struct_tags"], { description: "Transformation operation" }),
35422
- filePath: Type14.String({
35423
- description: "Path to the source file (absolute or relative to project root)"
35424
- }),
35425
- container: Type14.Optional(Type14.String({ description: "Class/struct/impl name for add_member" })),
35426
- code: Type14.Optional(Type14.String({ description: "Member code to insert (add_member)" })),
35427
- target: Type14.Optional(Type14.String({ description: "Target symbol name" })),
35428
- derives: Type14.Optional(Type14.Array(Type14.String(), { description: "Derive macro names (add_derive)" })),
35429
- catchBody: Type14.Optional(Type14.String({ description: "Catch block body (wrap_try_catch, default: 'throw error;')" })),
35430
- decorator: Type14.Optional(Type14.String({ description: "Decorator text without @ (add_decorator)" })),
35431
- field: Type14.Optional(Type14.String({ description: "Struct field name (add_struct_tags)" })),
35432
- tag: Type14.Optional(Type14.String({ description: "Tag key (add_struct_tags)" })),
35433
- value: Type14.Optional(Type14.String({ description: "Tag value (add_struct_tags)" })),
35434
- position: Type14.Optional(Type14.String({
35435
- description: "Position hint: 'first', 'last' (default), 'before:name', 'after:name' for add_member"
35436
- })),
35437
- validate: Type14.Optional(StringEnum6(["syntax", "full"], {
35438
- description: "Validation level (default: syntax)"
35439
- }))
35440
- });
35441
- function buildTransformSections(args, payload, theme) {
35442
- const response = asRecord2(payload);
35443
- if (!response)
35444
- return [theme.fg("muted", "No transform result.")];
35445
- const target = asString(response.target) ?? asString(response.scope) ?? args.target ?? args.container ?? args.field ?? args.filePath;
35446
- return [
35447
- `${theme.fg("success", "transformed")} ${theme.fg("accent", args.op)}`,
35448
- `${theme.fg("muted", "file")} ${theme.fg("accent", asString(response.file) ?? args.filePath)}`,
35449
- target ? `${theme.fg("muted", "target")} ${target}` : theme.fg("muted", "No target metadata.")
35450
- ];
35451
- }
35452
- function renderTransformCall(args, theme, context) {
35453
- const target = args.target ?? args.container ?? args.field;
35454
- const summary = [
35455
- theme.fg("accent", args.op),
35456
- accentPath(theme, args.filePath),
35457
- target ? theme.fg("toolOutput", target) : undefined
35458
- ].filter(Boolean).join(" ");
35459
- return renderToolCall("transform", summary, theme, context);
35460
- }
35461
- function renderTransformResult(result, args, theme, context) {
35462
- if (context.isError)
35463
- return renderErrorResult(result, "transform failed", theme, context);
35464
- return renderSections(buildTransformSections(args, extractStructuredPayload(result), theme), context);
35465
- }
35466
- function registerStructureTool(pi, ctx) {
35467
- pi.registerTool({
35468
- name: "aft_transform",
35469
- label: "transform",
35470
- description: "Scope-aware structural code transformations with correct indentation. Use aft_safety checkpoint/undo before risky transforms.",
35471
- parameters: TransformParams,
35472
- async execute(_toolCallId, params, _signal, _onUpdate, extCtx) {
35473
- validateTransformParams(params);
35474
- const filePath = await resolvePathArg(extCtx.cwd, params.filePath);
35475
- await assertExternalDirectoryPermission(extCtx, filePath, "modify", {
35476
- restrictToProjectRoot: ctx.config.restrict_to_project_root ?? false
35477
- });
35478
- const bridge = bridgeFor(ctx, extCtx.cwd);
35479
- const req = { file: filePath };
35480
- if (params.container !== undefined)
35481
- req.scope = params.container;
35482
- if (params.code !== undefined)
35483
- req.code = params.code;
35484
- if (params.target !== undefined)
35485
- req.target = params.target;
35486
- if (params.derives !== undefined)
35487
- req.derives = params.derives;
35488
- if (params.catchBody !== undefined)
35489
- req.catch_body = params.catchBody;
35490
- if (params.decorator !== undefined)
35491
- req.decorator = params.decorator;
35492
- if (params.field !== undefined)
35493
- req.field = params.field;
35494
- if (params.tag !== undefined)
35495
- req.tag = params.tag;
35496
- if (params.value !== undefined)
35497
- req.value = params.value;
35498
- if (params.position !== undefined)
35499
- req.position = params.position;
35500
- if (params.validate !== undefined)
35501
- req.validate = params.validate;
35502
- const response = await callBridge(bridge, params.op, req, extCtx);
35503
- return textResult(JSON.stringify(response, null, 2));
35504
- },
35505
- renderCall(args, theme, context) {
35506
- return renderTransformCall(args, theme, context);
35507
- },
35508
- renderResult(result, _options, theme, context) {
35509
- return renderTransformResult(result, context.args, theme, context);
35510
- }
35511
- });
35512
- }
35513
- function validateTransformParams(params) {
35514
- const op = params.op;
35515
- if (op === "add_member") {
35516
- if (isEmptyParam(params.container)) {
35517
- throw new Error("'container' is required for 'add_member' op");
35518
- }
35519
- if (isEmptyParam(params.code)) {
35520
- throw new Error("'code' is required for 'add_member' op");
35521
- }
35522
- }
35523
- if (op === "add_derive" || op === "wrap_try_catch" || op === "add_decorator" || op === "add_struct_tags") {
35524
- if (isEmptyParam(params.target)) {
35525
- throw new Error(`'target' is required for '${op}' op`);
35526
- }
35527
- }
35528
- if (op === "add_derive" && isEmptyParam(params.derives)) {
35529
- throw new Error("'derives' array is required for 'add_derive' op");
35530
- }
35531
- if (op === "add_decorator" && isEmptyParam(params.decorator)) {
35532
- throw new Error("'decorator' is required for 'add_decorator' op");
35533
- }
35534
- if (op === "add_struct_tags") {
35535
- if (isEmptyParam(params.field)) {
35536
- throw new Error("'field' is required for 'add_struct_tags' op");
35537
- }
35538
- if (isEmptyParam(params.tag)) {
35539
- throw new Error("'tag' is required for 'add_struct_tags' op");
35540
- }
35541
- if (isEmptyParam(params.value)) {
35542
- throw new Error("'value' is required for 'add_struct_tags' op");
35543
- }
35544
- }
35545
- }
35546
-
35547
35877
  // src/workflow-hints.ts
35548
- var HEADING = "## Prefer AFT tools for token efficiency";
35878
+ var HEADING = "## IMPORTANT NOTICE about your tools";
35549
35879
  function buildWorkflowHints(opts) {
35550
35880
  const sections = [];
35551
35881
  const grepName = opts.hoistBuiltins ? "grep" : "aft_grep";
@@ -35559,14 +35889,28 @@ function buildWorkflowHints(opts) {
35559
35889
  const hasBash = !opts.absentTools.has(bashName);
35560
35890
  const hasBgBash = opts.bashBackgroundEnabled && hasBash && !opts.absentTools.has("bash_status");
35561
35891
  if (hasBash && opts.bashCompressionEnabled) {
35562
- 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.");
35892
+ sections.push([
35893
+ "**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:",
35894
+ "- `bun test | grep fail` → run `bun test`",
35895
+ "- `cargo test 2>&1 | tail -20` → run `cargo test`",
35896
+ "- `npm run build | head -50` → run `npm run build`"
35897
+ ].join(`
35898
+ `));
35563
35899
  }
35564
35900
  if (hasOutline && hasZoom) {
35565
35901
  sections.push(`**Web/URL access**: \`aft_outline({ target: "<url>" })\` first for structure, then \`aft_zoom({ url: "<url>", symbols: "<heading>" })\` for the specific section.`);
35566
35902
  }
35567
35903
  if (hasOutline && hasZoom && (hasGrep || hasSearch)) {
35904
+ const searchName = hasSearch ? "aft_search" : grepName;
35568
35905
  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.`;
35569
- 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).`);
35906
+ const readName = opts.hoistBuiltins ? "read" : "aft_read";
35907
+ sections.push([
35908
+ `**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:`,
35909
+ `- \`grep -rn "handleAuth" src/\` in bash → \`${searchName}({ query: "handleAuth" })\``,
35910
+ `- \`find . -name "*.ts" | xargs grep watcher\` in bash → \`${searchName}({ query: "watcher invalidation" })\` (concepts work too)`,
35911
+ `- \`sed -n '100,160p' app.ts\` / \`cat app.ts\` in bash → \`${readName}({ filePath: "app.ts", startLine: 100, endLine: 160 })\``
35912
+ ].join(`
35913
+ `));
35570
35914
  }
35571
35915
  if (hasInspect) {
35572
35916
  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.");
@@ -35584,13 +35928,22 @@ function buildWorkflowHints(opts) {
35584
35928
  `));
35585
35929
  }
35586
35930
  if (hasBgBash) {
35587
- 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.`);
35931
+ sections.push([
35932
+ `**Long-running commands** (builds, installs, full test suites): \`${bashName}({ background: true })\` returns a \`task_id\` immediately. Then do ONE of these:`,
35933
+ "1. Keep working on something independent of the result.",
35934
+ "2. End your turn — a completion reminder with the result arrives automatically.",
35935
+ `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.`,
35936
+ `\`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.`
35937
+ ].join(`
35938
+ `));
35588
35939
  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: "..." })\`.`);
35589
35940
  }
35590
35941
  if (sections.length === 0) {
35591
35942
  return null;
35592
35943
  }
35593
- 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.");
35944
+ 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.
35945
+
35946
+ **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.`);
35594
35947
  return `${HEADING}
35595
35948
 
35596
35949
  ${sections.join(`
@@ -35682,21 +36035,15 @@ var PLUGIN_VERSION = (() => {
35682
36035
  return "0.0.0";
35683
36036
  }
35684
36037
  })();
35685
- var ANNOUNCEMENT_VERSION = "0.36.0";
36038
+ var ANNOUNCEMENT_VERSION = "0.37.0";
35686
36039
  var ANNOUNCEMENT_FEATURES = [
35687
- "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.",
35688
- "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.",
35689
- "Code search steering: guidance and hints now point to `aft_search` and parallel `aft_*` tools instead of `grep`/`find` in bash.",
35690
- "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."
36040
+ "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.",
36041
+ "`aft_transform` removed: usage data showed agents never called it `edit` covers everything it did, and every request now carries a smaller tool surface.",
36042
+ "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.",
36043
+ "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."
35691
36044
  ];
35692
36045
  var ANNOUNCEMENT_FOOTER = "Join us on Discord: https://discord.gg/DSa65w8wuf";
35693
- var ALL_ONLY_TOOLS = new Set([
35694
- "aft_callgraph",
35695
- "aft_delete",
35696
- "aft_move",
35697
- "aft_transform",
35698
- "aft_refactor"
35699
- ]);
36046
+ var ALL_ONLY_TOOLS = new Set(["aft_callgraph", "aft_delete", "aft_move", "aft_refactor"]);
35700
36047
  var pendingEagerWarnings = new Map;
35701
36048
  function isConfigureWarning(value) {
35702
36049
  if (!value || typeof value !== "object" || Array.isArray(value))
@@ -35774,7 +36121,6 @@ function resolveToolSurface(config2) {
35774
36121
  move: false,
35775
36122
  astSearch: false,
35776
36123
  astReplace: false,
35777
- structure: false,
35778
36124
  refactor: false
35779
36125
  };
35780
36126
  }
@@ -35797,7 +36143,6 @@ function resolveToolSurface(config2) {
35797
36143
  move: false,
35798
36144
  astSearch: ok("ast_grep_search"),
35799
36145
  astReplace: ok("ast_grep_replace"),
35800
- structure: false,
35801
36146
  refactor: false
35802
36147
  };
35803
36148
  if (surface === "all") {
@@ -35806,7 +36151,6 @@ function resolveToolSurface(config2) {
35806
36151
  navigate: allOnly("aft_callgraph"),
35807
36152
  delete: allOnly("aft_delete"),
35808
36153
  move: allOnly("aft_move"),
35809
- structure: allOnly("aft_transform"),
35810
36154
  refactor: allOnly("aft_refactor")
35811
36155
  };
35812
36156
  }
@@ -36012,7 +36356,7 @@ ${lines}
36012
36356
  }
36013
36357
  const surface = resolveToolSurface(config2);
36014
36358
  if (surface.hoistBash && resolveBashConfig(config2).enabled) {
36015
- registerBashTool(pi, ctx);
36359
+ registerBashTool(pi, ctx, surface.semantic);
36016
36360
  }
36017
36361
  registerHoistedTools(pi, ctx, surface);
36018
36362
  if (surface.outline || surface.zoom) {
@@ -36042,9 +36386,6 @@ ${lines}
36042
36386
  if (surface.delete || surface.move) {
36043
36387
  registerFsTools(pi, ctx, surface);
36044
36388
  }
36045
- if (surface.structure) {
36046
- registerStructureTool(pi, ctx);
36047
- }
36048
36389
  if (surface.refactor) {
36049
36390
  registerRefactorTool(pi, ctx);
36050
36391
  }
@@ -36054,8 +36395,9 @@ ${lines}
36054
36395
  const sessionID = resolveSessionId(extCtx);
36055
36396
  const bgContent = await appendToolResultBgCompletions({ ctx, directory: extCtx.cwd, sessionID }, event.content);
36056
36397
  let content = bgContent ?? event.content;
36398
+ const isInspect = typeof event.details === "object" && event.details !== null && "scanner_state" in event.details;
36057
36399
  const activeBridge = pool.getActiveBridgeForRoot(extCtx.cwd);
36058
- const bar = statusBarBlockForSession(sessionID, activeBridge?.getStatusBar());
36400
+ const bar = statusBarBlockForSession(sessionID, activeBridge?.getStatusBar(), isInspect);
36059
36401
  if (bar)
36060
36402
  content = [...content, { type: "text", text: bar }];
36061
36403
  if (content === event.content)