tarsk 0.5.59 → 0.5.60

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (106) hide show
  1. package/dist/index.js +862 -144
  2. package/dist/public/assets/account-view-DPM8TZiH.js +1 -0
  3. package/dist/public/assets/api-BcqwPTwD.js +1 -0
  4. package/dist/public/assets/{async-error-boundary-CaRNnJZe.js → async-error-boundary-B56Jpu_o.js} +1 -1
  5. package/dist/public/assets/browser-tab-k2H96rcs.js +8 -0
  6. package/dist/public/assets/{card-DFACZp3F.js → card-C2MSBS4a.js} +1 -1
  7. package/dist/public/assets/commit-dialog-CPsT5k3n.js +1 -0
  8. package/dist/public/assets/context-menu-7zA2_Vvh.js +1 -0
  9. package/dist/public/assets/create-repo-dialog-wC-DyHKe.js +1 -0
  10. package/dist/public/assets/dialogs-config-C7xqDv5K.js +51 -0
  11. package/dist/public/assets/diff-view-dAnk7skm.js +4 -0
  12. package/dist/public/assets/{explorer-tab-context-DCq1BBX_.js → explorer-tab-context-gi63e_bv.js} +1 -1
  13. package/dist/public/assets/explorer-tab-view-ai_mDxBQ.js +2 -0
  14. package/dist/public/assets/explorer-tree-BZFGE0sU.js +1 -0
  15. package/dist/public/assets/explorer-view-D6MhosMA.js +75 -0
  16. package/dist/public/assets/git-history-dialog-Dd_DmfZC.js +1 -0
  17. package/dist/public/assets/git-ops-button-BhHIYXxb.js +2 -0
  18. package/dist/public/assets/history-view-DVqIaJOR.js +16 -0
  19. package/dist/public/assets/index-CyxfX_DC.css +1 -0
  20. package/dist/public/assets/index-D2AOIdVd.js +75 -0
  21. package/dist/public/assets/mcp-server-card-CuBKoFtN.js +1 -0
  22. package/dist/public/assets/merged-pr-dialog-M4vwv3lT.js +1 -0
  23. package/dist/public/assets/model-list-controls-DBAyi2PU.js +1 -0
  24. package/dist/public/assets/{monaco-CMkNoWsM.js → monaco-D3l3Gbj2.js} +1 -1
  25. package/dist/public/assets/onboarding-BpwQ9yz5.js +1 -0
  26. package/dist/public/assets/project-env-vars-view-Cs3LO_18.js +1 -0
  27. package/dist/public/assets/project-model-aliases-view-Z5b4ZC1k.js +1 -0
  28. package/dist/public/assets/project-settings-view-BOO6FS8b.js +1 -0
  29. package/dist/public/assets/project-tools-view-cjwDYCta.js +1 -0
  30. package/dist/public/assets/providers-list-view-BBGnB3Rq.js +1 -0
  31. package/dist/public/assets/pull-request-dialog-VOoEmwTG.js +1 -0
  32. package/dist/public/assets/pull-with-changes-dialog-CoHIwHJZ.js +1 -0
  33. package/dist/public/assets/push-before-pr-dialog-9fV4cFz5.js +1 -0
  34. package/dist/public/assets/react-vendor-XZeum4Wo.js +14 -0
  35. package/dist/public/assets/settings-general-view-CDr9M3SH.js +1 -0
  36. package/dist/public/assets/settings-instructions-view-B2mWcZiS.js +1 -0
  37. package/dist/public/assets/settings-list-JqM9DgX9.js +1 -0
  38. package/dist/public/assets/settings-logs-view-BjlFrCXc.js +5 -0
  39. package/dist/public/assets/settings-mcp-servers-view-BmVtKqmN.js +5 -0
  40. package/dist/public/assets/settings-models-view-E-HNZWxO.js +1 -0
  41. package/dist/public/assets/settings-rules-view-CdOk3Osl.js +8 -0
  42. package/dist/public/assets/settings-skills-view-Cn-GySxU.js +3 -0
  43. package/dist/public/assets/settings-slash-commands-view-CqYBr6_A.js +1 -0
  44. package/dist/public/assets/settings-statistics-view-B92b4bKS.js +1 -0
  45. package/dist/public/assets/settings-subagents-view-AL0iV3gX.js +2 -0
  46. package/dist/public/assets/settings-system-prompt-view-B6CBgNW3.js +1 -0
  47. package/dist/public/assets/settings-telegram-view-DHQxRoFn.js +1 -0
  48. package/dist/public/assets/settings-view-CBLOR6Gr.js +2 -0
  49. package/dist/public/assets/{skeleton-DBWV1Usf.js → skeleton-CK0EzL_G.js} +1 -1
  50. package/dist/public/assets/slug-utils-CdbHvCiP.js +1 -0
  51. package/dist/public/assets/tasks-view-D1nPjESr.js +2 -0
  52. package/dist/public/assets/{terminal-panel-Dlon-VDl.js → terminal-panel-m9GEHtY2.js} +2 -2
  53. package/dist/public/assets/{ui-components-Dd2afm0c.js → ui-components-BuIiDdYp.js} +1 -1
  54. package/dist/public/assets/{use-deferred-search-Cu-o4nau.js → use-deferred-search-hIqqinof.js} +1 -1
  55. package/dist/public/assets/use-installed-ai-files-C1Uta9US.js +1 -0
  56. package/dist/public/assets/{use-settings-page-loading-D1JlmLmD.js → use-settings-page-loading-DtwNt6_e.js} +1 -1
  57. package/dist/public/assets/{utils-Dt07QR3w.js → utils-B1IYtes_.js} +1 -1
  58. package/dist/public/assets/{web-CdkfJJZ3.js → web-BF1VNxf3.js} +1 -1
  59. package/dist/public/assets/{web-DeCAC9ap.js → web-BGxrdTO9.js} +1 -1
  60. package/dist/public/assets/{web-DsLUxLTR.js → web-DrKfK3e-.js} +1 -1
  61. package/dist/public/assets/{web-CiaJzwav.js → web-LjAIxEG5.js} +1 -1
  62. package/dist/public/index.html +9 -9
  63. package/package.json +1 -1
  64. package/dist/public/assets/account-view-Cc6oDwk4.js +0 -1
  65. package/dist/public/assets/api-CtfZCd77.js +0 -1
  66. package/dist/public/assets/browser-tab-BfAt-Sho.js +0 -8
  67. package/dist/public/assets/commit-dialog-Ck0r2v9S.js +0 -1
  68. package/dist/public/assets/context-menu-CBvTcHUI.js +0 -1
  69. package/dist/public/assets/create-repo-dialog-CBvRaov7.js +0 -1
  70. package/dist/public/assets/dialogs-config-C2KFQf9x.js +0 -51
  71. package/dist/public/assets/diff-view-DDECIdhE.js +0 -4
  72. package/dist/public/assets/explorer-tab-view-72Krb0Ir.js +0 -2
  73. package/dist/public/assets/explorer-tree-CcFgAYMn.js +0 -1
  74. package/dist/public/assets/explorer-view-DsNxf6nu.js +0 -75
  75. package/dist/public/assets/git-history-dialog--akFTQOd.js +0 -1
  76. package/dist/public/assets/git-ops-button-CKGLh3Zk.js +0 -2
  77. package/dist/public/assets/history-view-IcXfB6BD.js +0 -15
  78. package/dist/public/assets/index-BmpLvHah.js +0 -75
  79. package/dist/public/assets/index-r8I03VVW.css +0 -1
  80. package/dist/public/assets/mcp-server-card-DDmDAlY8.js +0 -1
  81. package/dist/public/assets/merged-pr-dialog-CiwhRqFv.js +0 -1
  82. package/dist/public/assets/model-list-controls-D5IaOffK.js +0 -1
  83. package/dist/public/assets/onboarding-CFIVEI92.js +0 -1
  84. package/dist/public/assets/project-settings-view-Klpu7p3U.js +0 -1
  85. package/dist/public/assets/providers-list-view-Df-ONKwe.js +0 -1
  86. package/dist/public/assets/pull-request-dialog-DVYZgMuN.js +0 -1
  87. package/dist/public/assets/pull-with-changes-dialog-CHHJ0gpA.js +0 -1
  88. package/dist/public/assets/push-before-pr-dialog-DoNMB3C8.js +0 -1
  89. package/dist/public/assets/react-vendor-tSKmVA4e.js +0 -14
  90. package/dist/public/assets/settings-general-view-yDDYKQmO.js +0 -1
  91. package/dist/public/assets/settings-instructions-view-y9bUZtKn.js +0 -1
  92. package/dist/public/assets/settings-list-j_NDw-aW.js +0 -1
  93. package/dist/public/assets/settings-logs-view-BzrjJRk6.js +0 -5
  94. package/dist/public/assets/settings-mcp-servers-view-NuKeyQ92.js +0 -5
  95. package/dist/public/assets/settings-models-view-64UG4njI.js +0 -1
  96. package/dist/public/assets/settings-rules-view-DdxIfxki.js +0 -8
  97. package/dist/public/assets/settings-skills-view-DoVxhA-A.js +0 -3
  98. package/dist/public/assets/settings-slash-commands-view-vm0HNVw1.js +0 -1
  99. package/dist/public/assets/settings-statistics-view-CKc3bD1x.js +0 -1
  100. package/dist/public/assets/settings-subagents-view-DuHK-7uc.js +0 -2
  101. package/dist/public/assets/settings-system-prompt-view-1clIREZ5.js +0 -1
  102. package/dist/public/assets/settings-telegram-view-DCPC5GxD.js +0 -1
  103. package/dist/public/assets/settings-view-BweC2YuW.js +0 -2
  104. package/dist/public/assets/slug-utils-D8SJJo7H.js +0 -1
  105. package/dist/public/assets/tasks-view-Cg9N8GZb.js +0 -2
  106. package/dist/public/assets/use-installed-ai-files-X8NXRdGD.js +0 -1
package/dist/index.js CHANGED
@@ -636,8 +636,8 @@ var init_observations = __esm({
636
636
  "use strict";
637
637
  OBSERVATION_ID_PREFIX = "obs_";
638
638
  OBSERVATION_EXTERNALIZE_MIN_BYTES = 8 * 1024;
639
- OBSERVATION_PREVIEW_MAX_BYTES = 4 * 1024;
640
- OBSERVATION_PREVIEW_MAX_LINES = 120;
639
+ OBSERVATION_PREVIEW_MAX_BYTES = 10 * 1024;
640
+ OBSERVATION_PREVIEW_MAX_LINES = 250;
641
641
  OBSERVATION_RETRIEVAL_DEFAULT_LINES = 200;
642
642
  OBSERVATION_RETRIEVAL_MAX_LINES = 2e3;
643
643
  OBSERVATION_RETRIEVAL_MAX_BYTES = 50 * 1024;
@@ -648,7 +648,7 @@ var init_observations = __esm({
648
648
  function isMicrocompactEligibleTool(toolName) {
649
649
  return MICROCOMPACT_ELIGIBLE_TOOL_SET.has(toolName);
650
650
  }
651
- var MICROCOMPACT_KEEP_RECENT, MICROCOMPACT_KEEP_TOKENS, MICROCOMPACT_TRIGGER_MARGIN, MICROCOMPACT_MIN_SAVE_TOKENS, MICROCOMPACT_DIGEST_MIN_CHARS, MICROCOMPACT_MARKER_PREFIX, MICROCOMPACT_ELIGIBLE_TOOLS, MICROCOMPACT_ELIGIBLE_TOOL_SET;
651
+ var MICROCOMPACT_KEEP_RECENT, MICROCOMPACT_KEEP_TOKENS, MICROCOMPACT_TRIGGER_MARGIN, MICROCOMPACT_MIN_SAVE_TOKENS, MICROCOMPACT_DIGEST_MIN_CHARS, MICROCOMPACT_PROACTIVE_TURN_THRESHOLD, MICROCOMPACT_PROACTIVE_PAYLOAD_TOKENS, MICROCOMPACT_PROACTIVE_MIN_SAVE_TOKENS, MICROCOMPACT_MARKER_PREFIX, MICROCOMPACT_ELIGIBLE_TOOLS, MICROCOMPACT_ELIGIBLE_TOOL_SET;
652
652
  var init_microcompact = __esm({
653
653
  "../shared/dist/microcompact.js"() {
654
654
  "use strict";
@@ -657,6 +657,9 @@ var init_microcompact = __esm({
657
657
  MICROCOMPACT_TRIGGER_MARGIN = 2e4;
658
658
  MICROCOMPACT_MIN_SAVE_TOKENS = 2e4;
659
659
  MICROCOMPACT_DIGEST_MIN_CHARS = 2e3;
660
+ MICROCOMPACT_PROACTIVE_TURN_THRESHOLD = 15;
661
+ MICROCOMPACT_PROACTIVE_PAYLOAD_TOKENS = 5e4;
662
+ MICROCOMPACT_PROACTIVE_MIN_SAVE_TOKENS = 5e3;
660
663
  MICROCOMPACT_MARKER_PREFIX = "[Tool result compacted]";
661
664
  MICROCOMPACT_ELIGIBLE_TOOLS = [
662
665
  "read",
@@ -1102,6 +1105,77 @@ var init_tool_display = __esm({
1102
1105
  }
1103
1106
  });
1104
1107
 
1108
+ // ../shared/dist/tool-call-group.js
1109
+ function pluralize(count, singular, plural) {
1110
+ return `${count} ${count === 1 ? singular : plural}`;
1111
+ }
1112
+ function categoryPhrases(verb, singular, plural) {
1113
+ return {
1114
+ formatPhrase: (count) => verb === null ? pluralize(count, singular, plural) : `${verb} ${pluralize(count, singular, plural)}`,
1115
+ formatNeutral: (count) => pluralize(count, singular, plural)
1116
+ };
1117
+ }
1118
+ var otherToolCallGroupCategory, toolCallGroupCategories, categoryByToolName;
1119
+ var init_tool_call_group = __esm({
1120
+ "../shared/dist/tool-call-group.js"() {
1121
+ "use strict";
1122
+ otherToolCallGroupCategory = {
1123
+ id: "other",
1124
+ toolNames: [],
1125
+ ...categoryPhrases(null, "tool call", "tool calls")
1126
+ };
1127
+ toolCallGroupCategories = [
1128
+ {
1129
+ id: "files",
1130
+ toolNames: ["edit", "write"],
1131
+ ...categoryPhrases("edited", "file", "files")
1132
+ },
1133
+ {
1134
+ id: "commands",
1135
+ toolNames: ["bash", "execute_skill_script", "run_js"],
1136
+ ...categoryPhrases("ran", "command", "commands")
1137
+ },
1138
+ {
1139
+ id: "reads",
1140
+ toolNames: ["read", "get_observation"],
1141
+ ...categoryPhrases("read", "file", "files")
1142
+ },
1143
+ {
1144
+ id: "searches",
1145
+ toolNames: ["grep", "find"],
1146
+ ...categoryPhrases(null, "search", "searches")
1147
+ },
1148
+ {
1149
+ id: "webSearches",
1150
+ toolNames: ["web_search"],
1151
+ ...categoryPhrases(null, "web search", "web searches")
1152
+ },
1153
+ {
1154
+ id: "pages",
1155
+ toolNames: ["fetch"],
1156
+ ...categoryPhrases("fetched", "page", "pages")
1157
+ },
1158
+ {
1159
+ id: "browserActions",
1160
+ toolNames: ["browser"],
1161
+ ...categoryPhrases(null, "browser action", "browser actions")
1162
+ },
1163
+ {
1164
+ id: "images",
1165
+ toolNames: ["generate_image"],
1166
+ ...categoryPhrases("generated", "image", "images")
1167
+ },
1168
+ {
1169
+ id: "subagents",
1170
+ toolNames: ["agent"],
1171
+ ...categoryPhrases(null, "subagent", "subagents")
1172
+ },
1173
+ otherToolCallGroupCategory
1174
+ ];
1175
+ categoryByToolName = new Map(toolCallGroupCategories.flatMap((category) => category.toolNames.map((name) => [name, category])));
1176
+ }
1177
+ });
1178
+
1105
1179
  // ../shared/dist/project-tools.js
1106
1180
  function normalizeDisabledTools(names) {
1107
1181
  if (!Array.isArray(names)) {
@@ -1241,12 +1315,12 @@ function getFileExtension(filename) {
1241
1315
  return "";
1242
1316
  }
1243
1317
  const trimmed = filename.trim();
1244
- const basename7 = trimmed.split(/[/\\]/).pop() ?? trimmed;
1245
- const dotIndex = basename7.lastIndexOf(".");
1246
- if (dotIndex <= 0 || dotIndex === basename7.length - 1) {
1318
+ const basename8 = trimmed.split(/[/\\]/).pop() ?? trimmed;
1319
+ const dotIndex = basename8.lastIndexOf(".");
1320
+ if (dotIndex <= 0 || dotIndex === basename8.length - 1) {
1247
1321
  return "";
1248
1322
  }
1249
- return basename7.slice(dotIndex + 1).toLowerCase();
1323
+ return basename8.slice(dotIndex + 1).toLowerCase();
1250
1324
  }
1251
1325
  function getExplorerFileMediaType(path8, name) {
1252
1326
  const ext = getFileExtension(path8) || (name ? getFileExtension(name) : "");
@@ -2346,6 +2420,7 @@ var init_dist = __esm({
2346
2420
  init_typography();
2347
2421
  init_font_scale();
2348
2422
  init_tool_display();
2423
+ init_tool_call_group();
2349
2424
  init_project_tools();
2350
2425
  init_providers();
2351
2426
  init_web_fetch();
@@ -6837,7 +6912,8 @@ var init_command_pattern = __esm({
6837
6912
  // src/tools/path-utils.ts
6838
6913
  import { accessSync, constants } from "node:fs";
6839
6914
  import * as os from "node:os";
6840
- import { isAbsolute, resolve as resolvePath, relative } from "node:path";
6915
+ import { basename, isAbsolute, resolve as resolvePath, relative } from "node:path";
6916
+ import { globSync } from "glob";
6841
6917
  function normalizeUnicodeSpaces(str) {
6842
6918
  return str.replace(UNICODE_SPACES, " ");
6843
6919
  }
@@ -6872,6 +6948,41 @@ function findMatchingSuffixInCwd(resolvedPath2, cwd) {
6872
6948
  }
6873
6949
  return void 0;
6874
6950
  }
6951
+ function findFilesByBasenameInCwd(fileBasename, cwd, limit) {
6952
+ if (!fileBasename || fileBasename === "." || fileBasename === "..") {
6953
+ return [];
6954
+ }
6955
+ try {
6956
+ const found = globSync(`**/${fileBasename}`, {
6957
+ cwd,
6958
+ dot: false,
6959
+ ignore: ["**/node_modules/**", "**/.git/**"],
6960
+ nodir: true
6961
+ });
6962
+ return found.slice(0, limit).map((match) => match.replace(/\\/g, "/"));
6963
+ } catch {
6964
+ return [];
6965
+ }
6966
+ }
6967
+ function collectFileNotFoundHints(resolvedPath2, cwd) {
6968
+ const requestedRelative = relative(cwd, resolvedPath2).replace(/\\/g, "/");
6969
+ const suffixMatch = findMatchingSuffixInCwd(resolvedPath2, cwd);
6970
+ if (suffixMatch && suffixMatch !== requestedRelative && suffixMatch !== ".") {
6971
+ return [`Try: ${suffixMatch}`];
6972
+ }
6973
+ const matches = findFilesByBasenameInCwd(
6974
+ basename(resolvedPath2),
6975
+ cwd,
6976
+ BASENAME_SEARCH_LIMIT
6977
+ ).filter((match) => match !== requestedRelative);
6978
+ if (matches.length === 1) {
6979
+ return [`Try: ${matches[0]}`];
6980
+ }
6981
+ if (matches.length > 1) {
6982
+ return [`Similar files: ${matches.join(", ")}`];
6983
+ }
6984
+ return [];
6985
+ }
6875
6986
  function collectPathSuggestionHints(resolvedPath2, cwd) {
6876
6987
  const hints = ["Use a path relative to the project instead of an absolute path."];
6877
6988
  const suffixMatch = findMatchingSuffixInCwd(resolvedPath2, cwd);
@@ -6930,7 +7041,9 @@ function toDisplayPath(cwd, resolvedPath2, requestedPath) {
6930
7041
  }
6931
7042
  function fileNotFoundError(cwd, resolvedPath2, requestedPath) {
6932
7043
  const displayPath = toDisplayPath(cwd, resolvedPath2, requestedPath);
6933
- return new Error(`no such file or directory '${displayPath}'`);
7044
+ const hints = collectFileNotFoundHints(resolvedPath2, cwd);
7045
+ const suffix = hints.length > 0 ? ` ${hints.join(" ")}` : "";
7046
+ return new Error(`no such file or directory '${displayPath}'.${suffix}`);
6934
7047
  }
6935
7048
  function rethrowIfFileNotFound(error, cwd, resolvedPath2, requestedPath) {
6936
7049
  if (isEnoentError(error)) {
@@ -6961,16 +7074,18 @@ function resolveReadPath(filePath, cwd) {
6961
7074
  }
6962
7075
  return resolved;
6963
7076
  }
6964
- var UNICODE_SPACES, NARROW_NO_BREAK_SPACE;
7077
+ var UNICODE_SPACES, NARROW_NO_BREAK_SPACE, BASENAME_SEARCH_LIMIT;
6965
7078
  var init_path_utils = __esm({
6966
7079
  "src/tools/path-utils.ts"() {
6967
7080
  "use strict";
6968
7081
  UNICODE_SPACES = /[\u00A0\u2000-\u200A\u202F\u205F\u3000]/g;
6969
7082
  NARROW_NO_BREAK_SPACE = "\u202F";
7083
+ BASENAME_SEARCH_LIMIT = 3;
6970
7084
  }
6971
7085
  });
6972
7086
 
6973
7087
  // src/tools/bash-command-guard.ts
7088
+ import { isAbsolute as isAbsolute2 } from "node:path";
6974
7089
  function commandBinary(token) {
6975
7090
  if (!token) return "";
6976
7091
  const parts = token.split("/");
@@ -7150,7 +7265,7 @@ function shouldRedirectReadSegment(tokens) {
7150
7265
  }
7151
7266
  return isSedPrintRange(tokens);
7152
7267
  }
7153
- function validateBashDedicatedToolRedirect(command) {
7268
+ function getBashDedicatedToolRedirectMessage(command) {
7154
7269
  const segments = splitShellSegments(command);
7155
7270
  for (let index = 0; index < segments.length; index++) {
7156
7271
  const tokens = getCommandTokens(segments[index]);
@@ -7158,15 +7273,16 @@ function validateBashDedicatedToolRedirect(command) {
7158
7273
  continue;
7159
7274
  }
7160
7275
  if (shouldRedirectGrepSegment(tokens, index === 0)) {
7161
- throw redirectError("grep", "Shell grep/rg is not allowed for codebase search.");
7276
+ return redirectError("grep", "Shell grep/rg is not allowed for codebase search.").message;
7162
7277
  }
7163
7278
  if (shouldRedirectFindSegment(tokens)) {
7164
- throw redirectError("find", "Shell find is not allowed for file discovery.");
7279
+ return redirectError("find", "Shell find is not allowed for file discovery.").message;
7165
7280
  }
7166
7281
  if (shouldRedirectReadSegment(tokens)) {
7167
- throw redirectError("read", "Shell cat/head/tail/sed is not allowed for reading files.");
7282
+ return redirectError("read", "Shell cat/head/tail/sed is not allowed for reading files.").message;
7168
7283
  }
7169
7284
  }
7285
+ return void 0;
7170
7286
  }
7171
7287
  function validateBashFilesystemSearch(command, cwd) {
7172
7288
  for (const segment of splitShellSegments(command)) {
@@ -7192,6 +7308,109 @@ function validateBashFilesystemSearch(command, cwd) {
7192
7308
  }
7193
7309
  }
7194
7310
  }
7311
+ function unquoteRedirectTarget(target) {
7312
+ if (target.startsWith('"') && target.endsWith('"') || target.startsWith("'") && target.endsWith("'")) {
7313
+ return target.slice(1, -1);
7314
+ }
7315
+ return target;
7316
+ }
7317
+ function extractShellRedirectTargets(command) {
7318
+ const targets = [];
7319
+ let quote = null;
7320
+ let escaped = false;
7321
+ for (let index = 0; index < command.length; index++) {
7322
+ const char = command[index];
7323
+ if (escaped) {
7324
+ escaped = false;
7325
+ continue;
7326
+ }
7327
+ if (char === "\\" && quote !== "'") {
7328
+ escaped = true;
7329
+ continue;
7330
+ }
7331
+ if (quote) {
7332
+ if (char === quote) {
7333
+ quote = null;
7334
+ }
7335
+ continue;
7336
+ }
7337
+ if (char === "'" || char === '"') {
7338
+ quote = char;
7339
+ continue;
7340
+ }
7341
+ const rest = command.slice(index);
7342
+ const operatorMatch = rest.match(/^(\d>>|\d>|>>|>|&>>|&>)(?![&\d])/);
7343
+ if (!operatorMatch) {
7344
+ continue;
7345
+ }
7346
+ index += operatorMatch[0].length;
7347
+ while (index < command.length && /\s/.test(command[index])) {
7348
+ index++;
7349
+ }
7350
+ if (index >= command.length) {
7351
+ continue;
7352
+ }
7353
+ if (command[index] === "'" || command[index] === '"') {
7354
+ const quoteChar = command[index];
7355
+ index++;
7356
+ let target2 = "";
7357
+ while (index < command.length && command[index] !== quoteChar) {
7358
+ target2 += command[index];
7359
+ index++;
7360
+ }
7361
+ if (target2) {
7362
+ targets.push(target2);
7363
+ }
7364
+ continue;
7365
+ }
7366
+ let target = "";
7367
+ while (index < command.length && !/[\s;|&<>]/.test(command[index])) {
7368
+ target += command[index];
7369
+ index++;
7370
+ }
7371
+ if (target && !target.startsWith("&")) {
7372
+ targets.push(target);
7373
+ }
7374
+ index--;
7375
+ }
7376
+ return targets;
7377
+ }
7378
+ function isIgnorableRedirectTarget(target) {
7379
+ const cleaned = unquoteRedirectTarget(target);
7380
+ return IGNORABLE_REDIRECT_TARGETS.has(cleaned);
7381
+ }
7382
+ function isRedirectTargetOutsideRepo(target, repositoryRoot) {
7383
+ const cleaned = unquoteRedirectTarget(target);
7384
+ const expanded = expandPath(cleaned);
7385
+ if (expanded === "/dev/null" || expanded === "/dev/null2") {
7386
+ return false;
7387
+ }
7388
+ if (expanded === "/tmp" || expanded.startsWith("/tmp/")) {
7389
+ return true;
7390
+ }
7391
+ if (expanded === "~" || expanded.startsWith("~/")) {
7392
+ return true;
7393
+ }
7394
+ const resolved = isAbsolute2(expanded) ? expanded : resolveToCwd(cleaned, repositoryRoot);
7395
+ try {
7396
+ validatePathWithinCwd(resolved, repositoryRoot);
7397
+ return false;
7398
+ } catch {
7399
+ return true;
7400
+ }
7401
+ }
7402
+ function getBashOutsideRepoRedirectMessage(command, repositoryRoot) {
7403
+ for (const target of extractShellRedirectTargets(command)) {
7404
+ if (isIgnorableRedirectTarget(target)) {
7405
+ continue;
7406
+ }
7407
+ if (isRedirectTargetOutsideRepo(target, repositoryRoot)) {
7408
+ const display = unquoteRedirectTarget(target);
7409
+ return `Redirecting command output outside the project (${display}) is not allowed when sandbox is enabled. The grep and read tools cannot access paths outside the repo. Use a repo-relative log file instead (e.g. .tarsk-test.log), then grep or read that path.`;
7410
+ }
7411
+ }
7412
+ return void 0;
7413
+ }
7195
7414
  function isSearchShapedBashCommand(command) {
7196
7415
  for (const segment of splitShellSegments(command)) {
7197
7416
  const tokens = getCommandTokens(segment);
@@ -7205,7 +7424,7 @@ function isSearchShapedBashCommand(command) {
7205
7424
  }
7206
7425
  return false;
7207
7426
  }
7208
- var BROAD_SEARCH_ROOTS, FIND_OPTIONS_WITH_ARG, GREP_BINARIES, RECURSIVE_GREP_FLAGS, GREP_FLAGS_WITH_ARG, HEAD_TAIL_FLAGS_WITH_ARG;
7427
+ var BROAD_SEARCH_ROOTS, FIND_OPTIONS_WITH_ARG, GREP_BINARIES, RECURSIVE_GREP_FLAGS, GREP_FLAGS_WITH_ARG, HEAD_TAIL_FLAGS_WITH_ARG, IGNORABLE_REDIRECT_TARGETS;
7209
7428
  var init_bash_command_guard = __esm({
7210
7429
  "src/tools/bash-command-guard.ts"() {
7211
7430
  "use strict";
@@ -7250,6 +7469,7 @@ var init_bash_command_guard = __esm({
7250
7469
  "--exclude-dir"
7251
7470
  ]);
7252
7471
  HEAD_TAIL_FLAGS_WITH_ARG = /* @__PURE__ */ new Set(["-n", "-c", "--lines", "--bytes"]);
7472
+ IGNORABLE_REDIRECT_TARGETS = /* @__PURE__ */ new Set(["/dev/null", "/dev/null2", "NUL", "&1", "1"]);
7253
7473
  }
7254
7474
  });
7255
7475
 
@@ -7788,6 +8008,115 @@ var init_log = __esm({
7788
8008
  }
7789
8009
  });
7790
8010
 
8011
+ // src/tools/compress/test-runner.ts
8012
+ function stripAnsi(text) {
8013
+ return text.replace(ANSI_ESCAPE_RE, "");
8014
+ }
8015
+ function looksLikeTestRunnerOutput(text) {
8016
+ const plain = stripAnsi(text);
8017
+ return TEST_FILES_LINE_RE.test(plain) || TESTS_SUMMARY_LINE_RE.test(plain);
8018
+ }
8019
+ function isSummaryOrTimingLine(line) {
8020
+ const plain = stripAnsi(line);
8021
+ return TEST_FILES_LINE_RE.test(plain) || TESTS_SUMMARY_LINE_RE.test(plain) || TIMING_LINE_RE.test(plain);
8022
+ }
8023
+ function isFailureHeader(line) {
8024
+ return FAIL_HEADER_RE.test(stripAnsi(line));
8025
+ }
8026
+ function isWorkerNoise(line) {
8027
+ const plain = stripAnsi(line);
8028
+ return WORKER_NOISE_RE.test(plain) || /^\s*at processTicksAndRejections/.test(plain);
8029
+ }
8030
+ function shouldKeepFailureLine(line) {
8031
+ const plain = stripAnsi(line);
8032
+ if (!plain.trim()) {
8033
+ return false;
8034
+ }
8035
+ if (isWorkerNoise(line)) {
8036
+ return false;
8037
+ }
8038
+ if (/node_modules[/\\]/.test(plain) && !FAILURE_DETAIL_RE.test(plain)) {
8039
+ return false;
8040
+ }
8041
+ return true;
8042
+ }
8043
+ function extractFailureBlocks(lines) {
8044
+ const blocks = [];
8045
+ let blockLines = [];
8046
+ let inBlock = false;
8047
+ function flushBlock() {
8048
+ if (blockLines.length === 0) {
8049
+ return;
8050
+ }
8051
+ blocks.push(blockLines.join("\n"));
8052
+ blockLines = [];
8053
+ inBlock = false;
8054
+ }
8055
+ for (const line of lines) {
8056
+ if (isFailureHeader(line)) {
8057
+ flushBlock();
8058
+ inBlock = true;
8059
+ blockLines = [stripAnsi(line)];
8060
+ continue;
8061
+ }
8062
+ if (!inBlock) {
8063
+ continue;
8064
+ }
8065
+ if (blockLines.length >= MAX_FAILURE_BLOCK_LINES) {
8066
+ continue;
8067
+ }
8068
+ if (line.trim() === "" && blockLines.length > 2) {
8069
+ flushBlock();
8070
+ continue;
8071
+ }
8072
+ if (shouldKeepFailureLine(line)) {
8073
+ blockLines.push(stripAnsi(line));
8074
+ }
8075
+ }
8076
+ flushBlock();
8077
+ return blocks;
8078
+ }
8079
+ function summarizeTestRunnerOutput(text) {
8080
+ if (!text || !looksLikeTestRunnerOutput(text)) {
8081
+ return { text, applied: false };
8082
+ }
8083
+ const lines = text.split("\n");
8084
+ const summaryLines = [];
8085
+ for (const line of lines) {
8086
+ if (isSummaryOrTimingLine(line)) {
8087
+ summaryLines.push(stripAnsi(line).trimEnd());
8088
+ }
8089
+ }
8090
+ const failureBlocks = extractFailureBlocks(lines);
8091
+ const parts = [TEST_RUNNER_BANNER];
8092
+ if (summaryLines.length > 0) {
8093
+ parts.push(summaryLines.join("\n"));
8094
+ }
8095
+ if (failureBlocks.length > 0) {
8096
+ parts.push(failureBlocks.join("\n\n"));
8097
+ }
8098
+ const compressed = parts.join("\n");
8099
+ if (compressed === text || Buffer.byteLength(compressed) >= Buffer.byteLength(text)) {
8100
+ return { text, applied: false };
8101
+ }
8102
+ return { text: compressed, applied: true };
8103
+ }
8104
+ var TEST_RUNNER_BANNER, ANSI_ESCAPE_RE, TEST_FILES_LINE_RE, TESTS_SUMMARY_LINE_RE, TIMING_LINE_RE, FAIL_HEADER_RE, FAILURE_DETAIL_RE, WORKER_NOISE_RE, MAX_FAILURE_BLOCK_LINES;
8105
+ var init_test_runner = __esm({
8106
+ "src/tools/compress/test-runner.ts"() {
8107
+ "use strict";
8108
+ TEST_RUNNER_BANNER = "[Test output summarized; passing tests and worker teardown omitted]";
8109
+ ANSI_ESCAPE_RE = /\u001b\[[0-9;]*m/g;
8110
+ TEST_FILES_LINE_RE = /\bTest Files\b/;
8111
+ TESTS_SUMMARY_LINE_RE = /^\s*Tests\s+\d+/;
8112
+ TIMING_LINE_RE = /^\s*(Start at|Duration)\b/;
8113
+ FAIL_HEADER_RE = /^\s*FAIL\s+/;
8114
+ FAILURE_DETAIL_RE = /AssertionError|Expected|Received|\berror TS\d+\b|Cannot find name|✗|×|❯/;
8115
+ WORKER_NOISE_RE = /^\[vitest-pool\]:|^Emitted 'error' event|^Error: kill EPERM|^node:events:/;
8116
+ MAX_FAILURE_BLOCK_LINES = 14;
8117
+ }
8118
+ });
8119
+
7791
8120
  // src/tools/compress/bash.ts
7792
8121
  function looksLikePathLine(line) {
7793
8122
  if (!line) {
@@ -7903,6 +8232,11 @@ function compressBashOutput(text) {
7903
8232
  if (!text) {
7904
8233
  return text;
7905
8234
  }
8235
+ const testSummary = summarizeTestRunnerOutput(text);
8236
+ if (testSummary.applied) {
8237
+ logCompressOutput("bash.test-runner", text, testSummary.text);
8238
+ return testSummary.text;
8239
+ }
7906
8240
  const lines = text.split("\n");
7907
8241
  const attempts = [
7908
8242
  tryCompressPurePathList,
@@ -7925,6 +8259,7 @@ var init_bash = __esm({
7925
8259
  init_find();
7926
8260
  init_grep();
7927
8261
  init_log();
8262
+ init_test_runner();
7928
8263
  GIT_COMMIT_HEADER = /^[0-9a-f]{7,40} /;
7929
8264
  }
7930
8265
  });
@@ -8120,7 +8455,29 @@ function createBashTool(repositoryRoot, options) {
8120
8455
  const resolvedCommand = commandPrefix ? `${commandPrefix}
8121
8456
  ${command}` : command;
8122
8457
  tarskDebugLog(`[ai] bash-start: ${resolvedCommand}`);
8123
- validateBashDedicatedToolRedirect(resolvedCommand);
8458
+ if (sandboxConfig) {
8459
+ const outsideRepoRedirect = getBashOutsideRepoRedirectMessage(
8460
+ resolvedCommand,
8461
+ repositoryRoot
8462
+ );
8463
+ if (outsideRepoRedirect) {
8464
+ log12.debug("Redirecting bash away from out-of-repo output path", {
8465
+ command: resolvedCommand
8466
+ });
8467
+ return {
8468
+ content: [{ type: "text", text: outsideRepoRedirect }],
8469
+ details: void 0
8470
+ };
8471
+ }
8472
+ }
8473
+ const dedicatedRedirect = getBashDedicatedToolRedirectMessage(resolvedCommand);
8474
+ if (dedicatedRedirect) {
8475
+ log12.debug("Redirecting bash to dedicated tool", { command: resolvedCommand });
8476
+ return {
8477
+ content: [{ type: "text", text: dedicatedRedirect }],
8478
+ details: void 0
8479
+ };
8480
+ }
8124
8481
  validateBashFilesystemSearch(resolvedCommand, repositoryRoot);
8125
8482
  if (shellPermissionGate) {
8126
8483
  await shellPermissionGate.ensureAllowed(toolCallId, resolvedCommand, "bash", signal);
@@ -8526,16 +8883,86 @@ var init_tool_helpers = __esm({
8526
8883
  import { Type as Type2 } from "@sinclair/typebox";
8527
8884
  import { constants as constants2 } from "fs";
8528
8885
  import { access as fsAccess, readFile as fsReadFile, writeFile as fsWriteFile } from "fs/promises";
8886
+ function resolveReplacements(input) {
8887
+ if (input.replacements && input.replacements.length > 0) {
8888
+ return input.replacements.map((replacement) => ({
8889
+ oldText: replacement.oldText,
8890
+ newText: replacement.newText,
8891
+ replaceAll: replacement.replaceAll ?? input.replaceAll ?? false
8892
+ }));
8893
+ }
8894
+ if (input.oldText !== void 0 && input.newText !== void 0) {
8895
+ return [
8896
+ {
8897
+ oldText: input.oldText,
8898
+ newText: input.newText,
8899
+ replaceAll: input.replaceAll ?? false
8900
+ }
8901
+ ];
8902
+ }
8903
+ throw new Error(
8904
+ "Provide either oldText+newText or a non-empty replacements array of { oldText, newText } hunks."
8905
+ );
8906
+ }
8907
+ function applySingleReplacement(content, normalizedOldText, normalizedNewText) {
8908
+ const matchResult = fuzzyFindText(content, normalizedOldText);
8909
+ if (!matchResult.found) {
8910
+ return { content, replaced: false };
8911
+ }
8912
+ return {
8913
+ content: content.substring(0, matchResult.index) + normalizedNewText + content.substring(matchResult.index + matchResult.matchLength),
8914
+ replaced: true
8915
+ };
8916
+ }
8917
+ function applyReplacement(content, path8, oldText, newText, replaceAll) {
8918
+ const normalizedOldText = normalizeToLF(oldText);
8919
+ const normalizedNewText = normalizeToLF(newText);
8920
+ if (!replaceAll) {
8921
+ const matchResult = fuzzyFindText(content, normalizedOldText);
8922
+ if (!matchResult.found) {
8923
+ throw new Error(
8924
+ `Could not find the exact text in ${path8}. The old text must match exactly including all whitespace and newlines.`
8925
+ );
8926
+ }
8927
+ const fuzzyContent = normalizeForFuzzyMatch(content);
8928
+ const fuzzyOldText = normalizeForFuzzyMatch(normalizedOldText);
8929
+ const occurrences = fuzzyContent.split(fuzzyOldText).length - 1;
8930
+ if (occurrences > 1) {
8931
+ throw new Error(
8932
+ `Found ${occurrences} occurrences of the text in ${path8}. The text must be unique. Please provide more context to make it unique, or set replaceAll: true to replace every occurrence.`
8933
+ );
8934
+ }
8935
+ return content.substring(0, matchResult.index) + normalizedNewText + content.substring(matchResult.index + matchResult.matchLength);
8936
+ }
8937
+ let result = content;
8938
+ let replacedCount = 0;
8939
+ while (true) {
8940
+ const next = applySingleReplacement(result, normalizedOldText, normalizedNewText);
8941
+ if (!next.replaced) {
8942
+ break;
8943
+ }
8944
+ result = next.content;
8945
+ replacedCount++;
8946
+ }
8947
+ if (replacedCount === 0) {
8948
+ throw new Error(
8949
+ `Could not find the exact text in ${path8}. The old text must match exactly including all whitespace and newlines.`
8950
+ );
8951
+ }
8952
+ return result;
8953
+ }
8529
8954
  function createEditTool(cwd, options) {
8530
8955
  const ops = options?.operations ?? defaultEditOperations;
8531
8956
  return {
8532
8957
  name: "edit",
8533
8958
  label: "edit",
8534
- description: "Edit a file by replacing exact text. The oldText must match exactly (including whitespace). Use this for precise, surgical edits.",
8959
+ description: "Edit a file by replacing exact text (oldText must match uniquely unless replaceAll is true). Prefer fewer larger hunks over many tiny serial edits. Use replacements to apply multiple hunks in one call; set replaceAll on a hunk (or top-level) for mechanical refactors with repeated oldText. Call edit in parallel across independent files.",
8535
8960
  parameters: editSchema,
8536
- execute: async (_toolCallId, { path: path8, oldText, newText }, signal) => {
8961
+ execute: async (_toolCallId, input, signal) => {
8962
+ const { path: path8 } = input;
8537
8963
  const absolutePath = resolveToCwd(path8, cwd);
8538
8964
  validatePathWithinCwd(absolutePath, cwd);
8965
+ const hunks = resolveReplacements(input);
8539
8966
  return withAbortSignal(signal, async (isAborted) => {
8540
8967
  try {
8541
8968
  await ops.access(absolutePath);
@@ -8548,26 +8975,18 @@ function createEditTool(cwd, options) {
8548
8975
  if (isAborted()) return { content: [], details: void 0 };
8549
8976
  const { bom, text: content } = stripBom(rawContent);
8550
8977
  const originalEnding = detectLineEnding(content);
8551
- const normalizedContent = normalizeToLF(content);
8552
- const normalizedOldText = normalizeToLF(oldText);
8553
- const normalizedNewText = normalizeToLF(newText);
8554
- const matchResult = fuzzyFindText(normalizedContent, normalizedOldText);
8555
- if (!matchResult.found) {
8556
- throw new Error(
8557
- `Could not find the exact text in ${path8}. The old text must match exactly including all whitespace and newlines.`
8978
+ const baseContent = normalizeToLF(content);
8979
+ let newContent = baseContent;
8980
+ for (const hunk of hunks) {
8981
+ newContent = applyReplacement(
8982
+ newContent,
8983
+ path8,
8984
+ hunk.oldText,
8985
+ hunk.newText,
8986
+ hunk.replaceAll
8558
8987
  );
8988
+ if (isAborted()) return { content: [], details: void 0 };
8559
8989
  }
8560
- const fuzzyContent = normalizeForFuzzyMatch(normalizedContent);
8561
- const fuzzyOldText = normalizeForFuzzyMatch(normalizedOldText);
8562
- const occurrences = fuzzyContent.split(fuzzyOldText).length - 1;
8563
- if (occurrences > 1) {
8564
- throw new Error(
8565
- `Found ${occurrences} occurrences of the text in ${path8}. The text must be unique. Please provide more context to make it unique.`
8566
- );
8567
- }
8568
- if (isAborted()) return { content: [], details: void 0 };
8569
- const baseContent = normalizedContent;
8570
- const newContent = baseContent.substring(0, matchResult.index) + normalizedNewText + baseContent.substring(matchResult.index + matchResult.matchLength);
8571
8990
  if (baseContent === newContent) {
8572
8991
  throw new Error(
8573
8992
  `No changes made to ${path8}. The replacement produced identical content.`
@@ -8577,25 +8996,65 @@ function createEditTool(cwd, options) {
8577
8996
  await ops.writeFile(absolutePath, finalContent);
8578
8997
  if (isAborted()) return { content: [], details: void 0 };
8579
8998
  const diffResult = generateDiffString(baseContent, newContent);
8999
+ const hunkLabel = hunks.length === 1 ? "text" : `${hunks.length} hunks`;
8580
9000
  return {
8581
- content: [{ type: "text", text: `Successfully replaced text in ${path8}.` }],
8582
- details: { diff: diffResult.diff, firstChangedLine: diffResult.firstChangedLine }
9001
+ content: [
9002
+ {
9003
+ type: "text",
9004
+ text: `Successfully replaced ${hunkLabel} in ${path8}.`
9005
+ }
9006
+ ],
9007
+ details: {
9008
+ diff: diffResult.diff,
9009
+ firstChangedLine: diffResult.firstChangedLine,
9010
+ replacementCount: hunks.length
9011
+ }
8583
9012
  };
8584
9013
  });
8585
9014
  }
8586
9015
  };
8587
9016
  }
8588
- var editSchema, defaultEditOperations, editTool;
9017
+ var replacementSchema, editSchema, defaultEditOperations, editTool;
8589
9018
  var init_edit = __esm({
8590
9019
  "src/tools/edit.ts"() {
8591
9020
  "use strict";
8592
9021
  init_edit_diff();
8593
9022
  init_path_utils();
8594
9023
  init_tool_helpers();
9024
+ replacementSchema = Type2.Object({
9025
+ oldText: Type2.String({
9026
+ description: "Exact text to find and replace (must match exactly; unique unless replaceAll is true)"
9027
+ }),
9028
+ newText: Type2.String({ description: "Replacement text" }),
9029
+ replaceAll: Type2.Optional(
9030
+ Type2.Boolean({
9031
+ description: "When true, replace every occurrence of oldText in the file."
9032
+ })
9033
+ )
9034
+ });
8595
9035
  editSchema = Type2.Object({
8596
9036
  path: Type2.String({ description: "Path to the file to edit (relative or absolute)" }),
8597
- oldText: Type2.String({ description: "Exact text to find and replace (must match exactly)" }),
8598
- newText: Type2.String({ description: "New text to replace the old text with" })
9037
+ oldText: Type2.Optional(
9038
+ Type2.String({
9039
+ description: "Exact text to find and replace (must match exactly). Required unless replacements is set."
9040
+ })
9041
+ ),
9042
+ newText: Type2.Optional(
9043
+ Type2.String({
9044
+ description: "New text to replace oldText with. Required unless replacements is set."
9045
+ })
9046
+ ),
9047
+ replaceAll: Type2.Optional(
9048
+ Type2.Boolean({
9049
+ description: "When true with oldText/newText, replace every occurrence. Per-hunk replaceAll in replacements overrides this."
9050
+ })
9051
+ ),
9052
+ replacements: Type2.Optional(
9053
+ Type2.Array(replacementSchema, {
9054
+ minItems: 1,
9055
+ description: "Multiple hunks to apply in one call (applied in order). Prefer this over serial single-hunk edits on the same file."
9056
+ })
9057
+ )
8599
9058
  });
8600
9059
  defaultEditOperations = {
8601
9060
  readFile: (path8) => fsReadFile(path8),
@@ -8611,13 +9070,13 @@ import { Type as Type3 } from "@sinclair/typebox";
8611
9070
  import { existsSync as existsSync7 } from "fs";
8612
9071
  import os2 from "node:os";
8613
9072
  import path2 from "path";
8614
- import { globSync } from "glob";
9073
+ import { globSync as globSync2 } from "glob";
8615
9074
  function createFindTool(cwd, options) {
8616
9075
  const customOps = options?.operations;
8617
9076
  return {
8618
9077
  name: "find",
8619
9078
  label: "find",
8620
- description: `Search for files by glob pattern. Returns matching file paths relative to the search directory. When many results share directories, output is grouped as "dir/: file1, file2" lines (one path per file, "dir/name") to save space; bare filenames are root-level files. Output truncated to ${DEFAULT_LIMIT} results or ${DEFAULT_MAX_BYTES / 1024}KB.`,
9079
+ description: `Search for files by glob pattern. Returns matching file paths relative to the search directory. When many results share directories, output is grouped as "dir/: file1, file2" lines (one path per file, "dir/name") to save space; bare filenames are root-level files. Brace expansion like dir/{a,b}.tsx only matches that directory level; use **/name.tsx or dir/**/*.tsx for nested files. Output truncated to ${DEFAULT_LIMIT} results or ${DEFAULT_MAX_BYTES / 1024}KB.`,
8621
9080
  parameters: findSchema,
8622
9081
  execute: async (_toolCallId, { pattern, path: searchDir, limit }, signal) => {
8623
9082
  return withAbortSignal(signal, async (isAborted) => {
@@ -8727,7 +9186,7 @@ var init_find2 = __esm({
8727
9186
  glob: async (pattern, searchCwd, { ignore, limit }) => {
8728
9187
  const results = [];
8729
9188
  try {
8730
- const found = globSync(pattern, {
9189
+ const found = globSync2(pattern, {
8731
9190
  cwd: searchCwd,
8732
9191
  dot: true,
8733
9192
  ignore: ["**/node_modules/**", "**/.git/**", ...ignore],
@@ -9155,6 +9614,16 @@ function toRipgrepSearchPath(absoluteSearchPath, cwd) {
9155
9614
  }
9156
9615
  return relative8.replace(/\\/g, "/");
9157
9616
  }
9617
+ function formatRipgrepFailure(stderr, code) {
9618
+ const trimmed = stderr.trim();
9619
+ const base = trimmed || (code === 2 ? "ripgrep exited with code 2" : `ripgrep exited with code ${code}`);
9620
+ if (REGEX_PARSE_ERROR_RE.test(trimmed)) {
9621
+ return new Error(`${base}
9622
+
9623
+ ${REGEX_PARSE_HINT}`);
9624
+ }
9625
+ return new Error(base);
9626
+ }
9158
9627
  async function runRipgrep(args2, searchPath, cwd) {
9159
9628
  const rgSearchPath = toRipgrepSearchPath(searchPath, cwd);
9160
9629
  const { code, stdout, stderr } = await ripgrep([...args2, rgSearchPath], {
@@ -9162,10 +9631,10 @@ async function runRipgrep(args2, searchPath, cwd) {
9162
9631
  preopens: { ".": cwd }
9163
9632
  });
9164
9633
  if (code === 2) {
9165
- throw new Error(stderr.trim() || "ripgrep exited with code 2");
9634
+ throw formatRipgrepFailure(stderr, code);
9166
9635
  }
9167
9636
  if (code !== 0 && code !== 1) {
9168
- throw new Error(stderr.trim() || `ripgrep exited with code ${code}`);
9637
+ throw formatRipgrepFailure(stderr, code);
9169
9638
  }
9170
9639
  return parseRipgrepOutput(stdout);
9171
9640
  }
@@ -9188,13 +9657,25 @@ function sortFilesByMtime(files) {
9188
9657
  return timeComparison;
9189
9658
  }).map(([filePath]) => filePath);
9190
9659
  }
9191
- function formatContentResult(cwd, results, headLimit, offset) {
9660
+ function looksLikePathScopedGlob(glob3) {
9661
+ if (!glob3) {
9662
+ return false;
9663
+ }
9664
+ return glob3.includes("/") || glob3.includes("\\");
9665
+ }
9666
+ function emptyMatchText(input) {
9667
+ if (looksLikePathScopedGlob(input.glob)) {
9668
+ return 'No matches found. Tip: prefer path for file/directory scope (e.g. path="app/components") and extension globs like "*.tsx" or "**/*.ts"; path-prefixed globs can miss matches depending on the search root.';
9669
+ }
9670
+ return "No matches found";
9671
+ }
9672
+ function formatContentResult(cwd, results, headLimit, offset, emptyText) {
9192
9673
  const { items: limitedResults, appliedLimit } = applyHeadLimit(results, headLimit, offset);
9193
9674
  const finalLines = limitedResults.map((line) => relativizeContentLine(cwd, line));
9194
9675
  const limitInfo = formatLimitInfo(appliedLimit, offset > 0 ? offset : void 0);
9195
9676
  let text;
9196
9677
  if (finalLines.length === 0) {
9197
- text = "No matches found";
9678
+ text = emptyText;
9198
9679
  } else {
9199
9680
  const flat = finalLines.join("\n");
9200
9681
  const compressed = compressContent(finalLines);
@@ -9215,7 +9696,7 @@ ${compressed.text}` : compressed.text;
9215
9696
  };
9216
9697
  return { text, details };
9217
9698
  }
9218
- function formatCountResult(cwd, results, headLimit, offset) {
9699
+ function formatCountResult(cwd, results, headLimit, offset, emptyText) {
9219
9700
  const { items: limitedResults, appliedLimit } = applyHeadLimit(results, headLimit, offset);
9220
9701
  const finalCountLines = limitedResults.map((line) => relativizeCountLine(cwd, line));
9221
9702
  const limitInfo = formatLimitInfo(appliedLimit, offset > 0 ? offset : void 0);
@@ -9233,7 +9714,7 @@ function formatCountResult(cwd, results, headLimit, offset) {
9233
9714
  }
9234
9715
  let text;
9235
9716
  if (finalCountLines.length === 0) {
9236
- text = "No matches found";
9717
+ text = emptyText;
9237
9718
  } else {
9238
9719
  const flat = finalCountLines.join("\n");
9239
9720
  const compressed = compressCount(finalCountLines);
@@ -9258,14 +9739,14 @@ Found ${totalMatches} total ${occurrenceLabel} across ${fileCount} ${fileLabel}.
9258
9739
  };
9259
9740
  return { text, details };
9260
9741
  }
9261
- function formatFilesResult(cwd, results, headLimit, offset) {
9742
+ function formatFilesResult(cwd, results, headLimit, offset, emptyText) {
9262
9743
  const sortedMatches = sortFilesByMtime(results);
9263
9744
  const { items: finalMatches, appliedLimit } = applyHeadLimit(sortedMatches, headLimit, offset);
9264
9745
  const relativeMatches = finalMatches.map((filePath) => toRelativePath(cwd, filePath));
9265
9746
  const limitInfo = formatLimitInfo(appliedLimit, offset > 0 ? offset : void 0);
9266
9747
  let text;
9267
9748
  if (relativeMatches.length === 0) {
9268
- text = "No files found";
9749
+ text = emptyText === "No matches found" ? "No files found" : emptyText.replace("No matches found", "No files found");
9269
9750
  } else {
9270
9751
  const fileLabel = relativeMatches.length === 1 ? "file" : "files";
9271
9752
  const flat = relativeMatches.join("\n");
@@ -9306,13 +9787,14 @@ function createRipGrepTool(cwd) {
9306
9787
  const offset = input.offset ?? 0;
9307
9788
  const args2 = buildRipgrepArgs({ ...input, output_mode: outputMode });
9308
9789
  const results = await runRipgrep(args2, searchPath, cwd);
9790
+ const emptyText = emptyMatchText(input);
9309
9791
  let formatted;
9310
9792
  if (outputMode === "content") {
9311
- formatted = formatContentResult(cwd, results, headLimit, offset);
9793
+ formatted = formatContentResult(cwd, results, headLimit, offset, emptyText);
9312
9794
  } else if (outputMode === "count") {
9313
- formatted = formatCountResult(cwd, results, headLimit, offset);
9795
+ formatted = formatCountResult(cwd, results, headLimit, offset, emptyText);
9314
9796
  } else {
9315
- formatted = formatFilesResult(cwd, results, headLimit, offset);
9797
+ formatted = formatFilesResult(cwd, results, headLimit, offset, emptyText);
9316
9798
  }
9317
9799
  const truncation = truncateHead(formatted.text);
9318
9800
  let output = truncation.content;
@@ -9351,7 +9833,7 @@ function createRipGrepTool(cwd) {
9351
9833
  }
9352
9834
  };
9353
9835
  }
9354
- var VCS_DIRECTORIES_TO_EXCLUDE, DEFAULT_HEAD_LIMIT, ripGrepSchema, log15, PATHLESS_CONTENT_LINE2, ripGrepTool;
9836
+ var VCS_DIRECTORIES_TO_EXCLUDE, DEFAULT_HEAD_LIMIT, ripGrepSchema, log15, PATHLESS_CONTENT_LINE2, REGEX_PARSE_ERROR_RE, REGEX_PARSE_HINT, ripGrepTool;
9355
9837
  var init_rip_grep = __esm({
9356
9838
  "src/tools/rip-grep.ts"() {
9357
9839
  "use strict";
@@ -9438,6 +9920,8 @@ var init_rip_grep = __esm({
9438
9920
  });
9439
9921
  log15 = createLogger("tools.rip-grep");
9440
9922
  PATHLESS_CONTENT_LINE2 = /^\d+[:-]/;
9923
+ REGEX_PARSE_ERROR_RE = /regex parse error/i;
9924
+ REGEX_PARSE_HINT = "Tip: grep patterns use ripgrep regex syntax. Escape special characters ($, {, }, (, ), [, ], *, +, ?, |, \\) with a backslash, or search for a simpler literal substring.";
9441
9925
  ripGrepTool = createRipGrepTool(process.cwd());
9442
9926
  }
9443
9927
  });
@@ -10780,11 +11264,49 @@ var init_ralph_progress = __esm({
10780
11264
 
10781
11265
  // src/tools/todo.ts
10782
11266
  import { Type as Type13 } from "@sinclair/typebox";
11267
+ function truncateTodoDescription(description, maxLength = 48) {
11268
+ const oneLine = description.replace(/\s+/g, " ").trim();
11269
+ if (oneLine.length <= maxLength) {
11270
+ return oneLine;
11271
+ }
11272
+ return `${oneLine.slice(0, maxLength - 1)}\u2026`;
11273
+ }
11274
+ function formatUpdatedTodosAck(items) {
11275
+ if (items.length === 1) {
11276
+ const item = items[0];
11277
+ return `Updated 1 todo: "${truncateTodoDescription(item.description)}" \u2192 ${item.status}`;
11278
+ }
11279
+ const parts = items.map(
11280
+ (item) => `"${truncateTodoDescription(item.description)}" \u2192 ${item.status}`
11281
+ );
11282
+ return `Updated ${items.length} todos: ${parts.join("; ")}`;
11283
+ }
11284
+ function formatDeletedTodosAck(items) {
11285
+ if (items.length === 1) {
11286
+ return `Deleted 1 todo: "${truncateTodoDescription(items[0].description)}"`;
11287
+ }
11288
+ const parts = items.map((item) => `"${truncateTodoDescription(item.description)}"`);
11289
+ return `Deleted ${items.length} todos: ${parts.join("; ")}`;
11290
+ }
11291
+ async function fetchTodosByIds(db, threadId, ids) {
11292
+ const rows = /* @__PURE__ */ new Map();
11293
+ for (const id of ids) {
11294
+ const result = await db.execute({
11295
+ sql: `SELECT id, description, status FROM todos WHERE id = ? AND threadId = ?`,
11296
+ args: [id, threadId]
11297
+ });
11298
+ const row = result.rows[0];
11299
+ if (row) {
11300
+ rows.set(id, row);
11301
+ }
11302
+ }
11303
+ return rows;
11304
+ }
10783
11305
  function createTodoTool(threadId, threadPath) {
10784
11306
  return {
10785
11307
  name: "todo",
10786
11308
  label: "todo",
10787
- description: "Manage todo items to break down the current task into 2 more items. Use 'add' to create items when planning work, 'update' to change an item's status as you progress (pending \u2192 working \u2192 done), 'list' to see all current items, and 'delete' to remove one. Marking an item done does not end the conversation \u2014 continue working on remaining items. Call 'list' at the end so the user sees a completion checklist.",
11309
+ description: "Manage todo items to break down the current task into 2 more items. Use 'add' to create items when planning work, 'update' to change an item's status as you progress (pending \u2192 working \u2192 done), 'list' to see all current items, and 'delete' to remove one. Marking an item done does not end the conversation \u2014 continue working on remaining items. Use 'list' when you need the full checklist; 'update' and 'delete' return a short confirmation only.",
10788
11310
  parameters: todoSchema,
10789
11311
  execute: async (_toolCallId, { action, descriptions, ids, status, assignedTo, passes, learnings }) => {
10790
11312
  const db = await getDatabase();
@@ -10818,16 +11340,22 @@ function createTodoTool(threadId, threadPath) {
10818
11340
  details: void 0
10819
11341
  };
10820
11342
  }
10821
- const notFound = [];
11343
+ const existingById = await fetchTodosByIds(db, threadId, ids);
11344
+ const notFound = ids.filter((id) => !existingById.has(id));
11345
+ if (notFound.length > 0) {
11346
+ return {
11347
+ content: [
11348
+ {
11349
+ type: "text",
11350
+ text: `Error: todos not found: ${notFound.join(", ")}`
11351
+ }
11352
+ ],
11353
+ details: void 0
11354
+ };
11355
+ }
11356
+ const updatedItems = [];
10822
11357
  for (const id of ids) {
10823
- const existing = await db.execute({
10824
- sql: `SELECT id FROM todos WHERE id = ? AND threadId = ?`,
10825
- args: [id, threadId]
10826
- });
10827
- if (existing.rows.length === 0) {
10828
- notFound.push(id);
10829
- continue;
10830
- }
11358
+ const existing = existingById.get(id);
10831
11359
  const updates = [];
10832
11360
  const args2 = [];
10833
11361
  if (status) {
@@ -10849,24 +11377,16 @@ function createTodoTool(threadId, threadPath) {
10849
11377
  sql: `UPDATE todos SET ${updates.join(", ")} WHERE id = ? AND threadId = ?`,
10850
11378
  args: args2
10851
11379
  });
11380
+ updatedItems.push({
11381
+ description: existing.description,
11382
+ status: status ?? existing.status
11383
+ });
10852
11384
  }
10853
- if (notFound.length > 0) {
10854
- return {
10855
- content: [
10856
- {
10857
- type: "text",
10858
- text: `Error: todos not found: ${notFound.join(", ")}`
10859
- }
10860
- ],
10861
- details: void 0
10862
- };
10863
- }
10864
- const todosAfterUpdate = await fetchAllTodos(db, threadId);
10865
11385
  return {
10866
11386
  content: [
10867
11387
  {
10868
11388
  type: "text",
10869
- text: JSON.stringify({ todos: todosAfterUpdate })
11389
+ text: formatUpdatedTodosAck(updatedItems)
10870
11390
  }
10871
11391
  ],
10872
11392
  details: void 0
@@ -10891,18 +11411,19 @@ function createTodoTool(threadId, threadPath) {
10891
11411
  details: void 0
10892
11412
  };
10893
11413
  }
11414
+ const existingById = await fetchTodosByIds(db, threadId, ids);
11415
+ const deletedItems = ids.map((id) => existingById.get(id)).filter((row) => row !== void 0).map((row) => ({ description: row.description }));
10894
11416
  for (const id of ids) {
10895
11417
  await db.execute({
10896
11418
  sql: `DELETE FROM todos WHERE id = ? AND threadId = ?`,
10897
11419
  args: [id, threadId]
10898
11420
  });
10899
11421
  }
10900
- const todosAfterDelete = await fetchAllTodos(db, threadId);
10901
11422
  return {
10902
11423
  content: [
10903
11424
  {
10904
11425
  type: "text",
10905
- text: JSON.stringify({ todos: todosAfterDelete })
11426
+ text: formatDeletedTodosAck(deletedItems)
10906
11427
  }
10907
11428
  ],
10908
11429
  details: void 0
@@ -11665,7 +12186,7 @@ var init_spend_record = __esm({
11665
12186
 
11666
12187
  // src/features/git/git.utils.ts
11667
12188
  import { existsSync as existsSync11, readFileSync as readFileSync4, statSync as statSync6 } from "fs";
11668
- import { isAbsolute as isAbsolute2, normalize as normalize2, resolve as resolve2, join as join10 } from "path";
12189
+ import { isAbsolute as isAbsolute3, normalize as normalize2, resolve as resolve2, join as join10 } from "path";
11669
12190
  async function resolveModelAndKey(provider, modelId, metadataManager) {
11670
12191
  const providerConfig = await resolveProviderConfig(provider);
11671
12192
  if (!providerConfig) return null;
@@ -11812,7 +12333,7 @@ Generate only the commit message, nothing else:`;
11812
12333
  }
11813
12334
  }
11814
12335
  function resolveThreadPath(repoPath) {
11815
- const base = isAbsolute2(repoPath) ? repoPath : resolve2(getDataDir(), repoPath);
12336
+ const base = isAbsolute3(repoPath) ? repoPath : resolve2(getDataDir(), repoPath);
11816
12337
  return normalize2(base);
11817
12338
  }
11818
12339
  async function getGitRoot(cwd) {
@@ -17675,7 +18196,7 @@ function createGetObservationTool(threadId, options) {
17675
18196
  return {
17676
18197
  name: "get_observation",
17677
18198
  label: "get_observation",
17678
- description: `Retrieve exact stored tool output by observation ID. Output is limited to ${OBSERVATION_RETRIEVAL_MAX_LINES} lines or ${formatSize(OBSERVATION_RETRIEVAL_MAX_BYTES)}. Use offset and limit to page through large observations. IDs are scoped to the current thread.`,
18199
+ description: `Retrieve exact stored tool output by observation ID. Output is limited to ${OBSERVATION_RETRIEVAL_MAX_LINES} lines or ${formatSize(OBSERVATION_RETRIEVAL_MAX_BYTES)}. Use offset and limit to page through large observations; offset counts lines within the stored output (see the preview's lines=N attribute), not line numbers shown inside grep results. IDs are scoped to the current thread.`,
17679
18200
  parameters: getObservationSchema,
17680
18201
  execute: async (_toolCallId, input) => {
17681
18202
  if (!isObservationId(input.id)) {
@@ -17692,7 +18213,7 @@ function createGetObservationTool(threadId, options) {
17692
18213
  const startIndex = offset - 1;
17693
18214
  if (startIndex >= lines.length) {
17694
18215
  throw new Error(
17695
- `Offset ${offset} is beyond end of observation (${stored.ref.lineCount} lines total)`
18216
+ `Offset ${offset} is beyond end of observation (${stored.ref.lineCount} lines total). Observation offset is 1-indexed within the stored tool output, not a source-file line number. Use offset=1 to read from the start.`
17696
18217
  );
17697
18218
  }
17698
18219
  const selected = lines.slice(startIndex, startIndex + limit).join("\n");
@@ -17743,7 +18264,7 @@ var init_get_observation = __esm({
17743
18264
  }),
17744
18265
  offset: Type23.Optional(
17745
18266
  Type23.Integer({
17746
- description: "Line number to start reading from (1-indexed)",
18267
+ description: "Line number to start reading within the stored tool output (1-indexed). Not a source-file line number from grep/read content.",
17747
18268
  minimum: 1
17748
18269
  })
17749
18270
  ),
@@ -20618,6 +21139,13 @@ function usesTailPreview(toolName) {
20618
21139
  const normalized = toolName.toLowerCase();
20619
21140
  return normalized === "bash" || normalized.includes("shell") || normalized.includes("terminal");
20620
21141
  }
21142
+ function observationRetrievalHint(toolName, ref, firstLine, previewEndLine, nextOffset) {
21143
+ const base = `[Full output stored as ${ref.id} (${formatSize(ref.byteLength)}, ${ref.lineCount} lines). Preview shows lines ${firstLine}-${previewEndLine}. Use get_observation with id="${ref.id}", offset=${nextOffset}, and limit=200 to retrieve exact output.`;
21144
+ if (toolName.toLowerCase() === "grep") {
21145
+ return `${base} Line numbers in grep output (e.g. :275:) are source-file lines; get_observation offset is relative to this stored output (lines=${ref.lineCount}).`;
21146
+ }
21147
+ return base;
21148
+ }
20621
21149
  function buildObservationPreview(fullContent, toolName, ref) {
20622
21150
  const options = {
20623
21151
  maxBytes: OBSERVATION_PREVIEW_MAX_BYTES - PREVIEW_METADATA_RESERVE_BYTES,
@@ -20626,13 +21154,14 @@ function buildObservationPreview(fullContent, toolName, ref) {
20626
21154
  const tail = usesTailPreview(toolName);
20627
21155
  const truncation = tail ? truncateTail(fullContent, options) : truncateHead(fullContent, options);
20628
21156
  const firstLine = tail ? Math.max(1, ref.lineCount - truncation.outputLines + 1) : 1;
21157
+ const previewEndLine = firstLine + Math.max(0, truncation.outputLines - 1);
20629
21158
  const nextOffset = tail ? 1 : truncation.outputLines + 1;
20630
21159
  const preview = [
20631
21160
  `<observation_preview id="${ref.id}" tool="${toolName}" bytes="${ref.byteLength}" lines="${ref.lineCount}">`,
20632
21161
  truncation.content,
20633
21162
  "</observation_preview>",
20634
21163
  "",
20635
- `[Full output stored as ${ref.id} (${formatSize(ref.byteLength)}, ${ref.lineCount} lines). Preview shows lines ${firstLine}-${firstLine + Math.max(0, truncation.outputLines - 1)}. Use get_observation with id="${ref.id}", offset=${nextOffset}, and limit=200 to retrieve exact output.]`
21164
+ observationRetrievalHint(toolName, ref, firstLine, previewEndLine, nextOffset)
20636
21165
  ].join("\n");
20637
21166
  return {
20638
21167
  content: preview,
@@ -20758,6 +21287,94 @@ function estimateMessageTokens(message) {
20758
21287
  }
20759
21288
  return 0;
20760
21289
  }
21290
+ function countAssistantTurns(messages) {
21291
+ let count = 0;
21292
+ for (const message of messages) {
21293
+ if (typeof message === "object" && message !== null && "role" in message && message.role === "assistant") {
21294
+ count += 1;
21295
+ }
21296
+ }
21297
+ return count;
21298
+ }
21299
+ function isUserMessage(message) {
21300
+ return typeof message === "object" && message !== null && "role" in message && message.role === "user";
21301
+ }
21302
+ function countUserMessages(messages) {
21303
+ let count = 0;
21304
+ for (const message of messages) {
21305
+ if (isUserMessage(message)) {
21306
+ count += 1;
21307
+ }
21308
+ }
21309
+ return count;
21310
+ }
21311
+ function findLastUserMessageIndex(messages) {
21312
+ for (let i = messages.length - 1; i >= 0; i--) {
21313
+ if (isUserMessage(messages[i])) {
21314
+ return i;
21315
+ }
21316
+ }
21317
+ return -1;
21318
+ }
21319
+ function isCompactedToolResultText(text) {
21320
+ return text.startsWith(MICROCOMPACT_MARKER_PREFIX) || text.startsWith(TODO_DUPLICATE_OMITTED_PREFIX);
21321
+ }
21322
+ async function digestToolResultMessage(original, text, options, memo) {
21323
+ const memoized = memo.get(original.toolCallId);
21324
+ if (memoized) {
21325
+ return { compacted: memoized, observationIdsReused: 0, observationIdsCreated: 0 };
21326
+ }
21327
+ let observation = findObservationRef(original);
21328
+ let observationIdsReused = 0;
21329
+ let observationIdsCreated = 0;
21330
+ if (observation) {
21331
+ observationIdsReused = 1;
21332
+ } else {
21333
+ const created = await maybeExternalizeForDigest(original, text, options);
21334
+ if (created) {
21335
+ observation = created;
21336
+ observationIdsCreated = 1;
21337
+ }
21338
+ }
21339
+ const digest = observation ? buildObservationDigest(original.toolName, observation, original.isError) : buildHeuristicDigest(original.toolName, text, original.isError);
21340
+ const compacted = replaceToolResultContent(original, digest, observation);
21341
+ memo.set(original.toolCallId, compacted);
21342
+ return { compacted, observationIdsReused, observationIdsCreated };
21343
+ }
21344
+ function deduplicateIdenticalTodoResults(messages) {
21345
+ const lastIndexByContent = /* @__PURE__ */ new Map();
21346
+ for (let i = 0; i < messages.length; i++) {
21347
+ const message = messages[i];
21348
+ if (!isToolResultMessage(message) || message.toolName !== "todo") {
21349
+ continue;
21350
+ }
21351
+ const text = extractTextContent(message.content);
21352
+ if (text.length > 0) {
21353
+ lastIndexByContent.set(text, i);
21354
+ }
21355
+ }
21356
+ const next = messages.slice();
21357
+ let changed = false;
21358
+ for (let i = 0; i < next.length; i++) {
21359
+ const message = next[i];
21360
+ if (!isToolResultMessage(message) || message.toolName !== "todo") {
21361
+ continue;
21362
+ }
21363
+ const text = extractTextContent(message.content);
21364
+ const keepIndex = lastIndexByContent.get(text);
21365
+ if (keepIndex === void 0 || keepIndex === i) {
21366
+ continue;
21367
+ }
21368
+ const keptId = messages[keepIndex].toolCallId;
21369
+ next[i] = replaceToolResultContent(
21370
+ message,
21371
+ `${TODO_DUPLICATE_OMITTED_PREFIX}${keptId}]`,
21372
+ void 0
21373
+ );
21374
+ changed = true;
21375
+ }
21376
+ return changed ? next : messages;
21377
+ }
20761
21378
  function findObservationRef(message) {
20762
21379
  if (isObservationRef(message.details?.observation)) {
20763
21380
  return message.details.observation;
@@ -20887,6 +21504,9 @@ async function microcompactMessages(messages, options = {}) {
20887
21504
  const triggerMargin = options.triggerMargin ?? MICROCOMPACT_TRIGGER_MARGIN;
20888
21505
  const minSaveTokens = options.minSaveTokens ?? MICROCOMPACT_MIN_SAVE_TOKENS;
20889
21506
  const digestMinChars = options.digestMinChars ?? MICROCOMPACT_DIGEST_MIN_CHARS;
21507
+ const proactiveTurnThreshold = options.proactiveTurnThreshold ?? MICROCOMPACT_PROACTIVE_TURN_THRESHOLD;
21508
+ const proactivePayloadTokens = options.proactivePayloadTokens ?? MICROCOMPACT_PROACTIVE_PAYLOAD_TOKENS;
21509
+ const proactiveMinSaveTokens = options.proactiveMinSaveTokens ?? MICROCOMPACT_PROACTIVE_MIN_SAVE_TOKENS;
20890
21510
  const effectiveWindow = computeEffectiveContextWindow(
20891
21511
  options.contextWindow,
20892
21512
  options.maxOutputTokens
@@ -20894,12 +21514,16 @@ async function microcompactMessages(messages, options = {}) {
20894
21514
  const messageTokens = messages.reduce((sum, message) => sum + estimateMessageTokens(message), 0);
20895
21515
  const fixedOverheadTokens = typeof options.fixedOverheadTokens === "number" && options.fixedOverheadTokens > 0 ? options.fixedOverheadTokens : 0;
20896
21516
  const tokensBefore = messageTokens + fixedOverheadTokens;
20897
- if (tokensBefore < effectiveWindow - triggerMargin) {
21517
+ const agentTurnCount = countAssistantTurns(messages);
21518
+ const proactiveTrigger = agentTurnCount >= proactiveTurnThreshold || tokensBefore >= proactivePayloadTokens;
21519
+ const nearWindowCeiling = tokensBefore >= effectiveWindow - triggerMargin;
21520
+ if (!nearWindowCeiling && !proactiveTrigger) {
20898
21521
  return {
20899
21522
  messages,
20900
21523
  stats: { ...emptyStats, tokensBefore, tokensAfter: tokensBefore }
20901
21524
  };
20902
21525
  }
21526
+ const effectiveMinSaveTokens = proactiveTrigger && !nearWindowCeiling ? proactiveMinSaveTokens : minSaveTokens;
20903
21527
  const eligibleIndexes = [];
20904
21528
  for (let i = 0; i < messages.length; i++) {
20905
21529
  const message = messages[i];
@@ -20943,7 +21567,7 @@ async function microcompactMessages(messages, options = {}) {
20943
21567
  for (const index of clearCandidates) {
20944
21568
  clearableTokens += estimateMessageTokens(messages[index]);
20945
21569
  }
20946
- if (clearableTokens < minSaveTokens || clearCandidates.length === 0) {
21570
+ if (clearableTokens < effectiveMinSaveTokens || clearCandidates.length === 0) {
20947
21571
  return {
20948
21572
  messages,
20949
21573
  stats: {
@@ -20961,26 +21585,14 @@ async function microcompactMessages(messages, options = {}) {
20961
21585
  let clearedCount = 0;
20962
21586
  for (const index of clearCandidates) {
20963
21587
  const original = messages[index];
20964
- const memoized = memo.get(original.toolCallId);
20965
- if (memoized) {
20966
- next[index] = memoized;
20967
- clearedCount += 1;
20968
- continue;
20969
- }
20970
21588
  const text = extractTextContent(original.content);
20971
- let observation = findObservationRef(original);
20972
- if (observation) {
20973
- observationIdsReused += 1;
20974
- } else {
20975
- const created = await maybeExternalizeForDigest(original, text, options);
20976
- if (created) {
20977
- observation = created;
20978
- observationIdsCreated += 1;
20979
- }
20980
- }
20981
- const digest = observation ? buildObservationDigest(original.toolName, observation, original.isError) : buildHeuristicDigest(original.toolName, text, original.isError);
20982
- const compacted = replaceToolResultContent(original, digest, observation);
20983
- memo.set(original.toolCallId, compacted);
21589
+ const {
21590
+ compacted,
21591
+ observationIdsReused: reused,
21592
+ observationIdsCreated: created
21593
+ } = await digestToolResultMessage(original, text, options, memo);
21594
+ observationIdsReused += reused;
21595
+ observationIdsCreated += created;
20984
21596
  next[index] = compacted;
20985
21597
  clearedCount += 1;
20986
21598
  }
@@ -21002,19 +21614,98 @@ async function microcompactMessages(messages, options = {}) {
21002
21614
  }
21003
21615
  return { messages: next, stats };
21004
21616
  }
21617
+ async function prunePriorTurnToolResults(messages, options = {}) {
21618
+ const emptyStats = {
21619
+ eligibleCount: 0,
21620
+ keptCount: 0,
21621
+ clearedCount: 0,
21622
+ tokensBefore: 0,
21623
+ tokensAfter: 0,
21624
+ tokensSaved: 0,
21625
+ observationIdsReused: 0,
21626
+ observationIdsCreated: 0,
21627
+ applied: false
21628
+ };
21629
+ if (!areMicrocompactEnabled()) {
21630
+ return { messages, stats: emptyStats };
21631
+ }
21632
+ if (countUserMessages(messages) < 2) {
21633
+ return { messages, stats: emptyStats };
21634
+ }
21635
+ const lastUserIndex = findLastUserMessageIndex(messages);
21636
+ if (lastUserIndex <= 0) {
21637
+ return { messages, stats: emptyStats };
21638
+ }
21639
+ const memo = options.memo ?? /* @__PURE__ */ new Map();
21640
+ const digestMinChars = options.digestMinChars ?? MICROCOMPACT_DIGEST_MIN_CHARS;
21641
+ const next = messages.slice();
21642
+ let observationIdsReused = 0;
21643
+ let observationIdsCreated = 0;
21644
+ let clearedCount = 0;
21645
+ let eligibleCount = 0;
21646
+ for (let index = 0; index < lastUserIndex; index++) {
21647
+ const message = messages[index];
21648
+ if (!isToolResultMessage(message) || message.isError) {
21649
+ continue;
21650
+ }
21651
+ if (!isMicrocompactEligibleTool(message.toolName)) {
21652
+ continue;
21653
+ }
21654
+ const text = extractTextContent(message.content);
21655
+ if (text.length < digestMinChars || isCompactedToolResultText(text)) {
21656
+ continue;
21657
+ }
21658
+ eligibleCount += 1;
21659
+ const {
21660
+ compacted,
21661
+ observationIdsReused: reused,
21662
+ observationIdsCreated: created
21663
+ } = await digestToolResultMessage(message, text, options, memo);
21664
+ observationIdsReused += reused;
21665
+ observationIdsCreated += created;
21666
+ next[index] = compacted;
21667
+ clearedCount += 1;
21668
+ }
21669
+ if (clearedCount === 0) {
21670
+ return { messages, stats: emptyStats };
21671
+ }
21672
+ const messagesTokensBefore = messages.reduce(
21673
+ (sum, message) => sum + estimateMessageTokens(message),
21674
+ 0
21675
+ );
21676
+ const messagesTokensAfter = next.reduce(
21677
+ (sum, message) => sum + estimateMessageTokens(message),
21678
+ 0
21679
+ );
21680
+ const stats = {
21681
+ eligibleCount,
21682
+ keptCount: 0,
21683
+ clearedCount,
21684
+ tokensBefore: messagesTokensBefore,
21685
+ tokensAfter: messagesTokensAfter,
21686
+ tokensSaved: Math.max(0, messagesTokensBefore - messagesTokensAfter),
21687
+ observationIdsReused,
21688
+ observationIdsCreated,
21689
+ applied: true
21690
+ };
21691
+ log49.info("Applied prior-turn tool-result prune", stats);
21692
+ return { messages: next, stats };
21693
+ }
21005
21694
  function createMicrocompactTransformContext(options) {
21006
21695
  const memo = options.memo ?? /* @__PURE__ */ new Map();
21007
21696
  return async function transformContext(messages) {
21008
21697
  try {
21009
- const result = await microcompactMessages(messages, { ...options, memo });
21698
+ const deduped = deduplicateIdenticalTodoResults(messages);
21699
+ const pruned = await prunePriorTurnToolResults(deduped, { ...options, memo });
21700
+ const result = await microcompactMessages(pruned.messages, { ...options, memo });
21010
21701
  return result.messages;
21011
21702
  } catch (error) {
21012
21703
  log49.warn("Micro-compaction failed; sending original context", { error });
21013
- return messages;
21704
+ return deduplicateIdenticalTodoResults(messages);
21014
21705
  }
21015
21706
  };
21016
21707
  }
21017
- var log49, OBSERVATION_ID_IN_TEXT;
21708
+ var log49, OBSERVATION_ID_IN_TEXT, TODO_DUPLICATE_OMITTED_PREFIX;
21018
21709
  var init_microcompact2 = __esm({
21019
21710
  "src/features/microcompact/microcompact.ts"() {
21020
21711
  "use strict";
@@ -21024,6 +21715,7 @@ var init_microcompact2 = __esm({
21024
21715
  init_microcompact_config();
21025
21716
  log49 = createLogger("microcompact");
21026
21717
  OBSERVATION_ID_IN_TEXT = /obs_[a-f0-9]{64}/;
21718
+ TODO_DUPLICATE_OMITTED_PREFIX = "[Duplicate todo result omitted; identical to toolCallId=";
21027
21719
  }
21028
21720
  });
21029
21721
 
@@ -21833,7 +22525,7 @@ import {
21833
22525
  InMemorySessionRepo as InMemorySessionRepo2
21834
22526
  } from "@earendil-works/pi-agent-core";
21835
22527
  import { homedir as homedir6 } from "node:os";
21836
- import { resolve as resolve4, isAbsolute as isAbsolute3 } from "path";
22528
+ import { resolve as resolve4, isAbsolute as isAbsolute4 } from "path";
21837
22529
  function getAssistantHistoryContent(exchange, isMostRecent) {
21838
22530
  return resolveAssistantHistoryContent(exchange, isMostRecent);
21839
22531
  }
@@ -22023,7 +22715,7 @@ var init_agent_executor = __esm({
22023
22715
  if (!model) {
22024
22716
  throw new Error("No model specified in execution context");
22025
22717
  }
22026
- const cwd = isAbsolute3(context.threadPath) ? context.threadPath : resolve4(getDataDir(), context.threadPath);
22718
+ const cwd = isAbsolute4(context.threadPath) ? context.threadPath : resolve4(getDataDir(), context.threadPath);
22027
22719
  log52.info("Execution context", {
22028
22720
  model,
22029
22721
  cwd,
@@ -22241,6 +22933,11 @@ var init_agent_executor = __esm({
22241
22933
  });
22242
22934
  if (microcompactTransform) {
22243
22935
  harness.on("context", createHarnessContextHandler(microcompactTransform));
22936
+ } else {
22937
+ harness.on(
22938
+ "context",
22939
+ createHarnessContextHandler((messages) => deduplicateIdenticalTodoResults(messages))
22940
+ );
22244
22941
  }
22245
22942
  harness.on("before_provider_payload", (event) => {
22246
22943
  recordPromptCacheFingerprint(
@@ -24967,8 +25664,23 @@ async function resolveSeatbeltSandboxEnabled(db) {
24967
25664
  }
24968
25665
  return true;
24969
25666
  }
24970
- function buildAccountSettingsResponse(rawMaximumTurnCount, rawUseToolsThroughCode, rawVoiceAiProcessing, rawBetterStackLogging, rawFileLogging, rawShowDailySpend, rawSeatbeltSandboxEnabled, serverBindMode) {
25667
+ function isLoggingEnabled(rawLogging, rawFileLogging, rawBetterStackLogging) {
25668
+ if (rawLogging !== null && rawLogging !== void 0) {
25669
+ return rawLogging === true;
25670
+ }
25671
+ return rawFileLogging === true || rawBetterStackLogging === true;
25672
+ }
25673
+ async function applyLoggingEnabled(db, enabled) {
25674
+ await setState(db, KEY_LOGGING, enabled);
25675
+ await setState(db, KEY_FILE_LOGGING, enabled);
25676
+ await setState(db, KEY_BETTERSTACK_LOGGING, enabled);
25677
+ setFileLoggingEnabledByUser(enabled);
25678
+ setBetterStackLoggingEnabledByUser(enabled);
25679
+ syncRuntimeLogLevel(isCliDebugMode());
25680
+ }
25681
+ function buildAccountSettingsResponse(rawMaximumTurnCount, rawUseToolsThroughCode, rawVoiceAiProcessing, rawLogging, rawFileLogging, rawBetterStackLogging, rawShowDailySpend, rawSeatbeltSandboxEnabled, serverBindMode) {
24971
25682
  const betterStackEnvConfigured = isBetterStackEnvConfigured();
25683
+ const loggingEnabled = isLoggingEnabled(rawLogging, rawFileLogging, rawBetterStackLogging);
24972
25684
  const seatbeltHealth = checkSeatbeltSandboxHealth();
24973
25685
  const seatbeltPreferenceEnabled = isSeatbeltSandboxPreferenceEnabled(rawSeatbeltSandboxEnabled);
24974
25686
  const seatbeltSandboxEnabled = seatbeltPreferenceEnabled && seatbeltHealth.healthy;
@@ -24981,11 +25693,12 @@ function buildAccountSettingsResponse(rawMaximumTurnCount, rawUseToolsThroughCod
24981
25693
  seatbeltSandboxSupported: seatbeltHealth.supported,
24982
25694
  seatbeltSandboxHealthy: seatbeltHealth.healthy,
24983
25695
  seatbeltSandboxMessage: seatbeltHealth.message,
24984
- betterStackLoggingEnabled: rawBetterStackLogging === true,
25696
+ betterStackLoggingEnabled: loggingEnabled,
24985
25697
  betterStackEnvConfigured,
24986
25698
  betterStackIngestingHost: betterStackEnvConfigured ? getBetterStackIngestingHost() : null,
24987
- fileLoggingEnabled: rawFileLogging === true,
25699
+ fileLoggingEnabled: loggingEnabled,
24988
25700
  fileLoggingPath: getFileLoggingPath(),
25701
+ loggingEnabled,
24989
25702
  serverBindMode,
24990
25703
  networkAddresses: serverBindMode === "network" ? getLocalNetworkAddresses() : []
24991
25704
  };
@@ -25015,10 +25728,16 @@ async function getServerBindModeSetting() {
25015
25728
  }
25016
25729
  async function initializeLoggingSettings() {
25017
25730
  const db = await getDatabase();
25731
+ const rawLogging = await getState(db, KEY_LOGGING);
25018
25732
  const rawBetterStackLogging = await getState(db, KEY_BETTERSTACK_LOGGING);
25019
25733
  const rawFileLogging = await getState(db, KEY_FILE_LOGGING);
25020
- setBetterStackLoggingEnabledByUser(rawBetterStackLogging === true);
25021
- setFileLoggingEnabledByUser(rawFileLogging === true);
25734
+ const loggingEnabled = isLoggingEnabled(rawLogging, rawFileLogging, rawBetterStackLogging);
25735
+ if (loggingEnabled && rawLogging !== true) {
25736
+ await applyLoggingEnabled(db, true);
25737
+ return;
25738
+ }
25739
+ setBetterStackLoggingEnabledByUser(loggingEnabled);
25740
+ setFileLoggingEnabledByUser(loggingEnabled);
25022
25741
  syncRuntimeLogLevel(isCliDebugMode());
25023
25742
  }
25024
25743
  async function getAccountSettings(c) {
@@ -25028,6 +25747,7 @@ async function getAccountSettings(c) {
25028
25747
  const rawMaximumTurnCount = await getState(db, KEY_MAXIMUM_TURN_COUNT);
25029
25748
  const rawBetterStackLogging = await getState(db, KEY_BETTERSTACK_LOGGING);
25030
25749
  const rawFileLogging = await getState(db, KEY_FILE_LOGGING);
25750
+ const rawLogging = await getState(db, KEY_LOGGING);
25031
25751
  const rawShowDailySpend = await getState(db, KEY_SHOW_DAILY_SPEND);
25032
25752
  let rawSeatbeltSandboxEnabled = await getState(db, KEY_SEATBELT_SANDBOX_ENABLED);
25033
25753
  const serverBindMode = await getServerBindModeSetting();
@@ -25040,8 +25760,9 @@ async function getAccountSettings(c) {
25040
25760
  rawMaximumTurnCount,
25041
25761
  rawUseToolsThroughCode,
25042
25762
  rawVoiceAiProcessing,
25043
- rawBetterStackLogging,
25763
+ rawLogging,
25044
25764
  rawFileLogging,
25765
+ rawBetterStackLogging,
25045
25766
  rawShowDailySpend,
25046
25767
  rawSeatbeltSandboxEnabled,
25047
25768
  serverBindMode
@@ -25078,15 +25799,9 @@ async function updateAccountSettings(c) {
25078
25799
  }
25079
25800
  await setState(db, KEY_SEATBELT_SANDBOX_ENABLED, body.seatbeltSandboxEnabled);
25080
25801
  }
25081
- if (typeof body.betterStackLoggingEnabled === "boolean") {
25082
- await setState(db, KEY_BETTERSTACK_LOGGING, body.betterStackLoggingEnabled);
25083
- setBetterStackLoggingEnabledByUser(body.betterStackLoggingEnabled);
25084
- syncRuntimeLogLevel(isCliDebugMode());
25085
- }
25086
- if (typeof body.fileLoggingEnabled === "boolean") {
25087
- await setState(db, KEY_FILE_LOGGING, body.fileLoggingEnabled);
25088
- setFileLoggingEnabledByUser(body.fileLoggingEnabled);
25089
- syncRuntimeLogLevel(isCliDebugMode());
25802
+ const loggingEnabledUpdate = typeof body.loggingEnabled === "boolean" ? body.loggingEnabled : typeof body.fileLoggingEnabled === "boolean" ? body.fileLoggingEnabled : typeof body.betterStackLoggingEnabled === "boolean" ? body.betterStackLoggingEnabled : void 0;
25803
+ if (typeof loggingEnabledUpdate === "boolean") {
25804
+ await applyLoggingEnabled(db, loggingEnabledUpdate);
25090
25805
  }
25091
25806
  if (body.maximumTurnCount !== void 0) {
25092
25807
  await setState(db, KEY_MAXIMUM_TURN_COUNT, normalizeMaximumTurnCount(body.maximumTurnCount));
@@ -25099,6 +25814,7 @@ async function updateAccountSettings(c) {
25099
25814
  const rawMaximumTurnCount = await getState(db, KEY_MAXIMUM_TURN_COUNT);
25100
25815
  const rawBetterStackLogging = await getState(db, KEY_BETTERSTACK_LOGGING);
25101
25816
  const rawFileLogging = await getState(db, KEY_FILE_LOGGING);
25817
+ const rawLogging = await getState(db, KEY_LOGGING);
25102
25818
  const rawShowDailySpend = await getState(db, KEY_SHOW_DAILY_SPEND);
25103
25819
  const rawSeatbeltSandboxEnabled = await getState(db, KEY_SEATBELT_SANDBOX_ENABLED);
25104
25820
  const serverBindMode = readServerBindMode();
@@ -25107,15 +25823,16 @@ async function updateAccountSettings(c) {
25107
25823
  rawMaximumTurnCount,
25108
25824
  rawUseToolsThroughCode,
25109
25825
  rawVoiceAiProcessing,
25110
- rawBetterStackLogging,
25826
+ rawLogging,
25111
25827
  rawFileLogging,
25828
+ rawBetterStackLogging,
25112
25829
  rawShowDailySpend,
25113
25830
  rawSeatbeltSandboxEnabled,
25114
25831
  serverBindMode
25115
25832
  )
25116
25833
  );
25117
25834
  }
25118
- var DEFAULT_MAXIMUM_TURN_COUNT, KEY_MAXIMUM_TURN_COUNT, KEY_USE_TOOLS_THROUGH_CODE, KEY_VOICE_AI_PROCESSING, KEY_BETTERSTACK_LOGGING, KEY_FILE_LOGGING, KEY_SHOW_DAILY_SPEND, KEY_SEATBELT_SANDBOX_ENABLED, LEGACY_KEY_SERVER_BIND_MODE;
25835
+ var DEFAULT_MAXIMUM_TURN_COUNT, KEY_MAXIMUM_TURN_COUNT, KEY_USE_TOOLS_THROUGH_CODE, KEY_VOICE_AI_PROCESSING, KEY_BETTERSTACK_LOGGING, KEY_FILE_LOGGING, KEY_LOGGING, KEY_SHOW_DAILY_SPEND, KEY_SEATBELT_SANDBOX_ENABLED, LEGACY_KEY_SERVER_BIND_MODE;
25119
25836
  var init_account_settings_route = __esm({
25120
25837
  "src/features/account/account-settings.route.ts"() {
25121
25838
  "use strict";
@@ -25134,6 +25851,7 @@ var init_account_settings_route = __esm({
25134
25851
  KEY_VOICE_AI_PROCESSING = "VoiceAiProcessing";
25135
25852
  KEY_BETTERSTACK_LOGGING = "BetterStackLogging";
25136
25853
  KEY_FILE_LOGGING = "FileLogging";
25854
+ KEY_LOGGING = "Logging";
25137
25855
  KEY_SHOW_DAILY_SPEND = "ShowDailySpend";
25138
25856
  KEY_SEATBELT_SANDBOX_ENABLED = "SeatbeltSandboxEnabled";
25139
25857
  LEGACY_KEY_SERVER_BIND_MODE = "ServerBindMode";
@@ -30421,7 +31139,7 @@ var init_ai_files_route_helpers = __esm({
30421
31139
 
30422
31140
  // src/features/projects/projects-ai-files.route.ts
30423
31141
  import { readdir as readdir6 } from "fs/promises";
30424
- import { join as join28, extname as extname3, basename } from "path";
31142
+ import { join as join28, extname as extname3, basename as basename2 } from "path";
30425
31143
  import { existsSync as existsSync20 } from "fs";
30426
31144
  async function buildFullTree(dirPath, relativeDirPath) {
30427
31145
  const nodes = [];
@@ -30474,7 +31192,7 @@ async function buildMarkdownFilteredTree(dirPath, relativeDirPath) {
30474
31192
  } else if (entry.isFile() && MARKDOWN_EXTS.has(extname3(entry.name))) {
30475
31193
  nodes.push({
30476
31194
  id: entryRelPath,
30477
- name: basename(entry.name, extname3(entry.name)),
31195
+ name: basename2(entry.name, extname3(entry.name)),
30478
31196
  type: "file",
30479
31197
  path: entryRelPath
30480
31198
  });
@@ -30533,7 +31251,7 @@ async function buildAIFileTree(projectPath) {
30533
31251
  if (entry.isFile() && MARKDOWN_EXTS.has(extname3(entry.name))) {
30534
31252
  nodes.push({
30535
31253
  id: entryRelPath,
30536
- name: basename(entry.name, extname3(entry.name)),
31254
+ name: basename2(entry.name, extname3(entry.name)),
30537
31255
  type: "file",
30538
31256
  path: entryRelPath
30539
31257
  });
@@ -32344,7 +33062,7 @@ var init_random_words = __esm({
32344
33062
 
32345
33063
  // src/features/projects/projects.creator.ts
32346
33064
  import { randomUUID as randomUUID13 } from "crypto";
32347
- import { basename as basename2, join as join30, relative as relative4, resolve as resolvePath2 } from "path";
33065
+ import { basename as basename3, join as join30, relative as relative4, resolve as resolvePath2 } from "path";
32348
33066
  import { realpathSync as realpathSync2 } from "fs";
32349
33067
  import { access as access4, mkdir as mkdir5, rm as rm3, stat as stat4, readdir as readdir8 } from "fs/promises";
32350
33068
  async function loadUtils() {
@@ -32844,7 +33562,7 @@ var init_projects_creator = __esm({
32844
33562
  try {
32845
33563
  const existingNames = existingProjects.map((p) => p.name);
32846
33564
  const projectName = this.ensureUniqueProjectName(
32847
- basename2(normalizedPath) || "project",
33565
+ basename3(normalizedPath) || "project",
32848
33566
  existingNames
32849
33567
  );
32850
33568
  yield {
@@ -34611,7 +35329,7 @@ var init_scaffold_routes = __esm({
34611
35329
 
34612
35330
  // src/features/slash-commands/slash-commands.manager.ts
34613
35331
  import { readdir as readdir9, readFile as readFile13, mkdir as mkdir6, writeFile as writeFile6, unlink as unlink4 } from "fs/promises";
34614
- import { join as join33, basename as basename3, extname as extname4, relative as relative5 } from "path";
35332
+ import { join as join33, basename as basename4, extname as extname4, relative as relative5 } from "path";
34615
35333
  import { existsSync as existsSync22 } from "fs";
34616
35334
  import { homedir as homedir9 } from "os";
34617
35335
  function slugify(filename) {
@@ -34739,7 +35457,7 @@ var init_slash_commands_manager = __esm({
34739
35457
  const filePath = join33(dir, file);
34740
35458
  const fileContent = await readFile13(filePath, "utf-8");
34741
35459
  const { metadata, content } = parseFrontmatter2(fileContent);
34742
- const nameWithoutExt = basename3(file, extname4(file));
35460
+ const nameWithoutExt = basename4(file, extname4(file));
34743
35461
  const commandName = slugify(nameWithoutExt);
34744
35462
  if (!commandName) continue;
34745
35463
  commands.push({
@@ -35440,7 +36158,7 @@ var init_rules_routes = __esm({
35440
36158
  });
35441
36159
 
35442
36160
  // src/features/threads/thread-folder-name.ts
35443
- import { basename as basename4 } from "path";
36161
+ import { basename as basename5 } from "path";
35444
36162
  function normalizeThreadFolderName(input) {
35445
36163
  return input.trim().toLowerCase().replace(/[\\/]+/g, "-").replace(/[^a-z0-9._-]+/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "").slice(0, 120);
35446
36164
  }
@@ -35470,7 +36188,7 @@ function deriveRepoSlugFromGitUrl(gitUrl) {
35470
36188
  if (segments.length === 1) {
35471
36189
  return normalizeThreadFolderName(segments[0]);
35472
36190
  }
35473
- return normalizeThreadFolderName(basename4(trimmed));
36191
+ return normalizeThreadFolderName(basename5(trimmed));
35474
36192
  }
35475
36193
  var init_thread_folder_name = __esm({
35476
36194
  "src/features/threads/thread-folder-name.ts"() {
@@ -36214,7 +36932,7 @@ var init_project_scripts_database = __esm({
36214
36932
  // src/core/project-inspector.ts
36215
36933
  import { readFile as readFile14, readdir as readdir10 } from "fs/promises";
36216
36934
  import { existsSync as existsSync23 } from "fs";
36217
- import { join as join35, basename as basename5, relative as relative6 } from "path";
36935
+ import { join as join35, basename as basename6, relative as relative6 } from "path";
36218
36936
  import { glob as glob2 } from "glob";
36219
36937
  async function inspectProject(projectPath, projectId) {
36220
36938
  const packageManager = await detectPackageManager2(projectPath);
@@ -36309,7 +37027,7 @@ async function collectWorkspacesFromGlobs(projectPath, patterns, options = {}) {
36309
37027
  for (const folder of folders) {
36310
37028
  const absPath = join35(projectPath, folder);
36311
37029
  if (!existsSync23(join35(absPath, "package.json"))) continue;
36312
- let name = basename5(folder);
37030
+ let name = basename6(folder);
36313
37031
  if (options.preferPackageName) {
36314
37032
  const wsPkg = await readPackageJson(absPath);
36315
37033
  name = wsPkg?.name ?? name;
@@ -36752,7 +37470,7 @@ var init_threads_files_route = __esm({
36752
37470
 
36753
37471
  // src/features/threads/threads-explorer.route.ts
36754
37472
  import { readdir as readdir11, readFile as readFile15, rename as rename3, writeFile as writeFile7, mkdir as mkdir8, access as access5, open as open2, stat as stat6, rm as rm6 } from "fs/promises";
36755
- import { isAbsolute as isAbsolute4, join as join36, resolve as resolve5, basename as basename6 } from "path";
37473
+ import { isAbsolute as isAbsolute5, join as join36, resolve as resolve5, basename as basename7 } from "path";
36756
37474
  import { spawn as spawn2 } from "child_process";
36757
37475
  import openFilePath from "open";
36758
37476
  async function loadUtils2() {
@@ -37068,7 +37786,7 @@ async function handlePrintMarkdownExplorerFile(c, threadManager) {
37068
37786
  const exportDir = join36(thread.path, ".tarsk", "exports");
37069
37787
  await mkdir8(exportDir, { recursive: true });
37070
37788
  const timestamp = (/* @__PURE__ */ new Date()).toISOString().replaceAll(":", "-");
37071
- const safeBaseName = basename6(filePath, ".md").replace(/[^a-zA-Z0-9._-]+/g, "-") || "document";
37789
+ const safeBaseName = basename7(filePath, ".md").replace(/[^a-zA-Z0-9._-]+/g, "-") || "document";
37072
37790
  const outputPath = join36(exportDir, `${safeBaseName}-${timestamp}.pdf`);
37073
37791
  await runMarkdownToPdf(absPath, outputPath);
37074
37792
  await openExportPath(outputPath);
@@ -37131,7 +37849,7 @@ async function handleCreateExplorerFile(c, threadManager) {
37131
37849
  }
37132
37850
  }
37133
37851
  function resolveExplorerMediaAbsolutePath(threadPath, filePath) {
37134
- if (isAbsolute4(filePath)) {
37852
+ if (isAbsolute5(filePath)) {
37135
37853
  const normalized = resolve5(filePath);
37136
37854
  const dataDir = resolve5(getDataDir());
37137
37855
  if ((normalized === dataDir || normalized.startsWith(`${dataDir}/`)) && getExplorerFileMediaType(normalized)) {