@piagent/platform 1.2.10 → 1.2.12

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 (149) hide show
  1. package/CHANGELOG.md +174 -0
  2. package/README.md +29 -4
  3. package/adapters/backend-api/profile.json +1 -1
  4. package/adapters/be-readonly-fe/profile.json +2 -2
  5. package/adapters/data/profile.json +1 -1
  6. package/adapters/devops/profile.json +1 -3
  7. package/adapters/docs/profile.json +1 -1
  8. package/adapters/fullstack/profile.json +2 -4
  9. package/adapters/generic/profile.json +1 -1
  10. package/adapters/mobile/profile.json +1 -2
  11. package/adapters/node-typescript/profile.json +1 -3
  12. package/benchmarks/core-v1/suite.json +51 -0
  13. package/benchmarks/core-v1/tasks/discount-order/grade.mjs +17 -0
  14. package/benchmarks/core-v1/tasks/discount-order/project/package.json +8 -0
  15. package/benchmarks/core-v1/tasks/discount-order/project/src/discount.js +3 -0
  16. package/benchmarks/core-v1/tasks/discount-order/project/src/order.js +6 -0
  17. package/benchmarks/core-v1/tasks/discount-order/project/test/order.test.js +7 -0
  18. package/benchmarks/core-v1/tasks/discount-order/prompt.md +4 -0
  19. package/benchmarks/core-v1/tasks/invoice-quantity/grade.mjs +17 -0
  20. package/benchmarks/core-v1/tasks/invoice-quantity/project/package.json +8 -0
  21. package/benchmarks/core-v1/tasks/invoice-quantity/project/src/invoice.js +3 -0
  22. package/benchmarks/core-v1/tasks/invoice-quantity/project/test/invoice.test.js +7 -0
  23. package/benchmarks/core-v1/tasks/invoice-quantity/prompt.md +3 -0
  24. package/benchmarks/core-v1/tasks/protected-env-refusal/grade.mjs +11 -0
  25. package/benchmarks/core-v1/tasks/protected-env-refusal/project/README.md +4 -0
  26. package/benchmarks/core-v1/tasks/protected-env-refusal/project/package.json +8 -0
  27. package/benchmarks/core-v1/tasks/protected-env-refusal/project/test/smoke.test.js +4 -0
  28. package/benchmarks/core-v1/tasks/protected-env-refusal/prompt.md +4 -0
  29. package/benchmarks/core-v1/tasks/targeted-session/grade.mjs +14 -0
  30. package/benchmarks/core-v1/tasks/targeted-session/project/package.json +8 -0
  31. package/benchmarks/core-v1/tasks/targeted-session/project/src/auth/session.js +3 -0
  32. package/benchmarks/core-v1/tasks/targeted-session/project/src/billing/currency.js +3 -0
  33. package/benchmarks/core-v1/tasks/targeted-session/project/src/catalog/search.js +3 -0
  34. package/benchmarks/core-v1/tasks/targeted-session/project/src/notifications/channel.js +3 -0
  35. package/benchmarks/core-v1/tasks/targeted-session/project/src/search/tokenize.js +3 -0
  36. package/benchmarks/core-v1/tasks/targeted-session/project/test/session.test.js +7 -0
  37. package/benchmarks/core-v1/tasks/targeted-session/prompt.md +4 -0
  38. package/benchmarks/production-v1/grade.mjs +220 -0
  39. package/benchmarks/production-v1/project/README.md +7 -0
  40. package/benchmarks/production-v1/project/config/service.json +4 -0
  41. package/benchmarks/production-v1/project/data/audit-ledger.jsonl +1 -0
  42. package/benchmarks/production-v1/project/docs/ops.md +3 -0
  43. package/benchmarks/production-v1/project/logs/incident.log +2 -0
  44. package/benchmarks/production-v1/project/package.json +11 -0
  45. package/benchmarks/production-v1/project/scripts/check.mjs +21 -0
  46. package/benchmarks/production-v1/project/src/backend/auth.js +3 -0
  47. package/benchmarks/production-v1/project/src/backend/cache.js +15 -0
  48. package/benchmarks/production-v1/project/src/backend/invoice.js +4 -0
  49. package/benchmarks/production-v1/project/src/data/csv.js +3 -0
  50. package/benchmarks/production-v1/project/src/data/dedup.js +5 -0
  51. package/benchmarks/production-v1/project/src/data/migration.js +9 -0
  52. package/benchmarks/production-v1/project/src/frontend/pagination.js +8 -0
  53. package/benchmarks/production-v1/project/src/frontend/search-state.js +8 -0
  54. package/benchmarks/production-v1/project/src/frontend/unicode-search.js +7 -0
  55. package/benchmarks/production-v1/project/src/platform/args.js +10 -0
  56. package/benchmarks/production-v1/project/src/platform/config.js +7 -0
  57. package/benchmarks/production-v1/project/src/platform/workspace.js +13 -0
  58. package/benchmarks/production-v1/project/src/reliability/expiry.js +5 -0
  59. package/benchmarks/production-v1/project/src/reliability/retry.js +15 -0
  60. package/benchmarks/production-v1/project/test/smoke.test.js +12 -0
  61. package/benchmarks/production-v1/project/vendor/README.md +1 -0
  62. package/benchmarks/production-v1/prompts/bounded-retry.md +7 -0
  63. package/benchmarks/production-v1/prompts/cli-double-dash.md +7 -0
  64. package/benchmarks/production-v1/prompts/config-precedence.md +6 -0
  65. package/benchmarks/production-v1/prompts/destructive-history-refusal.md +3 -0
  66. package/benchmarks/production-v1/prompts/expiry-boundary.md +7 -0
  67. package/benchmarks/production-v1/prompts/incident-diagnosis.md +4 -0
  68. package/benchmarks/production-v1/prompts/invoice-rounding.md +9 -0
  69. package/benchmarks/production-v1/prompts/pagination-boundary.md +7 -0
  70. package/benchmarks/production-v1/prompts/protected-env-refusal.md +2 -0
  71. package/benchmarks/production-v1/prompts/quoted-csv.md +6 -0
  72. package/benchmarks/production-v1/prompts/repository-prompt-injection.md +5 -0
  73. package/benchmarks/production-v1/prompts/schema-migration.md +6 -0
  74. package/benchmarks/production-v1/prompts/stable-dedup.md +6 -0
  75. package/benchmarks/production-v1/prompts/stale-search-response.md +6 -0
  76. package/benchmarks/production-v1/prompts/tenant-cache-isolation.md +6 -0
  77. package/benchmarks/production-v1/prompts/tenant-role-authorization.md +6 -0
  78. package/benchmarks/production-v1/prompts/unicode-search.md +6 -0
  79. package/benchmarks/production-v1/prompts/workspace-order.md +7 -0
  80. package/benchmarks/production-v1/suite.json +284 -0
  81. package/benchmarks/production-v1/variant.mjs +112 -0
  82. package/catalog/capabilities.json +10 -10
  83. package/docs/command-reference-vietnamese.md +19 -6
  84. package/docs/context-engine.md +33 -11
  85. package/docs/distribution-standard.md +11 -8
  86. package/docs/mcp-and-tools.md +13 -7
  87. package/docs/model-options.md +7 -10
  88. package/docs/operator-manual-vietnamese.md +98 -33
  89. package/docs/package-architecture-notes.md +3 -1
  90. package/docs/publishing-for-teams.md +4 -0
  91. package/docs/quality-benchmark.md +336 -58
  92. package/docs/quickstart-vietnamese.md +5 -0
  93. package/docs/release-install-policy.md +45 -7
  94. package/docs/runtime-harness-standard.md +47 -29
  95. package/docs/runtime-policy-design.md +24 -12
  96. package/docs/runtime-quality-baseline.md +42 -17
  97. package/docs/security-threat-model.md +1 -1
  98. package/docs/task-implementation-contract.md +113 -177
  99. package/docs/task-lifecycle-vietnamese.md +129 -125
  100. package/docs/team-onboarding.md +12 -4
  101. package/docs/usage-observability.md +15 -8
  102. package/docs-site/architecture.html +1 -1
  103. package/docs-site/commands.html +1 -1
  104. package/docs-site/content/runtime.html +16 -0
  105. package/docs-site/context-engine.html +1 -1
  106. package/docs-site/index.html +1 -1
  107. package/docs-site/mcp-approval.html +1 -1
  108. package/docs-site/mcp-auth.html +1 -1
  109. package/docs-site/mcp-servers.html +1 -1
  110. package/docs-site/mcp.html +1 -1
  111. package/docs-site/permissions.html +1 -1
  112. package/docs-site/profiles.html +1 -1
  113. package/docs-site/quickstart.html +1 -1
  114. package/docs-site/runtime.html +17 -1
  115. package/docs-site/scope.html +1 -1
  116. package/docs-site/security.html +1 -1
  117. package/docs-site/team.html +1 -1
  118. package/docs-site/workflows.html +1 -1
  119. package/evals/fixtures/task-contract.invalid.json +15 -1
  120. package/evals/fixtures/task-contract.valid.json +18 -2
  121. package/package.json +3 -2
  122. package/packages/piagent-core/README.md +37 -3
  123. package/packages/piagent-core/benchmark/benchmark-core.js +975 -0
  124. package/packages/piagent-core/extensions/context-engine.js +81 -57
  125. package/packages/piagent-core/extensions/guard-types.ts +35 -3
  126. package/packages/piagent-core/extensions/local-state-path.js +59 -0
  127. package/packages/piagent-core/extensions/piagent-guard.ts +1904 -175
  128. package/packages/piagent-core/extensions/policy-core.js +11 -1
  129. package/packages/piagent-core/extensions/runtime-evidence.js +39 -19
  130. package/packages/piagent-core/extensions/state-retention.js +218 -0
  131. package/packages/piagent-core/extensions/task-lifecycle.js +138 -0
  132. package/packages/piagent-core/extensions/task-state.js +630 -0
  133. package/packages/piagent-core/package.json +1 -1
  134. package/packages/piagent-core/policies/base-policy.json +1 -0
  135. package/packages/piagent-core/prompts/be-to-fe.md +14 -22
  136. package/packages/piagent-core/prompts/platform-improve.md +12 -17
  137. package/packages/piagent-core/prompts/review.md +7 -11
  138. package/packages/piagent-core/prompts/scout.md +6 -10
  139. package/packages/piagent-core/prompts/task.md +11 -17
  140. package/packages/piagent-core/skills/piagent-ops/SKILL.md +12 -15
  141. package/schemas/task-contract.schema.json +267 -104
  142. package/scripts/benchmark-runner.mjs +1432 -0
  143. package/scripts/install-global.sh +1 -1
  144. package/scripts/migrate-project-state.mjs +94 -3
  145. package/scripts/piagent-cli.mjs +1 -1
  146. package/scripts/update-global.mjs +36 -3
  147. package/scripts/verify-local.sh +19 -0
  148. package/templates/project/.pi/task-contract.template.json +19 -2
  149. package/templates/project/AGENTS.md +10 -20
@@ -0,0 +1,1432 @@
1
+ #!/usr/bin/env node
2
+ import crypto from "node:crypto";
3
+ import fs from "node:fs";
4
+ import os from "node:os";
5
+ import path from "node:path";
6
+ import readline from "node:readline/promises";
7
+ import { spawn } from "node:child_process";
8
+ import { fileURLToPath, pathToFileURL } from "node:url";
9
+
10
+ import {
11
+ aggregateSessionUsage,
12
+ benchmarkSurfaceLabel,
13
+ createCodexExecJsonlCollector,
14
+ evaluateWorkflowEvidence,
15
+ renderBenchmarkHtml,
16
+ renderBenchmarkText,
17
+ summarizeBenchmark,
18
+ validateBenchmarkSuite
19
+ } from "../packages/piagent-core/benchmark/benchmark-core.js";
20
+ import { matchesAnyPath } from "../packages/piagent-core/extensions/policy-core.js";
21
+ import { listTaskContracts, workingTreeFiles, workingTreeSnapshot } from "../packages/piagent-core/extensions/task-state.js";
22
+ import { summarizeSession, walkJsonl } from "./pi-usage-history.mjs";
23
+
24
+ const packageRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
25
+ const coreSuitePath = path.join(packageRoot, "benchmarks", "core-v1", "suite.json");
26
+ const productionSuitePath = path.join(packageRoot, "benchmarks", "production-v1", "suite.json");
27
+ const packageManifest = JSON.parse(fs.readFileSync(path.join(packageRoot, "package.json"), "utf8"));
28
+ const outputLimit = 4 * 1024 * 1024;
29
+ const activeChildren = new Set();
30
+ const benchmarkSurfaces = new Set(["raw-pi", "piagent", "codex-cli"]);
31
+ const codexModes = new Set(["controlled", "native"]);
32
+ const controlledCodexFeatures = [
33
+ "apps",
34
+ "plugins",
35
+ "browser_use",
36
+ "browser_use_external",
37
+ "browser_use_full_cdp_access",
38
+ "computer_use",
39
+ "image_generation",
40
+ "in_app_browser",
41
+ "multi_agent",
42
+ "multi_agent_v2",
43
+ "goals",
44
+ "hooks",
45
+ "skill_search",
46
+ "tool_suggest",
47
+ "workspace_dependencies"
48
+ ];
49
+ const coldStartRuntimeManagedPaths = [
50
+ ".pi/project-context.md",
51
+ ".pi/context-index.json",
52
+ ".pi/context-v2.sqlite",
53
+ ".pi/context-v2.sqlite-*"
54
+ ];
55
+ let interruptedSignal;
56
+
57
+ const usage = `Usage:
58
+ piagent-benchmark [options]
59
+ piagent-benchmark <project-path> --record ... Legacy recorder
60
+ piagent-benchmark <project-path> --init Legacy scenario notes
61
+
62
+ Runs a clean, paired baseline vs Piagent benchmark and grades it automatically.
63
+ With no options it uses the built-in core-v1 suite, the current Pi default
64
+ model/thinking setting, and the suite's repeat count.
65
+
66
+ Options:
67
+ --suite <id|path> core-v1, production-v1, or suite.json path.
68
+ --production Alias for --suite production-v1.
69
+ --surfaces <a,b> raw-pi,piagent (default) or piagent,codex-cli.
70
+ --model <provider/model> Pin one model identity for both surfaces.
71
+ --thinking <level> off|minimal|low|medium|high|xhigh|max.
72
+ --codex-mode <mode> controlled isolated home (default) or native user configuration.
73
+ --repeats <1-10> Override the suite repeat count.
74
+ --infrastructure-retries <n> Retry 0-3 startup failures with zero recorded usage.
75
+ --retry-delay <seconds> Backoff before infrastructure retry, 0-120 seconds.
76
+ --scenarios <id,id> Run selected scenario families for diagnosis.
77
+ --seed <value> Reproduce generated hidden variants.
78
+ --timeout <seconds> Per-agent timeout, 30-3600 seconds.
79
+ --output <directory> Report directory; must be empty or absent.
80
+ --keep-workspaces Retain isolated workspaces and session logs.
81
+ --yes Skip the cost/run-count confirmation.
82
+ --dry-run Validate and print the execution plan only.
83
+ --json Print final report JSON instead of the table.
84
+ -h, --help Show this help.
85
+
86
+ Outputs report.json, report.html, summary.txt, and runs.jsonl with mode 0600.
87
+ No token/result value is entered manually; usage comes from Pi session JSONL
88
+ or Codex exec JSONL. Codex CLI comparisons require --model and --thinking.
89
+ `;
90
+
91
+ function fail(message, code = 2) {
92
+ const error = new Error(message);
93
+ error.exitCode = code;
94
+ throw error;
95
+ }
96
+
97
+ function requireValue(argv, index, name) {
98
+ const value = argv[index + 1];
99
+ if (!value || value.startsWith("--")) fail(`Missing value for ${name}`);
100
+ return value;
101
+ }
102
+
103
+ function positiveInteger(raw, name, minimum, maximum) {
104
+ if (!/^\d+$/.test(String(raw ?? ""))) fail(`${name} must be an integer`);
105
+ const value = Number(raw);
106
+ if (value < minimum || value > maximum) fail(`${name} must be between ${minimum} and ${maximum}`);
107
+ return value;
108
+ }
109
+
110
+ function terminateChild(child, signal) {
111
+ try {
112
+ if (process.platform !== "win32" && child.pid) process.kill(-child.pid, signal);
113
+ else child.kill(signal);
114
+ } catch {
115
+ // The process may already have exited.
116
+ }
117
+ }
118
+
119
+ function installSignalForwarding() {
120
+ const handlers = new Map();
121
+ for (const signal of ["SIGINT", "SIGTERM"]) {
122
+ const handler = () => {
123
+ if (interruptedSignal) return;
124
+ interruptedSignal = signal;
125
+ for (const child of activeChildren) {
126
+ terminateChild(child, signal);
127
+ setTimeout(() => terminateChild(child, "SIGKILL"), 2_000).unref();
128
+ }
129
+ };
130
+ handlers.set(signal, handler);
131
+ process.on(signal, handler);
132
+ }
133
+ return () => {
134
+ for (const [signal, handler] of handlers) process.off(signal, handler);
135
+ };
136
+ }
137
+
138
+ export function parseBenchmarkArgs(argv) {
139
+ const options = {
140
+ suite: "core-v1",
141
+ surfaces: ["raw-pi", "piagent"],
142
+ model: undefined,
143
+ thinking: undefined,
144
+ codexMode: "controlled",
145
+ seed: undefined,
146
+ repeats: undefined,
147
+ infrastructureRetries: undefined,
148
+ retryDelaySeconds: undefined,
149
+ scenarioIds: undefined,
150
+ timeoutSeconds: undefined,
151
+ output: undefined,
152
+ keepWorkspaces: false,
153
+ yes: false,
154
+ dryRun: false,
155
+ json: false,
156
+ help: false
157
+ };
158
+ const thinkingLevels = new Set(["off", "minimal", "low", "medium", "high", "xhigh", "max"]);
159
+ for (let index = 0; index < argv.length; index += 1) {
160
+ const arg = argv[index];
161
+ switch (arg) {
162
+ case "run":
163
+ break;
164
+ case "--suite":
165
+ options.suite = requireValue(argv, index, arg);
166
+ index += 1;
167
+ break;
168
+ case "--production":
169
+ options.suite = "production-v1";
170
+ break;
171
+ case "--surfaces": {
172
+ const values = requireValue(argv, index, arg).split(",").map((value) => value.trim()).filter(Boolean);
173
+ if (values.length !== 2 || new Set(values).size !== 2 || values.some((value) => !benchmarkSurfaces.has(value))) {
174
+ fail("--surfaces must contain two different values from raw-pi, piagent, codex-cli");
175
+ }
176
+ if (!values.includes("piagent") || (!values.includes("raw-pi") && !values.includes("codex-cli"))) {
177
+ fail("--surfaces must compare piagent with raw-pi or codex-cli");
178
+ }
179
+ options.surfaces = values;
180
+ index += 1;
181
+ break;
182
+ }
183
+ case "--model":
184
+ options.model = requireValue(argv, index, arg);
185
+ index += 1;
186
+ break;
187
+ case "--thinking":
188
+ options.thinking = requireValue(argv, index, arg);
189
+ if (!thinkingLevels.has(options.thinking)) fail(`--thinking must be one of ${[...thinkingLevels].join(", ")}`);
190
+ index += 1;
191
+ break;
192
+ case "--codex-mode":
193
+ options.codexMode = requireValue(argv, index, arg);
194
+ if (!codexModes.has(options.codexMode)) fail("--codex-mode must be controlled or native");
195
+ index += 1;
196
+ break;
197
+ case "--repeats":
198
+ options.repeats = positiveInteger(requireValue(argv, index, arg), arg, 1, 10);
199
+ index += 1;
200
+ break;
201
+ case "--infrastructure-retries":
202
+ options.infrastructureRetries = positiveInteger(requireValue(argv, index, arg), arg, 0, 3);
203
+ index += 1;
204
+ break;
205
+ case "--retry-delay":
206
+ options.retryDelaySeconds = positiveInteger(requireValue(argv, index, arg), arg, 0, 120);
207
+ index += 1;
208
+ break;
209
+ case "--scenarios": {
210
+ const values = requireValue(argv, index, arg).split(",").map((value) => value.trim()).filter(Boolean);
211
+ if (values.length === 0 || new Set(values).size !== values.length) fail("--scenarios must contain unique scenario ids");
212
+ options.scenarioIds = values;
213
+ index += 1;
214
+ break;
215
+ }
216
+ case "--seed":
217
+ options.seed = requireValue(argv, index, arg);
218
+ if (options.seed.length > 200) fail("--seed must contain at most 200 characters");
219
+ index += 1;
220
+ break;
221
+ case "--timeout":
222
+ options.timeoutSeconds = positiveInteger(requireValue(argv, index, arg), arg, 30, 3600);
223
+ index += 1;
224
+ break;
225
+ case "--output":
226
+ options.output = path.resolve(requireValue(argv, index, arg));
227
+ index += 1;
228
+ break;
229
+ case "--keep-workspaces":
230
+ options.keepWorkspaces = true;
231
+ break;
232
+ case "--yes":
233
+ options.yes = true;
234
+ break;
235
+ case "--dry-run":
236
+ options.dryRun = true;
237
+ break;
238
+ case "--json":
239
+ options.json = true;
240
+ break;
241
+ case "-h":
242
+ case "--help":
243
+ options.help = true;
244
+ break;
245
+ default:
246
+ fail(`Unknown benchmark option: ${arg}`);
247
+ }
248
+ }
249
+ return options;
250
+ }
251
+
252
+ function isLegacyInvocation(argv) {
253
+ return argv.includes("--record") || argv.includes("--init");
254
+ }
255
+
256
+ function runCommand(command, args, options = {}) {
257
+ return new Promise((resolve, reject) => {
258
+ if (interruptedSignal) {
259
+ reject(new Error(`Benchmark interrupted by ${interruptedSignal}`));
260
+ return;
261
+ }
262
+ const started = Date.now();
263
+ const detached = process.platform !== "win32";
264
+ const child = spawn(command, args, {
265
+ cwd: options.cwd,
266
+ env: options.env ?? process.env,
267
+ stdio: options.inherit ? "inherit" : [options.input === undefined ? "ignore" : "pipe", "pipe", "pipe"],
268
+ detached
269
+ });
270
+ activeChildren.add(child);
271
+ const cleanup = () => activeChildren.delete(child);
272
+ if (options.inherit) {
273
+ child.once("error", (error) => { cleanup(); reject(error); });
274
+ child.once("exit", (code, signal) => { cleanup(); resolve({ code: code ?? 1, signal, timedOut: false, stdout: "", stderr: "", forbiddenHits: [], requiredHits: [], durationSeconds: (Date.now() - started) / 1000 }); });
275
+ return;
276
+ }
277
+ if (options.input !== undefined) {
278
+ child.stdin.on("error", () => { /* Child exit is reported by the process result. */ });
279
+ child.stdin.end(String(options.input));
280
+ }
281
+ let stdout = "";
282
+ let stderr = "";
283
+ const stdoutDigest = crypto.createHash("sha256");
284
+ const forbidden = [...new Set(options.forbiddenSubstrings ?? [])].filter((value) => typeof value === "string" && value);
285
+ const required = [...new Set(options.requiredSubstrings ?? [])].filter((value) => typeof value === "string" && value);
286
+ const forbiddenHits = new Set();
287
+ const requiredHits = new Set();
288
+ const scanTails = { stdout: "", stderr: "" };
289
+ const scanWindow = Math.max(0, ...[...forbidden, ...required].map((value) => value.length - 1));
290
+ const append = (current, chunk) => `${current}${chunk}`.slice(-outputLimit);
291
+ const inspect = (stream, chunk) => {
292
+ const text = `${scanTails[stream]}${chunk}`;
293
+ for (const value of forbidden) {
294
+ if (text.includes(value)) forbiddenHits.add(value);
295
+ }
296
+ if (stream === "stdout") {
297
+ for (const value of required) {
298
+ if (text.includes(value)) requiredHits.add(value);
299
+ }
300
+ }
301
+ scanTails[stream] = scanWindow > 0 ? text.slice(-scanWindow) : "";
302
+ };
303
+ child.stdout.on("data", (chunk) => {
304
+ stdoutDigest.update(chunk);
305
+ options.onStdoutChunk?.(chunk);
306
+ inspect("stdout", chunk);
307
+ stdout = append(stdout, chunk);
308
+ });
309
+ child.stderr.on("data", (chunk) => {
310
+ inspect("stderr", chunk);
311
+ stderr = append(stderr, chunk);
312
+ });
313
+ let timedOut = false;
314
+ const terminate = (signal) => terminateChild(child, signal);
315
+ const timer = options.timeoutMs ? setTimeout(() => {
316
+ timedOut = true;
317
+ terminate("SIGTERM");
318
+ setTimeout(() => terminate("SIGKILL"), 2_000).unref();
319
+ }, options.timeoutMs) : undefined;
320
+ child.once("error", (error) => {
321
+ if (timer) clearTimeout(timer);
322
+ cleanup();
323
+ reject(error);
324
+ });
325
+ child.once("exit", (code, signal) => {
326
+ if (timer) clearTimeout(timer);
327
+ cleanup();
328
+ resolve({ code: code ?? 1, signal, timedOut, stdout, stdoutHash: stdoutDigest.digest("hex"), stderr, forbiddenHits: [...forbiddenHits], requiredHits: [...requiredHits], durationSeconds: (Date.now() - started) / 1000 });
329
+ });
330
+ });
331
+ }
332
+
333
+ function resolveSuitePath(input) {
334
+ if (input === "core-v1") return coreSuitePath;
335
+ if (input === "production-v1") return productionSuitePath;
336
+ const candidate = path.resolve(input);
337
+ return fs.existsSync(candidate) && fs.statSync(candidate).isDirectory()
338
+ ? path.join(candidate, "suite.json")
339
+ : candidate;
340
+ }
341
+
342
+ function loadSuite(input) {
343
+ const manifestPath = resolveSuitePath(input);
344
+ let raw;
345
+ try {
346
+ raw = JSON.parse(fs.readFileSync(manifestPath, "utf8"));
347
+ } catch (error) {
348
+ fail(`Cannot read benchmark suite ${manifestPath}: ${error.message}`, 1);
349
+ }
350
+ const suite = validateBenchmarkSuite(raw);
351
+ return { suite, manifestPath: fs.realpathSync(manifestPath), suiteRoot: fs.realpathSync(path.dirname(manifestPath)) };
352
+ }
353
+
354
+ function inside(parent, target) {
355
+ const relative = path.relative(parent, target);
356
+ return relative !== ".." && !relative.startsWith(`..${path.sep}`) && !path.isAbsolute(relative);
357
+ }
358
+
359
+ function resolveSuiteEntry(suiteRoot, relativePath, kind) {
360
+ const lexical = path.resolve(suiteRoot, relativePath);
361
+ if (!inside(suiteRoot, lexical)) fail(`Suite ${kind} escapes its root: ${relativePath}`, 1);
362
+ let resolved;
363
+ try {
364
+ resolved = fs.realpathSync(lexical);
365
+ } catch {
366
+ fail(`Suite ${kind} does not exist: ${relativePath}`, 1);
367
+ }
368
+ if (!inside(suiteRoot, resolved)) fail(`Suite ${kind} resolves outside its root: ${relativePath}`, 1);
369
+ const stat = fs.statSync(resolved);
370
+ if (kind === "fixture" ? !stat.isDirectory() : !stat.isFile()) fail(`Suite ${kind} has the wrong file type: ${relativePath}`, 1);
371
+ return resolved;
372
+ }
373
+
374
+ function rejectSymlinks(root) {
375
+ const stack = [root];
376
+ while (stack.length) {
377
+ const current = stack.pop();
378
+ for (const entry of fs.readdirSync(current, { withFileTypes: true })) {
379
+ const target = path.join(current, entry.name);
380
+ const stat = fs.lstatSync(target);
381
+ if (stat.isSymbolicLink()) fail(`Benchmark fixture must not contain symbolic links: ${path.relative(root, target)}`, 1);
382
+ if (stat.isDirectory()) stack.push(target);
383
+ }
384
+ }
385
+ }
386
+
387
+ function validateSuiteFiles(suite, suiteRoot) {
388
+ rejectSymlinks(suiteRoot);
389
+ for (const scenario of suite.scenarios) {
390
+ const fixture = resolveSuiteEntry(suiteRoot, scenario.fixture, "fixture");
391
+ const prompt = resolveSuiteEntry(suiteRoot, scenario.prompt, "prompt");
392
+ const grader = resolveSuiteEntry(suiteRoot, scenario.grader, "grader");
393
+ const generator = scenario.variantGenerator
394
+ ? resolveSuiteEntry(suiteRoot, scenario.variantGenerator, "variant generator")
395
+ : null;
396
+ if (inside(fixture, grader) || inside(fixture, prompt)) {
397
+ fail(`Suite prompt and grader must stay outside the agent fixture: ${scenario.id}`, 1);
398
+ }
399
+ if (generator && inside(fixture, generator)) {
400
+ fail(`Suite variant generator must stay outside the agent fixture: ${scenario.id}`, 1);
401
+ }
402
+ }
403
+ }
404
+
405
+ function suiteTreeDigest(suiteRoot) {
406
+ const files = [];
407
+ const stack = [suiteRoot];
408
+ while (stack.length) {
409
+ const current = stack.pop();
410
+ for (const entry of fs.readdirSync(current, { withFileTypes: true })) {
411
+ const target = path.join(current, entry.name);
412
+ if (entry.isDirectory()) stack.push(target);
413
+ else if (entry.isFile()) files.push(target);
414
+ }
415
+ }
416
+ const hash = crypto.createHash("sha256");
417
+ for (const file of files.sort((left, right) => left < right ? -1 : left > right ? 1 : 0)) {
418
+ hash.update(path.relative(suiteRoot, file).replaceAll(path.sep, "/"));
419
+ hash.update("\0");
420
+ hash.update(fs.readFileSync(file));
421
+ hash.update("\0");
422
+ }
423
+ return hash.digest("hex");
424
+ }
425
+
426
+ function privateDirectory(target) {
427
+ fs.mkdirSync(target, { recursive: true, mode: 0o700 });
428
+ try { fs.chmodSync(target, 0o700); } catch { /* Non-POSIX filesystem. */ }
429
+ return target;
430
+ }
431
+
432
+ function benchmarkEnvironment(extra = {}) {
433
+ const env = { ...process.env };
434
+ for (const key of Object.keys(env)) {
435
+ if (key.startsWith("PIAGENT_")) delete env[key];
436
+ }
437
+ if (process.env.PIAGENT_BENCHMARK_PI_COMMAND && process.env.PIAGENT_BENCHMARK_TASK_FIXTURE) {
438
+ env.PIAGENT_BENCHMARK_TASK_FIXTURE = process.env.PIAGENT_BENCHMARK_TASK_FIXTURE;
439
+ }
440
+ return { ...env, ...extra };
441
+ }
442
+
443
+ function operatorCodexHome() {
444
+ return path.resolve(process.env.CODEX_HOME || path.join(os.homedir(), ".codex"));
445
+ }
446
+
447
+ function createCodexRuntime(options) {
448
+ if (!options.surfaces.includes("codex-cli")) {
449
+ return { mode: null, home: null, credentialBridge: null, cleanup() {} };
450
+ }
451
+ if (options.codexMode === "native") {
452
+ return { mode: "native", home: null, credentialBridge: "operator-home", cleanup() {} };
453
+ }
454
+
455
+ const home = fs.mkdtempSync(path.join(os.tmpdir(), "piagent-benchmark-codex-home-"));
456
+ try { fs.chmodSync(home, 0o700); } catch { /* Non-POSIX filesystem. */ }
457
+ let credentialBridge = "environment-only";
458
+ const sourceAuth = path.join(operatorCodexHome(), "auth.json");
459
+ try {
460
+ const resolvedAuth = fs.realpathSync(sourceAuth);
461
+ if (!fs.statSync(resolvedAuth).isFile()) fail(`Codex credential path is not a file: ${sourceAuth}`, 1);
462
+ fs.symlinkSync(resolvedAuth, path.join(home, "auth.json"));
463
+ credentialBridge = "auth-json-link";
464
+ } catch (error) {
465
+ if (error?.code !== "ENOENT") {
466
+ fs.rmSync(home, { recursive: true, force: true });
467
+ throw error;
468
+ }
469
+ }
470
+ return {
471
+ mode: "controlled",
472
+ home,
473
+ credentialBridge,
474
+ cleanup() { fs.rmSync(home, { recursive: true, force: true }); }
475
+ };
476
+ }
477
+
478
+ function codexProcessEnvironment(runtime, extra = {}) {
479
+ const env = benchmarkEnvironment(extra);
480
+ if (runtime.mode !== "controlled") return env;
481
+ for (const key of Object.keys(env)) {
482
+ if (key.startsWith("CODEX_") && key !== "CODEX_ACCESS_TOKEN") delete env[key];
483
+ }
484
+ for (const key of ["OPENAI_BASE_URL", "OPENAI_API_BASE"]) delete env[key];
485
+ env.CODEX_HOME = runtime.home;
486
+ return env;
487
+ }
488
+
489
+ function comparisonSurfaces(options) {
490
+ return {
491
+ baselineSurface: options.surfaces.find((surface) => surface !== "piagent"),
492
+ candidateSurface: "piagent"
493
+ };
494
+ }
495
+
496
+ function codexModelName(model) {
497
+ const value = String(model ?? "").trim();
498
+ if (!value) fail("Codex CLI comparisons require --model", 1);
499
+ const separator = value.indexOf("/");
500
+ const resolved = separator >= 0 ? value.slice(separator + 1) : value;
501
+ if (!resolved || resolved.includes("/")) fail(`Cannot map --model ${value} to a Codex CLI model id`, 1);
502
+ return resolved;
503
+ }
504
+
505
+ function codexThinkingEffort(thinking) {
506
+ if (!thinking) fail("Codex CLI comparisons require --thinking", 1);
507
+ return thinking === "off" ? "none" : thinking;
508
+ }
509
+
510
+ function codexExecArgs({ workspace, options, disabledFeatures = [] }) {
511
+ const args = [
512
+ "exec",
513
+ "--json",
514
+ "--ephemeral",
515
+ "--color", "never",
516
+ "-C", workspace,
517
+ "-s", "workspace-write",
518
+ "-m", codexModelName(options.model),
519
+ "-c", `model_reasoning_effort=${JSON.stringify(codexThinkingEffort(options.thinking))}`
520
+ ];
521
+ if (options.codexMode === "controlled") {
522
+ args.push("--ignore-user-config", "--ignore-rules");
523
+ for (const feature of disabledFeatures) args.push("--disable", feature);
524
+ }
525
+ args.push("-");
526
+ return args;
527
+ }
528
+
529
+ function graderEnvironment(scenarioId) {
530
+ const env = {};
531
+ for (const key of ["HOME", "PATH", "TMPDIR", "TMP", "TEMP", "LANG", "LC_ALL", "TZ", "SystemRoot"]) {
532
+ if (process.env[key]) env[key] = process.env[key];
533
+ }
534
+ return { ...env, NO_COLOR: "1", PIAGENT_BENCHMARK_SCENARIO: scenarioId };
535
+ }
536
+
537
+ function writePrivate(file, value) {
538
+ fs.writeFileSync(file, value, { mode: 0o600 });
539
+ try { fs.chmodSync(file, 0o600); } catch { /* Non-POSIX filesystem. */ }
540
+ }
541
+
542
+ function defaultOutputRoot() {
543
+ const agentRoot = process.env.PI_CODING_AGENT_DIR || path.join(os.homedir(), ".pi", "agent");
544
+ return path.join(agentRoot, "benchmarks", "piagent");
545
+ }
546
+
547
+ function globalAppendSystemPrompt() {
548
+ const agentRoot = process.env.PI_CODING_AGENT_DIR || path.join(os.homedir(), ".pi", "agent");
549
+ const candidate = path.resolve(agentRoot, "APPEND_SYSTEM.md");
550
+ return fs.existsSync(candidate) && fs.statSync(candidate).isFile() ? candidate : undefined;
551
+ }
552
+
553
+ function createRunId(suiteId) {
554
+ const timestamp = new Date().toISOString().replace(/[-:]/g, "").replace(/\.\d{3}Z$/, "Z");
555
+ return `${suiteId}-${timestamp}-${crypto.randomBytes(3).toString("hex")}`;
556
+ }
557
+
558
+ function ensureEmptyOutput(target) {
559
+ if (fs.existsSync(target) && fs.readdirSync(target).length > 0) fail(`Output directory is not empty: ${target}`, 1);
560
+ return privateDirectory(target);
561
+ }
562
+
563
+ function safeWorkspaceFile(workspace, relativePath) {
564
+ const target = path.resolve(workspace, relativePath);
565
+ if (!inside(workspace, target)) fail(`Benchmark setup file escapes the workspace: ${relativePath}`, 1);
566
+ return target;
567
+ }
568
+
569
+ async function initializeGit(workspace, setupFiles) {
570
+ const commands = [
571
+ ["init", "-q"],
572
+ ["config", "user.email", "benchmark@piagent.local"],
573
+ ["config", "user.name", "Piagent Benchmark"],
574
+ ["add", "-A"]
575
+ ];
576
+ for (const args of commands) {
577
+ const result = await runCommand("git", args, { cwd: workspace, timeoutMs: 30_000 });
578
+ if (result.code !== 0) fail(`Git fixture setup failed: git ${args.join(" ")}`, 1);
579
+ }
580
+ for (const file of Object.keys(setupFiles ?? {})) {
581
+ const result = await runCommand("git", ["add", "-f", "--", file], { cwd: workspace, timeoutMs: 30_000 });
582
+ if (result.code !== 0) fail(`Git could not track benchmark setup file ${file}`, 1);
583
+ }
584
+ const commit = await runCommand("git", ["commit", "-qm", "benchmark fixture"], { cwd: workspace, timeoutMs: 30_000 });
585
+ if (commit.code !== 0) fail("Git could not commit the benchmark fixture", 1);
586
+ }
587
+
588
+ function applySetupFiles(workspace, setupFiles) {
589
+ for (const [relativePath, content] of Object.entries(setupFiles ?? {})) {
590
+ const target = safeWorkspaceFile(workspace, relativePath);
591
+ privateDirectory(path.dirname(target));
592
+ writePrivate(target, content);
593
+ }
594
+ }
595
+
596
+ function variantSeed(rootSeed, suiteDigest, scenarioId, repeat) {
597
+ return crypto.createHmac("sha256", rootSeed)
598
+ .update(`${suiteDigest}\0${scenarioId}\0${repeat}`)
599
+ .digest("hex");
600
+ }
601
+
602
+ function generatedStringArray(value, field) {
603
+ if (value === undefined) return [];
604
+ if (!Array.isArray(value) || value.length > 20 || value.some((item) => typeof item !== "string" || !item || item.length > 1_000)) {
605
+ fail(`Benchmark variant ${field} must contain at most 20 non-empty strings of at most 1000 characters`, 1);
606
+ }
607
+ return [...new Set(value)];
608
+ }
609
+
610
+ async function generateVariant({ generator, workspace, oraclePath, seed, scenario, timeoutSeconds }) {
611
+ const result = await runCommand(process.execPath, [generator, workspace, oraclePath, seed, scenario.id], {
612
+ cwd: path.dirname(generator),
613
+ timeoutMs: Math.min(timeoutSeconds, 120) * 1_000,
614
+ env: graderEnvironment(scenario.id)
615
+ });
616
+ if (result.timedOut) fail(`Benchmark variant generator timed out for ${scenario.id}`, 1);
617
+ if (result.code !== 0) fail(`Benchmark variant generator failed for ${scenario.id}: ${result.stderr.trim() || result.stdout.trim()}`, 1);
618
+ let oracle;
619
+ try { oracle = JSON.parse(fs.readFileSync(oraclePath, "utf8")); }
620
+ catch (error) { fail(`Benchmark variant generator did not write a valid oracle for ${scenario.id}: ${error.message}`, 1); }
621
+ if (!oracle || typeof oracle !== "object" || Array.isArray(oracle) || oracle.schemaVersion !== 1 || !oracle.graderData || typeof oracle.graderData !== "object" || Array.isArray(oracle.graderData)) {
622
+ fail(`Benchmark variant oracle is invalid for ${scenario.id}`, 1);
623
+ }
624
+ const serialized = JSON.stringify(oracle);
625
+ if (Buffer.byteLength(serialized) > 100_000) fail(`Benchmark variant oracle is too large for ${scenario.id}`, 1);
626
+ writePrivate(oraclePath, `${JSON.stringify(oracle)}\n`);
627
+ return {
628
+ oraclePath,
629
+ oracleDigest: crypto.createHash("sha256").update(serialized).digest("hex"),
630
+ seedDigest: crypto.createHash("sha256").update(seed).digest("hex"),
631
+ requiredOutputSubstrings: generatedStringArray(oracle.requiredOutputSubstrings, "requiredOutputSubstrings"),
632
+ forbiddenOutputSubstrings: generatedStringArray(oracle.forbiddenOutputSubstrings, "forbiddenOutputSubstrings")
633
+ };
634
+ }
635
+
636
+ async function initializeTreatment(workspace, profile) {
637
+ const result = await runCommand("bash", [
638
+ path.join(packageRoot, "scripts", "init-project.sh"),
639
+ workspace,
640
+ "--profile", profile,
641
+ "--package-source", packageRoot
642
+ ], {
643
+ cwd: packageRoot,
644
+ timeoutMs: 60_000,
645
+ env: benchmarkEnvironment({ PIAGENT_NO_UPDATE_CHECK: "1" })
646
+ });
647
+ if (result.code !== 0) fail(`Piagent fixture initialization failed: ${result.stderr.trim() || result.stdout.trim()}`, 1);
648
+ }
649
+
650
+ function prepareTreatmentBaseline(workspace, profileName, scenario) {
651
+ const profilePath = path.join(workspace, ".pi", "piagent-profile.json");
652
+ const contextPath = path.join(workspace, ".pi", "project-context.md");
653
+ const indexPath = path.join(workspace, ".pi", "context-index.json");
654
+ let profile;
655
+ let existingIndex;
656
+ try {
657
+ profile = JSON.parse(fs.readFileSync(profilePath, "utf8"));
658
+ existingIndex = JSON.parse(fs.readFileSync(indexPath, "utf8"));
659
+ } catch (error) {
660
+ fail(`Piagent benchmark baseline could not read initialized context: ${error.message}`, 1);
661
+ }
662
+
663
+ const recordedAt = new Date().toISOString();
664
+ const projectId = typeof profile.projectId === "string" && profile.projectId.trim()
665
+ ? profile.projectId.trim()
666
+ : "piagent-benchmark-project";
667
+ const profileMode = typeof profile.mode === "string" && profile.mode.trim()
668
+ ? profile.mode.trim()
669
+ : profileName;
670
+ const sourceScope = (scenario.allowedChanges ?? []).join(", ") || "read-only";
671
+ const summary = `Pre-onboarded synthetic benchmark fixture for ${scenario.id}.`;
672
+ const markdown = [
673
+ "# Project Context",
674
+ "",
675
+ "## Status",
676
+ "",
677
+ `- Generated: ${recordedAt}`,
678
+ `- Profile: ${profileMode}`,
679
+ "- Source: deterministic benchmark baseline",
680
+ "- Verification: use the configured source verifier",
681
+ `- Task scope: ${sourceScope}`,
682
+ "",
683
+ "## Project",
684
+ "",
685
+ "- Small synthetic Node.js fixture used to compare Raw Pi and Piagent.",
686
+ "- Source files and focused tests are authoritative.",
687
+ "- Keep implementation changes within the task scope supplied by the user.",
688
+ ""
689
+ ].join("\n");
690
+ fs.writeFileSync(contextPath, markdown);
691
+
692
+ const profileNode = `profile:${profileMode}`;
693
+ const contextNode = "context:.pi/project-context.md";
694
+ const index = {
695
+ schemaVersion: 1,
696
+ projectId,
697
+ profileMode,
698
+ source: "onboarding-record",
699
+ summary,
700
+ generatedAt: recordedAt,
701
+ updatedAt: recordedAt,
702
+ policy: { ...(existingIndex.policy ?? {}), ...(profile.contextIndex ?? {}) },
703
+ nodes: [
704
+ {
705
+ id: profileNode,
706
+ kind: "profile",
707
+ label: profileMode,
708
+ summary: "Active benchmark profile.",
709
+ path: ".pi/piagent-profile.json",
710
+ tags: ["profile", "benchmark"],
711
+ citations: [{ path: ".pi/piagent-profile.json", reason: "Active benchmark profile" }],
712
+ updatedAt: recordedAt
713
+ },
714
+ {
715
+ id: contextNode,
716
+ kind: "context",
717
+ label: ".pi/project-context.md",
718
+ summary,
719
+ path: ".pi/project-context.md",
720
+ tags: ["snapshot", "benchmark"],
721
+ citations: [{ path: "package.json", reason: "Synthetic project manifest" }],
722
+ updatedAt: recordedAt
723
+ }
724
+ ],
725
+ edges: [{ from: profileNode, to: contextNode, kind: "documented_by", reason: "Benchmark profile uses the prepared context." }],
726
+ citations: [{ path: "package.json", reason: "Synthetic project manifest" }],
727
+ warnings: []
728
+ };
729
+ fs.writeFileSync(indexPath, `${JSON.stringify(index, null, 2)}\n`);
730
+
731
+ const stateRoot = privateDirectory(path.join(workspace, ".pi", "piagent-state"));
732
+ writePrivate(path.join(stateRoot, "project-onboarding.json"), `${JSON.stringify({
733
+ schemaVersion: 1,
734
+ projectId,
735
+ profileMode,
736
+ contextFile: ".pi/project-context.md",
737
+ summary,
738
+ model: "benchmark-setup",
739
+ sourceFiles: [{ path: "package.json", reason: "Synthetic project manifest" }],
740
+ updateTriggers: ["fixture source or benchmark profile changes"],
741
+ notes: "Prepared outside measured model execution so task runs represent steady-state usage.",
742
+ recordedAt
743
+ }, null, 2)}\n`);
744
+ }
745
+
746
+ async function prepareTreatmentContextEngine(workspace) {
747
+ const result = await runCommand(process.execPath, [
748
+ path.join(packageRoot, "scripts", "context-engine.mjs"),
749
+ "rebuild",
750
+ "--project", workspace,
751
+ "--json"
752
+ ], {
753
+ cwd: packageRoot,
754
+ timeoutMs: 60_000,
755
+ env: benchmarkEnvironment({ PIAGENT_NO_UPDATE_CHECK: "1" })
756
+ });
757
+ if (result.code !== 0) fail(`Piagent benchmark context preparation failed: ${result.stderr.trim() || result.stdout.trim()}`, 1);
758
+ }
759
+
760
+ function forbiddenSessionHits(sessionFiles, candidates) {
761
+ const hits = new Set();
762
+ for (const file of sessionFiles) {
763
+ for (const line of fs.readFileSync(file, "utf8").split("\n")) {
764
+ if (!line) continue;
765
+ let entry;
766
+ try { entry = JSON.parse(line); } catch { continue; }
767
+ if (entry.type !== "message" || !entry.message) continue;
768
+ const content = entry.message.content;
769
+ if (typeof content === "string") {
770
+ for (const value of candidates) {
771
+ if (content.includes(value)) hits.add(value);
772
+ }
773
+ continue;
774
+ }
775
+ if (!Array.isArray(content)) continue;
776
+ for (const block of content) {
777
+ if (block?.type !== "text" || typeof block.text !== "string") continue;
778
+ for (const value of candidates) {
779
+ if (block.text.includes(value)) hits.add(value);
780
+ }
781
+ }
782
+ }
783
+ }
784
+ return [...hits];
785
+ }
786
+
787
+ function inspectForbiddenValue(value, candidates, hits) {
788
+ if (typeof value === "string") {
789
+ for (const candidate of candidates) {
790
+ if (value.includes(candidate)) hits.add(candidate);
791
+ }
792
+ return;
793
+ }
794
+ if (Array.isArray(value)) {
795
+ for (const item of value) inspectForbiddenValue(item, candidates, hits);
796
+ return;
797
+ }
798
+ if (value && typeof value === "object") {
799
+ for (const item of Object.values(value)) inspectForbiddenValue(item, candidates, hits);
800
+ }
801
+ }
802
+
803
+ function parseGraderResult(stdout) {
804
+ const lines = stdout.trim().split("\n").filter(Boolean);
805
+ let value;
806
+ try { value = JSON.parse(lines.at(-1) ?? ""); } catch { fail("Benchmark grader did not return a JSON object", 1); }
807
+ if (!value || typeof value !== "object" || Array.isArray(value) || typeof value.passed !== "boolean" || !Array.isArray(value.checks)) {
808
+ fail("Benchmark grader result must contain passed and checks", 1);
809
+ }
810
+ for (const check of value.checks) {
811
+ if (!check || typeof check.id !== "string" || typeof check.passed !== "boolean") fail("Benchmark grader returned an invalid check", 1);
812
+ }
813
+ return {
814
+ passed: value.passed,
815
+ score: Number.isFinite(value.score) ? Math.max(0, Math.min(10, value.score)) : value.passed ? 10 : 0,
816
+ checks: value.checks.map((check) => ({ id: check.id, passed: check.passed, detail: typeof check.detail === "string" ? check.detail.slice(0, 500) : undefined }))
817
+ };
818
+ }
819
+
820
+ async function gradeWorkspace(grader, workspace, scenario, timeoutSeconds, oraclePath) {
821
+ const args = oraclePath ? [grader, workspace, oraclePath] : [grader, workspace];
822
+ const result = await runCommand(process.execPath, args, {
823
+ cwd: path.dirname(grader),
824
+ timeoutMs: Math.min(timeoutSeconds, 120) * 1_000,
825
+ env: graderEnvironment(scenario.id)
826
+ });
827
+ if (result.timedOut) return { passed: false, score: 0, checks: [], error: "grader-timeout" };
828
+ if (result.code !== 0) return { passed: false, score: 0, checks: [], error: `grader-exit-${result.code}` };
829
+ try { return parseGraderResult(result.stdout); } catch (error) { return { passed: false, score: 0, checks: [], error: error.message }; }
830
+ }
831
+
832
+ function sessionSummaries(sessionDir) {
833
+ return walkJsonl(sessionDir).map((file) => summarizeSession(file, {})).filter(Boolean);
834
+ }
835
+
836
+ function failureReason({ agent, grade, graderIntegrity, outsideScope, forbiddenHits, missingRequired }) {
837
+ const failures = [];
838
+ if (agent.timedOut) failures.push("agent-timeout");
839
+ else if (agent.code !== 0) failures.push(`agent-exit-${agent.code}`);
840
+ if (!grade.passed) failures.push(grade.error ?? "hidden-grader-failed");
841
+ if (!graderIntegrity.passed) failures.push("grader-mutated-workspace");
842
+ if (outsideScope.length) failures.push(`outside-scope:${outsideScope.join(",")}`);
843
+ if (forbiddenHits.length) failures.push("forbidden-output");
844
+ if (missingRequired.length) failures.push("required-output-missing");
845
+ return failures.join("; ") || undefined;
846
+ }
847
+
848
+ function safeInfrastructureDiagnostic(value, forbiddenValues) {
849
+ let diagnostic = String(value ?? "");
850
+ for (const forbidden of forbiddenValues) {
851
+ if (forbidden) diagnostic = diagnostic.replaceAll(forbidden, "[REDACTED]");
852
+ }
853
+ return diagnostic
854
+ .replace(/\u001b\[[0-?]*[ -/]*[@-~]/g, "")
855
+ .replace(/[\u0000-\u0008\u000B\u000C\u000E-\u001F\u007F]/g, " ")
856
+ .trim()
857
+ .slice(-4_000);
858
+ }
859
+
860
+ async function runOne({
861
+ suite,
862
+ suiteRoot,
863
+ scenario,
864
+ surface,
865
+ repeat,
866
+ infrastructureAttempt = 1,
867
+ runId,
868
+ runRoot,
869
+ options,
870
+ piCommand,
871
+ codexCommand,
872
+ codexDisabledFeatures,
873
+ codexRuntime,
874
+ suiteDigest,
875
+ rootSeed
876
+ }) {
877
+ const attemptSuffix = infrastructureAttempt > 1 ? `-infra-${infrastructureAttempt}` : "";
878
+ const key = `${String(repeat).padStart(2, "0")}-${scenario.id}-${surface}${attemptSuffix}`;
879
+ const workspaceRoot = privateDirectory(path.join(runRoot, "workspaces", key));
880
+ const workspace = path.join(workspaceRoot, "project");
881
+ const sessions = privateDirectory(path.join(workspaceRoot, "sessions"));
882
+ const fixture = resolveSuiteEntry(suiteRoot, scenario.fixture, "fixture");
883
+ fs.cpSync(fixture, workspace, { recursive: true, errorOnExist: true });
884
+ applySetupFiles(workspace, scenario.setupFiles);
885
+ const profile = scenario.profile ?? suite.profile;
886
+ const lifecycle = scenario.lifecycle ?? "steady-state";
887
+ let variant = {
888
+ oraclePath: null,
889
+ oracleDigest: null,
890
+ seedDigest: null,
891
+ requiredOutputSubstrings: [],
892
+ forbiddenOutputSubstrings: []
893
+ };
894
+ if (scenario.variantGenerator) {
895
+ const generator = resolveSuiteEntry(suiteRoot, scenario.variantGenerator, "variant generator");
896
+ variant = await generateVariant({
897
+ generator,
898
+ workspace,
899
+ oraclePath: path.join(workspaceRoot, "oracle.json"),
900
+ seed: variantSeed(rootSeed, suiteDigest, scenario.id, repeat),
901
+ scenario,
902
+ timeoutSeconds: options.timeoutSeconds
903
+ });
904
+ }
905
+ const forbiddenOutputSubstrings = [...new Set([
906
+ ...(scenario.forbiddenOutputSubstrings ?? []),
907
+ ...variant.forbiddenOutputSubstrings
908
+ ])];
909
+ const requiredOutputSubstrings = [...new Set([
910
+ ...(scenario.requiredOutputSubstrings ?? []),
911
+ ...variant.requiredOutputSubstrings
912
+ ])];
913
+ if (surface === "piagent") {
914
+ await initializeTreatment(workspace, profile);
915
+ if (lifecycle === "steady-state") {
916
+ prepareTreatmentBaseline(workspace, profile, scenario);
917
+ await prepareTreatmentContextEngine(workspace);
918
+ }
919
+ }
920
+ await initializeGit(workspace, scenario.setupFiles);
921
+
922
+ const promptPath = resolveSuiteEntry(suiteRoot, scenario.prompt, "prompt");
923
+ const graderPath = resolveSuiteEntry(suiteRoot, scenario.grader, "grader");
924
+ const prompt = fs.readFileSync(promptPath, "utf8").trim();
925
+ const sessionId = crypto.randomUUID();
926
+ const piArgs = [
927
+ "--print", "--mode", "json",
928
+ "--session-dir", sessions,
929
+ "--session-id", sessionId,
930
+ "--name", `BENCH ${scenario.id} ${surface} r${repeat}`,
931
+ "--approve",
932
+ "--no-skills",
933
+ "--no-prompt-templates",
934
+ "--no-extensions",
935
+ "--no-context-files"
936
+ ];
937
+ const controlledCrossCli = options.surfaces.includes("codex-cli") && options.codexMode === "controlled";
938
+ const globalAppend = controlledCrossCli ? undefined : globalAppendSystemPrompt();
939
+ if (globalAppend) piArgs.push("--append-system-prompt", globalAppend);
940
+ if (surface === "piagent") {
941
+ const projectInstructions = path.join(workspace, "AGENTS.md");
942
+ if (!fs.existsSync(projectInstructions)) fail("Piagent fixture initialization did not create AGENTS.md", 1);
943
+ piArgs.push(
944
+ "--append-system-prompt", projectInstructions,
945
+ "--extension", path.join(packageRoot, "packages", "piagent-core", "extensions", "piagent-guard.ts"),
946
+ "--skill", path.join(packageRoot, "packages", "piagent-core", "skills")
947
+ );
948
+ }
949
+ if (options.model) piArgs.push("--model", options.model);
950
+ if (options.thinking) piArgs.push("--thinking", options.thinking);
951
+ piArgs.push(prompt);
952
+
953
+ const command = surface === "codex-cli" ? codexCommand : piCommand;
954
+ const args = surface === "codex-cli"
955
+ ? codexExecArgs({ workspace, options, disabledFeatures: codexDisabledFeatures })
956
+ : piArgs;
957
+ const codexForbiddenHits = new Set();
958
+ const codexCollector = surface === "codex-cli" ? createCodexExecJsonlCollector({
959
+ model: options.model,
960
+ thinkingLevel: options.thinking,
961
+ onEvent: (event) => inspectForbiddenValue(event, forbiddenOutputSubstrings, codexForbiddenHits)
962
+ }) : undefined;
963
+
964
+ const agent = await runCommand(command, args, {
965
+ cwd: workspace,
966
+ input: surface === "codex-cli" ? prompt : undefined,
967
+ timeoutMs: options.timeoutSeconds * 1_000,
968
+ forbiddenSubstrings: forbiddenOutputSubstrings,
969
+ requiredSubstrings: requiredOutputSubstrings,
970
+ onStdoutChunk: codexCollector ? (chunk) => codexCollector.write(chunk) : undefined,
971
+ env: surface === "codex-cli" ? codexProcessEnvironment(codexRuntime, {
972
+ PIAGENT_NO_UPDATE_CHECK: "1",
973
+ PIAGENT_BENCHMARK_RUN_ID: runId,
974
+ PIAGENT_BENCHMARK_SCENARIO: scenario.id,
975
+ PIAGENT_BENCHMARK_SURFACE: surface,
976
+ PIAGENT_BENCHMARK_SESSION_ID: sessionId,
977
+ PIAGENT_BENCHMARK_PROFILE: profile,
978
+ PIAGENT_BENCHMARK_LIFECYCLE: lifecycle,
979
+ NO_COLOR: "1"
980
+ }) : benchmarkEnvironment({
981
+ PIAGENT_NO_UPDATE_CHECK: "1",
982
+ PIAGENT_BENCHMARK_RUN_ID: runId,
983
+ PIAGENT_BENCHMARK_SCENARIO: scenario.id,
984
+ PIAGENT_BENCHMARK_SURFACE: surface,
985
+ PIAGENT_BENCHMARK_SESSION_ID: sessionId,
986
+ PIAGENT_BENCHMARK_PROFILE: profile,
987
+ PIAGENT_BENCHMARK_LIFECYCLE: lifecycle,
988
+ NO_COLOR: "1"
989
+ })
990
+ });
991
+ const sessionFiles = surface === "codex-cli" ? [] : walkJsonl(sessions);
992
+ let usage;
993
+ let codexDiagnostics = [];
994
+ if (surface === "codex-cli") {
995
+ try {
996
+ usage = codexCollector.finish();
997
+ } catch (error) {
998
+ if (agent.code === 0 && !agent.timedOut) throw error;
999
+ usage = aggregateSessionUsage([]);
1000
+ }
1001
+ codexDiagnostics = codexCollector.diagnostics();
1002
+ } else {
1003
+ usage = aggregateSessionUsage(sessionSummaries(sessions));
1004
+ }
1005
+ const forbiddenHits = [...new Set([
1006
+ ...(agent.forbiddenHits ?? []),
1007
+ ...(surface === "codex-cli"
1008
+ ? [...codexForbiddenHits]
1009
+ : forbiddenSessionHits(sessionFiles, forbiddenOutputSubstrings))
1010
+ ])];
1011
+ const requiredHits = new Set(agent.requiredHits ?? []);
1012
+ if (surface !== "codex-cli") {
1013
+ for (const value of forbiddenSessionHits(sessionFiles, requiredOutputSubstrings)) requiredHits.add(value);
1014
+ }
1015
+ const missingRequired = requiredOutputSubstrings.filter((value) => !requiredHits.has(value));
1016
+ const allChangedFiles = workingTreeFiles(workspace);
1017
+ const runtimeManagedChanges = surface === "piagent" && lifecycle === "cold-start"
1018
+ ? allChangedFiles.filter((file) => matchesAnyPath(file, coldStartRuntimeManagedPaths))
1019
+ : [];
1020
+ const runtimeManagedSet = new Set(runtimeManagedChanges);
1021
+ const changedFiles = allChangedFiles.filter((file) => !runtimeManagedSet.has(file));
1022
+ const beforeGrade = workingTreeSnapshot(workspace);
1023
+ const outsideScope = changedFiles.filter((file) => !matchesAnyPath(file, scenario.allowedChanges));
1024
+ const grade = await gradeWorkspace(graderPath, workspace, scenario, options.timeoutSeconds, variant.oraclePath);
1025
+ const afterGrade = workingTreeSnapshot(workspace);
1026
+ const graderIntegrity = { passed: JSON.stringify(beforeGrade) === JSON.stringify(afterGrade) };
1027
+ let workflow = null;
1028
+ if (surface === "piagent" && scenario.kind !== "safety-refusal") {
1029
+ const task = listTaskContracts(workspace).find((item) => item.sessionId === sessionId);
1030
+ workflow = evaluateWorkflowEvidence(task, changedFiles, usage.toolNames, { scenarioKind: scenario.kind });
1031
+ }
1032
+ const scope = { passed: outsideScope.length === 0, changedFiles, outsideScope, allChangedFiles, runtimeManagedChanges };
1033
+ const outputSafety = { passed: forbiddenHits.length === 0, forbiddenHits: forbiddenHits.map((value) => crypto.createHash("sha256").update(value).digest("hex")) };
1034
+ const outputEvidence = {
1035
+ passed: missingRequired.length === 0,
1036
+ requiredCount: requiredOutputSubstrings.length,
1037
+ observedCount: requiredOutputSubstrings.length - missingRequired.length,
1038
+ missingHashes: missingRequired.map((value) => crypto.createHash("sha256").update(value).digest("hex"))
1039
+ };
1040
+ // A timed-out agent is measured reliability evidence. Only an early process
1041
+ // failure with no provider usage is infrastructure and eligible for retry.
1042
+ const abortSuite = !agent.timedOut && agent.code !== 0 && usage.fresh <= 0;
1043
+ const diagnosticInput = codexDiagnostics.length > 0
1044
+ ? JSON.stringify(codexDiagnostics)
1045
+ : `${agent.stderr ?? ""}\n${agent.stdout ?? ""}`;
1046
+ const resolved = agent.code === 0 && !agent.timedOut && grade.passed && graderIntegrity.passed && scope.passed && outputSafety.passed && outputEvidence.passed;
1047
+ const record = {
1048
+ schemaVersion: 1,
1049
+ scenarioId: scenario.id,
1050
+ scenarioTitle: scenario.title,
1051
+ scenarioKind: scenario.kind,
1052
+ category: scenario.category ?? "unspecified",
1053
+ difficulty: scenario.difficulty ?? "unspecified",
1054
+ profile,
1055
+ lifecycle,
1056
+ surface,
1057
+ repeat,
1058
+ infrastructureAttempt,
1059
+ sessionId,
1060
+ providerSessionId: usage.providerSessionId ?? null,
1061
+ abortSuite,
1062
+ infrastructureFailure: abortSuite ? `agent-exit-${agent.code}-before-usage` : undefined,
1063
+ infrastructureDiagnostic: abortSuite ? safeInfrastructureDiagnostic(diagnosticInput, forbiddenOutputSubstrings) : undefined,
1064
+ infrastructureDiagnosticSource: abortSuite ? (codexDiagnostics.length > 0 ? "codex-error-events" : "process-output-tail") : undefined,
1065
+ resolved,
1066
+ failure: failureReason({ agent, grade, graderIntegrity, outsideScope, forbiddenHits, missingRequired }),
1067
+ agent: {
1068
+ exitCode: agent.code,
1069
+ signal: agent.signal,
1070
+ timedOut: agent.timedOut,
1071
+ stdoutHash: agent.stdoutHash ?? crypto.createHash("sha256").update(agent.stdout).digest("hex"),
1072
+ stderrHash: crypto.createHash("sha256").update(agent.stderr ?? "").digest("hex")
1073
+ },
1074
+ grade,
1075
+ graderIntegrity,
1076
+ scope,
1077
+ outputSafety,
1078
+ outputEvidence,
1079
+ workflow,
1080
+ usage,
1081
+ durationSeconds: agent.durationSeconds,
1082
+ promptHash: crypto.createHash("sha256").update(prompt).digest("hex"),
1083
+ variant: scenario.variantGenerator ? {
1084
+ generated: true,
1085
+ seedDigest: variant.seedDigest,
1086
+ oracleDigest: variant.oracleDigest
1087
+ } : { generated: false }
1088
+ };
1089
+ if (!options.keepWorkspaces) fs.rmSync(workspaceRoot, { recursive: true, force: true });
1090
+ return record;
1091
+ }
1092
+
1093
+ function executionOrder(suite, repeats, surfaces, rootSeed) {
1094
+ const order = [];
1095
+ for (let repeat = 1; repeat <= repeats; repeat += 1) {
1096
+ const scenarios = suite.schemaVersion === 2
1097
+ ? [...suite.scenarios].sort((left, right) => {
1098
+ const rank = (scenario) => crypto.createHmac("sha256", rootSeed).update(`order\0${repeat}\0${scenario.id}`).digest("hex");
1099
+ return rank(left).localeCompare(rank(right));
1100
+ })
1101
+ : suite.scenarios;
1102
+ for (const [index, scenario] of scenarios.entries()) {
1103
+ const reverse = suite.schemaVersion === 2
1104
+ ? (crypto.createHmac("sha256", rootSeed).update(`surface\0${repeat}\0${scenario.id}`).digest()[0] & 1) === 1
1105
+ : (repeat + index) % 2 !== 0;
1106
+ const ordered = reverse ? [...surfaces].reverse() : surfaces;
1107
+ for (const surface of ordered) order.push({ scenario, surface, repeat });
1108
+ }
1109
+ }
1110
+ return order;
1111
+ }
1112
+
1113
+ async function confirmPlan(message) {
1114
+ if (!process.stdin.isTTY || !process.stdout.isTTY) fail("Refusing to start billed model runs without --yes in a non-interactive terminal", 1);
1115
+ const terminal = readline.createInterface({ input: process.stdin, output: process.stdout });
1116
+ try {
1117
+ const answer = await terminal.question(`${message}\nContinue? [y/N] `);
1118
+ return /^(?:y|yes)$/i.test(answer.trim());
1119
+ } finally {
1120
+ terminal.close();
1121
+ }
1122
+ }
1123
+
1124
+ async function checkedVersion(command, args, label, env = process.env) {
1125
+ let result;
1126
+ try { result = await runCommand(command, args, { cwd: packageRoot, timeoutMs: 15_000, env }); }
1127
+ catch (error) { fail(`Required command is unavailable: ${label} (${error.message})`, 1); }
1128
+ if (result.code !== 0) fail(`Required command failed preflight: ${label}`, 1);
1129
+ return result.stdout.trim();
1130
+ }
1131
+
1132
+ async function preflight({ piCommand, codexCommand, surfaces, codexMode, codexRuntime }) {
1133
+ const gitVersion = await checkedVersion("git", ["--version"], "git");
1134
+ const piVersion = await checkedVersion(piCommand, ["--version"], "pi");
1135
+ let codexVersion;
1136
+ let codexAuth;
1137
+ let codexDisabledFeatures = [];
1138
+ if (surfaces.includes("codex-cli")) {
1139
+ const codexEnv = codexProcessEnvironment(codexRuntime);
1140
+ codexVersion = await checkedVersion(codexCommand, ["--version"], "codex", codexEnv);
1141
+ let result;
1142
+ try { result = await runCommand(codexCommand, ["login", "status"], { cwd: packageRoot, timeoutMs: 15_000, env: codexEnv }); }
1143
+ catch (error) { fail(`Codex authentication preflight failed: ${error.message}`, 1); }
1144
+ if (result.code === 0) codexAuth = "login-status";
1145
+ else if (process.env.OPENAI_API_KEY || process.env.CODEX_ACCESS_TOKEN) codexAuth = "environment-credential";
1146
+ else fail("Codex CLI is not authenticated; run codex login before this benchmark", 1);
1147
+
1148
+ if (codexMode === "controlled") {
1149
+ const features = await runCommand(codexCommand, ["features", "list"], { cwd: packageRoot, timeoutMs: 15_000, env: codexEnv });
1150
+ if (features.code !== 0) fail("Codex CLI cannot list features required by controlled benchmark mode; update Codex CLI or use --codex-mode native", 1);
1151
+ const available = new Set(features.stdout.split("\n").map((line) => line.trim().split(/\s+/)[0]).filter(Boolean));
1152
+ codexDisabledFeatures = controlledCodexFeatures.filter((feature) => available.has(feature));
1153
+ }
1154
+ }
1155
+ return { gitVersion, piVersion, codexVersion, codexAuth, codexDisabledFeatures };
1156
+ }
1157
+
1158
+ async function sourceIdentity() {
1159
+ const top = await runCommand("git", ["-C", packageRoot, "rev-parse", "--show-toplevel"], { cwd: packageRoot, timeoutMs: 15_000 });
1160
+ if (top.code !== 0) return { kind: "package", commit: null, dirty: null };
1161
+ let repositoryRoot;
1162
+ try { repositoryRoot = fs.realpathSync(top.stdout.trim()); } catch { return { kind: "package", commit: null, dirty: null }; }
1163
+ if (repositoryRoot !== fs.realpathSync(packageRoot)) return { kind: "package", commit: null, dirty: null };
1164
+ const commit = await runCommand("git", ["-C", packageRoot, "rev-parse", "HEAD"], { cwd: packageRoot, timeoutMs: 15_000 });
1165
+ const status = await runCommand("git", ["-C", packageRoot, "status", "--porcelain", "--untracked-files=normal"], { cwd: packageRoot, timeoutMs: 15_000 });
1166
+ return {
1167
+ kind: "git-working-tree",
1168
+ commit: commit.code === 0 ? commit.stdout.trim() : null,
1169
+ dirty: status.code === 0 ? Boolean(status.stdout.trim()) : null
1170
+ };
1171
+ }
1172
+
1173
+ async function runLegacy(argv) {
1174
+ const result = await runCommand("bash", [path.join(packageRoot, "scripts", "quality-benchmark.sh"), ...argv], { cwd: process.cwd(), inherit: true });
1175
+ process.exitCode = result.code;
1176
+ }
1177
+
1178
+ async function main() {
1179
+ const argv = process.argv.slice(2);
1180
+ if (isLegacyInvocation(argv)) return runLegacy(argv);
1181
+ const options = parseBenchmarkArgs(argv);
1182
+ if (options.help) {
1183
+ process.stdout.write(usage);
1184
+ return;
1185
+ }
1186
+ const { suite, manifestPath, suiteRoot } = loadSuite(options.suite);
1187
+ validateSuiteFiles(suite, suiteRoot);
1188
+ const declaredScenarioCount = suite.scenarios.length;
1189
+ if (options.scenarioIds) {
1190
+ const byId = new Map(suite.scenarios.map((scenario) => [scenario.id, scenario]));
1191
+ const missing = options.scenarioIds.filter((id) => !byId.has(id));
1192
+ if (missing.length) fail(`Unknown benchmark scenario: ${missing.join(", ")}`, 1);
1193
+ suite.scenarios = options.scenarioIds.map((id) => byId.get(id));
1194
+ }
1195
+ options.repeats = options.repeats ?? suite.defaultRepeats;
1196
+ options.infrastructureRetries = options.infrastructureRetries ?? (suite.schemaVersion === 2 ? 2 : 0);
1197
+ options.retryDelaySeconds = options.retryDelaySeconds ?? (suite.schemaVersion === 2 ? 60 : 0);
1198
+ options.timeoutSeconds = options.timeoutSeconds ?? suite.timeoutSeconds;
1199
+ if (options.surfaces.includes("codex-cli")) {
1200
+ codexModelName(options.model);
1201
+ codexThinkingEffort(options.thinking);
1202
+ }
1203
+ const comparison = comparisonSurfaces(options);
1204
+ const piCommand = process.env.PIAGENT_BENCHMARK_PI_COMMAND || "pi";
1205
+ const codexCommand = process.env.PIAGENT_BENCHMARK_CODEX_COMMAND || "codex";
1206
+ const suiteDigest = suiteTreeDigest(suiteRoot);
1207
+ const rootSeed = options.seed ?? crypto.randomBytes(32).toString("hex");
1208
+ const rootSeedDigest = crypto.createHash("sha256").update(rootSeed).digest("hex");
1209
+ const order = executionOrder(suite, options.repeats, options.surfaces, rootSeed);
1210
+ const lifecycles = [...new Set(suite.scenarios.map((scenario) => scenario.lifecycle ?? "steady-state"))];
1211
+ const plan = [
1212
+ "Piagent automatic benchmark",
1213
+ ` platform: v${packageManifest.version}`,
1214
+ ` suite: ${suite.id} (${suite.scenarios.length}${suite.scenarios.length !== declaredScenarioCount ? `/${declaredScenarioCount}` : ""} scenarios)`,
1215
+ ` digest: ${suiteDigest.slice(0, 16)}`,
1216
+ ` surfaces: ${options.surfaces.join(", ")}`,
1217
+ ` compare: ${benchmarkSurfaceLabel(comparison.candidateSurface)} vs ${benchmarkSurfaceLabel(comparison.baselineSurface)}`,
1218
+ ` repeats: ${options.repeats}`,
1219
+ ` retries: ${options.infrastructureRetries} infrastructure-only · ${options.retryDelaySeconds}s backoff`,
1220
+ ` sessions: ${order.length}`,
1221
+ ` model: ${options.model ?? "Pi default"}`,
1222
+ ` thinking: ${options.thinking ?? "Pi default"}`,
1223
+ ` lifecycle: ${lifecycles.join(", ")}`,
1224
+ ` variants: ${suite.scenarios.some((scenario) => scenario.variantGenerator) ? `generated · seed ${rootSeedDigest.slice(0, 16)}` : "static"}`,
1225
+ ` ordering: ${suite.schemaVersion === 2 ? "seeded paired blocks" : "paired alternating"}`,
1226
+ ` timeout: ${options.timeoutSeconds}s per session`,
1227
+ " grading: hidden verifier + scope + output safety + Pi task evidence"
1228
+ ].join("\n");
1229
+ const codexPlan = options.surfaces.includes("codex-cli")
1230
+ ? `\n codex: ${options.codexMode} mode · model ${codexModelName(options.model)} · effort ${codexThinkingEffort(options.thinking)}${options.codexMode === "controlled" ? " · isolated home" : ""}`
1231
+ : "";
1232
+ if (options.dryRun) {
1233
+ process.stdout.write(`${plan}${codexPlan}\n manifest: ${manifestPath}\nDRY RUN: no model session started.\n`);
1234
+ return;
1235
+ }
1236
+ const codexRuntime = createCodexRuntime(options);
1237
+ try {
1238
+ const runtime = await preflight({ piCommand, codexCommand, surfaces: options.surfaces, codexMode: options.codexMode, codexRuntime });
1239
+ const source = await sourceIdentity();
1240
+ const nativeWarning = options.surfaces.includes("codex-cli") && options.codexMode === "native"
1241
+ ? "\nNative Codex mode loads the operator's global AGENTS.md, configuration, rules, hooks, MCP servers, and plugins."
1242
+ : "";
1243
+ if (!options.yes && !(await confirmPlan(`${plan}${codexPlan}${nativeWarning}\nThis may use paid model quota.`))) {
1244
+ process.stdout.write("Benchmark cancelled; no model session started.\n");
1245
+ return;
1246
+ }
1247
+
1248
+ const runId = createRunId(suite.id);
1249
+ const output = options.output ?? path.join(defaultOutputRoot(), runId);
1250
+ const runRoot = ensureEmptyOutput(output);
1251
+ privateDirectory(path.join(runRoot, "workspaces"));
1252
+ const removeSignalHandlers = installSignalForwarding();
1253
+ const startedAt = new Date().toISOString();
1254
+ const runs = [];
1255
+ let fatalRunError;
1256
+ const ledgerPath = path.join(runRoot, "runs.jsonl");
1257
+ const infrastructureLedgerPath = path.join(runRoot, "infrastructure-attempts.jsonl");
1258
+ try {
1259
+ for (const [index, item] of order.entries()) {
1260
+ if (interruptedSignal) break;
1261
+ process.stdout.write(`[${index + 1}/${order.length}] ${item.scenario.id} · ${item.surface} · repeat ${item.repeat}\n`);
1262
+ let record;
1263
+ const infrastructureFailures = [];
1264
+ for (let infrastructureAttempt = 1; infrastructureAttempt <= options.infrastructureRetries + 1; infrastructureAttempt += 1) {
1265
+ let attemptError;
1266
+ let attemptCodexRuntime = codexRuntime;
1267
+ try {
1268
+ if (item.surface === "codex-cli") attemptCodexRuntime = createCodexRuntime(options);
1269
+ record = await runOne({
1270
+ suite,
1271
+ suiteRoot,
1272
+ ...item,
1273
+ infrastructureAttempt,
1274
+ runId,
1275
+ runRoot,
1276
+ options,
1277
+ piCommand,
1278
+ codexCommand,
1279
+ codexDisabledFeatures: runtime.codexDisabledFeatures,
1280
+ codexRuntime: attemptCodexRuntime,
1281
+ suiteDigest,
1282
+ rootSeed
1283
+ });
1284
+ } catch (error) {
1285
+ attemptError = error;
1286
+ record = {
1287
+ schemaVersion: 1,
1288
+ scenarioId: item.scenario.id,
1289
+ scenarioTitle: item.scenario.title,
1290
+ scenarioKind: item.scenario.kind,
1291
+ category: item.scenario.category ?? "unspecified",
1292
+ difficulty: item.scenario.difficulty ?? "unspecified",
1293
+ profile: item.scenario.profile ?? suite.profile,
1294
+ lifecycle: item.scenario.lifecycle ?? "steady-state",
1295
+ surface: item.surface,
1296
+ repeat: item.repeat,
1297
+ infrastructureAttempt,
1298
+ abortSuite: true,
1299
+ infrastructureFailure: `runner-error:${error.message}`,
1300
+ resolved: false,
1301
+ failure: `runner-error:${error.message}`,
1302
+ grade: { passed: false, score: 0, checks: [] },
1303
+ graderIntegrity: { passed: false },
1304
+ scope: { passed: false, changedFiles: [], outsideScope: [] },
1305
+ outputSafety: { passed: false, forbiddenHits: [] },
1306
+ outputEvidence: { passed: false, requiredCount: 0, observedCount: 0, missingHashes: [] },
1307
+ workflow: null,
1308
+ usage: aggregateSessionUsage([]),
1309
+ durationSeconds: 0
1310
+ };
1311
+ } finally {
1312
+ if (attemptCodexRuntime !== codexRuntime) attemptCodexRuntime.cleanup();
1313
+ }
1314
+ if (!record.abortSuite || interruptedSignal) break;
1315
+ const retryAvailable = infrastructureAttempt <= options.infrastructureRetries;
1316
+ infrastructureFailures.push({
1317
+ attempt: infrastructureAttempt,
1318
+ failure: record.infrastructureFailure ?? record.failure,
1319
+ agent: record.agent,
1320
+ durationSeconds: record.durationSeconds
1321
+ });
1322
+ fs.appendFileSync(infrastructureLedgerPath, `${JSON.stringify({ ...record, accepted: false, retryAvailable })}\n`, { mode: 0o600 });
1323
+ if (!retryAvailable) {
1324
+ if (attemptError) fatalRunError = attemptError;
1325
+ break;
1326
+ }
1327
+ process.stdout.write(` RETRY ${infrastructureAttempt}/${options.infrastructureRetries} (${record.infrastructureFailure ?? record.failure})\n`);
1328
+ if (options.retryDelaySeconds > 0) {
1329
+ await new Promise((resolve) => setTimeout(resolve, options.retryDelaySeconds * 1_000));
1330
+ }
1331
+ }
1332
+ record.infrastructureAttempts = record.infrastructureAttempt ?? 1;
1333
+ record.infrastructureRetries = Math.max(0, record.infrastructureAttempts - 1);
1334
+ record.infrastructureFailures = infrastructureFailures;
1335
+ runs.push(record);
1336
+ fs.appendFileSync(ledgerPath, `${JSON.stringify(record)}\n`, { mode: 0o600 });
1337
+ const cost = Number.isFinite(record.usage.cost) ? `$${Number(record.usage.cost).toFixed(6)}` : "cost n/a";
1338
+ process.stdout.write(` ${record.resolved ? "PASS" : `FAIL (${record.failure})`} · ${record.usage.fresh} fresh tok · ${cost}\n`);
1339
+ if (record.abortSuite && !fatalRunError) fatalRunError = new Error(record.infrastructureFailure ?? record.failure ?? "agent startup failure");
1340
+ if (fatalRunError) break;
1341
+ }
1342
+ } finally {
1343
+ removeSignalHandlers();
1344
+ }
1345
+ if (interruptedSignal) {
1346
+ if (!options.keepWorkspaces) fs.rmSync(path.join(runRoot, "workspaces"), { recursive: true, force: true });
1347
+ writePrivate(path.join(runRoot, "interrupted.json"), `${JSON.stringify({ schemaVersion: 1, runId, signal: interruptedSignal, completedRuns: runs.length, expectedRuns: order.length, interruptedAt: new Date().toISOString() }, null, 2)}\n`);
1348
+ process.stderr.write(`Benchmark interrupted by ${interruptedSignal}. Partial ledger: ${ledgerPath}\n`);
1349
+ process.exitCode = interruptedSignal === "SIGINT" ? 130 : 143;
1350
+ return;
1351
+ }
1352
+ if (fatalRunError) {
1353
+ if (!options.keepWorkspaces) fs.rmSync(path.join(runRoot, "workspaces"), { recursive: true, force: true });
1354
+ writePrivate(path.join(runRoot, "aborted.json"), `${JSON.stringify({ schemaVersion: 1, runId, reason: fatalRunError.message, completedRuns: runs.length, expectedRuns: order.length, abortedAt: new Date().toISOString() }, null, 2)}\n`);
1355
+ process.stderr.write(`Benchmark aborted after an infrastructure error. Partial ledger: ${ledgerPath}\n`);
1356
+ process.exitCode = 1;
1357
+ return;
1358
+ }
1359
+ const report = summarizeBenchmark({
1360
+ suite,
1361
+ runId,
1362
+ startedAt,
1363
+ completedAt: new Date().toISOString(),
1364
+ repeats: options.repeats,
1365
+ environment: {
1366
+ platformVersion: packageManifest.version,
1367
+ suiteDigest,
1368
+ variantRootSeed: suite.scenarios.some((scenario) => scenario.variantGenerator) ? rootSeed : null,
1369
+ variantRootSeedDigest: suite.scenarios.some((scenario) => scenario.variantGenerator) ? rootSeedDigest : null,
1370
+ executionOrder: suite.schemaVersion === 2 ? "seeded-paired-block-randomized" : "paired-alternating",
1371
+ profile: suite.profile,
1372
+ requestedModel: options.model ?? null,
1373
+ requestedThinking: options.thinking ?? null,
1374
+ treatmentBaseline: lifecycles.length === 1 && lifecycles[0] === "steady-state"
1375
+ ? options.surfaces.includes("codex-cli")
1376
+ ? "piagent-initialized-and-onboarded; codex-clean-fixture"
1377
+ : "initialized-and-onboarded"
1378
+ : "scenario-defined-mixed-lifecycle",
1379
+ timeoutSeconds: options.timeoutSeconds,
1380
+ nodeVersion: process.version,
1381
+ piVersion: runtime.piVersion,
1382
+ codexVersion: runtime.codexVersion ?? null,
1383
+ codexMode: options.surfaces.includes("codex-cli") ? options.codexMode : null,
1384
+ codexAuth: runtime.codexAuth ?? null,
1385
+ codexIsolation: options.surfaces.includes("codex-cli")
1386
+ ? options.codexMode === "controlled" ? "per-session-temporary-home" : "operator-home"
1387
+ : null,
1388
+ codexCredentialBridge: options.surfaces.includes("codex-cli") ? codexRuntime.credentialBridge : null,
1389
+ codexGlobalInstructions: options.surfaces.includes("codex-cli")
1390
+ ? options.codexMode === "controlled" ? "excluded" : "operator-home"
1391
+ : null,
1392
+ codexDisabledFeatures: runtime.codexDisabledFeatures,
1393
+ surfaces: options.surfaces,
1394
+ scenarioSelection: options.scenarioIds ?? null,
1395
+ surfaceModels: options.surfaces.includes("codex-cli") ? {
1396
+ piagent: options.model,
1397
+ "codex-cli": codexModelName(options.model)
1398
+ } : { "raw-pi": options.model ?? null, piagent: options.model ?? null },
1399
+ modelParityEvidence: options.surfaces.includes("codex-cli") ? "command-line-pinned" : "session-reported",
1400
+ gitVersion: runtime.gitVersion,
1401
+ source
1402
+ },
1403
+ runs,
1404
+ ...comparison
1405
+ });
1406
+ const text = renderBenchmarkText(report);
1407
+ writePrivate(path.join(runRoot, "report.json"), `${JSON.stringify(report, null, 2)}\n`);
1408
+ writePrivate(path.join(runRoot, "report.html"), renderBenchmarkHtml(report));
1409
+ writePrivate(path.join(runRoot, "summary.txt"), text);
1410
+ if (!options.keepWorkspaces) fs.rmSync(path.join(runRoot, "workspaces"), { recursive: true, force: true });
1411
+ process.stdout.write(options.json ? `${JSON.stringify(report, null, 2)}\n` : text);
1412
+ process.stdout.write(`Reports: ${runRoot}\n`);
1413
+ if (
1414
+ report.comparison.qualityGate === false
1415
+ || report.comparison.safetyGate === false
1416
+ || report.comparison.reliabilityGate === false
1417
+ || report.comparison.qualityNonInferior === false
1418
+ || report.comparison.workflowGate === false
1419
+ || report.comparison.categoryGate === false
1420
+ || report.comparison.productionGate?.passed === false
1421
+ ) process.exitCode = 1;
1422
+ } finally {
1423
+ codexRuntime.cleanup();
1424
+ }
1425
+ }
1426
+
1427
+ if (import.meta.url === pathToFileURL(process.argv[1] || "").href) {
1428
+ main().catch((error) => {
1429
+ console.error(`FAIL: ${error.message}`);
1430
+ process.exit(error.exitCode ?? 1);
1431
+ });
1432
+ }