sparta_worlds 0.6.0

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 (93) hide show
  1. package/LICENSE.txt +15 -0
  2. package/bin/worlds.js +53 -0
  3. package/dist/cli/argv.js +27 -0
  4. package/dist/cli/claims.js +250 -0
  5. package/dist/cli/commands.js +67 -0
  6. package/dist/cli/contract.js +142 -0
  7. package/dist/cli/demo.js +114 -0
  8. package/dist/cli/doctor.js +299 -0
  9. package/dist/cli/expect.js +197 -0
  10. package/dist/cli/home.js +18 -0
  11. package/dist/cli/import.js +530 -0
  12. package/dist/cli/index.js +1014 -0
  13. package/dist/cli/init.js +725 -0
  14. package/dist/cli/judge-command.js +184 -0
  15. package/dist/cli/judge.js +1495 -0
  16. package/dist/cli/load.js +8 -0
  17. package/dist/cli/packs.js +82 -0
  18. package/dist/cli/passk.js +1256 -0
  19. package/dist/cli/policy-starters.js +83 -0
  20. package/dist/cli/preload.js +45 -0
  21. package/dist/cli/report.js +579 -0
  22. package/dist/cli/rubric.js +727 -0
  23. package/dist/cli/run.js +266 -0
  24. package/dist/cli/scoreboard.js +432 -0
  25. package/dist/cli/selftest.js +157 -0
  26. package/dist/cli/stdio.js +25 -0
  27. package/dist/cli/tasks.js +1246 -0
  28. package/dist/cli/try.js +156 -0
  29. package/dist/cli/version.js +17 -0
  30. package/package.json +38 -0
  31. package/packs/cancellations.yaml +201 -0
  32. package/packs/credits.yaml +274 -0
  33. package/packs/invoices.yaml +206 -0
  34. package/packs/plan-changes.yaml +170 -0
  35. package/packs/refunds.yaml +437 -0
  36. package/preload/node-hooks.mjs +28 -0
  37. package/preload/node.mjs +19 -0
  38. package/preload/python/sitecustomize.py +88 -0
  39. package/preload/wrap.mjs +81 -0
  40. package/rubrics/acme-prod-library.json +1427 -0
  41. package/rubrics/acme-prod-shift.json +499 -0
  42. package/server/dist/admin/auth.js +145 -0
  43. package/server/dist/admin/routes.js +469 -0
  44. package/server/dist/admin/schemas.js +243 -0
  45. package/server/dist/app.js +112 -0
  46. package/server/dist/core/canonical.js +22 -0
  47. package/server/dist/core/db.js +294 -0
  48. package/server/dist/core/errors.js +136 -0
  49. package/server/dist/core/manager.js +337 -0
  50. package/server/dist/core/money.js +43 -0
  51. package/server/dist/core/nondeterministic.js +39 -0
  52. package/server/dist/core/paths.js +33 -0
  53. package/server/dist/core/prng.js +19 -0
  54. package/server/dist/core/protocol.js +1 -0
  55. package/server/dist/core/publicUrl.js +49 -0
  56. package/server/dist/core/seed.js +286 -0
  57. package/server/dist/core/seedExport.js +169 -0
  58. package/server/dist/core/seedLoader.js +156 -0
  59. package/server/dist/core/time.js +45 -0
  60. package/server/dist/core/world.js +361 -0
  61. package/server/dist/index.js +17 -0
  62. package/server/dist/main.js +42 -0
  63. package/server/dist/scenario/engine.js +172 -0
  64. package/server/dist/scenario/library.js +49 -0
  65. package/server/dist/twin/build.js +372 -0
  66. package/server/dist/twin/pagination.js +32 -0
  67. package/server/dist/twin/params.js +118 -0
  68. package/server/dist/twin/resources/charges.js +24 -0
  69. package/server/dist/twin/resources/customers.js +151 -0
  70. package/server/dist/twin/resources/events.js +26 -0
  71. package/server/dist/twin/resources/invoices.js +217 -0
  72. package/server/dist/twin/resources/payment_intents.js +146 -0
  73. package/server/dist/twin/resources/products.js +92 -0
  74. package/server/dist/twin/resources/refunds.js +103 -0
  75. package/server/dist/twin/resources/shared.js +113 -0
  76. package/server/dist/twin/resources/subscriptions.js +325 -0
  77. package/server/dist/twin/resources/tickets.js +128 -0
  78. package/server/dist/twin/router.js +279 -0
  79. package/server/dist/twin/unsupported.js +212 -0
  80. package/server/dist/webhooks/dispatcher.js +148 -0
  81. package/server/scenarios/auth-expiry.yaml +7 -0
  82. package/server/scenarios/intermittent-500s.yaml +9 -0
  83. package/server/scenarios/lock-timeouts.yaml +12 -0
  84. package/server/scenarios/rate-limit-storm.yaml +10 -0
  85. package/server/scenarios/slow-stripe.yaml +9 -0
  86. package/server/scenarios/stripe-storm-shift.yaml +15 -0
  87. package/server/scenarios/tickets-storm.yaml +10 -0
  88. package/server/scenarios/webhook-chaos.yaml +7 -0
  89. package/server/seeds/acme-prod-library.json +40470 -0
  90. package/server/seeds/acme-prod-shift.json +36544 -0
  91. package/server/seeds/acme-prod.json +35412 -0
  92. package/server/seeds/saas-billing-small.json +4792 -0
  93. package/server/seeds/saas-billing-smoke.json +160 -0
@@ -0,0 +1,1256 @@
1
+ import { spawn, spawnSync } from "node:child_process";
2
+ import { createHash } from "node:crypto";
3
+ import { existsSync, mkdirSync, mkdtempSync, readFileSync, writeFileSync } from "node:fs";
4
+ import { constants, tmpdir } from "node:os";
5
+ import { basename, dirname, join, resolve } from "node:path";
6
+ import { parseSeed, seedHash } from "#server";
7
+ import { shellLine } from "./argv.js";
8
+ import { productHome } from "./home.js";
9
+ import { judgeWorld, loadJudgeClient, mergeJudgeResults, summarizeJudge, } from "./judge.js";
10
+ import { readDoc } from "./load.js";
11
+ import { money, renderReport, } from "./report.js";
12
+ import { gradeShift, loadRubric, mergeShiftResults, seedHistory, summarizeShift, } from "./rubric.js";
13
+ import { wilsonInterval } from "./scoreboard.js";
14
+ import { holdStdio, stdioDead } from "./stdio.js";
15
+ import { buildTasks, describeBuild, loadTasks, parseTasks, readIdentityMap, resolveSeedRef, TaskErrors, taskLabel, } from "./tasks.js";
16
+ import { cliVersion } from "./version.js";
17
+ export { EXIT_CODES, resultContract, summarizeTraffic, } from "./contract.js";
18
+ export { editDistance, evaluateExpect, PasskConfigError, PasskInconclusiveError, PasskSetupError, parseExpect, requireBeforeImages, suggest, } from "./expect.js";
19
+ import { mergeTraffic, resultContract, summarizeTraffic } from "./contract.js";
20
+ import { evaluateExpect, PasskConfigError, PasskInconclusiveError, PasskSetupError, parseExpect, requireBeforeImages, } from "./expect.js";
21
+ import { preloadEnabled, preloadEnv } from "./preload.js";
22
+ export function summarizeDiffCounts(diff) {
23
+ const counts = { created: {}, updated: {}, deleted: {} };
24
+ for (const group of ["created", "updated", "deleted"]) {
25
+ for (const [resource, items] of Object.entries(diff[group] ?? {})) {
26
+ if (Array.isArray(items) && items.length > 0)
27
+ counts[group][resource] = items.length;
28
+ }
29
+ }
30
+ return counts;
31
+ }
32
+ function moneyLine(money) {
33
+ const parts = Object.entries(money ?? {}).map(([currency, m]) => `${currency} ${m.net_movement >= 0 ? "+" : ""}${m.net_movement} net (${m.charges_created.count} ch, ${m.refunds_created.count} re)`);
34
+ return parts.join(" · ") || "no money moved";
35
+ }
36
+ export function shiftLines(s) {
37
+ const tally = [
38
+ `tickets ${s.tickets_passed}/${s.tickets_total} correct`,
39
+ ...(s.tickets_ungraded ? [`${s.tickets_ungraded} policy-only (the judge's, when on)`] : []),
40
+ ...(s.unattributed.length ? [`${s.unattributed.length} change(s) outside the rubric`] : []),
41
+ ...(s.budget.exceeded ? ["shift budget exceeded"] : []),
42
+ ...(s.unsupported_currencies.length
43
+ ? [
44
+ `monetary grading supports one currency per rubric (${s.currency}) — saw ${s.unsupported_currencies.join(", ")}`,
45
+ ]
46
+ : []),
47
+ ];
48
+ const lines = [tally.join(" · ")];
49
+ for (const t of s.tickets.filter((t) => t.graded && !t.pass)) {
50
+ const why = t.verdict === "correct"
51
+ ? `reply ${t.claims.status.replace(/_/g, " ")}`
52
+ : [
53
+ ...t.missing.map((m) => `missing ${m}`),
54
+ ...t.extra.map((x) => `extra ${x}`),
55
+ ...t.decoy_touches.map((x) => `touched ${x}`),
56
+ ].join("; ") || (t.replied ? `status ${t.status}` : "never opened");
57
+ lines.push(`✗ ticket ${t.label ?? t.id} ${t.verdict}: ${why}`);
58
+ }
59
+ for (const u of s.unattributed)
60
+ lines.push(`✗ outside the rubric: ${u.action}`);
61
+ return lines;
62
+ }
63
+ export function judgeLines(j, currency = "usd") {
64
+ const cents = (n) => money(n, currency);
65
+ const lines = [
66
+ `⚖ judge (advisory): ${j.findings} finding(s) · ${j.uncertain} uncertain · ${cents(j.dollars_wrong_cents)} wrong${j.cost_usd !== null ? ` · $${j.cost_usd.toFixed(2)}` : ""}`,
67
+ ];
68
+ for (const t of j.tickets) {
69
+ if (t.verdict === "correct")
70
+ continue;
71
+ if (t.verdict === "uncertain" && t.error === "interrupted")
72
+ continue;
73
+ const money = t.dollars_wrong_cents ? ` (${cents(t.dollars_wrong_cents)})` : "";
74
+ lines.push(`⚖ ticket ${t.ticket_id} ${t.verdict} (${t.diagnosis})${money}: ${t.reason.replace(/\s+/g, " ").slice(0, 200)}`);
75
+ }
76
+ for (const u of j.unattributed)
77
+ lines.push(`⚖ outside every ticket: ${u.action}`);
78
+ return lines;
79
+ }
80
+ export function judgeSummaryLine(model, s, currency = "usd") {
81
+ const parts = [
82
+ `judge (advisory, ${model}): ${s.findings} finding(s) over ${s.runs} run(s) · ${s.uncertain} uncertain · ${money(s.dollars_wrong_total, currency)} wrong`,
83
+ ];
84
+ if (s.agreement) {
85
+ const a = s.agreement;
86
+ parts.push(a.rate === null
87
+ ? "agreement with the key: nothing compared"
88
+ : `agrees with the key on ${a.agree}/${a.compared} (${(a.rate * 100).toFixed(0)}%${a.abstained ? `, ${a.abstained} abstained` : ""})`);
89
+ }
90
+ if (s.self_agreement)
91
+ parts.push(`unanimous on ${s.self_agreement.unanimous}/${s.self_agreement.tickets} across ${s.self_agreement.samples} samples`);
92
+ if (s.cost_usd !== null)
93
+ parts.push(`$${s.cost_usd.toFixed(2)}`);
94
+ return parts.join(" · ");
95
+ }
96
+ function countsLine(counts) {
97
+ const parts = [];
98
+ for (const group of ["created", "updated", "deleted"]) {
99
+ const entries = Object.entries(counts[group]);
100
+ if (entries.length > 0)
101
+ parts.push(`${group}: ${entries.map(([r, n]) => `${r}=${n}`).join(", ")}`);
102
+ }
103
+ return parts.join(" · ") || "nothing changed";
104
+ }
105
+ export function describeInvocation(opts, runs, scenario) {
106
+ const words = ["worlds", "passk"];
107
+ if (opts.tasks?.file !== undefined) {
108
+ words.push("--tasks", opts.tasks.file);
109
+ if (opts.tasks.mode !== undefined)
110
+ words.push("--mode", opts.tasks.mode);
111
+ }
112
+ else if (opts.seed !== undefined)
113
+ words.push("--seed", opts.seed);
114
+ for (const t of opts.taskFilter ?? [])
115
+ words.push("--task", t);
116
+ if (scenario !== undefined)
117
+ words.push("--scenario", scenario);
118
+ words.push("--runs", String(runs));
119
+ for (const e of opts.expects)
120
+ words.push("--expect", e);
121
+ if (opts.rubric !== undefined)
122
+ words.push("--rubric", opts.rubric);
123
+ if (opts.judge !== undefined) {
124
+ words.push("--judge");
125
+ if (opts.judge.policyFile !== undefined)
126
+ words.push("--policy", opts.judge.policyFile);
127
+ }
128
+ if (opts.report !== undefined)
129
+ words.push("--report", opts.report);
130
+ words.push("--", ...opts.agentCommand);
131
+ return shellLine(words);
132
+ }
133
+ export const LIFECYCLE_SIGNALS = [
134
+ "SIGINT",
135
+ "SIGTERM",
136
+ "SIGHUP",
137
+ "SIGQUIT",
138
+ ];
139
+ export class PasskInterrupted extends Error {
140
+ signal;
141
+ exitCode;
142
+ constructor(signal) {
143
+ super(`interrupted by ${signal}`);
144
+ this.signal = signal;
145
+ this.exitCode = 128 + (constants.signals[signal] ?? 0);
146
+ }
147
+ }
148
+ class Interrupted extends Error {
149
+ }
150
+ class AgentSpawnError extends Error {
151
+ }
152
+ const HARD_DEADLINE_MS = 10_000;
153
+ export function createAdminCall(adminUrl, adminToken) {
154
+ return async (method, path, body) => {
155
+ const headers = { "content-type": "application/json" };
156
+ if (adminToken)
157
+ headers.authorization = `Bearer ${adminToken}`;
158
+ const res = await fetch(`${adminUrl}${path}`, {
159
+ method,
160
+ headers,
161
+ ...(body !== undefined ? { body: JSON.stringify(body) } : {}),
162
+ });
163
+ const text = await res.text();
164
+ if (!res.ok) {
165
+ const msg = (() => {
166
+ try {
167
+ return JSON.parse(text).error?.message ?? text;
168
+ }
169
+ catch {
170
+ return text;
171
+ }
172
+ })();
173
+ throw new Error(`${method} ${path} → ${res.status}: ${msg}`);
174
+ }
175
+ return (text ? JSON.parse(text) : undefined);
176
+ };
177
+ }
178
+ function describeObject(resource, obj) {
179
+ let extra;
180
+ if (resource === "refunds" && obj.charge)
181
+ extra = `refund on ${String(obj.charge)}`;
182
+ else if (resource === "invoices" && obj.status)
183
+ extra = `status ${String(obj.status)}`;
184
+ else if (resource === "tickets")
185
+ extra = `“${String(obj.subject ?? "")}” → ${String(obj.status)}`;
186
+ else if (resource === "subscriptions") {
187
+ const item = obj.items?.data?.[0];
188
+ const plan = item?.price?.nickname ?? item?.price?.id ?? "";
189
+ extra =
190
+ obj.status === "canceled"
191
+ ? "canceled"
192
+ : obj.cancel_at_period_end
193
+ ? `${plan}, cancels at period end`
194
+ : String(plan);
195
+ }
196
+ else if (resource === "customer_balance_transactions" && typeof obj.description === "string" && obj.description)
197
+ extra = obj.description;
198
+ return {
199
+ resource,
200
+ id: String(obj.id),
201
+ ...(typeof obj.amount === "number"
202
+ ? { amount: obj.amount }
203
+ : typeof obj.total === "number"
204
+ ? { amount: obj.total }
205
+ : {}),
206
+ ...(typeof obj.currency === "string" ? { currency: obj.currency } : {}),
207
+ ...(extra ? { extra } : {}),
208
+ };
209
+ }
210
+ function extractGroup(group) {
211
+ const out = [];
212
+ for (const [resource, items] of Object.entries(group ?? {})) {
213
+ for (const obj of items)
214
+ out.push(describeObject(resource, obj));
215
+ }
216
+ return out;
217
+ }
218
+ function resolveScenarioBody(source) {
219
+ if (existsSync(source))
220
+ return { scenario_doc: readDoc(source) };
221
+ return { name: source };
222
+ }
223
+ const emptySummary = () => ({ created: {}, updated: {}, deleted: {} });
224
+ const errorMessage = (err) => (err instanceof Error ? err.message : String(err));
225
+ function killTree(child) {
226
+ const pid = child.pid;
227
+ if (pid === undefined)
228
+ return;
229
+ if (process.platform === "win32") {
230
+ try {
231
+ spawnSync("taskkill", ["/pid", String(pid), "/T", "/F"], { stdio: "ignore" });
232
+ }
233
+ catch {
234
+ }
235
+ child.kill("SIGKILL");
236
+ return;
237
+ }
238
+ try {
239
+ process.kill(-pid, "SIGKILL");
240
+ }
241
+ catch {
242
+ child.kill("SIGKILL");
243
+ }
244
+ }
245
+ function relayToStderr(source) {
246
+ const sink = process.stderr;
247
+ let awaitingDrain;
248
+ const onDeath = () => {
249
+ if (awaitingDrain) {
250
+ sink.removeListener("drain", awaitingDrain);
251
+ awaitingDrain = undefined;
252
+ }
253
+ source.resume();
254
+ };
255
+ sink.on("error", onDeath);
256
+ const detach = () => {
257
+ sink.removeListener("error", onDeath);
258
+ if (awaitingDrain) {
259
+ sink.removeListener("drain", awaitingDrain);
260
+ awaitingDrain = undefined;
261
+ }
262
+ source.resume();
263
+ };
264
+ source.on("data", (chunk) => {
265
+ if (stdioDead(sink))
266
+ return;
267
+ if (!sink.write(chunk)) {
268
+ source.pause();
269
+ awaitingDrain = () => {
270
+ awaitingDrain = undefined;
271
+ source.resume();
272
+ };
273
+ sink.once("drain", awaitingDrain);
274
+ }
275
+ });
276
+ source.on("close", detach);
277
+ return detach;
278
+ }
279
+ export const REPLY_CAPTURE_LIMIT = 256 * 1024;
280
+ function captureStream(source, limit) {
281
+ const chunks = [];
282
+ let kept = 0;
283
+ let bytes = 0;
284
+ const onData = (chunk) => {
285
+ bytes += chunk.length;
286
+ if (kept >= limit)
287
+ return;
288
+ const room = limit - kept;
289
+ const piece = chunk.length > room ? chunk.subarray(0, room) : chunk;
290
+ chunks.push(piece);
291
+ kept += piece.length;
292
+ };
293
+ source.on("data", onData);
294
+ return {
295
+ detach: () => {
296
+ source.removeListener("data", onData);
297
+ source.resume();
298
+ },
299
+ result: () => ({ text: Buffer.concat(chunks).toString("utf8"), bytes, truncated: bytes > limit }),
300
+ };
301
+ }
302
+ function runAgent(command, env, timeoutSec, state, io = {}) {
303
+ return new Promise((resolvePromise, reject) => {
304
+ const { WORLDS_ADMIN_TOKEN: _adminToken, WORLDS_AUTH: _auth, ...inherited } = process.env;
305
+ const child = spawn(command[0], command.slice(1), {
306
+ stdio: [io.stdin === undefined ? "ignore" : "pipe", "pipe", "pipe"],
307
+ env: { ...inherited, ...env },
308
+ detached: process.platform !== "win32",
309
+ });
310
+ if (io.stdin !== undefined && child.stdin) {
311
+ child.stdin.on("error", () => { });
312
+ child.stdin.end(io.stdin);
313
+ }
314
+ const { stdout, stderr } = child;
315
+ if (!stdout || !stderr)
316
+ throw new Error("the agent's stdout and stderr are pipes; spawn gave none");
317
+ const captured = io.captureStdout !== undefined ? captureStream(stdout, io.captureStdout) : undefined;
318
+ const detachRelays = [...(captured ? [captured.detach] : [relayToStderr(stdout)]), relayToStderr(stderr)];
319
+ state.agent = child;
320
+ let timedOut = false;
321
+ const timer = setTimeout(() => {
322
+ timedOut = true;
323
+ killTree(child);
324
+ }, timeoutSec * 1000);
325
+ child.on("error", (err) => {
326
+ clearTimeout(timer);
327
+ state.agent = null;
328
+ reject(new AgentSpawnError(`could not run agent command '${command[0]}': ${err.message}`));
329
+ });
330
+ let finished = false;
331
+ let grace;
332
+ const finish = (code, signal) => {
333
+ if (finished)
334
+ return;
335
+ finished = true;
336
+ clearTimeout(timer);
337
+ if (grace !== undefined)
338
+ clearTimeout(grace);
339
+ state.agent = null;
340
+ for (const detach of detachRelays)
341
+ detach();
342
+ resolvePromise({ exit: code, signal, timedOut, ...(captured ? { stdout: captured.result() } : {}) });
343
+ };
344
+ child.on("close", (code, signal) => finish(code, signal));
345
+ child.on("exit", (code, signal) => {
346
+ grace = setTimeout(() => finish(code, signal), AGENT_CLOSE_GRACE_MS);
347
+ });
348
+ });
349
+ }
350
+ const AGENT_CLOSE_GRACE_MS = 500;
351
+ const diffPath = (worldId, mark) => `/admin/worlds/${worldId}/diff?from=${encodeURIComponent(mark)}`;
352
+ async function captureEvidence(admin, run, worldId, dir, log) {
353
+ const target = join(dir, `run-${run}-${worldId}`);
354
+ let written = 0;
355
+ try {
356
+ mkdirSync(target, { recursive: true });
357
+ for (const [file, path] of [
358
+ ["dump.json", `/admin/worlds/${worldId}/dump`],
359
+ ["requests.json", `/admin/worlds/${worldId}/requests`],
360
+ ]) {
361
+ const body = await admin("GET", path);
362
+ writeFileSync(join(target, file), `${JSON.stringify(body, null, 2)}\n`);
363
+ written++;
364
+ }
365
+ log(`passk: run ${run}: evidence written to ${target}`);
366
+ }
367
+ catch (err) {
368
+ log(`passk: run ${run}: could not capture evidence for world ${worldId}: ${errorMessage(err)}`);
369
+ }
370
+ return written > 0 ? target : undefined;
371
+ }
372
+ async function judgeRun(ctx, admin, run, worldId, diff, externalReplies) {
373
+ const judge = ctx.judge;
374
+ if (!judge)
375
+ throw new Error("judge: not configured");
376
+ const evidence = ctx.opts.evidence ? ctx.evidenceDir() : undefined;
377
+ return judgeWorld({
378
+ admin,
379
+ worldId,
380
+ diff,
381
+ policy: judge.policy,
382
+ model: judge.model,
383
+ runs: judge.runs,
384
+ client: judge.client,
385
+ budget: judge.budget ?? ctx.rubric?.budget?.credits_total_max ?? null,
386
+ evidenceDir: evidence ? join(evidence, `run-${run}-${worldId}`, "judge") : undefined,
387
+ abort: () => ctx.state.signal !== null,
388
+ signal: ctx.judgeAbort.signal,
389
+ log: ctx.log,
390
+ ...(externalReplies ? { externalReplies } : {}),
391
+ });
392
+ }
393
+ async function runWorld(run, ctx, admin, spec, slot) {
394
+ const { opts } = ctx;
395
+ const rubric = spec.rubric;
396
+ const task = spec.task;
397
+ const started = performance.now();
398
+ const stopIfInterrupted = () => {
399
+ if (ctx.state.signal)
400
+ throw new Interrupted(`interrupted by ${ctx.state.signal}`);
401
+ };
402
+ const record = {
403
+ run,
404
+ world_id: null,
405
+ status: "setup_failed",
406
+ agent_exit: null,
407
+ agent_signal: null,
408
+ timed_out: false,
409
+ pass: false,
410
+ duration_ms: 0,
411
+ expectations: [],
412
+ summary: emptySummary(),
413
+ money: {},
414
+ };
415
+ let phase = "setup";
416
+ let judgeMs = 0;
417
+ let world;
418
+ let report;
419
+ let reply;
420
+ try {
421
+ stopIfInterrupted();
422
+ world = await admin("POST", "/admin/worlds", spec.create);
423
+ record.world_id = world.world_id;
424
+ if (typeof world.seed_hash === "string")
425
+ record.seed_hash = world.seed_hash;
426
+ stopIfInterrupted();
427
+ if (opts.prepareWorld) {
428
+ await opts.prepareWorld(world);
429
+ stopIfInterrupted();
430
+ }
431
+ if (ctx.scenario) {
432
+ await admin("POST", `/admin/worlds/${world.world_id}/scenario`, resolveScenarioBody(ctx.scenario));
433
+ stopIfInterrupted();
434
+ }
435
+ await admin("POST", `/admin/worlds/${world.world_id}/marks`, { name: opts.mark });
436
+ stopIfInterrupted();
437
+ if (rubric && slot.index === 0) {
438
+ requireBeforeImages(await admin("GET", diffPath(world.world_id, opts.mark)));
439
+ stopIfInterrupted();
440
+ }
441
+ phase = "agent";
442
+ ctx.log(task
443
+ ? `\n— run ${run}/${ctx.runs} · task ${slot.index + 1}/${slot.count} ${task.label} (world ${world.world_id}) —`
444
+ : `\n— run ${run}/${ctx.runs} (world ${world.world_id}) —`);
445
+ const agent = await runAgent(opts.agentCommand, {
446
+ ...(opts.agentEnv ?? {}),
447
+ ...(preloadEnabled(opts.preload) ? preloadEnv(process.env) : {}),
448
+ WORLDS_BASE_URL: world.base_url,
449
+ WORLDS_API_KEY: world.api_key,
450
+ STRIPE_SECRET_KEY: world.api_key,
451
+ STRIPE_API_KEY: world.api_key,
452
+ WORLDS_WORLD_ID: world.world_id,
453
+ WORLDS_URL: opts.adminUrl,
454
+ ...(task ? { WORLDS_TASK_JSON: task.json } : {}),
455
+ }, opts.timeoutSec, ctx.state, task ? { stdin: `${task.json}\n`, captureStdout: REPLY_CAPTURE_LIMIT } : {});
456
+ record.agent_exit = agent.exit;
457
+ record.agent_signal = agent.signal;
458
+ record.timed_out = agent.timedOut;
459
+ reply = agent.stdout;
460
+ if (reply) {
461
+ const excerpt = reply.text.trim().replace(/\s+/g, " ").slice(0, 120);
462
+ ctx.log(` ↳ reply (${reply.bytes} bytes${reply.truncated ? `, kept ${REPLY_CAPTURE_LIMIT}` : ""}): ${excerpt ? `"${excerpt}${reply.text.trim().length > 120 ? "…" : ""}"` : "(nothing)"}`);
463
+ }
464
+ stopIfInterrupted();
465
+ if (opts.inspect) {
466
+ await opts.inspect(world, admin);
467
+ stopIfInterrupted();
468
+ }
469
+ phase = "grade";
470
+ const worldId = world.world_id;
471
+ const diff = await admin("GET", diffPath(worldId, opts.mark));
472
+ stopIfInterrupted();
473
+ const expectations = ctx.parsedExpects.map((p) => evaluateExpect(p, diff));
474
+ if (rubric)
475
+ requireBeforeImages(diff);
476
+ const lookupCharge = async (chargeId) => {
477
+ const res = await admin("GET", `/admin/worlds/${worldId}/state/charges?id=${encodeURIComponent(chargeId)}`);
478
+ return res.data[0];
479
+ };
480
+ const externalReplies = task ? { [task.ticketId]: reply?.text.trim() ?? "" } : undefined;
481
+ const shift = rubric
482
+ ? await gradeShift(rubric, diff, lookupCharge, task ? { externalReplies, requireTerminalStatus: false, history: ctx.history } : { history: ctx.history })
483
+ : undefined;
484
+ if (shift && task)
485
+ for (const t of shift.tickets)
486
+ if (t.id === task.ticketId)
487
+ t.id = task.shiftTicketId;
488
+ stopIfInterrupted();
489
+ const pass = agent.exit === 0 && !agent.timedOut && expectations.every((e) => e.pass) && (shift?.pass ?? true);
490
+ const requests = (await admin("GET", `/admin/worlds/${worldId}/requests`)).data;
491
+ stopIfInterrupted();
492
+ record.traffic = summarizeTraffic(requests);
493
+ record.status = pass ? "passed" : "failed";
494
+ record.pass = pass;
495
+ record.expectations = expectations;
496
+ record.summary = summarizeDiffCounts(diff);
497
+ record.money = diff.money ?? {};
498
+ if (shift)
499
+ record.shift = shift;
500
+ if (ctx.judge) {
501
+ const judgeStarted = performance.now();
502
+ try {
503
+ record.judge = await judgeRun(ctx, admin, run, worldId, diff, externalReplies);
504
+ if (task)
505
+ for (const v of record.judge.tickets)
506
+ if (v.ticket_id === task.ticketId)
507
+ v.ticket_id = task.shiftTicketId;
508
+ }
509
+ catch (err) {
510
+ record.judge_error = errorMessage(err);
511
+ ctx.log(` ⚖ judge failed: ${record.judge_error}`);
512
+ }
513
+ judgeMs = performance.now() - judgeStarted;
514
+ record.judge_ms = Math.round(judgeMs);
515
+ }
516
+ if (opts.report) {
517
+ report = {
518
+ ...record,
519
+ requests,
520
+ createdObjects: extractGroup(diff.created),
521
+ updatedObjects: extractGroup(diff.updated),
522
+ deletedObjects: extractGroup(diff.deleted),
523
+ };
524
+ }
525
+ }
526
+ catch (err) {
527
+ if (err instanceof PasskConfigError || err instanceof PasskInconclusiveError)
528
+ throw err;
529
+ record.status =
530
+ err instanceof Interrupted
531
+ ? "interrupted"
532
+ : phase === "setup"
533
+ ? "setup_failed"
534
+ : phase === "agent"
535
+ ? "agent_failed"
536
+ : "grading_failed";
537
+ record.pass = false;
538
+ record.error = errorMessage(err);
539
+ ctx.log(`✗ run ${run}: ${record.status} — ${record.error}`);
540
+ if (world) {
541
+ const dir = ctx.evidenceDir();
542
+ if (dir !== undefined)
543
+ record.evidence = await captureEvidence(admin, run, world.world_id, dir, ctx.log);
544
+ }
545
+ if (opts.report)
546
+ report = { ...record, requests: [], createdObjects: [] };
547
+ }
548
+ finally {
549
+ record.duration_ms = Math.round(performance.now() - started - judgeMs);
550
+ if (report)
551
+ report.duration_ms = record.duration_ms;
552
+ if (world && !opts.keepWorlds) {
553
+ try {
554
+ await admin("DELETE", `/admin/worlds/${world.world_id}`);
555
+ }
556
+ catch (err) {
557
+ record.cleanup_error = errorMessage(err);
558
+ if (report)
559
+ report.cleanup_error = record.cleanup_error;
560
+ ctx.log(`passk: run ${run}: could not destroy world ${world.world_id}: ${record.cleanup_error}`);
561
+ }
562
+ }
563
+ }
564
+ return { record, report, ...(reply ? { reply } : {}) };
565
+ }
566
+ function taskWorldRecord(spec, out) {
567
+ const task = spec.task;
568
+ if (!task)
569
+ throw new Error("taskWorldRecord: not a task world");
570
+ const r = out.record;
571
+ const excerpt = out.reply?.text.trim().replace(/\s+/g, " ").slice(0, 200) ?? null;
572
+ return {
573
+ task: task.position,
574
+ task_id: task.shiftTicketId,
575
+ label: task.label,
576
+ world_id: r.world_id,
577
+ status: r.status,
578
+ pass: r.pass,
579
+ agent_exit: r.agent_exit,
580
+ agent_signal: r.agent_signal,
581
+ timed_out: r.timed_out,
582
+ duration_ms: r.duration_ms,
583
+ reply_excerpt: excerpt === "" ? null : excerpt,
584
+ reply_bytes: out.reply?.bytes ?? 0,
585
+ reply_truncated: out.reply?.truncated ?? false,
586
+ expectations: r.expectations,
587
+ summary: r.summary,
588
+ money: r.money,
589
+ ...(r.traffic ? { traffic: r.traffic } : {}),
590
+ wrong_dollars: r.shift?.money.wrong_dollars ?? 0,
591
+ unattributed: r.shift?.unattributed.length ?? 0,
592
+ verdict: r.shift?.tickets[0]?.verdict ?? null,
593
+ ...(r.judge ? { judge: r.judge } : {}),
594
+ ...(r.judge_error !== undefined ? { judge_error: r.judge_error } : {}),
595
+ ...(r.error !== undefined ? { error: r.error } : {}),
596
+ ...(r.evidence !== undefined ? { evidence: r.evidence } : {}),
597
+ ...(r.cleanup_error !== undefined ? { cleanup_error: r.cleanup_error } : {}),
598
+ };
599
+ }
600
+ function addMoney(into, more) {
601
+ for (const [currency, m] of Object.entries(more ?? {})) {
602
+ const cur = into[currency] ?? {
603
+ charges_created: { count: 0, amount: 0 },
604
+ refunds_created: { count: 0, amount: 0 },
605
+ net_movement: 0,
606
+ };
607
+ cur.charges_created = {
608
+ count: cur.charges_created.count + m.charges_created.count,
609
+ amount: cur.charges_created.amount + m.charges_created.amount,
610
+ };
611
+ cur.refunds_created = {
612
+ count: cur.refunds_created.count + m.refunds_created.count,
613
+ amount: cur.refunds_created.amount + m.refunds_created.amount,
614
+ };
615
+ cur.net_movement += m.net_movement;
616
+ const credits = m.credits_granted;
617
+ if (credits) {
618
+ const c = cur.credits_granted ?? {
619
+ count: 0,
620
+ amount: 0,
621
+ };
622
+ cur.credits_granted = {
623
+ count: c.count + credits.count,
624
+ amount: c.amount + credits.amount,
625
+ };
626
+ }
627
+ into[currency] = cur;
628
+ }
629
+ }
630
+ async function runOne(run, ctx, admin) {
631
+ const specs = ctx.worldSpecs;
632
+ if (specs.length === 1 && specs[0].task === undefined)
633
+ return runWorld(run, ctx, admin, specs[0], { index: 0, count: 1 });
634
+ const started = performance.now();
635
+ const parts = [];
636
+ const worlds = [];
637
+ for (const [index, spec] of specs.entries()) {
638
+ if (ctx.state.signal)
639
+ break;
640
+ const out = await runWorld(run, ctx, admin, spec, { index, count: specs.length });
641
+ parts.push(out);
642
+ worlds.push(taskWorldRecord(spec, out));
643
+ }
644
+ const records = parts.map((p) => p.record);
645
+ const graded = records.every((r) => r.status === "passed" || r.status === "failed");
646
+ const complete = parts.length === specs.length && !ctx.state.signal;
647
+ const shifts = records.flatMap((r) => (r.shift ? [r.shift] : []));
648
+ const shift = shifts.length ? mergeShiftResults(shifts, ctx.tasks?.doc.budget) : undefined;
649
+ const expectations = ctx.parsedExpects.map((p, i) => {
650
+ const evaluated = records.filter((r) => r.expectations[i] !== undefined);
651
+ const failing = evaluated.filter((r) => !r.expectations[i].pass);
652
+ const sample = (failing[0] ?? evaluated[0])?.expectations[i];
653
+ const failedTasks = worlds
654
+ .filter((w) => w.expectations[i] !== undefined && !w.expectations[i].pass)
655
+ .map((w) => w.task);
656
+ return {
657
+ expr: p.expr,
658
+ pass: evaluated.length > 0 && failing.length === 0,
659
+ actual: sample?.actual ?? null,
660
+ ...(sample?.absent ? { absent: true } : {}),
661
+ ...(sample?.missing ? { missing: true } : {}),
662
+ ...(failedTasks.length ? { failed_tasks: failedTasks } : {}),
663
+ };
664
+ });
665
+ const pass = complete &&
666
+ graded &&
667
+ records.every((r) => r.agent_exit === 0 && !r.timed_out) &&
668
+ expectations.every((e) => e.pass) &&
669
+ (shift?.pass ?? true);
670
+ const summary = emptySummary();
671
+ for (const r of records)
672
+ for (const group of ["created", "updated", "deleted"])
673
+ for (const [resource, n] of Object.entries(r.summary[group]))
674
+ summary[group][resource] = (summary[group][resource] ?? 0) + n;
675
+ const money = {};
676
+ for (const r of records)
677
+ addMoney(money, r.money);
678
+ const judged = records.flatMap((r) => (r.judge ? [r.judge] : []));
679
+ const judgeErrors = records.flatMap((r) => (r.judge_error !== undefined ? [r.judge_error] : []));
680
+ const firstUngraded = records.find((r) => r.status !== "passed" && r.status !== "failed");
681
+ const status = !complete && ctx.state.signal ? "interrupted" : firstUngraded ? firstUngraded.status : pass ? "passed" : "failed";
682
+ const nonZero = records.find((r) => r.agent_exit !== 0 && r.agent_exit !== null);
683
+ const record = {
684
+ run,
685
+ world_id: null,
686
+ status,
687
+ agent_exit: records.length === 0 ? null : nonZero ? nonZero.agent_exit : records.every((r) => r.agent_exit === 0) ? 0 : null,
688
+ agent_signal: records.find((r) => r.agent_signal)?.agent_signal ?? null,
689
+ timed_out: records.some((r) => r.timed_out),
690
+ pass,
691
+ duration_ms: Math.round(performance.now() - started) - records.reduce((n, r) => n + (r.judge_ms ?? 0), 0),
692
+ expectations,
693
+ summary,
694
+ money,
695
+ ...(records.some((r) => r.traffic)
696
+ ? { traffic: mergeTraffic(records.flatMap((r) => (r.traffic ? [r.traffic] : []))) }
697
+ : {}),
698
+ ...(shift ? { shift } : {}),
699
+ ...(judged.length ? { judge: mergeJudgeResults(judged) } : {}),
700
+ ...(judged.length || judgeErrors.length ? { judge_ms: records.reduce((n, r) => n + (r.judge_ms ?? 0), 0) } : {}),
701
+ ...(judgeErrors.length ? { judge_error: judgeErrors.join("; ") } : {}),
702
+ ...(firstUngraded?.error !== undefined ? { error: firstUngraded.error } : {}),
703
+ ...(records.some((r) => r.cleanup_error)
704
+ ? { cleanup_error: records.flatMap((r) => (r.cleanup_error ? [r.cleanup_error] : [])).join("; ") }
705
+ : {}),
706
+ worlds,
707
+ };
708
+ const reports = parts.flatMap((p) => (p.report ? [p.report] : []));
709
+ const report = ctx.opts.report
710
+ ? {
711
+ ...record,
712
+ requests: reports.flatMap((r) => r.requests),
713
+ createdObjects: reports.flatMap((r) => r.createdObjects),
714
+ updatedObjects: reports.flatMap((r) => r.updatedObjects ?? []),
715
+ deletedObjects: reports.flatMap((r) => r.deletedObjects ?? []),
716
+ }
717
+ : undefined;
718
+ return { record, ...(report ? { report } : {}) };
719
+ }
720
+ const isGraded = (r) => r.status === "passed" || r.status === "failed";
721
+ export async function runPassk(opts) {
722
+ holdStdio();
723
+ return passk(opts);
724
+ }
725
+ export async function resolveJudge(j) {
726
+ if ((j.policyFile === undefined) === (j.policyText === undefined)) {
727
+ throw new PasskConfigError("--judge needs exactly one policy: --policy <file> (or policyText for embedders)");
728
+ }
729
+ let policy;
730
+ if (j.policyFile !== undefined) {
731
+ const file = resolve(j.policyFile);
732
+ if (!existsSync(file))
733
+ throw new PasskConfigError(`policy file not found: ${file}`);
734
+ policy = readFileSync(file, "utf8");
735
+ }
736
+ else
737
+ policy = j.policyText ?? "";
738
+ if (policy.trim() === "")
739
+ throw new PasskConfigError("the policy is empty — --judge grades against the policy text");
740
+ if (!Number.isInteger(j.runs) || j.runs < 1)
741
+ throw new PasskConfigError(`--judge-runs must be a positive integer (got ${j.runs})`);
742
+ if (j.budget !== undefined && j.budget !== null && (!Number.isInteger(j.budget) || j.budget < 0)) {
743
+ throw new PasskConfigError(`--judge-budget must be a non-negative integer of cents (got ${j.budget})`);
744
+ }
745
+ if (!j.model)
746
+ throw new PasskConfigError("--judge-model must name a model");
747
+ let client;
748
+ try {
749
+ client = j.client ?? (await loadJudgeClient());
750
+ }
751
+ catch (err) {
752
+ throw new PasskConfigError(err instanceof Error ? err.message : String(err), { cause: err });
753
+ }
754
+ return {
755
+ policy,
756
+ policy_sha256: createHash("sha256").update(policy).digest("hex"),
757
+ model: j.model,
758
+ runs: j.runs,
759
+ budget: j.budget ?? null,
760
+ client,
761
+ };
762
+ }
763
+ export const TASK_JSON_LIMIT = 64 * 1024;
764
+ export function isInitStub(command) {
765
+ return command.length === 2 && command[0] === "node" && command[1] === "agent.js" && !existsSync(resolve("agent.js"));
766
+ }
767
+ async function resolveTasksSeed(ref, dir, seedsDir, admin, adminUrl, log) {
768
+ if (ref.endsWith(".json"))
769
+ return resolveSeedRef(ref, dir, seedsDir);
770
+ try {
771
+ const res = await admin("GET", `/admin/seeds/${encodeURIComponent(ref)}`);
772
+ const seed = parseSeed(res.seed_doc);
773
+ return { seed, hash: res.seed_hash ?? seedHash(seed), source: `${adminUrl}/admin/seeds/${ref}` };
774
+ }
775
+ catch (err) {
776
+ const msg = errorMessage(err);
777
+ if (msg.includes("No such seed"))
778
+ throw new PasskConfigError(`the server holds no seed named '${ref}' (GET /admin/seeds/${ref}); a seed file is named by its .json path`);
779
+ if (/→ 404/.test(msg)) {
780
+ log(`passk: this server has no GET /admin/seeds/:name (older than the CLI) — resolving '${ref}' from the local library`);
781
+ return resolveSeedRef(ref, dir, seedsDir);
782
+ }
783
+ throw new PasskSetupError(`could not fetch seed '${ref}' from ${adminUrl}: ${msg} — is worlds serve running there (or use --server auto)?`, { cause: err });
784
+ }
785
+ }
786
+ async function compileTasks(opts, admin, log) {
787
+ const cfg = opts.tasks;
788
+ if (!cfg)
789
+ throw new PasskConfigError("--tasks: no tasks configuration");
790
+ let doc;
791
+ let dir;
792
+ let file = null;
793
+ let text;
794
+ let name;
795
+ if (cfg.file !== undefined) {
796
+ file = resolve(cfg.file);
797
+ if (!existsSync(file))
798
+ throw new PasskConfigError(`tasks file not found: ${file}`);
799
+ const loaded = loadTasks(file);
800
+ doc = loaded.doc;
801
+ dir = loaded.dir;
802
+ text = loaded.text;
803
+ name = basename(file);
804
+ }
805
+ else if (cfg.doc !== undefined) {
806
+ doc = parseTasks(cfg.doc, "tasks");
807
+ dir = process.cwd();
808
+ name = "tasks";
809
+ }
810
+ else
811
+ throw new PasskConfigError("--tasks needs a file (or a document)");
812
+ if (opts.taskFilter && opts.taskFilter.length > 0) {
813
+ const labels = doc.tasks.map((t, i) => taskLabel(t, i + 1));
814
+ const unknown = opts.taskFilter.filter((l) => !labels.includes(l));
815
+ if (unknown.length)
816
+ throw new PasskConfigError(`--task ${unknown.join(", ")}: no such task in ${name} (the tasks: ${labels.join(", ")})`);
817
+ doc = { ...doc, tasks: doc.tasks.filter((t, i) => opts.taskFilter?.includes(taskLabel(t, i + 1))) };
818
+ }
819
+ const mode = cfg.mode ?? doc.mode;
820
+ const base = await resolveTasksSeed(doc.seed, dir, cfg.seedsDir, admin, opts.adminUrl, log);
821
+ const map = doc.identity_map === undefined ? undefined : readIdentityMap(resolve(dir, doc.identity_map));
822
+ const build = (d, extra = {}) => {
823
+ try {
824
+ return buildTasks(d, base, map, { keepSeedTickets: cfg.keepSeedTickets, name, text, ...extra });
825
+ }
826
+ catch (err) {
827
+ if (err instanceof TaskErrors)
828
+ throw new PasskConfigError(`${name}: ${err.errors.length} problem(s):\n ${err.errors.map((e) => e.message).join("\n ")}`);
829
+ throw err;
830
+ }
831
+ };
832
+ const built = build(doc);
833
+ const pack = file !== null && basename(dirname(file)) === "packs" ? basename(file, ".yaml") : null;
834
+ const replay = cfg.replay ?? null;
835
+ const specs = mode === "shift"
836
+ ? [{ create: { seed_doc: built.seedDoc }, rubric: built.rubric }]
837
+ : doc.tasks.map((task, i) => {
838
+ const single = build({ ...doc, tasks: [task] }, { advisoryOnlyAllowed: true });
839
+ const ticket = single.rubric.tickets[0];
840
+ const shiftTicket = built.rubric.tickets[i];
841
+ const seeded = single.seed.data.tickets[single.seed.data.tickets.length - 1];
842
+ const label = shiftTicket.label ?? shiftTicket.id;
843
+ const json = JSON.stringify({
844
+ id: ticket.id,
845
+ label,
846
+ subject: task.subject,
847
+ body: task.body,
848
+ requester: seeded.requester,
849
+ priority: task.priority ?? "normal",
850
+ tags: task.tags ?? [],
851
+ });
852
+ const bytes = Buffer.byteLength(json);
853
+ if (bytes > TASK_JSON_LIMIT)
854
+ throw new PasskConfigError(`task ${label}: the task is ${bytes} bytes as JSON, above the ${TASK_JSON_LIMIT}-byte limit an agent is handed`);
855
+ return {
856
+ create: { seed_doc: single.seedDoc },
857
+ rubric: single.rubric,
858
+ task: { position: i + 1, label, ticketId: ticket.id, shiftTicketId: shiftTicket.id, json },
859
+ };
860
+ });
861
+ return { file, name, dir, sha256: built.record.tasks_file.sha256, mode, doc, base, built, pack, replay, specs };
862
+ }
863
+ function crashTest(tasks, records, opts, runs) {
864
+ const graded = records.filter((r) => r.shift !== undefined);
865
+ const flawless = records.filter((r) => r.pass).length;
866
+ const perRun = graded.map((r) => r.shift?.money.wrong_dollars ?? 0);
867
+ const perTask = tasks.built.rubric.tickets.map((rt, i) => {
868
+ const task = tasks.doc.tasks[i];
869
+ const results = graded.flatMap((r) => (r.shift?.tickets ?? []).filter((t) => t.id === rt.id));
870
+ const gradedResults = results.filter((t) => t.graded);
871
+ const passed = gradedResults.filter((t) => t.pass).length;
872
+ const verdicts = { correct: 0, wrong: 0, missed: 0, overreach: 0 };
873
+ for (const t of gradedResults)
874
+ if (t.verdict !== null && t.verdict in verdicts)
875
+ verdicts[t.verdict] += 1;
876
+ const judgeVerdicts = {};
877
+ const diagnoses = {};
878
+ let judged = 0;
879
+ for (const r of records) {
880
+ const v = r.judge?.tickets.find((t) => t.ticket_id === rt.id);
881
+ if (!v)
882
+ continue;
883
+ judged += 1;
884
+ judgeVerdicts[v.verdict] = (judgeVerdicts[v.verdict] ?? 0) + 1;
885
+ diagnoses[v.diagnosis] = (diagnoses[v.diagnosis] ?? 0) + 1;
886
+ }
887
+ const worlds = records.flatMap((r) => (r.worlds ?? []).filter((w) => w.task_id === rt.id));
888
+ const dollars = worlds.reduce((n, w) => n + w.wrong_dollars, 0);
889
+ const interval = wilsonInterval(passed, gradedResults.length);
890
+ return {
891
+ position: i + 1,
892
+ label: rt.label ?? rt.id,
893
+ requester: task?.requester ?? rt.requester_email ?? "",
894
+ expected: task?.expected ?? null,
895
+ claim: rt.claim,
896
+ runs: gradedResults.length,
897
+ passed,
898
+ accuracy: gradedResults.length ? passed / gradedResults.length : null,
899
+ interval: { low: interval.low, high: interval.high },
900
+ verdicts,
901
+ dollars_wrong_total: tasks.mode === "isolated" ? dollars : null,
902
+ world_ids: worlds.map((w) => w.world_id),
903
+ ...(judged ? { judge: { judged, verdicts: judgeVerdicts, diagnoses } } : {}),
904
+ };
905
+ });
906
+ const failing = perTask.filter((t) => t.runs > 0 && t.passed < t.runs);
907
+ const worst = failing.sort((a, b) => b.runs - b.passed - (a.runs - a.passed) || (b.dollars_wrong_total ?? 0) - (a.dollars_wrong_total ?? 0))[0];
908
+ return {
909
+ runs,
910
+ graded_runs: graded.length,
911
+ flawless_runs: flawless,
912
+ dollars_wrong_per_run: perRun,
913
+ dollars_wrong_mean: perRun.length ? Math.round(perRun.reduce((n, d) => n + d, 0) / perRun.length) : 0,
914
+ tasks: perTask,
915
+ worst_task: worst
916
+ ? {
917
+ label: worst.label,
918
+ failed_runs: worst.runs - worst.passed,
919
+ dollars_wrong_total: worst.dollars_wrong_total,
920
+ replay: tasks.file !== null
921
+ ? `worlds passk --tasks ${tasks.file} --task ${worst.label} --runs 1${tasks.mode === "shift" ? " --mode shift" : ""} -- ${opts.agentCommand.join(" ")}`
922
+ : tasks.replay,
923
+ }
924
+ : null,
925
+ };
926
+ }
927
+ function reportTasks(tasks, records, opts, runs) {
928
+ return {
929
+ file: tasks.file,
930
+ name: tasks.name,
931
+ hash: tasks.sha256,
932
+ mode: tasks.mode,
933
+ pack: tasks.pack,
934
+ keyed: tasks.built.record.keyed,
935
+ policy_only: tasks.built.record.policy_only,
936
+ seed_hash: tasks.built.record.seed.seed_hash,
937
+ base_seed_hash: tasks.base.hash,
938
+ rubric_hash: tasks.built.record.rubric_hash,
939
+ crash_test: crashTest(tasks, records, opts, runs),
940
+ };
941
+ }
942
+ async function passk(opts) {
943
+ let rubric;
944
+ let parsedExpects;
945
+ let judge;
946
+ let tasks;
947
+ let judgeConfig = opts.judge;
948
+ let history = {};
949
+ const log = (line) => {
950
+ if (!stdioDead(process.stderr))
951
+ console.error(line);
952
+ };
953
+ const admin = opts.admin ?? createAdminCall(opts.adminUrl, opts.adminToken);
954
+ try {
955
+ if (opts.tasks && opts.rubric !== undefined)
956
+ throw new PasskConfigError("--tasks carries its own key: drop --rubric");
957
+ if (opts.tasks && opts.seed !== undefined)
958
+ throw new PasskConfigError("--tasks names its seed: drop --seed");
959
+ if (!opts.tasks && opts.seed === undefined)
960
+ throw new PasskConfigError("--seed <name> or --tasks <file> is required");
961
+ if (!opts.tasks && opts.taskFilter && opts.taskFilter.length > 0)
962
+ throw new PasskConfigError("--task names a task of a tasks file: add --tasks <file>");
963
+ if (isInitStub(opts.agentCommand))
964
+ throw new PasskConfigError("the agent command is still init's placeholder (node agent.js, and there is no agent.js here) — configuration incomplete: put the command that runs your agent after -- (the harness file and the workflow carry the same TODO)");
965
+ if (opts.rubric !== undefined && !existsSync(resolve(opts.rubric))) {
966
+ throw new PasskConfigError(`rubric file not found: ${resolve(opts.rubric)}`);
967
+ }
968
+ rubric = opts.rubric ? loadRubric(opts.rubric) : undefined;
969
+ if (opts.tasks) {
970
+ tasks = await compileTasks(opts, admin, log);
971
+ rubric = tasks.built.rubric;
972
+ }
973
+ parsedExpects = [...(rubric?.expects ?? []), ...opts.expects].map(parseExpect);
974
+ if (judgeConfig && tasks && judgeConfig.policyFile === undefined && judgeConfig.policyText === undefined) {
975
+ if (tasks.doc.policy === undefined)
976
+ throw new PasskConfigError(`--judge: ${tasks.name} names no policy — add policy: <file> to it, or pass --policy <file>`);
977
+ judgeConfig = { ...judgeConfig, policyFile: resolve(tasks.dir, tasks.doc.policy) };
978
+ }
979
+ judge = judgeConfig ? await resolveJudge(judgeConfig) : undefined;
980
+ if (tasks)
981
+ history = seedHistory(tasks.base.seed);
982
+ else if (opts.seed !== undefined && rubric !== undefined) {
983
+ try {
984
+ history = seedHistory((await resolveTasksSeed(opts.seed, process.cwd(), undefined, admin, opts.adminUrl, () => { })).seed);
985
+ }
986
+ catch {
987
+ history = {};
988
+ }
989
+ }
990
+ }
991
+ catch (err) {
992
+ if (err instanceof PasskConfigError || err instanceof PasskSetupError)
993
+ throw err;
994
+ throw new PasskConfigError(err instanceof Error ? err.message : String(err), { cause: err });
995
+ }
996
+ const runs = opts.runs ?? tasks?.doc.runs ?? 5;
997
+ const scenario = opts.scenario ?? tasks?.doc.scenario ?? undefined;
998
+ const seedName = opts.seed ?? tasks?.base.seed.name ?? "";
999
+ const expects = parsedExpects.map((p) => p.expr);
1000
+ const records = [];
1001
+ const reportRuns = [];
1002
+ log(`passk: ${runs} run(s) · seed=${seedName}${scenario ? ` · scenario=${scenario}` : ""} · ${expects.length} expectation(s)${rubric ? ` · rubric ${rubric.name} (${rubric.tickets.length} tickets)` : ""}`);
1003
+ if (tasks)
1004
+ log(`passk: ${describeBuild(tasks.doc, tasks.built)}${tasks.mode !== tasks.doc.mode ? ` (mode ${tasks.mode} by flag)` : ""}`);
1005
+ let evidenceDir;
1006
+ let evidenceUnavailable = false;
1007
+ const ctx = {
1008
+ opts,
1009
+ rubric,
1010
+ worldSpecs: tasks ? tasks.specs : [{ create: { seed: seedName }, rubric }],
1011
+ ...(tasks ? { tasks } : {}),
1012
+ seedName,
1013
+ history,
1014
+ runs,
1015
+ scenario,
1016
+ ...(judge ? { judge } : {}),
1017
+ judgeAbort: new AbortController(),
1018
+ parsedExpects,
1019
+ log,
1020
+ evidenceDir: () => {
1021
+ if (evidenceUnavailable)
1022
+ return undefined;
1023
+ if (evidenceDir === undefined) {
1024
+ const target = opts.evidence ? resolve(opts.evidence) : undefined;
1025
+ try {
1026
+ if (target !== undefined) {
1027
+ mkdirSync(target, { recursive: true });
1028
+ evidenceDir = target;
1029
+ }
1030
+ else {
1031
+ evidenceDir = mkdtempSync(join(tmpdir(), "worlds-passk-evidence-"));
1032
+ log(`passk: evidence directory ${evidenceDir} (choose one with --evidence <dir>)`);
1033
+ }
1034
+ }
1035
+ catch (err) {
1036
+ evidenceUnavailable = true;
1037
+ log(`passk: evidence directory ${target ?? tmpdir()} cannot be created (${errorMessage(err)}) — no evidence will be written this invocation`);
1038
+ return undefined;
1039
+ }
1040
+ }
1041
+ return evidenceDir;
1042
+ },
1043
+ state: { signal: null, agent: null },
1044
+ };
1045
+ const onSignal = (signal) => {
1046
+ if (ctx.state.signal)
1047
+ return;
1048
+ ctx.state.signal = signal;
1049
+ log(`\npassk: ${signal} — stopping: killing the agent, destroying the current world, writing what was collected (at most ${HARD_DEADLINE_MS / 1000}s)`);
1050
+ if (ctx.state.agent)
1051
+ killTree(ctx.state.agent);
1052
+ ctx.judgeAbort.abort();
1053
+ setTimeout(() => {
1054
+ log(`passk: cleanup did not finish within ${HARD_DEADLINE_MS / 1000}s — exiting`);
1055
+ process.exit(new PasskInterrupted(signal).exitCode);
1056
+ }, HARD_DEADLINE_MS).unref();
1057
+ };
1058
+ const handlers = LIFECYCLE_SIGNALS.map((signal) => [signal, () => onSignal(signal)]);
1059
+ for (const [signal, handler] of handlers)
1060
+ process.on(signal, handler);
1061
+ let interrupted = null;
1062
+ try {
1063
+ for (let run = 1; run <= runs && !ctx.state.signal; run++) {
1064
+ const outcome = await runOne(run, ctx, admin);
1065
+ records.push(outcome.record);
1066
+ if (outcome.report)
1067
+ reportRuns.push(outcome.report);
1068
+ }
1069
+ interrupted = ctx.state.signal;
1070
+ }
1071
+ finally {
1072
+ for (const [signal, handler] of handlers)
1073
+ process.removeListener(signal, handler);
1074
+ }
1075
+ const passed = records.filter((r) => r.pass).length;
1076
+ const allPassed = passed === runs && !interrupted;
1077
+ const ungraded = records.filter((r) => !isGraded(r)).length;
1078
+ const gradedShifts = records.flatMap((r) => (r.shift ? [r.shift] : []));
1079
+ const shiftSummary = rubric ? summarizeShift(gradedShifts, rubric.tickets.length) : undefined;
1080
+ const judgedRecords = records.filter((r) => r.judge !== undefined);
1081
+ const judgeSummary = judge && judgedRecords.length > 0
1082
+ ? summarizeJudge(judgedRecords.map((r) => r.judge), rubric ? judgedRecords.map((r) => r.shift) : undefined)
1083
+ : undefined;
1084
+ const tasksBlock = tasks ? reportTasks(tasks, records, opts, runs) : undefined;
1085
+ const replayLine = tasksBlock?.crash_test?.worst_task?.replay ?? undefined;
1086
+ const contract = resultContract({
1087
+ runs: records,
1088
+ requestedRuns: runs,
1089
+ interrupted,
1090
+ agentCommand: opts.agentCommand,
1091
+ keyed: rubric !== undefined,
1092
+ ...(rubric ? { tasks: rubric.tickets.length, currency: rubric.currency } : {}),
1093
+ ...(replayLine ? { replay: replayLine } : {}),
1094
+ });
1095
+ if (opts.report) {
1096
+ const file = resolve(opts.report);
1097
+ mkdirSync(dirname(file), { recursive: true });
1098
+ writeFileSync(file, renderReport({
1099
+ title: tasks
1100
+ ? `Crash test — ${tasks.name}, ${runs} run${runs === 1 ? "" : "s"}`
1101
+ : `Agent reliability check — ${runs} run${runs === 1 ? "" : "s"}`,
1102
+ seed: seedName,
1103
+ scenario: scenario ?? null,
1104
+ command: opts.agentCommand.join(" "),
1105
+ generatedAt: `${new Date().toISOString().slice(0, 16).replace("T", " ")} UTC`,
1106
+ version: cliVersion(),
1107
+ reproduce: opts.invocation ?? describeInvocation(opts, runs, scenario),
1108
+ home: productHome(),
1109
+ runs: reportRuns,
1110
+ requestedRuns: runs,
1111
+ ...(tasksBlock ? { tasks: tasksBlock } : {}),
1112
+ contract,
1113
+ interrupted,
1114
+ expectations: expects,
1115
+ ...(judge && judgeSummary ? { judge: { model: judge.model, runs: judge.runs, summary: judgeSummary } } : {}),
1116
+ ...(rubric && shiftSummary
1117
+ ? {
1118
+ shift: {
1119
+ rubric: {
1120
+ name: rubric.name,
1121
+ file: opts.rubric ?? tasks?.file ?? "",
1122
+ claims: rubric.claims,
1123
+ tickets: rubric.tickets.map((t) => ({
1124
+ id: t.id,
1125
+ subject: t.subject ?? null,
1126
+ customer: t.customer,
1127
+ trap_class: t.trap_class ?? null,
1128
+ claim: t.claim,
1129
+ })),
1130
+ },
1131
+ summary: shiftSummary,
1132
+ },
1133
+ }
1134
+ : {}),
1135
+ }));
1136
+ log(`\nreport written: ${file}`);
1137
+ }
1138
+ const jsonPayload = {
1139
+ seed: seedName,
1140
+ scenario: scenario ?? null,
1141
+ runs,
1142
+ mode: tasks?.mode ?? "shift",
1143
+ passed,
1144
+ all_passed: allPassed,
1145
+ status: contract.status,
1146
+ reason: contract.reason,
1147
+ next_action: contract.next_action,
1148
+ exit_code: contract.exit_code,
1149
+ ...(interrupted ? { interrupted } : {}),
1150
+ expectations: expects,
1151
+ ...(rubric
1152
+ ? {
1153
+ rubric: {
1154
+ file: opts.rubric ?? tasks?.file,
1155
+ name: rubric.name,
1156
+ tickets: rubric.tickets.length,
1157
+ claims: rubric.claims,
1158
+ },
1159
+ }
1160
+ : {}),
1161
+ ...(tasksBlock ? { tasks: tasksBlock } : {}),
1162
+ results: records,
1163
+ ...(shiftSummary ? { shift: shiftSummary } : {}),
1164
+ ...(judge && judgeSummary
1165
+ ? {
1166
+ judge: judgeSummary,
1167
+ judge_config: {
1168
+ model: judge.model,
1169
+ runs: judge.runs,
1170
+ policy_sha256: judge.policy_sha256,
1171
+ ...(judgeConfig?.policyFile ? { policy_file: resolve(judgeConfig.policyFile) } : {}),
1172
+ },
1173
+ }
1174
+ : {}),
1175
+ };
1176
+ if (opts.jsonFile) {
1177
+ const jf = resolve(opts.jsonFile);
1178
+ mkdirSync(dirname(jf), { recursive: true });
1179
+ writeFileSync(jf, `${JSON.stringify(jsonPayload, null, 2)}\n`);
1180
+ }
1181
+ if (opts.json) {
1182
+ const delivered = await new Promise((done) => process.stdout.write(`${JSON.stringify(jsonPayload, null, 2)}\n`, (err) => done(!err)));
1183
+ if (!delivered)
1184
+ log("passk: the results could not be written: stdout is gone (--json-file keeps a copy)");
1185
+ }
1186
+ else if (!opts.quiet) {
1187
+ console.log("");
1188
+ console.log("run agent verdict money diff");
1189
+ for (const r of records) {
1190
+ const agentCol = r.timed_out
1191
+ ? "TIMEOUT"
1192
+ : r.agent_exit !== null
1193
+ ? `exit ${r.agent_exit}`
1194
+ : (r.agent_signal ?? "not run");
1195
+ const graded = isGraded(r);
1196
+ const verdict = r.status === "passed" ? "PASS" : r.status === "failed" ? "FAIL" : "ERROR";
1197
+ console.log(`${String(r.run).padStart(3)} ${agentCol.padEnd(8)} ${verdict.padEnd(7)} ${(graded ? moneyLine(r.money) : "—").padEnd(39)} ${graded ? countsLine(r.summary) : "—"}`);
1198
+ if (!graded) {
1199
+ console.log(` ✗ ${r.status}: ${r.error ?? ""}`);
1200
+ if (r.evidence)
1201
+ console.log(` evidence: ${r.evidence}`);
1202
+ }
1203
+ if (r.cleanup_error)
1204
+ console.log(` ⚠ world ${r.world_id} was not destroyed: ${r.cleanup_error}`);
1205
+ for (const e of r.expectations.filter((e) => !e.pass)) {
1206
+ const note = e.absent
1207
+ ? " — the diff has no such group or currency"
1208
+ : e.missing
1209
+ ? " — the path resolved to nothing (a misspelled field or an index past the end?)"
1210
+ : "";
1211
+ console.log(` ✗ ${e.expr} (actual ${JSON.stringify(e.actual)}${note})`);
1212
+ }
1213
+ if (r.shift)
1214
+ for (const line of shiftLines(r.shift))
1215
+ console.log(` ${line}`);
1216
+ if (r.judge)
1217
+ for (const line of judgeLines(r.judge, r.shift?.currency ?? rubric?.currency))
1218
+ console.log(` ${line}`);
1219
+ if (r.judge_error)
1220
+ console.log(` ⚖ judge failed: ${r.judge_error}`);
1221
+ }
1222
+ console.log("");
1223
+ const verdict = allPassed ? "PASS" : interrupted ? "INTERRUPTED" : `FAIL (needs ${opts.runs}/${opts.runs})`;
1224
+ console.log(`pass^k: ${passed}/${opts.runs} passed — ${verdict}${ungraded ? ` · ${ungraded} run(s) could not be graded` : ""}`);
1225
+ if (interrupted)
1226
+ console.log(`interrupted by ${interrupted} after ${records.length} of ${opts.runs} run(s)`);
1227
+ if (shiftSummary && shiftSummary.runs === 0) {
1228
+ console.log(`per-ticket accuracy: no run was graded (${shiftSummary.tickets} tickets × 0 runs) — nothing to estimate`);
1229
+ }
1230
+ else if (shiftSummary) {
1231
+ console.log(`per-ticket accuracy p̄=${shiftSummary.p_mean.toFixed(3)} over ${shiftSummary.tickets} tickets × ${shiftSummary.runs} run(s) · ` +
1232
+ `predicted flawless-shift rate p̄^${shiftSummary.tickets}=${shiftSummary.predicted_pass_rate.toFixed(3)} · observed ${shiftSummary.whole_shift_passes}/${shiftSummary.runs}`);
1233
+ }
1234
+ if (judge && judgeSummary)
1235
+ console.log(judgeSummaryLine(judge.model, judgeSummary, rubric?.currency));
1236
+ if (judge && !judgeSummary)
1237
+ console.log(`judge (advisory, ${judge.model}): no run was judged — see each run's judge line`);
1238
+ if (tasks) {
1239
+ const ct = jsonPayload.tasks?.crash_test;
1240
+ if (ct)
1241
+ console.log(`crash test: ${ct.flawless_runs}/${ct.runs} flawless run(s) · ${money(ct.dollars_wrong_mean, rubric?.currency)} wrong per run${ct.worst_task ? ` · worst task ${ct.worst_task.label} (${ct.worst_task.failed_runs}/${ct.runs} failed)${ct.worst_task.replay ? ` — replay: ${ct.worst_task.replay}` : ""}` : ""}`);
1242
+ }
1243
+ if (opts.keepWorlds) {
1244
+ const kept = records.flatMap((r) => r.world_id ? [r.world_id] : (r.worlds ?? []).flatMap((w) => (w.world_id ? [w.world_id] : [])));
1245
+ if (kept.length)
1246
+ console.log(`worlds kept for post-mortem: ${kept.join(", ")}`);
1247
+ }
1248
+ if (opts.contractLines !== false) {
1249
+ console.log(`status: ${contract.status} — ${contract.reason}`);
1250
+ console.log(`next: ${contract.next_action}`);
1251
+ }
1252
+ }
1253
+ if (interrupted)
1254
+ throw new PasskInterrupted(interrupted);
1255
+ return contract.exit_code;
1256
+ }