@qwen-code/arena-agent 0.0.10-dev.3 → 0.0.10-dev.5

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 (56) hide show
  1. package/.env.example +8 -1
  2. package/README.md +25 -12
  3. package/dist/agent.d.ts +4 -0
  4. package/dist/agent.js +19 -6
  5. package/dist/agent.js.map +1 -1
  6. package/dist/brave-search.js +1 -3
  7. package/dist/brave-search.js.map +1 -1
  8. package/dist/canonical-data.js +1 -1
  9. package/dist/canonical-data.js.map +1 -1
  10. package/dist/cli.js +5 -3
  11. package/dist/cli.js.map +1 -1
  12. package/dist/code-tools.d.ts +1 -1
  13. package/dist/code-tools.js +340 -50
  14. package/dist/code-tools.js.map +1 -1
  15. package/dist/compaction.d.ts +29 -2
  16. package/dist/compaction.js +242 -187
  17. package/dist/compaction.js.map +1 -1
  18. package/dist/config.d.ts +1 -0
  19. package/dist/config.js +2 -1
  20. package/dist/config.js.map +1 -1
  21. package/dist/fetch-page.js +9 -33
  22. package/dist/fetch-page.js.map +1 -1
  23. package/dist/file-edit.d.ts +6 -0
  24. package/dist/file-edit.js +58 -0
  25. package/dist/file-edit.js.map +1 -0
  26. package/dist/history.d.ts +6 -3
  27. package/dist/history.js +42 -11
  28. package/dist/history.js.map +1 -1
  29. package/dist/legacy-compaction.d.ts +16 -0
  30. package/dist/legacy-compaction.js +207 -0
  31. package/dist/legacy-compaction.js.map +1 -0
  32. package/dist/mock.js +5 -0
  33. package/dist/mock.js.map +1 -1
  34. package/dist/process-manager.js +2 -1
  35. package/dist/process-manager.js.map +1 -1
  36. package/dist/prompts.d.ts +2 -0
  37. package/dist/prompts.js +6 -2
  38. package/dist/prompts.js.map +1 -1
  39. package/dist/routify-web-fetch.js +2 -3
  40. package/dist/routify-web-fetch.js.map +1 -1
  41. package/dist/sandbox-file-write.d.ts +10 -0
  42. package/dist/sandbox-file-write.js +58 -0
  43. package/dist/sandbox-file-write.js.map +1 -0
  44. package/dist/server.js +8 -5
  45. package/dist/server.js.map +1 -1
  46. package/dist/session-memory-prompts.d.ts +5 -0
  47. package/dist/session-memory-prompts.js +7 -0
  48. package/dist/session-memory-prompts.js.map +1 -0
  49. package/dist/spec.js +15 -9
  50. package/dist/spec.js.map +1 -1
  51. package/dist/tools.js +168 -88
  52. package/dist/tools.js.map +1 -1
  53. package/dist/tui.js +4 -2
  54. package/dist/tui.js.map +1 -1
  55. package/dist/types.d.ts +2 -0
  56. package/package.json +6 -2
@@ -12,6 +12,7 @@ import iconv from "iconv-lite";
12
12
  import mime from "mime/lite";
13
13
  import { executeBraveWebSearch } from "./brave-search.js";
14
14
  import { FetchPageReader } from "./fetch-page.js";
15
+ import { replaceUniqueText } from "./file-edit.js";
15
16
  import { generatedImageBytesForExtension } from "./image.js";
16
17
  import { generateImageWithModelRouter, } from "./model-router-media.js";
17
18
  import { executeRoutifyWebFetch, routifyWebFetchEnabled, } from "./routify-web-fetch.js";
@@ -638,6 +639,57 @@ function interleaveMedia(left, right) {
638
639
  function normalizeMediaQuery(value) {
639
640
  return value.trim().replace(/\s+/g, " ").toLowerCase();
640
641
  }
642
+ // Captured sessions call apply_patch even though no recorded request ever
643
+ // advertised it: six react calls (s05/s17/s33) and 260+ html calls executed
644
+ // successfully against the untouched 13-tool/7-tool contracts, while one h19
645
+ // call was rejected by a turn whose runtime lacked it. The canonical schemas
646
+ // stay untouched; the executor accepts the call the way the recording
647
+ // environment's runtime did.
648
+ const APPLY_PATCH_PARAMETERS = {
649
+ type: "object",
650
+ properties: {
651
+ input: { type: "string" },
652
+ },
653
+ required: ["input"],
654
+ additionalProperties: false,
655
+ };
656
+ const APPLY_PATCH_DESCRIPTION = "Apply a patch to files in the project. The patch uses the codex patch format: " +
657
+ "'*** Begin Patch' ... '*** End Patch', with '*** Add File: <path>' (content as " +
658
+ "'+'-prefixed lines), '*** Update File: <path>' ('@@'-separated hunks with ' ' " +
659
+ "context, '-' removed and '+' added lines), and '*** Delete File: <path>'.";
660
+ // CODE_HARNESS_APPLY_PATCH_TOOL opts into advertising the tool to the model.
661
+ // It stays out of the tool list by default while the executor still accepts
662
+ // the call, matching the recorded runtime.
663
+ function applyPatchToolAdvertised() {
664
+ const value = process.env.CODE_HARNESS_APPLY_PATCH_TOOL;
665
+ return (value !== undefined && !["0", "false", "no"].includes(value.toLowerCase()));
666
+ }
667
+ function locateHunkContext(lines, findLines, fromIndex) {
668
+ const limit = lines.length - findLines.length;
669
+ for (let start = Math.max(0, fromIndex); start <= limit; start += 1) {
670
+ let matched = true;
671
+ for (let offset = 0; offset < findLines.length; offset += 1) {
672
+ if (lines[start + offset] !== findLines[offset]) {
673
+ matched = false;
674
+ break;
675
+ }
676
+ }
677
+ if (matched)
678
+ return start;
679
+ }
680
+ for (let start = Math.max(0, fromIndex); start <= limit; start += 1) {
681
+ let matched = true;
682
+ for (let offset = 0; offset < findLines.length; offset += 1) {
683
+ if (lines[start + offset].trim() !== findLines[offset].trim()) {
684
+ matched = false;
685
+ break;
686
+ }
687
+ }
688
+ if (matched)
689
+ return start;
690
+ }
691
+ return undefined;
692
+ }
641
693
  export class CodeToolExecutor {
642
694
  config;
643
695
  workspace;
@@ -671,12 +723,30 @@ export class CodeToolExecutor {
671
723
  this.workspaceContext = options.workspaceContext;
672
724
  this.pageReader = new FetchPageReader({ signal: options.signal });
673
725
  const disallowed = new Set(config.disallowedTools || []);
674
- this.definitions = toolDefinitions(this.workspaceContext, config.mode).filter((definition) => !disallowed.has(definition.function.name));
726
+ const canonical = toolDefinitions(this.workspaceContext, config.mode).filter((definition) => !disallowed.has(definition.function.name));
727
+ const advertiseApplyPatch = applyPatchToolAdvertised() && !disallowed.has("apply_patch");
728
+ this.definitions = advertiseApplyPatch
729
+ ? [
730
+ ...canonical,
731
+ {
732
+ type: "function",
733
+ function: {
734
+ name: "apply_patch",
735
+ description: APPLY_PATCH_DESCRIPTION,
736
+ parameters: APPLY_PATCH_PARAMETERS,
737
+ },
738
+ },
739
+ ]
740
+ : canonical;
675
741
  this.seedFileRank = capturedSeedFileRank(config.mode);
676
742
  addFormats(this.ajv);
677
743
  for (const definition of this.definitions) {
678
744
  this.validators.set(definition.function.name, this.ajv.compile(definition.function.parameters));
679
745
  }
746
+ // Hidden mode: advertise nothing, but keep the call executable.
747
+ if (!advertiseApplyPatch && !disallowed.has("apply_patch")) {
748
+ this.validators.set("apply_patch", this.ajv.compile(APPLY_PATCH_PARAMETERS));
749
+ }
680
750
  }
681
751
  // The tool schemas offered to the model for this session. Identical to the
682
752
  // canonical trace set, adjusted for the selected workspace runtime.
@@ -706,6 +776,9 @@ export class CodeToolExecutor {
706
776
  argumentsValue.offset ??= 1;
707
777
  argumentsValue.limit ??= 2000;
708
778
  }
779
+ else if (name === "list_files" || name === "glob_files") {
780
+ argumentsValue.path ??= "";
781
+ }
709
782
  else if (name === "grep_files") {
710
783
  argumentsValue.path ??= "";
711
784
  argumentsValue.glob ??= "";
@@ -777,6 +850,8 @@ export class CodeToolExecutor {
777
850
  return this.createCodeFile(String(values.path), String(values.content));
778
851
  case "edit_file":
779
852
  return this.editCodeFile(String(values.path), String(values.context), String(values.replacement));
853
+ case "apply_patch":
854
+ return this.applyCodePatch(String(values.input));
780
855
  case "delete_file":
781
856
  return this.deleteCodeFile(String(values.path));
782
857
  case "list_files":
@@ -806,8 +881,9 @@ export class CodeToolExecutor {
806
881
  }
807
882
  }
808
883
  lexicalPath(raw) {
809
- if (!raw)
810
- throw new Error("path must not be empty");
884
+ if (!raw || path.isAbsolute(raw) || raw.includes("..")) {
885
+ throw new Error("Path cannot be empty, start with '/', or contain '..'");
886
+ }
811
887
  let candidate;
812
888
  if (!this.workspaceContext && (raw === "/home/user" || raw === "~")) {
813
889
  candidate = this.workspace;
@@ -852,8 +928,7 @@ export class CodeToolExecutor {
852
928
  return path.relative(this.workspaceReal, target).split(path.sep).join("/");
853
929
  }
854
930
  rememberListedFile(filePath) {
855
- if (this.seedFileRank.has(filePath) ||
856
- this.listedFileOrder.has(filePath)) {
931
+ if (this.seedFileRank.has(filePath) || this.listedFileOrder.has(filePath)) {
857
932
  return;
858
933
  }
859
934
  this.listedFileOrder.set(filePath, this.nextListedFileOrder);
@@ -888,7 +963,7 @@ export class CodeToolExecutor {
888
963
  return -1;
889
964
  if (rightSeedRank !== undefined)
890
965
  return 1;
891
- return (this.listedFileOrder.get(left) - this.listedFileOrder.get(right));
966
+ return this.listedFileOrder.get(left) - this.listedFileOrder.get(right);
892
967
  });
893
968
  }
894
969
  async existsWithExactCase(rawPath) {
@@ -966,14 +1041,23 @@ export class CodeToolExecutor {
966
1041
  }
967
1042
  const lines = decodeText(data).split("\n");
968
1043
  const start = Math.max(1, Math.trunc(offset));
1044
+ if (start > lines.length) {
1045
+ return {
1046
+ status: "success",
1047
+ file: {
1048
+ path: this.relativePath(target),
1049
+ content: `Offset ${start} is out of range for this file (${lines.length} lines)`,
1050
+ contentType: contentTypeForFile(target),
1051
+ },
1052
+ totalLines: lines.length,
1053
+ };
1054
+ }
969
1055
  const end = Math.min(lines.length, start + Math.max(1, Math.trunc(limit)) - 1);
970
- const numbered = start > lines.length
971
- ? ""
972
- : lines
973
- .slice(start - 1, end)
974
- .map((line, index) => `${start + index}: ${line}`)
975
- .join("\n");
976
- const footer = start === 1 && end === lines.length
1056
+ const numbered = lines
1057
+ .slice(start - 1, end)
1058
+ .map((line, index) => `${start + index}: ${line}`)
1059
+ .join("\n");
1060
+ const footer = end === lines.length
977
1061
  ? `(End of file - ${lines.length} lines)`
978
1062
  : `(Showing lines ${start}-${end} of ${lines.length}. Use offset=${end + 1} to continue.)`;
979
1063
  return {
@@ -1063,15 +1147,165 @@ export class CodeToolExecutor {
1063
1147
  message: `File "${rawPath}" does not exist`,
1064
1148
  };
1065
1149
  }
1066
- if (!source.includes(context)) {
1150
+ const edited = replaceUniqueText(source, context, replacement, {
1151
+ whitespaceInsensitiveMultiline: true,
1152
+ });
1153
+ if (edited === undefined) {
1154
+ // Captured sessions re-issue an edit after compaction or a retry. The
1155
+ // provider reports success without touching the file when the context
1156
+ // is already gone and the replacement is in place (f39 re-issues four
1157
+ // dashboard edits this way).
1158
+ if (replacement && source.includes(replacement)) {
1159
+ return this.mutationResult("Edited", rawPath);
1160
+ }
1067
1161
  return {
1068
1162
  status: "error",
1069
1163
  message: "Context not found. Read the file to verify the text exists.",
1070
1164
  };
1071
1165
  }
1072
- await writeFile(target, source.replace(context, replacement), "utf8");
1166
+ await writeFile(target, edited, "utf8");
1073
1167
  return this.mutationResult("Edited", rawPath);
1074
1168
  }
1169
+ // Some recorded sessions exposed a codex-style apply_patch tool (*** Begin
1170
+ // Patch / Add|Update|Delete File / @@ hunks) that the provider executed
1171
+ // without ever advertising it. Its observable contract: "Patch applied[.
1172
+ // Type errors found.]" on success, "Invalid Context <anchor>:\n<context>"
1173
+ // when a hunk context cannot be located — the anchor is the line where the
1174
+ // previous hunk's match ended and the failed search started (h17 reports
1175
+ // 666 after a four-line context hunk matching lines 663-666).
1176
+ async applyCodePatch(patchText) {
1177
+ const kindByMarker = {
1178
+ Add: "add",
1179
+ Update: "update",
1180
+ Delete: "delete",
1181
+ };
1182
+ const operations = [];
1183
+ let current;
1184
+ for (const line of patchText.split("\n")) {
1185
+ const marker = /^\*\*\* (Add|Update|Delete) File: (.+)$/.exec(line);
1186
+ if (marker) {
1187
+ current = {
1188
+ kind: kindByMarker[marker[1]],
1189
+ path: marker[2].trim(),
1190
+ lines: [],
1191
+ };
1192
+ operations.push(current);
1193
+ continue;
1194
+ }
1195
+ if (line.startsWith("*** ")) {
1196
+ current = undefined;
1197
+ continue;
1198
+ }
1199
+ current?.lines.push(line);
1200
+ }
1201
+ for (const operation of operations) {
1202
+ const target = await this.confinedPath(operation.path);
1203
+ if (operation.kind === "add") {
1204
+ if (await this.existsWithExactCase(operation.path)) {
1205
+ return {
1206
+ status: "error",
1207
+ message: `Add File Error: Duplicate Path: ${operation.path}`,
1208
+ };
1209
+ }
1210
+ // The recorded provider writes exactly the "+" lines joined by
1211
+ // newlines without appending a trailing one (h17's 27-line
1212
+ // manifest.json reads back as totalLines 27, not 28).
1213
+ const content = operation.lines
1214
+ .filter((line) => line.startsWith("+"))
1215
+ .map((line) => line.slice(1))
1216
+ .join("\n");
1217
+ mkdirSync(path.dirname(target), { recursive: true });
1218
+ await writeFile(target, content, "utf8");
1219
+ this.rememberListedFile(this.relativePath(target));
1220
+ continue;
1221
+ }
1222
+ if (operation.kind === "delete") {
1223
+ if (!(await this.existsWithExactCase(operation.path))) {
1224
+ return {
1225
+ status: "error",
1226
+ message: `Delete File Error: File Not Found: ${operation.path}`,
1227
+ };
1228
+ }
1229
+ await rm(target, { force: true });
1230
+ this.forgetListedPath(this.relativePath(target));
1231
+ continue;
1232
+ }
1233
+ let source;
1234
+ try {
1235
+ if (!(await this.existsWithExactCase(operation.path)))
1236
+ throw new Error("missing");
1237
+ source = await readFile(target, "utf8");
1238
+ }
1239
+ catch {
1240
+ return {
1241
+ status: "error",
1242
+ message: `Update File Error: File Not Found: ${operation.path}`,
1243
+ };
1244
+ }
1245
+ // Hunks are separated by @@ lines; within a hunk, " " context and "-"
1246
+ // removed lines form the find pattern, " " context and "+" added lines
1247
+ // form the replacement. Hunks stream over the original file: each
1248
+ // context is searched forward from where the previous hunk ended, and a
1249
+ // miss reports the search anchor over the find text the way the
1250
+ // recorded provider does (h17: "Invalid Context 666:"). Whitespace
1251
+ // drifted contexts match by trimmed lines.
1252
+ const hunks = [];
1253
+ let hunk = [];
1254
+ for (const line of operation.lines) {
1255
+ if (line.startsWith("@@")) {
1256
+ if (hunk.length)
1257
+ hunks.push(hunk);
1258
+ hunk = [];
1259
+ continue;
1260
+ }
1261
+ hunk.push(line);
1262
+ }
1263
+ if (hunk.length)
1264
+ hunks.push(hunk);
1265
+ const originalLines = source.split("\n");
1266
+ const output = [];
1267
+ let position = 0;
1268
+ for (const hunkLines of hunks) {
1269
+ const findLines = [];
1270
+ const replaceLines = [];
1271
+ for (const line of hunkLines) {
1272
+ if (line.startsWith("-")) {
1273
+ findLines.push(line.slice(1));
1274
+ }
1275
+ else if (line.startsWith("+")) {
1276
+ replaceLines.push(line.slice(1));
1277
+ }
1278
+ else {
1279
+ findLines.push(line.slice(1));
1280
+ replaceLines.push(line.slice(1));
1281
+ }
1282
+ }
1283
+ if (!findLines.length)
1284
+ continue;
1285
+ const start = locateHunkContext(originalLines, findLines, position);
1286
+ if (start === undefined) {
1287
+ return {
1288
+ status: "error",
1289
+ message: `Invalid Context ${position}:\n${findLines.join("\n")}`,
1290
+ };
1291
+ }
1292
+ output.push(...originalLines.slice(position, start));
1293
+ output.push(...replaceLines);
1294
+ position = start + findLines.length;
1295
+ }
1296
+ output.push(...originalLines.slice(position));
1297
+ await writeFile(target, output.join("\n"), "utf8");
1298
+ }
1299
+ const lintError = await this.typeErrors();
1300
+ if (lintError) {
1301
+ return {
1302
+ status: "success",
1303
+ message: "Patch applied. Type errors found.",
1304
+ lint_error: lintError,
1305
+ };
1306
+ }
1307
+ return { status: "success", message: "Patch applied." };
1308
+ }
1075
1309
  async deleteCodeFile(rawPath) {
1076
1310
  const target = await this.confinedPath(rawPath);
1077
1311
  await rm(target, { force: true, recursive: true });
@@ -1091,21 +1325,61 @@ export class CodeToolExecutor {
1091
1325
  };
1092
1326
  }
1093
1327
  async grepCodeFiles(values) {
1328
+ const pattern = String(values.pattern);
1329
+ if (pattern.length > 200) {
1330
+ return {
1331
+ status: "error",
1332
+ message: "Pattern too long (max 200 characters)",
1333
+ };
1334
+ }
1094
1335
  const target = await this.confinedPath(String(values.path || "."));
1095
- const targetInfo = await stat(target);
1096
- const files = await this.walkFiles(target);
1097
1336
  const glob = String(values.glob || "");
1337
+ const mode = String(values.output_mode || "content");
1098
1338
  const flags = Boolean(values["-i"]) ? "i" : "";
1099
1339
  let expression;
1100
1340
  try {
1101
- expression = new RegExp(String(values.pattern), flags);
1341
+ expression = new RegExp(pattern, flags);
1102
1342
  }
1103
1343
  catch (error) {
1104
1344
  throw new Error(`Invalid regular expression: ${errorMessage(error)}`);
1105
1345
  }
1106
- const symmetric = Math.max(Number(values.context || 0), Number(values["-C"] || 0));
1346
+ let targetInfo;
1347
+ try {
1348
+ targetInfo = await stat(target);
1349
+ }
1350
+ catch (error) {
1351
+ if (error.code !== "ENOENT")
1352
+ throw error;
1353
+ if (mode === "files_with_matches") {
1354
+ return { status: "success", mode, files: [], truncated: false };
1355
+ }
1356
+ if (mode === "count") {
1357
+ return {
1358
+ status: "success",
1359
+ mode,
1360
+ counts: [],
1361
+ totalMatches: 0,
1362
+ truncated: false,
1363
+ };
1364
+ }
1365
+ return {
1366
+ status: "success",
1367
+ mode: "content",
1368
+ matches: [],
1369
+ truncated: false,
1370
+ };
1371
+ }
1372
+ const files = await this.walkFiles(target);
1373
+ // Full Stack captures expose a different context-grouping adapter from
1374
+ // HTML/React: -C/context are ignored, the trailing group spans B+A lines,
1375
+ // and a later match reuses the prior trailing group as contextBefore.
1376
+ const groupedContext = this.config.mode === "full-stack";
1377
+ const symmetric = groupedContext
1378
+ ? 0
1379
+ : Math.max(Number(values.context || 0), Number(values["-C"] || 0));
1107
1380
  const before = symmetric || Number(values["-B"] || 0);
1108
1381
  const after = symmetric || Number(values["-A"] || 0);
1382
+ const contextSpan = groupedContext ? before + after : after;
1109
1383
  const matches = [];
1110
1384
  const matchedFiles = [];
1111
1385
  const counts = [];
@@ -1115,6 +1389,11 @@ export class CodeToolExecutor {
1115
1389
  const relativeToSearch = targetInfo.isFile()
1116
1390
  ? path.basename(filePath)
1117
1391
  : path.relative(target, filePath).split(path.sep).join("/");
1392
+ if (relativeToSearch
1393
+ .split("/")
1394
+ .some((component) => component.startsWith("."))) {
1395
+ continue;
1396
+ }
1118
1397
  if (glob && !matchesGlob(relativeToSearch, glob))
1119
1398
  continue;
1120
1399
  let content;
@@ -1129,11 +1408,15 @@ export class CodeToolExecutor {
1129
1408
  continue;
1130
1409
  }
1131
1410
  const lines = content.split("\n");
1132
- let fileCount = 0;
1411
+ const matchingLines = [];
1133
1412
  for (let index = 0; index < lines.length; index += 1) {
1134
- if (!expression.test(lines[index]))
1135
- continue;
1136
- fileCount += 1;
1413
+ if (expression.test(lines[index]))
1414
+ matchingLines.push(index);
1415
+ }
1416
+ const fileCount = matchingLines.length;
1417
+ let previousAfter = [];
1418
+ for (let position = 0; position < matchingLines.length; position += 1) {
1419
+ const index = matchingLines[position];
1137
1420
  totalMatches += 1;
1138
1421
  if (matches.length < maximumSearchResults) {
1139
1422
  const match = {
@@ -1141,11 +1424,29 @@ export class CodeToolExecutor {
1141
1424
  lineNumber: index + 1,
1142
1425
  lineContent: lines[index],
1143
1426
  };
1144
- if (before > 0) {
1145
- match.contextBefore = lines.slice(Math.max(0, index - before), index);
1427
+ if (groupedContext) {
1428
+ const contextBefore = position === 0
1429
+ ? lines.slice(Math.max(0, index - before), index)
1430
+ : previousAfter;
1431
+ if (contextBefore.length > 0)
1432
+ match.contextBefore = contextBefore;
1433
+ const nextMatch = matchingLines[position + 1] ?? lines.length;
1434
+ previousAfter = lines.slice(index + 1, Math.min(index + 1 + contextSpan, nextMatch));
1435
+ if (nextMatch === lines.length &&
1436
+ previousAfter.at(-1) === "" &&
1437
+ index + 1 + contextSpan >= lines.length) {
1438
+ previousAfter = previousAfter.slice(0, -1);
1439
+ }
1440
+ if (previousAfter.length > 0)
1441
+ match.contextAfter = previousAfter;
1146
1442
  }
1147
- if (after > 0) {
1148
- match.contextAfter = lines.slice(index + 1, index + 1 + after);
1443
+ else {
1444
+ if (before > 0) {
1445
+ match.contextBefore = lines.slice(Math.max(0, index - before), index);
1446
+ }
1447
+ if (after > 0) {
1448
+ match.contextAfter = lines.slice(index + 1, index + 1 + after);
1449
+ }
1149
1450
  }
1150
1451
  matches.push(match);
1151
1452
  }
@@ -1158,7 +1459,6 @@ export class CodeToolExecutor {
1158
1459
  }
1159
1460
  }
1160
1461
  }
1161
- const mode = String(values.output_mode || "content");
1162
1462
  if (mode === "files_with_matches") {
1163
1463
  return {
1164
1464
  status: "success",
@@ -1185,7 +1485,15 @@ export class CodeToolExecutor {
1185
1485
  }
1186
1486
  async globCodeFiles(pattern, rawPath) {
1187
1487
  const target = await this.confinedPath(rawPath || ".");
1188
- const files = await this.walkFiles(target);
1488
+ let files;
1489
+ try {
1490
+ files = await this.walkFiles(target);
1491
+ }
1492
+ catch (error) {
1493
+ if (error.code !== "ENOENT")
1494
+ throw error;
1495
+ return { status: "success", paths: [], truncated: false };
1496
+ }
1189
1497
  const allPaths = files
1190
1498
  .filter((filePath) => matchesGlob(path.relative(target, filePath).split(path.sep).join("/"), pattern))
1191
1499
  .map((filePath) => this.relativePath(filePath));
@@ -1302,18 +1610,6 @@ export class CodeToolExecutor {
1302
1610
  stderr: this.virtualizeCodeOutput(result.stderr),
1303
1611
  };
1304
1612
  }
1305
- virtualizeBashOutput(value) {
1306
- if (this.workspaceContext || this.config.mode === "full-stack")
1307
- return value;
1308
- let virtualized = value;
1309
- const workspaceRoots = [...new Set([this.workspace, this.workspaceReal])]
1310
- .filter((root) => root !== path.parse(root).root)
1311
- .sort((left, right) => right.length - left.length);
1312
- for (const root of workspaceRoots) {
1313
- virtualized = virtualized.replaceAll(root, "/home/user");
1314
- }
1315
- return virtualized;
1316
- }
1317
1613
  async fullStackBash(command, workdir) {
1318
1614
  const configuredTimeout = Number(process.env.CODE_HARNESS_BASH_TIMEOUT_SECONDS || 300);
1319
1615
  const timeoutSeconds = Number.isFinite(configuredTimeout) && configuredTimeout > 0
@@ -1454,12 +1750,6 @@ export class CodeToolExecutor {
1454
1750
  if (!(await stat(workdir)).isDirectory()) {
1455
1751
  throw new Error(`cwd is not a directory: ${cwd}`);
1456
1752
  }
1457
- // Runtime-aware callers execute commands exactly as written and report
1458
- // real local paths. The legacy /home/user translation is limited to the
1459
- // older non-Full-Stack replay contract.
1460
- const translatedCommand = this.workspaceContext || this.config.mode === "full-stack"
1461
- ? command
1462
- : command.replaceAll("/home/user", '"$ARENA_WORKSPACE"');
1463
1753
  return new Promise((resolve, reject) => {
1464
1754
  const env = {
1465
1755
  ...process.env,
@@ -1477,7 +1767,7 @@ export class CodeToolExecutor {
1477
1767
  // detached:管道成员(npm、tail…)与 bash 同进程组。只杀 bash 会留下
1478
1768
  // 孤儿持有 stdout/stderr 管道,close 永不触发,整个 turn 挂死;
1479
1769
  // 必须杀整组,并在宽限期后强制结算兜底。
1480
- const child = spawn("/bin/bash", ["-c", translatedCommand], {
1770
+ const child = spawn("/bin/bash", ["-c", command], {
1481
1771
  cwd: workdir,
1482
1772
  env,
1483
1773
  stdio: ["ignore", "pipe", "pipe"],
@@ -1531,8 +1821,8 @@ export class CodeToolExecutor {
1531
1821
  stderr +=
1532
1822
  (stderr ? "\n" : "") + `command exceeded ${timeoutSeconds}s`;
1533
1823
  }
1534
- const out = capBashOutput(this.virtualizeBashOutput(stdout));
1535
- const err = capBashOutput(this.virtualizeBashOutput(stderr));
1824
+ const out = capBashOutput(stdout);
1825
+ const err = capBashOutput(stderr);
1536
1826
  resolve({
1537
1827
  status: "completed",
1538
1828
  stderr: err.text,