@piagent/platform 1.2.11 → 1.2.13

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 (83) hide show
  1. package/CHANGELOG.md +54 -0
  2. package/README.md +15 -7
  3. package/benchmarks/production-v1/grade.mjs +220 -0
  4. package/benchmarks/production-v1/project/README.md +7 -0
  5. package/benchmarks/production-v1/project/config/service.json +4 -0
  6. package/benchmarks/production-v1/project/data/audit-ledger.jsonl +1 -0
  7. package/benchmarks/production-v1/project/docs/ops.md +3 -0
  8. package/benchmarks/production-v1/project/logs/incident.log +2 -0
  9. package/benchmarks/production-v1/project/package.json +11 -0
  10. package/benchmarks/production-v1/project/scripts/check.mjs +21 -0
  11. package/benchmarks/production-v1/project/src/backend/auth.js +3 -0
  12. package/benchmarks/production-v1/project/src/backend/cache.js +15 -0
  13. package/benchmarks/production-v1/project/src/backend/invoice.js +4 -0
  14. package/benchmarks/production-v1/project/src/data/csv.js +3 -0
  15. package/benchmarks/production-v1/project/src/data/dedup.js +5 -0
  16. package/benchmarks/production-v1/project/src/data/migration.js +9 -0
  17. package/benchmarks/production-v1/project/src/frontend/pagination.js +8 -0
  18. package/benchmarks/production-v1/project/src/frontend/search-state.js +8 -0
  19. package/benchmarks/production-v1/project/src/frontend/unicode-search.js +7 -0
  20. package/benchmarks/production-v1/project/src/platform/args.js +10 -0
  21. package/benchmarks/production-v1/project/src/platform/config.js +7 -0
  22. package/benchmarks/production-v1/project/src/platform/workspace.js +13 -0
  23. package/benchmarks/production-v1/project/src/reliability/expiry.js +5 -0
  24. package/benchmarks/production-v1/project/src/reliability/retry.js +15 -0
  25. package/benchmarks/production-v1/project/test/smoke.test.js +12 -0
  26. package/benchmarks/production-v1/project/vendor/README.md +1 -0
  27. package/benchmarks/production-v1/prompts/bounded-retry.md +7 -0
  28. package/benchmarks/production-v1/prompts/cli-double-dash.md +7 -0
  29. package/benchmarks/production-v1/prompts/config-precedence.md +6 -0
  30. package/benchmarks/production-v1/prompts/destructive-history-refusal.md +3 -0
  31. package/benchmarks/production-v1/prompts/expiry-boundary.md +7 -0
  32. package/benchmarks/production-v1/prompts/incident-diagnosis.md +4 -0
  33. package/benchmarks/production-v1/prompts/invoice-rounding.md +9 -0
  34. package/benchmarks/production-v1/prompts/pagination-boundary.md +7 -0
  35. package/benchmarks/production-v1/prompts/protected-env-refusal.md +2 -0
  36. package/benchmarks/production-v1/prompts/quoted-csv.md +6 -0
  37. package/benchmarks/production-v1/prompts/repository-prompt-injection.md +5 -0
  38. package/benchmarks/production-v1/prompts/schema-migration.md +6 -0
  39. package/benchmarks/production-v1/prompts/stable-dedup.md +6 -0
  40. package/benchmarks/production-v1/prompts/stale-search-response.md +6 -0
  41. package/benchmarks/production-v1/prompts/tenant-cache-isolation.md +6 -0
  42. package/benchmarks/production-v1/prompts/tenant-role-authorization.md +6 -0
  43. package/benchmarks/production-v1/prompts/unicode-search.md +6 -0
  44. package/benchmarks/production-v1/prompts/workspace-order.md +7 -0
  45. package/benchmarks/production-v1/suite.json +284 -0
  46. package/benchmarks/production-v1/variant.mjs +112 -0
  47. package/docs/command-reference-vietnamese.md +15 -6
  48. package/docs/distribution-standard.md +7 -7
  49. package/docs/operator-manual-vietnamese.md +29 -13
  50. package/docs/quality-benchmark.md +222 -18
  51. package/docs/release-install-policy.md +15 -11
  52. package/docs/runtime-policy-design.md +2 -1
  53. package/docs/runtime-quality-baseline.md +13 -2
  54. package/docs/security-threat-model.md +1 -1
  55. package/docs/team-onboarding.md +2 -2
  56. package/docs-site/architecture.html +2 -1
  57. package/docs-site/assets/docs.css +407 -0
  58. package/docs-site/benchmark.html +339 -0
  59. package/docs-site/commands.html +2 -1
  60. package/docs-site/content/benchmark.html +214 -0
  61. package/docs-site/content/index.html +5 -0
  62. package/docs-site/content/runtime.html +7 -1
  63. package/docs-site/context-engine.html +4 -3
  64. package/docs-site/index.html +7 -1
  65. package/docs-site/mcp-approval.html +2 -1
  66. package/docs-site/mcp-auth.html +2 -1
  67. package/docs-site/mcp-servers.html +2 -1
  68. package/docs-site/mcp.html +2 -1
  69. package/docs-site/permissions.html +2 -1
  70. package/docs-site/profiles.html +2 -1
  71. package/docs-site/quickstart.html +2 -1
  72. package/docs-site/runtime.html +11 -4
  73. package/docs-site/scope.html +2 -1
  74. package/docs-site/security.html +2 -1
  75. package/docs-site/team.html +2 -1
  76. package/docs-site/workflows.html +2 -1
  77. package/package.json +1 -1
  78. package/packages/piagent-core/benchmark/benchmark-core.js +325 -41
  79. package/packages/piagent-core/extensions/piagent-guard.ts +47 -12
  80. package/packages/piagent-core/package.json +1 -1
  81. package/scripts/benchmark-runner.mjs +328 -68
  82. package/scripts/build-docs-site.mjs +6 -0
  83. package/scripts/verify-local.sh +4 -0
@@ -23,6 +23,7 @@ import { summarizeSession, walkJsonl } from "./pi-usage-history.mjs";
23
23
 
24
24
  const packageRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
25
25
  const coreSuitePath = path.join(packageRoot, "benchmarks", "core-v1", "suite.json");
26
+ const productionSuitePath = path.join(packageRoot, "benchmarks", "production-v1", "suite.json");
26
27
  const packageManifest = JSON.parse(fs.readFileSync(path.join(packageRoot, "package.json"), "utf8"));
27
28
  const outputLimit = 4 * 1024 * 1024;
28
29
  const activeChildren = new Set();
@@ -45,6 +46,12 @@ const controlledCodexFeatures = [
45
46
  "tool_suggest",
46
47
  "workspace_dependencies"
47
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
+ ];
48
55
  let interruptedSignal;
49
56
 
50
57
  const usage = `Usage:
@@ -57,12 +64,17 @@ With no options it uses the built-in core-v1 suite, the current Pi default
57
64
  model/thinking setting, and the suite's repeat count.
58
65
 
59
66
  Options:
60
- --suite <core-v1|path> Built-in suite id or suite.json path.
67
+ --suite <id|path> core-v1, production-v1, or suite.json path.
68
+ --production Alias for --suite production-v1.
61
69
  --surfaces <a,b> raw-pi,piagent (default) or piagent,codex-cli.
62
70
  --model <provider/model> Pin one model identity for both surfaces.
63
71
  --thinking <level> off|minimal|low|medium|high|xhigh|max.
64
72
  --codex-mode <mode> controlled isolated home (default) or native user configuration.
65
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.
66
78
  --timeout <seconds> Per-agent timeout, 30-3600 seconds.
67
79
  --output <directory> Report directory; must be empty or absent.
68
80
  --keep-workspaces Retain isolated workspaces and session logs.
@@ -130,7 +142,11 @@ export function parseBenchmarkArgs(argv) {
130
142
  model: undefined,
131
143
  thinking: undefined,
132
144
  codexMode: "controlled",
145
+ seed: undefined,
133
146
  repeats: undefined,
147
+ infrastructureRetries: undefined,
148
+ retryDelaySeconds: undefined,
149
+ scenarioIds: undefined,
134
150
  timeoutSeconds: undefined,
135
151
  output: undefined,
136
152
  keepWorkspaces: false,
@@ -149,6 +165,9 @@ export function parseBenchmarkArgs(argv) {
149
165
  options.suite = requireValue(argv, index, arg);
150
166
  index += 1;
151
167
  break;
168
+ case "--production":
169
+ options.suite = "production-v1";
170
+ break;
152
171
  case "--surfaces": {
153
172
  const values = requireValue(argv, index, arg).split(",").map((value) => value.trim()).filter(Boolean);
154
173
  if (values.length !== 2 || new Set(values).size !== 2 || values.some((value) => !benchmarkSurfaces.has(value))) {
@@ -179,6 +198,26 @@ export function parseBenchmarkArgs(argv) {
179
198
  options.repeats = positiveInteger(requireValue(argv, index, arg), arg, 1, 10);
180
199
  index += 1;
181
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;
182
221
  case "--timeout":
183
222
  options.timeoutSeconds = positiveInteger(requireValue(argv, index, arg), arg, 30, 3600);
184
223
  index += 1;
@@ -232,7 +271,7 @@ function runCommand(command, args, options = {}) {
232
271
  const cleanup = () => activeChildren.delete(child);
233
272
  if (options.inherit) {
234
273
  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 }); });
274
+ child.once("exit", (code, signal) => { cleanup(); resolve({ code: code ?? 1, signal, timedOut: false, stdout: "", stderr: "", forbiddenHits: [], requiredHits: [], durationSeconds: (Date.now() - started) / 1000 }); });
236
275
  return;
237
276
  }
238
277
  if (options.input !== undefined) {
@@ -243,15 +282,22 @@ function runCommand(command, args, options = {}) {
243
282
  let stderr = "";
244
283
  const stdoutDigest = crypto.createHash("sha256");
245
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);
246
286
  const forbiddenHits = new Set();
287
+ const requiredHits = new Set();
247
288
  const scanTails = { stdout: "", stderr: "" };
248
- const scanWindow = Math.max(0, ...forbidden.map((value) => value.length - 1));
289
+ const scanWindow = Math.max(0, ...[...forbidden, ...required].map((value) => value.length - 1));
249
290
  const append = (current, chunk) => `${current}${chunk}`.slice(-outputLimit);
250
291
  const inspect = (stream, chunk) => {
251
292
  const text = `${scanTails[stream]}${chunk}`;
252
293
  for (const value of forbidden) {
253
294
  if (text.includes(value)) forbiddenHits.add(value);
254
295
  }
296
+ if (stream === "stdout") {
297
+ for (const value of required) {
298
+ if (text.includes(value)) requiredHits.add(value);
299
+ }
300
+ }
255
301
  scanTails[stream] = scanWindow > 0 ? text.slice(-scanWindow) : "";
256
302
  };
257
303
  child.stdout.on("data", (chunk) => {
@@ -279,13 +325,14 @@ function runCommand(command, args, options = {}) {
279
325
  child.once("exit", (code, signal) => {
280
326
  if (timer) clearTimeout(timer);
281
327
  cleanup();
282
- resolve({ code: code ?? 1, signal, timedOut, stdout, stdoutHash: stdoutDigest.digest("hex"), stderr, forbiddenHits: [...forbiddenHits], durationSeconds: (Date.now() - started) / 1000 });
328
+ resolve({ code: code ?? 1, signal, timedOut, stdout, stdoutHash: stdoutDigest.digest("hex"), stderr, forbiddenHits: [...forbiddenHits], requiredHits: [...requiredHits], durationSeconds: (Date.now() - started) / 1000 });
283
329
  });
284
330
  });
285
331
  }
286
332
 
287
333
  function resolveSuitePath(input) {
288
334
  if (input === "core-v1") return coreSuitePath;
335
+ if (input === "production-v1") return productionSuitePath;
289
336
  const candidate = path.resolve(input);
290
337
  return fs.existsSync(candidate) && fs.statSync(candidate).isDirectory()
291
338
  ? path.join(candidate, "suite.json")
@@ -343,9 +390,15 @@ function validateSuiteFiles(suite, suiteRoot) {
343
390
  const fixture = resolveSuiteEntry(suiteRoot, scenario.fixture, "fixture");
344
391
  const prompt = resolveSuiteEntry(suiteRoot, scenario.prompt, "prompt");
345
392
  const grader = resolveSuiteEntry(suiteRoot, scenario.grader, "grader");
393
+ const generator = scenario.variantGenerator
394
+ ? resolveSuiteEntry(suiteRoot, scenario.variantGenerator, "variant generator")
395
+ : null;
346
396
  if (inside(fixture, grader) || inside(fixture, prompt)) {
347
397
  fail(`Suite prompt and grader must stay outside the agent fixture: ${scenario.id}`, 1);
348
398
  }
399
+ if (generator && inside(fixture, generator)) {
400
+ fail(`Suite variant generator must stay outside the agent fixture: ${scenario.id}`, 1);
401
+ }
349
402
  }
350
403
  }
351
404
 
@@ -540,6 +593,46 @@ function applySetupFiles(workspace, setupFiles) {
540
593
  }
541
594
  }
542
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
+
543
636
  async function initializeTreatment(workspace, profile) {
544
637
  const result = await runCommand("bash", [
545
638
  path.join(packageRoot, "scripts", "init-project.sh"),
@@ -724,8 +817,9 @@ function parseGraderResult(stdout) {
724
817
  };
725
818
  }
726
819
 
727
- async function gradeWorkspace(grader, workspace, scenario, timeoutSeconds) {
728
- const result = await runCommand(process.execPath, [grader, workspace], {
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, {
729
823
  cwd: path.dirname(grader),
730
824
  timeoutMs: Math.min(timeoutSeconds, 120) * 1_000,
731
825
  env: graderEnvironment(scenario.id)
@@ -739,7 +833,7 @@ function sessionSummaries(sessionDir) {
739
833
  return walkJsonl(sessionDir).map((file) => summarizeSession(file, {})).filter(Boolean);
740
834
  }
741
835
 
742
- function failureReason({ agent, grade, graderIntegrity, outsideScope, forbiddenHits }) {
836
+ function failureReason({ agent, grade, graderIntegrity, outsideScope, forbiddenHits, missingRequired }) {
743
837
  const failures = [];
744
838
  if (agent.timedOut) failures.push("agent-timeout");
745
839
  else if (agent.code !== 0) failures.push(`agent-exit-${agent.code}`);
@@ -747,34 +841,81 @@ function failureReason({ agent, grade, graderIntegrity, outsideScope, forbiddenH
747
841
  if (!graderIntegrity.passed) failures.push("grader-mutated-workspace");
748
842
  if (outsideScope.length) failures.push(`outside-scope:${outsideScope.join(",")}`);
749
843
  if (forbiddenHits.length) failures.push("forbidden-output");
844
+ if (missingRequired.length) failures.push("required-output-missing");
750
845
  return failures.join("; ") || undefined;
751
846
  }
752
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
+
753
860
  async function runOne({
754
861
  suite,
755
862
  suiteRoot,
756
863
  scenario,
757
864
  surface,
758
865
  repeat,
866
+ infrastructureAttempt = 1,
759
867
  runId,
760
868
  runRoot,
761
869
  options,
762
870
  piCommand,
763
871
  codexCommand,
764
872
  codexDisabledFeatures,
765
- codexRuntime
873
+ codexRuntime,
874
+ suiteDigest,
875
+ rootSeed
766
876
  }) {
767
- const key = `${String(repeat).padStart(2, "0")}-${scenario.id}-${surface}`;
877
+ const attemptSuffix = infrastructureAttempt > 1 ? `-infra-${infrastructureAttempt}` : "";
878
+ const key = `${String(repeat).padStart(2, "0")}-${scenario.id}-${surface}${attemptSuffix}`;
768
879
  const workspaceRoot = privateDirectory(path.join(runRoot, "workspaces", key));
769
880
  const workspace = path.join(workspaceRoot, "project");
770
881
  const sessions = privateDirectory(path.join(workspaceRoot, "sessions"));
771
882
  const fixture = resolveSuiteEntry(suiteRoot, scenario.fixture, "fixture");
772
883
  fs.cpSync(fixture, workspace, { recursive: true, errorOnExist: true });
773
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
+ ])];
774
913
  if (surface === "piagent") {
775
- await initializeTreatment(workspace, suite.profile);
776
- prepareTreatmentBaseline(workspace, suite.profile, scenario);
777
- await prepareTreatmentContextEngine(workspace);
914
+ await initializeTreatment(workspace, profile);
915
+ if (lifecycle === "steady-state") {
916
+ prepareTreatmentBaseline(workspace, profile, scenario);
917
+ await prepareTreatmentContextEngine(workspace);
918
+ }
778
919
  }
779
920
  await initializeGit(workspace, scenario.setupFiles);
780
921
 
@@ -817,14 +958,15 @@ async function runOne({
817
958
  const codexCollector = surface === "codex-cli" ? createCodexExecJsonlCollector({
818
959
  model: options.model,
819
960
  thinkingLevel: options.thinking,
820
- onEvent: (event) => inspectForbiddenValue(event, scenario.forbiddenOutputSubstrings ?? [], codexForbiddenHits)
961
+ onEvent: (event) => inspectForbiddenValue(event, forbiddenOutputSubstrings, codexForbiddenHits)
821
962
  }) : undefined;
822
963
 
823
964
  const agent = await runCommand(command, args, {
824
965
  cwd: workspace,
825
966
  input: surface === "codex-cli" ? prompt : undefined,
826
967
  timeoutMs: options.timeoutSeconds * 1_000,
827
- forbiddenSubstrings: scenario.forbiddenOutputSubstrings,
968
+ forbiddenSubstrings: forbiddenOutputSubstrings,
969
+ requiredSubstrings: requiredOutputSubstrings,
828
970
  onStdoutChunk: codexCollector ? (chunk) => codexCollector.write(chunk) : undefined,
829
971
  env: surface === "codex-cli" ? codexProcessEnvironment(codexRuntime, {
830
972
  PIAGENT_NO_UPDATE_CHECK: "1",
@@ -832,6 +974,8 @@ async function runOne({
832
974
  PIAGENT_BENCHMARK_SCENARIO: scenario.id,
833
975
  PIAGENT_BENCHMARK_SURFACE: surface,
834
976
  PIAGENT_BENCHMARK_SESSION_ID: sessionId,
977
+ PIAGENT_BENCHMARK_PROFILE: profile,
978
+ PIAGENT_BENCHMARK_LIFECYCLE: lifecycle,
835
979
  NO_COLOR: "1"
836
980
  }) : benchmarkEnvironment({
837
981
  PIAGENT_NO_UPDATE_CHECK: "1",
@@ -839,11 +983,14 @@ async function runOne({
839
983
  PIAGENT_BENCHMARK_SCENARIO: scenario.id,
840
984
  PIAGENT_BENCHMARK_SURFACE: surface,
841
985
  PIAGENT_BENCHMARK_SESSION_ID: sessionId,
986
+ PIAGENT_BENCHMARK_PROFILE: profile,
987
+ PIAGENT_BENCHMARK_LIFECYCLE: lifecycle,
842
988
  NO_COLOR: "1"
843
989
  })
844
990
  });
845
991
  const sessionFiles = surface === "codex-cli" ? [] : walkJsonl(sessions);
846
992
  let usage;
993
+ let codexDiagnostics = [];
847
994
  if (surface === "codex-cli") {
848
995
  try {
849
996
  usage = codexCollector.finish();
@@ -851,6 +998,7 @@ async function runOne({
851
998
  if (agent.code === 0 && !agent.timedOut) throw error;
852
999
  usage = aggregateSessionUsage([]);
853
1000
  }
1001
+ codexDiagnostics = codexCollector.diagnostics();
854
1002
  } else {
855
1003
  usage = aggregateSessionUsage(sessionSummaries(sessions));
856
1004
  }
@@ -858,54 +1006,104 @@ async function runOne({
858
1006
  ...(agent.forbiddenHits ?? []),
859
1007
  ...(surface === "codex-cli"
860
1008
  ? [...codexForbiddenHits]
861
- : forbiddenSessionHits(sessionFiles, scenario.forbiddenOutputSubstrings ?? []))
1009
+ : forbiddenSessionHits(sessionFiles, forbiddenOutputSubstrings))
862
1010
  ])];
863
- const changedFiles = workingTreeFiles(workspace);
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));
864
1022
  const beforeGrade = workingTreeSnapshot(workspace);
865
1023
  const outsideScope = changedFiles.filter((file) => !matchesAnyPath(file, scenario.allowedChanges));
866
- const grade = await gradeWorkspace(graderPath, workspace, scenario, options.timeoutSeconds);
1024
+ const grade = await gradeWorkspace(graderPath, workspace, scenario, options.timeoutSeconds, variant.oraclePath);
867
1025
  const afterGrade = workingTreeSnapshot(workspace);
868
1026
  const graderIntegrity = { passed: JSON.stringify(beforeGrade) === JSON.stringify(afterGrade) };
869
1027
  let workflow = null;
870
- if (surface === "piagent" && scenario.kind === "source-change") {
1028
+ if (surface === "piagent" && scenario.kind !== "safety-refusal") {
871
1029
  const task = listTaskContracts(workspace).find((item) => item.sessionId === sessionId);
872
- workflow = evaluateWorkflowEvidence(task, changedFiles, usage.toolNames);
1030
+ workflow = evaluateWorkflowEvidence(task, changedFiles, usage.toolNames, { scenarioKind: scenario.kind });
873
1031
  }
874
- const scope = { passed: outsideScope.length === 0, changedFiles, outsideScope };
1032
+ const scope = { passed: outsideScope.length === 0, changedFiles, outsideScope, allChangedFiles, runtimeManagedChanges };
875
1033
  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;
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;
878
1047
  const record = {
879
1048
  schemaVersion: 1,
880
1049
  scenarioId: scenario.id,
881
1050
  scenarioTitle: scenario.title,
882
1051
  scenarioKind: scenario.kind,
1052
+ category: scenario.category ?? "unspecified",
1053
+ difficulty: scenario.difficulty ?? "unspecified",
1054
+ profile,
1055
+ lifecycle,
883
1056
  surface,
884
1057
  repeat,
1058
+ infrastructureAttempt,
885
1059
  sessionId,
886
1060
  providerSessionId: usage.providerSessionId ?? null,
887
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,
888
1065
  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") },
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
+ },
891
1074
  grade,
892
1075
  graderIntegrity,
893
1076
  scope,
894
1077
  outputSafety,
1078
+ outputEvidence,
895
1079
  workflow,
896
1080
  usage,
897
1081
  durationSeconds: agent.durationSeconds,
898
- promptHash: crypto.createHash("sha256").update(prompt).digest("hex")
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 }
899
1088
  };
900
1089
  if (!options.keepWorkspaces) fs.rmSync(workspaceRoot, { recursive: true, force: true });
901
1090
  return record;
902
1091
  }
903
1092
 
904
- function executionOrder(suite, repeats, surfaces) {
1093
+ function executionOrder(suite, repeats, surfaces, rootSeed) {
905
1094
  const order = [];
906
1095
  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();
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;
909
1107
  for (const surface of ordered) order.push({ scenario, surface, repeat });
910
1108
  }
911
1109
  }
@@ -987,29 +1185,44 @@ async function main() {
987
1185
  }
988
1186
  const { suite, manifestPath, suiteRoot } = loadSuite(options.suite);
989
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
+ }
990
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);
991
1198
  options.timeoutSeconds = options.timeoutSeconds ?? suite.timeoutSeconds;
992
1199
  if (options.surfaces.includes("codex-cli")) {
993
1200
  codexModelName(options.model);
994
1201
  codexThinkingEffort(options.thinking);
995
1202
  }
996
1203
  const comparison = comparisonSurfaces(options);
997
- const order = executionOrder(suite, options.repeats, options.surfaces);
998
1204
  const piCommand = process.env.PIAGENT_BENCHMARK_PI_COMMAND || "pi";
999
1205
  const codexCommand = process.env.PIAGENT_BENCHMARK_CODEX_COMMAND || "codex";
1000
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"))];
1001
1211
  const plan = [
1002
1212
  "Piagent automatic benchmark",
1003
1213
  ` platform: v${packageManifest.version}`,
1004
- ` suite: ${suite.id} (${suite.scenarios.length} scenarios)`,
1214
+ ` suite: ${suite.id} (${suite.scenarios.length}${suite.scenarios.length !== declaredScenarioCount ? `/${declaredScenarioCount}` : ""} scenarios)`,
1005
1215
  ` digest: ${suiteDigest.slice(0, 16)}`,
1006
1216
  ` surfaces: ${options.surfaces.join(", ")}`,
1007
1217
  ` compare: ${benchmarkSurfaceLabel(comparison.candidateSurface)} vs ${benchmarkSurfaceLabel(comparison.baselineSurface)}`,
1008
1218
  ` repeats: ${options.repeats}`,
1219
+ ` retries: ${options.infrastructureRetries} infrastructure-only · ${options.retryDelaySeconds}s backoff`,
1009
1220
  ` sessions: ${order.length}`,
1010
1221
  ` model: ${options.model ?? "Pi default"}`,
1011
1222
  ` thinking: ${options.thinking ?? "Pi default"}`,
1012
- " lifecycle: Piagent steady-state (initialized and onboarded before measurement)",
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"}`,
1013
1226
  ` timeout: ${options.timeoutSeconds}s per session`,
1014
1227
  " grading: hidden verifier + scope + output safety + Pi task evidence"
1015
1228
  ].join("\n");
@@ -1041,50 +1254,89 @@ async function main() {
1041
1254
  const runs = [];
1042
1255
  let fatalRunError;
1043
1256
  const ledgerPath = path.join(runRoot, "runs.jsonl");
1257
+ const infrastructureLedgerPath = path.join(runRoot, "infrastructure-attempts.jsonl");
1044
1258
  try {
1045
1259
  for (const [index, item] of order.entries()) {
1046
1260
  if (interruptedSignal) break;
1047
1261
  process.stdout.write(`[${index + 1}/${order.length}] ${item.scenario.id} · ${item.surface} · repeat ${item.repeat}\n`);
1048
1262
  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
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
1061
1321
  });
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
- };
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
+ }
1082
1331
  }
1332
+ record.infrastructureAttempts = record.infrastructureAttempt ?? 1;
1333
+ record.infrastructureRetries = Math.max(0, record.infrastructureAttempts - 1);
1334
+ record.infrastructureFailures = infrastructureFailures;
1083
1335
  runs.push(record);
1084
1336
  fs.appendFileSync(ledgerPath, `${JSON.stringify(record)}\n`, { mode: 0o600 });
1085
1337
  const cost = Number.isFinite(record.usage.cost) ? `$${Number(record.usage.cost).toFixed(6)}` : "cost n/a";
1086
1338
  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");
1339
+ if (record.abortSuite && !fatalRunError) fatalRunError = new Error(record.infrastructureFailure ?? record.failure ?? "agent startup failure");
1088
1340
  if (fatalRunError) break;
1089
1341
  }
1090
1342
  } finally {
@@ -1113,12 +1365,17 @@ async function main() {
1113
1365
  environment: {
1114
1366
  platformVersion: packageManifest.version,
1115
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",
1116
1371
  profile: suite.profile,
1117
1372
  requestedModel: options.model ?? null,
1118
1373
  requestedThinking: options.thinking ?? null,
1119
- treatmentBaseline: options.surfaces.includes("codex-cli")
1120
- ? "piagent-initialized-and-onboarded; codex-clean-fixture"
1121
- : "initialized-and-onboarded",
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",
1122
1379
  timeoutSeconds: options.timeoutSeconds,
1123
1380
  nodeVersion: process.version,
1124
1381
  piVersion: runtime.piVersion,
@@ -1126,7 +1383,7 @@ async function main() {
1126
1383
  codexMode: options.surfaces.includes("codex-cli") ? options.codexMode : null,
1127
1384
  codexAuth: runtime.codexAuth ?? null,
1128
1385
  codexIsolation: options.surfaces.includes("codex-cli")
1129
- ? options.codexMode === "controlled" ? "temporary-home" : "operator-home"
1386
+ ? options.codexMode === "controlled" ? "per-session-temporary-home" : "operator-home"
1130
1387
  : null,
1131
1388
  codexCredentialBridge: options.surfaces.includes("codex-cli") ? codexRuntime.credentialBridge : null,
1132
1389
  codexGlobalInstructions: options.surfaces.includes("codex-cli")
@@ -1134,6 +1391,7 @@ async function main() {
1134
1391
  : null,
1135
1392
  codexDisabledFeatures: runtime.codexDisabledFeatures,
1136
1393
  surfaces: options.surfaces,
1394
+ scenarioSelection: options.scenarioIds ?? null,
1137
1395
  surfaceModels: options.surfaces.includes("codex-cli") ? {
1138
1396
  piagent: options.model,
1139
1397
  "codex-cli": codexModelName(options.model)
@@ -1158,6 +1416,8 @@ async function main() {
1158
1416
  || report.comparison.reliabilityGate === false
1159
1417
  || report.comparison.qualityNonInferior === false
1160
1418
  || report.comparison.workflowGate === false
1419
+ || report.comparison.categoryGate === false
1420
+ || report.comparison.productionGate?.passed === false
1161
1421
  ) process.exitCode = 1;
1162
1422
  } finally {
1163
1423
  codexRuntime.cleanup();