@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.
- package/.env.example +8 -1
- package/README.md +25 -12
- package/dist/agent.d.ts +4 -0
- package/dist/agent.js +19 -6
- package/dist/agent.js.map +1 -1
- package/dist/brave-search.js +1 -3
- package/dist/brave-search.js.map +1 -1
- package/dist/canonical-data.js +1 -1
- package/dist/canonical-data.js.map +1 -1
- package/dist/cli.js +5 -3
- package/dist/cli.js.map +1 -1
- package/dist/code-tools.d.ts +1 -1
- package/dist/code-tools.js +340 -50
- package/dist/code-tools.js.map +1 -1
- package/dist/compaction.d.ts +29 -2
- package/dist/compaction.js +242 -187
- package/dist/compaction.js.map +1 -1
- package/dist/config.d.ts +1 -0
- package/dist/config.js +2 -1
- package/dist/config.js.map +1 -1
- package/dist/fetch-page.js +9 -33
- package/dist/fetch-page.js.map +1 -1
- package/dist/file-edit.d.ts +6 -0
- package/dist/file-edit.js +58 -0
- package/dist/file-edit.js.map +1 -0
- package/dist/history.d.ts +6 -3
- package/dist/history.js +42 -11
- package/dist/history.js.map +1 -1
- package/dist/legacy-compaction.d.ts +16 -0
- package/dist/legacy-compaction.js +207 -0
- package/dist/legacy-compaction.js.map +1 -0
- package/dist/mock.js +5 -0
- package/dist/mock.js.map +1 -1
- package/dist/process-manager.js +2 -1
- package/dist/process-manager.js.map +1 -1
- package/dist/prompts.d.ts +2 -0
- package/dist/prompts.js +6 -2
- package/dist/prompts.js.map +1 -1
- package/dist/routify-web-fetch.js +2 -3
- package/dist/routify-web-fetch.js.map +1 -1
- package/dist/sandbox-file-write.d.ts +10 -0
- package/dist/sandbox-file-write.js +58 -0
- package/dist/sandbox-file-write.js.map +1 -0
- package/dist/server.js +8 -5
- package/dist/server.js.map +1 -1
- package/dist/session-memory-prompts.d.ts +5 -0
- package/dist/session-memory-prompts.js +7 -0
- package/dist/session-memory-prompts.js.map +1 -0
- package/dist/spec.js +15 -9
- package/dist/spec.js.map +1 -1
- package/dist/tools.js +168 -88
- package/dist/tools.js.map +1 -1
- package/dist/tui.js +4 -2
- package/dist/tui.js.map +1 -1
- package/dist/types.d.ts +2 -0
- package/package.json +6 -2
package/dist/code-tools.js
CHANGED
|
@@ -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
|
-
|
|
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("
|
|
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
|
|
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 =
|
|
971
|
-
|
|
972
|
-
:
|
|
973
|
-
|
|
974
|
-
|
|
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
|
-
|
|
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,
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
1411
|
+
const matchingLines = [];
|
|
1133
1412
|
for (let index = 0; index < lines.length; index += 1) {
|
|
1134
|
-
if (
|
|
1135
|
-
|
|
1136
|
-
|
|
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 (
|
|
1145
|
-
|
|
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
|
-
|
|
1148
|
-
|
|
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
|
-
|
|
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",
|
|
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(
|
|
1535
|
-
const err = capBashOutput(
|
|
1824
|
+
const out = capBashOutput(stdout);
|
|
1825
|
+
const err = capBashOutput(stderr);
|
|
1536
1826
|
resolve({
|
|
1537
1827
|
status: "completed",
|
|
1538
1828
|
stderr: err.text,
|