@tangle-network/agent-eval 0.217.1 → 0.218.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 (172) hide show
  1. package/CHANGELOG.md +36 -1
  2. package/dist/adapters/http.d.ts +2 -2
  3. package/dist/analyst/index.d.ts +1 -1
  4. package/dist/analyst/index.d.ts.map +1 -1
  5. package/dist/analyst/index.js +9 -9
  6. package/dist/analyst/index.js.map +1 -1
  7. package/dist/{analyze-runs-DyeNcenW.d.ts → analyze-runs-DjlZOVXx.d.ts} +2 -2
  8. package/dist/{analyze-runs-DyeNcenW.d.ts.map → analyze-runs-DjlZOVXx.d.ts.map} +1 -1
  9. package/dist/{benchmark-command-xQK8nOI6.js → benchmark-command-BupdHRyf.js} +661 -562
  10. package/dist/benchmark-command-BupdHRyf.js.map +1 -0
  11. package/dist/benchmarks/index.d.ts +4 -4
  12. package/dist/benchmarks/index.js +3 -3
  13. package/dist/campaign/index.d.ts +4 -4
  14. package/dist/campaign/index.js +10 -10
  15. package/dist/{campaign-CJGkxAQ6.js → campaign-CGvUTnmc.js} +312 -281
  16. package/dist/{campaign-CJGkxAQ6.js.map → campaign-CGvUTnmc.js.map} +1 -1
  17. package/dist/{campaign-evidence-CQYw8_0H.js → campaign-evidence-DunzcJGv.js} +2 -2
  18. package/dist/{campaign-evidence-CQYw8_0H.js.map → campaign-evidence-DunzcJGv.js.map} +1 -1
  19. package/dist/{chat-client-ldqxYYTN.js → chat-client-B2pXlPdx.js} +2 -2
  20. package/dist/{chat-client-ldqxYYTN.js.map → chat-client-B2pXlPdx.js.map} +1 -1
  21. package/dist/{chat-json-call-DavzexSp.js → chat-json-call-Dm9c-b41.js} +2 -2
  22. package/dist/{chat-json-call-DavzexSp.js.map → chat-json-call-Dm9c-b41.js.map} +1 -1
  23. package/dist/cli.js +10 -10
  24. package/dist/{completion-verifier-B_jw6C9p.js → completion-verifier-DbLVf3i8.js} +4 -4
  25. package/dist/{completion-verifier-B_jw6C9p.js.map → completion-verifier-DbLVf3i8.js.map} +1 -1
  26. package/dist/contract/index.d.ts +7 -7
  27. package/dist/contract/index.js +90 -10
  28. package/dist/contract/index.js.map +1 -1
  29. package/dist/{default-registry-ClYjq3QE.js → default-registry-BJ-Pq6yL.js} +309 -277
  30. package/dist/{default-registry-ClYjq3QE.js.map → default-registry-BJ-Pq6yL.js.map} +1 -1
  31. package/dist/{define-agent-eval-BDxNlP3D.js → define-agent-eval-BGdFKlrc.js} +165 -105
  32. package/dist/define-agent-eval-BGdFKlrc.js.map +1 -0
  33. package/dist/{define-agent-eval-BIkH_gAR.d.ts → define-agent-eval-BlH10zEw.d.ts} +6 -6
  34. package/dist/{define-agent-eval-BIkH_gAR.d.ts.map → define-agent-eval-BlH10zEw.d.ts.map} +1 -1
  35. package/dist/diagnosis/index.d.ts +1 -1
  36. package/dist/{dspy-rlm-engine-DhdCr1Dc.js → dspy-rlm-engine-CSMwudLO.js} +2 -2
  37. package/dist/{dspy-rlm-engine-DhdCr1Dc.js.map → dspy-rlm-engine-CSMwudLO.js.map} +1 -1
  38. package/dist/eval-campaign-BUMIxKTE.js +393 -0
  39. package/dist/eval-campaign-BUMIxKTE.js.map +1 -0
  40. package/dist/{evaluation-jTjC_QQD.d.ts → evaluation-BOZ38vOH.d.ts} +2 -2
  41. package/dist/{evaluation-jTjC_QQD.d.ts.map → evaluation-BOZ38vOH.d.ts.map} +1 -1
  42. package/dist/evaluation.d.ts +1 -1
  43. package/dist/experiment/index.d.ts +1 -1
  44. package/dist/experiment/index.d.ts.map +1 -1
  45. package/dist/experiment/index.js +110 -71
  46. package/dist/experiment/index.js.map +1 -1
  47. package/dist/{exporters-Cl6IeNAu.js → exporters-DWvM16LE.js} +2 -2
  48. package/dist/{exporters-Cl6IeNAu.js.map → exporters-DWvM16LE.js.map} +1 -1
  49. package/dist/{external-optimizer-process-KVFGvYX1.js → external-optimizer-process-C3Wm7Vhn.js} +2 -2
  50. package/dist/{external-optimizer-process-KVFGvYX1.js.map → external-optimizer-process-C3Wm7Vhn.js.map} +1 -1
  51. package/dist/{external-optimizer-subprocess-XIHBOSKt.js → external-optimizer-subprocess-B5gdLRZv.js} +3 -3
  52. package/dist/{external-optimizer-subprocess-XIHBOSKt.js.map → external-optimizer-subprocess-B5gdLRZv.js.map} +1 -1
  53. package/dist/{findings-store-BMVQtEsm.js → findings-store-BtTzPL0d.js} +5 -5
  54. package/dist/{findings-store-BMVQtEsm.js.map → findings-store-BtTzPL0d.js.map} +1 -1
  55. package/dist/{hf-dataset-D9f7-oX7.js → hf-dataset-CKxIJQba.js} +3 -3
  56. package/dist/{hf-dataset-D9f7-oX7.js.map → hf-dataset-CKxIJQba.js.map} +1 -1
  57. package/dist/hosted/index.js +1 -1
  58. package/dist/{index-ZuWx52WG.d.ts → index-B16FNdW5.d.ts} +34 -17
  59. package/dist/index-B16FNdW5.d.ts.map +1 -0
  60. package/dist/{index-9jJ2fKSX.d.ts → index-CalAsXMR.d.ts} +7 -5
  61. package/dist/index-CalAsXMR.d.ts.map +1 -0
  62. package/dist/{index-BiT6RtNH.d.ts → index-DbMOXYnP.d.ts} +6 -6
  63. package/dist/{index-BiT6RtNH.d.ts.map → index-DbMOXYnP.d.ts.map} +1 -1
  64. package/dist/index.d.ts +9 -9
  65. package/dist/index.d.ts.map +1 -1
  66. package/dist/index.js +611 -688
  67. package/dist/index.js.map +1 -1
  68. package/dist/{integrity-DOd9Q86O.js → integrity-C8oUeB0g.js} +322 -197
  69. package/dist/integrity-C8oUeB0g.js.map +1 -0
  70. package/dist/{journal-CR4QikYp.js → journal-C7om8Gum.js} +48 -38
  71. package/dist/journal-C7om8Gum.js.map +1 -0
  72. package/dist/ledger-core/index.d.ts +1 -1
  73. package/dist/ledger-core/index.js +2 -2
  74. package/dist/ledger-core/lock-holder.d.ts +2 -0
  75. package/dist/ledger-core/lock-holder.js +181 -0
  76. package/dist/ledger-core/lock-holder.js.map +1 -0
  77. package/dist/{ledger-core-DrJhbms9.js → ledger-core-CAxUdDk0.js} +2 -2
  78. package/dist/{ledger-core-DrJhbms9.js.map → ledger-core-CAxUdDk0.js.map} +1 -1
  79. package/dist/{llm-client-BshCREn6.js → llm-client-_Y6ic-tf.js} +238 -248
  80. package/dist/llm-client-_Y6ic-tf.js.map +1 -0
  81. package/dist/{llm-judge-C8jMTNNG.js → llm-judge-Djxh-Uhs.js} +646 -458
  82. package/dist/llm-judge-Djxh-Uhs.js.map +1 -0
  83. package/dist/lock-holder-DaJbfpci.d.ts +86 -0
  84. package/dist/lock-holder-DaJbfpci.d.ts.map +1 -0
  85. package/dist/{matrix-DHe-dwVJ.d.ts → matrix-CCSBna4q.d.ts} +2 -2
  86. package/dist/{matrix-DHe-dwVJ.d.ts.map → matrix-CCSBna4q.d.ts.map} +1 -1
  87. package/dist/{mint-zszyYlpA.js → mint-CbdwjTPW.js} +2 -2
  88. package/dist/{mint-zszyYlpA.js.map → mint-CbdwjTPW.js.map} +1 -1
  89. package/dist/multishot/golden/index.d.ts +1 -1
  90. package/dist/multishot/index.d.ts +2 -2
  91. package/dist/openapi.json +1 -1
  92. package/dist/{pareto-BzsxidFD.js → pareto-CTv8vozZ.js} +4 -4
  93. package/dist/{pareto-BzsxidFD.js.map → pareto-CTv8vozZ.js.map} +1 -1
  94. package/dist/{promotion-gate-DaY95yU6.d.ts → promotion-gate-CxEHRsZW.d.ts} +2 -2
  95. package/dist/{promotion-gate-DaY95yU6.d.ts.map → promotion-gate-CxEHRsZW.d.ts.map} +1 -1
  96. package/dist/{proposal-findings-BbWidvUk.js → proposal-findings-n-5WBbEx.js} +2 -2
  97. package/dist/{proposal-findings-BbWidvUk.js.map → proposal-findings-n-5WBbEx.js.map} +1 -1
  98. package/dist/{render-C-w2EZ6p.js → render-Cs34y0iX.js} +404 -208
  99. package/dist/render-Cs34y0iX.js.map +1 -0
  100. package/dist/reporting.d.ts +2 -2
  101. package/dist/{researcher-0ABVIdGa.d.ts → researcher-GU1tV4dn.d.ts} +2 -2
  102. package/dist/{researcher-0ABVIdGa.d.ts.map → researcher-GU1tV4dn.d.ts.map} +1 -1
  103. package/dist/rl.d.ts +2 -2
  104. package/dist/rl.js +4 -4
  105. package/dist/rollout/index.js +5 -5
  106. package/dist/{rollout-D6C50mfA.js → rollout-Dd1ucT_M.js} +5 -5
  107. package/dist/{rollout-D6C50mfA.js.map → rollout-Dd1ucT_M.js.map} +1 -1
  108. package/dist/schema-C-ziyJD7.d.ts.map +1 -1
  109. package/dist/{schema-DxcKTzbi.js → schema-CvhfLjtZ.js} +77 -47
  110. package/dist/schema-CvhfLjtZ.js.map +1 -0
  111. package/dist/search/index.d.ts +2 -2
  112. package/dist/search/index.d.ts.map +1 -1
  113. package/dist/search/index.js +2 -2
  114. package/dist/{search-kernel-DxqjBxA-.d.ts → search-kernel-p9JB3SmL.d.ts} +3 -3
  115. package/dist/{search-kernel-DxqjBxA-.d.ts.map → search-kernel-p9JB3SmL.d.ts.map} +1 -1
  116. package/dist/{search-ledger-TUvB5gWH.js → search-ledger-ClpN4I3e.js} +4 -4
  117. package/dist/{search-ledger-TUvB5gWH.js.map → search-ledger-ClpN4I3e.js.map} +1 -1
  118. package/dist/{search-ledger-file-UKCO2r8a.js → search-ledger-file-Bd5Tioq6.js} +3 -3
  119. package/dist/{search-ledger-file-UKCO2r8a.js.map → search-ledger-file-Bd5Tioq6.js.map} +1 -1
  120. package/dist/{search-shipper-Cw2z3-td.js → search-shipper-Bg2HW3Uv.js} +3 -3
  121. package/dist/{search-shipper-Cw2z3-td.js.map → search-shipper-Bg2HW3Uv.js.map} +1 -1
  122. package/dist/{search-summary-DQIvJsSB.js → search-summary-CrBuvVnf.js} +3 -3
  123. package/dist/{search-summary-DQIvJsSB.js.map → search-summary-CrBuvVnf.js.map} +1 -1
  124. package/dist/{server-DoVbqqtJ.js → server-C6M0qR8h.js} +2 -2
  125. package/dist/{server-DoVbqqtJ.js.map → server-C6M0qR8h.js.map} +1 -1
  126. package/dist/{single-run-lock-U58yf8N6.js → single-run-lock-Dekd8_Wv.js} +7 -7
  127. package/dist/single-run-lock-Dekd8_Wv.js.map +1 -0
  128. package/dist/{skillopt-optimization-method-kyL94-ri.js → skillopt-optimization-method-5dlex7MJ.js} +1339 -1059
  129. package/dist/skillopt-optimization-method-5dlex7MJ.js.map +1 -0
  130. package/dist/{statistical-heldout-C9AbgREN.d.ts → statistical-heldout-BHfLZHrm.d.ts} +2 -2
  131. package/dist/{statistical-heldout-C9AbgREN.d.ts.map → statistical-heldout-BHfLZHrm.d.ts.map} +1 -1
  132. package/dist/{summary-report-DM7tlml1.d.ts → summary-report-CkbRcSuu.d.ts} +16 -1
  133. package/dist/summary-report-CkbRcSuu.d.ts.map +1 -0
  134. package/dist/supervisor-run/index.d.ts.map +1 -1
  135. package/dist/supervisor-run/index.js +3 -3
  136. package/dist/{systemone-Bp_Ac0p1.d.ts → systemone-BMzDJb1i.d.ts} +3 -3
  137. package/dist/{systemone-Bp_Ac0p1.d.ts.map → systemone-BMzDJb1i.d.ts.map} +1 -1
  138. package/dist/systemone.d.ts +2 -2
  139. package/dist/{terminal-record-78g6E4vh.js → terminal-record-D4Rv-j1W.js} +241 -193
  140. package/dist/{terminal-record-78g6E4vh.js.map → terminal-record-D4Rv-j1W.js.map} +1 -1
  141. package/dist/trajectory-replay/index.d.ts +4 -1
  142. package/dist/trajectory-replay/index.d.ts.map +1 -1
  143. package/dist/trajectory-replay/index.js +834 -824
  144. package/dist/trajectory-replay/index.js.map +1 -1
  145. package/dist/{tree-BGRglkKd.js → tree-B5e1uAeL.js} +122 -14
  146. package/dist/tree-B5e1uAeL.js.map +1 -0
  147. package/dist/{types-DtMbFw6U.d.ts → types-BKCzzmit.d.ts} +2 -2
  148. package/dist/{types-DtMbFw6U.d.ts.map → types-BKCzzmit.d.ts.map} +1 -1
  149. package/dist/{types-D8GhJoHk.d.ts → types-DtkO1G9L.d.ts} +2 -2
  150. package/dist/{types-D8GhJoHk.d.ts.map → types-DtkO1G9L.d.ts.map} +1 -1
  151. package/dist/wire/index.js +1 -1
  152. package/docs/design/statistics-decisions.md +1 -1
  153. package/docs/eval-surface-map.md +1 -0
  154. package/docs/profile-json-identity.md +1 -2
  155. package/docs/public-api.md +27 -16
  156. package/package.json +7 -2
  157. package/dist/benchmark-command-xQK8nOI6.js.map +0 -1
  158. package/dist/define-agent-eval-BDxNlP3D.js.map +0 -1
  159. package/dist/eval-campaign-BLEfBGbA.js +0 -343
  160. package/dist/eval-campaign-BLEfBGbA.js.map +0 -1
  161. package/dist/index-9jJ2fKSX.d.ts.map +0 -1
  162. package/dist/index-ZuWx52WG.d.ts.map +0 -1
  163. package/dist/integrity-DOd9Q86O.js.map +0 -1
  164. package/dist/journal-CR4QikYp.js.map +0 -1
  165. package/dist/llm-client-BshCREn6.js.map +0 -1
  166. package/dist/llm-judge-C8jMTNNG.js.map +0 -1
  167. package/dist/render-C-w2EZ6p.js.map +0 -1
  168. package/dist/schema-DxcKTzbi.js.map +0 -1
  169. package/dist/single-run-lock-U58yf8N6.js.map +0 -1
  170. package/dist/skillopt-optimization-method-kyL94-ri.js.map +0 -1
  171. package/dist/summary-report-DM7tlml1.d.ts.map +0 -1
  172. package/dist/tree-BGRglkKd.js.map +0 -1
@@ -104,229 +104,6 @@ function applyMutation(step, mutation) {
104
104
  return step;
105
105
  }
106
106
  //#endregion
107
- //#region src/trajectory-replay/corpus.ts
108
- /**
109
- * Corpus enumeration for batch replay verification.
110
- *
111
- * A labeled trajectory corpus is a gold label file (array of trajectory
112
- * entries with `incorrect_stages[].incorrect_step_ids`) plus a prepared
113
- * directory:
114
- * <prepared>/normalized/<traj_id>/steps.json normalized steps
115
- * <prepared>/normalized/<traj_id>/task.md optional task statement
116
- * <prepared>/extracted/<traj_id>/swe_raw/** raw mini-SWE trajectory
117
- *
118
- * Only trajectories that record their environment are replayable: the raw
119
- * trajectory must carry `info.docker_config.base_image`. Every excluded case
120
- * is returned with a machine-readable reason — the batch report surfaces the
121
- * full exclusion table, never a silently shrunk denominator.
122
- */
123
- function readLabelEntries(labelsPath) {
124
- const parsed = JSON.parse(readFileSync(labelsPath, "utf8"));
125
- if (!Array.isArray(parsed)) throw new Error(`${labelsPath} is not a JSON array of label entries`);
126
- for (const entry of parsed) if (typeof entry.traj_id !== "string") throw new Error(`${labelsPath} has an entry without a string traj_id`);
127
- return parsed;
128
- }
129
- function goldIncorrectSteps(entry) {
130
- const ids = /* @__PURE__ */ new Set();
131
- for (const stage of entry.incorrect_stages ?? []) for (const id of stage.incorrect_step_ids ?? []) ids.add(id);
132
- return [...ids].sort((a, b) => a - b);
133
- }
134
- function findRawTrajFiles(sweRawDir) {
135
- if (!existsSync(sweRawDir)) return [];
136
- return readdirSync(sweRawDir, {
137
- recursive: true,
138
- encoding: "utf8"
139
- }).filter((relative) => relative.endsWith(".traj.json")).map((relative) => join(sweRawDir, relative)).sort();
140
- }
141
- /** First non-empty output line of the trajectory's first bare `pwd` step. */
142
- function cwdFromPwdObservation(steps) {
143
- for (const step of steps) {
144
- if (step.action.trim() !== "pwd") continue;
145
- const line = parseObservationOutput(step.observation).split("\n").map((l) => l.trim()).find((l) => l.startsWith("/"));
146
- if (line) return line;
147
- }
148
- return null;
149
- }
150
- function taskStatementOf(preparedDir, trajId, raw) {
151
- const taskPath = join(preparedDir, "normalized", trajId, "task.md");
152
- if (existsSync(taskPath)) {
153
- const text = readFileSync(taskPath, "utf8").trim();
154
- if (text.length > 0) return text;
155
- }
156
- const userMessage = raw.messages?.find((m) => m.role === "user");
157
- if (typeof userMessage?.content === "string" && userMessage.content.trim().length > 0) return userMessage.content.trim();
158
- return null;
159
- }
160
- /**
161
- * Resolves the replay resources for one trajectory, independent of gold
162
- * labels — the finding wire uses this with a finding-supplied step instead.
163
- */
164
- function resolveCaseResources(corpus, trajId) {
165
- const sweRawDir = join(corpus.preparedDir, "extracted", trajId, "swe_raw");
166
- const rawFiles = findRawTrajFiles(sweRawDir);
167
- if (rawFiles.length === 0) return {
168
- resolved: false,
169
- reason: "no-swe-raw-trajectory",
170
- detail: sweRawDir
171
- };
172
- if (rawFiles.length > 1) return {
173
- resolved: false,
174
- reason: "ambiguous-swe-raw-trajectory",
175
- detail: rawFiles.join(", ")
176
- };
177
- let raw;
178
- try {
179
- raw = JSON.parse(readFileSync(rawFiles[0], "utf8"));
180
- } catch (err) {
181
- return {
182
- resolved: false,
183
- reason: "unreadable-raw-trajectory",
184
- detail: `${rawFiles[0]}: ${err instanceof Error ? err.message : String(err)}`
185
- };
186
- }
187
- const dockerConfig = raw.info?.docker_config;
188
- const image = dockerConfig?.base_image;
189
- if (typeof image !== "string" || image.length === 0) return {
190
- resolved: false,
191
- reason: "no-docker-image",
192
- detail: rawFiles[0]
193
- };
194
- const stepsPath = join(corpus.preparedDir, "normalized", trajId, "steps.json");
195
- if (!existsSync(stepsPath)) return {
196
- resolved: false,
197
- reason: "missing-steps-json",
198
- detail: stepsPath
199
- };
200
- const steps = JSON.parse(readFileSync(stepsPath, "utf8"));
201
- const runConfigCwd = raw.info?.config?.environment?.cwd;
202
- const dockerCwd = dockerConfig?.cwd;
203
- let cwd;
204
- let cwdSource;
205
- if (typeof runConfigCwd === "string" && runConfigCwd.length > 0) {
206
- cwd = runConfigCwd;
207
- cwdSource = "run-config";
208
- } else if (typeof dockerCwd === "string" && dockerCwd.length > 0) {
209
- cwd = dockerCwd;
210
- cwdSource = "docker-config";
211
- } else {
212
- const observed = cwdFromPwdObservation(steps);
213
- if (!observed) return {
214
- resolved: false,
215
- reason: "cwd-underivable"
216
- };
217
- cwd = observed;
218
- cwdSource = "pwd-observation";
219
- }
220
- const recordedTimeout = raw.info?.config?.environment?.timeout;
221
- return {
222
- resolved: true,
223
- resources: {
224
- corpus: corpus.name,
225
- trajId,
226
- stepsPath,
227
- steps,
228
- taskStatement: taskStatementOf(corpus.preparedDir, trajId, raw),
229
- image,
230
- cwd,
231
- cwdSource,
232
- recordedStepTimeoutMs: typeof recordedTimeout === "number" && recordedTimeout > 0 ? recordedTimeout * 1e3 : null
233
- }
234
- };
235
- }
236
- /**
237
- * Replayable = a raw trajectory with a recorded image AND at least one gold
238
- * incorrect step that is a real mid-trajectory action. Gold steps whose action
239
- * is the submit command are skipped when choosing k (counted per case); a case
240
- * whose golds are ALL submit steps is excluded as gold-only-submit-step.
241
- * Exclusion reasons are reported in resolution order:
242
- * raw trajectory → image → gold labels → steps.json → k range → cwd.
243
- */
244
- function enumerateReplayableCases(corpora) {
245
- const replayable = [];
246
- const excluded = [];
247
- let labelEntryCount = 0;
248
- for (const corpus of corpora) for (const entry of readLabelEntries(corpus.labelsPath)) {
249
- labelEntryCount += 1;
250
- const trajId = entry.traj_id;
251
- const resolution = resolveCaseResources(corpus, trajId);
252
- if (!resolution.resolved) {
253
- excluded.push({
254
- corpus: corpus.name,
255
- trajId,
256
- reason: resolution.reason,
257
- detail: resolution.detail
258
- });
259
- continue;
260
- }
261
- const gold = goldIncorrectSteps(entry);
262
- if (gold.length === 0) {
263
- excluded.push({
264
- corpus: corpus.name,
265
- trajId,
266
- reason: "no-gold-incorrect-step"
267
- });
268
- continue;
269
- }
270
- const resources = resolution.resources;
271
- let submitGoldsSkipped = 0;
272
- let target = null;
273
- let missingGoldId = null;
274
- for (const goldId of gold) {
275
- const step = resources.steps.find((s) => s.step_id === goldId);
276
- if (!step) {
277
- missingGoldId = goldId;
278
- break;
279
- }
280
- if (step.action.trim() === "submit" || isSubmitOnlyAction(step.action)) {
281
- submitGoldsSkipped += 1;
282
- continue;
283
- }
284
- target = step;
285
- break;
286
- }
287
- if (missingGoldId !== null) {
288
- excluded.push({
289
- corpus: corpus.name,
290
- trajId,
291
- reason: "gold-step-outside-steps",
292
- detail: `k=${missingGoldId}, steps=${resources.steps.length}`
293
- });
294
- continue;
295
- }
296
- if (!target) {
297
- excluded.push({
298
- corpus: corpus.name,
299
- trajId,
300
- reason: "gold-only-submit-step",
301
- detail: `${submitGoldsSkipped} gold step(s), all submit commands`
302
- });
303
- continue;
304
- }
305
- const recordedReturncodeAtK = parseRecordedReturncode(target.observation);
306
- if (recordedReturncodeAtK === null) {
307
- excluded.push({
308
- corpus: corpus.name,
309
- trajId,
310
- reason: "no-recorded-returncode-at-k",
311
- detail: `k=${target.step_id}; arm A cannot confirm execution without a recorded returncode`
312
- });
313
- continue;
314
- }
315
- replayable.push({
316
- ...resources,
317
- goldIncorrectSteps: gold,
318
- k: target.step_id,
319
- submitGoldsSkipped,
320
- recordedReturncodeAtK
321
- });
322
- }
323
- return {
324
- replayable,
325
- excluded,
326
- labelEntryCount
327
- };
328
- }
329
- //#endregion
330
107
  //#region src/trajectory-replay/fix.ts
331
108
  /**
332
109
  * Counterfactual patch synthesis: turn a recorded incorrect step into a
@@ -604,129 +381,9 @@ async function runFixLoop(caller, input, executor, options) {
604
381
  };
605
382
  }
606
383
  //#endregion
607
- //#region src/trajectory-replay/image-preparer.ts
384
+ //#region src/trajectory-replay/verify.ts
608
385
  /**
609
- * Replay-ready image derivation.
610
- *
611
- * A recorded trajectory names the image it ran in, but the image is not always
612
- * runnable as-is: sandbox platforms pin customer commands to a non-root
613
- * identity, so a root-owned working tree must be chowned before the replay can
614
- * write to it. `ImagePreparer` is that step, injectable so a consumer whose
615
- * images are already replay-ready supplies its own no-op or none at all.
616
- */
617
- const execFileAsync = promisify(execFile);
618
- function derivedImageTag(image, cwd) {
619
- return `ctb-replay:${createHash("sha256").update(`${image}\n${cwd}`).digest("hex").slice(0, 12)}-uid1000`;
620
- }
621
- /**
622
- * Pulls the base image when absent and builds `FROM <base>; RUN chown -R
623
- * 1000:1000 <cwd>` tagged by content hash, so repeated batches reuse both
624
- * the pull and the build. cwd `/` skips the chown (never chown -R /) and
625
- * replays on the base image directly.
626
- */
627
- function dockerImagePreparer(options = {}) {
628
- const pullTimeoutMs = options.pullTimeoutMs ?? 12e5;
629
- const buildTimeoutMs = options.buildTimeoutMs ?? 9e5;
630
- const imageExists = async (tag) => {
631
- try {
632
- await execFileAsync("docker", [
633
- "image",
634
- "inspect",
635
- tag
636
- ], { maxBuffer: 8388608 });
637
- return true;
638
- } catch {
639
- return false;
640
- }
641
- };
642
- const errorTail = (err) => {
643
- return (err && typeof err === "object" && "stderr" in err && typeof err.stderr === "string" && err.stderr.length > 0 ? err.stderr : err instanceof Error ? err.message : String(err)).trim().split("\n").slice(-3).join(" | ").slice(0, 400);
644
- };
645
- return { async ensure(image, cwd) {
646
- if (cwd === "/") {
647
- if (await imageExists(image)) return {
648
- succeeded: true,
649
- value: {
650
- derivedImage: image,
651
- pulled: false,
652
- built: false
653
- }
654
- };
655
- try {
656
- await execFileAsync("docker", ["pull", image], {
657
- timeout: pullTimeoutMs,
658
- maxBuffer: 33554432
659
- });
660
- } catch (err) {
661
- return {
662
- succeeded: false,
663
- error: `pull ${image}: ${errorTail(err)}`
664
- };
665
- }
666
- return {
667
- succeeded: true,
668
- value: {
669
- derivedImage: image,
670
- pulled: true,
671
- built: false
672
- }
673
- };
674
- }
675
- const derived = derivedImageTag(image, cwd);
676
- if (await imageExists(derived)) return {
677
- succeeded: true,
678
- value: {
679
- derivedImage: derived,
680
- pulled: false,
681
- built: false
682
- }
683
- };
684
- let pulled = false;
685
- if (!await imageExists(image)) try {
686
- await execFileAsync("docker", ["pull", image], {
687
- timeout: pullTimeoutMs,
688
- maxBuffer: 33554432
689
- });
690
- pulled = true;
691
- } catch (err) {
692
- return {
693
- succeeded: false,
694
- error: `pull ${image}: ${errorTail(err)}`
695
- };
696
- }
697
- const contextDir = mkdtempSync(join(tmpdir(), "ctb-replay-image-"));
698
- const quotedCwd = `'${cwd.replaceAll("'", `'\\''`)}'`;
699
- writeFileSync(join(contextDir, "Dockerfile"), `FROM ${image}\nRUN chown -R 1000:1000 ${quotedCwd}\n`);
700
- try {
701
- await execFileAsync("docker", [
702
- "build",
703
- "-t",
704
- derived,
705
- contextDir
706
- ], {
707
- timeout: buildTimeoutMs,
708
- maxBuffer: 33554432
709
- });
710
- } catch (err) {
711
- return {
712
- succeeded: false,
713
- error: `build ${derived} from ${image}: ${errorTail(err)}`
714
- };
715
- }
716
- return {
717
- succeeded: true,
718
- value: {
719
- derivedImage: derived,
720
- pulled,
721
- built: true
722
- }
723
- };
724
- } };
725
- }
726
- //#endregion
727
- //#region src/trajectory-replay/verify.ts
728
- /**
729
- * Execution-replay verification for recorded shell trajectories.
386
+ * Execution-replay verification for recorded shell trajectories.
730
387
  *
731
388
  * Turns a cited claim ("step k is the error-critical step") into an executed
732
389
  * proof: replay steps 1..k-1 inside the trajectory's own image, then
@@ -1116,33 +773,8 @@ function renderReport(verdict, target, armA, armB) {
1116
773
  return lines.join("\n");
1117
774
  }
1118
775
  //#endregion
1119
- //#region src/trajectory-replay/batch.ts
1120
- /**
1121
- * Batch replay verification across gold-labeled trajectory corpora.
1122
- *
1123
- * For every replayable case (a raw trajectory with a recorded image AND at
1124
- * least one gold incorrect step) the batch:
1125
- * 1. derives the replay-ready image through the injected `ImagePreparer`,
1126
- * 2. replays the prefix and runs arm A — the recorded gold step k, and
1127
- * 3. optionally generates a corrected command with one LLM call and runs
1128
- * arm B in its own fresh session.
1129
- *
1130
- * Headline metrics:
1131
- * replayability rate — fraction of replayable cases where the prefix
1132
- * replays within the divergence tolerance AND arm A reproduces a
1133
- * recorded failure at k; a zero exit without an error signature is not
1134
- * evidence of failure;
1135
- * prefix fidelity — executed prefix steps and the share of them that did
1136
- * not confirm the recording, split by kind. A corpus whose recordings
1137
- * carry no returncodes shows up here as unknown-expectation steps, never
1138
- * as a clean replay;
1139
- * fix-flip rate — fraction of arm-B-executed cases where the failure
1140
- * vanished (exit 0, signature absent).
1141
- *
1142
- * Image pulls and execs run strictly serially: pulls contend on disk and
1143
- * registry bandwidth, and serial cases keep wall-time attribution per case
1144
- * honest. Pull failures are report rows, never silent skips.
1145
- */
776
+ //#region src/trajectory-replay/batch-fix.ts
777
+ /** Arm B and the counterfactual fix phase of a replay batch. */
1146
778
  function seededSample(items, size, seed) {
1147
779
  const pool = [...items];
1148
780
  const random = mulberry32(seed);
@@ -1195,470 +827,223 @@ async function executeArmB(replayCase, fixCommand, backend, signature, stepTimeo
1195
827
  stderr: exec.stderr
1196
828
  };
1197
829
  }
1198
- function rate(numerator, denominator) {
1199
- return denominator > 0 ? numerator / denominator : null;
830
+ /** A fix outcome with nothing measured; callers name what they did measure. */
831
+ function fixResult(measured) {
832
+ return {
833
+ attempted: true,
834
+ sampledOut: false,
835
+ command: null,
836
+ llmError: null,
837
+ usage: null,
838
+ armBExit: null,
839
+ armBPrefixExecuted: null,
840
+ armBPrefixDivergences: null,
841
+ armBPrefixDivergencePct: null,
842
+ failureVanished: null,
843
+ armBError: null,
844
+ attempts: null,
845
+ flippedAtAttempt: null,
846
+ ...measured
847
+ };
1200
848
  }
1201
- async function runReplayBatch(options) {
1202
- const onProgress = options.onProgress ?? (() => {});
1203
- const enumeration = enumerateReplayableCases(options.corpora);
1204
- let selected = enumeration.replayable;
1205
- if (options.caseFilter) selected = selected.filter((c) => c.trajId.includes(options.caseFilter));
1206
- if (options.caseLimit !== void 0) selected = selected.slice(0, options.caseLimit);
1207
- onProgress(`enumerated ${enumeration.labelEntryCount} label entries → ${enumeration.replayable.length} replayable, ${enumeration.excluded.length} excluded; executing ${selected.length}`);
1208
- const preparer = options.preparer ?? dockerImagePreparer();
1209
- const backendFactory = options.backendFactory;
1210
- mkdirSync(options.out, { recursive: true });
1211
- const progressPath = join(options.out, "cases.jsonl");
1212
- const rows = [];
1213
- const pullFailures = [];
1214
- const verdictByTraj = /* @__PURE__ */ new Map();
1215
- for (const [index, replayCase] of selected.entries()) {
1216
- const caseStart = Date.now();
1217
- const label = `[${index + 1}/${selected.length}] ${replayCase.corpus}/${replayCase.trajId}`;
1218
- onProgress(`${label}: preparing image ${replayCase.image}`);
1219
- const preparation = await preparer.ensure(replayCase.image, replayCase.cwd);
1220
- const base = {
1221
- corpus: replayCase.corpus,
1222
- trajId: replayCase.trajId,
1223
- image: replayCase.image,
1224
- cwd: replayCase.cwd,
1225
- cwdSource: replayCase.cwdSource,
1226
- k: replayCase.k,
1227
- stepCount: replayCase.steps.length,
1228
- goldIncorrectSteps: replayCase.goldIncorrectSteps,
1229
- submitGoldsSkipped: replayCase.submitGoldsSkipped,
1230
- recordedReturncodeAtK: replayCase.recordedReturncodeAtK
849
+ function runArmB(run, fixCase, command, label) {
850
+ const { options } = run;
851
+ return executeArmB(fixCase.replayCase, command, options.backendFactory(fixCase.row.derivedImage), fixCase.signature, fixCase.stepTimeoutMs, options.prefixLimit, (message) => run.onProgress(`${label}: ${message}`));
852
+ }
853
+ /** The fix summary a finished loop reports: the flipping attempt, else the last executed one, else why it stopped. */
854
+ function loopFixResult(result, usage) {
855
+ const summary = (result.flippedAtAttempt !== null ? result.attempts.find((a) => a.attempt === result.flippedAtAttempt) : null) ?? [...result.attempts].reverse().find((a) => a.executed) ?? null;
856
+ const lastRecord = result.attempts.at(-1) ?? null;
857
+ if (summary) return fixResult({
858
+ command: summary.command,
859
+ usage,
860
+ armBExit: summary.exitCode,
861
+ armBPrefixExecuted: summary.prefixExecuted,
862
+ armBPrefixDivergences: summary.prefixDivergences,
863
+ armBPrefixDivergencePct: summary.prefixDivergencePct,
864
+ failureVanished: summary.failureVanished,
865
+ attempts: result.attempts,
866
+ flippedAtAttempt: result.flippedAtAttempt
867
+ });
868
+ if (result.aborted) return fixResult({
869
+ command: lastRecord?.command ?? null,
870
+ usage,
871
+ armBError: lastRecord?.armBError ?? "sandbox error",
872
+ attempts: result.attempts
873
+ });
874
+ return fixResult({
875
+ llmError: lastRecord?.llmError ?? "no attempt produced a runnable fix",
876
+ usage,
877
+ attempts: result.attempts
878
+ });
879
+ }
880
+ /** Iterative fix: failed arms feed their real output into up to `fixAttempts` prompts. */
881
+ async function fixByLoop(run, fixCase, caller, tally) {
882
+ const { options, onProgress } = run;
883
+ const { row, label, caseOut } = fixCase;
884
+ const fixAttempts = options.fixAttempts ?? 3;
885
+ onProgress(`${label}: fix loop (budget ${fixAttempts} attempts)`);
886
+ const result = await runFixLoop(caller, fixCase.promptInput, async (command, attempt) => {
887
+ onProgress(`${label}: attempt ${attempt} arm B — ${command.split("\n")[0].slice(0, 120)}`);
888
+ const armB = await runArmB(run, fixCase, command, `${label}: attempt ${attempt}`);
889
+ mkdirSync(caseOut, { recursive: true });
890
+ writeFileSync(join(caseOut, `armB-attempt${attempt}-result.json`), `${JSON.stringify({
891
+ command,
892
+ ...armB
893
+ }, null, 2)}\n`);
894
+ writeFileSync(join(caseOut, "armB-result.json"), `${JSON.stringify({
895
+ command,
896
+ attempt,
897
+ ...armB
898
+ }, null, 2)}\n`);
899
+ return armB;
900
+ }, {
901
+ maxAttempts: fixAttempts,
902
+ onProgress: (message) => onProgress(`${label}: ${message}`)
903
+ });
904
+ tally.calls += result.llmCalls;
905
+ tally.failures += result.llmFailures;
906
+ tally.promptTokens += result.promptTokens;
907
+ tally.completionTokens += result.completionTokens;
908
+ tally.callsWithoutUsage += result.callsWithoutUsage;
909
+ const usage = result.promptTokens + result.completionTokens > 0 ? {
910
+ promptTokens: result.promptTokens,
911
+ completionTokens: result.completionTokens
912
+ } : null;
913
+ onProgress(`${label}: loop done — flipped=${result.flipped}` + (result.flippedAtAttempt !== null ? ` at attempt ${result.flippedAtAttempt}` : "") + ` (${result.llmCalls} calls, ${result.attempts.filter((a) => a.executed).length} arms)`);
914
+ return {
915
+ ...row,
916
+ fix: loopFixResult(result, usage)
917
+ };
918
+ }
919
+ /** One-shot fix: a single generated command, one arm B. */
920
+ async function fixByOneShot(run, fixCase, caller, tally) {
921
+ const { onProgress } = run;
922
+ const { row, label, caseOut } = fixCase;
923
+ onProgress(`${label}: generating corrected command`);
924
+ tally.calls += 1;
925
+ const generated = await generateFixCommand(caller, fixCase.promptInput);
926
+ if (!generated.succeeded) {
927
+ tally.failures += 1;
928
+ onProgress(`${label}: LLM failed — ${generated.error.slice(0, 200)}`);
929
+ return {
930
+ ...row,
931
+ fix: fixResult({ llmError: generated.error })
1231
932
  };
1232
- if (!preparation.succeeded) {
1233
- pullFailures.push({
1234
- corpus: replayCase.corpus,
1235
- trajId: replayCase.trajId,
1236
- image: replayCase.image,
1237
- error: preparation.error
1238
- });
1239
- const row = {
1240
- ...base,
1241
- derivedImage: null,
1242
- signature: null,
1243
- status: "image-unavailable",
1244
- error: preparation.error,
1245
- imagePulled: false,
1246
- imageBuilt: false,
1247
- prefixExecuted: null,
1248
- prefixDivergences: null,
1249
- prefixDivergencePct: null,
1250
- prefixConfirmed: null,
1251
- prefixReturncodeMismatches: null,
1252
- prefixUnknownExpectations: null,
1253
- armAExit: null,
1254
- armAReturncodeMatch: false,
1255
- armASignatureMatch: false,
1256
- replayed: false,
1257
- fix: null,
1258
- wallMs: Date.now() - caseStart
933
+ }
934
+ if (generated.value.usage === null || generated.value.usage === void 0) tally.callsWithoutUsage += 1;
935
+ tally.promptTokens += generated.value.usage?.promptTokens ?? 0;
936
+ tally.completionTokens += generated.value.usage?.completionTokens ?? 0;
937
+ onProgress(`${label}: arm B — ${generated.value.command.split("\n")[0].slice(0, 120)}`);
938
+ try {
939
+ const armB = await runArmB(run, fixCase, generated.value.command, label);
940
+ const fix = fixResult({
941
+ command: generated.value.command,
942
+ usage: generated.value.usage,
943
+ armBExit: armB.exitCode,
944
+ armBPrefixExecuted: armB.prefixExecuted,
945
+ armBPrefixDivergences: armB.prefixDivergences,
946
+ armBPrefixDivergencePct: armB.prefixDivergencePct,
947
+ failureVanished: armB.failureVanished,
948
+ flippedAtAttempt: armB.failureVanished ? 1 : null
949
+ });
950
+ mkdirSync(caseOut, { recursive: true });
951
+ writeFileSync(join(caseOut, "armB-result.json"), `${JSON.stringify({
952
+ command: generated.value.command,
953
+ ...armB
954
+ }, null, 2)}\n`);
955
+ onProgress(`${label}: armB exit=${armB.exitCode} failureVanished=${armB.failureVanished}`);
956
+ return {
957
+ ...row,
958
+ fix
959
+ };
960
+ } catch (err) {
961
+ const message = err instanceof Error ? err.message : String(err);
962
+ onProgress(`${label}: arm B error — ${message.slice(0, 200)}`);
963
+ return {
964
+ ...row,
965
+ fix: fixResult({
966
+ command: generated.value.command,
967
+ usage: generated.value.usage,
968
+ armBError: message.slice(0, 500)
969
+ })
970
+ };
971
+ }
972
+ }
973
+ /** Phase 2: counterfactual fixes for the cases where arm A reproduced, capped and seeded-sampled. */
974
+ async function runFixPhase(run, selected, rows, verdicts) {
975
+ const { options, onProgress } = run;
976
+ const caller = options.fixCaller;
977
+ if (!caller) throw new Error(`trajectory-replay: fix=${options.fix} requires a fixCaller`);
978
+ const maxFixCases = options.maxFixCases ?? 30;
979
+ const seed = options.seed ?? 17;
980
+ const eligible = rows.filter((r) => r.status === "ok" && r.replayed);
981
+ const sampled = eligible.length > maxFixCases ? seededSample(eligible, maxFixCases, seed) : new Set(eligible);
982
+ if (eligible.length > maxFixCases) onProgress(`fix phase: ${eligible.length} eligible > cap ${maxFixCases}; seeded sample (seed=${seed})`);
983
+ const tally = {
984
+ calls: 0,
985
+ failures: 0,
986
+ promptTokens: 0,
987
+ completionTokens: 0,
988
+ callsWithoutUsage: 0
989
+ };
990
+ for (const row of eligible) {
991
+ const index = rows.indexOf(row);
992
+ if (!sampled.has(row)) {
993
+ rows[index] = {
994
+ ...row,
995
+ fix: fixResult({
996
+ attempted: false,
997
+ sampledOut: true
998
+ })
1259
999
  };
1260
- rows.push(row);
1261
- appendFileSync(progressPath, `${JSON.stringify(row)}\n`);
1262
- onProgress(`${label}: image unavailable — ${preparation.error}`);
1263
1000
  continue;
1264
1001
  }
1265
- const { derivedImage, pulled, built } = preparation.value;
1266
- const stepTimeoutMs = options.stepTimeoutMs ?? replayCase.recordedStepTimeoutMs ?? 12e4;
1267
- const caseOut = join(options.out, caseOutDirName(replayCase));
1268
- onProgress(`${label}: arm A on ${derivedImage} (k=${replayCase.k}, timeout ${stepTimeoutMs}ms)`);
1269
- try {
1270
- const verdict = await replayVerify({
1271
- stepsPath: replayCase.stepsPath,
1272
- image: derivedImage,
1273
- at: replayCase.k,
1274
- cwd: replayCase.cwd,
1275
- out: caseOut,
1276
- caseId: replayCase.trajId,
1277
- stepTimeoutMs,
1278
- prefixLimit: options.prefixLimit,
1279
- backend: backendFactory(derivedImage),
1280
- onProgress: (message) => onProgress(`${label}: ${message}`)
1281
- });
1282
- verdictByTraj.set(replayCase.trajId, verdict);
1283
- const returncodeMatch = verdict.recordedReturncode !== null && verdict.armA.exitCode === verdict.recordedReturncode;
1284
- const row = {
1285
- ...base,
1286
- derivedImage,
1287
- signature: verdict.signature,
1288
- status: "ok",
1289
- error: null,
1290
- imagePulled: pulled,
1291
- imageBuilt: built,
1292
- prefixExecuted: verdict.prefixExecuted,
1293
- prefixDivergences: verdict.prefixDivergences.length,
1294
- prefixDivergencePct: verdict.prefixDivergencePct,
1295
- prefixConfirmed: verdict.prefixConfirmed,
1296
- prefixReturncodeMismatches: verdict.prefixReturncodeMismatches,
1297
- prefixUnknownExpectations: verdict.prefixUnknownExpectations,
1298
- armAExit: verdict.armA.exitCode,
1299
- armAReturncodeMatch: returncodeMatch,
1300
- armASignatureMatch: verdict.armA.failureSignatureMatch,
1301
- replayed: verdict.valid,
1302
- fix: null,
1303
- wallMs: Date.now() - caseStart
1304
- };
1305
- rows.push(row);
1306
- appendFileSync(progressPath, `${JSON.stringify(row)}\n`);
1307
- onProgress(`${label}: armA exit=${verdict.armA.exitCode} rcMatch=${returncodeMatch} divergences=${verdict.prefixDivergences.length}/${verdict.prefixExecuted} (${verdict.prefixDivergencePct}%, ${verdict.prefixUnknownExpectations} unknown-expectation)`);
1308
- } catch (err) {
1309
- const message = err instanceof Error ? err.message : String(err);
1310
- const row = {
1311
- ...base,
1312
- derivedImage,
1313
- signature: null,
1314
- status: "replay-error",
1315
- error: message.slice(0, 500),
1316
- imagePulled: pulled,
1317
- imageBuilt: built,
1318
- prefixExecuted: null,
1319
- prefixDivergences: null,
1320
- prefixDivergencePct: null,
1321
- prefixConfirmed: null,
1322
- prefixReturncodeMismatches: null,
1323
- prefixUnknownExpectations: null,
1324
- armAExit: null,
1325
- armAReturncodeMatch: false,
1326
- armASignatureMatch: false,
1327
- replayed: false,
1328
- fix: null,
1329
- wallMs: Date.now() - caseStart
1330
- };
1331
- rows.push(row);
1332
- appendFileSync(progressPath, `${JSON.stringify(row)}\n`);
1333
- onProgress(`${label}: replay error — ${message.slice(0, 200)}`);
1334
- }
1335
- }
1336
- let llm = null;
1337
- if (options.fix !== "none") {
1338
- const caller = options.fixCaller;
1339
- if (!caller) throw new Error(`trajectory-replay: fix=${options.fix} requires a fixCaller`);
1340
- const fixAttempts = options.fixAttempts ?? 3;
1341
- const maxFixCases = options.maxFixCases ?? 30;
1342
- const seed = options.seed ?? 17;
1343
- const eligible = rows.filter((r) => r.status === "ok" && r.replayed);
1344
- const sampled = eligible.length > maxFixCases ? seededSample(eligible, maxFixCases, seed) : new Set(eligible);
1345
- if (eligible.length > maxFixCases) onProgress(`fix phase: ${eligible.length} eligible > cap ${maxFixCases}; seeded sample (seed=${seed})`);
1346
- let calls = 0;
1347
- let failures = 0;
1348
- let promptTokens = 0;
1349
- let completionTokens = 0;
1350
- let callsWithoutUsage = 0;
1351
- for (const row of eligible) {
1352
- const index = rows.indexOf(row);
1353
- if (!sampled.has(row)) {
1354
- rows[index] = {
1355
- ...row,
1356
- fix: {
1357
- attempted: false,
1358
- sampledOut: true,
1359
- command: null,
1360
- llmError: null,
1361
- usage: null,
1362
- armBExit: null,
1363
- armBPrefixExecuted: null,
1364
- armBPrefixDivergences: null,
1365
- armBPrefixDivergencePct: null,
1366
- failureVanished: null,
1367
- armBError: null,
1368
- attempts: null,
1369
- flippedAtAttempt: null
1370
- }
1371
- };
1372
- continue;
1373
- }
1374
- const replayCase = selected.find((c) => c.trajId === row.trajId && c.corpus === row.corpus);
1375
- const label = `fix ${row.corpus}/${row.trajId}`;
1376
- const signature = verdictByTraj.get(row.trajId)?.signature ?? null;
1377
- const stepTimeoutMs = options.stepTimeoutMs ?? replayCase.recordedStepTimeoutMs ?? 12e4;
1378
- const promptInput = {
1002
+ const replayCase = selected.find((c) => c.trajId === row.trajId && c.corpus === row.corpus);
1003
+ const fixCase = {
1004
+ row,
1005
+ replayCase,
1006
+ label: `fix ${row.corpus}/${row.trajId}`,
1007
+ signature: verdicts.get(row.trajId)?.signature ?? null,
1008
+ stepTimeoutMs: options.stepTimeoutMs ?? replayCase.recordedStepTimeoutMs ?? 12e4,
1009
+ caseOut: join(options.out, caseOutDirName(row)),
1010
+ promptInput: {
1379
1011
  taskStatement: replayCase.taskStatement,
1380
1012
  steps: replayCase.steps,
1381
1013
  k: replayCase.k
1382
- };
1383
- const caseOut = join(options.out, caseOutDirName(row));
1384
- if (options.fix === "loop") {
1385
- onProgress(`${label}: fix loop (budget ${fixAttempts} attempts)`);
1386
- const result = await runFixLoop(caller, promptInput, async (command, attempt) => {
1387
- onProgress(`${label}: attempt ${attempt} arm B — ${command.split("\n")[0].slice(0, 120)}`);
1388
- const armB = await executeArmB(replayCase, command, backendFactory(row.derivedImage), signature, stepTimeoutMs, options.prefixLimit, (message) => onProgress(`${label}: attempt ${attempt}: ${message}`));
1389
- mkdirSync(caseOut, { recursive: true });
1390
- writeFileSync(join(caseOut, `armB-attempt${attempt}-result.json`), `${JSON.stringify({
1391
- command,
1392
- ...armB
1393
- }, null, 2)}\n`);
1394
- writeFileSync(join(caseOut, "armB-result.json"), `${JSON.stringify({
1395
- command,
1396
- attempt,
1397
- ...armB
1398
- }, null, 2)}\n`);
1399
- return armB;
1400
- }, {
1401
- maxAttempts: fixAttempts,
1402
- onProgress: (message) => onProgress(`${label}: ${message}`)
1403
- });
1404
- calls += result.llmCalls;
1405
- failures += result.llmFailures;
1406
- promptTokens += result.promptTokens;
1407
- completionTokens += result.completionTokens;
1408
- callsWithoutUsage += result.callsWithoutUsage;
1409
- const usage = result.promptTokens + result.completionTokens > 0 ? {
1410
- promptTokens: result.promptTokens,
1411
- completionTokens: result.completionTokens
1412
- } : null;
1413
- const summary = (result.flippedAtAttempt !== null ? result.attempts.find((a) => a.attempt === result.flippedAtAttempt) : null) ?? [...result.attempts].reverse().find((a) => a.executed) ?? null;
1414
- const lastRecord = result.attempts.at(-1) ?? null;
1415
- rows[index] = {
1416
- ...row,
1417
- fix: summary ? {
1418
- attempted: true,
1419
- sampledOut: false,
1420
- command: summary.command,
1421
- llmError: null,
1422
- usage,
1423
- armBExit: summary.exitCode,
1424
- armBPrefixExecuted: summary.prefixExecuted,
1425
- armBPrefixDivergences: summary.prefixDivergences,
1426
- armBPrefixDivergencePct: summary.prefixDivergencePct,
1427
- failureVanished: summary.failureVanished,
1428
- armBError: null,
1429
- attempts: result.attempts,
1430
- flippedAtAttempt: result.flippedAtAttempt
1431
- } : result.aborted ? {
1432
- attempted: true,
1433
- sampledOut: false,
1434
- command: lastRecord?.command ?? null,
1435
- llmError: null,
1436
- usage,
1437
- armBExit: null,
1438
- armBPrefixExecuted: null,
1439
- armBPrefixDivergences: null,
1440
- armBPrefixDivergencePct: null,
1441
- failureVanished: null,
1442
- armBError: lastRecord?.armBError ?? "sandbox error",
1443
- attempts: result.attempts,
1444
- flippedAtAttempt: null
1445
- } : {
1446
- attempted: true,
1447
- sampledOut: false,
1448
- command: null,
1449
- llmError: lastRecord?.llmError ?? "no attempt produced a runnable fix",
1450
- usage,
1451
- armBExit: null,
1452
- armBPrefixExecuted: null,
1453
- armBPrefixDivergences: null,
1454
- armBPrefixDivergencePct: null,
1455
- failureVanished: null,
1456
- armBError: null,
1457
- attempts: result.attempts,
1458
- flippedAtAttempt: null
1459
- }
1460
- };
1461
- onProgress(`${label}: loop done — flipped=${result.flipped}` + (result.flippedAtAttempt !== null ? ` at attempt ${result.flippedAtAttempt}` : "") + ` (${result.llmCalls} calls, ${result.attempts.filter((a) => a.executed).length} arms)`);
1462
- continue;
1463
- }
1464
- onProgress(`${label}: generating corrected command`);
1465
- calls += 1;
1466
- const generated = await generateFixCommand(caller, promptInput);
1467
- if (!generated.succeeded) {
1468
- failures += 1;
1469
- rows[index] = {
1470
- ...row,
1471
- fix: {
1472
- attempted: true,
1473
- sampledOut: false,
1474
- command: null,
1475
- llmError: generated.error,
1476
- usage: null,
1477
- armBExit: null,
1478
- armBPrefixExecuted: null,
1479
- armBPrefixDivergences: null,
1480
- armBPrefixDivergencePct: null,
1481
- failureVanished: null,
1482
- armBError: null,
1483
- attempts: null,
1484
- flippedAtAttempt: null
1485
- }
1486
- };
1487
- onProgress(`${label}: LLM failed — ${generated.error.slice(0, 200)}`);
1488
- continue;
1489
- }
1490
- if (generated.value.usage === null || generated.value.usage === void 0) callsWithoutUsage += 1;
1491
- promptTokens += generated.value.usage?.promptTokens ?? 0;
1492
- completionTokens += generated.value.usage?.completionTokens ?? 0;
1493
- onProgress(`${label}: arm B — ${generated.value.command.split("\n")[0].slice(0, 120)}`);
1494
- try {
1495
- const armB = await executeArmB(replayCase, generated.value.command, backendFactory(row.derivedImage), signature, stepTimeoutMs, options.prefixLimit, (message) => onProgress(`${label}: ${message}`));
1496
- rows[index] = {
1497
- ...row,
1498
- fix: {
1499
- attempted: true,
1500
- sampledOut: false,
1501
- command: generated.value.command,
1502
- llmError: null,
1503
- usage: generated.value.usage,
1504
- armBExit: armB.exitCode,
1505
- armBPrefixExecuted: armB.prefixExecuted,
1506
- armBPrefixDivergences: armB.prefixDivergences,
1507
- armBPrefixDivergencePct: armB.prefixDivergencePct,
1508
- failureVanished: armB.failureVanished,
1509
- armBError: null,
1510
- attempts: null,
1511
- flippedAtAttempt: armB.failureVanished ? 1 : null
1512
- }
1513
- };
1514
- mkdirSync(caseOut, { recursive: true });
1515
- writeFileSync(join(caseOut, "armB-result.json"), `${JSON.stringify({
1516
- command: generated.value.command,
1517
- ...armB
1518
- }, null, 2)}\n`);
1519
- onProgress(`${label}: armB exit=${armB.exitCode} failureVanished=${armB.failureVanished}`);
1520
- } catch (err) {
1521
- const message = err instanceof Error ? err.message : String(err);
1522
- rows[index] = {
1523
- ...row,
1524
- fix: {
1525
- attempted: true,
1526
- sampledOut: false,
1527
- command: generated.value.command,
1528
- llmError: null,
1529
- usage: generated.value.usage,
1530
- armBExit: null,
1531
- armBPrefixExecuted: null,
1532
- armBPrefixDivergences: null,
1533
- armBPrefixDivergencePct: null,
1534
- failureVanished: null,
1535
- armBError: message.slice(0, 500),
1536
- attempts: null,
1537
- flippedAtAttempt: null
1538
- }
1539
- };
1540
- onProgress(`${label}: arm B error — ${message.slice(0, 200)}`);
1541
1014
  }
1542
- }
1543
- llm = {
1544
- model: options.fixModelLabel ?? "unknown",
1545
- calls,
1546
- failures,
1547
- promptTokens,
1548
- completionTokens,
1549
- callsWithoutUsage
1550
1015
  };
1016
+ rows[index] = options.fix === "loop" ? await fixByLoop(run, fixCase, caller, tally) : await fixByOneShot(run, fixCase, caller, tally);
1551
1017
  }
1552
- const executedRows = rows.filter((r) => r.status === "ok");
1553
- const replayed = executedRows.filter((r) => r.replayed);
1554
- const sumOver = (pick) => executedRows.reduce((total, row) => total + (pick(row) ?? 0), 0);
1555
- const executedSteps = sumOver((r) => r.prefixExecuted);
1556
- const divergentSteps = sumOver((r) => r.prefixDivergences);
1557
- const prefixFidelity = {
1558
- executedSteps,
1559
- divergentSteps,
1560
- returncodeMismatches: sumOver((r) => r.prefixReturncodeMismatches),
1561
- unknownExpectations: sumOver((r) => r.prefixUnknownExpectations),
1562
- divergencePct: executedSteps > 0 ? Number((divergentSteps / executedSteps * 100).toFixed(1)) : null,
1563
- tolerancePct: 10,
1564
- casesWithinTolerance: executedRows.filter((r) => r.prefixDivergencePct !== null && r.prefixDivergencePct <= 10).length,
1565
- casesExecuted: executedRows.length
1018
+ return {
1019
+ model: options.fixModelLabel ?? "unknown",
1020
+ ...tally
1566
1021
  };
1567
- const signatureStrict = executedRows.filter((r) => r.replayed && r.armASignatureMatch);
1568
- const armBExecuted = rows.filter((r) => r.fix?.attempted && r.fix.failureVanished !== null);
1569
- const flipped = armBExecuted.filter((r) => r.fix.failureVanished === true);
1570
- const armBNonzeroRc = armBExecuted.filter((r) => r.recordedReturncodeAtK !== null && r.recordedReturncodeAtK !== 0);
1571
- const flippedNonzeroRc = armBNonzeroRc.filter((r) => r.fix.failureVanished === true);
1572
- const attempt1Executed = rows.filter((r) => r.fix?.attempts?.find((a) => a.attempt === 1)?.executed === true);
1573
- const flippedAt1 = rows.filter((r) => r.fix?.flippedAtAttempt === 1);
1574
- const flipsByAttempt = {};
1575
- for (const row of rows) {
1576
- const at = row.fix?.flippedAtAttempt;
1577
- if (typeof at === "number") flipsByAttempt[String(at)] = (flipsByAttempt[String(at)] ?? 0) + 1;
1578
- }
1579
- const excludedByReason = {};
1580
- for (const excluded of enumeration.excluded) excludedByReason[excluded.reason] = (excludedByReason[excluded.reason] ?? 0) + 1;
1581
- const submitGoldsByCorpus = {};
1582
- const submitEntry = (corpus) => submitGoldsByCorpus[corpus] ??= {
1583
- submitOnlyCases: 0,
1584
- goldsSkippedWithinReplayable: 0
1585
- };
1586
- for (const excluded of enumeration.excluded) if (excluded.reason === "gold-only-submit-step") submitEntry(excluded.corpus).submitOnlyCases += 1;
1587
- for (const replayCase of enumeration.replayable) if (replayCase.submitGoldsSkipped > 0) submitEntry(replayCase.corpus).goldsSkippedWithinReplayable += replayCase.submitGoldsSkipped;
1588
- const report = {
1589
- generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
1590
- corpora: options.corpora.map((c) => ({
1591
- name: c.name,
1592
- labelsPath: c.labelsPath,
1593
- preparedDir: c.preparedDir
1594
- })),
1595
- totals: {
1596
- labelEntries: enumeration.labelEntryCount,
1597
- replayable: enumeration.replayable.length,
1598
- executed: selected.length,
1599
- excludedByReason,
1600
- submitGoldsByCorpus
1601
- },
1602
- headline: {
1603
- replayabilityRate: {
1604
- numerator: replayed.length,
1605
- denominator: selected.length,
1606
- value: rate(replayed.length, selected.length)
1607
- },
1608
- signatureStrictRate: {
1609
- numerator: signatureStrict.length,
1610
- denominator: selected.length,
1611
- value: rate(signatureStrict.length, selected.length)
1612
- },
1613
- prefixFidelity,
1614
- fixFlipRate: options.fix !== "none" ? {
1615
- numerator: flipped.length,
1616
- denominator: armBExecuted.length,
1617
- value: rate(flipped.length, armBExecuted.length)
1618
- } : null,
1619
- fixFlipRateNonzeroRc: options.fix !== "none" ? {
1620
- numerator: flippedNonzeroRc.length,
1621
- denominator: armBNonzeroRc.length,
1622
- value: rate(flippedNonzeroRc.length, armBNonzeroRc.length)
1623
- } : null,
1624
- fixFlipAttempt1: options.fix === "loop" ? {
1625
- numerator: flippedAt1.length,
1626
- denominator: attempt1Executed.length,
1627
- value: rate(flippedAt1.length, attempt1Executed.length)
1628
- } : null,
1629
- flipsByAttempt: options.fix === "loop" ? flipsByAttempt : null
1630
- },
1631
- llm,
1632
- excluded: enumeration.excluded,
1633
- pullFailures,
1634
- cases: rows
1635
- };
1636
- writeFileSync(join(options.out, "batch-report.json"), `${JSON.stringify(report, null, 2)}\n`);
1637
- writeFileSync(join(options.out, "batch-report.md"), renderBatchReport(report));
1638
- return report;
1639
- }
1640
- function pct(value) {
1641
- return value === null ? "—" : `${(value * 100).toFixed(1)}%`;
1642
- }
1643
- function renderBatchReport(report) {
1644
- const lines = [];
1645
- const { headline, totals } = report;
1646
- lines.push("# Replay-verify batch report");
1647
- lines.push("");
1648
- lines.push(`Generated ${report.generatedAt}.`);
1649
- lines.push("");
1650
- lines.push("## Headline");
1651
- lines.push("");
1652
- const fidelity = headline.prefixFidelity;
1653
- lines.push(`- **Replayability rate: ${pct(headline.replayabilityRate.value)}** (${headline.replayabilityRate.numerator}/${headline.replayabilityRate.denominator} replayable cases where the prefix replayed within ${fidelity.tolerancePct}% divergence AND arm A reproduced an observed failure at the gold step k).`);
1654
- lines.push(`- Signature-strict rate: ${pct(headline.signatureStrictRate.value)} (${headline.signatureStrictRate.numerator}/${headline.signatureStrictRate.denominator}; additionally requires the recorded error substring in arm A output).`);
1655
- lines.push(`- **Prefix divergence: ${fidelity.divergencePct === null ? "—" : `${fidelity.divergencePct}%`}** (${fidelity.divergentSteps}/${fidelity.executedSteps} executed prefix steps did not confirm the recording: ${fidelity.returncodeMismatches} returncode mismatches, ${fidelity.unknownExpectations} with no recorded returncode to check). ${fidelity.casesWithinTolerance}/${fidelity.casesExecuted} executed cases are within the ${fidelity.tolerancePct}% tolerance.`);
1656
- if (headline.fixFlipRate) lines.push(`- **Fix-flip rate: ${pct(headline.fixFlipRate.value)}** (${headline.fixFlipRate.numerator}/${headline.fixFlipRate.denominator} arm-B-executed cases where the generated fix made the failure vanish).`);
1657
- if (headline.fixFlipRateNonzeroRc) lines.push(`- Fix-flip rate on recorded-rc≠0 cases: ${pct(headline.fixFlipRateNonzeroRc.value)} (${headline.fixFlipRateNonzeroRc.numerator}/${headline.fixFlipRateNonzeroRc.denominator}; real recorded failures — a gold step recorded with rc 0 flips vacuously).`);
1658
- if (headline.fixFlipAttempt1) lines.push(`- Fix-flip@1: ${pct(headline.fixFlipAttempt1.value)} (${headline.fixFlipAttempt1.numerator}/${headline.fixFlipAttempt1.denominator} cases whose attempt 1 executed — the one-shot-comparable number).`);
1659
- if (headline.flipsByAttempt && Object.keys(headline.flipsByAttempt).length > 0) {
1660
- const parts = Object.entries(headline.flipsByAttempt).sort((a, b) => Number(a[0]) - Number(b[0])).map(([attempt, count]) => `attempt ${attempt}: ${count}`);
1661
- lines.push(`- Flips by attempt: ${parts.join(", ")}.`);
1022
+ }
1023
+ //#endregion
1024
+ //#region src/trajectory-replay/batch-report.ts
1025
+ function pct(value) {
1026
+ return value === null ? "—" : `${(value * 100).toFixed(1)}%`;
1027
+ }
1028
+ function renderBatchReport(report) {
1029
+ const lines = [];
1030
+ const { headline, totals } = report;
1031
+ lines.push("# Replay-verify batch report");
1032
+ lines.push("");
1033
+ lines.push(`Generated ${report.generatedAt}.`);
1034
+ lines.push("");
1035
+ lines.push("## Headline");
1036
+ lines.push("");
1037
+ const fidelity = headline.prefixFidelity;
1038
+ lines.push(`- **Replayability rate: ${pct(headline.replayabilityRate.value)}** (${headline.replayabilityRate.numerator}/${headline.replayabilityRate.denominator} replayable cases where the prefix replayed within ${fidelity.tolerancePct}% divergence AND arm A reproduced an observed failure at the gold step k).`);
1039
+ lines.push(`- Signature-strict rate: ${pct(headline.signatureStrictRate.value)} (${headline.signatureStrictRate.numerator}/${headline.signatureStrictRate.denominator}; additionally requires the recorded error substring in arm A output).`);
1040
+ lines.push(`- **Prefix divergence: ${fidelity.divergencePct === null ? "—" : `${fidelity.divergencePct}%`}** (${fidelity.divergentSteps}/${fidelity.executedSteps} executed prefix steps did not confirm the recording: ${fidelity.returncodeMismatches} returncode mismatches, ${fidelity.unknownExpectations} with no recorded returncode to check). ${fidelity.casesWithinTolerance}/${fidelity.casesExecuted} executed cases are within the ${fidelity.tolerancePct}% tolerance.`);
1041
+ if (headline.fixFlipRate) lines.push(`- **Fix-flip rate: ${pct(headline.fixFlipRate.value)}** (${headline.fixFlipRate.numerator}/${headline.fixFlipRate.denominator} arm-B-executed cases where the generated fix made the failure vanish).`);
1042
+ if (headline.fixFlipRateNonzeroRc) lines.push(`- Fix-flip rate on recorded-rc≠0 cases: ${pct(headline.fixFlipRateNonzeroRc.value)} (${headline.fixFlipRateNonzeroRc.numerator}/${headline.fixFlipRateNonzeroRc.denominator}; real recorded failures — a gold step recorded with rc 0 flips vacuously).`);
1043
+ if (headline.fixFlipAttempt1) lines.push(`- Fix-flip@1: ${pct(headline.fixFlipAttempt1.value)} (${headline.fixFlipAttempt1.numerator}/${headline.fixFlipAttempt1.denominator} cases whose attempt 1 executed — the one-shot-comparable number).`);
1044
+ if (headline.flipsByAttempt && Object.keys(headline.flipsByAttempt).length > 0) {
1045
+ const parts = Object.entries(headline.flipsByAttempt).sort((a, b) => Number(a[0]) - Number(b[0])).map(([attempt, count]) => `attempt ${attempt}: ${count}`);
1046
+ lines.push(`- Flips by attempt: ${parts.join(", ")}.`);
1662
1047
  }
1663
1048
  lines.push("");
1664
1049
  lines.push("## Enumeration");
@@ -1725,6 +1110,631 @@ function renderBatchReport(report) {
1725
1110
  return lines.join("\n");
1726
1111
  }
1727
1112
  //#endregion
1113
+ //#region src/trajectory-replay/corpus.ts
1114
+ /**
1115
+ * Corpus enumeration for batch replay verification.
1116
+ *
1117
+ * A labeled trajectory corpus is a gold label file (array of trajectory
1118
+ * entries with `incorrect_stages[].incorrect_step_ids`) plus a prepared
1119
+ * directory:
1120
+ * <prepared>/normalized/<traj_id>/steps.json normalized steps
1121
+ * <prepared>/normalized/<traj_id>/task.md optional task statement
1122
+ * <prepared>/extracted/<traj_id>/swe_raw/** raw mini-SWE trajectory
1123
+ *
1124
+ * Only trajectories that record their environment are replayable: the raw
1125
+ * trajectory must carry `info.docker_config.base_image`. Every excluded case
1126
+ * is returned with a machine-readable reason — the batch report surfaces the
1127
+ * full exclusion table, never a silently shrunk denominator.
1128
+ */
1129
+ function readLabelEntries(labelsPath) {
1130
+ const parsed = JSON.parse(readFileSync(labelsPath, "utf8"));
1131
+ if (!Array.isArray(parsed)) throw new Error(`${labelsPath} is not a JSON array of label entries`);
1132
+ for (const entry of parsed) if (typeof entry.traj_id !== "string") throw new Error(`${labelsPath} has an entry without a string traj_id`);
1133
+ return parsed;
1134
+ }
1135
+ function goldIncorrectSteps(entry) {
1136
+ const ids = /* @__PURE__ */ new Set();
1137
+ for (const stage of entry.incorrect_stages ?? []) for (const id of stage.incorrect_step_ids ?? []) ids.add(id);
1138
+ return [...ids].sort((a, b) => a - b);
1139
+ }
1140
+ function findRawTrajFiles(sweRawDir) {
1141
+ if (!existsSync(sweRawDir)) return [];
1142
+ return readdirSync(sweRawDir, {
1143
+ recursive: true,
1144
+ encoding: "utf8"
1145
+ }).filter((relative) => relative.endsWith(".traj.json")).map((relative) => join(sweRawDir, relative)).sort();
1146
+ }
1147
+ /** First non-empty output line of the trajectory's first bare `pwd` step. */
1148
+ function cwdFromPwdObservation(steps) {
1149
+ for (const step of steps) {
1150
+ if (step.action.trim() !== "pwd") continue;
1151
+ const line = parseObservationOutput(step.observation).split("\n").map((l) => l.trim()).find((l) => l.startsWith("/"));
1152
+ if (line) return line;
1153
+ }
1154
+ return null;
1155
+ }
1156
+ function taskStatementOf(preparedDir, trajId, raw) {
1157
+ const taskPath = join(preparedDir, "normalized", trajId, "task.md");
1158
+ if (existsSync(taskPath)) {
1159
+ const text = readFileSync(taskPath, "utf8").trim();
1160
+ if (text.length > 0) return text;
1161
+ }
1162
+ const userMessage = raw.messages?.find((m) => m.role === "user");
1163
+ if (typeof userMessage?.content === "string" && userMessage.content.trim().length > 0) return userMessage.content.trim();
1164
+ return null;
1165
+ }
1166
+ /**
1167
+ * Resolves the replay resources for one trajectory, independent of gold
1168
+ * labels — the finding wire uses this with a finding-supplied step instead.
1169
+ */
1170
+ function resolveCaseResources(corpus, trajId) {
1171
+ const sweRawDir = join(corpus.preparedDir, "extracted", trajId, "swe_raw");
1172
+ const rawFiles = findRawTrajFiles(sweRawDir);
1173
+ if (rawFiles.length === 0) return {
1174
+ resolved: false,
1175
+ reason: "no-swe-raw-trajectory",
1176
+ detail: sweRawDir
1177
+ };
1178
+ if (rawFiles.length > 1) return {
1179
+ resolved: false,
1180
+ reason: "ambiguous-swe-raw-trajectory",
1181
+ detail: rawFiles.join(", ")
1182
+ };
1183
+ let raw;
1184
+ try {
1185
+ raw = JSON.parse(readFileSync(rawFiles[0], "utf8"));
1186
+ } catch (err) {
1187
+ return {
1188
+ resolved: false,
1189
+ reason: "unreadable-raw-trajectory",
1190
+ detail: `${rawFiles[0]}: ${err instanceof Error ? err.message : String(err)}`
1191
+ };
1192
+ }
1193
+ const dockerConfig = raw.info?.docker_config;
1194
+ const image = dockerConfig?.base_image;
1195
+ if (typeof image !== "string" || image.length === 0) return {
1196
+ resolved: false,
1197
+ reason: "no-docker-image",
1198
+ detail: rawFiles[0]
1199
+ };
1200
+ const stepsPath = join(corpus.preparedDir, "normalized", trajId, "steps.json");
1201
+ if (!existsSync(stepsPath)) return {
1202
+ resolved: false,
1203
+ reason: "missing-steps-json",
1204
+ detail: stepsPath
1205
+ };
1206
+ const steps = JSON.parse(readFileSync(stepsPath, "utf8"));
1207
+ const runConfigCwd = raw.info?.config?.environment?.cwd;
1208
+ const dockerCwd = dockerConfig?.cwd;
1209
+ let cwd;
1210
+ let cwdSource;
1211
+ if (typeof runConfigCwd === "string" && runConfigCwd.length > 0) {
1212
+ cwd = runConfigCwd;
1213
+ cwdSource = "run-config";
1214
+ } else if (typeof dockerCwd === "string" && dockerCwd.length > 0) {
1215
+ cwd = dockerCwd;
1216
+ cwdSource = "docker-config";
1217
+ } else {
1218
+ const observed = cwdFromPwdObservation(steps);
1219
+ if (!observed) return {
1220
+ resolved: false,
1221
+ reason: "cwd-underivable"
1222
+ };
1223
+ cwd = observed;
1224
+ cwdSource = "pwd-observation";
1225
+ }
1226
+ const recordedTimeout = raw.info?.config?.environment?.timeout;
1227
+ return {
1228
+ resolved: true,
1229
+ resources: {
1230
+ corpus: corpus.name,
1231
+ trajId,
1232
+ stepsPath,
1233
+ steps,
1234
+ taskStatement: taskStatementOf(corpus.preparedDir, trajId, raw),
1235
+ image,
1236
+ cwd,
1237
+ cwdSource,
1238
+ recordedStepTimeoutMs: typeof recordedTimeout === "number" && recordedTimeout > 0 ? recordedTimeout * 1e3 : null
1239
+ }
1240
+ };
1241
+ }
1242
+ /**
1243
+ * Replayable = a raw trajectory with a recorded image AND at least one gold
1244
+ * incorrect step that is a real mid-trajectory action. Gold steps whose action
1245
+ * is the submit command are skipped when choosing k (counted per case); a case
1246
+ * whose golds are ALL submit steps is excluded as gold-only-submit-step.
1247
+ * Exclusion reasons are reported in resolution order:
1248
+ * raw trajectory → image → gold labels → steps.json → k range → cwd.
1249
+ */
1250
+ function enumerateReplayableCases(corpora) {
1251
+ const replayable = [];
1252
+ const excluded = [];
1253
+ let labelEntryCount = 0;
1254
+ for (const corpus of corpora) for (const entry of readLabelEntries(corpus.labelsPath)) {
1255
+ labelEntryCount += 1;
1256
+ const trajId = entry.traj_id;
1257
+ const resolution = resolveCaseResources(corpus, trajId);
1258
+ if (!resolution.resolved) {
1259
+ excluded.push({
1260
+ corpus: corpus.name,
1261
+ trajId,
1262
+ reason: resolution.reason,
1263
+ detail: resolution.detail
1264
+ });
1265
+ continue;
1266
+ }
1267
+ const gold = goldIncorrectSteps(entry);
1268
+ if (gold.length === 0) {
1269
+ excluded.push({
1270
+ corpus: corpus.name,
1271
+ trajId,
1272
+ reason: "no-gold-incorrect-step"
1273
+ });
1274
+ continue;
1275
+ }
1276
+ const resources = resolution.resources;
1277
+ let submitGoldsSkipped = 0;
1278
+ let target = null;
1279
+ let missingGoldId = null;
1280
+ for (const goldId of gold) {
1281
+ const step = resources.steps.find((s) => s.step_id === goldId);
1282
+ if (!step) {
1283
+ missingGoldId = goldId;
1284
+ break;
1285
+ }
1286
+ if (step.action.trim() === "submit" || isSubmitOnlyAction(step.action)) {
1287
+ submitGoldsSkipped += 1;
1288
+ continue;
1289
+ }
1290
+ target = step;
1291
+ break;
1292
+ }
1293
+ if (missingGoldId !== null) {
1294
+ excluded.push({
1295
+ corpus: corpus.name,
1296
+ trajId,
1297
+ reason: "gold-step-outside-steps",
1298
+ detail: `k=${missingGoldId}, steps=${resources.steps.length}`
1299
+ });
1300
+ continue;
1301
+ }
1302
+ if (!target) {
1303
+ excluded.push({
1304
+ corpus: corpus.name,
1305
+ trajId,
1306
+ reason: "gold-only-submit-step",
1307
+ detail: `${submitGoldsSkipped} gold step(s), all submit commands`
1308
+ });
1309
+ continue;
1310
+ }
1311
+ const recordedReturncodeAtK = parseRecordedReturncode(target.observation);
1312
+ if (recordedReturncodeAtK === null) {
1313
+ excluded.push({
1314
+ corpus: corpus.name,
1315
+ trajId,
1316
+ reason: "no-recorded-returncode-at-k",
1317
+ detail: `k=${target.step_id}; arm A cannot confirm execution without a recorded returncode`
1318
+ });
1319
+ continue;
1320
+ }
1321
+ replayable.push({
1322
+ ...resources,
1323
+ goldIncorrectSteps: gold,
1324
+ k: target.step_id,
1325
+ submitGoldsSkipped,
1326
+ recordedReturncodeAtK
1327
+ });
1328
+ }
1329
+ return {
1330
+ replayable,
1331
+ excluded,
1332
+ labelEntryCount
1333
+ };
1334
+ }
1335
+ //#endregion
1336
+ //#region src/trajectory-replay/image-preparer.ts
1337
+ /**
1338
+ * Replay-ready image derivation.
1339
+ *
1340
+ * A recorded trajectory names the image it ran in, but the image is not always
1341
+ * runnable as-is: sandbox platforms pin customer commands to a non-root
1342
+ * identity, so a root-owned working tree must be chowned before the replay can
1343
+ * write to it. `ImagePreparer` is that step, injectable so a consumer whose
1344
+ * images are already replay-ready supplies its own no-op or none at all.
1345
+ */
1346
+ const execFileAsync = promisify(execFile);
1347
+ function derivedImageTag(image, cwd) {
1348
+ return `ctb-replay:${createHash("sha256").update(`${image}\n${cwd}`).digest("hex").slice(0, 12)}-uid1000`;
1349
+ }
1350
+ /**
1351
+ * Pulls the base image when absent and builds `FROM <base>; RUN chown -R
1352
+ * 1000:1000 <cwd>` tagged by content hash, so repeated batches reuse both
1353
+ * the pull and the build. cwd `/` skips the chown (never chown -R /) and
1354
+ * replays on the base image directly.
1355
+ */
1356
+ function dockerImagePreparer(options = {}) {
1357
+ const pullTimeoutMs = options.pullTimeoutMs ?? 12e5;
1358
+ const buildTimeoutMs = options.buildTimeoutMs ?? 9e5;
1359
+ const imageExists = async (tag) => {
1360
+ try {
1361
+ await execFileAsync("docker", [
1362
+ "image",
1363
+ "inspect",
1364
+ tag
1365
+ ], { maxBuffer: 8388608 });
1366
+ return true;
1367
+ } catch {
1368
+ return false;
1369
+ }
1370
+ };
1371
+ const errorTail = (err) => {
1372
+ return (err && typeof err === "object" && "stderr" in err && typeof err.stderr === "string" && err.stderr.length > 0 ? err.stderr : err instanceof Error ? err.message : String(err)).trim().split("\n").slice(-3).join(" | ").slice(0, 400);
1373
+ };
1374
+ return { async ensure(image, cwd) {
1375
+ if (cwd === "/") {
1376
+ if (await imageExists(image)) return {
1377
+ succeeded: true,
1378
+ value: {
1379
+ derivedImage: image,
1380
+ pulled: false,
1381
+ built: false
1382
+ }
1383
+ };
1384
+ try {
1385
+ await execFileAsync("docker", ["pull", image], {
1386
+ timeout: pullTimeoutMs,
1387
+ maxBuffer: 33554432
1388
+ });
1389
+ } catch (err) {
1390
+ return {
1391
+ succeeded: false,
1392
+ error: `pull ${image}: ${errorTail(err)}`
1393
+ };
1394
+ }
1395
+ return {
1396
+ succeeded: true,
1397
+ value: {
1398
+ derivedImage: image,
1399
+ pulled: true,
1400
+ built: false
1401
+ }
1402
+ };
1403
+ }
1404
+ const derived = derivedImageTag(image, cwd);
1405
+ if (await imageExists(derived)) return {
1406
+ succeeded: true,
1407
+ value: {
1408
+ derivedImage: derived,
1409
+ pulled: false,
1410
+ built: false
1411
+ }
1412
+ };
1413
+ let pulled = false;
1414
+ if (!await imageExists(image)) try {
1415
+ await execFileAsync("docker", ["pull", image], {
1416
+ timeout: pullTimeoutMs,
1417
+ maxBuffer: 33554432
1418
+ });
1419
+ pulled = true;
1420
+ } catch (err) {
1421
+ return {
1422
+ succeeded: false,
1423
+ error: `pull ${image}: ${errorTail(err)}`
1424
+ };
1425
+ }
1426
+ const contextDir = mkdtempSync(join(tmpdir(), "ctb-replay-image-"));
1427
+ const quotedCwd = `'${cwd.replaceAll("'", `'\\''`)}'`;
1428
+ writeFileSync(join(contextDir, "Dockerfile"), `FROM ${image}\nRUN chown -R 1000:1000 ${quotedCwd}\n`);
1429
+ try {
1430
+ await execFileAsync("docker", [
1431
+ "build",
1432
+ "-t",
1433
+ derived,
1434
+ contextDir
1435
+ ], {
1436
+ timeout: buildTimeoutMs,
1437
+ maxBuffer: 33554432
1438
+ });
1439
+ } catch (err) {
1440
+ return {
1441
+ succeeded: false,
1442
+ error: `build ${derived} from ${image}: ${errorTail(err)}`
1443
+ };
1444
+ }
1445
+ return {
1446
+ succeeded: true,
1447
+ value: {
1448
+ derivedImage: derived,
1449
+ pulled,
1450
+ built: true
1451
+ }
1452
+ };
1453
+ } };
1454
+ }
1455
+ //#endregion
1456
+ //#region src/trajectory-replay/batch.ts
1457
+ /**
1458
+ * Batch replay verification across gold-labeled trajectory corpora.
1459
+ *
1460
+ * For every replayable case (a raw trajectory with a recorded image AND at
1461
+ * least one gold incorrect step) the batch:
1462
+ * 1. derives the replay-ready image through the injected `ImagePreparer`,
1463
+ * 2. replays the prefix and runs arm A — the recorded gold step k, and
1464
+ * 3. optionally generates a corrected command with one LLM call and runs
1465
+ * arm B in its own fresh session.
1466
+ *
1467
+ * Headline metrics:
1468
+ * replayability rate — fraction of replayable cases where the prefix
1469
+ * replays within the divergence tolerance AND arm A reproduces a
1470
+ * recorded failure at k; a zero exit without an error signature is not
1471
+ * evidence of failure;
1472
+ * prefix fidelity — executed prefix steps and the share of them that did
1473
+ * not confirm the recording, split by kind. A corpus whose recordings
1474
+ * carry no returncodes shows up here as unknown-expectation steps, never
1475
+ * as a clean replay;
1476
+ * fix-flip rate — fraction of arm-B-executed cases where the failure
1477
+ * vanished (exit 0, signature absent).
1478
+ *
1479
+ * Image pulls and execs run strictly serially: pulls contend on disk and
1480
+ * registry bandwidth, and serial cases keep wall-time attribution per case
1481
+ * honest. Pull failures are report rows, never silent skips.
1482
+ */
1483
+ function rate(numerator, denominator) {
1484
+ return denominator > 0 ? numerator / denominator : null;
1485
+ }
1486
+ function caseIdentity(replayCase) {
1487
+ return {
1488
+ corpus: replayCase.corpus,
1489
+ trajId: replayCase.trajId,
1490
+ image: replayCase.image,
1491
+ cwd: replayCase.cwd,
1492
+ cwdSource: replayCase.cwdSource,
1493
+ k: replayCase.k,
1494
+ stepCount: replayCase.steps.length,
1495
+ goldIncorrectSteps: replayCase.goldIncorrectSteps,
1496
+ submitGoldsSkipped: replayCase.submitGoldsSkipped,
1497
+ recordedReturncodeAtK: replayCase.recordedReturncodeAtK
1498
+ };
1499
+ }
1500
+ /** The row of a case that never produced an arm A verdict. */
1501
+ function unreplayedRow(replayCase, caseStart, failure) {
1502
+ return {
1503
+ ...caseIdentity(replayCase),
1504
+ derivedImage: failure.derivedImage,
1505
+ signature: null,
1506
+ status: failure.status,
1507
+ error: failure.error,
1508
+ imagePulled: failure.imagePulled,
1509
+ imageBuilt: failure.imageBuilt,
1510
+ prefixExecuted: null,
1511
+ prefixDivergences: null,
1512
+ prefixDivergencePct: null,
1513
+ prefixConfirmed: null,
1514
+ prefixReturncodeMismatches: null,
1515
+ prefixUnknownExpectations: null,
1516
+ armAExit: null,
1517
+ armAReturncodeMatch: false,
1518
+ armASignatureMatch: false,
1519
+ replayed: false,
1520
+ fix: null,
1521
+ wallMs: Date.now() - caseStart
1522
+ };
1523
+ }
1524
+ function verdictRow(replayCase, caseStart, image, verdict, returncodeMatch) {
1525
+ return {
1526
+ ...caseIdentity(replayCase),
1527
+ derivedImage: image.derivedImage,
1528
+ signature: verdict.signature,
1529
+ status: "ok",
1530
+ error: null,
1531
+ imagePulled: image.pulled,
1532
+ imageBuilt: image.built,
1533
+ prefixExecuted: verdict.prefixExecuted,
1534
+ prefixDivergences: verdict.prefixDivergences.length,
1535
+ prefixDivergencePct: verdict.prefixDivergencePct,
1536
+ prefixConfirmed: verdict.prefixConfirmed,
1537
+ prefixReturncodeMismatches: verdict.prefixReturncodeMismatches,
1538
+ prefixUnknownExpectations: verdict.prefixUnknownExpectations,
1539
+ armAExit: verdict.armA.exitCode,
1540
+ armAReturncodeMatch: returncodeMatch,
1541
+ armASignatureMatch: verdict.armA.failureSignatureMatch,
1542
+ replayed: verdict.valid,
1543
+ fix: null,
1544
+ wallMs: Date.now() - caseStart
1545
+ };
1546
+ }
1547
+ /** Phase 1 for one case: prepare its image and run arm A. Every outcome is a row, never a skip. */
1548
+ async function replayArmA(run, replayCase, label, verdicts, pullFailures) {
1549
+ const { options, onProgress } = run;
1550
+ const caseStart = Date.now();
1551
+ onProgress(`${label}: preparing image ${replayCase.image}`);
1552
+ const preparation = await run.preparer.ensure(replayCase.image, replayCase.cwd);
1553
+ if (!preparation.succeeded) {
1554
+ pullFailures.push({
1555
+ corpus: replayCase.corpus,
1556
+ trajId: replayCase.trajId,
1557
+ image: replayCase.image,
1558
+ error: preparation.error
1559
+ });
1560
+ const row = unreplayedRow(replayCase, caseStart, {
1561
+ derivedImage: null,
1562
+ status: "image-unavailable",
1563
+ error: preparation.error,
1564
+ imagePulled: false,
1565
+ imageBuilt: false
1566
+ });
1567
+ appendFileSync(run.progressPath, `${JSON.stringify(row)}\n`);
1568
+ onProgress(`${label}: image unavailable — ${preparation.error}`);
1569
+ return row;
1570
+ }
1571
+ const { derivedImage, pulled, built } = preparation.value;
1572
+ const stepTimeoutMs = options.stepTimeoutMs ?? replayCase.recordedStepTimeoutMs ?? 12e4;
1573
+ const caseOut = join(options.out, caseOutDirName(replayCase));
1574
+ onProgress(`${label}: arm A on ${derivedImage} (k=${replayCase.k}, timeout ${stepTimeoutMs}ms)`);
1575
+ try {
1576
+ const verdict = await replayVerify({
1577
+ stepsPath: replayCase.stepsPath,
1578
+ image: derivedImage,
1579
+ at: replayCase.k,
1580
+ cwd: replayCase.cwd,
1581
+ out: caseOut,
1582
+ caseId: replayCase.trajId,
1583
+ stepTimeoutMs,
1584
+ prefixLimit: options.prefixLimit,
1585
+ backend: options.backendFactory(derivedImage),
1586
+ onProgress: (message) => onProgress(`${label}: ${message}`)
1587
+ });
1588
+ verdicts.set(replayCase.trajId, verdict);
1589
+ const returncodeMatch = verdict.recordedReturncode !== null && verdict.armA.exitCode === verdict.recordedReturncode;
1590
+ const row = verdictRow(replayCase, caseStart, {
1591
+ derivedImage,
1592
+ pulled,
1593
+ built
1594
+ }, verdict, returncodeMatch);
1595
+ appendFileSync(run.progressPath, `${JSON.stringify(row)}\n`);
1596
+ onProgress(`${label}: armA exit=${verdict.armA.exitCode} rcMatch=${returncodeMatch} divergences=${verdict.prefixDivergences.length}/${verdict.prefixExecuted} (${verdict.prefixDivergencePct}%, ${verdict.prefixUnknownExpectations} unknown-expectation)`);
1597
+ return row;
1598
+ } catch (err) {
1599
+ const message = err instanceof Error ? err.message : String(err);
1600
+ const row = unreplayedRow(replayCase, caseStart, {
1601
+ derivedImage,
1602
+ status: "replay-error",
1603
+ error: message.slice(0, 500),
1604
+ imagePulled: pulled,
1605
+ imageBuilt: built
1606
+ });
1607
+ appendFileSync(run.progressPath, `${JSON.stringify(row)}\n`);
1608
+ onProgress(`${label}: replay error — ${message.slice(0, 200)}`);
1609
+ return row;
1610
+ }
1611
+ }
1612
+ function prefixFidelityOf(executedRows) {
1613
+ const sumOver = (pick) => executedRows.reduce((total, row) => total + (pick(row) ?? 0), 0);
1614
+ const executedSteps = sumOver((r) => r.prefixExecuted);
1615
+ const divergentSteps = sumOver((r) => r.prefixDivergences);
1616
+ return {
1617
+ executedSteps,
1618
+ divergentSteps,
1619
+ returncodeMismatches: sumOver((r) => r.prefixReturncodeMismatches),
1620
+ unknownExpectations: sumOver((r) => r.prefixUnknownExpectations),
1621
+ divergencePct: executedSteps > 0 ? Number((divergentSteps / executedSteps * 100).toFixed(1)) : null,
1622
+ tolerancePct: 10,
1623
+ casesWithinTolerance: executedRows.filter((r) => r.prefixDivergencePct !== null && r.prefixDivergencePct <= 10).length,
1624
+ casesExecuted: executedRows.length
1625
+ };
1626
+ }
1627
+ /** Fix-flip headline rates over the cases whose arm B executed. */
1628
+ function fixFlipHeadline(rows, fix) {
1629
+ const armBExecuted = rows.filter((r) => r.fix?.attempted && r.fix.failureVanished !== null);
1630
+ const flipped = armBExecuted.filter((r) => r.fix.failureVanished === true);
1631
+ const armBNonzeroRc = armBExecuted.filter((r) => r.recordedReturncodeAtK !== null && r.recordedReturncodeAtK !== 0);
1632
+ const flippedNonzeroRc = armBNonzeroRc.filter((r) => r.fix.failureVanished === true);
1633
+ const attempt1Executed = rows.filter((r) => r.fix?.attempts?.find((a) => a.attempt === 1)?.executed === true);
1634
+ const flippedAt1 = rows.filter((r) => r.fix?.flippedAtAttempt === 1);
1635
+ const flipsByAttempt = {};
1636
+ for (const row of rows) {
1637
+ const at = row.fix?.flippedAtAttempt;
1638
+ if (typeof at === "number") flipsByAttempt[String(at)] = (flipsByAttempt[String(at)] ?? 0) + 1;
1639
+ }
1640
+ return {
1641
+ fixFlipRate: fix !== "none" ? {
1642
+ numerator: flipped.length,
1643
+ denominator: armBExecuted.length,
1644
+ value: rate(flipped.length, armBExecuted.length)
1645
+ } : null,
1646
+ fixFlipRateNonzeroRc: fix !== "none" ? {
1647
+ numerator: flippedNonzeroRc.length,
1648
+ denominator: armBNonzeroRc.length,
1649
+ value: rate(flippedNonzeroRc.length, armBNonzeroRc.length)
1650
+ } : null,
1651
+ fixFlipAttempt1: fix === "loop" ? {
1652
+ numerator: flippedAt1.length,
1653
+ denominator: attempt1Executed.length,
1654
+ value: rate(flippedAt1.length, attempt1Executed.length)
1655
+ } : null,
1656
+ flipsByAttempt: fix === "loop" ? flipsByAttempt : null
1657
+ };
1658
+ }
1659
+ /** Per-corpus submit-gold accounting: cases dropped for pure-submit golds, and submit golds skipped inside replayable cases. */
1660
+ function submitGoldsOf(enumeration) {
1661
+ const byCorpus = {};
1662
+ const entry = (corpus) => byCorpus[corpus] ??= {
1663
+ submitOnlyCases: 0,
1664
+ goldsSkippedWithinReplayable: 0
1665
+ };
1666
+ for (const excluded of enumeration.excluded) if (excluded.reason === "gold-only-submit-step") entry(excluded.corpus).submitOnlyCases += 1;
1667
+ for (const replayCase of enumeration.replayable) if (replayCase.submitGoldsSkipped > 0) entry(replayCase.corpus).goldsSkippedWithinReplayable += replayCase.submitGoldsSkipped;
1668
+ return byCorpus;
1669
+ }
1670
+ function buildBatchReport(options, enumeration, selected, rows, llm, pullFailures) {
1671
+ const executedRows = rows.filter((r) => r.status === "ok");
1672
+ const replayed = executedRows.filter((r) => r.replayed);
1673
+ const signatureStrict = executedRows.filter((r) => r.replayed && r.armASignatureMatch);
1674
+ const excludedByReason = {};
1675
+ for (const excluded of enumeration.excluded) excludedByReason[excluded.reason] = (excludedByReason[excluded.reason] ?? 0) + 1;
1676
+ return {
1677
+ generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
1678
+ corpora: options.corpora.map((c) => ({
1679
+ name: c.name,
1680
+ labelsPath: c.labelsPath,
1681
+ preparedDir: c.preparedDir
1682
+ })),
1683
+ totals: {
1684
+ labelEntries: enumeration.labelEntryCount,
1685
+ replayable: enumeration.replayable.length,
1686
+ executed: selected.length,
1687
+ excludedByReason,
1688
+ submitGoldsByCorpus: submitGoldsOf(enumeration)
1689
+ },
1690
+ headline: {
1691
+ replayabilityRate: {
1692
+ numerator: replayed.length,
1693
+ denominator: selected.length,
1694
+ value: rate(replayed.length, selected.length)
1695
+ },
1696
+ signatureStrictRate: {
1697
+ numerator: signatureStrict.length,
1698
+ denominator: selected.length,
1699
+ value: rate(signatureStrict.length, selected.length)
1700
+ },
1701
+ prefixFidelity: prefixFidelityOf(executedRows),
1702
+ ...fixFlipHeadline(rows, options.fix)
1703
+ },
1704
+ llm,
1705
+ excluded: enumeration.excluded,
1706
+ pullFailures,
1707
+ cases: rows
1708
+ };
1709
+ }
1710
+ async function runReplayBatch(options) {
1711
+ const onProgress = options.onProgress ?? (() => {});
1712
+ const enumeration = enumerateReplayableCases(options.corpora);
1713
+ let selected = enumeration.replayable;
1714
+ if (options.caseFilter) selected = selected.filter((c) => c.trajId.includes(options.caseFilter));
1715
+ if (options.caseLimit !== void 0) selected = selected.slice(0, options.caseLimit);
1716
+ onProgress(`enumerated ${enumeration.labelEntryCount} label entries → ${enumeration.replayable.length} replayable, ${enumeration.excluded.length} excluded; executing ${selected.length}`);
1717
+ mkdirSync(options.out, { recursive: true });
1718
+ const run = {
1719
+ options,
1720
+ onProgress,
1721
+ progressPath: join(options.out, "cases.jsonl"),
1722
+ preparer: options.preparer ?? dockerImagePreparer()
1723
+ };
1724
+ const rows = [];
1725
+ const pullFailures = [];
1726
+ const verdictByTraj = /* @__PURE__ */ new Map();
1727
+ for (const [index, replayCase] of selected.entries()) {
1728
+ const label = `[${index + 1}/${selected.length}] ${replayCase.corpus}/${replayCase.trajId}`;
1729
+ rows.push(await replayArmA(run, replayCase, label, verdictByTraj, pullFailures));
1730
+ }
1731
+ const llm = options.fix !== "none" ? await runFixPhase(run, selected, rows, verdictByTraj) : null;
1732
+ const report = buildBatchReport(options, enumeration, selected, rows, llm, pullFailures);
1733
+ writeFileSync(join(options.out, "batch-report.json"), `${JSON.stringify(report, null, 2)}\n`);
1734
+ writeFileSync(join(options.out, "batch-report.md"), renderBatchReport(report));
1735
+ return report;
1736
+ }
1737
+ //#endregion
1728
1738
  //#region src/trajectory-replay/wire.ts
1729
1739
  /**
1730
1740
  * Finding-to-replay wire: the entry point that turns a cited incorrect-steps