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