opencode-swarm 7.136.0 → 7.136.2
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/.opencode/skills/swarm-pr-feedback/SKILL.md +41 -1
- package/.opencode/skills/swarm-pr-review/SKILL.md +83 -24
- package/README.md +18 -0
- package/dist/background/candidate-contract.d.ts +13 -0
- package/dist/background/workspace-snapshot.d.ts +54 -0
- package/dist/cli/{config-doctor-q0krbxwv.js → config-doctor-b67nb84b.js} +2 -2
- package/dist/cli/{curation-policy-v6ywe8qd.js → curation-policy-kvzhfaj1.js} +2 -2
- package/dist/cli/{curator-llm-factory-mhwje8rf.js → curator-llm-factory-x7kvry9x.js} +17 -14
- package/dist/cli/{curator-tqedvpy1.js → curator-nrmj8bds.js} +17 -14
- package/dist/cli/{evidence-summary-service-w006jnpg.js → evidence-summary-service-cf8sz2bq.js} +3 -1
- package/dist/cli/{guardrail-explain-m0m74hvr.js → guardrail-explain-5kd44rcj.js} +18 -15
- package/dist/cli/{guardrail-log-5g8x6qc6.js → guardrail-log-aksrzqdx.js} +3 -3
- package/dist/cli/hashing-mjwn6j4y.js +34 -0
- package/dist/cli/{hive-promoter-s804newd.js → hive-promoter-cy21rhnd.js} +17 -14
- package/dist/cli/{index-n8j0cnp1.js → index-0755132s.js} +4 -4
- package/dist/cli/{index-1kjg590p.js → index-1kn24ja0.js} +1 -1
- package/dist/cli/{index-j08trvny.js → index-21115szq.js} +5 -5
- package/dist/cli/{index-2cv1j3rf.js → index-37v2wxqe.js} +1 -1
- package/dist/cli/{index-cfz5750k.js → index-7ayaq27q.js} +5 -1
- package/dist/cli/{index-9bk16zkv.js → index-84nz7bhe.js} +1 -1
- package/dist/cli/{index-bwfzq91q.js → index-ad6j0m7k.js} +12 -6
- package/dist/cli/{index-qh9466j3.js → index-azghvnja.js} +2 -2
- package/dist/cli/{index-cxq1t7j1.js → index-ckybsn6p.js} +19 -16
- package/dist/cli/{index-cggqh2dz.js → index-kwwxevne.js} +53 -996
- package/dist/cli/{index-nw9cn84t.js → index-m5fw4mer.js} +19 -2
- package/dist/cli/{index-2w4tsmva.js → index-rw3f73aq.js} +1 -1
- package/dist/cli/{index-wpqypdge.js → index-s0rbdp61.js} +2 -2
- package/dist/cli/{index-bbejqagj.js → index-s0vahtdm.js} +1 -1
- package/dist/cli/{index-81dkzncr.js → index-sr2cynyw.js} +4535 -2325
- package/dist/cli/index-wjm896ey.js +1100 -0
- package/dist/cli/{index-w58bmwyq.js → index-wnz282j3.js} +2 -2
- package/dist/cli/{index-y6a7gjtj.js → index-wsdnttf4.js} +228 -81
- package/dist/cli/{index-m1hew17b.js → index-y0xaq4f3.js} +39 -4
- package/dist/cli/index-y8552snf.js +264 -0
- package/dist/cli/index-z1tm47nj.js +783 -0
- package/dist/cli/index.js +17 -14
- package/dist/cli/{knowledge-escalator-f1m2xp5x.js → knowledge-escalator-ta3xjrdj.js} +3 -3
- package/dist/cli/{knowledge-events-rksgem6f.js → knowledge-events-dk6banmz.js} +1 -1
- package/dist/cli/{knowledge-store-t6d6fr8b.js → knowledge-store-h3jz10bv.js} +1 -1
- package/dist/cli/{knowledge-validator-5ggzb3j1.js → knowledge-validator-jsz1dxkr.js} +4 -4
- package/dist/cli/runner-2v413r74.js +21 -0
- package/dist/cli/{scan-cursor-18fnwr58.js → scan-cursor-48gwzh9c.js} +2 -2
- package/dist/cli/{schema-jw15d8bt.js → schema-f937b9cs.js} +5 -1
- package/dist/cli/{skill-generator-3n5y902z.js → skill-generator-4p10ktw1.js} +5 -5
- package/dist/cli/{workspace-snapshot-jmyamqnv.js → workspace-snapshot-h5rzw37b.js} +5 -1
- package/dist/commands/registry.d.ts +72 -0
- package/dist/commands/skill-opt.d.ts +41 -0
- package/dist/config/schema.d.ts +47 -0
- package/dist/hooks/guardrails/file-authority.d.ts +11 -2
- package/dist/hooks/pr-workflow-gate.d.ts +261 -4
- package/dist/hooks/pr-workflow-response-gate.d.ts +27 -9
- package/dist/hooks/write-target-resolver.d.ts +19 -0
- package/dist/index.js +355 -320
- package/dist/services/skill-evaluator.d.ts +17 -0
- package/dist/services/skill-optimizer/activation.d.ts +58 -0
- package/dist/services/skill-optimizer/candidates.d.ts +88 -0
- package/dist/services/skill-optimizer/controller.d.ts +146 -0
- package/dist/services/skill-optimizer/deterministic-seed.d.ts +29 -0
- package/dist/services/skill-optimizer/lifecycle.d.ts +70 -0
- package/dist/services/skill-optimizer/promoted-external-staleness.d.ts +98 -0
- package/dist/services/skill-optimizer/retirement.d.ts +54 -0
- package/dist/services/skill-optimizer/skill-eval-tasks.d.ts +47 -0
- package/dist/services/skill-optimizer/smoke.d.ts +46 -0
- package/dist/services/skill-optimizer/store.d.ts +118 -0
- package/dist/tools/write-pr-review-trigger-eval.d.ts +2 -1
- package/dist/utils/stable-stringify.d.ts +46 -0
- package/evaluation-fixtures/skill-eval/scoring/score-skill-eval.cjs +97 -0
- package/package.json +1 -1
|
@@ -0,0 +1,1100 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
import {
|
|
3
|
+
CostRecordSchema,
|
|
4
|
+
EvaluationCandidateV1Schema,
|
|
5
|
+
EvaluationRunV1Schema,
|
|
6
|
+
EvaluationTaskV1Schema,
|
|
7
|
+
admitEvaluationTask,
|
|
8
|
+
claimHeldOutTest,
|
|
9
|
+
readEvaluationRun,
|
|
10
|
+
saveEvaluationRun,
|
|
11
|
+
saveTaskSetSnapshot
|
|
12
|
+
} from "./index-z1tm47nj.js";
|
|
13
|
+
import {
|
|
14
|
+
canonicalHash,
|
|
15
|
+
computeCandidateInputContentHash,
|
|
16
|
+
computeRunIntegrityHash,
|
|
17
|
+
computeTaskInputContentHash,
|
|
18
|
+
computeTaskSetContentHash,
|
|
19
|
+
resolveContainedExistingPath
|
|
20
|
+
} from "./index-y8552snf.js";
|
|
21
|
+
import {
|
|
22
|
+
exports_external,
|
|
23
|
+
init_zod
|
|
24
|
+
} from "./index-bpmtbmy9.js";
|
|
25
|
+
import {
|
|
26
|
+
bunSpawn
|
|
27
|
+
} from "./index-y111zefa.js";
|
|
28
|
+
|
|
29
|
+
// src/evaluation/runner.ts
|
|
30
|
+
import * as fs3 from "fs";
|
|
31
|
+
import * as path3 from "path";
|
|
32
|
+
|
|
33
|
+
// node_modules/yocto-queue/index.js
|
|
34
|
+
class Node {
|
|
35
|
+
value;
|
|
36
|
+
next;
|
|
37
|
+
constructor(value) {
|
|
38
|
+
this.value = value;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
class Queue {
|
|
43
|
+
#head;
|
|
44
|
+
#tail;
|
|
45
|
+
#size;
|
|
46
|
+
constructor() {
|
|
47
|
+
this.clear();
|
|
48
|
+
}
|
|
49
|
+
enqueue(value) {
|
|
50
|
+
const node = new Node(value);
|
|
51
|
+
if (this.#head) {
|
|
52
|
+
this.#tail.next = node;
|
|
53
|
+
this.#tail = node;
|
|
54
|
+
} else {
|
|
55
|
+
this.#head = node;
|
|
56
|
+
this.#tail = node;
|
|
57
|
+
}
|
|
58
|
+
this.#size++;
|
|
59
|
+
}
|
|
60
|
+
dequeue() {
|
|
61
|
+
const current = this.#head;
|
|
62
|
+
if (!current) {
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
this.#head = this.#head.next;
|
|
66
|
+
this.#size--;
|
|
67
|
+
if (!this.#head) {
|
|
68
|
+
this.#tail = undefined;
|
|
69
|
+
}
|
|
70
|
+
return current.value;
|
|
71
|
+
}
|
|
72
|
+
peek() {
|
|
73
|
+
if (!this.#head) {
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
return this.#head.value;
|
|
77
|
+
}
|
|
78
|
+
clear() {
|
|
79
|
+
this.#head = undefined;
|
|
80
|
+
this.#tail = undefined;
|
|
81
|
+
this.#size = 0;
|
|
82
|
+
}
|
|
83
|
+
get size() {
|
|
84
|
+
return this.#size;
|
|
85
|
+
}
|
|
86
|
+
*[Symbol.iterator]() {
|
|
87
|
+
let current = this.#head;
|
|
88
|
+
while (current) {
|
|
89
|
+
yield current.value;
|
|
90
|
+
current = current.next;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
*drain() {
|
|
94
|
+
while (this.#head) {
|
|
95
|
+
yield this.dequeue();
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// node_modules/p-limit/index.js
|
|
101
|
+
function pLimit(concurrency) {
|
|
102
|
+
let rejectOnClear = false;
|
|
103
|
+
if (typeof concurrency === "object") {
|
|
104
|
+
({ concurrency, rejectOnClear = false } = concurrency);
|
|
105
|
+
}
|
|
106
|
+
validateConcurrency(concurrency);
|
|
107
|
+
if (typeof rejectOnClear !== "boolean") {
|
|
108
|
+
throw new TypeError("Expected `rejectOnClear` to be a boolean");
|
|
109
|
+
}
|
|
110
|
+
const queue = new Queue;
|
|
111
|
+
let activeCount = 0;
|
|
112
|
+
const resumeNext = () => {
|
|
113
|
+
if (activeCount < concurrency && queue.size > 0) {
|
|
114
|
+
activeCount++;
|
|
115
|
+
queue.dequeue().run();
|
|
116
|
+
}
|
|
117
|
+
};
|
|
118
|
+
const next = () => {
|
|
119
|
+
activeCount--;
|
|
120
|
+
resumeNext();
|
|
121
|
+
};
|
|
122
|
+
const run = async (function_, resolve, arguments_) => {
|
|
123
|
+
const result = (async () => function_(...arguments_))();
|
|
124
|
+
resolve(result);
|
|
125
|
+
try {
|
|
126
|
+
await result;
|
|
127
|
+
} catch {}
|
|
128
|
+
next();
|
|
129
|
+
};
|
|
130
|
+
const enqueue = (function_, resolve, reject, arguments_) => {
|
|
131
|
+
const queueItem = { reject };
|
|
132
|
+
new Promise((internalResolve) => {
|
|
133
|
+
queueItem.run = internalResolve;
|
|
134
|
+
queue.enqueue(queueItem);
|
|
135
|
+
}).then(run.bind(undefined, function_, resolve, arguments_));
|
|
136
|
+
if (activeCount < concurrency) {
|
|
137
|
+
resumeNext();
|
|
138
|
+
}
|
|
139
|
+
};
|
|
140
|
+
const generator = (function_, ...arguments_) => new Promise((resolve, reject) => {
|
|
141
|
+
enqueue(function_, resolve, reject, arguments_);
|
|
142
|
+
});
|
|
143
|
+
Object.defineProperties(generator, {
|
|
144
|
+
activeCount: {
|
|
145
|
+
get: () => activeCount
|
|
146
|
+
},
|
|
147
|
+
pendingCount: {
|
|
148
|
+
get: () => queue.size
|
|
149
|
+
},
|
|
150
|
+
clearQueue: {
|
|
151
|
+
value() {
|
|
152
|
+
if (!rejectOnClear) {
|
|
153
|
+
queue.clear();
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
const abortError = AbortSignal.abort().reason;
|
|
157
|
+
while (queue.size > 0) {
|
|
158
|
+
queue.dequeue().reject(abortError);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
},
|
|
162
|
+
concurrency: {
|
|
163
|
+
get: () => concurrency,
|
|
164
|
+
set(newConcurrency) {
|
|
165
|
+
validateConcurrency(newConcurrency);
|
|
166
|
+
concurrency = newConcurrency;
|
|
167
|
+
queueMicrotask(() => {
|
|
168
|
+
while (activeCount < concurrency && queue.size > 0) {
|
|
169
|
+
resumeNext();
|
|
170
|
+
}
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
},
|
|
174
|
+
map: {
|
|
175
|
+
async value(iterable, function_) {
|
|
176
|
+
const promises = Array.from(iterable, (value, index) => this(function_, value, index));
|
|
177
|
+
return Promise.all(promises);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
});
|
|
181
|
+
return generator;
|
|
182
|
+
}
|
|
183
|
+
function validateConcurrency(concurrency) {
|
|
184
|
+
if (!((Number.isInteger(concurrency) || concurrency === Number.POSITIVE_INFINITY) && concurrency > 0)) {
|
|
185
|
+
throw new TypeError("Expected `concurrency` to be a number from 1 and up");
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// src/evaluation/runner.ts
|
|
190
|
+
init_zod();
|
|
191
|
+
|
|
192
|
+
// src/utils/external-tool-runner.ts
|
|
193
|
+
import * as fs from "fs";
|
|
194
|
+
import * as path from "path";
|
|
195
|
+
var DEFAULT_WINDOWS_EXTENSIONS = [".exe", ".cmd", ".bat"];
|
|
196
|
+
function isExecutableFile(candidate) {
|
|
197
|
+
try {
|
|
198
|
+
const stats = fs.statSync(candidate);
|
|
199
|
+
return stats.isFile();
|
|
200
|
+
} catch {
|
|
201
|
+
return false;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
function resolveExecutableFromPath(names, envPath = process.env.PATH ?? "", platform = process.platform) {
|
|
205
|
+
const pathEntries = envPath.split(path.delimiter).filter(Boolean);
|
|
206
|
+
const isWindows = platform === "win32";
|
|
207
|
+
for (const rawName of names) {
|
|
208
|
+
if (!rawName)
|
|
209
|
+
continue;
|
|
210
|
+
if (path.isAbsolute(rawName) && isExecutableFile(rawName)) {
|
|
211
|
+
return rawName;
|
|
212
|
+
}
|
|
213
|
+
const extensions = isWindows && path.extname(rawName) === "" ? DEFAULT_WINDOWS_EXTENSIONS : [""];
|
|
214
|
+
for (const dir of pathEntries) {
|
|
215
|
+
for (const ext of extensions) {
|
|
216
|
+
const candidate = path.join(dir, `${rawName}${ext}`);
|
|
217
|
+
if (isExecutableFile(candidate)) {
|
|
218
|
+
return candidate;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
return null;
|
|
224
|
+
}
|
|
225
|
+
async function readBoundedStream(stream, maxBytes) {
|
|
226
|
+
const reader = stream.getReader();
|
|
227
|
+
let lockReleased = false;
|
|
228
|
+
const releaseLock = () => {
|
|
229
|
+
if (lockReleased)
|
|
230
|
+
return;
|
|
231
|
+
lockReleased = true;
|
|
232
|
+
try {
|
|
233
|
+
reader.releaseLock();
|
|
234
|
+
} catch {}
|
|
235
|
+
};
|
|
236
|
+
const cancelAndRelease = async () => {
|
|
237
|
+
try {
|
|
238
|
+
await reader.cancel();
|
|
239
|
+
} catch {} finally {
|
|
240
|
+
releaseLock();
|
|
241
|
+
}
|
|
242
|
+
};
|
|
243
|
+
if (maxBytes <= 0) {
|
|
244
|
+
await cancelAndRelease();
|
|
245
|
+
return { text: "", truncated: true };
|
|
246
|
+
}
|
|
247
|
+
const chunks = [];
|
|
248
|
+
let total = 0;
|
|
249
|
+
let truncated = false;
|
|
250
|
+
try {
|
|
251
|
+
for (;; ) {
|
|
252
|
+
const { done, value } = await reader.read();
|
|
253
|
+
if (done)
|
|
254
|
+
break;
|
|
255
|
+
if (!value)
|
|
256
|
+
continue;
|
|
257
|
+
const remaining = maxBytes - total;
|
|
258
|
+
if (value.byteLength > remaining) {
|
|
259
|
+
if (remaining > 0) {
|
|
260
|
+
chunks.push(value.slice(0, remaining));
|
|
261
|
+
total += remaining;
|
|
262
|
+
}
|
|
263
|
+
truncated = true;
|
|
264
|
+
await cancelAndRelease();
|
|
265
|
+
break;
|
|
266
|
+
}
|
|
267
|
+
chunks.push(value);
|
|
268
|
+
total += value.byteLength;
|
|
269
|
+
}
|
|
270
|
+
} finally {
|
|
271
|
+
releaseLock();
|
|
272
|
+
}
|
|
273
|
+
const out = new Uint8Array(total);
|
|
274
|
+
let offset = 0;
|
|
275
|
+
for (const chunk of chunks) {
|
|
276
|
+
out.set(chunk, offset);
|
|
277
|
+
offset += chunk.byteLength;
|
|
278
|
+
}
|
|
279
|
+
return {
|
|
280
|
+
text: new TextDecoder().decode(out),
|
|
281
|
+
truncated
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
function timeoutKillSignal(platform) {
|
|
285
|
+
return platform === "win32" ? "SIGTERM" : "SIGKILL";
|
|
286
|
+
}
|
|
287
|
+
function killProcess(proc) {
|
|
288
|
+
try {
|
|
289
|
+
proc?.kill(timeoutKillSignal(_internals.platform()));
|
|
290
|
+
} catch {}
|
|
291
|
+
}
|
|
292
|
+
async function runExternalTool(options) {
|
|
293
|
+
if (!path.isAbsolute(options.cwd)) {
|
|
294
|
+
return {
|
|
295
|
+
status: "spawn-error",
|
|
296
|
+
exitCode: null,
|
|
297
|
+
stdout: "",
|
|
298
|
+
stderr: "",
|
|
299
|
+
stdoutTruncated: false,
|
|
300
|
+
stderrTruncated: false,
|
|
301
|
+
message: "external tool cwd must be absolute"
|
|
302
|
+
};
|
|
303
|
+
}
|
|
304
|
+
if (options.abortSignal?.aborted) {
|
|
305
|
+
return {
|
|
306
|
+
status: "cancelled",
|
|
307
|
+
exitCode: null,
|
|
308
|
+
stdout: "",
|
|
309
|
+
stderr: "",
|
|
310
|
+
stdoutTruncated: false,
|
|
311
|
+
stderrTruncated: false,
|
|
312
|
+
message: "external tool execution cancelled before spawn"
|
|
313
|
+
};
|
|
314
|
+
}
|
|
315
|
+
let proc;
|
|
316
|
+
let timeoutHandle;
|
|
317
|
+
let abortListener;
|
|
318
|
+
let exitSettled = false;
|
|
319
|
+
let settledExitCode = null;
|
|
320
|
+
try {
|
|
321
|
+
proc = _internals.bunSpawn([options.executable, ...options.args], {
|
|
322
|
+
cwd: options.cwd,
|
|
323
|
+
env: options.env,
|
|
324
|
+
stdin: "ignore",
|
|
325
|
+
stdout: "pipe",
|
|
326
|
+
stderr: "pipe",
|
|
327
|
+
timeout: options.timeoutMs
|
|
328
|
+
});
|
|
329
|
+
const exitPromise = proc.exited.then((exitCode) => {
|
|
330
|
+
exitSettled = true;
|
|
331
|
+
settledExitCode = exitCode;
|
|
332
|
+
return { kind: "exit", exitCode };
|
|
333
|
+
});
|
|
334
|
+
const timeout = new Promise((resolve) => {
|
|
335
|
+
timeoutHandle = setTimeout(() => {
|
|
336
|
+
if (exitSettled) {
|
|
337
|
+
resolve({
|
|
338
|
+
kind: "exit",
|
|
339
|
+
exitCode: settledExitCode ?? proc?.exitCode ?? 0
|
|
340
|
+
});
|
|
341
|
+
return;
|
|
342
|
+
}
|
|
343
|
+
killProcess(proc);
|
|
344
|
+
resolve({ kind: "timeout" });
|
|
345
|
+
}, options.timeoutMs);
|
|
346
|
+
});
|
|
347
|
+
const cancelled = new Promise((resolve) => {
|
|
348
|
+
if (!options.abortSignal)
|
|
349
|
+
return;
|
|
350
|
+
let handled = false;
|
|
351
|
+
abortListener = () => {
|
|
352
|
+
if (handled)
|
|
353
|
+
return;
|
|
354
|
+
handled = true;
|
|
355
|
+
killProcess(proc);
|
|
356
|
+
resolve({ kind: "cancelled" });
|
|
357
|
+
};
|
|
358
|
+
options.abortSignal.addEventListener("abort", abortListener, {
|
|
359
|
+
once: true
|
|
360
|
+
});
|
|
361
|
+
if (options.abortSignal.aborted)
|
|
362
|
+
abortListener();
|
|
363
|
+
});
|
|
364
|
+
const stdoutPromise = readBoundedStream(proc.stdout, options.maxStdoutBytes);
|
|
365
|
+
const stderrPromise = readBoundedStream(proc.stderr, options.maxStderrBytes);
|
|
366
|
+
const exitResult = await Promise.race([exitPromise, timeout, cancelled]);
|
|
367
|
+
if (exitResult.kind === "timeout") {
|
|
368
|
+
return {
|
|
369
|
+
status: "timeout",
|
|
370
|
+
exitCode: proc.exitCode,
|
|
371
|
+
stdout: "",
|
|
372
|
+
stderr: "",
|
|
373
|
+
stdoutTruncated: false,
|
|
374
|
+
stderrTruncated: false
|
|
375
|
+
};
|
|
376
|
+
}
|
|
377
|
+
if (exitResult.kind === "cancelled") {
|
|
378
|
+
return {
|
|
379
|
+
status: "cancelled",
|
|
380
|
+
exitCode: proc.exitCode,
|
|
381
|
+
stdout: "",
|
|
382
|
+
stderr: "",
|
|
383
|
+
stdoutTruncated: false,
|
|
384
|
+
stderrTruncated: false,
|
|
385
|
+
message: "external tool execution cancelled"
|
|
386
|
+
};
|
|
387
|
+
}
|
|
388
|
+
const [stdout, stderr] = await Promise.all([stdoutPromise, stderrPromise]);
|
|
389
|
+
return {
|
|
390
|
+
status: "completed",
|
|
391
|
+
exitCode: exitResult.exitCode,
|
|
392
|
+
stdout: stdout.text,
|
|
393
|
+
stderr: stderr.text,
|
|
394
|
+
stdoutTruncated: stdout.truncated,
|
|
395
|
+
stderrTruncated: stderr.truncated
|
|
396
|
+
};
|
|
397
|
+
} catch (err) {
|
|
398
|
+
return {
|
|
399
|
+
status: "spawn-error",
|
|
400
|
+
exitCode: proc?.exitCode ?? null,
|
|
401
|
+
stdout: "",
|
|
402
|
+
stderr: "",
|
|
403
|
+
stdoutTruncated: false,
|
|
404
|
+
stderrTruncated: false,
|
|
405
|
+
message: err instanceof Error ? err.message : String(err)
|
|
406
|
+
};
|
|
407
|
+
} finally {
|
|
408
|
+
if (timeoutHandle !== undefined)
|
|
409
|
+
clearTimeout(timeoutHandle);
|
|
410
|
+
if (abortListener && options.abortSignal) {
|
|
411
|
+
options.abortSignal.removeEventListener("abort", abortListener);
|
|
412
|
+
}
|
|
413
|
+
if (proc) {
|
|
414
|
+
try {
|
|
415
|
+
proc.kill();
|
|
416
|
+
} catch {}
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
var _internals = {
|
|
421
|
+
bunSpawn,
|
|
422
|
+
platform: () => process.platform
|
|
423
|
+
};
|
|
424
|
+
|
|
425
|
+
// src/evaluation/disposable-worktree.ts
|
|
426
|
+
import * as crypto from "crypto";
|
|
427
|
+
import * as fs2 from "fs";
|
|
428
|
+
import * as os from "os";
|
|
429
|
+
import * as path2 from "path";
|
|
430
|
+
var GIT_TIMEOUT_MS = 30000;
|
|
431
|
+
var MAX_GIT_OUTPUT_BYTES = 4 * 1024 * 1024;
|
|
432
|
+
var WORKTREE_PARENT = "opencode-swarm-evaluation";
|
|
433
|
+
|
|
434
|
+
class DisposableWorktreeCleanupError extends Error {
|
|
435
|
+
worktreePath;
|
|
436
|
+
pathPresent;
|
|
437
|
+
registrationPresent;
|
|
438
|
+
cleanupErrors;
|
|
439
|
+
code = "evaluation-worktree-cleanup-failed";
|
|
440
|
+
constructor(worktreePath, pathPresent, registrationPresent, cleanupErrors) {
|
|
441
|
+
super(`Evaluation worktree cleanup could not be verified for ${worktreePath} ` + `(pathPresent=${pathPresent}, registrationPresent=${String(registrationPresent)})`);
|
|
442
|
+
this.worktreePath = worktreePath;
|
|
443
|
+
this.pathPresent = pathPresent;
|
|
444
|
+
this.registrationPresent = registrationPresent;
|
|
445
|
+
this.cleanupErrors = cleanupErrors;
|
|
446
|
+
this.name = "DisposableWorktreeCleanupError";
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
async function git(projectRoot, args, abortSignal) {
|
|
450
|
+
const executable = _internals2.resolveExecutableFromPath(["git"]);
|
|
451
|
+
if (!executable)
|
|
452
|
+
throw new Error("git executable not found");
|
|
453
|
+
const result = await _internals2.runExternalTool({
|
|
454
|
+
executable,
|
|
455
|
+
args: ["-C", projectRoot, ...args],
|
|
456
|
+
cwd: projectRoot,
|
|
457
|
+
timeoutMs: GIT_TIMEOUT_MS,
|
|
458
|
+
maxStdoutBytes: MAX_GIT_OUTPUT_BYTES,
|
|
459
|
+
maxStderrBytes: MAX_GIT_OUTPUT_BYTES,
|
|
460
|
+
abortSignal
|
|
461
|
+
});
|
|
462
|
+
if (result.status !== "completed" || result.exitCode !== 0) {
|
|
463
|
+
throw new Error(`git ${args[0] ?? ""} failed: ${result.message ?? result.stderr.trim() ?? result.status}`);
|
|
464
|
+
}
|
|
465
|
+
return { stdout: result.stdout, stderr: result.stderr };
|
|
466
|
+
}
|
|
467
|
+
async function captureWorkingTreeFingerprint(projectRoot, abortSignal) {
|
|
468
|
+
const canonicalRoot = fs2.realpathSync(projectRoot);
|
|
469
|
+
const head = await git(canonicalRoot, ["rev-parse", "HEAD"], abortSignal);
|
|
470
|
+
const porcelain = await git(canonicalRoot, [
|
|
471
|
+
"status",
|
|
472
|
+
"--porcelain=v1",
|
|
473
|
+
"-z",
|
|
474
|
+
"--untracked-files=all",
|
|
475
|
+
"--",
|
|
476
|
+
".",
|
|
477
|
+
":(exclude).swarm/**"
|
|
478
|
+
], abortSignal);
|
|
479
|
+
return {
|
|
480
|
+
head: head.stdout.trim(),
|
|
481
|
+
porcelainHash: crypto.createHash("sha256").update(porcelain.stdout).digest("hex")
|
|
482
|
+
};
|
|
483
|
+
}
|
|
484
|
+
async function createDisposableWorktree(projectRoot, baseRef, abortSignal) {
|
|
485
|
+
const canonicalRoot = fs2.realpathSync(projectRoot);
|
|
486
|
+
if (!/^[A-Fa-f0-9]{40,64}$/.test(baseRef)) {
|
|
487
|
+
throw new Error("Evaluation baseRef must be a full commit hash");
|
|
488
|
+
}
|
|
489
|
+
const parent = path2.join(_internals2.tmpdir(), WORKTREE_PARENT);
|
|
490
|
+
fs2.mkdirSync(parent, { recursive: true });
|
|
491
|
+
const canonicalParent = fs2.realpathSync(parent);
|
|
492
|
+
const worktreePath = path2.join(canonicalParent, crypto.randomUUID());
|
|
493
|
+
await git(canonicalRoot, ["worktree", "add", "--detach", worktreePath, baseRef], abortSignal);
|
|
494
|
+
return { path: fs2.realpathSync(worktreePath), baseSha: baseRef };
|
|
495
|
+
}
|
|
496
|
+
async function removeDisposableWorktree(projectRoot, worktreePath) {
|
|
497
|
+
const canonicalRoot = fs2.realpathSync(projectRoot);
|
|
498
|
+
const expectedParent = fs2.realpathSync(path2.join(_internals2.tmpdir(), WORKTREE_PARENT));
|
|
499
|
+
const resolved = path2.resolve(worktreePath);
|
|
500
|
+
const relative2 = path2.relative(expectedParent, resolved);
|
|
501
|
+
if (!relative2 || relative2 === ".." || relative2.startsWith(`..${path2.sep}`) || path2.isAbsolute(relative2)) {
|
|
502
|
+
throw new Error("Refusing to remove a worktree outside the evaluation root");
|
|
503
|
+
}
|
|
504
|
+
const cleanupErrors = [];
|
|
505
|
+
try {
|
|
506
|
+
await git(canonicalRoot, ["worktree", "remove", "--force", resolved]);
|
|
507
|
+
} catch (error) {
|
|
508
|
+
cleanupErrors.push(`git-remove: ${error instanceof Error ? error.message : String(error)}`);
|
|
509
|
+
}
|
|
510
|
+
try {
|
|
511
|
+
_internals2.rmSync(resolved, { recursive: true, force: true });
|
|
512
|
+
} catch (error) {
|
|
513
|
+
cleanupErrors.push(`filesystem-remove: ${error instanceof Error ? error.message : String(error)}`);
|
|
514
|
+
}
|
|
515
|
+
try {
|
|
516
|
+
await git(canonicalRoot, ["worktree", "prune", "--expire", "now"]);
|
|
517
|
+
} catch (error) {
|
|
518
|
+
cleanupErrors.push(`git-prune: ${error instanceof Error ? error.message : String(error)}`);
|
|
519
|
+
}
|
|
520
|
+
const pathPresent = _internals2.existsSync(resolved);
|
|
521
|
+
let registrationPresent;
|
|
522
|
+
try {
|
|
523
|
+
const listed = await git(canonicalRoot, [
|
|
524
|
+
"worktree",
|
|
525
|
+
"list",
|
|
526
|
+
"--porcelain",
|
|
527
|
+
"-z"
|
|
528
|
+
]);
|
|
529
|
+
const normalizedTarget = normalizeWorktreePath(resolved);
|
|
530
|
+
registrationPresent = listed.stdout.split("\x00").filter((field) => field.startsWith("worktree ")).some((field) => normalizeWorktreePath(field.slice("worktree ".length)) === normalizedTarget);
|
|
531
|
+
} catch (error) {
|
|
532
|
+
cleanupErrors.push(`git-list: ${error instanceof Error ? error.message : String(error)}`);
|
|
533
|
+
}
|
|
534
|
+
if (pathPresent || registrationPresent !== false) {
|
|
535
|
+
throw new DisposableWorktreeCleanupError(resolved, pathPresent, registrationPresent, cleanupErrors);
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
function normalizeWorktreePath(value) {
|
|
539
|
+
const resolved = path2.resolve(value);
|
|
540
|
+
return process.platform === "win32" ? resolved.toLowerCase() : resolved;
|
|
541
|
+
}
|
|
542
|
+
async function withDisposableWorktree(args) {
|
|
543
|
+
const before = await _internals2.captureWorkingTreeFingerprint(args.projectRoot, args.abortSignal);
|
|
544
|
+
let worktree;
|
|
545
|
+
try {
|
|
546
|
+
worktree = await _internals2.createDisposableWorktree(args.projectRoot, args.baseRef, args.abortSignal);
|
|
547
|
+
return await args.run(worktree);
|
|
548
|
+
} finally {
|
|
549
|
+
let cleanupFailure;
|
|
550
|
+
if (worktree) {
|
|
551
|
+
try {
|
|
552
|
+
await _internals2.removeDisposableWorktree(args.projectRoot, worktree.path);
|
|
553
|
+
} catch (error) {
|
|
554
|
+
cleanupFailure = error;
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
const after = await _internals2.captureWorkingTreeFingerprint(args.projectRoot);
|
|
558
|
+
if (before.head !== after.head || before.porcelainHash !== after.porcelainHash) {
|
|
559
|
+
throw new Error("Evaluation changed the active working tree");
|
|
560
|
+
}
|
|
561
|
+
if (cleanupFailure) {
|
|
562
|
+
throw cleanupFailure;
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
var _internals2 = {
|
|
567
|
+
runExternalTool,
|
|
568
|
+
resolveExecutableFromPath,
|
|
569
|
+
tmpdir: os.tmpdir,
|
|
570
|
+
rmSync: fs2.rmSync,
|
|
571
|
+
existsSync: fs2.existsSync,
|
|
572
|
+
captureWorkingTreeFingerprint,
|
|
573
|
+
createDisposableWorktree,
|
|
574
|
+
removeDisposableWorktree
|
|
575
|
+
};
|
|
576
|
+
|
|
577
|
+
// src/evaluation/runner.ts
|
|
578
|
+
var ScorerOutputSchema = exports_external.object({
|
|
579
|
+
v: exports_external.literal(1),
|
|
580
|
+
score: exports_external.number().finite(),
|
|
581
|
+
cost: CostRecordSchema,
|
|
582
|
+
metadata: exports_external.record(exports_external.string(), exports_external.unknown()).optional()
|
|
583
|
+
}).strict();
|
|
584
|
+
var BuiltinVerdictSchema = exports_external.object({
|
|
585
|
+
v: exports_external.literal(1),
|
|
586
|
+
caught: exports_external.boolean(),
|
|
587
|
+
reason: exports_external.string().optional()
|
|
588
|
+
}).strict();
|
|
589
|
+
|
|
590
|
+
class ScorerFailure extends Error {
|
|
591
|
+
outcome;
|
|
592
|
+
failureCode;
|
|
593
|
+
retryable;
|
|
594
|
+
constructor(outcome, failureCode, retryable) {
|
|
595
|
+
super(failureCode);
|
|
596
|
+
this.outcome = outcome;
|
|
597
|
+
this.failureCode = failureCode;
|
|
598
|
+
this.retryable = retryable;
|
|
599
|
+
this.name = "ScorerFailure";
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
function aggregateCost(results) {
|
|
603
|
+
if (results.some((result) => result.cost.source === "unavailable")) {
|
|
604
|
+
return { source: "unavailable" };
|
|
605
|
+
}
|
|
606
|
+
return {
|
|
607
|
+
source: results.some((result) => result.cost.source === "estimated") ? "estimated" : "reported",
|
|
608
|
+
usd: results.reduce((sum, result) => sum + (result.cost.usd ?? 0), 0)
|
|
609
|
+
};
|
|
610
|
+
}
|
|
611
|
+
function failureResult(args) {
|
|
612
|
+
return {
|
|
613
|
+
v: 1,
|
|
614
|
+
taskId: args.task.id,
|
|
615
|
+
category: args.task.category,
|
|
616
|
+
protected: args.task.protected,
|
|
617
|
+
repetition: args.repetition,
|
|
618
|
+
candidateId: args.candidate.id,
|
|
619
|
+
seed: args.seed,
|
|
620
|
+
outcome: args.outcome,
|
|
621
|
+
scoreRange: args.task.scorer.scoreRange,
|
|
622
|
+
cost: args.cost ?? { source: "unavailable" },
|
|
623
|
+
durationMs: args.durationMs,
|
|
624
|
+
retries: 0,
|
|
625
|
+
failureCode: args.failureCode
|
|
626
|
+
};
|
|
627
|
+
}
|
|
628
|
+
function parseBuiltinScore(text) {
|
|
629
|
+
let value;
|
|
630
|
+
try {
|
|
631
|
+
value = JSON.parse(text.trim());
|
|
632
|
+
} catch {
|
|
633
|
+
throw new ScorerFailure("malformed", "builtin-invalid-json", false);
|
|
634
|
+
}
|
|
635
|
+
const parsed = BuiltinVerdictSchema.safeParse(value);
|
|
636
|
+
if (!parsed.success) {
|
|
637
|
+
throw new ScorerFailure("malformed", "builtin-schema-invalid", false);
|
|
638
|
+
}
|
|
639
|
+
const verdict = parsed.data;
|
|
640
|
+
return {
|
|
641
|
+
v: 1,
|
|
642
|
+
score: verdict.caught ? 1 : 0,
|
|
643
|
+
cost: { source: "unavailable" },
|
|
644
|
+
metadata: verdict.reason ? { reason: verdict.reason } : undefined
|
|
645
|
+
};
|
|
646
|
+
}
|
|
647
|
+
async function scoreExecution(args) {
|
|
648
|
+
if (args.task.scorer.kind === "builtin")
|
|
649
|
+
return parseBuiltinScore(args.execution.text);
|
|
650
|
+
const sourceEnvironment = resolveContainedExistingPath(args.inputRoot, args.task.environment.path);
|
|
651
|
+
const sourceExecutable = resolveContainedExistingPath(args.inputRoot, args.task.scorer.argv[0]);
|
|
652
|
+
const scorerRelative = path3.relative(sourceEnvironment, sourceExecutable);
|
|
653
|
+
if (!scorerRelative || scorerRelative === ".." || scorerRelative.startsWith(`..${path3.sep}`) || path3.isAbsolute(scorerRelative)) {
|
|
654
|
+
throw new ScorerFailure("malformed", "scorer-outside-task-package", false);
|
|
655
|
+
}
|
|
656
|
+
const isolatedExecutable = resolveContainedExistingPath(args.isolatedRoot, scorerRelative);
|
|
657
|
+
if (!fs3.statSync(isolatedExecutable).isFile()) {
|
|
658
|
+
throw new ScorerFailure("malformed", "scorer-not-a-file", false);
|
|
659
|
+
}
|
|
660
|
+
const extension = path3.extname(isolatedExecutable).toLowerCase();
|
|
661
|
+
const isJavaScript = [".cjs", ".js", ".mjs"].includes(extension);
|
|
662
|
+
const executable = isJavaScript ? process.execPath : isolatedExecutable;
|
|
663
|
+
const scorerArgs = isJavaScript ? [isolatedExecutable, ...args.task.scorer.argv.slice(1)] : args.task.scorer.argv.slice(1);
|
|
664
|
+
const artifactDirectory = path3.join(args.isolatedRoot, ".artifacts");
|
|
665
|
+
fs3.mkdirSync(artifactDirectory, { recursive: true });
|
|
666
|
+
fs3.writeFileSync(path3.join(artifactDirectory, "model-output.json"), JSON.stringify({ v: 1, text: args.execution.text }));
|
|
667
|
+
const result = await _internals3.runExternalTool({
|
|
668
|
+
executable,
|
|
669
|
+
args: scorerArgs,
|
|
670
|
+
cwd: args.isolatedRoot,
|
|
671
|
+
timeoutMs: Math.min(args.task.scorer.timeoutMs, args.timeoutMs),
|
|
672
|
+
maxStdoutBytes: args.maxOutputBytes,
|
|
673
|
+
maxStderrBytes: args.maxOutputBytes,
|
|
674
|
+
abortSignal: args.abortSignal,
|
|
675
|
+
env: {
|
|
676
|
+
...process.env,
|
|
677
|
+
SWARM_EVAL_TASK_ID: args.task.id,
|
|
678
|
+
SWARM_EVAL_CANDIDATE_ID: args.candidate.id,
|
|
679
|
+
SWARM_EVAL_ARTIFACT_DIR: artifactDirectory,
|
|
680
|
+
SWARM_EVAL_SEED: args.seed
|
|
681
|
+
}
|
|
682
|
+
});
|
|
683
|
+
if (result.status === "timeout")
|
|
684
|
+
throw new ScorerFailure("timeout", "scorer-timeout", false);
|
|
685
|
+
if (result.status === "cancelled")
|
|
686
|
+
throw new ScorerFailure("cancelled", "scorer-cancelled", false);
|
|
687
|
+
if (result.status === "spawn-error")
|
|
688
|
+
throw new ScorerFailure("infrastructure_failure", "scorer-spawn-failure", true);
|
|
689
|
+
if (result.exitCode !== 0) {
|
|
690
|
+
throw new ScorerFailure("infrastructure_failure", `scorer-exit-${result.exitCode ?? "none"}`, true);
|
|
691
|
+
}
|
|
692
|
+
if (result.stdoutTruncated || result.stderrTruncated) {
|
|
693
|
+
throw new ScorerFailure("malformed", "scorer-output-truncated", false);
|
|
694
|
+
}
|
|
695
|
+
let value;
|
|
696
|
+
try {
|
|
697
|
+
value = JSON.parse(result.stdout.trim());
|
|
698
|
+
} catch {
|
|
699
|
+
throw new ScorerFailure("malformed", "scorer-invalid-json", false);
|
|
700
|
+
}
|
|
701
|
+
const parsed = ScorerOutputSchema.safeParse(value);
|
|
702
|
+
if (!parsed.success) {
|
|
703
|
+
throw new ScorerFailure("malformed", "scorer-schema-invalid", false);
|
|
704
|
+
}
|
|
705
|
+
return parsed.data;
|
|
706
|
+
}
|
|
707
|
+
async function runExecutorWithinBudget(args) {
|
|
708
|
+
const startedAt = Date.now();
|
|
709
|
+
const controller = new AbortController;
|
|
710
|
+
let timedOut = false;
|
|
711
|
+
const parentAbort = () => controller.abort();
|
|
712
|
+
args.parentSignal.addEventListener("abort", parentAbort, { once: true });
|
|
713
|
+
if (args.parentSignal.aborted)
|
|
714
|
+
controller.abort();
|
|
715
|
+
const timer = setTimeout(() => {
|
|
716
|
+
timedOut = true;
|
|
717
|
+
controller.abort();
|
|
718
|
+
}, args.maxTimeMs);
|
|
719
|
+
try {
|
|
720
|
+
const aborted = new Promise((resolve2) => {
|
|
721
|
+
const finish = () => resolve2({
|
|
722
|
+
status: timedOut ? "timeout" : "cancelled",
|
|
723
|
+
text: "",
|
|
724
|
+
durationMs: Date.now() - startedAt,
|
|
725
|
+
cost: { source: "unavailable" },
|
|
726
|
+
error: timedOut ? "executor-task-timeout" : "executor-cancelled"
|
|
727
|
+
});
|
|
728
|
+
controller.signal.addEventListener("abort", finish, { once: true });
|
|
729
|
+
if (controller.signal.aborted)
|
|
730
|
+
finish();
|
|
731
|
+
});
|
|
732
|
+
return await Promise.race([
|
|
733
|
+
args.options.executor({
|
|
734
|
+
task: args.task,
|
|
735
|
+
candidate: args.candidate,
|
|
736
|
+
isolatedRoot: args.isolatedRoot,
|
|
737
|
+
instruction: args.instruction,
|
|
738
|
+
payload: args.payload,
|
|
739
|
+
seed: args.seed,
|
|
740
|
+
abortSignal: controller.signal
|
|
741
|
+
}),
|
|
742
|
+
aborted
|
|
743
|
+
]);
|
|
744
|
+
} finally {
|
|
745
|
+
clearTimeout(timer);
|
|
746
|
+
args.parentSignal.removeEventListener("abort", parentAbort);
|
|
747
|
+
controller.abort();
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
async function executePairMember(args) {
|
|
751
|
+
const startedAt = Date.now();
|
|
752
|
+
const seed = canonicalHash(`${args.options.seed}:${args.task.id}:${args.candidate.id}:${args.repetition}`);
|
|
753
|
+
if (args.signal.aborted) {
|
|
754
|
+
return failureResult({
|
|
755
|
+
...args,
|
|
756
|
+
seed,
|
|
757
|
+
outcome: "cancelled",
|
|
758
|
+
durationMs: 0,
|
|
759
|
+
failureCode: "cancelled-before-execution"
|
|
760
|
+
});
|
|
761
|
+
}
|
|
762
|
+
if (args.task.environment.kind === "container") {
|
|
763
|
+
return failureResult({
|
|
764
|
+
...args,
|
|
765
|
+
seed,
|
|
766
|
+
outcome: "unsupported",
|
|
767
|
+
durationMs: 0,
|
|
768
|
+
failureCode: "container-runner-unavailable"
|
|
769
|
+
});
|
|
770
|
+
}
|
|
771
|
+
try {
|
|
772
|
+
return await _internals3.withDisposableWorktree({
|
|
773
|
+
projectRoot: args.options.projectRoot,
|
|
774
|
+
baseRef: args.baseSha,
|
|
775
|
+
abortSignal: args.signal,
|
|
776
|
+
run: async (worktree) => {
|
|
777
|
+
const isolatedRoot = path3.join(worktree.path, ".swarm-evaluation-task");
|
|
778
|
+
fs3.mkdirSync(isolatedRoot, { recursive: true });
|
|
779
|
+
try {
|
|
780
|
+
fs3.cpSync(resolveContainedExistingPath(args.inputRoot, args.task.environment.path), isolatedRoot, { recursive: true, errorOnExist: false });
|
|
781
|
+
const instruction = fs3.readFileSync(resolveContainedExistingPath(args.inputRoot, args.task.instructionPath), "utf8");
|
|
782
|
+
const payload = fs3.readFileSync(resolveContainedExistingPath(args.inputRoot, args.candidate.payloadPath), "utf8");
|
|
783
|
+
const remainingBeforeExecution = args.options.budgets.maxTaskTimeMs - (Date.now() - startedAt);
|
|
784
|
+
if (remainingBeforeExecution <= 0) {
|
|
785
|
+
return failureResult({
|
|
786
|
+
...args,
|
|
787
|
+
seed,
|
|
788
|
+
outcome: "timeout",
|
|
789
|
+
durationMs: Date.now() - startedAt,
|
|
790
|
+
failureCode: "task-budget-exhausted-before-executor"
|
|
791
|
+
});
|
|
792
|
+
}
|
|
793
|
+
const execution = await runExecutorWithinBudget({
|
|
794
|
+
options: args.options,
|
|
795
|
+
task: args.task,
|
|
796
|
+
candidate: args.candidate,
|
|
797
|
+
isolatedRoot,
|
|
798
|
+
instruction,
|
|
799
|
+
payload,
|
|
800
|
+
seed,
|
|
801
|
+
parentSignal: args.signal,
|
|
802
|
+
maxTimeMs: remainingBeforeExecution
|
|
803
|
+
});
|
|
804
|
+
if (execution.status !== "completed") {
|
|
805
|
+
return failureResult({
|
|
806
|
+
...args,
|
|
807
|
+
seed,
|
|
808
|
+
outcome: execution.status === "infrastructure_failure" ? "infrastructure_failure" : execution.status,
|
|
809
|
+
durationMs: execution.durationMs,
|
|
810
|
+
cost: execution.cost,
|
|
811
|
+
failureCode: execution.error ?? `executor-${execution.status}`
|
|
812
|
+
});
|
|
813
|
+
}
|
|
814
|
+
if (Buffer.byteLength(execution.text, "utf8") > args.options.budgets.maxOutputBytes) {
|
|
815
|
+
return failureResult({
|
|
816
|
+
...args,
|
|
817
|
+
seed,
|
|
818
|
+
outcome: "malformed",
|
|
819
|
+
durationMs: Date.now() - startedAt,
|
|
820
|
+
cost: execution.cost,
|
|
821
|
+
failureCode: "executor-output-truncated"
|
|
822
|
+
});
|
|
823
|
+
}
|
|
824
|
+
let scored;
|
|
825
|
+
try {
|
|
826
|
+
const remainingBeforeScorer = args.options.budgets.maxTaskTimeMs - (Date.now() - startedAt);
|
|
827
|
+
if (remainingBeforeScorer <= 0) {
|
|
828
|
+
throw new ScorerFailure("timeout", "task-budget-exhausted-before-scorer", false);
|
|
829
|
+
}
|
|
830
|
+
scored = await scoreExecution({
|
|
831
|
+
task: args.task,
|
|
832
|
+
candidate: args.candidate,
|
|
833
|
+
inputRoot: args.inputRoot,
|
|
834
|
+
isolatedRoot,
|
|
835
|
+
execution,
|
|
836
|
+
seed,
|
|
837
|
+
maxOutputBytes: args.options.budgets.maxOutputBytes,
|
|
838
|
+
timeoutMs: remainingBeforeScorer,
|
|
839
|
+
abortSignal: args.signal
|
|
840
|
+
});
|
|
841
|
+
} catch (error) {
|
|
842
|
+
const scorerFailure = error instanceof ScorerFailure ? error : undefined;
|
|
843
|
+
return failureResult({
|
|
844
|
+
...args,
|
|
845
|
+
seed,
|
|
846
|
+
outcome: args.signal.aborted ? "cancelled" : scorerFailure?.outcome ?? "malformed",
|
|
847
|
+
durationMs: Date.now() - startedAt,
|
|
848
|
+
cost: execution.cost,
|
|
849
|
+
failureCode: scorerFailure?.failureCode ?? (error instanceof Error ? error.message.slice(0, 160) : "scorer-error")
|
|
850
|
+
});
|
|
851
|
+
}
|
|
852
|
+
const [minimum, maximum] = args.task.scorer.scoreRange;
|
|
853
|
+
if (scored.score < minimum || scored.score > maximum) {
|
|
854
|
+
return failureResult({
|
|
855
|
+
...args,
|
|
856
|
+
seed,
|
|
857
|
+
outcome: "malformed",
|
|
858
|
+
durationMs: Date.now() - startedAt,
|
|
859
|
+
cost: scored.cost,
|
|
860
|
+
failureCode: "score-out-of-range"
|
|
861
|
+
});
|
|
862
|
+
}
|
|
863
|
+
return {
|
|
864
|
+
v: 1,
|
|
865
|
+
taskId: args.task.id,
|
|
866
|
+
category: args.task.category,
|
|
867
|
+
protected: args.task.protected,
|
|
868
|
+
repetition: args.repetition,
|
|
869
|
+
candidateId: args.candidate.id,
|
|
870
|
+
seed,
|
|
871
|
+
outcome: "scored",
|
|
872
|
+
score: scored.score,
|
|
873
|
+
scoreRange: args.task.scorer.scoreRange,
|
|
874
|
+
cost: scored.cost,
|
|
875
|
+
durationMs: Date.now() - startedAt,
|
|
876
|
+
retries: 0,
|
|
877
|
+
metadata: scored.metadata
|
|
878
|
+
};
|
|
879
|
+
} finally {
|
|
880
|
+
fs3.rmSync(isolatedRoot, { recursive: true, force: true });
|
|
881
|
+
}
|
|
882
|
+
}
|
|
883
|
+
});
|
|
884
|
+
} catch (error) {
|
|
885
|
+
return failureResult({
|
|
886
|
+
...args,
|
|
887
|
+
seed,
|
|
888
|
+
outcome: "infrastructure_failure",
|
|
889
|
+
durationMs: Date.now() - startedAt,
|
|
890
|
+
failureCode: canonicalHash(String(error)).slice(0, 32)
|
|
891
|
+
});
|
|
892
|
+
}
|
|
893
|
+
}
|
|
894
|
+
async function executePairMemberWithRetries(args) {
|
|
895
|
+
let result;
|
|
896
|
+
for (let attempt = 0;attempt <= args.options.budgets.maxRetries; attempt++) {
|
|
897
|
+
result = await executePairMember(args);
|
|
898
|
+
result.retries = attempt;
|
|
899
|
+
if (result.outcome !== "infrastructure_failure" || args.signal.aborted) {
|
|
900
|
+
return result;
|
|
901
|
+
}
|
|
902
|
+
}
|
|
903
|
+
return result;
|
|
904
|
+
}
|
|
905
|
+
async function runEvaluation(options) {
|
|
906
|
+
const inputRoot = options.inputRoot ?? options.projectRoot;
|
|
907
|
+
const tasks = options.tasks.map((task) => EvaluationTaskV1Schema.parse(task));
|
|
908
|
+
if (tasks.length === 0 || tasks.length > options.budgets.maxTasks) {
|
|
909
|
+
throw new Error("evaluation task count exceeds its declared budget");
|
|
910
|
+
}
|
|
911
|
+
if (tasks.some((task) => task.split !== options.split)) {
|
|
912
|
+
throw new Error("evaluation task split does not match the run split");
|
|
913
|
+
}
|
|
914
|
+
const baseline = EvaluationCandidateV1Schema.parse(options.baseline);
|
|
915
|
+
const candidate = EvaluationCandidateV1Schema.parse(options.candidate);
|
|
916
|
+
for (const item of [baseline, candidate]) {
|
|
917
|
+
if (await computeCandidateInputContentHash(inputRoot, item) !== item.contentHash) {
|
|
918
|
+
throw new Error(`candidate ${item.id} content hash is invalid`);
|
|
919
|
+
}
|
|
920
|
+
}
|
|
921
|
+
const snapshotDraft = {
|
|
922
|
+
v: 1,
|
|
923
|
+
id: `taskset-${canonicalHash(tasks.map((task) => task.id)).slice(0, 20)}`,
|
|
924
|
+
split: options.split,
|
|
925
|
+
tasks,
|
|
926
|
+
createdAt: new Date().toISOString()
|
|
927
|
+
};
|
|
928
|
+
for (const task of tasks) {
|
|
929
|
+
await admitEvaluationTask(options.projectRoot, task, inputRoot);
|
|
930
|
+
}
|
|
931
|
+
const taskSet = {
|
|
932
|
+
...snapshotDraft,
|
|
933
|
+
contentHash: computeTaskSetContentHash(snapshotDraft)
|
|
934
|
+
};
|
|
935
|
+
const storedTaskSet = await saveTaskSetSnapshot(options.projectRoot, taskSet, inputRoot);
|
|
936
|
+
const runId = `eval-${canonicalHash({
|
|
937
|
+
baseline: baseline.contentHash,
|
|
938
|
+
candidate: candidate.contentHash,
|
|
939
|
+
taskSet: storedTaskSet.contentHash,
|
|
940
|
+
split: options.split,
|
|
941
|
+
seed: options.seed,
|
|
942
|
+
models: options.models,
|
|
943
|
+
budgets: options.budgets
|
|
944
|
+
}).slice(0, 32)}`;
|
|
945
|
+
const existingRun = await readEvaluationRun(options.projectRoot, runId);
|
|
946
|
+
if (existingRun)
|
|
947
|
+
return existingRun;
|
|
948
|
+
if (options.split === "test") {
|
|
949
|
+
await claimHeldOutTest(options.projectRoot, {
|
|
950
|
+
v: 1,
|
|
951
|
+
runId,
|
|
952
|
+
taskSetHash: storedTaskSet.contentHash,
|
|
953
|
+
baselineHash: baseline.contentHash,
|
|
954
|
+
candidateHash: candidate.contentHash,
|
|
955
|
+
claimedAt: new Date().toISOString()
|
|
956
|
+
});
|
|
957
|
+
}
|
|
958
|
+
const before = await _internals3.captureWorkingTreeFingerprint(options.projectRoot);
|
|
959
|
+
const controller = new AbortController;
|
|
960
|
+
let externallyCancelled = options.abortSignal?.aborted ?? false;
|
|
961
|
+
let timedOut = false;
|
|
962
|
+
let budgetInconclusive = false;
|
|
963
|
+
let scheduledSpendUsd = 0;
|
|
964
|
+
const externalAbort = () => {
|
|
965
|
+
externallyCancelled = true;
|
|
966
|
+
controller.abort();
|
|
967
|
+
};
|
|
968
|
+
options.abortSignal?.addEventListener("abort", externalAbort, { once: true });
|
|
969
|
+
if (externallyCancelled)
|
|
970
|
+
controller.abort();
|
|
971
|
+
const totalExecutions = tasks.length * 2 * options.budgets.maxRepetitions;
|
|
972
|
+
const executionWaves = Math.ceil(totalExecutions / options.budgets.maxConcurrency);
|
|
973
|
+
const totalTimeoutMs = Math.min(2147483647, options.budgets.maxTaskTimeMs * executionWaves * (options.budgets.maxRetries + 1) + 1000);
|
|
974
|
+
const timeout = setTimeout(() => {
|
|
975
|
+
timedOut = true;
|
|
976
|
+
controller.abort();
|
|
977
|
+
}, totalTimeoutMs);
|
|
978
|
+
try {
|
|
979
|
+
const limit = pLimit(options.budgets.maxConcurrency);
|
|
980
|
+
const jobs = [];
|
|
981
|
+
for (const task of tasks) {
|
|
982
|
+
for (const item of [baseline, candidate]) {
|
|
983
|
+
for (let repetition = 0;repetition < options.budgets.maxRepetitions; repetition++) {
|
|
984
|
+
jobs.push(limit(() => executePairMemberWithRetries({
|
|
985
|
+
options,
|
|
986
|
+
inputRoot,
|
|
987
|
+
task,
|
|
988
|
+
candidate: item,
|
|
989
|
+
repetition,
|
|
990
|
+
signal: controller.signal,
|
|
991
|
+
baseSha: before.head
|
|
992
|
+
}).then((result) => {
|
|
993
|
+
if (options.budgets.maxSpendUsd !== undefined) {
|
|
994
|
+
if (result.cost.source === "unavailable") {
|
|
995
|
+
budgetInconclusive = true;
|
|
996
|
+
controller.abort();
|
|
997
|
+
} else {
|
|
998
|
+
scheduledSpendUsd += result.cost.usd ?? 0;
|
|
999
|
+
if (scheduledSpendUsd > options.budgets.maxSpendUsd) {
|
|
1000
|
+
budgetInconclusive = true;
|
|
1001
|
+
controller.abort();
|
|
1002
|
+
}
|
|
1003
|
+
}
|
|
1004
|
+
}
|
|
1005
|
+
return result;
|
|
1006
|
+
})));
|
|
1007
|
+
}
|
|
1008
|
+
}
|
|
1009
|
+
}
|
|
1010
|
+
const results = await Promise.all(jobs);
|
|
1011
|
+
for (const task of tasks) {
|
|
1012
|
+
if (await computeTaskInputContentHash(inputRoot, task) !== task.contentHash) {
|
|
1013
|
+
throw new Error(`task ${task.id} changed during evaluation`);
|
|
1014
|
+
}
|
|
1015
|
+
}
|
|
1016
|
+
for (const item of [baseline, candidate]) {
|
|
1017
|
+
if (await computeCandidateInputContentHash(inputRoot, item) !== item.contentHash) {
|
|
1018
|
+
throw new Error(`candidate ${item.id} changed during evaluation`);
|
|
1019
|
+
}
|
|
1020
|
+
}
|
|
1021
|
+
const cost = aggregateCost(results);
|
|
1022
|
+
const spendUnknown = options.budgets.maxSpendUsd !== undefined && cost.source === "unavailable";
|
|
1023
|
+
const overSpend = options.budgets.maxSpendUsd !== undefined && cost.source !== "unavailable" && (cost.usd ?? 0) > options.budgets.maxSpendUsd;
|
|
1024
|
+
const incomplete = results.some((result) => result.outcome !== "scored");
|
|
1025
|
+
const runDraft = {
|
|
1026
|
+
v: 1,
|
|
1027
|
+
runId,
|
|
1028
|
+
createdAt: new Date().toISOString(),
|
|
1029
|
+
status: externallyCancelled || timedOut ? "cancelled" : incomplete || spendUnknown || overSpend || budgetInconclusive ? "inconclusive" : "complete",
|
|
1030
|
+
baseline,
|
|
1031
|
+
candidate,
|
|
1032
|
+
taskSet: {
|
|
1033
|
+
id: storedTaskSet.id,
|
|
1034
|
+
contentHash: storedTaskSet.contentHash,
|
|
1035
|
+
taskIds: tasks.map((task) => task.id)
|
|
1036
|
+
},
|
|
1037
|
+
split: options.split,
|
|
1038
|
+
seed: options.seed,
|
|
1039
|
+
models: options.models,
|
|
1040
|
+
environment: {
|
|
1041
|
+
platform: process.platform,
|
|
1042
|
+
arch: process.arch,
|
|
1043
|
+
runtime: `node-${process.versions.node};bun-${process.versions.bun ?? "compat"}`,
|
|
1044
|
+
baseSha: before.head,
|
|
1045
|
+
activeTreeHash: canonicalHash(before),
|
|
1046
|
+
taskSetHash: storedTaskSet.contentHash
|
|
1047
|
+
},
|
|
1048
|
+
budgets: options.budgets,
|
|
1049
|
+
results,
|
|
1050
|
+
cost
|
|
1051
|
+
};
|
|
1052
|
+
const run = EvaluationRunV1Schema.parse({
|
|
1053
|
+
...runDraft,
|
|
1054
|
+
integrityHash: computeRunIntegrityHash(runDraft)
|
|
1055
|
+
});
|
|
1056
|
+
await saveEvaluationRun(options.projectRoot, run);
|
|
1057
|
+
return run;
|
|
1058
|
+
} finally {
|
|
1059
|
+
clearTimeout(timeout);
|
|
1060
|
+
options.abortSignal?.removeEventListener("abort", externalAbort);
|
|
1061
|
+
const after = await _internals3.captureWorkingTreeFingerprint(options.projectRoot);
|
|
1062
|
+
if (before.head !== after.head || before.porcelainHash !== after.porcelainHash) {
|
|
1063
|
+
throw new Error("evaluation changed the active working tree");
|
|
1064
|
+
}
|
|
1065
|
+
}
|
|
1066
|
+
}
|
|
1067
|
+
var _internals3 = {
|
|
1068
|
+
captureWorkingTreeFingerprint,
|
|
1069
|
+
runExternalTool,
|
|
1070
|
+
withDisposableWorktree
|
|
1071
|
+
};
|
|
1072
|
+
function createModelEvaluationExecutor(dispatcher, parentSessionId) {
|
|
1073
|
+
return async (args) => {
|
|
1074
|
+
const result = await dispatcher({
|
|
1075
|
+
directory: args.isolatedRoot,
|
|
1076
|
+
agentName: args.candidate.agent ?? "reviewer",
|
|
1077
|
+
modelId: args.candidate.model,
|
|
1078
|
+
system: args.payload,
|
|
1079
|
+
prompt: [
|
|
1080
|
+
"EVALUATION TASK:",
|
|
1081
|
+
args.instruction,
|
|
1082
|
+
"",
|
|
1083
|
+
"Return the task-required scorer payload exactly."
|
|
1084
|
+
].join(`
|
|
1085
|
+
`),
|
|
1086
|
+
timeoutMs: args.task.scorer.timeoutMs,
|
|
1087
|
+
parentSessionId,
|
|
1088
|
+
abortSignal: args.abortSignal
|
|
1089
|
+
});
|
|
1090
|
+
return {
|
|
1091
|
+
status: result.status === "error" ? "infrastructure_failure" : result.status,
|
|
1092
|
+
text: result.text,
|
|
1093
|
+
durationMs: result.durationMs,
|
|
1094
|
+
cost: { source: "unavailable" },
|
|
1095
|
+
error: result.error
|
|
1096
|
+
};
|
|
1097
|
+
};
|
|
1098
|
+
}
|
|
1099
|
+
|
|
1100
|
+
export { resolveExecutableFromPath, runExternalTool, pLimit, captureWorkingTreeFingerprint, runEvaluation, _internals3 as _internals, createModelEvaluationExecutor };
|