reeval 0.0.1

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 (213) hide show
  1. package/LICENSE +202 -0
  2. package/THIRD_PARTY_NOTICES.md +2160 -0
  3. package/assets/package.json +9 -0
  4. package/assets/pnpm-lock.yaml +1336 -0
  5. package/dist/agent-DVHkx8aT.js +532 -0
  6. package/dist/agent-DVHkx8aT.js.map +1 -0
  7. package/dist/analysis-CxSCt1YY.js +16 -0
  8. package/dist/analysis-CxSCt1YY.js.map +1 -0
  9. package/dist/assemble-B-nmz8oH.js +695 -0
  10. package/dist/assemble-B-nmz8oH.js.map +1 -0
  11. package/dist/assemble-CmRVDZ2Q.js +2 -0
  12. package/dist/attempt-BdRQ9jKX.js +176 -0
  13. package/dist/attempt-BdRQ9jKX.js.map +1 -0
  14. package/dist/bridge/index.mjs +1911 -0
  15. package/dist/bridge/package.json +13 -0
  16. package/dist/bridge/pnpm-lock.yaml +1041 -0
  17. package/dist/bridge/pnpm-workspace.yaml +2 -0
  18. package/dist/budget-DBUrH4jX.js +8 -0
  19. package/dist/budget-DBUrH4jX.js.map +1 -0
  20. package/dist/cli--CQ3Iy9q.js +1865 -0
  21. package/dist/cli--CQ3Iy9q.js.map +1 -0
  22. package/dist/cli-spec-DKAu09Gi.js +478 -0
  23. package/dist/cli-spec-DKAu09Gi.js.map +1 -0
  24. package/dist/cli.d.ts +1 -0
  25. package/dist/cli.js +8 -0
  26. package/dist/cli.js.map +1 -0
  27. package/dist/colors-QV9dFVhN.js +34 -0
  28. package/dist/colors-QV9dFVhN.js.map +1 -0
  29. package/dist/command-BSJzHMtP.js +280 -0
  30. package/dist/command-BSJzHMtP.js.map +1 -0
  31. package/dist/command-CEm8PZ1N.js +1043 -0
  32. package/dist/command-CEm8PZ1N.js.map +1 -0
  33. package/dist/command-D-jnQWVG.js +673 -0
  34. package/dist/command-D-jnQWVG.js.map +1 -0
  35. package/dist/command-DQAvbG7d.js +432 -0
  36. package/dist/command-DQAvbG7d.js.map +1 -0
  37. package/dist/commands-A4udfyKZ.js +2 -0
  38. package/dist/commands-CF8mOfiu.js +207 -0
  39. package/dist/commands-CF8mOfiu.js.map +1 -0
  40. package/dist/config-Cuj0kVYj.js +415 -0
  41. package/dist/config-Cuj0kVYj.js.map +1 -0
  42. package/dist/context-loi9OCZv.js +106 -0
  43. package/dist/context-loi9OCZv.js.map +1 -0
  44. package/dist/core-tLN-z1Be.js +96 -0
  45. package/dist/core-tLN-z1Be.js.map +1 -0
  46. package/dist/db-Cjj2ZsfQ.js +2 -0
  47. package/dist/db-DxDzMhtB.js +827 -0
  48. package/dist/db-DxDzMhtB.js.map +1 -0
  49. package/dist/debug-D24NOQH1.js +89 -0
  50. package/dist/debug-D24NOQH1.js.map +1 -0
  51. package/dist/debug-SEmW4w8f.js +2 -0
  52. package/dist/dist-BZXYHN2X.js +1673 -0
  53. package/dist/dist-BZXYHN2X.js.map +1 -0
  54. package/dist/docker-DQDZWXF9.js +20 -0
  55. package/dist/docker-DQDZWXF9.js.map +1 -0
  56. package/dist/docker-DnWAVdEm.js +2 -0
  57. package/dist/docker-oSbCExHL.js +153 -0
  58. package/dist/docker-oSbCExHL.js.map +1 -0
  59. package/dist/doctor-CxlH1pDM.js +525 -0
  60. package/dist/doctor-CxlH1pDM.js.map +1 -0
  61. package/dist/egress-73SQfKdx.js +619 -0
  62. package/dist/egress-73SQfKdx.js.map +1 -0
  63. package/dist/env-BQqj93gu.js +21 -0
  64. package/dist/env-BQqj93gu.js.map +1 -0
  65. package/dist/environment-Mz7wvrtD.js +552 -0
  66. package/dist/environment-Mz7wvrtD.js.map +1 -0
  67. package/dist/errors-C9w_ud7g.js +160 -0
  68. package/dist/errors-C9w_ud7g.js.map +1 -0
  69. package/dist/errors-CSxfJ4qu.js +11 -0
  70. package/dist/errors-CSxfJ4qu.js.map +1 -0
  71. package/dist/files--6CEUXm7.js +2 -0
  72. package/dist/files-BLlrhU4-.js +62 -0
  73. package/dist/files-BLlrhU4-.js.map +1 -0
  74. package/dist/files-Bd25OAQH.js +2 -0
  75. package/dist/files-BvHCFWgD.js +2 -0
  76. package/dist/files-D2-qn_Dl.js +275 -0
  77. package/dist/files-D2-qn_Dl.js.map +1 -0
  78. package/dist/files-bF2fPuT9.js +99 -0
  79. package/dist/files-bF2fPuT9.js.map +1 -0
  80. package/dist/git-D6hMeieS.js +44 -0
  81. package/dist/git-D6hMeieS.js.map +1 -0
  82. package/dist/harness-ls5aX1UO.js +131 -0
  83. package/dist/harness-ls5aX1UO.js.map +1 -0
  84. package/dist/harness.d.ts +74 -0
  85. package/dist/harness.js +2 -0
  86. package/dist/header-DzFDoMCI.js +78 -0
  87. package/dist/header-DzFDoMCI.js.map +1 -0
  88. package/dist/ids-Bp6ut7ZD.js +49 -0
  89. package/dist/ids-Bp6ut7ZD.js.map +1 -0
  90. package/dist/images-Cc1-coFc.js +286 -0
  91. package/dist/images-Cc1-coFc.js.map +1 -0
  92. package/dist/index.d.ts +97 -0
  93. package/dist/index.js +1959 -0
  94. package/dist/index.js.map +1 -0
  95. package/dist/init-CFljS8BJ.js +344 -0
  96. package/dist/init-CFljS8BJ.js.map +1 -0
  97. package/dist/init-z2ZPn9Tj.js +2 -0
  98. package/dist/invocations-rXktgSAn.js +157 -0
  99. package/dist/invocations-rXktgSAn.js.map +1 -0
  100. package/dist/jiti-BOLXpKyG.js +46 -0
  101. package/dist/jiti-BOLXpKyG.js.map +1 -0
  102. package/dist/judge-D8Uf4dA9.js +146 -0
  103. package/dist/judge-D8Uf4dA9.js.map +1 -0
  104. package/dist/judge.d.ts +21 -0
  105. package/dist/judge.js +19 -0
  106. package/dist/judge.js.map +1 -0
  107. package/dist/live-Bs3LReMV.js +98 -0
  108. package/dist/live-Bs3LReMV.js.map +1 -0
  109. package/dist/matchers-DyLvm7EG.js +374 -0
  110. package/dist/matchers-DyLvm7EG.js.map +1 -0
  111. package/dist/model-BdO79gWz.js +44 -0
  112. package/dist/model-BdO79gWz.js.map +1 -0
  113. package/dist/model-CDL98xue.js +725 -0
  114. package/dist/model-CDL98xue.js.map +1 -0
  115. package/dist/model-v-0_Ik7S.js +2 -0
  116. package/dist/node-kG64XxdE.js +506 -0
  117. package/dist/node-kG64XxdE.js.map +1 -0
  118. package/dist/opencode-providers-DZuD_oYt.js +3149 -0
  119. package/dist/opencode-providers-DZuD_oYt.js.map +1 -0
  120. package/dist/options-CAQhu--C.js +452 -0
  121. package/dist/options-CAQhu--C.js.map +1 -0
  122. package/dist/patch-BkKje4cp.js +69 -0
  123. package/dist/patch-BkKje4cp.js.map +1 -0
  124. package/dist/paths-CBvm6pap.js +72 -0
  125. package/dist/paths-CBvm6pap.js.map +1 -0
  126. package/dist/plan-gwk6dJ8c.js +237 -0
  127. package/dist/plan-gwk6dJ8c.js.map +1 -0
  128. package/dist/preflight-BtubXKQ8.js +2 -0
  129. package/dist/preflight-BypZMNBG.js +185 -0
  130. package/dist/preflight-BypZMNBG.js.map +1 -0
  131. package/dist/prepare-Cwv0bHs_.js +3 -0
  132. package/dist/prepare-D6Uzcubb.js +213 -0
  133. package/dist/prepare-D6Uzcubb.js.map +1 -0
  134. package/dist/provenance-Dfb8YY5Y.js +129 -0
  135. package/dist/provenance-Dfb8YY5Y.js.map +1 -0
  136. package/dist/provider-BDbNOQhQ.js +2 -0
  137. package/dist/provider-CmAYTQQs.js +14 -0
  138. package/dist/provider-CmAYTQQs.js.map +1 -0
  139. package/dist/providers-2p3ylMHI.js +107 -0
  140. package/dist/providers-2p3ylMHI.js.map +1 -0
  141. package/dist/providers-BUOxitcT.js +2 -0
  142. package/dist/query-CFATHZmV.js +91 -0
  143. package/dist/query-CFATHZmV.js.map +1 -0
  144. package/dist/query-DR4m6CXf.js +2 -0
  145. package/dist/record-DQ8wzyco.js +99 -0
  146. package/dist/record-DQ8wzyco.js.map +1 -0
  147. package/dist/redact-C4fL_bZP.js +167 -0
  148. package/dist/redact-C4fL_bZP.js.map +1 -0
  149. package/dist/results-CeXb8Wde.js +37 -0
  150. package/dist/results-CeXb8Wde.js.map +1 -0
  151. package/dist/root-CFDjpWuc.js +110 -0
  152. package/dist/root-CFDjpWuc.js.map +1 -0
  153. package/dist/sandbox-By2gkdkC.js +133 -0
  154. package/dist/sandbox-By2gkdkC.js.map +1 -0
  155. package/dist/sandbox.d.ts +41 -0
  156. package/dist/sandbox.js +2 -0
  157. package/dist/server-Bi61uiY0.js +350 -0
  158. package/dist/server-Bi61uiY0.js.map +1 -0
  159. package/dist/server-CsSBL8NC.js +2 -0
  160. package/dist/source-BXzrmof5.js +2 -0
  161. package/dist/source-Bsfa3LZH.js +40 -0
  162. package/dist/source-Bsfa3LZH.js.map +1 -0
  163. package/dist/src-55j1zNHH.js +3 -0
  164. package/dist/src-AEwxd1UP.js +602 -0
  165. package/dist/src-AEwxd1UP.js.map +1 -0
  166. package/dist/src-BDwSuyIo.js +3 -0
  167. package/dist/src-BQyMiWri.js +490 -0
  168. package/dist/src-BQyMiWri.js.map +1 -0
  169. package/dist/src-C-LHyTSV.js +3 -0
  170. package/dist/src-Cmp-7YrD.js +1136 -0
  171. package/dist/src-Cmp-7YrD.js.map +1 -0
  172. package/dist/src-D0RdqpNb.js +252 -0
  173. package/dist/src-D0RdqpNb.js.map +1 -0
  174. package/dist/src-D8KYzxBX.js +793 -0
  175. package/dist/src-D8KYzxBX.js.map +1 -0
  176. package/dist/src-DEgff9Z7.js +4 -0
  177. package/dist/src-DXx_29pq.js +567 -0
  178. package/dist/src-DXx_29pq.js.map +1 -0
  179. package/dist/src-DwwiVLqv.js +2937 -0
  180. package/dist/src-DwwiVLqv.js.map +1 -0
  181. package/dist/src-N7E_mfcZ.js +2 -0
  182. package/dist/src-W9g-y5X5.js +3 -0
  183. package/dist/src-daS0wKH2.js +3 -0
  184. package/dist/state-Cyw3QTsN.js +2 -0
  185. package/dist/state-DZ7eqb1F.js +167 -0
  186. package/dist/state-DZ7eqb1F.js.map +1 -0
  187. package/dist/stats-BNAHlNMD.js +114 -0
  188. package/dist/stats-BNAHlNMD.js.map +1 -0
  189. package/dist/studio-app/assets/index-D6XyfFSH.js +58 -0
  190. package/dist/studio-app/assets/index-xwzWdcE_.css +2 -0
  191. package/dist/studio-app/favicon.svg +1 -0
  192. package/dist/studio-app/index.html +15 -0
  193. package/dist/summary-D7Ttky0z.js +166 -0
  194. package/dist/summary-D7Ttky0z.js.map +1 -0
  195. package/dist/text-gEcKjAyp.js +29 -0
  196. package/dist/text-gEcKjAyp.js.map +1 -0
  197. package/dist/types-C5vI3g1C.js +9 -0
  198. package/dist/types-C5vI3g1C.js.map +1 -0
  199. package/dist/types-DLec-6Cx.d.ts +1535 -0
  200. package/dist/usage-DsYUqIUM.js +24 -0
  201. package/dist/usage-DsYUqIUM.js.map +1 -0
  202. package/dist/view/index.d.ts +645 -0
  203. package/dist/view/index.js +8 -0
  204. package/dist/views-BrHOv6wg.js +220 -0
  205. package/dist/views-BrHOv6wg.js.map +1 -0
  206. package/dist/views-CwCWLWqo.js +3 -0
  207. package/dist/worker-entry-DnYkANPG.js +506 -0
  208. package/dist/worker-entry-DnYkANPG.js.map +1 -0
  209. package/dist/workspace-C2S0Z6gl.js +327 -0
  210. package/dist/workspace-C2S0Z6gl.js.map +1 -0
  211. package/dist/workspace-CyoXG6CY.js +2 -0
  212. package/docker/Dockerfile +19 -0
  213. package/package.json +79 -0
package/dist/index.js ADDED
@@ -0,0 +1,1959 @@
1
+ import { n as isTimeout, t as UsageError } from "./errors-CSxfJ4qu.js";
2
+ import { _ as stepCommand, b as workspaceOf, d as isSecret, f as isSecretTemplate, h as parseGitRepo, l as isHostFile, m as mcpHosts, o as defineEnvironment, t as DEFAULT_SKILL_EXCLUDE, x as parseDuration, y as workspaceLabel } from "./environment-Mz7wvrtD.js";
3
+ import { t as inCI } from "./env-BQqj93gu.js";
4
+ import { a as registerSecret, i as redactSecrets } from "./redact-C4fL_bZP.js";
5
+ import { t as InfraError } from "./types-C5vI3g1C.js";
6
+ import { _ as layoutOf, a as unenforceable, h as expectedBlockedFor, n as allowedHosts, o as createFakeSandbox, p as brokeringFor, r as egressLabel, t as DEFAULT_EGRESS_ALLOW, v as refusedMcp } from "./egress-73SQfKdx.js";
7
+ import { c as relativeFile, i as evalSlug, l as sha256, n as attemptKey, r as evalId, s as recordFileName, t as attemptId } from "./ids-Bp6ut7ZD.js";
8
+ import { l as runJudge, s as judgeModelName } from "./judge-D8Uf4dA9.js";
9
+ import { S as placeholder, et as run, tt as shellQuote } from "./src-DwwiVLqv.js";
10
+ import { a as evalPlan, d as secretVarsIn, f as unbrokered, i as checkOptions, l as matchesTags, n as cascade, o as expand, p as unbrokeredError, s as harnessId, t as brokeredIn, u as sandboxId } from "./options-CAQhu--C.js";
11
+ import { n as badCharacterAt, o as defineConfig } from "./config-Cuj0kVYj.js";
12
+ import { a as withLease, c as registerGroup, l as registerTest, o as collectingFile, p as expect, s as getRunContext, t as HarnessCapabilityError } from "./matchers-DyLvm7EG.js";
13
+ import { a as withDebug, i as debugTrace, n as attemptDebug, r as debugLog, t as AttemptDebug } from "./attempt-BdRQ9jKX.js";
14
+ import { t as attemptDebugDir } from "./files-BLlrhU4-.js";
15
+ import { n as clip, r as formatDuration, t as capMiddle } from "./text-gEcKjAyp.js";
16
+ import { t as contextTaker } from "./context-loi9OCZv.js";
17
+ import { t as PrepareError } from "./provider-CmAYTQQs.js";
18
+ import { i as preparedWorkspace } from "./workspace-C2S0Z6gl.js";
19
+ import { r as patchForJudge, t as changedFilesForJudge } from "./patch-BkKje4cp.js";
20
+ import { existsSync, readFileSync, readdirSync, statSync } from "node:fs";
21
+ import { basename, isAbsolute, join, relative, sep } from "node:path";
22
+ import { setTimeout as setTimeout$1 } from "node:timers/promises";
23
+ import { mkdir, readFile, rename, rm, writeFile } from "node:fs/promises";
24
+ import { parse } from "yaml";
25
+ //#region src/attempt/checks.ts
26
+ /** Runs a check or setup step with the checks' brokered secrets swapped in (see brokerPhases). */
27
+ const asCheck = (a, fn) => a.phases ? a.phases.checks(fn) : fn();
28
+ function requireSandbox(a) {
29
+ if (!a.sandbox) throw new Error("No sandbox yet: call t.send() or await t.sandbox() before t.exec() or t.readFile()");
30
+ return a.sandbox;
31
+ }
32
+ /**
33
+ * Phase 4, checks: `t.exec` runs with the checks' variables and secrets, each run recorded and
34
+ * timed; `t.readFile` reads the sandbox as is.
35
+ */
36
+ function createChecks(a) {
37
+ const checkEnv = () => a.env?.checkVars ?? {};
38
+ const exec = async (command) => {
39
+ const box = requireSandbox(a);
40
+ const start = performance.now();
41
+ a.report({
42
+ kind: "check-start",
43
+ command
44
+ });
45
+ const r = await a.timed("checksMs", () => asCheck(a, () => box.exec(command, {
46
+ signal: a.signal,
47
+ env: checkEnv()
48
+ })));
49
+ const durationMs = Math.round(performance.now() - start);
50
+ a.record.checks.push({
51
+ command,
52
+ ...r,
53
+ durationMs
54
+ });
55
+ debugLog("check", `$ ${clip(command, 160)} → exit ${r.exitCode} · ${formatDuration(durationMs)}`);
56
+ debugTrace("check", "output", {
57
+ command,
58
+ ...r,
59
+ durationMs
60
+ }, `check output: ${clip(command, 80)}`);
61
+ a.report({
62
+ kind: "check-end",
63
+ command,
64
+ exitCode: r.exitCode,
65
+ ms: durationMs
66
+ });
67
+ return r;
68
+ };
69
+ const readFile = (path) => requireSandbox(a).readFile(path);
70
+ return {
71
+ exec,
72
+ readFile
73
+ };
74
+ }
75
+ //#endregion
76
+ //#region src/attempt/debug.ts
77
+ /** Which config and profile the attempt runs under, and where each resolved option came from. */
78
+ function tracePlan(debug, spec, ctx) {
79
+ const config = ctx.configFile ?? "none (no config file found)";
80
+ const from = (key, fallback = ctx.configFile ? `${config} or its default` : "default") => `(${spec.sources[key] ?? fallback})`;
81
+ const profile = ctx.profile ? `profile ${ctx.profile.name} (${ctx.profile.reason})` : "no profile";
82
+ debug.log("plan", `config ${config} · ${profile} · run ${ctx.runId} · attempt ${spec.cell.key}#${spec.n}`);
83
+ debug.log("plan", `harness ${harnessId(spec.harness)} ${from("harness")}`);
84
+ debug.log("plan", `sandbox ${sandboxId(spec.sandbox)} ${from("sandbox")}`);
85
+ const env = spec.environment;
86
+ const role = spec.cell.role === "variant" ? ` · variant ${spec.cell.variant}` : "";
87
+ const envFrom = spec.cell.role === "variant" ? from("compare") : from("environment", "none set");
88
+ debug.log("plan", `environment ${env.name}${role} ${envFrom}`);
89
+ debug.log("plan", `egress ${egressLabel(spec.egress) || "allow-all"} ${from("egress")}`);
90
+ const gate = typeof spec.gate === "string" ? spec.gate : `passRate ${spec.gate.passRate}`;
91
+ debug.log("plan", `run ${spec.n} · runs ${ctx.runs !== void 0 ? "(--runs)" : from("runs")} · gate ${gate} · timeout ${formatDuration(spec.timeoutMs)} ${from("timeout", "config.timeout or 15m")}`);
92
+ const judge = spec.judge;
93
+ const model = typeof judge.model === "string" ? judge.model : judge.model && "kind" in judge.model ? "fake judge" : judge.model ? "a model object" : "none";
94
+ debug.log("plan", `judge ${model}${judge.threshold !== void 0 ? ` · threshold ${judge.threshold}` : ""} ${from("judge")}`);
95
+ for (const note of spec.notes) debug.log("plan", `profile: ${note}`);
96
+ debug.trace("plan", "config", {
97
+ config: ctx.config,
98
+ profile: ctx.profile
99
+ }, "resolved config");
100
+ debug.trace("plan", "attempt", {
101
+ cell: spec.cell,
102
+ n: spec.n,
103
+ harness: spec.harness,
104
+ sandbox: spec.sandbox,
105
+ egress: spec.egress,
106
+ gate: spec.gate,
107
+ timeoutMs: spec.timeoutMs,
108
+ judge: spec.judge,
109
+ sources: spec.sources,
110
+ env: Object.keys(spec.env).sort()
111
+ }, "resolved attempt");
112
+ }
113
+ /** `POST api2.cursor.sh/auth/exchange`, `GET host/v1/*` or `POST host (path ~ /re/)`. */
114
+ function endpointLabel({ method, host, path }) {
115
+ const where = !path ? host : "exact" in path ? `${host}${path.exact}` : "startsWith" in path ? `${host}${path.startsWith}*` : `${host} (path ~ /${path.regex}/)`;
116
+ return `${method?.join("|") ?? "*"} ${where}`;
117
+ }
118
+ /** The resolved environment and what the gateway brokers, by name. */
119
+ function traceEnvironment(debug, spec, env) {
120
+ const r = env.record;
121
+ debug.log("env", `environment ${r.name} (digest ${r.digest.replace(/^sha256:/, "").slice(0, 12)})${r.parent ? ` · extends ${r.parent}` : ""}${r.overrides.length ? ` · overrides ${r.overrides.join(", ")}` : ""}`);
122
+ const unset = r.unset.length ? ` · unset (optional) ${r.unset.join(", ")}` : "";
123
+ debug.log("env", `vars ${r.vars.join(", ") || "none"} · secrets ${r.secrets.join(", ") || "none"}${unset}`);
124
+ for (const s of env.substitutions) debug.log("broker", `${s.name} is a placeholder in the sandbox · swapped in on HTTPS to ${s.hosts.join(", ")} in the ${s.in.join(", ")} · for ${s.phase === "checks" ? "checks and setup only" : "the agent and checks"}`);
125
+ const brokering = brokeringFor(spec.harness);
126
+ if (brokering) {
127
+ const endpoints = brokering.endpoints.map(endpointLabel).join(", ");
128
+ debug.log("broker", `${brokering.credential} stays on the host · the gateway swaps it in on ${endpoints}`);
129
+ }
130
+ debug.trace("env", "environment", {
131
+ record: r,
132
+ vars: Object.keys(env.vars).sort(),
133
+ checkVars: Object.keys(env.checkVars).sort(),
134
+ files: env.files.map((f) => f.path),
135
+ setup: env.setup,
136
+ skills: env.skills.map((s) => s.name),
137
+ workspaceSkills: env.workspaceSkills.map((s) => s.name),
138
+ mcp: Object.fromEntries(Object.entries(env.mcp).map(([name, m]) => [name, m.transport === "stdio" ? {
139
+ transport: m.transport,
140
+ command: m.command,
141
+ args: m.args,
142
+ env: Object.keys(m.env)
143
+ } : {
144
+ transport: m.transport,
145
+ url: m.url,
146
+ headers: Object.keys(m.headers)
147
+ }])),
148
+ brokered: env.substitutions.map(({ name, hosts, in: where, phase }) => ({
149
+ name,
150
+ hosts,
151
+ in: where,
152
+ phase
153
+ }))
154
+ }, "resolved environment");
155
+ }
156
+ //#endregion
157
+ //#region src/environment/resolve.ts
158
+ const SKILL_NAME = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
159
+ const DEFAULT_SETUP_TIMEOUT = "5m";
160
+ function broker(secrets, name, value, hosts, where = ["header"], phase = "agent") {
161
+ const at = badCharacterAt(value);
162
+ if (at && where.includes("header")) throw new InfraError(`${name} has whitespace or a line break in it (character ${at}), so it can't go in a request header. A key copied from a wrapped terminal line picks these up; set it again as one line.`);
163
+ const sub = {
164
+ name,
165
+ placeholder: placeholder(),
166
+ value,
167
+ hosts: [...hosts],
168
+ in: [...where],
169
+ phase
170
+ };
171
+ secrets.brokered.push(sub);
172
+ return sub.placeholder;
173
+ }
174
+ /** The host value, or its placeholder when it's brokered to `secret.hosts` (else `hosts`). */
175
+ function hostValue(name, secret, where, secrets, hosts = secret.hosts) {
176
+ const from = secret.from ?? name;
177
+ const value = process.env[from];
178
+ if (!value) {
179
+ if (secret.optional) {
180
+ secrets.unset.push(name);
181
+ return;
182
+ }
183
+ const source = from === name ? name : `${name} (from ${from})`;
184
+ throw new InfraError(`Secret ${source} for ${where} isn't set. Set it in the environment or .env.local.`);
185
+ }
186
+ registerSecret(value);
187
+ if (secret.to === "checks") secrets.checks.push(name);
188
+ else secrets.names.push(name);
189
+ if (!hosts) return value;
190
+ return broker(secrets, name, value, hosts, secret.in, secret.to === "checks" ? "checks" : "agent");
191
+ }
192
+ /** Each host variable in the template, or with `hosts` one placeholder per variable. */
193
+ function template(value, where, secrets, hosts) {
194
+ const placeholders = /* @__PURE__ */ new Map();
195
+ return value.template.replace(/\$\{([A-Za-z_][A-Za-z0-9_]*)\}/g, (_, name) => {
196
+ const v = process.env[name];
197
+ if (!v) throw new InfraError(`secret.template needs ${name} for ${where}, and it isn't set`);
198
+ registerSecret(v);
199
+ if (!hosts) return v;
200
+ const p = placeholders.get(name) ?? broker(secrets, name, v, hosts);
201
+ placeholders.set(name, p);
202
+ return p;
203
+ });
204
+ }
205
+ /** A `SKILL.md` split into its YAML frontmatter's `name` and `description` and its body. */
206
+ function parseSkillMd(text, where) {
207
+ const match = /^---\r?\n(?:([\s\S]*?)\r?\n)?---[ \t]*(?:\r?\n|$)([\s\S]*)$/.exec(text);
208
+ if (!match) return { body: text };
209
+ let front;
210
+ try {
211
+ front = parse(match[1] ?? "");
212
+ } catch (error) {
213
+ const reason = error.message.split("\n")[0];
214
+ throw new InfraError(`${where}: the SKILL.md frontmatter isn't valid YAML (${reason}). Quote values that contain ": ".`);
215
+ }
216
+ if (front != null && (typeof front !== "object" || Array.isArray(front))) throw new InfraError(`${where}: the SKILL.md frontmatter must be a YAML mapping`);
217
+ const field = (key) => {
218
+ const value = front?.[key];
219
+ if (value == null) return void 0;
220
+ if (typeof value !== "string") throw new InfraError(`${where}: the SKILL.md ${key} must be a string (got ${typeof value})`);
221
+ return value.trim() || void 0;
222
+ };
223
+ return {
224
+ name: field("name"),
225
+ description: field("description"),
226
+ body: match[2] ?? ""
227
+ };
228
+ }
229
+ /** A gitignore-style test for `DEFAULT_SKILL_EXCLUDE`-shaped patterns against a skill-relative path. */
230
+ function excludeMatcher(patterns) {
231
+ const rules = patterns.map((pattern) => {
232
+ const dirOnly = pattern.endsWith("/");
233
+ const body = pattern.replace(/\/+$/, "");
234
+ const anchored = body.includes("/");
235
+ const glob = body.replace(/^\/+/, "").split("*").map((part) => part.replace(/[.+?^${}()|[\]\\]/g, "\\$&")).join("[^/]*");
236
+ return {
237
+ dirOnly,
238
+ re: new RegExp(`${anchored ? "^" : "(?:^|/)"}${glob}$`)
239
+ };
240
+ });
241
+ return (path, isDir) => rules.some((r) => (isDir || !r.dirOnly) && r.re.test(path));
242
+ }
243
+ function parseSkill(dir, source, exclude) {
244
+ const front = parseSkillMd(readFileSync(join(dir, "SKILL.md"), "utf8"), `Skill ${basename(dir)} in ${source}`);
245
+ const name = front.name || basename(dir);
246
+ checkSkill(name, front.description, source);
247
+ const skip = excludeMatcher(exclude);
248
+ const paths = [];
249
+ const excluded = [];
250
+ const walk = (rel) => {
251
+ for (const entry of readdirSync(join(dir, rel), { withFileTypes: true })) {
252
+ const path = rel ? `${rel}/${entry.name}` : entry.name;
253
+ const isDir = entry.isDirectory();
254
+ if (entry.name === ".git") continue;
255
+ if (skip(path, isDir)) excluded.push(isDir ? `${path}/` : path);
256
+ else if (isDir) walk(path);
257
+ else if (entry.isFile() && path !== "SKILL.md") paths.push(path);
258
+ }
259
+ };
260
+ walk("");
261
+ const files = paths.sort().map((path) => ({
262
+ path,
263
+ content: readFileSync(join(dir, path), "utf8")
264
+ }));
265
+ return {
266
+ skill: {
267
+ name,
268
+ description: front.description ?? "",
269
+ content: front.body,
270
+ files
271
+ },
272
+ excluded: excluded.sort()
273
+ };
274
+ }
275
+ function checkSkill(name, description, source) {
276
+ if (!SKILL_NAME.test(name)) throw new InfraError(`Skill "${name}" in ${source}: the name must be kebab-case`);
277
+ if (!description) throw new InfraError(`Skill "${name}" in ${source} has no description in its SKILL.md frontmatter`);
278
+ }
279
+ function loadSkills(dir, source, only, exclude = DEFAULT_SKILL_EXCLUDE) {
280
+ if (!existsSync(dir) || !statSync(dir).isDirectory()) throw new InfraError(`Skill source ${source} isn't a directory`);
281
+ const dirs = existsSync(join(dir, "SKILL.md")) ? [dir] : readdirSync(dir, { withFileTypes: true }).filter((e) => e.isDirectory() && existsSync(join(dir, e.name, "SKILL.md"))).map((e) => join(dir, e.name)).sort();
282
+ if (dirs.length === 0) throw new InfraError(`Skill source ${source} has no SKILL.md (and no subdirectories with one)`);
283
+ const skills = dirs.map((d) => parseSkill(d, source, exclude));
284
+ if (!only) return skills;
285
+ const missing = only.filter((n) => !skills.some((s) => s.skill.name === n));
286
+ if (missing.length) throw new InfraError(`only: ${JSON.stringify(missing)} not found in ${source} (found: ${skills.map((s) => s.skill.name).join(", ")})`);
287
+ return skills.filter((s) => only.includes(s.skill.name));
288
+ }
289
+ async function git(args, cwd, what) {
290
+ const r = await run("git", args, { cwd });
291
+ if (r.exitCode !== 0) throw new InfraError(`${what} failed: ${r.stderr.trim()}`);
292
+ return r.stdout.trim();
293
+ }
294
+ /** One clone per source and ref per worker process; the commit is recorded. */
295
+ const clones = /* @__PURE__ */ new Map();
296
+ function cloneSkills(root, url, ref) {
297
+ const key = `${url}#${ref ?? ""}`;
298
+ let clone = clones.get(key);
299
+ if (!clone) {
300
+ clone = (async () => {
301
+ const dir = join(root, ".reeval", "cache", "skills", `${sha256(key).slice(0, 16)}-${process.pid}`);
302
+ await rm(dir, {
303
+ recursive: true,
304
+ force: true
305
+ });
306
+ await mkdir(dir, { recursive: true });
307
+ const what = `Cloning ${url}${ref ? ` at ${ref}` : ""}`;
308
+ await git(["init", "-q"], dir, what);
309
+ await git([
310
+ "fetch",
311
+ "-q",
312
+ "--depth",
313
+ "1",
314
+ url,
315
+ ref ?? "HEAD"
316
+ ], dir, what);
317
+ await git([
318
+ "-c",
319
+ "advice.detachedHead=false",
320
+ "checkout",
321
+ "-q",
322
+ "FETCH_HEAD"
323
+ ], dir, what);
324
+ return {
325
+ dir,
326
+ commit: await git(["rev-parse", "HEAD"], dir, what)
327
+ };
328
+ })();
329
+ clones.set(key, clone);
330
+ clone.catch(() => clones.delete(key));
331
+ }
332
+ return clone;
333
+ }
334
+ const packs = /* @__PURE__ */ new Map();
335
+ /** `npm pack` into the cache, unpacked; returns the package directory. */
336
+ function unpackNpm(root, spec) {
337
+ let pack = packs.get(spec);
338
+ if (!pack) {
339
+ pack = (async () => {
340
+ const dir = join(root, ".reeval", "cache", "skills", `npm-${sha256(spec).slice(0, 16)}-${process.pid}`);
341
+ await rm(dir, {
342
+ recursive: true,
343
+ force: true
344
+ });
345
+ await mkdir(dir, { recursive: true });
346
+ const packed = await run("npm", [
347
+ "pack",
348
+ spec,
349
+ "--silent",
350
+ "--pack-destination",
351
+ dir
352
+ ], { cwd: dir });
353
+ const tarball = packed.stdout.trim().split("\n").at(-1);
354
+ if (packed.exitCode !== 0 || !tarball) throw new InfraError(`npm pack ${spec} failed: ${packed.stderr.trim()}`);
355
+ const untar = await run("tar", ["-xzf", tarball], { cwd: dir });
356
+ if (untar.exitCode !== 0) throw new InfraError(`Unpacking ${spec} failed: ${untar.stderr.trim()}`);
357
+ return join(dir, "package");
358
+ })();
359
+ packs.set(spec, pack);
360
+ pack.catch(() => packs.delete(spec));
361
+ }
362
+ return pack;
363
+ }
364
+ async function resolveSkills(root, s) {
365
+ switch (s.source) {
366
+ case "local": {
367
+ const label = `local:${relative(root, s.path).split(sep).join("/") || "."}`;
368
+ return {
369
+ skills: loadSkills(s.path, label, s.only, s.exclude),
370
+ source: label,
371
+ commit: null
372
+ };
373
+ }
374
+ case "git": {
375
+ const { url } = parseGitRepo(s.repo);
376
+ const { dir, commit } = await cloneSkills(root, url, s.ref);
377
+ const label = `${s.repo}${s.ref ? `#${s.ref}` : ""}${s.path ? `:${s.path}` : ""}`;
378
+ return {
379
+ skills: loadSkills(s.path ? join(dir, s.path) : dir, label, s.only, s.exclude),
380
+ source: label,
381
+ commit
382
+ };
383
+ }
384
+ case "registry": {
385
+ const [repo = "", name = ""] = s.id.split("@");
386
+ const { dir, commit } = await cloneSkills(root, `https://github.com/${repo}.git`, s.version);
387
+ const label = `registry:${s.id}${s.version ? `#${s.version}` : ""}`;
388
+ return {
389
+ skills: loadSkills(existsSync(join(dir, "skills")) ? join(dir, "skills") : dir, label, [name]),
390
+ source: label,
391
+ commit
392
+ };
393
+ }
394
+ case "npm": {
395
+ const dir = await unpackNpm(root, s.spec);
396
+ const label = `npm:${isAbsolute(s.spec) ? relative(root, s.spec).split(sep).join("/") : s.spec}:${s.path}`;
397
+ return {
398
+ skills: loadSkills(join(dir, s.path), label, s.only, s.exclude),
399
+ source: label,
400
+ commit: null
401
+ };
402
+ }
403
+ case "inline": {
404
+ checkSkill(s.name, s.description, "skill.inline");
405
+ const files = Object.entries(s.files).sort(([a], [b]) => a < b ? -1 : 1).map(([path, content]) => ({
406
+ path,
407
+ content
408
+ }));
409
+ return {
410
+ skills: [{
411
+ skill: {
412
+ name: s.name,
413
+ description: s.description,
414
+ content: s.content,
415
+ files
416
+ },
417
+ excluded: []
418
+ }],
419
+ source: "inline",
420
+ commit: null
421
+ };
422
+ }
423
+ }
424
+ }
425
+ const skillHash = (s) => sha256(JSON.stringify(s));
426
+ const readHostFile = (f, where) => {
427
+ if (!existsSync(f.path)) throw new InfraError(`${f.path} for ${where} doesn't exist`);
428
+ return readFileSync(f.path);
429
+ };
430
+ /**
431
+ * Layers the environment (root first; a later layer wins on shared keys, and each such key is
432
+ * an override), over the options `env` merged down the cascade. `where` names the attempt, and
433
+ * `rulesFile` is the workspace file the harness's agent reads `rules` from.
434
+ */
435
+ async function resolveEnvironment(environment, optionsEnv, root, where, rulesFile, egress = "allow-all", prepared = () => void 0) {
436
+ const envWhere = `environment ${environment.name} (${where})`;
437
+ const overrides = /* @__PURE__ */ new Set();
438
+ const env = { ...optionsEnv };
439
+ const files = /* @__PURE__ */ new Map();
440
+ const mcp = /* @__PURE__ */ new Map();
441
+ const skills = /* @__PURE__ */ new Map();
442
+ let rules;
443
+ const setup = [];
444
+ const seen = {
445
+ env: /* @__PURE__ */ new Set(),
446
+ files: /* @__PURE__ */ new Set(),
447
+ mcp: /* @__PURE__ */ new Set()
448
+ };
449
+ let egressSet = false;
450
+ let workspace;
451
+ for (const { spec } of environment.layers) {
452
+ if (spec.workspace !== void 0) {
453
+ if (workspace) overrides.add("workspace");
454
+ workspace = spec.workspace;
455
+ }
456
+ if (spec.egress !== void 0) {
457
+ if (egressSet) overrides.add("egress");
458
+ egressSet = true;
459
+ }
460
+ for (const [k, v] of Object.entries(spec.env ?? {})) {
461
+ if (seen.env.has(k)) overrides.add(`env.${k}`);
462
+ seen.env.add(k);
463
+ env[k] = v;
464
+ }
465
+ for (const [path, v] of Object.entries(spec.files ?? {})) {
466
+ if (seen.files.has(path)) overrides.add(`files.${path}`);
467
+ seen.files.add(path);
468
+ files.set(path, v);
469
+ }
470
+ if (spec.rules !== void 0) {
471
+ if (rules !== void 0) overrides.add("rules");
472
+ rules = spec.rules;
473
+ }
474
+ for (const [name, server] of Object.entries(spec.mcp ?? {})) {
475
+ if (seen.mcp.has(name)) overrides.add(`mcp.${name}`);
476
+ seen.mcp.add(name);
477
+ mcp.set(name, server);
478
+ }
479
+ for (const step of spec.setup ?? []) {
480
+ const s = typeof step === "string" ? { run: step } : step;
481
+ setup.push({
482
+ run: s.run,
483
+ timeoutMs: parseDuration(s.timeout ?? DEFAULT_SETUP_TIMEOUT),
484
+ ...s.cwd ? { cwd: s.cwd } : {}
485
+ });
486
+ }
487
+ const layerSkills = /* @__PURE__ */ new Set();
488
+ for (const source of spec.skills ?? []) {
489
+ const found = await resolveSkills(root, source);
490
+ for (const loaded of found.skills) {
491
+ const name = loaded.skill.name;
492
+ if (skills.has(name) && !layerSkills.has(name)) overrides.add(`skills.${name}`);
493
+ layerSkills.add(name);
494
+ skills.delete(name);
495
+ skills.set(name, {
496
+ ...loaded,
497
+ placement: source.placement,
498
+ source: found.source,
499
+ commit: found.commit
500
+ });
501
+ }
502
+ }
503
+ }
504
+ const secrets = {
505
+ names: [],
506
+ checks: [],
507
+ unset: [],
508
+ brokered: []
509
+ };
510
+ const vars = {};
511
+ const checkVars = {};
512
+ for (const [name, value] of Object.entries(env)) {
513
+ if (!isSecret(value)) {
514
+ vars[name] = value;
515
+ continue;
516
+ }
517
+ const resolved = hostValue(name, value, envWhere, secrets);
518
+ if (resolved === void 0) continue;
519
+ if (value.to === "checks") checkVars[name] = resolved;
520
+ else vars[name] = resolved;
521
+ }
522
+ const secretOf = (value, key, hosts) => {
523
+ if (typeof value === "string") return value;
524
+ if (isSecretTemplate(value)) return template(value, envWhere, secrets, hosts);
525
+ return hostValue(key, value, envWhere, secrets, value.hosts ?? hosts);
526
+ };
527
+ const resolvedMcp = {};
528
+ for (const [name, s] of mcp) if (s.transport === "stdio") {
529
+ const e = {};
530
+ for (const [k, v] of Object.entries(s.env)) {
531
+ const r = secretOf(v, k);
532
+ if (r !== void 0) e[k] = r;
533
+ }
534
+ resolvedMcp[name] = {
535
+ transport: "stdio",
536
+ command: s.command,
537
+ args: s.args,
538
+ env: e
539
+ };
540
+ } else {
541
+ const h = {};
542
+ const hosts = mcpHosts(s.url);
543
+ for (const [k, v] of Object.entries(s.headers)) {
544
+ const r = secretOf(v, k, hosts);
545
+ if (r !== void 0) h[k] = r;
546
+ }
547
+ resolvedMcp[name] = {
548
+ transport: s.transport,
549
+ url: s.url,
550
+ headers: h
551
+ };
552
+ }
553
+ const fileList = [...files].map(([path, v]) => ({
554
+ path,
555
+ content: isHostFile(v) ? readHostFile(v, envWhere) : v
556
+ }));
557
+ const rulesContent = rules === void 0 ? void 0 : isHostFile(rules) ? Buffer.from(readHostFile(rules, envWhere)).toString("utf8") : rules;
558
+ const skillList = [...skills.values()];
559
+ const skillRecords = skillList.map(({ skill, placement, source, commit, excluded }) => ({
560
+ name: skill.name,
561
+ source,
562
+ commit,
563
+ sha256: skillHash(skill),
564
+ placement,
565
+ excluded
566
+ }));
567
+ const fileRecords = fileList.map((f) => ({
568
+ path: f.path,
569
+ sha256: sha256(f.content)
570
+ }));
571
+ const rulesRecord = rulesContent === void 0 ? void 0 : {
572
+ path: rulesFile,
573
+ sha256: sha256(rulesContent)
574
+ };
575
+ if (rulesContent !== void 0) fileList.push({
576
+ path: rulesFile,
577
+ content: rulesContent
578
+ });
579
+ const secretNames = [.../* @__PURE__ */ new Set([...secrets.names, ...secrets.checks])].sort();
580
+ const brokered = secrets.brokered.map((b) => JSON.stringify([
581
+ b.name,
582
+ b.hosts,
583
+ b.in,
584
+ b.phase
585
+ ])).sort();
586
+ const brokeredNames = [...new Set(secrets.brokered.map((b) => b.name))].sort();
587
+ const varNames = Object.keys(vars).filter((n) => !secrets.names.includes(n)).sort();
588
+ const literalVars = Object.fromEntries(varNames.map((n) => [n, vars[n]]));
589
+ const mcpShape = Object.fromEntries(Object.entries(resolvedMcp).map(([name, s]) => s.transport === "stdio" ? [name, {
590
+ command: s.command,
591
+ args: s.args,
592
+ env: Object.keys(s.env).sort()
593
+ }] : [name, {
594
+ transport: s.transport,
595
+ url: s.url,
596
+ headers: Object.keys(s.headers).sort()
597
+ }]));
598
+ const tree = workspace && prepared(workspace);
599
+ if (workspace && !tree) throw new InfraError(`The workspace git(${JSON.stringify(workspaceLabel(workspace))}) for ${envWhere} wasn't prepared before the run started`);
600
+ const workspaceRecord = workspace && tree ? {
601
+ source: isAbsolute(workspace.url) ? relative(root, workspace.url).split(sep).join("/") || "." : workspace.repo,
602
+ ref: workspace.ref ?? null,
603
+ path: workspace.path ?? null,
604
+ commit: tree.commit,
605
+ tree: tree.tree,
606
+ ...workspace.init ? { init: true } : {}
607
+ } : void 0;
608
+ const digest = `sha256:${sha256(JSON.stringify({
609
+ setup,
610
+ skills: skillRecords.map((s) => [
611
+ s.name,
612
+ s.sha256,
613
+ s.placement
614
+ ]),
615
+ mcp: mcpShape,
616
+ files: fileRecords,
617
+ rules: rulesRecord?.sha256,
618
+ vars: literalVars,
619
+ secrets: secretNames,
620
+ checks: [...secrets.checks].sort(),
621
+ unset: [...secrets.unset].sort(),
622
+ brokered,
623
+ ...egress === "allow-all" ? {} : { egress: typeof egress === "string" ? egress : [...egress.allow].sort() },
624
+ ...workspaceRecord ? { workspace: workspaceRecord.init ? [workspaceRecord.tree, "init"] : workspaceRecord.tree } : {}
625
+ }))}`;
626
+ return {
627
+ record: {
628
+ name: environment.name,
629
+ parent: environment.parent,
630
+ overrides: [...overrides],
631
+ digest,
632
+ setup: setup.map(stepCommand),
633
+ skills: skillRecords,
634
+ mcp: Object.keys(resolvedMcp).sort(),
635
+ files: rulesRecord ? [...fileRecords, rulesRecord] : fileRecords,
636
+ vars: varNames,
637
+ secrets: secretNames,
638
+ brokered: brokeredNames,
639
+ unset: [...secrets.unset].sort(),
640
+ egress,
641
+ ...workspaceRecord ? { workspace: workspaceRecord } : {}
642
+ },
643
+ setup,
644
+ skills: skillList.filter((s) => s.placement === "harness").map((s) => s.skill),
645
+ workspaceSkills: skillList.filter((s) => s.placement === "workspace").map((s) => s.skill),
646
+ mcp: resolvedMcp,
647
+ files: fileList,
648
+ vars,
649
+ checkVars,
650
+ substitutions: secrets.brokered,
651
+ ...workspace && tree ? { workspace: {
652
+ tar: tree.tar,
653
+ label: workspaceLabel(workspace),
654
+ commit: tree.commit,
655
+ ...workspace.init ? { init: true } : {}
656
+ } } : {}
657
+ };
658
+ }
659
+ /** A skill as a `SKILL.md` file plus its other files, for writing into a directory. */
660
+ function skillFiles(skill) {
661
+ return [{
662
+ path: "SKILL.md",
663
+ content: `${`---\nname: ${skill.name}\ndescription: ${JSON.stringify(skill.description)}\n---\n`}${skill.content}`
664
+ }, ...skill.files];
665
+ }
666
+ //#endregion
667
+ //#region src/harness/fake.ts
668
+ const lineCount = (text) => text ? text.split("\n").length - (text.endsWith("\n") ? 1 : 0) : 0;
669
+ /**
670
+ * Writes canned files and returns a canned message on every turn, after running its `script`'s
671
+ * real sandbox actions. Harness-placed skills are written under `~/` where its layout's adapter
672
+ * puts them (`~/.agents/skills` by default, as Cursor's does).
673
+ */
674
+ const createFakeHarness = (descriptor) => {
675
+ let starts = 0;
676
+ return { async start(sandbox, _signal, { skills, workspaceSkills, mcp }) {
677
+ if (descriptor.failWith) throw new InfraError(descriptor.failWith);
678
+ const hangs = descriptor.startHangs;
679
+ if (hangs === true || starts++ < (hangs ?? 0)) return new Promise(() => {});
680
+ const dir = `~/${layoutOf(descriptor).harnessSkillDir}`;
681
+ for (const skill of skills) {
682
+ for (const f of skillFiles(skill)) await sandbox.writeFile(`${dir}/${skill.name}/${f.path}`, f.content);
683
+ debugLog("env", `skill ${skill.name} placed at ${dir}/${skill.name}/ by the fake agent`);
684
+ }
685
+ const names = [...skills.map((s) => s.name), ...workspaceSkills];
686
+ const loaded = [names.length ? `skills: ${names.join(", ")}` : "", Object.keys(mcp).length ? `mcp: ${Object.keys(mcp).join(", ")}` : ""].filter(Boolean);
687
+ let calls = 0;
688
+ let turns = 0;
689
+ const skilled = names.length > 0 && descriptor.withSkill;
690
+ const files = (skilled ? descriptor.withSkill?.files : void 0) ?? descriptor.files ?? {};
691
+ const reply = (skilled ? descriptor.withSkill?.text : void 0) ?? descriptor.text ?? "Done.";
692
+ const flaked = Math.random() < (descriptor.flakeRate ?? 0);
693
+ return {
694
+ async send(prompt, signal) {
695
+ const startedAt = Date.now();
696
+ const script = descriptor.script ?? [];
697
+ const steps = script[Math.min(turns++, script.length - 1)] ?? [];
698
+ const at = () => {
699
+ const ms = Date.now() - startedAt;
700
+ return {
701
+ ts: new Date(startedAt + ms).toISOString(),
702
+ atMs: ms
703
+ };
704
+ };
705
+ const transcript = [{
706
+ ...at(),
707
+ type: "text",
708
+ text: `Working on: ${prompt}`,
709
+ durationMs: 0
710
+ }, ...loaded.length ? [{
711
+ ...at(),
712
+ type: "text",
713
+ text: `Loaded ${loaded.join("; ")}`,
714
+ durationMs: 0
715
+ }] : []];
716
+ const done = (text, status) => ({
717
+ text,
718
+ status,
719
+ startedAt: new Date(startedAt).toISOString(),
720
+ durationMs: Date.now() - startedAt,
721
+ transcript,
722
+ usage: null,
723
+ model: descriptor.servedModel ?? null,
724
+ unreported: []
725
+ });
726
+ const write = async (path, content) => {
727
+ const time = at();
728
+ const before = await sandbox.readFile(path);
729
+ await sandbox.writeFile(path, content);
730
+ debugLog("harness", `tool write ${path} (${before === null ? "create" : "modify"})`);
731
+ transcript.push({
732
+ ...time,
733
+ type: "file-edit",
734
+ toolCallId: `call_${++calls}`,
735
+ path,
736
+ kind: before === null ? "create" : "modify",
737
+ added: lineCount(content),
738
+ removed: before === null ? 0 : lineCount(before),
739
+ durationMs: Date.now() - startedAt - time.atMs
740
+ });
741
+ };
742
+ if (descriptor.reasoning) transcript.push({
743
+ ...at(),
744
+ type: "reasoning",
745
+ text: descriptor.reasoning,
746
+ durationMs: 0
747
+ });
748
+ for (const tool of descriptor.tools ?? []) transcript.push({
749
+ ...at(),
750
+ type: "tool",
751
+ toolCallId: `call_${++calls}`,
752
+ tool: tool.tool,
753
+ input: tool.input,
754
+ output: tool.output,
755
+ ok: tool.ok ?? true,
756
+ durationMs: 0
757
+ });
758
+ if (flaked) {
759
+ const text = "I ran out of ideas and left the workspace unchanged.";
760
+ transcript.push({
761
+ ...at(),
762
+ type: "text",
763
+ text,
764
+ durationMs: 0
765
+ });
766
+ return done(text, "completed");
767
+ }
768
+ let scripted;
769
+ for (const step of steps) if ("exec" in step) {
770
+ const time = at();
771
+ debugLog("harness", `tool shell called: $ ${step.exec}`);
772
+ const r = await sandbox.exec(step.exec, { signal });
773
+ debugLog("harness", `tool shell returned exit ${r.exitCode} in ${Date.now() - startedAt - time.atMs}ms`);
774
+ transcript.push({
775
+ ...time,
776
+ type: "tool",
777
+ toolCallId: `call_${++calls}`,
778
+ tool: "shell",
779
+ input: { command: step.exec },
780
+ output: r.stdout,
781
+ ok: r.exitCode === 0,
782
+ durationMs: Date.now() - startedAt - time.atMs
783
+ });
784
+ } else if ("write" in step) for (const [path, content] of Object.entries(step.write)) await write(path, content);
785
+ else if ("mcp" in step) {
786
+ const time = at();
787
+ const [server = "", tool = ""] = step.mcp.split("/");
788
+ debugLog("harness", `tool mcp__${server}__${tool} called`);
789
+ const r = await callMcpTool(sandbox, mcp, server, tool, signal);
790
+ debugLog("harness", `tool mcp__${server}__${tool} ${r.ok ? "returned" : "failed"}`);
791
+ transcript.push({
792
+ ...time,
793
+ type: "tool",
794
+ toolCallId: `call_${++calls}`,
795
+ tool: `mcp__${server}__${tool}`,
796
+ input: {},
797
+ output: r.output,
798
+ ok: r.ok,
799
+ durationMs: Date.now() - startedAt - time.atMs
800
+ });
801
+ } else if ("error" in step) {
802
+ debugLog("harness", `turn failed: ${step.error}`);
803
+ transcript.push({
804
+ ...at(),
805
+ type: "error",
806
+ message: step.error
807
+ });
808
+ return done("", "error");
809
+ } else scripted = step.reply;
810
+ if (descriptor.delayMs) try {
811
+ await setTimeout$1(descriptor.delayMs, void 0, descriptor.ignoreAbort ? {} : { signal });
812
+ } catch {
813
+ return done("", "aborted");
814
+ }
815
+ for (const [path, content] of Object.entries(files)) await write(path, content);
816
+ const text = scripted ?? reply;
817
+ transcript.push({
818
+ ...at(),
819
+ type: "text",
820
+ text,
821
+ durationMs: 0
822
+ });
823
+ return done(text, "completed");
824
+ },
825
+ async close() {}
826
+ };
827
+ } };
828
+ };
829
+ /**
830
+ * Starts a stdio MCP server in the sandbox, as an agent would, and calls one tool with no
831
+ * arguments: initialize, then the call.
832
+ */
833
+ async function callMcpTool(sandbox, servers, name, tool, signal) {
834
+ const server = servers[name];
835
+ if (!server) {
836
+ const known = Object.keys(servers);
837
+ throw new InfraError(`The fake agent's script calls MCP server "${name}", which the environment doesn't have (it has ${known.length ? known.join(", ") : "none"})`);
838
+ }
839
+ if (server.transport !== "stdio") throw new InfraError(`The fake agent calls stdio MCP servers only; "${name}" is ${server.transport}`);
840
+ const messages = [
841
+ {
842
+ jsonrpc: "2.0",
843
+ id: 1,
844
+ method: "initialize",
845
+ params: {
846
+ protocolVersion: "2024-11-05",
847
+ capabilities: {},
848
+ clientInfo: {
849
+ name: "reeval-fake-agent",
850
+ version: "1"
851
+ }
852
+ }
853
+ },
854
+ {
855
+ jsonrpc: "2.0",
856
+ method: "notifications/initialized"
857
+ },
858
+ {
859
+ jsonrpc: "2.0",
860
+ id: 2,
861
+ method: "tools/call",
862
+ params: {
863
+ name: tool,
864
+ arguments: {}
865
+ }
866
+ }
867
+ ].map((m) => shellQuote(JSON.stringify(m)));
868
+ const command = [server.command, ...server.args].map(shellQuote).join(" ");
869
+ const script = [
870
+ "d=$(mktemp -d) && : > \"$d/out\" && : > \"$d/err\" || exit 1",
871
+ "{",
872
+ " trap '' PIPE",
873
+ ` printf '%s\\n' ${messages.join(" ")} 2>/dev/null`,
874
+ " i=0",
875
+ ` while [ $i -lt 300 ] && [ ! -f "$d/code" ] && ! grep -Eq '"id": ?2[,}]' "$d/out"; do sleep 0.1; i=$((i + 1)); done`,
876
+ `} | { ${command} > "$d/out" 2> "$d/err"; echo $? > "$d/code"; }`,
877
+ "code=$(cat \"$d/code\")",
878
+ "cat \"$d/out\"; cat \"$d/err\" >&2; rm -rf \"$d\"; exit $code"
879
+ ].join("\n");
880
+ const r = await sandbox.exec(script, {
881
+ signal,
882
+ env: server.env
883
+ });
884
+ const reply = r.stdout.split("\n").flatMap((line) => {
885
+ try {
886
+ return [JSON.parse(line)];
887
+ } catch {
888
+ return [];
889
+ }
890
+ }).find((m) => m.id === 2);
891
+ if (!reply) return {
892
+ output: r.stderr.trim() || `no reply (exit ${r.exitCode})`,
893
+ ok: false
894
+ };
895
+ if (reply.error) return {
896
+ output: reply.error.message,
897
+ ok: false
898
+ };
899
+ return {
900
+ output: (reply.result?.content ?? []).flatMap((c) => c.type === "text" ? [c.text] : []).join("\n"),
901
+ ok: !reply.result?.isError
902
+ };
903
+ }
904
+ //#endregion
905
+ //#region src/harness/index.ts
906
+ async function createHarness(descriptor) {
907
+ if (descriptor.kind === "fake") return createFakeHarness(descriptor);
908
+ const { createAgentHarness } = await import("./agent-DVHkx8aT.js");
909
+ return createAgentHarness(descriptor);
910
+ }
911
+ /** Where each harness's agent looks for repo skills, relative to the workspace. */
912
+ const workspaceSkillDir = (descriptor) => layoutOf(descriptor).workspaceSkillDir;
913
+ //#endregion
914
+ //#region src/attempt/environment.ts
915
+ /**
916
+ * Phase 1: resolves the attempt's environment (files, skills, secrets and their placeholders)
917
+ * and finds the image a Docker sandbox starts from, which preflight prepared. Runs on the test's
918
+ * signal, before the eval timer starts.
919
+ */
920
+ async function prepareEnvironment(a, root, runId, where, signal) {
921
+ const { spec, record } = a;
922
+ const env = await resolveEnvironment(spec.environment, spec.env, root, where, layoutOf(spec.harness).rulesFile, spec.egress.policy, (w) => preparedWorkspace(root, runId, w));
923
+ a.env = env;
924
+ record.environment = env.record;
925
+ const { secrets, brokered } = env.record;
926
+ record.env = {
927
+ vars: Object.keys(env.vars).filter((n) => !secrets.includes(n)).sort(),
928
+ secrets,
929
+ brokered
930
+ };
931
+ const debug = attemptDebug();
932
+ if (debug) traceEnvironment(debug, spec, env);
933
+ record.image = null;
934
+ if (spec.sandbox.kind === "docker") {
935
+ const { preparedFor } = await import("./preflight-BtubXKQ8.js");
936
+ const { dockerProvider } = await import("./providers-BUOxitcT.js");
937
+ signal.throwIfAborted();
938
+ const prepared = preparedFor(root, runId, dockerProvider(), spec.sandbox, spec.harness);
939
+ if (!prepared) throw new InfraError(`The sandbox image for ${where} wasn't prepared before the run started`);
940
+ record.image = prepared;
941
+ a.image = record.image.ref;
942
+ const { ref, id, base, install, agentCli } = record.image;
943
+ debugLog("image", `ready ${ref} (${id.replace(/^sha256:/, "").slice(0, 12)}) · prepared before the run${ref !== base ? ` · on ${base}` : ""}${agentCli ? ` · agent CLI ${agentCli}` : ""}${install.length ? ` · install: ${install.join(" && ")}` : ""}`);
944
+ } else debugLog("image", `none: the ${spec.sandbox.kind} sandbox runs on the host`);
945
+ }
946
+ //#endregion
947
+ //#region src/attempt/judge.ts
948
+ const noJudgeModel = (configFile) => `t.judge() has no model. Set judge: { model: 'typesafe-ai/jev-1.13.0' } in ${configFile}, on the group or eval, or pass { model } to this call.`;
949
+ const isTurn = (value) => typeof value === "object" && value !== null && "transcript" in value && "diff" in value;
950
+ function judgeState(turn, output = turn.text) {
951
+ return {
952
+ task: turn.prompt,
953
+ output,
954
+ files: changedFilesForJudge(turn.diff.files),
955
+ diff: patchForJudge(turn.diff.patch)
956
+ };
957
+ }
958
+ /**
959
+ * Phase 4, the judge: grades a turn (the last by default) or a value against a question, under
960
+ * the judge provider's concurrency lease. Every call's input is recorded, including calls that
961
+ * fail, and a failure marks the attempt's assertion errors as the judge's.
962
+ */
963
+ function judgeCaller(a) {
964
+ return async (question, { on, ...call } = {}) => {
965
+ if (typeof question !== "string" || !question.trim()) throw new Error(`t.judge() needs a question: a non-empty string (got ${JSON.stringify(question)})`);
966
+ const { threshold } = call;
967
+ if (threshold !== void 0 && !(typeof threshold === "number" && threshold >= 0 && threshold <= 1)) throw new Error(`t.judge() threshold must be between 0 and 1 (got ${String(threshold)})`);
968
+ const subject = isTurn(on) ? on : a.turns.at(-1);
969
+ if (!subject) throw new Error("t.judge() has no turn to grade: call t.send() first");
970
+ const options = {
971
+ ...a.spec.judge,
972
+ ...call
973
+ };
974
+ const { model } = options;
975
+ if (model === void 0) {
976
+ a.judgeErrored = true;
977
+ throw new Error(noJudgeModel(getRunContext().config.file));
978
+ }
979
+ const state = judgeState(subject, on === void 0 || isTurn(on) ? void 0 : on);
980
+ const input = {
981
+ question,
982
+ model: judgeModelName(model),
983
+ state
984
+ };
985
+ a.record.judgeInputs?.push(input);
986
+ const start = performance.now();
987
+ const ms = () => Math.round(performance.now() - start);
988
+ a.report({
989
+ kind: "judge-start",
990
+ question
991
+ });
992
+ debugLog("judge", `asking ${input.model}: ${JSON.stringify(clip(question, 160))}`);
993
+ debugTrace("judge", "prompt", input, "judge prompt");
994
+ const { maxConcurrent, ...judgeOptions } = options;
995
+ const provider = judgeModelName(model).split("/")[0];
996
+ try {
997
+ const { verdict, retries } = await a.timed("judgeMs", () => withLease(maxConcurrent ? { [`judge:${provider}`]: maxConcurrent } : {}, () => runJudge(question, state, {
998
+ ...judgeOptions,
999
+ model,
1000
+ threshold: options.threshold ?? .8
1001
+ }, a.signal), a.signal));
1002
+ a.record.judgements.push(retries.length ? {
1003
+ ...verdict,
1004
+ retries
1005
+ } : verdict);
1006
+ const { score, threshold } = verdict;
1007
+ debugLog("judge", `${verdict.resolvedModel ?? verdict.model} · ${formatDuration(ms())} · score ${score.toFixed(2)} ${score >= threshold ? "≥" : "<"} ${threshold} · ${verdict.verdict}${retries.length ? ` · ${retries.length} retries` : ""} · reason: P(yes) ${score.toFixed(2)} is ${score >= threshold ? "at or above" : "below"} the ${threshold} threshold`);
1008
+ debugTrace("judge", "response", {
1009
+ verdict,
1010
+ retries
1011
+ }, "judge response");
1012
+ a.report({
1013
+ kind: "judge-end",
1014
+ question,
1015
+ verdict: verdict.verdict,
1016
+ score,
1017
+ threshold,
1018
+ ms: ms()
1019
+ });
1020
+ return verdict;
1021
+ } catch (error) {
1022
+ a.judgeErrored = true;
1023
+ const { retries } = error;
1024
+ input.error = error instanceof Error ? error.message : String(error);
1025
+ if (retries?.length) input.retries = retries;
1026
+ debugLog("judge", `${input.model} · ${formatDuration(ms())} · error: ${input.error}`);
1027
+ debugTrace("judge", "error", {
1028
+ error: input.error,
1029
+ retries
1030
+ }, "judge error");
1031
+ a.report({
1032
+ kind: "judge-end",
1033
+ question,
1034
+ verdict: "error",
1035
+ score: null,
1036
+ threshold: null,
1037
+ ms: ms()
1038
+ });
1039
+ throw error;
1040
+ }
1041
+ };
1042
+ }
1043
+ //#endregion
1044
+ //#region src/sandbox/index.ts
1045
+ async function createSandbox(descriptor, options) {
1046
+ try {
1047
+ switch (descriptor.kind) {
1048
+ case "fake": return await createFakeSandbox(descriptor, options.env);
1049
+ case "docker": {
1050
+ const { createDockerSandbox } = await import("./docker-DnWAVdEm.js");
1051
+ return await createDockerSandbox(descriptor, options);
1052
+ }
1053
+ }
1054
+ } catch (error) {
1055
+ if (error instanceof InfraError || options.signal.aborted) throw error;
1056
+ throw new InfraError(`Failed to create ${descriptor.kind} sandbox: ${error.message}`);
1057
+ }
1058
+ }
1059
+ //#endregion
1060
+ //#region src/attempt/record.ts
1061
+ function classify(error, signal) {
1062
+ const message = error instanceof Error ? error.message : String(error);
1063
+ if (signal.aborted) {
1064
+ const reason = signal.reason instanceof Error ? signal.reason.message : String(signal.reason);
1065
+ return isTimeout(reason) ? {
1066
+ kind: "timeout",
1067
+ message: "The eval timed out"
1068
+ } : {
1069
+ kind: "aborted",
1070
+ message: "The run was cancelled"
1071
+ };
1072
+ }
1073
+ if (error instanceof InfraError) return {
1074
+ kind: "infra",
1075
+ message
1076
+ };
1077
+ return {
1078
+ kind: "assertion",
1079
+ message
1080
+ };
1081
+ }
1082
+ /**
1083
+ * Why the attempt failed, into the record: the eval's own timeout, a cancel, an infra error, or an
1084
+ * assertion, which is the judge's when a judge call failed or gave a failing verdict.
1085
+ */
1086
+ function recordFailure(a, error, timedOut) {
1087
+ const { record } = a;
1088
+ record.error = timedOut ? {
1089
+ kind: "timeout",
1090
+ message: "The eval timed out"
1091
+ } : classify(error, a.signal);
1092
+ record.error.message = capMiddle(record.error.message);
1093
+ if (error instanceof PrepareError && !timedOut) {
1094
+ const { class: kind, stage, ref, cause, log, setBy } = error.failure;
1095
+ record.error.prepare = {
1096
+ class: kind,
1097
+ stage,
1098
+ ref,
1099
+ cause,
1100
+ log,
1101
+ setBy: setBy ?? []
1102
+ };
1103
+ }
1104
+ const judgeFailed = a.judgeErrored || record.judgements.some((j) => j.verdict !== "pass");
1105
+ if (record.error.kind === "assertion" && judgeFailed) record.error.kind = "judge";
1106
+ record.outcome = record.error.kind === "infra" ? "infra_error" : "fail";
1107
+ }
1108
+ /**
1109
+ * The attempt's egress policy and, when its sandbox records it, what it contacted and was refused.
1110
+ * Refused hosts the harness always tries are `expectedBlocked`, summarised in one `-v` line.
1111
+ */
1112
+ function recordEgress(a, sandbox) {
1113
+ const traffic = sandbox?.traffic?.();
1114
+ const expected = expectedBlockedFor(a.spec.harness);
1115
+ const refused = traffic?.blocked ?? [];
1116
+ const isExpected = (h) => !!expected?.hosts.includes(h.host);
1117
+ const expectedBlocked = refused.filter(isExpected);
1118
+ a.record.egress = {
1119
+ policy: a.spec.egress.policy,
1120
+ allowed: allowedHosts(a.spec.egress),
1121
+ recorded: !!traffic,
1122
+ contacted: traffic?.contacted ?? [],
1123
+ blocked: refused.filter((h) => !isExpected(h)),
1124
+ expectedBlocked
1125
+ };
1126
+ if (expected && expectedBlocked.length) {
1127
+ const hosts = expectedBlocked.map((h) => `${h.host}:${h.port} ×${h.connections}`).join(", ");
1128
+ debugLog("gateway", `expected blocks, not logged per request: ${hosts} · ${expected.why}`);
1129
+ }
1130
+ a.record.servedModels = sandbox?.servedModels?.() ?? null;
1131
+ }
1132
+ /** Phase 6: writes the attempt's record, redacted, and returns what the runner reports. */
1133
+ async function writeRecord(a, stagingDir, id) {
1134
+ const { record, spec } = a;
1135
+ const recordPath = join(stagingDir, "evals", recordFileName(attemptKey(id, spec.cell.key, spec.n)));
1136
+ await mkdir(join(stagingDir, "evals"), { recursive: true });
1137
+ const tmp = `${recordPath}.${process.pid}.tmp`;
1138
+ await writeFile(tmp, redactSecrets(JSON.stringify(record, null, 2)));
1139
+ await rename(tmp, recordPath);
1140
+ return {
1141
+ outcome: record.outcome,
1142
+ errorKind: record.error?.kind ?? null,
1143
+ judgements: record.judgements.map((j) => ({
1144
+ verdict: j.verdict,
1145
+ score: j.score,
1146
+ threshold: j.threshold,
1147
+ question: clip(j.question, 200)
1148
+ })),
1149
+ turnErrors: turnErrors(record.turns.map((t) => t.transcript)),
1150
+ ...record.error?.prepare ? { prepare: {
1151
+ ref: record.error.prepare.ref,
1152
+ stage: record.error.prepare.stage
1153
+ } } : {}
1154
+ };
1155
+ }
1156
+ /** `turn N error: <message>` for each turn with an error event. */
1157
+ function turnErrors(transcripts) {
1158
+ return transcripts.flatMap((events, i) => {
1159
+ const found = events.find((e) => e.type === "error");
1160
+ return found?.type === "error" ? [`turn ${i + 1} error: ${redactSecrets(clip(found.message.trim(), 500))}`] : [];
1161
+ });
1162
+ }
1163
+ //#endregion
1164
+ //#region src/diff.ts
1165
+ const GIT = "git -c user.name=reeval -c user.email=reeval@localhost -c core.hooksPath=/dev/null";
1166
+ /** Records the workspace state before the agent runs, in a git dir outside the workspace. */
1167
+ async function snapshot(sandbox, signal) {
1168
+ const r = await sandbox.exec(`export GIT_DIR="$PWD/../.reeval-git-$(basename "$PWD")" GIT_WORK_TREE="$PWD" && ${GIT} init -q && ${GIT} add -A && ${GIT} commit -q --allow-empty -m base`, { signal });
1169
+ if (r.exitCode !== 0) throw new InfraError(`Failed to snapshot the workspace: ${r.stderr.trim()}`);
1170
+ }
1171
+ async function diffSinceSnapshot(sandbox, signal) {
1172
+ const env = `export GIT_DIR="$PWD/../.reeval-git-$(basename "$PWD")" GIT_WORK_TREE="$PWD" && ${GIT} add -A && `;
1173
+ const patch = await sandbox.exec(`${env}${GIT} diff --cached --binary HEAD`, { signal });
1174
+ const names = await sandbox.exec(`${env}${GIT} diff --cached --name-status --no-renames HEAD`, { signal });
1175
+ if (patch.exitCode !== 0 || names.exitCode !== 0) throw new InfraError(`Failed to diff the workspace: ${patch.stderr || names.stderr}`);
1176
+ const status = {
1177
+ A: "added",
1178
+ M: "modified",
1179
+ D: "deleted"
1180
+ };
1181
+ const files = names.stdout.split("\n").filter(Boolean).map((line) => {
1182
+ const [code = "M", ...rest] = line.split(" ");
1183
+ return {
1184
+ path: rest.join(" "),
1185
+ status: status[code[0]] ?? "modified"
1186
+ };
1187
+ });
1188
+ return {
1189
+ patch: patch.stdout,
1190
+ files
1191
+ };
1192
+ }
1193
+ //#endregion
1194
+ //#region src/sandbox/phases.ts
1195
+ const strip = ({ phase: _, ...sub }) => sub;
1196
+ /**
1197
+ * Which brokered secrets the gateway swaps in when. `to: 'checks'` secrets are active only while
1198
+ * at least one check or setup step is running, so a request the agent makes between them gets
1199
+ * the placeholder.
1200
+ */
1201
+ function brokerPhases(box, subs) {
1202
+ const agent = subs.filter((s) => s.phase === "agent").map(strip);
1203
+ const all = subs.map(strip);
1204
+ const perPhase = box.setSubstitutions && all.length > agent.length;
1205
+ let running = 0;
1206
+ return {
1207
+ async start() {
1208
+ if (all.length > 0) await box.setSubstitutions?.(agent);
1209
+ },
1210
+ async checks(fn) {
1211
+ if (!perPhase) return fn();
1212
+ if (running === 0) await box.setSubstitutions?.(all);
1213
+ running++;
1214
+ try {
1215
+ return await fn();
1216
+ } finally {
1217
+ if (--running === 0) await box.setSubstitutions?.(agent);
1218
+ }
1219
+ }
1220
+ };
1221
+ }
1222
+ //#endregion
1223
+ //#region src/attempt/state.ts
1224
+ /** Extra time an attempt gets, outside its eval timeout, when its image may need building. */
1225
+ const SETUP_BUDGET_MS = 12e5;
1226
+ function createAttempt(spec, signal, progress) {
1227
+ const record = {
1228
+ startedAt: (/* @__PURE__ */ new Date()).toISOString(),
1229
+ outcome: "pass",
1230
+ error: null,
1231
+ turns: [],
1232
+ diff: null,
1233
+ checks: [],
1234
+ judgements: [],
1235
+ timings: {
1236
+ setupMs: 0,
1237
+ agentMs: 0,
1238
+ diffMs: 0,
1239
+ checksMs: 0,
1240
+ judgeMs: 0
1241
+ },
1242
+ judgeInputs: []
1243
+ };
1244
+ return {
1245
+ spec,
1246
+ record,
1247
+ turns: [],
1248
+ report: (update) => progress({
1249
+ at: Date.now(),
1250
+ ...update
1251
+ }),
1252
+ async timed(phase, fn) {
1253
+ const start = performance.now();
1254
+ try {
1255
+ return await fn();
1256
+ } finally {
1257
+ record.timings[phase] += Math.round(performance.now() - start);
1258
+ }
1259
+ },
1260
+ signal,
1261
+ judgeErrored: false
1262
+ };
1263
+ }
1264
+ const startTimeoutMs = (harness) => parseDuration(harness.startTimeout ?? "90s");
1265
+ /**
1266
+ * Setup steps and the harness's start (once retried) run outside the eval timeout, so the
1267
+ * attempt's own limit makes room for them.
1268
+ */
1269
+ function setupAllowance(planned) {
1270
+ const setupMs = planned.environment.layers.flatMap((l) => l.spec.setup ?? []).reduce((ms, step) => ms + parseDuration((typeof step === "string" ? void 0 : step.timeout) ?? "5m"), 0);
1271
+ const { sandbox } = planned;
1272
+ return setupMs + (sandbox.kind === "docker" && !!(sandbox.from || sandbox.dockerfile || sandbox.install?.length) ? SETUP_BUDGET_MS : 0) + 2 * startTimeoutMs(planned.harness);
1273
+ }
1274
+ /** The eval's timer, paused while the sandbox and its setup steps start. */
1275
+ function evalTimer(ms) {
1276
+ const controller = new AbortController();
1277
+ let left = ms;
1278
+ let since = Date.now();
1279
+ let timer;
1280
+ const fire = () => controller.abort(new DOMException(`Eval timed out after ${ms / 1e3}s`, "TimeoutError"));
1281
+ const start = () => {
1282
+ since = Date.now();
1283
+ timer = setTimeout(fire, Math.max(0, left));
1284
+ };
1285
+ start();
1286
+ return {
1287
+ signal: controller.signal,
1288
+ pause() {
1289
+ clearTimeout(timer);
1290
+ left -= Date.now() - since;
1291
+ },
1292
+ resume: start,
1293
+ clear: () => clearTimeout(timer)
1294
+ };
1295
+ }
1296
+ //#endregion
1297
+ //#region src/attempt/session.ts
1298
+ const tailOf = (text) => text.trim().split("\n").slice(-10).join("\n");
1299
+ /** Setup steps in order, each with its own timeout; a failure is an infra error. */
1300
+ async function runSetup(a, box, env) {
1301
+ const { record, spec } = a;
1302
+ const steps = env.setup;
1303
+ record.setup = [];
1304
+ for (const [i, step] of steps.entries()) {
1305
+ const what = `setup step ${i + 1}/${steps.length} "${clip(step.run, 80)}" in environment ${spec.environment.name}`;
1306
+ const start = performance.now();
1307
+ const stepSignal = AbortSignal.any([a.signal, AbortSignal.timeout(step.timeoutMs)]);
1308
+ const r = await asCheck(a, () => box.exec(step.run, {
1309
+ signal: stepSignal,
1310
+ env: env.checkVars,
1311
+ ...step.cwd ? { cwd: step.cwd } : {}
1312
+ })).catch((error) => {
1313
+ if (a.signal.aborted) throw error;
1314
+ if (!stepSignal.aborted) throw new InfraError(`${what} couldn't run: ${error instanceof Error ? error.message : error}`);
1315
+ return null;
1316
+ });
1317
+ const durationMs = Math.round(performance.now() - start);
1318
+ const output = r ? [r.stdout, r.stderr].filter(Boolean).join("\n") : "";
1319
+ record.setup.push({
1320
+ run: step.run,
1321
+ exitCode: r?.exitCode ?? null,
1322
+ durationMs,
1323
+ output
1324
+ });
1325
+ debugLog("env", `setup ${i + 1}/${steps.length} $ ${clip(step.run, 120)} → ${r ? `exit ${r.exitCode}` : `timed out after ${formatDuration(step.timeoutMs)}`} · ${formatDuration(durationMs)}${step.cwd ? ` · in ${step.cwd}` : ""}`);
1326
+ debugTrace("env", "setup", {
1327
+ run: step.run,
1328
+ cwd: step.cwd,
1329
+ exitCode: r?.exitCode ?? null,
1330
+ durationMs,
1331
+ stdout: r?.stdout,
1332
+ stderr: r?.stderr
1333
+ }, `setup ${i + 1} output`);
1334
+ if (r?.exitCode === 0) continue;
1335
+ throw new InfraError(r ? `${what} exited ${r.exitCode} after ${formatDuration(durationMs)} (see setup.log):\n${tailOf(r.stderr || r.stdout)}` : `${what} timed out after ${formatDuration(step.timeoutMs)} (see setup.log)`);
1336
+ }
1337
+ }
1338
+ const WORKSPACE_TAR = ".reeval-workspace.tar";
1339
+ /** Unpacks the git workspace's tarball into the (empty) workspace. */
1340
+ async function copyWorkspace(box, workspace, signal) {
1341
+ const start = performance.now();
1342
+ const tar = await readFile(workspace.tar);
1343
+ await box.writeFile(WORKSPACE_TAR, tar);
1344
+ const r = await box.exec(`tar -xf ${WORKSPACE_TAR} && rm -f ${WORKSPACE_TAR}`, { signal });
1345
+ if (r.exitCode !== 0) throw new InfraError(`Couldn't unpack the workspace ${workspace.label} into the sandbox (the image needs tar): ${tailOf(r.stderr || r.stdout)}`);
1346
+ if (workspace.init) {
1347
+ const at = "2000-01-01T00:00:00Z";
1348
+ const init = await box.exec(`git init -q -b main && git add -A && git -c commit.gpgsign=false commit -q --no-verify -m ${shellQuote(`${workspace.label} at ${workspace.commit}`)}`, {
1349
+ signal,
1350
+ env: {
1351
+ GIT_AUTHOR_NAME: "reeval",
1352
+ GIT_AUTHOR_EMAIL: "reeval@localhost",
1353
+ GIT_AUTHOR_DATE: at,
1354
+ GIT_COMMITTER_NAME: "reeval",
1355
+ GIT_COMMITTER_EMAIL: "reeval@localhost",
1356
+ GIT_COMMITTER_DATE: at
1357
+ }
1358
+ });
1359
+ if (init.exitCode !== 0) throw new InfraError(`Couldn't give the workspace ${workspace.label} a .git (init: true needs git in the image): ${tailOf(init.stderr || init.stdout)}`);
1360
+ }
1361
+ debugLog("env", `workspace ${workspace.label} at ${workspace.commit.slice(0, 12)} (${tar.byteLength} bytes of tar${workspace.init ? ", with a .git" : ""}) in ${formatDuration(performance.now() - start)}`);
1362
+ }
1363
+ /**
1364
+ * Starts the harness session within its `startTimeout`. A start that doesn't become ready in time
1365
+ * is aborted (and closed, should it finish later) and started once more: a stuck bootstrap is
1366
+ * usually transient, and nothing has run yet, so the retry can't change what the attempt
1367
+ * measures. A second timeout is an infra error; a start that fails outright isn't retried.
1368
+ */
1369
+ async function startHarness(a, harness, box, options) {
1370
+ const { harness: descriptor } = a.spec;
1371
+ const ms = startTimeoutMs(descriptor);
1372
+ const late = `${descriptor.kind}: session did not become ready within ${formatDuration(ms)}`;
1373
+ for (let tries = 1;; tries++) {
1374
+ const stop = new AbortController();
1375
+ let timer;
1376
+ const timedOut = new Promise((resolve) => {
1377
+ timer = setTimeout(() => resolve(null), ms);
1378
+ });
1379
+ const starting = harness.start(box, AbortSignal.any([a.signal, stop.signal]), options);
1380
+ const session = await Promise.race([starting, timedOut]).finally(() => clearTimeout(timer));
1381
+ if (session) return session;
1382
+ stop.abort(new InfraError(late));
1383
+ starting.then((s) => s.close().catch(() => {}), () => {});
1384
+ a.signal.throwIfAborted();
1385
+ if (tries === 2) throw new InfraError(`${late} (tried twice)`);
1386
+ a.record.startRetries = [{
1387
+ error: late,
1388
+ waitedMs: ms
1389
+ }];
1390
+ debugLog("harness", `${late}; starting it once more`);
1391
+ }
1392
+ }
1393
+ /**
1394
+ * Phase 2: starts the sandbox (with the brokered secrets its gateway swaps in), writes the
1395
+ * environment's files and workspace skills, runs its setup steps, snapshots the workspace for
1396
+ * the diff, and starts the harness session with the harness-placed skills and MCP servers.
1397
+ */
1398
+ async function startSession(a, runId, setupStart) {
1399
+ const { spec } = a;
1400
+ const env = a.env;
1401
+ const box = await createSandbox(spec.sandbox, {
1402
+ runId,
1403
+ timeoutMs: spec.timeoutMs + setupAllowance(spec) + 6e4,
1404
+ signal: a.signal,
1405
+ env: env.vars,
1406
+ image: a.image,
1407
+ brokering: brokeringFor(spec.harness),
1408
+ expectedBlocked: expectedBlockedFor(spec.harness)?.hosts,
1409
+ substitutions: env.substitutions,
1410
+ egress: spec.egress
1411
+ });
1412
+ a.sandbox = box;
1413
+ if (spec.sandbox.kind === "fake") debugLog("container", `fake sandbox at ${box.workdir}`);
1414
+ a.phases = brokerPhases(box, env.substitutions);
1415
+ if (a.signal.aborted) box.destroy();
1416
+ a.signal.throwIfAborted();
1417
+ await a.phases.start();
1418
+ if (env.substitutions.length) {
1419
+ const agent = env.substitutions.filter((s) => s.phase === "agent").map((s) => s.name);
1420
+ debugLog("broker", `gateway swaps in ${agent.join(", ") || "nothing"} for the agent`);
1421
+ }
1422
+ if (env.workspace) await copyWorkspace(box, env.workspace, a.signal);
1423
+ for (const f of env.files) {
1424
+ await box.writeFile(f.path, f.content);
1425
+ const size = typeof f.content === "string" ? Buffer.byteLength(f.content) : f.content.byteLength;
1426
+ debugLog("env", `file ${f.path} (${size} bytes)`);
1427
+ }
1428
+ const skillDir = workspaceSkillDir(spec.harness);
1429
+ for (const skill of env.workspaceSkills) {
1430
+ const files = skillFiles(skill);
1431
+ for (const f of files) await box.writeFile(`${skillDir}/${skill.name}/${f.path}`, f.content);
1432
+ debugLog("env", `skill ${skill.name} → ${skillDir}/${skill.name}/ (${files.length} files, workspace)`);
1433
+ }
1434
+ const layout = layoutOf(spec.harness);
1435
+ for (const skill of env.skills) debugLog("env", `skill ${skill.name} → the harness adapter places it (~/${layout.harnessSkillDir}/${skill.name}/ for ${spec.harness.kind === "fake" ? `the fake ${spec.harness.layout ?? "cursor"} layout` : spec.harness.kind})`);
1436
+ for (const [name, server] of Object.entries(env.mcp)) debugLog("env", `mcp ${name} (${server.transport}${server.transport === "stdio" ? `: ${server.command} ${server.args.join(" ")}` : `: ${server.url}`}) → the harness adapter writes its MCP config`);
1437
+ await runSetup(a, box, env);
1438
+ await snapshot(box, a.signal);
1439
+ debugLog("env", "workspace snapshot taken: the diff starts here");
1440
+ const harness = await createHarness(spec.harness);
1441
+ const harnessStart = performance.now();
1442
+ debugLog("harness", `starting ${harnessId(spec.harness)}: bootstrap and session`);
1443
+ const session = await startHarness(a, harness, box, {
1444
+ skills: env.skills,
1445
+ workspaceSkills: env.workspaceSkills.map((s) => s.name),
1446
+ mcp: env.mcp
1447
+ });
1448
+ debugLog("harness", `${harnessId(spec.harness)} session ready in ${formatDuration(performance.now() - harnessStart)}`);
1449
+ a.session = session;
1450
+ a.report({
1451
+ kind: "sandbox-ready",
1452
+ ms: Math.round(performance.now() - setupStart)
1453
+ });
1454
+ return [box, session];
1455
+ }
1456
+ //#endregion
1457
+ //#region src/attempt/turns.ts
1458
+ /** How long a turn gets to stop once its attempt is aborted, before reeval stops waiting for it. */
1459
+ const TURN_STOP_GRACE_MS = 3e3;
1460
+ /**
1461
+ * Resolves `grace` ms after `signal` aborts, with the turn as cut off; `cancel` drops the timer
1462
+ * once the turn settles on its own.
1463
+ */
1464
+ function cutOff(signal, startedAt, grace = TURN_STOP_GRACE_MS) {
1465
+ let timer;
1466
+ let onAbort = () => {};
1467
+ const cutoff = new Promise((resolve) => {
1468
+ onAbort = () => {
1469
+ timer = setTimeout(() => {
1470
+ const message = `The agent didn't stop within ${grace / 1e3}s of being cancelled, so reeval stopped waiting for its turn`;
1471
+ const at = Date.now() - Date.parse(startedAt);
1472
+ resolve({
1473
+ text: "",
1474
+ status: "aborted",
1475
+ transcript: [{
1476
+ type: "error",
1477
+ message,
1478
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
1479
+ atMs: at
1480
+ }],
1481
+ usage: null,
1482
+ model: null,
1483
+ unreported: [],
1484
+ startedAt,
1485
+ durationMs: at
1486
+ });
1487
+ }, grace);
1488
+ };
1489
+ if (signal.aborted) onAbort();
1490
+ else signal.addEventListener("abort", onAbort, { once: true });
1491
+ });
1492
+ const cancel = () => {
1493
+ clearTimeout(timer);
1494
+ signal.removeEventListener("abort", onAbort);
1495
+ };
1496
+ return {
1497
+ cutoff,
1498
+ cancel
1499
+ };
1500
+ }
1501
+ /**
1502
+ * Phase 3: one turn. Sends the prompt to the started session, records the turn as it goes (an
1503
+ * errored entry first, so a turn that throws is still in the record), takes the agent's context
1504
+ * after the first, and diffs the workspace since the snapshot.
1505
+ */
1506
+ function turnSender(a, start, takeContext, checks) {
1507
+ return async (prompt) => {
1508
+ const [box, agent] = await start();
1509
+ const entry = {
1510
+ prompt,
1511
+ text: "",
1512
+ status: "error",
1513
+ transcript: [],
1514
+ usage: null,
1515
+ model: null,
1516
+ unreported: [],
1517
+ startedAt: (/* @__PURE__ */ new Date()).toISOString(),
1518
+ durationMs: 0
1519
+ };
1520
+ a.record.turns.push(entry);
1521
+ const turnNumber = a.record.turns.length;
1522
+ a.report({
1523
+ kind: "turn-start",
1524
+ turn: turnNumber
1525
+ });
1526
+ debugLog("harness", `turn ${turnNumber} started: ${JSON.stringify(clip(prompt, 160))}`);
1527
+ debugTrace("harness", "prompt", {
1528
+ turn: turnNumber,
1529
+ prompt
1530
+ }, `turn ${turnNumber} prompt`);
1531
+ const { cutoff, cancel } = cutOff(a.signal, entry.startedAt);
1532
+ const { raw, ...result } = await a.timed("agentMs", () => Promise.race([agent.send(prompt, a.signal), cutoff])).finally(cancel);
1533
+ Object.assign(entry, result, raw ? { raw } : {});
1534
+ for (const event of result.transcript) debugTrace("harness", "transcript", {
1535
+ turn: turnNumber,
1536
+ ...event
1537
+ }, `transcript ${event.type}`);
1538
+ await takeContext(box, a.signal);
1539
+ const count = (...types) => result.transcript.filter((e) => types.includes(e.type)).length;
1540
+ const failure = result.transcript.find((e) => e.type === "error");
1541
+ a.report({
1542
+ kind: "turn-end",
1543
+ turn: turnNumber,
1544
+ status: result.status,
1545
+ ms: result.durationMs,
1546
+ tools: count("tool", "web-search", "web-fetch"),
1547
+ fileEdits: count("file-edit"),
1548
+ ...failure?.type === "error" ? { error: redactSecrets(clip(failure.message.trim(), 300)) } : {}
1549
+ });
1550
+ const plural = (n, word) => `${n} ${word}${n === 1 ? "" : "s"}`;
1551
+ debugLog("harness", `turn ${turnNumber} ${result.status} in ${formatDuration(result.durationMs)} · ${plural(count("tool", "web-search", "web-fetch"), "tool call")} · ${plural(count("file-edit"), "file edit")}${failure && "message" in failure ? ` · error: ${clip(String(failure.message), 200)}` : ""}${result.model ? ` · model ${result.model}` : ""}`);
1552
+ a.signal.throwIfAborted();
1553
+ const diff = await a.timed("diffMs", () => diffSinceSnapshot(box, a.signal));
1554
+ a.record.diff = diff;
1555
+ const data = {
1556
+ prompt,
1557
+ ...result,
1558
+ diff
1559
+ };
1560
+ const turn = Object.freeze({
1561
+ ...data,
1562
+ ...checks
1563
+ });
1564
+ a.turns.push(turn);
1565
+ return turn;
1566
+ };
1567
+ }
1568
+ //#endregion
1569
+ //#region src/attempt/index.ts
1570
+ const TRACE_BUFFER_DEFAULT = 8 * 2 ** 20;
1571
+ async function runAttempt(spec, fn, handle) {
1572
+ const ctx = getRunContext();
1573
+ const { config, runId } = ctx;
1574
+ const verbosity = ctx.verbosity ?? 0;
1575
+ const id = evalId(relativeFile(config.root, handle.file), [...spec.groups, spec.name]);
1576
+ const debug = new AttemptDebug({
1577
+ dir: attemptDebugDir(config.root, runId, id, spec.cell.key, spec.n),
1578
+ verbosity,
1579
+ traceLimit: config.debug?.traceBufferBytes ?? TRACE_BUFFER_DEFAULT,
1580
+ ...verbosity > 0 ? { emit: handle.debug } : {}
1581
+ });
1582
+ return withDebug(debug, () => attemptBody(spec, fn, handle, id, debug));
1583
+ }
1584
+ async function attemptBody(spec, fn, { signal: testSignal, setMeta, progress, unawaited }, id, debug) {
1585
+ const setupStart = performance.now();
1586
+ const ctx = getRunContext();
1587
+ const { runId, stagingDir, config } = ctx;
1588
+ tracePlan(debug, spec, ctx);
1589
+ const a = createAttempt(spec, testSignal, progress);
1590
+ let timer;
1591
+ let clock;
1592
+ const destroy = () => void a.sandbox?.destroy();
1593
+ testSignal.addEventListener("abort", destroy, { once: true });
1594
+ let starting;
1595
+ const start = () => {
1596
+ starting ??= (async () => {
1597
+ clock?.pause();
1598
+ try {
1599
+ return await a.timed("setupMs", () => startSession(a, runId, setupStart));
1600
+ } finally {
1601
+ clock?.resume();
1602
+ }
1603
+ })();
1604
+ return starting;
1605
+ };
1606
+ const takeContext = contextTaker(a);
1607
+ const { exec, readFile } = createChecks(a);
1608
+ const t = {
1609
+ get signal() {
1610
+ return a.signal;
1611
+ },
1612
+ turns: a.turns,
1613
+ exec,
1614
+ readFile,
1615
+ attempt: {
1616
+ id: attemptId(spec.cell.key, spec.n),
1617
+ cell: spec.cell,
1618
+ n: spec.n
1619
+ },
1620
+ async sandbox() {
1621
+ await start();
1622
+ return {
1623
+ exec,
1624
+ readFile
1625
+ };
1626
+ },
1627
+ send: turnSender(a, start, takeContext, {
1628
+ exec,
1629
+ readFile
1630
+ }),
1631
+ judge: judgeCaller(a)
1632
+ };
1633
+ const timedOut = () => !!timer?.aborted && !testSignal.aborted;
1634
+ try {
1635
+ a.report({ kind: "sandbox" });
1636
+ const where = `${id} [${attemptId(spec.cell.key, spec.n)}]`;
1637
+ await a.timed("setupMs", () => prepareEnvironment(a, config.root, runId, where, testSignal));
1638
+ clock = evalTimer(spec.timeoutMs);
1639
+ timer = clock.signal;
1640
+ a.signal = AbortSignal.any([testSignal, timer]);
1641
+ timer.addEventListener("abort", destroy, { once: true });
1642
+ await fn(t);
1643
+ a.signal.throwIfAborted();
1644
+ const missed = await unawaited();
1645
+ if (missed) throw missed;
1646
+ } catch (error) {
1647
+ recordFailure(a, error, timedOut());
1648
+ if (timedOut()) throw new Error(`Eval timed out after ${spec.timeoutMs / 1e3}s`);
1649
+ throw error;
1650
+ } finally {
1651
+ if (!a.signal.aborted) await unawaited().catch(() => {});
1652
+ clock?.clear();
1653
+ const { sandbox } = a;
1654
+ if (sandbox && !a.signal.aborted) await takeContext(sandbox, AbortSignal.timeout(3e4));
1655
+ if (sandbox) a.report({ kind: "cleanup" });
1656
+ testSignal.removeEventListener("abort", destroy);
1657
+ const teardown = performance.now();
1658
+ await a.session?.close().catch(() => {});
1659
+ if (a.session) debug.log("teardown", "harness session closed");
1660
+ recordEgress(a, sandbox);
1661
+ await sandbox?.destroy();
1662
+ if (sandbox) debug.log("teardown", `sandbox destroyed (${formatDuration(performance.now() - teardown)} for teardown)`);
1663
+ const { record } = a;
1664
+ const error = record.error ? ` · ${record.error.kind}: ${record.error.message}` : "";
1665
+ debug.log("teardown", `outcome ${record.outcome}${error}`);
1666
+ const rel = (path) => relativeFile(config.root, path) || ".";
1667
+ const scratch = `.reeval/runs/${runId}`;
1668
+ debug.log("teardown", `record ${scratch}.json · artifacts ${scratch}/${evalSlug(id)}/${evalSlug(spec.cell.key)}/${spec.n} · debug ${rel(debug.dir)}`);
1669
+ setMeta(await writeRecord(a, stagingDir, id));
1670
+ await debug.finish(debug.verbosity === 2 || record.outcome !== "pass");
1671
+ }
1672
+ }
1673
+ //#endregion
1674
+ //#region src/plan-eval.ts
1675
+ /** Lets the attempt's own eval timer fire before the engine's backstop, so the message and cleanup are its own. */
1676
+ const GRACE_MS = 5e3;
1677
+ function configLevel(config) {
1678
+ return {
1679
+ harness: config.harness,
1680
+ sandbox: config.sandbox,
1681
+ environment: config.environment,
1682
+ compare: void 0,
1683
+ runs: config.runs,
1684
+ gate: config.gate,
1685
+ env: config.env,
1686
+ egress: config.egress !== void 0 ? { inherited: config.egress } : {},
1687
+ judge: config.judge,
1688
+ timeout: void 0,
1689
+ tags: [],
1690
+ pin: [],
1691
+ sources: {}
1692
+ };
1693
+ }
1694
+ /**
1695
+ * The tests an eval registers, or `null` when the run's tags leave it out (it isn't counted in
1696
+ * `order`). Throws a UsageError for options the plan refuses.
1697
+ */
1698
+ function planEval(declaration, ctx) {
1699
+ const { name, mode, groups: enclosing, options: own } = declaration;
1700
+ const groups = enclosing.map((g) => g.name);
1701
+ const where = `evaluate("${[...groups, name].join(" › ")}")`;
1702
+ checkOptions(own, where);
1703
+ const levels = [...enclosing.map((g) => ({
1704
+ options: g.options,
1705
+ where: `group("${g.name}")`
1706
+ })), {
1707
+ options: own,
1708
+ where
1709
+ }];
1710
+ const resolvedEval = cascade(configLevel(ctx.config), levels, where, {
1711
+ profile: ctx.profile,
1712
+ runs: ctx.runs
1713
+ });
1714
+ const { resolved } = resolvedEval;
1715
+ if (!matchesTags(resolved.tags, ctx.tags)) return null;
1716
+ const timeoutMs = resolved.timeout === void 0 ? ctx.config.timeoutMs : parseDuration(resolved.timeout);
1717
+ const plannedAttempts = resolvedEval.skip ? [] : expand(resolved);
1718
+ if (ctx.config.secrets.unbrokered === "error") {
1719
+ const [first] = unbrokered(resolved.env, plannedAttempts);
1720
+ if (first) throw new UsageError(unbrokeredError(first, where, ctx.config.file));
1721
+ }
1722
+ for (const planned of plannedAttempts) {
1723
+ const why = unenforceable(planned.egress, planned.sandbox, where);
1724
+ if (why) throw new UsageError(why);
1725
+ const mcp = refusedMcp(planned.harness, planned.environment.layers);
1726
+ if (mcp) throw new UsageError(`${where} on ${harnessId(planned.harness)}: ${mcp}`);
1727
+ }
1728
+ const path = ctx.file ? relativeFile(ctx.config.root, ctx.file) : null;
1729
+ const setBy = {
1730
+ sandbox: resolved.sources?.sandbox ?? ctx.config.file,
1731
+ harness: resolved.sources?.harness ?? ctx.config.file
1732
+ };
1733
+ const plan = evalPlan(resolvedEval, plannedAttempts, {
1734
+ configFile: ctx.config.file,
1735
+ file: path
1736
+ });
1737
+ if (resolvedEval.skip) {
1738
+ const [first] = expand(resolved);
1739
+ if (!first) return [];
1740
+ const attempt = {
1741
+ name,
1742
+ groups,
1743
+ tags: resolved.tags,
1744
+ cell: first.cell,
1745
+ n: 1,
1746
+ label: "",
1747
+ harness: first.harness,
1748
+ sandbox: first.sandbox,
1749
+ setBy,
1750
+ gate: "none",
1751
+ brokered: [],
1752
+ secretVars: [],
1753
+ egress: first.egress,
1754
+ plan
1755
+ };
1756
+ return [{
1757
+ name: `${name} [${attemptId(first.cell.key, 1)}]`,
1758
+ mode: "skip",
1759
+ attempt
1760
+ }];
1761
+ }
1762
+ const cells = [];
1763
+ return plannedAttempts.map((planned) => {
1764
+ if (!cells.includes(planned.cell.key)) cells.push(planned.cell.key);
1765
+ const workspace = workspaceOf(planned.environment.layers);
1766
+ const attempt = {
1767
+ name,
1768
+ groups,
1769
+ tags: resolved.tags,
1770
+ cell: planned.cell,
1771
+ n: planned.n,
1772
+ label: planned.label,
1773
+ harness: planned.harness,
1774
+ sandbox: planned.sandbox,
1775
+ setBy,
1776
+ gate: planned.gate,
1777
+ ...planned.minLift !== void 0 ? { minLift: planned.minLift } : {},
1778
+ brokered: brokeredIn(resolved.env, planned),
1779
+ secretVars: secretVarsIn(resolved.env, planned),
1780
+ egress: planned.egress,
1781
+ ...workspace ? { workspace } : {},
1782
+ plan
1783
+ };
1784
+ const rankFile = relativeFile(ctx.config.root, ctx.file ?? "");
1785
+ return {
1786
+ name: `${name} [${attemptId(planned.cell.key, planned.n)}]`,
1787
+ mode,
1788
+ attempt,
1789
+ timeout: timeoutMs + setupAllowance(planned) + GRACE_MS,
1790
+ gate: {
1791
+ limits: attemptLimits(planned, ctx.concurrency),
1792
+ rank: {
1793
+ eval: evalId(rankFile, [...groups, name]),
1794
+ file: rankFile,
1795
+ order: ctx.order,
1796
+ n: planned.n,
1797
+ cell: cells.indexOf(planned.cell.key)
1798
+ }
1799
+ },
1800
+ spec: {
1801
+ ...planned,
1802
+ name,
1803
+ groups,
1804
+ timeoutMs,
1805
+ env: resolved.env,
1806
+ judge: resolved.judge,
1807
+ sources: resolved.sources ?? {},
1808
+ notes: resolvedEval.notes
1809
+ }
1810
+ };
1811
+ });
1812
+ }
1813
+ /** The run-wide cap, plus the sandbox provider's and harness's own caps when they set one. */
1814
+ function attemptLimits(planned, concurrency) {
1815
+ const limits = { attempts: concurrency };
1816
+ const { sandbox, harness } = planned;
1817
+ if (sandbox.maxConcurrent) limits[`sandbox:${sandbox.kind}`] = sandbox.maxConcurrent;
1818
+ if (harness.maxConcurrent) limits[`harness:${harness.kind}`] = harness.maxConcurrent;
1819
+ return limits;
1820
+ }
1821
+ //#endregion
1822
+ //#region src/evaluate.ts
1823
+ let groupStack = [];
1824
+ /** Each eval file's declarations as `evaluate` and `group` were called, and each eval's body by id. */
1825
+ const trees = /* @__PURE__ */ new Map();
1826
+ const bodies = /* @__PURE__ */ new Map();
1827
+ /** Options as the tree holds them; from TypeScript they may still hold function forms. */
1828
+ const data = (options) => options;
1829
+ function declare(node) {
1830
+ const parent = groupStack.at(-1)?.node;
1831
+ if (parent) return void parent.children.push(node);
1832
+ const file = collectingFile() ?? "";
1833
+ const tree = trees.get(file) ?? { children: [] };
1834
+ trees.set(file, tree);
1835
+ tree.children.push(node);
1836
+ }
1837
+ /** `(name, fn)`, `(name, options, fn)` as in Vitest, or `(name, fn, options)`. */
1838
+ function splitArgs(what, name, a, b) {
1839
+ if (typeof a === "function" && typeof b === "function") throw new UsageError(`${what}("${name}") got two functions. Options must be a plain object; helpers such as secret are only available in defineConfig and defineEnvironment callbacks. Put the secret in an environment (environment: …) or in the config's env.`);
1840
+ if (typeof a === "function") return [b ?? {}, a];
1841
+ if (typeof b === "function") return [a ?? {}, b];
1842
+ throw new UsageError(what === "group" ? `group("${name}") has no function. Pass the group's body, e.g. group("${name}", () => { … }).` : `${what}("${name}") has no function. Pass the eval's body, e.g. ${what}("${name}", async (t) => { … }).`);
1843
+ }
1844
+ /** `evaluate.only` and `group.only` are refused in CI, where they'd silently skip the rest. */
1845
+ function checkOnly(mode, what) {
1846
+ if (mode !== "only" || !inCI()) return;
1847
+ const error = /* @__PURE__ */ new Error(`${what}.only isn't allowed in CI`);
1848
+ error.name = "OnlyInCiError";
1849
+ throw error;
1850
+ }
1851
+ const defineGroup = (mode) => (name, a, b) => {
1852
+ checkOnly(mode, "group");
1853
+ const [options, fn] = splitArgs("group", name, a, b);
1854
+ checkOptions(options, `group("${name}")`);
1855
+ const node = {
1856
+ type: "group",
1857
+ name,
1858
+ mode,
1859
+ options: data(options),
1860
+ children: []
1861
+ };
1862
+ declare(node);
1863
+ const stack = [...groupStack, {
1864
+ name,
1865
+ options,
1866
+ node
1867
+ }];
1868
+ registerGroup(name, mode, () => {
1869
+ const outer = groupStack;
1870
+ groupStack = stack;
1871
+ try {
1872
+ fn();
1873
+ } finally {
1874
+ groupStack = outer;
1875
+ }
1876
+ });
1877
+ };
1878
+ const group = Object.assign(defineGroup("run"), {
1879
+ skip: defineGroup("skip"),
1880
+ only: defineGroup("only"),
1881
+ todo: (name) => registerTodo(name)
1882
+ });
1883
+ /**
1884
+ * A placeholder, `evaluate.todo` or `group.todo`: one eval entry that never runs, counted as
1885
+ * skipped and shown as todo. The enclosing groups' tags filter it as they do an eval.
1886
+ */
1887
+ function registerTodo(name) {
1888
+ declare({
1889
+ type: "todo",
1890
+ name
1891
+ });
1892
+ const tags = [...new Set(groupStack.flatMap((g) => g.options.tags ?? []))];
1893
+ if (matchesTags(tags, getRunContext().tags)) registerTest(name, { mode: "todo" }, async () => {});
1894
+ }
1895
+ /** The evals declared so far, by file and name path, so a second one with the same id is refused. */
1896
+ const declared = /* @__PURE__ */ new Set();
1897
+ function checkUnique(where, groups, name, hint) {
1898
+ const key = `${collectingFile() ?? ""}\0${[...groups, name].join("\0")}`;
1899
+ if (declared.has(key)) throw new UsageError(`${where} is declared twice in this file. Each eval needs its own name, or its attempts are merged into one eval.${hint}`);
1900
+ declared.add(key);
1901
+ }
1902
+ function register(mode, name, own, fn, hint = "") {
1903
+ const groups = groupStack.map((g) => g.name);
1904
+ checkUnique(`evaluate("${[...groups, name].join(" › ")}")`, groups, name, hint);
1905
+ const body = `b${bodies.size + 1}`;
1906
+ bodies.set(body, fn);
1907
+ declare({
1908
+ type: "eval",
1909
+ name,
1910
+ mode,
1911
+ options: data(own),
1912
+ body
1913
+ });
1914
+ const tests = planEval({
1915
+ name,
1916
+ mode,
1917
+ groups: groupStack.map(({ name, options }) => ({
1918
+ name,
1919
+ options
1920
+ })),
1921
+ options: own
1922
+ }, {
1923
+ ...getRunContext(),
1924
+ file: collectingFile(),
1925
+ order: registered
1926
+ });
1927
+ if (!tests) return;
1928
+ registered++;
1929
+ for (const { name: testName, spec, ...options } of tests) registerTest(testName, options, spec ? (handle) => runAttempt(spec, fn, handle) : async () => {});
1930
+ }
1931
+ /** Evals registered so far in this worker, for the scheduler's tie-break. */
1932
+ let registered = 0;
1933
+ const defineEval = (mode) => (name, a, b) => {
1934
+ checkOnly(mode, "evaluate");
1935
+ const [own, fn] = splitArgs("evaluate", name, a, b);
1936
+ register(mode, name, own, fn);
1937
+ };
1938
+ /** `$key` is a property of the case, `%s` the case itself, `%#` its index. */
1939
+ function formatName(template, item, index) {
1940
+ return template.replace(/%#/g, String(index)).replace(/%s/g, typeof item === "string" ? item : JSON.stringify(item)).replace(/\$([A-Za-z_][A-Za-z0-9_]*)/g, (whole, key) => typeof item === "object" && item !== null && key in item ? String(item[key]) : whole);
1941
+ }
1942
+ /** One eval per case: `evaluate.for(cases)('adds $a and $b', async (t, c) => …)`. */
1943
+ const evalFor = (cases) => (name, a, b) => {
1944
+ const [own, fn] = splitArgs("evaluate.for", name, a, b);
1945
+ const hint = ` For evaluate.for, put $key, %s or %# in the name ("${name}") so each case's name differs.`;
1946
+ cases.forEach((item, i) => {
1947
+ register("run", formatName(name, item, i), own, (t) => fn(t, item), hint);
1948
+ });
1949
+ };
1950
+ const evaluate = Object.assign(defineEval("run"), {
1951
+ skip: defineEval("skip"),
1952
+ only: defineEval("only"),
1953
+ todo: registerTodo,
1954
+ for: evalFor
1955
+ });
1956
+ //#endregion
1957
+ export { DEFAULT_EGRESS_ALLOW, DEFAULT_SKILL_EXCLUDE, HarnessCapabilityError, defineConfig, defineEnvironment, evaluate, expect, group };
1958
+
1959
+ //# sourceMappingURL=index.js.map