@bjoernrochel/sevro 0.1.0-rc.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (71) hide show
  1. package/LICENSE +159 -0
  2. package/README.md +71 -0
  3. package/docs/advisory-fixture.md +23 -0
  4. package/docs/builtin-graders-v1.md +77 -0
  5. package/docs/cache-ownership-validation.md +49 -0
  6. package/docs/claude-continuation-validation.md +47 -0
  7. package/docs/codex-host.md +134 -0
  8. package/docs/concurrency-validation.md +86 -0
  9. package/docs/configuration-root-validation.md +70 -0
  10. package/docs/development-cli.md +327 -0
  11. package/docs/engine-slice.md +114 -0
  12. package/docs/extension-protocol-v1.md +292 -0
  13. package/docs/fixture-cleanup-validation.md +70 -0
  14. package/docs/identity-v1.md +65 -0
  15. package/docs/native-controls.md +29 -0
  16. package/docs/releases.md +88 -0
  17. package/docs/report-v1.md +32 -0
  18. package/docs/results-v1.md +118 -0
  19. package/examples/basic/graded.json +13 -0
  20. package/examples/basic/host.ts +8 -0
  21. package/examples/basic/prompt-only.json +7 -0
  22. package/package.json +45 -0
  23. package/schemas/cli-result-v1.schema.json +184 -0
  24. package/schemas/extension-v1.schema.json +659 -0
  25. package/schemas/report-v1.schema.json +164 -0
  26. package/schemas/run-evidence-v1.schema.json +510 -0
  27. package/src/advisory-fixture.ts +282 -0
  28. package/src/advisory.ts +105 -0
  29. package/src/cli.ts +837 -0
  30. package/src/engine.ts +2341 -0
  31. package/src/extension-client.ts +257 -0
  32. package/src/extension-session.ts +335 -0
  33. package/src/fixture-bin.ts +87 -0
  34. package/src/fixture-cleanup.ts +38 -0
  35. package/src/fixture-setup.ts +163 -0
  36. package/src/generated-fixture.ts +220 -0
  37. package/src/git-hooks.ts +63 -0
  38. package/src/graders/git-head.ts +176 -0
  39. package/src/graders/output.ts +359 -0
  40. package/src/graders/semantic.ts +120 -0
  41. package/src/graders/shell.ts +277 -0
  42. package/src/hosts/claude-continuation.ts +94 -0
  43. package/src/hosts/claude-credential.ts +73 -0
  44. package/src/hosts/claude-events.ts +128 -0
  45. package/src/hosts/claude-nested-skills.ts +218 -0
  46. package/src/hosts/claude-repository-invocation.ts +239 -0
  47. package/src/hosts/claude-settings.ts +50 -0
  48. package/src/hosts/claude-tool-calls.ts +159 -0
  49. package/src/hosts/claude.ts +511 -0
  50. package/src/hosts/codex-configuration.ts +85 -0
  51. package/src/hosts/codex-events.ts +102 -0
  52. package/src/hosts/codex-native-calls.ts +702 -0
  53. package/src/hosts/codex-profile.ts +78 -0
  54. package/src/hosts/codex-skill-reads.ts +345 -0
  55. package/src/hosts/codex.ts +769 -0
  56. package/src/hosts/isolation-roots.ts +60 -0
  57. package/src/hosts/mac-sandbox.ts +108 -0
  58. package/src/hosts/native-controls.ts +124 -0
  59. package/src/hosts/protected-worktrees.ts +56 -0
  60. package/src/hosts/workspace-fingerprint.ts +55 -0
  61. package/src/identity.ts +156 -0
  62. package/src/instrumentation.ts +106 -0
  63. package/src/preparation.ts +211 -0
  64. package/src/provenance.ts +244 -0
  65. package/src/report.ts +351 -0
  66. package/src/repository-fixture.ts +286 -0
  67. package/src/results.ts +160 -0
  68. package/src/run-owner.ts +276 -0
  69. package/src/schema.ts +28 -0
  70. package/src/storage.ts +23 -0
  71. package/src/trial-scheduler.ts +27 -0
package/src/engine.ts ADDED
@@ -0,0 +1,2341 @@
1
+ import { createHash, randomUUID } from "node:crypto";
2
+ import {
3
+ advisoryPrompt,
4
+ parseAdvisoryAssessment,
5
+ type AdvisoryAssessment,
6
+ } from "./advisory";
7
+ import {
8
+ advisoryBaseRevision,
9
+ buildBlindAdvisoryFixture,
10
+ } from "./advisory-fixture";
11
+ import {
12
+ appendFile,
13
+ mkdir,
14
+ mkdtemp,
15
+ readFile,
16
+ readdir,
17
+ realpath,
18
+ rm,
19
+ writeFile,
20
+ } from "node:fs/promises";
21
+ import { tmpdir } from "node:os";
22
+ import { dirname, isAbsolute, join, resolve } from "node:path";
23
+ import { fileURLToPath, pathToFileURL } from "node:url";
24
+ import {
25
+ gradeOutput,
26
+ isOutputGrader,
27
+ prepareOutputChecks,
28
+ type OutputCheckDeclaration,
29
+ } from "./graders/output";
30
+ import {
31
+ assessGitHeadCheck,
32
+ gitHeadRevision,
33
+ gitHeadState,
34
+ prepareGitHeadChecks,
35
+ type GitHeadCheckDeclaration,
36
+ } from "./graders/git-head";
37
+ import {
38
+ assessShellCheck,
39
+ prepareShellChecks,
40
+ runShellCheck,
41
+ type ShellCheckDeclaration,
42
+ } from "./graders/shell";
43
+ import {
44
+ parseSemanticVerdicts,
45
+ prepareSemanticChecks,
46
+ semanticPrompt,
47
+ type SemanticCheckDeclaration,
48
+ } from "./graders/semantic";
49
+ import { evaluationProtectedRoots } from "./hosts/isolation-roots";
50
+ import { canonicalJson, createEvaluationIdentity, hashJson } from "./identity";
51
+ import { fixtureBinDirectory, installFixtureBin } from "./fixture-bin";
52
+ import { clearFixtureContents } from "./fixture-cleanup";
53
+ import { installGitHooks } from "./git-hooks";
54
+ import {
55
+ InstrumentationEvidenceError,
56
+ prepareInstrumentation,
57
+ verifyAppliedInstrumentation,
58
+ type InstrumentationCapability,
59
+ type InstrumentationRequest,
60
+ } from "./instrumentation";
61
+ import {
62
+ materializeGeneratedFixture,
63
+ prepareGeneratedFixture,
64
+ type GeneratedFixture,
65
+ } from "./generated-fixture";
66
+ import {
67
+ prepareFixtureSetup,
68
+ runFixtureSetup,
69
+ type FixtureSetup,
70
+ } from "./fixture-setup";
71
+ import {
72
+ fixtureParts,
73
+ prepareArtifacts,
74
+ safePreparationTarget,
75
+ type InlineArtifact,
76
+ type PreparationSources,
77
+ } from "./preparation";
78
+ import {
79
+ applyRepositoryOverlay,
80
+ cloneRepositorySource,
81
+ prepareRepositoryFixture,
82
+ resolveRepositorySource,
83
+ type RepositoryFixture,
84
+ type RepositorySource,
85
+ } from "./repository-fixture";
86
+ import type { EvaluationResult, ExtensionCase } from "./extension-session";
87
+ import { openExtensionSession } from "./extension-session";
88
+ import {
89
+ applyTaskVerdictPolicy,
90
+ assessTrial,
91
+ exitCodeFor,
92
+ summarizeAssessments,
93
+ summarizeCases,
94
+ type Assessment,
95
+ type CheckOutcome,
96
+ } from "./results";
97
+ import { assertCliResult, assertRunEvidence } from "./schema";
98
+ import { atomicWriteJson } from "./storage";
99
+ import { projectProvenance, runnerProvenance } from "./provenance";
100
+ import { checkpointRunOwner, startRunOwner } from "./run-owner";
101
+ import { scheduleTrials } from "./trial-scheduler";
102
+
103
+ const MAX_FINAL_MESSAGE_BYTES = 8 * 1024 * 1024;
104
+
105
+ export class EvaluationConfigurationError extends Error {
106
+ constructor(message: string) {
107
+ super(message);
108
+ this.name = "EvaluationConfigurationError";
109
+ }
110
+ }
111
+
112
+ export interface HostResult {
113
+ finalMessage: string | null;
114
+ complete: boolean;
115
+ executionFailed?: boolean;
116
+ artifacts?: { id: string; bytes: Uint8Array }[];
117
+ observations?: {
118
+ id: string;
119
+ completeness: "complete" | "partial" | "unavailable";
120
+ data: Record<string, unknown>;
121
+ }[];
122
+ actualCondition?: "passive" | "enforced" | "unknown";
123
+ appliedInstrumentation?: InstrumentationRequest[];
124
+ inputTokens?: number | null;
125
+ outputTokens?: number | null;
126
+ costUsd?: number | null;
127
+ usageComplete?: boolean;
128
+ }
129
+
130
+ export interface HostAdapter {
131
+ id: string;
132
+ model: string;
133
+ effort: string;
134
+ instrumentation?: InstrumentationCapability[];
135
+ hostCapabilities?: string[];
136
+ configuration?: Record<string, unknown>;
137
+ run(request: {
138
+ prompt: string;
139
+ followUpPrompt?: string;
140
+ workspace: string;
141
+ condition: "passive" | "enforced";
142
+ fixtureBinDir?: string;
143
+ instrumentation?: InstrumentationRequest[];
144
+ codexMarketplace?: {
145
+ artifactRoot: string;
146
+ marketplaceName: string;
147
+ pluginNames: string[];
148
+ artifactPaths: string[];
149
+ };
150
+ claudePluginDirs?: {
151
+ artifactRoots: string[];
152
+ artifactPaths: string[];
153
+ };
154
+ explicitSkillInvocation?:
155
+ | {
156
+ scope?: "plugin";
157
+ pluginName: string;
158
+ skillName: string;
159
+ token: string;
160
+ }
161
+ | {
162
+ scope: "repository";
163
+ pluginName?: never;
164
+ skillName: string;
165
+ token: string;
166
+ };
167
+ signal?: AbortSignal;
168
+ }): Promise<HostResult>;
169
+ }
170
+
171
+ export interface ResolvedCase {
172
+ id: string;
173
+ prompt: string;
174
+ followUpPrompt?: string;
175
+ fixture:
176
+ | { files: Record<string, string>; sourceRef?: never }
177
+ | Omit<RepositoryFixture, "kind">
178
+ | (GeneratedFixture & { sourceRef?: never });
179
+ checks: {
180
+ id: string;
181
+ grader: string;
182
+ configuration: Record<string, unknown>;
183
+ }[];
184
+ requiredEvidence: string[];
185
+ }
186
+
187
+ export interface EvaluationOptions {
188
+ projectRoot: string;
189
+ resultsRoot: string;
190
+ runStateRoot?: string;
191
+ case: ResolvedCase;
192
+ host: HostAdapter;
193
+ semanticHost?: HostAdapter;
194
+ advisoryHost?: HostAdapter;
195
+ advisoryExcludedPaths?: string[];
196
+ runnerBuildDigest: string;
197
+ runnerCheckoutRoot?: string;
198
+ projectDigest: string;
199
+ condition: "passive" | "enforced";
200
+ trialCount: number;
201
+ jobs?: number;
202
+ passThreshold: number;
203
+ dry?: boolean;
204
+ signal?: AbortSignal;
205
+ extension?: {
206
+ session: Awaited<ReturnType<typeof openExtensionSession>>;
207
+ resolvedCase: ExtensionCase;
208
+ };
209
+ preparationSources?: PreparationSources;
210
+ shellIsolation?: {
211
+ protectedRoots: string[];
212
+ toolchainBinDir?: string;
213
+ uvRuntimeCache?: boolean;
214
+ };
215
+ }
216
+
217
+ interface TrialSummary extends Assessment {
218
+ trial: number;
219
+ checks: CheckOutcome[];
220
+ domainOutcomes: NonNullable<EvaluationResult["domainOutcomes"]>;
221
+ artifactPath: string;
222
+ }
223
+
224
+ interface CaseSummary extends Assessment {
225
+ caseId: string;
226
+ trials: TrialSummary[];
227
+ }
228
+
229
+ export interface CliResult extends Assessment {
230
+ format: "sevro.cli-result.v1";
231
+ runId: string;
232
+ exitCode: number;
233
+ evidencePath: string;
234
+ cases: CaseSummary[];
235
+ }
236
+
237
+ async function createFixture(
238
+ fixture: ResolvedCase["fixture"],
239
+ artifacts: InlineArtifact[],
240
+ sources: PreparationSources | undefined,
241
+ repository: RepositorySource | null,
242
+ repositoryFixture: RepositoryFixture | null,
243
+ generated: GeneratedFixture | null,
244
+ setup: FixtureSetup | null,
245
+ projectRoot: string,
246
+ signal?: AbortSignal,
247
+ reservedWorkspace?: string,
248
+ ): Promise<string> {
249
+ const paths = Object.entries(
250
+ generated || repository ? {} : (fixture.files ?? {}),
251
+ ).map(([path, content]) => ({
252
+ path,
253
+ parts: fixtureParts(path),
254
+ content,
255
+ }));
256
+ const workspace =
257
+ reservedWorkspace ??
258
+ (await realpath(await mkdtemp(join(tmpdir(), "sevro-case-"))));
259
+ try {
260
+ if (repository)
261
+ await cloneRepositorySource(
262
+ fixture.sourceRef!,
263
+ sources!,
264
+ repository,
265
+ workspace,
266
+ );
267
+ if (generated) await materializeGeneratedFixture(generated, workspace);
268
+ if (repositoryFixture)
269
+ await applyRepositoryOverlay(repositoryFixture, workspace);
270
+ await installFixtureBin(
271
+ generated?.bin ?? repositoryFixture?.bin,
272
+ workspace,
273
+ );
274
+ await installGitHooks(
275
+ generated?.hooks ?? repositoryFixture?.hooks,
276
+ workspace,
277
+ );
278
+ for (const file of paths) {
279
+ const target = join(workspace, ...file.parts);
280
+ await mkdir(dirname(target), { recursive: true, mode: 0o700 });
281
+ await writeFile(target, file.content, { flag: "wx", mode: 0o600 });
282
+ }
283
+ if (setup)
284
+ await runFixtureSetup(setup, {
285
+ workspace,
286
+ projectRoot,
287
+ fixtureBinDir: (await fixtureBinDirectory(workspace)) ?? undefined,
288
+ signal,
289
+ });
290
+ for (const artifact of artifacts) {
291
+ const target = await safePreparationTarget(
292
+ workspace,
293
+ artifact.relativePath,
294
+ );
295
+ await writeFile(target, artifact.bytes, {
296
+ flag: "wx",
297
+ mode: artifact.executable ? 0o700 : 0o600,
298
+ });
299
+ }
300
+ const gitExcluded = artifacts.filter((artifact) => artifact.gitExclude);
301
+ if (gitExcluded.length) {
302
+ const special = new Set(["\\", "*", "?", "[", "]", "#", "!", " "]);
303
+ const patterns = gitExcluded.map((artifact) =>
304
+ [...artifact.relativePath]
305
+ .map((character) =>
306
+ special.has(character) ? `\\${character}` : character,
307
+ )
308
+ .join(""),
309
+ );
310
+ await appendFile(
311
+ join(workspace, ".git", "info", "exclude"),
312
+ `\n${patterns.map((path) => `/${path}`).join("\n")}\n`,
313
+ );
314
+ }
315
+ await fixtureBinDirectory(workspace);
316
+ return workspace;
317
+ } catch (error) {
318
+ if (!reservedWorkspace)
319
+ await rm(workspace, { recursive: true, force: true });
320
+ throw error;
321
+ }
322
+ }
323
+
324
+ function sha256(content: string | Uint8Array): string {
325
+ return createHash("sha256").update(content).digest("hex");
326
+ }
327
+
328
+ function hostObservations(
329
+ result: HostResult,
330
+ hostId: string,
331
+ existingIds: Set<string>,
332
+ ) {
333
+ const items = result.observations ?? [];
334
+ if (items.length > 128) throw new Error("too many host observations");
335
+ const ids = new Set([...existingIds, "sevro.observation.final-message"]);
336
+ let totalBytes = 0;
337
+ return items.map((item) => {
338
+ if (
339
+ !/^[a-z][a-z0-9]*(?:[.-][a-z0-9]+)+$/.test(item.id) ||
340
+ item.id.startsWith("sevro.observation.") ||
341
+ ids.has(item.id) ||
342
+ !["complete", "partial", "unavailable"].includes(item.completeness) ||
343
+ !item.data ||
344
+ typeof item.data !== "object" ||
345
+ Array.isArray(item.data)
346
+ )
347
+ throw new Error("invalid host observation");
348
+ ids.add(item.id);
349
+ const data = canonicalJson(item.data);
350
+ totalBytes += Buffer.byteLength(data, "utf8");
351
+ if (totalBytes > 8 * 1024 * 1024)
352
+ throw new Error("host observations exceed 8 MiB");
353
+ return {
354
+ id: item.id,
355
+ source: hostId,
356
+ completeness: item.completeness,
357
+ data: JSON.parse(data) as Record<string, unknown>,
358
+ };
359
+ });
360
+ }
361
+
362
+ function hostArtifacts(result: HostResult, existingIds: Set<string>) {
363
+ const items = result.artifacts ?? [];
364
+ if (!Array.isArray(items) || items.length > 32)
365
+ throw new Error("too many host artifacts");
366
+ const ids = new Set(existingIds);
367
+ let totalBytes = 0;
368
+ return items.map((item) => {
369
+ if (
370
+ !item ||
371
+ !/^[a-z][a-z0-9]*(?:[.-][a-z0-9]+)+$/.test(item.id) ||
372
+ ids.has(item.id) ||
373
+ !(item.bytes instanceof Uint8Array)
374
+ )
375
+ throw new Error("invalid host artifact");
376
+ ids.add(item.id);
377
+ totalBytes += item.bytes.byteLength;
378
+ if (
379
+ item.bytes.byteLength > 8 * 1024 * 1024 ||
380
+ totalBytes > 32 * 1024 * 1024
381
+ )
382
+ throw new Error("host artifacts exceed the size limit");
383
+ return { id: item.id, bytes: Buffer.from(item.bytes) };
384
+ });
385
+ }
386
+
387
+ async function verifyRetainedArtifacts(
388
+ artifacts: { path: string; sha256: string }[],
389
+ ): Promise<void> {
390
+ for (const artifact of artifacts) {
391
+ let actual: string;
392
+ try {
393
+ actual = sha256(await readFile(fileURLToPath(artifact.path)));
394
+ } catch {
395
+ throw new Error("retained preparation artifact is unreadable");
396
+ }
397
+ if (actual !== artifact.sha256)
398
+ throw new Error("retained preparation artifact changed");
399
+ }
400
+ }
401
+
402
+ function usage(result: HostResult | null) {
403
+ return {
404
+ inputTokens: result?.inputTokens ?? null,
405
+ outputTokens: result?.outputTokens ?? null,
406
+ costUsd: result?.costUsd ?? null,
407
+ complete: result?.usageComplete ?? false,
408
+ };
409
+ }
410
+
411
+ /** Execute resolved evaluator data through an injected host and retain every trial. */
412
+ export async function runEvaluation(
413
+ options: EvaluationOptions,
414
+ ): Promise<{ result: CliResult }> {
415
+ if (
416
+ !isAbsolute(options.projectRoot) ||
417
+ !isAbsolute(options.resultsRoot) ||
418
+ (options.runStateRoot !== undefined && !isAbsolute(options.runStateRoot))
419
+ )
420
+ throw new EvaluationConfigurationError(
421
+ "project, results, and run-state roots must be absolute",
422
+ );
423
+ if (
424
+ !options.case.id ||
425
+ !options.case.prompt ||
426
+ !options.host.id ||
427
+ !options.host.model ||
428
+ !options.host.effort
429
+ )
430
+ throw new EvaluationConfigurationError(
431
+ "case and host identities must be nonempty",
432
+ );
433
+ if (
434
+ options.case.followUpPrompt !== undefined &&
435
+ (typeof options.case.followUpPrompt !== "string" ||
436
+ !options.case.followUpPrompt.trim())
437
+ )
438
+ throw new EvaluationConfigurationError("follow-up prompt must be nonempty");
439
+ if (
440
+ options.case.followUpPrompt !== undefined &&
441
+ !options.host.hostCapabilities?.includes("sevro.host.continuation")
442
+ )
443
+ throw new EvaluationConfigurationError(
444
+ "selected host does not support continuation",
445
+ );
446
+ if (
447
+ options.case.followUpPrompt !== undefined &&
448
+ options.extension &&
449
+ !options.extension.session.identity.capabilities.includes(
450
+ "sevro.host.continuation",
451
+ )
452
+ )
453
+ throw new EvaluationConfigurationError(
454
+ "continuation capability was not negotiated",
455
+ );
456
+ if (
457
+ !options.case.fixture ||
458
+ typeof options.case.fixture !== "object" ||
459
+ Array.isArray(options.case.fixture)
460
+ )
461
+ throw new EvaluationConfigurationError("invalid fixture declaration");
462
+ let generated: GeneratedFixture | null = null;
463
+ let repositoryFixture: RepositoryFixture | null = null;
464
+ if ("kind" in options.case.fixture) {
465
+ try {
466
+ generated = prepareGeneratedFixture(options.case.fixture);
467
+ } catch (error) {
468
+ throw new EvaluationConfigurationError(
469
+ error instanceof Error ? error.message : "invalid generated fixture",
470
+ );
471
+ }
472
+ } else if (Object.hasOwn(options.case.fixture, "sourceRef")) {
473
+ try {
474
+ repositoryFixture = prepareRepositoryFixture({
475
+ kind: "repository",
476
+ ...options.case.fixture,
477
+ });
478
+ } catch (error) {
479
+ throw new EvaluationConfigurationError(
480
+ error instanceof Error ? error.message : "invalid repository fixture",
481
+ );
482
+ }
483
+ } else if (
484
+ !Object.hasOwn(options.case.fixture, "files") ||
485
+ options.case.fixture.files === null ||
486
+ typeof options.case.fixture.files !== "object" ||
487
+ Array.isArray(options.case.fixture.files) ||
488
+ Object.values(options.case.fixture.files).some(
489
+ (content) => typeof content !== "string",
490
+ )
491
+ ) {
492
+ throw new EvaluationConfigurationError(
493
+ "fixture must declare inline files or a repository source",
494
+ );
495
+ }
496
+ if (!Number.isSafeInteger(options.trialCount) || options.trialCount < 1)
497
+ throw new EvaluationConfigurationError(
498
+ "trial count must be a positive integer",
499
+ );
500
+ const jobs = options.jobs ?? 3;
501
+ if (!Number.isSafeInteger(jobs) || jobs < 1)
502
+ throw new EvaluationConfigurationError("jobs must be a positive integer");
503
+ if (
504
+ !Number.isFinite(options.passThreshold) ||
505
+ options.passThreshold <= 0 ||
506
+ options.passThreshold > 1
507
+ )
508
+ throw new EvaluationConfigurationError(
509
+ "pass threshold must be greater than zero and at most one",
510
+ );
511
+ if (
512
+ new Set(options.case.requiredEvidence).size !==
513
+ options.case.requiredEvidence.length ||
514
+ options.case.requiredEvidence.some(
515
+ (id) => !/^[a-z][a-z0-9]*(?:[.-][a-z0-9]+)+$/.test(id),
516
+ )
517
+ )
518
+ throw new EvaluationConfigurationError("invalid required evidence IDs");
519
+ const allBuiltinDeclarations = options.case.checks.filter((check) =>
520
+ isOutputGrader(check.grader),
521
+ ) as OutputCheckDeclaration[];
522
+ const allShellDeclarations = options.case.checks.filter(
523
+ (check) => check.grader === "sevro.shell",
524
+ ) as ShellCheckDeclaration[];
525
+ const allGitHeadDeclarations = options.case.checks.filter(
526
+ (check) => check.grader === "sevro.git-head",
527
+ ) as GitHeadCheckDeclaration[];
528
+ const allSemanticDeclarations = options.case.checks.filter(
529
+ (check) => check.grader === "sevro.semantic",
530
+ ) as SemanticCheckDeclaration[];
531
+ const extensionDeclarations = options.case.checks.filter(
532
+ (check) =>
533
+ !isOutputGrader(check.grader) &&
534
+ check.grader !== "sevro.shell" &&
535
+ check.grader !== "sevro.git-head" &&
536
+ check.grader !== "sevro.semantic",
537
+ );
538
+ const replacedBuiltinGraders =
539
+ options.extension?.session.identity.replacedBuiltinGraders ?? [];
540
+ const replaced = new Set(replacedBuiltinGraders);
541
+ if (
542
+ replaced.size !== replacedBuiltinGraders.length ||
543
+ replacedBuiltinGraders.some(
544
+ (id) =>
545
+ ![
546
+ ...allBuiltinDeclarations,
547
+ ...allShellDeclarations,
548
+ ...allGitHeadDeclarations,
549
+ ...allSemanticDeclarations,
550
+ ].some((check) => check.grader === id),
551
+ )
552
+ )
553
+ throw new EvaluationConfigurationError(
554
+ "unknown or duplicate built-in grader replacement",
555
+ );
556
+ if (replaced.size && !extensionDeclarations.length)
557
+ throw new EvaluationConfigurationError(
558
+ "built-in grader replacement requires an extension check",
559
+ );
560
+ const builtinDeclarations = allBuiltinDeclarations.filter(
561
+ (check) => !replaced.has(check.grader),
562
+ );
563
+ const shellDeclarations = allShellDeclarations.filter(
564
+ (check) => !replaced.has(check.grader),
565
+ );
566
+ const gitHeadDeclarations = allGitHeadDeclarations.filter(
567
+ (check) => !replaced.has(check.grader),
568
+ );
569
+ const semanticDeclarations = allSemanticDeclarations.filter(
570
+ (check) => !replaced.has(check.grader),
571
+ );
572
+ if (semanticDeclarations.length && !options.semanticHost)
573
+ throw new EvaluationConfigurationError(
574
+ "semantic checks require an explicit semantic host",
575
+ );
576
+ if (
577
+ options.semanticHost &&
578
+ (!options.semanticHost.id ||
579
+ !options.semanticHost.model ||
580
+ !options.semanticHost.effort)
581
+ )
582
+ throw new EvaluationConfigurationError(
583
+ "semantic host identity is incomplete",
584
+ );
585
+ if (options.advisoryHost) {
586
+ if (!generated && !options.case.fixture.sourceRef)
587
+ throw new EvaluationConfigurationError(
588
+ "advisory review requires a Git fixture",
589
+ );
590
+ if (
591
+ !options.advisoryHost.id ||
592
+ !options.advisoryHost.model ||
593
+ !options.advisoryHost.effort
594
+ )
595
+ throw new EvaluationConfigurationError(
596
+ "advisory host identity is incomplete",
597
+ );
598
+ try {
599
+ for (const path of options.advisoryExcludedPaths ?? []) {
600
+ fixtureParts(path);
601
+ if (
602
+ path.toLowerCase() === ".git" ||
603
+ path.toLowerCase().startsWith(".git/")
604
+ )
605
+ throw new Error(
606
+ "repository metadata cannot be an advisory exclusion",
607
+ );
608
+ }
609
+ } catch {
610
+ throw new EvaluationConfigurationError("invalid advisory exclusion path");
611
+ }
612
+ } else if (options.advisoryExcludedPaths?.length) {
613
+ throw new EvaluationConfigurationError(
614
+ "advisory exclusions require an advisory host",
615
+ );
616
+ }
617
+ if (shellDeclarations.length && !options.shellIsolation)
618
+ throw new EvaluationConfigurationError(
619
+ "shell checks require explicit protected source roots",
620
+ );
621
+ if (gitHeadDeclarations.length && !generated && !repositoryFixture)
622
+ throw new EvaluationConfigurationError(
623
+ "Git HEAD checks require a Git fixture",
624
+ );
625
+ if (
626
+ new Set(options.case.checks.map((check) => check.id)).size !==
627
+ options.case.checks.length
628
+ )
629
+ throw new EvaluationConfigurationError("case check IDs must be unique");
630
+ if (
631
+ extensionDeclarations.length &&
632
+ (!options.extension ||
633
+ extensionDeclarations.some(
634
+ (check) =>
635
+ !options.extension!.session.identity.graders.includes(check.grader),
636
+ ))
637
+ )
638
+ throw new EvaluationConfigurationError(
639
+ "case declares an unavailable extension grader",
640
+ );
641
+ if (options.extension) {
642
+ const resolved = options.extension.resolvedCase;
643
+ const fixture =
644
+ resolved.fixture.kind === "inline"
645
+ ? { files: resolved.fixture.files }
646
+ : resolved.fixture.kind === "repository"
647
+ ? {
648
+ sourceRef: resolved.fixture.sourceRef,
649
+ ...(resolved.fixture.files
650
+ ? { files: resolved.fixture.files }
651
+ : {}),
652
+ ...(resolved.fixture.staged
653
+ ? { staged: resolved.fixture.staged }
654
+ : {}),
655
+ ...(resolved.fixture.commitFiles ? { commitFiles: true } : {}),
656
+ ...(resolved.fixture.hooks
657
+ ? { hooks: resolved.fixture.hooks }
658
+ : {}),
659
+ ...(resolved.fixture.bin ? { bin: resolved.fixture.bin } : {}),
660
+ }
661
+ : resolved.fixture;
662
+ if (
663
+ hashJson({
664
+ id: resolved.id,
665
+ prompt: resolved.prompt,
666
+ ...(resolved.followUpPrompt !== undefined
667
+ ? { followUpPrompt: resolved.followUpPrompt }
668
+ : {}),
669
+ fixture,
670
+ checks: resolved.checks,
671
+ requiredEvidence: resolved.requiredEvidence,
672
+ }) !==
673
+ hashJson({
674
+ id: options.case.id,
675
+ prompt: options.case.prompt,
676
+ ...(options.case.followUpPrompt !== undefined
677
+ ? { followUpPrompt: options.case.followUpPrompt }
678
+ : {}),
679
+ fixture: options.case.fixture,
680
+ checks: options.case.checks,
681
+ requiredEvidence: options.case.requiredEvidence,
682
+ })
683
+ )
684
+ throw new EvaluationConfigurationError(
685
+ "resolved extension case does not match the selected case",
686
+ );
687
+ }
688
+ let prepared: ReturnType<typeof prepareOutputChecks>;
689
+ let preparedShell: ReturnType<typeof prepareShellChecks>;
690
+ let preparedGitHead: ReturnType<typeof prepareGitHeadChecks>;
691
+ let preparedSemantic: ReturnType<typeof prepareSemanticChecks>;
692
+ try {
693
+ prepared = prepareOutputChecks(allBuiltinDeclarations).filter(
694
+ (check) => !replaced.has(check.grader),
695
+ );
696
+ const allPreparedShell = prepareShellChecks(allShellDeclarations);
697
+ preparedShell = replaced.has("sevro.shell") ? [] : allPreparedShell;
698
+ const allPreparedGitHead = prepareGitHeadChecks(allGitHeadDeclarations);
699
+ preparedGitHead = replaced.has("sevro.git-head") ? [] : allPreparedGitHead;
700
+ const allPreparedSemantic = prepareSemanticChecks(allSemanticDeclarations);
701
+ preparedSemantic = replaced.has("sevro.semantic")
702
+ ? []
703
+ : allPreparedSemantic;
704
+ for (const path of Object.keys(options.case.fixture.files ?? {}))
705
+ fixtureParts(path);
706
+ } catch (error) {
707
+ throw new EvaluationConfigurationError(
708
+ error instanceof Error ? error.message : "invalid case configuration",
709
+ );
710
+ }
711
+
712
+ const projectRoot = await realpath(options.projectRoot).catch(() => {
713
+ throw new EvaluationConfigurationError("project root is unreadable");
714
+ });
715
+ const repository = options.case.fixture.sourceRef
716
+ ? await resolveRepositorySource(
717
+ options.case.fixture.sourceRef,
718
+ options.preparationSources,
719
+ ).catch((error) => {
720
+ throw new EvaluationConfigurationError(
721
+ error instanceof Error ? error.message : "invalid repository source",
722
+ );
723
+ })
724
+ : null;
725
+ const [runner, project] = await Promise.all([
726
+ runnerProvenance(
727
+ options.runnerBuildDigest,
728
+ options.runnerCheckoutRoot,
729
+ ).catch((error) => {
730
+ throw new EvaluationConfigurationError(
731
+ error instanceof Error ? error.message : "invalid runner provenance",
732
+ );
733
+ }),
734
+ projectProvenance(projectRoot),
735
+ ]);
736
+ const extensionPreparation = options.extension
737
+ ? await options.extension.session.prepare(
738
+ options.extension.resolvedCase,
739
+ {
740
+ id: options.host.id,
741
+ capabilities: [
742
+ ...(options.host.instrumentation ?? []).map((item) => item.id),
743
+ ...(options.host.hostCapabilities ?? []),
744
+ ],
745
+ },
746
+ options.condition,
747
+ )
748
+ : null;
749
+ let fixtureSetup: FixtureSetup | null;
750
+ try {
751
+ fixtureSetup = prepareFixtureSetup(extensionPreparation?.fixtureSetup);
752
+ } catch (error) {
753
+ throw new EvaluationConfigurationError(
754
+ error instanceof Error ? error.message : "invalid fixture setup",
755
+ );
756
+ }
757
+ if (
758
+ fixtureSetup &&
759
+ !options.extension?.session.identity.capabilities.includes(
760
+ "sevro.fixture.setup",
761
+ )
762
+ )
763
+ throw new EvaluationConfigurationError(
764
+ "fixture setup capability was not negotiated",
765
+ );
766
+ if (fixtureSetup && !generated && !repository)
767
+ throw new EvaluationConfigurationError(
768
+ "fixture setup requires a Git fixture",
769
+ );
770
+ let requestedInstrumentation: InstrumentationRequest[];
771
+ try {
772
+ requestedInstrumentation = prepareInstrumentation(
773
+ extensionPreparation?.requestedInstrumentation ?? [],
774
+ options.host.instrumentation ?? [],
775
+ options.extension?.session.identity.capabilities ?? [],
776
+ options.condition,
777
+ );
778
+ } catch (error) {
779
+ throw new EvaluationConfigurationError(
780
+ error instanceof Error ? error.message : "invalid instrumentation",
781
+ );
782
+ }
783
+ let inlineArtifacts: InlineArtifact[];
784
+ try {
785
+ const fixturePaths = generated
786
+ ? [
787
+ ...new Set([
788
+ ...generated.commits.flatMap((commit) => Object.keys(commit.files)),
789
+ ...Object.keys(generated.files ?? {}),
790
+ ]),
791
+ ]
792
+ : Object.keys(options.case.fixture.files ?? {});
793
+ inlineArtifacts = await prepareArtifacts(
794
+ extensionPreparation?.artifacts ?? [],
795
+ fixturePaths,
796
+ options.preparationSources,
797
+ );
798
+ } catch (error) {
799
+ throw new EvaluationConfigurationError(
800
+ error instanceof Error ? error.message : "invalid preparation artifacts",
801
+ );
802
+ }
803
+ if (
804
+ (repository || generated) &&
805
+ inlineArtifacts.some((artifact) =>
806
+ fixtureParts(artifact.relativePath).some(
807
+ (part) => part.toLowerCase() === ".git",
808
+ ),
809
+ )
810
+ )
811
+ throw new EvaluationConfigurationError(
812
+ "preparation artifacts cannot modify repository metadata",
813
+ );
814
+ if (
815
+ !repository &&
816
+ !generated &&
817
+ inlineArtifacts.some((artifact) => artifact.gitExclude)
818
+ )
819
+ throw new EvaluationConfigurationError(
820
+ "Git-excluded preparation artifacts require a Git fixture",
821
+ );
822
+ if (
823
+ semanticDeclarations.length &&
824
+ inlineArtifacts.some((item) => item.id === "sevro.semantic.verdicts")
825
+ )
826
+ throw new EvaluationConfigurationError(
827
+ "preparation artifact uses a reserved semantic evidence ID",
828
+ );
829
+ const codexMarketplace = extensionPreparation?.codexMarketplace;
830
+ const claudePluginDirs = extensionPreparation?.claudePluginDirs;
831
+ if (claudePluginDirs) {
832
+ const roots = claudePluginDirs.artifactRoots;
833
+ try {
834
+ if (
835
+ !Array.isArray(roots) ||
836
+ !roots.length ||
837
+ new Set(roots).size !== roots.length
838
+ )
839
+ throw new Error("invalid plugin roots");
840
+ for (const root of roots) fixtureParts(root);
841
+ if (
842
+ roots.some((root) =>
843
+ roots.some((other) => root !== other && root.startsWith(`${other}/`)),
844
+ )
845
+ )
846
+ throw new Error("overlapping plugin roots");
847
+ } catch {
848
+ throw new EvaluationConfigurationError(
849
+ "invalid Claude plugin directory declaration",
850
+ );
851
+ }
852
+ if (
853
+ !options.extension?.session.identity.capabilities.includes(
854
+ "sevro.claude.plugin-dirs",
855
+ ) ||
856
+ !options.host.hostCapabilities?.includes("sevro.claude.plugin-dirs")
857
+ )
858
+ throw new EvaluationConfigurationError(
859
+ "Claude plugin directory capability was not negotiated",
860
+ );
861
+ for (const root of roots) {
862
+ const files = inlineArtifacts.filter((artifact) =>
863
+ artifact.relativePath.startsWith(`${root}/`),
864
+ );
865
+ if (
866
+ !files.length ||
867
+ !files.every((artifact) => artifact.gitExclude) ||
868
+ !files.some(
869
+ (artifact) =>
870
+ artifact.relativePath === `${root}/.claude-plugin/plugin.json`,
871
+ )
872
+ )
873
+ throw new EvaluationConfigurationError(
874
+ "Claude plugin directory requires Git-excluded package artifacts and a manifest",
875
+ );
876
+ }
877
+ }
878
+ if (codexMarketplace) {
879
+ const { artifactRoot, marketplaceName, pluginNames } = codexMarketplace;
880
+ try {
881
+ fixtureParts(artifactRoot);
882
+ if (
883
+ !/^[a-z][a-z0-9-]*$/.test(marketplaceName) ||
884
+ !pluginNames.length ||
885
+ pluginNames.some((name) => !/^[a-z][a-z0-9-]*$/.test(name)) ||
886
+ new Set(pluginNames).size !== pluginNames.length
887
+ )
888
+ throw new Error("invalid marketplace or plugin name");
889
+ } catch {
890
+ throw new EvaluationConfigurationError(
891
+ "invalid Codex marketplace declaration",
892
+ );
893
+ }
894
+ if (
895
+ !options.extension?.session.identity.capabilities.includes(
896
+ "sevro.codex.plugin-marketplace",
897
+ ) ||
898
+ !options.host.hostCapabilities?.includes("sevro.codex.plugin-marketplace")
899
+ )
900
+ throw new EvaluationConfigurationError(
901
+ "Codex marketplace capability was not negotiated",
902
+ );
903
+ const packageArtifacts = inlineArtifacts.filter((artifact) =>
904
+ artifact.relativePath.startsWith(`${artifactRoot}/`),
905
+ );
906
+ if (
907
+ !packageArtifacts.length ||
908
+ !packageArtifacts.every((artifact) => artifact.gitExclude) ||
909
+ !packageArtifacts.some(
910
+ (artifact) =>
911
+ artifact.relativePath ===
912
+ `${artifactRoot}/.claude-plugin/marketplace.json`,
913
+ )
914
+ )
915
+ throw new EvaluationConfigurationError(
916
+ "Codex marketplace requires Git-excluded package artifacts and a manifest",
917
+ );
918
+ }
919
+ const codexSkillInvocation = extensionPreparation?.codexSkillInvocation;
920
+ const codexRepositorySkillInvocation =
921
+ extensionPreparation?.codexRepositorySkillInvocation;
922
+ const claudeSkillInvocation = extensionPreparation?.claudeSkillInvocation;
923
+ const claudeRepositorySkillInvocation =
924
+ extensionPreparation?.claudeRepositorySkillInvocation;
925
+ const invocationPlaceholder = "{{sevro.skill_invocation}}";
926
+ const legacyCodexPlaceholder = "{{sevro.codex.skill_invocation}}";
927
+ const repositoryInvocation =
928
+ codexRepositorySkillInvocation ?? claudeRepositorySkillInvocation;
929
+ const invocation = repositoryInvocation
930
+ ? { ...repositoryInvocation, scope: "repository" as const }
931
+ : (codexSkillInvocation ?? claudeSkillInvocation);
932
+ const invocationToken = codexRepositorySkillInvocation
933
+ ? `$${codexRepositorySkillInvocation.skillName}`
934
+ : claudeRepositorySkillInvocation
935
+ ? `/${claudeRepositorySkillInvocation.skillName}`
936
+ : codexSkillInvocation
937
+ ? `$${codexSkillInvocation.pluginName}:${codexSkillInvocation.skillName}`
938
+ : claudeSkillInvocation
939
+ ? `/${claudeSkillInvocation.pluginName}:${claudeSkillInvocation.skillName}`
940
+ : null;
941
+ const placeholderCount =
942
+ options.case.prompt.split(invocationPlaceholder).length -
943
+ 1 +
944
+ (options.case.followUpPrompt?.split(invocationPlaceholder).length ?? 1) -
945
+ 1 +
946
+ options.case.prompt.split(legacyCodexPlaceholder).length -
947
+ 1 +
948
+ (options.case.followUpPrompt?.split(legacyCodexPlaceholder).length ?? 1) -
949
+ 1;
950
+ if (
951
+ [
952
+ codexSkillInvocation,
953
+ codexRepositorySkillInvocation,
954
+ claudeSkillInvocation,
955
+ claudeRepositorySkillInvocation,
956
+ ].filter(Boolean).length > 1
957
+ )
958
+ throw new EvaluationConfigurationError(
959
+ "only one explicit skill invocation may be declared",
960
+ );
961
+ if (codexSkillInvocation) {
962
+ const { pluginName, skillName } = codexSkillInvocation;
963
+ if (
964
+ !codexMarketplace ||
965
+ !codexMarketplace.pluginNames.includes(pluginName) ||
966
+ !/^[a-z][a-z0-9-]*$/.test(pluginName) ||
967
+ !/^[A-Za-z0-9._-]+$/.test(skillName) ||
968
+ placeholderCount !== 1 ||
969
+ !inlineArtifacts.some(
970
+ (artifact) =>
971
+ artifact.relativePath ===
972
+ `${codexMarketplace.artifactRoot}/plugin/skills/${skillName}/SKILL.md`,
973
+ )
974
+ )
975
+ throw new EvaluationConfigurationError(
976
+ "invalid Codex skill invocation declaration",
977
+ );
978
+ if (
979
+ !options.extension?.session.identity.capabilities.includes(
980
+ "sevro.codex.explicit-invocation",
981
+ ) ||
982
+ !options.host.hostCapabilities?.includes(
983
+ "sevro.codex.explicit-invocation",
984
+ )
985
+ )
986
+ throw new EvaluationConfigurationError(
987
+ "Codex explicit invocation capability was not negotiated",
988
+ );
989
+ } else if (codexRepositorySkillInvocation) {
990
+ const { skillName } = codexRepositorySkillInvocation;
991
+ if (
992
+ !/^[A-Za-z0-9._-]+$/.test(skillName) ||
993
+ skillName === "." ||
994
+ skillName === ".." ||
995
+ placeholderCount !== 1 ||
996
+ !inlineArtifacts.some(
997
+ (artifact) =>
998
+ artifact.gitExclude &&
999
+ artifact.relativePath === `.agents/skills/${skillName}/SKILL.md`,
1000
+ )
1001
+ )
1002
+ throw new EvaluationConfigurationError(
1003
+ "invalid Codex repository skill invocation declaration",
1004
+ );
1005
+ if (
1006
+ !options.extension?.session.identity.capabilities.includes(
1007
+ "sevro.codex.repository-invocation",
1008
+ ) ||
1009
+ !options.host.hostCapabilities?.includes(
1010
+ "sevro.codex.repository-invocation",
1011
+ )
1012
+ )
1013
+ throw new EvaluationConfigurationError(
1014
+ "Codex repository invocation capability was not negotiated",
1015
+ );
1016
+ } else if (claudeRepositorySkillInvocation) {
1017
+ const { skillName } = claudeRepositorySkillInvocation;
1018
+ if (
1019
+ !/^[A-Za-z0-9._-]+$/.test(skillName) ||
1020
+ skillName === "." ||
1021
+ skillName === ".." ||
1022
+ placeholderCount !== 1 ||
1023
+ !options.case.prompt.startsWith(invocationPlaceholder) ||
1024
+ options.case.prompt.includes(legacyCodexPlaceholder) ||
1025
+ !inlineArtifacts.some(
1026
+ (artifact) =>
1027
+ artifact.gitExclude &&
1028
+ artifact.relativePath === `.claude/skills/${skillName}/SKILL.md`,
1029
+ )
1030
+ )
1031
+ throw new EvaluationConfigurationError(
1032
+ "invalid Claude repository skill invocation declaration",
1033
+ );
1034
+ if (
1035
+ !options.extension?.session.identity.capabilities.includes(
1036
+ "sevro.claude.repository-invocation",
1037
+ ) ||
1038
+ !options.host.hostCapabilities?.includes(
1039
+ "sevro.claude.repository-invocation",
1040
+ )
1041
+ )
1042
+ throw new EvaluationConfigurationError(
1043
+ "Claude repository invocation capability was not negotiated",
1044
+ );
1045
+ } else if (claudeSkillInvocation) {
1046
+ const { pluginName, skillName } = claudeSkillInvocation;
1047
+ if (
1048
+ !claudePluginDirs ||
1049
+ !/^[a-z][a-z0-9-]*$/.test(pluginName) ||
1050
+ !/^[A-Za-z0-9._-]+$/.test(skillName) ||
1051
+ placeholderCount !== 1 ||
1052
+ options.case.prompt.includes(legacyCodexPlaceholder) ||
1053
+ options.case.followUpPrompt?.includes(legacyCodexPlaceholder) ||
1054
+ !claudePluginDirs.artifactRoots.some((root) =>
1055
+ inlineArtifacts.some(
1056
+ (artifact) =>
1057
+ artifact.relativePath === `${root}/skills/${skillName}/SKILL.md`,
1058
+ ),
1059
+ )
1060
+ )
1061
+ throw new EvaluationConfigurationError(
1062
+ "invalid Claude skill invocation declaration",
1063
+ );
1064
+ if (
1065
+ !options.extension?.session.identity.capabilities.includes(
1066
+ "sevro.claude.explicit-invocation",
1067
+ ) ||
1068
+ !options.host.hostCapabilities?.includes(
1069
+ "sevro.claude.explicit-invocation",
1070
+ )
1071
+ )
1072
+ throw new EvaluationConfigurationError(
1073
+ "Claude explicit invocation capability was not negotiated",
1074
+ );
1075
+ } else if (placeholderCount)
1076
+ throw new EvaluationConfigurationError(
1077
+ "skill invocation placeholder requires a declaration",
1078
+ );
1079
+ const extensionData = options.extension
1080
+ ? {
1081
+ ...options.extension.resolvedCase.extensionData,
1082
+ ...extensionPreparation?.extensionData,
1083
+ }
1084
+ : {};
1085
+ const runId = randomUUID();
1086
+ const route = {
1087
+ role: "candidate",
1088
+ host: options.host.id,
1089
+ model: options.host.model,
1090
+ effort: options.host.effort,
1091
+ };
1092
+ const routes = [
1093
+ route,
1094
+ ...(preparedSemantic.length
1095
+ ? [
1096
+ {
1097
+ role: "semantic" as const,
1098
+ host: options.semanticHost!.id,
1099
+ model: options.semanticHost!.model,
1100
+ effort: options.semanticHost!.effort,
1101
+ },
1102
+ ]
1103
+ : []),
1104
+ ...(options.advisoryHost
1105
+ ? [
1106
+ {
1107
+ role: "advisory" as const,
1108
+ host: options.advisoryHost.id,
1109
+ model: options.advisoryHost.model,
1110
+ effort: options.advisoryHost.effort,
1111
+ },
1112
+ ]
1113
+ : []),
1114
+ ];
1115
+ const redactedConfig = {
1116
+ condition: options.condition,
1117
+ executionMode: options.dry ? "dry" : "executed",
1118
+ trialCount: options.trialCount,
1119
+ jobs,
1120
+ passThreshold: options.passThreshold,
1121
+ ...([
1122
+ options.host,
1123
+ preparedSemantic.length ? options.semanticHost : undefined,
1124
+ options.advisoryHost,
1125
+ ].some((host) => host?.configuration !== undefined)
1126
+ ? {
1127
+ hostConfiguration: {
1128
+ ...(options.host.configuration !== undefined
1129
+ ? { candidate: options.host.configuration }
1130
+ : {}),
1131
+ ...(preparedSemantic.length &&
1132
+ options.semanticHost?.configuration !== undefined
1133
+ ? { semantic: options.semanticHost.configuration }
1134
+ : {}),
1135
+ ...(options.advisoryHost?.configuration !== undefined
1136
+ ? { advisory: options.advisoryHost.configuration }
1137
+ : {}),
1138
+ },
1139
+ }
1140
+ : {}),
1141
+ extensionConfigurationDigest:
1142
+ options.extension?.session.identity.configurationDigest ?? null,
1143
+ ...(options.extension
1144
+ ? {
1145
+ extensionConfiguration:
1146
+ options.extension.session.redactedConfiguration,
1147
+ }
1148
+ : {}),
1149
+ ...(fixtureSetup ? { fixtureSetupDigest: hashJson(fixtureSetup) } : {}),
1150
+ ...(codexMarketplace ? { codexMarketplace } : {}),
1151
+ ...(claudePluginDirs ? { claudePluginDirs } : {}),
1152
+ ...(codexSkillInvocation ? { codexSkillInvocation } : {}),
1153
+ ...(codexRepositorySkillInvocation
1154
+ ? { codexRepositorySkillInvocation }
1155
+ : {}),
1156
+ ...(claudeSkillInvocation ? { claudeSkillInvocation } : {}),
1157
+ ...(claudeRepositorySkillInvocation
1158
+ ? { claudeRepositorySkillInvocation }
1159
+ : {}),
1160
+ ...(options.advisoryHost
1161
+ ? {
1162
+ advisoryExcludedPaths: [
1163
+ ...(options.advisoryExcludedPaths ?? []),
1164
+ ].sort(),
1165
+ }
1166
+ : {}),
1167
+ };
1168
+ const activeGraders: {
1169
+ id: string;
1170
+ source: "builtin" | "extension";
1171
+ version: string;
1172
+ }[] = [
1173
+ ...new Set(
1174
+ [
1175
+ ...builtinDeclarations,
1176
+ ...shellDeclarations,
1177
+ ...gitHeadDeclarations,
1178
+ ...semanticDeclarations,
1179
+ ].map((check) => check.grader),
1180
+ ),
1181
+ ]
1182
+ .sort()
1183
+ .map((id) => ({ id, source: "builtin", version: "1.0.0" }));
1184
+ if (options.extension) {
1185
+ for (const id of [
1186
+ ...new Set(extensionDeclarations.map((check) => check.grader)),
1187
+ ].sort())
1188
+ activeGraders.push({
1189
+ id,
1190
+ source: "extension",
1191
+ version: options.extension.session.identity.version,
1192
+ });
1193
+ }
1194
+ let evaluationIdentity: ReturnType<typeof createEvaluationIdentity>;
1195
+ try {
1196
+ evaluationIdentity = createEvaluationIdentity({
1197
+ runnerBuildDigest: options.runnerBuildDigest,
1198
+ projectDigest: options.projectDigest,
1199
+ configurationDigest: hashJson(redactedConfig),
1200
+ extensionDigest: options.extension?.session.identity.sourceDigest ?? null,
1201
+ extensionProtocol: options.extension?.session.identity.protocol ?? null,
1202
+ caseDigest: hashJson({
1203
+ id: options.case.id,
1204
+ prompt: options.case.prompt,
1205
+ ...(options.case.followUpPrompt !== undefined
1206
+ ? { followUpPrompt: options.case.followUpPrompt }
1207
+ : {}),
1208
+ extensionData,
1209
+ }),
1210
+ fixtureDigest: hashJson({
1211
+ ...(generated
1212
+ ? generated
1213
+ : repository
1214
+ ? {
1215
+ ...repositoryFixture,
1216
+ revision: repository.revision,
1217
+ }
1218
+ : { files: options.case.fixture.files }),
1219
+ artifacts: inlineArtifacts.map(
1220
+ ({ id, relativePath, sha256, gitExclude, executable }) => ({
1221
+ id,
1222
+ relativePath,
1223
+ sha256,
1224
+ ...(gitExclude ? { gitExclude: true } : {}),
1225
+ ...(executable ? { executable: true } : {}),
1226
+ }),
1227
+ ),
1228
+ ...(fixtureSetup ? { fixtureSetup } : {}),
1229
+ ...(codexMarketplace ? { codexMarketplace } : {}),
1230
+ ...(claudePluginDirs ? { claudePluginDirs } : {}),
1231
+ ...(codexSkillInvocation ? { codexSkillInvocation } : {}),
1232
+ ...(codexRepositorySkillInvocation
1233
+ ? { codexRepositorySkillInvocation }
1234
+ : {}),
1235
+ ...(claudeSkillInvocation ? { claudeSkillInvocation } : {}),
1236
+ ...(claudeRepositorySkillInvocation
1237
+ ? { claudeRepositorySkillInvocation }
1238
+ : {}),
1239
+ }),
1240
+ checksDigest: hashJson(options.case.checks),
1241
+ requiredEvidenceDigest: hashJson(options.case.requiredEvidence),
1242
+ evaluatorDigest: hashJson({
1243
+ policy: "sevro.builtin-output.v1",
1244
+ extension: options.extension?.session.identity ?? null,
1245
+ extensionData,
1246
+ }),
1247
+ graderDigest: hashJson(activeGraders),
1248
+ instrumentationDigest: hashJson({
1249
+ requested: requestedInstrumentation,
1250
+ applied: requestedInstrumentation,
1251
+ }),
1252
+ routeDigest: hashJson(routes.length > 1 ? routes : route),
1253
+ condition: options.condition,
1254
+ trialCount: options.trialCount,
1255
+ passThreshold: options.passThreshold,
1256
+ });
1257
+ } catch {
1258
+ throw new EvaluationConfigurationError(
1259
+ "invalid evaluation identity inputs",
1260
+ );
1261
+ }
1262
+
1263
+ await mkdir(options.resultsRoot, { recursive: true, mode: 0o700 });
1264
+ const runDir = resolve(await realpath(options.resultsRoot), runId);
1265
+ await mkdir(runDir, { mode: 0o700 });
1266
+ const runStateRoot = options.runStateRoot ?? options.resultsRoot;
1267
+ await mkdir(runStateRoot, { recursive: true, mode: 0o700 });
1268
+ const stateRoot = await realpath(runStateRoot);
1269
+ const stateDir = join(stateRoot, runId);
1270
+ if (stateDir !== runDir) await mkdir(stateDir, { mode: 0o700 });
1271
+ const activePath = join(stateRoot, "active", `${runId}.json`);
1272
+ const checkpointPath = join(stateDir, "checkpoint.json");
1273
+ const evidencePath = join(runDir, "run.json");
1274
+ const trialSummaries: TrialSummary[] = [];
1275
+ let owner: ReturnType<typeof startRunOwner>;
1276
+ try {
1277
+ owner = startRunOwner({
1278
+ stateRoot,
1279
+ evaluationDigest: evaluationIdentity.digest,
1280
+ attemptId: runId,
1281
+ activeRunPath: activePath,
1282
+ checkpointPath,
1283
+ evidenceDirectory: stateDir,
1284
+ artifactPath: evidencePath,
1285
+ });
1286
+ } catch (error) {
1287
+ await rm(runDir, { recursive: true, force: true });
1288
+ if (stateDir !== runDir)
1289
+ await rm(stateDir, { recursive: true, force: true });
1290
+ throw error;
1291
+ }
1292
+ const reservedWorkspaces: string[] = [];
1293
+ const reservedSemanticWorkspaces: string[] = [];
1294
+ const retainedWorkspaces = new Set<string>();
1295
+ try {
1296
+ const artifactRefs: {
1297
+ id: string;
1298
+ path: string;
1299
+ sha256: string;
1300
+ gitExclude?: boolean;
1301
+ executable?: boolean;
1302
+ }[] = [];
1303
+ for (const artifact of inlineArtifacts) {
1304
+ const retainedPath = join(
1305
+ runDir,
1306
+ "prepared",
1307
+ ...fixtureParts(artifact.relativePath),
1308
+ );
1309
+ await mkdir(dirname(retainedPath), { recursive: true, mode: 0o700 });
1310
+ await writeFile(retainedPath, artifact.bytes, {
1311
+ flag: "wx",
1312
+ mode: artifact.executable ? 0o700 : 0o600,
1313
+ });
1314
+ artifactRefs.push({
1315
+ id: artifact.id,
1316
+ path: pathToFileURL(retainedPath).href,
1317
+ sha256: artifact.sha256,
1318
+ ...(artifact.gitExclude ? { gitExclude: true } : {}),
1319
+ ...(artifact.executable ? { executable: true } : {}),
1320
+ });
1321
+ }
1322
+
1323
+ const trialEvidence: Record<string, unknown>[] = [];
1324
+ function saveState(status: "active" | "complete" | "interrupted"): void {
1325
+ const completedTrials = trialSummaries.map((trial) => ({
1326
+ trial: trial.trial,
1327
+ artifactPath: trial.artifactPath,
1328
+ }));
1329
+ checkpointRunOwner(owner, completedTrials, status);
1330
+ }
1331
+ for (
1332
+ let trial = 1;
1333
+ trial <= options.trialCount && !options.signal?.aborted;
1334
+ trial++
1335
+ ) {
1336
+ reservedWorkspaces.push(
1337
+ await realpath(await mkdtemp(join(tmpdir(), "sevro-case-"))),
1338
+ );
1339
+ if (preparedSemantic.length)
1340
+ reservedSemanticWorkspaces.push(
1341
+ await realpath(await mkdtemp(join(tmpdir(), "sevro-case-"))),
1342
+ );
1343
+ }
1344
+ const diagnostics: {
1345
+ trial: number;
1346
+ diagnostic: { code: string; message: string };
1347
+ }[] = [];
1348
+ const runTrial = async (trial: number, stopAdmission: () => void) => {
1349
+ let diagnostic: { code: string; message: string } | undefined;
1350
+ const reservation = reservedWorkspaces[trial - 1]!;
1351
+ retainedWorkspaces.add(reservation);
1352
+ const workspace = await createFixture(
1353
+ options.case.fixture,
1354
+ inlineArtifacts,
1355
+ options.preparationSources,
1356
+ repository,
1357
+ repositoryFixture,
1358
+ generated,
1359
+ fixtureSetup,
1360
+ projectRoot,
1361
+ options.signal,
1362
+ reservation,
1363
+ );
1364
+ const fixtureBinDir = (await fixtureBinDirectory(workspace)) ?? undefined;
1365
+ const trialPrompt = options.case.prompt
1366
+ .replaceAll("{{sevro.workspace}}", workspace)
1367
+ .replaceAll(
1368
+ invocationPlaceholder,
1369
+ invocationToken ?? invocationPlaceholder,
1370
+ )
1371
+ .replaceAll(
1372
+ legacyCodexPlaceholder,
1373
+ invocationToken ?? legacyCodexPlaceholder,
1374
+ );
1375
+ const trialFollowUpPrompt = options.case.followUpPrompt
1376
+ ?.replaceAll("{{sevro.workspace}}", workspace)
1377
+ .replaceAll(
1378
+ invocationPlaceholder,
1379
+ invocationToken ?? invocationPlaceholder,
1380
+ )
1381
+ .replaceAll(
1382
+ legacyCodexPlaceholder,
1383
+ invocationToken ?? legacyCodexPlaceholder,
1384
+ );
1385
+ let persisted = false;
1386
+ try {
1387
+ const gitHeadBase = preparedGitHead.length
1388
+ ? await gitHeadRevision(workspace, options.signal)
1389
+ : null;
1390
+ const advisoryRevision = options.advisoryHost
1391
+ ? await advisoryBaseRevision(workspace)
1392
+ : null;
1393
+ let hostResult: HostResult | null = null;
1394
+ let appliedInstrumentation: InstrumentationRequest[] = [];
1395
+ let additionalObservations: ReturnType<typeof hostObservations> = [];
1396
+ let producedArtifacts: ReturnType<typeof hostArtifacts> = [];
1397
+ let execution: "completed" | "failed" | "cancelled" | "not_run" =
1398
+ options.dry ? "not_run" : "completed";
1399
+ let candidateDurationMs: number | null = null;
1400
+ if (!options.dry) {
1401
+ const candidateStarted = performance.now();
1402
+ try {
1403
+ if (options.signal?.aborted) throw new Error("cancelled");
1404
+ hostResult = await options.host.run({
1405
+ prompt: trialPrompt,
1406
+ ...(trialFollowUpPrompt !== undefined
1407
+ ? { followUpPrompt: trialFollowUpPrompt }
1408
+ : {}),
1409
+ workspace,
1410
+ condition: options.condition,
1411
+ fixtureBinDir,
1412
+ instrumentation: requestedInstrumentation,
1413
+ ...(codexMarketplace
1414
+ ? {
1415
+ codexMarketplace: {
1416
+ ...codexMarketplace,
1417
+ artifactPaths: inlineArtifacts
1418
+ .filter((artifact) =>
1419
+ artifact.relativePath.startsWith(
1420
+ `${codexMarketplace.artifactRoot}/`,
1421
+ ),
1422
+ )
1423
+ .map((artifact) => artifact.relativePath),
1424
+ },
1425
+ }
1426
+ : {}),
1427
+ ...(claudePluginDirs
1428
+ ? {
1429
+ claudePluginDirs: {
1430
+ ...claudePluginDirs,
1431
+ artifactPaths: inlineArtifacts
1432
+ .filter((artifact) =>
1433
+ claudePluginDirs.artifactRoots.some((root) =>
1434
+ artifact.relativePath.startsWith(`${root}/`),
1435
+ ),
1436
+ )
1437
+ .map((artifact) => artifact.relativePath),
1438
+ },
1439
+ }
1440
+ : {}),
1441
+ ...(invocation
1442
+ ? {
1443
+ explicitSkillInvocation: {
1444
+ ...invocation,
1445
+ token: invocationToken!,
1446
+ },
1447
+ }
1448
+ : {}),
1449
+ signal: options.signal,
1450
+ });
1451
+ if (options.signal?.aborted) throw new Error("cancelled");
1452
+ appliedInstrumentation = verifyAppliedInstrumentation(
1453
+ requestedInstrumentation,
1454
+ hostResult.appliedInstrumentation,
1455
+ options.condition,
1456
+ hostResult.actualCondition,
1457
+ );
1458
+ if (
1459
+ hostResult.finalMessage !== null &&
1460
+ Buffer.byteLength(hostResult.finalMessage, "utf8") >
1461
+ MAX_FINAL_MESSAGE_BYTES
1462
+ )
1463
+ throw new Error("oversized host result");
1464
+ additionalObservations = hostObservations(
1465
+ hostResult,
1466
+ options.host.id,
1467
+ new Set([
1468
+ ...artifactRefs.map((item) => item.id),
1469
+ ...options.case.checks.map((item) => item.id),
1470
+ ]),
1471
+ );
1472
+ producedArtifacts = hostArtifacts(
1473
+ hostResult,
1474
+ new Set([
1475
+ "sevro.observation.final-message",
1476
+ ...artifactRefs.map((item) => item.id),
1477
+ ...additionalObservations.map((item) => item.id),
1478
+ ...options.case.checks.map((item) => item.id),
1479
+ ...preparedShell.map(
1480
+ (item) => `sevro.observation.shell.${item.id}`,
1481
+ ),
1482
+ ...(preparedGitHead.length
1483
+ ? ["sevro.observation.git-head"]
1484
+ : []),
1485
+ ...(preparedSemantic.length ? ["sevro.semantic.verdicts"] : []),
1486
+ ]),
1487
+ );
1488
+ if (hostResult.executionFailed) {
1489
+ execution = "failed";
1490
+ stopAdmission();
1491
+ diagnostic = {
1492
+ code: "sevro.host.failed",
1493
+ message: "host execution did not complete",
1494
+ };
1495
+ }
1496
+ } catch (error) {
1497
+ execution = options.signal?.aborted ? "cancelled" : "failed";
1498
+ stopAdmission();
1499
+ hostResult = null;
1500
+ diagnostic = {
1501
+ code:
1502
+ execution === "cancelled"
1503
+ ? "sevro.run.cancelled"
1504
+ : error instanceof InstrumentationEvidenceError
1505
+ ? "sevro.instrumentation.mismatch"
1506
+ : "sevro.host.failed",
1507
+ message:
1508
+ execution === "cancelled"
1509
+ ? "run cancelled"
1510
+ : error instanceof InstrumentationEvidenceError
1511
+ ? error.message
1512
+ : "host execution did not complete",
1513
+ };
1514
+ } finally {
1515
+ candidateDurationMs = Math.max(
1516
+ 0,
1517
+ performance.now() - candidateStarted,
1518
+ );
1519
+ }
1520
+ }
1521
+ const trialArtifactRefs = [...artifactRefs];
1522
+ for (const artifact of producedArtifacts) {
1523
+ const path = join(runDir, `trial-${trial}-host-${artifact.id}.bin`);
1524
+ try {
1525
+ await writeFile(path, artifact.bytes, { flag: "wx", mode: 0o600 });
1526
+ } catch {
1527
+ throw new Error(
1528
+ `trial persistence failed; fixture retained at ${workspace}`,
1529
+ );
1530
+ }
1531
+ trialArtifactRefs.push({
1532
+ id: artifact.id,
1533
+ path: pathToFileURL(path).href,
1534
+ sha256: sha256(artifact.bytes),
1535
+ });
1536
+ }
1537
+ await verifyRetainedArtifacts(trialArtifactRefs);
1538
+ const rawDigest =
1539
+ hostResult?.finalMessage == null
1540
+ ? null
1541
+ : sha256(hostResult.finalMessage);
1542
+ const completeness =
1543
+ hostResult?.finalMessage == null
1544
+ ? "unavailable"
1545
+ : hostResult.complete
1546
+ ? "complete"
1547
+ : "partial";
1548
+ const observation = {
1549
+ id: "sevro.observation.final-message",
1550
+ source: options.host.id,
1551
+ completeness,
1552
+ data: rawDigest
1553
+ ? {
1554
+ sha256: rawDigest,
1555
+ byteLength: Buffer.byteLength(
1556
+ hostResult!.finalMessage!,
1557
+ "utf8",
1558
+ ),
1559
+ }
1560
+ : {},
1561
+ };
1562
+ const shellObservations: {
1563
+ id: string;
1564
+ source: string;
1565
+ completeness: "complete";
1566
+ data: Record<string, unknown>;
1567
+ }[] = [];
1568
+ const gitHeadObservations: {
1569
+ id: string;
1570
+ source: string;
1571
+ completeness: "complete";
1572
+ data: Record<string, unknown>;
1573
+ }[] = [];
1574
+ const semanticObservations: {
1575
+ id: string;
1576
+ source: string;
1577
+ completeness: "complete" | "partial";
1578
+ data: Record<string, unknown>;
1579
+ }[] = [];
1580
+ let checks: CheckOutcome[] = [];
1581
+ let extensionMetrics: {
1582
+ id: string;
1583
+ value: number | null;
1584
+ unit: string;
1585
+ }[] = [];
1586
+ let domainOutcomes: NonNullable<EvaluationResult["domainOutcomes"]> =
1587
+ [];
1588
+ let taskPolicyRecommendation:
1589
+ "passed" | "failed" | "not_assessed" | null = null;
1590
+ let graderError = false;
1591
+ if (execution === "completed") {
1592
+ try {
1593
+ checks = gradeOutput(
1594
+ hostResult!.finalMessage,
1595
+ hostResult!.complete,
1596
+ prepared,
1597
+ ).map((check) => ({
1598
+ ...check,
1599
+ evidenceRefs: rawDigest ? [observation.id] : [],
1600
+ }));
1601
+ } catch {
1602
+ graderError = true;
1603
+ stopAdmission();
1604
+ diagnostic = {
1605
+ code: "sevro.grader.error",
1606
+ message: "output grading did not complete",
1607
+ };
1608
+ }
1609
+ }
1610
+ if (execution === "completed" && !graderError && preparedShell.length) {
1611
+ try {
1612
+ const protectedRoots = await evaluationProtectedRoots({
1613
+ workspace,
1614
+ projectRoot,
1615
+ resultsRoot: options.resultsRoot,
1616
+ additionalRoots: [
1617
+ ...options.shellIsolation!.protectedRoots,
1618
+ ...(options.preparationSources
1619
+ ? [options.preparationSources.root]
1620
+ : []),
1621
+ ...(repository ? [repository.path] : []),
1622
+ runStateRoot,
1623
+ ],
1624
+ });
1625
+ for (const check of preparedShell) {
1626
+ const shellResult = await runShellCheck(check, {
1627
+ workspace,
1628
+ fixtureBinDir,
1629
+ toolchainBinDir: options.shellIsolation?.toolchainBinDir,
1630
+ uvRuntimeCache: options.shellIsolation?.uvRuntimeCache,
1631
+ protectedRoots,
1632
+ protectedRootsCanonical: true,
1633
+ privateStateRoot: join(stateDir, "shell-sandbox"),
1634
+ signal: options.signal,
1635
+ });
1636
+ const graded = assessShellCheck(check, shellResult);
1637
+ const observationId = `sevro.observation.shell.${check.id}`;
1638
+ shellObservations.push({
1639
+ id: observationId,
1640
+ source: "sevro.shell",
1641
+ completeness: "complete",
1642
+ data: {
1643
+ exitCode: shellResult.exitCode,
1644
+ expectedExitCode: check.expectedExitCode,
1645
+ ...(shellResult.stdout === null
1646
+ ? {}
1647
+ : {
1648
+ stdoutSha256: sha256(shellResult.stdout),
1649
+ stdoutByteLength: Buffer.byteLength(
1650
+ shellResult.stdout,
1651
+ "utf8",
1652
+ ),
1653
+ }),
1654
+ },
1655
+ });
1656
+ checks.push({
1657
+ id: check.id,
1658
+ grader: "sevro.shell",
1659
+ status: graded.passed ? "passed" : "failed",
1660
+ detail: graded.detail,
1661
+ evidenceRefs: [observationId],
1662
+ });
1663
+ }
1664
+ } catch {
1665
+ if (options.signal?.aborted) {
1666
+ execution = "cancelled";
1667
+ diagnostic = {
1668
+ code: "sevro.run.cancelled",
1669
+ message: "run cancelled",
1670
+ };
1671
+ } else {
1672
+ graderError = true;
1673
+ diagnostic = {
1674
+ code: "sevro.grader.error",
1675
+ message: "shell grading did not complete",
1676
+ };
1677
+ stopAdmission();
1678
+ }
1679
+ }
1680
+ }
1681
+ if (
1682
+ execution === "completed" &&
1683
+ !graderError &&
1684
+ preparedGitHead.length
1685
+ ) {
1686
+ try {
1687
+ const state = await gitHeadState(
1688
+ workspace,
1689
+ gitHeadBase!,
1690
+ options.signal,
1691
+ );
1692
+ const observationId = "sevro.observation.git-head";
1693
+ gitHeadObservations.push({
1694
+ id: observationId,
1695
+ source: "sevro.git-head",
1696
+ completeness: "complete",
1697
+ data: {
1698
+ baseRevision: gitHeadBase,
1699
+ currentRevision: state.currentRevision,
1700
+ baseAncestor: state.baseAncestor,
1701
+ },
1702
+ });
1703
+ checks.push(
1704
+ ...preparedGitHead.map((check) => {
1705
+ const graded = assessGitHeadCheck(check, gitHeadBase!, state);
1706
+ return {
1707
+ id: check.id,
1708
+ grader: "sevro.git-head",
1709
+ status: graded.passed
1710
+ ? ("passed" as const)
1711
+ : ("failed" as const),
1712
+ detail: graded.detail,
1713
+ evidenceRefs: [observationId],
1714
+ };
1715
+ }),
1716
+ );
1717
+ } catch {
1718
+ if (options.signal?.aborted) {
1719
+ execution = "cancelled";
1720
+ diagnostic = {
1721
+ code: "sevro.run.cancelled",
1722
+ message: "run cancelled",
1723
+ };
1724
+ } else {
1725
+ graderError = true;
1726
+ diagnostic = {
1727
+ code: "sevro.grader.error",
1728
+ message: "Git HEAD grading did not complete",
1729
+ };
1730
+ stopAdmission();
1731
+ }
1732
+ }
1733
+ }
1734
+ if (
1735
+ execution === "completed" &&
1736
+ !graderError &&
1737
+ preparedSemantic.length
1738
+ ) {
1739
+ if (hostResult?.finalMessage == null || !hostResult.complete) {
1740
+ checks.push(
1741
+ ...preparedSemantic.map((check) => ({
1742
+ id: check.id,
1743
+ grader: "sevro.semantic",
1744
+ status: "unavailable" as const,
1745
+ evidenceRefs: [],
1746
+ })),
1747
+ );
1748
+ } else {
1749
+ const semanticWorkspace = await createFixture(
1750
+ { files: {} },
1751
+ [],
1752
+ undefined,
1753
+ null,
1754
+ null,
1755
+ null,
1756
+ null,
1757
+ projectRoot,
1758
+ options.signal,
1759
+ reservedSemanticWorkspaces[trial - 1],
1760
+ );
1761
+ let semanticResult: HostResult | null = null;
1762
+ let semanticArtifacts: ReturnType<typeof hostArtifacts> = [];
1763
+ try {
1764
+ const response = await options.semanticHost!.run({
1765
+ prompt: semanticPrompt(
1766
+ hostResult.finalMessage,
1767
+ preparedSemantic,
1768
+ ),
1769
+ workspace: semanticWorkspace,
1770
+ condition: "passive",
1771
+ signal: options.signal,
1772
+ });
1773
+ semanticArtifacts = hostArtifacts(
1774
+ {
1775
+ ...response,
1776
+ artifacts: response.artifacts?.map((item) => ({
1777
+ ...item,
1778
+ id: `sevro.semantic.${item.id}`,
1779
+ })),
1780
+ },
1781
+ new Set([
1782
+ ...trialArtifactRefs.map((item) => item.id),
1783
+ ...options.case.checks.map((item) => item.id),
1784
+ "sevro.semantic.verdicts",
1785
+ ]),
1786
+ );
1787
+ semanticResult = response;
1788
+ } catch {
1789
+ if (options.signal?.aborted) {
1790
+ execution = "cancelled";
1791
+ diagnostic = {
1792
+ code: "sevro.run.cancelled",
1793
+ message: "run cancelled",
1794
+ };
1795
+ } else {
1796
+ graderError = true;
1797
+ diagnostic = {
1798
+ code: "sevro.grader.error",
1799
+ message: "semantic grading did not complete",
1800
+ };
1801
+ stopAdmission();
1802
+ }
1803
+ } finally {
1804
+ await rm(semanticWorkspace, { recursive: true, force: true });
1805
+ }
1806
+ if (semanticResult) {
1807
+ for (const artifact of semanticArtifacts) {
1808
+ const path = join(runDir, `trial-${trial}-${artifact.id}.bin`);
1809
+ try {
1810
+ await writeFile(path, artifact.bytes, {
1811
+ flag: "wx",
1812
+ mode: 0o600,
1813
+ });
1814
+ } catch {
1815
+ throw new Error(
1816
+ `trial persistence failed; fixture retained at ${workspace}`,
1817
+ );
1818
+ }
1819
+ trialArtifactRefs.push({
1820
+ id: artifact.id,
1821
+ path: pathToFileURL(path).href,
1822
+ sha256: sha256(artifact.bytes),
1823
+ });
1824
+ }
1825
+ semanticObservations.push({
1826
+ id: "sevro.observation.semantic.usage",
1827
+ source: options.semanticHost!.id,
1828
+ completeness: semanticResult.usageComplete
1829
+ ? "complete"
1830
+ : "partial",
1831
+ data: usage(semanticResult),
1832
+ });
1833
+ if (
1834
+ semanticResult.finalMessage !== null &&
1835
+ Buffer.byteLength(semanticResult.finalMessage, "utf8") <=
1836
+ 1024 * 1024
1837
+ ) {
1838
+ const bytes = Buffer.from(semanticResult.finalMessage, "utf8");
1839
+ const path = join(
1840
+ runDir,
1841
+ `trial-${trial}-semantic-verdicts.json`,
1842
+ );
1843
+ try {
1844
+ await writeFile(path, bytes, { flag: "wx", mode: 0o600 });
1845
+ } catch {
1846
+ throw new Error(
1847
+ `trial persistence failed; fixture retained at ${workspace}`,
1848
+ );
1849
+ }
1850
+ trialArtifactRefs.push({
1851
+ id: "sevro.semantic.verdicts",
1852
+ path: pathToFileURL(path).href,
1853
+ sha256: sha256(bytes),
1854
+ });
1855
+ }
1856
+ try {
1857
+ if (!semanticResult.complete || !semanticResult.finalMessage)
1858
+ throw new Error("semantic grader result is incomplete");
1859
+ const verdicts = parseSemanticVerdicts(
1860
+ semanticResult.finalMessage,
1861
+ preparedSemantic,
1862
+ );
1863
+ for (const verdict of verdicts) {
1864
+ const observationId = `sevro.observation.semantic.${sha256(verdict.id)}`;
1865
+ semanticObservations.push({
1866
+ id: observationId,
1867
+ source: options.semanticHost!.id,
1868
+ completeness: "complete",
1869
+ data: { verdict: verdict.verdict, reason: verdict.reason },
1870
+ });
1871
+ checks.push({
1872
+ id: verdict.id,
1873
+ grader: "sevro.semantic",
1874
+ status: verdict.verdict === "pass" ? "passed" : "failed",
1875
+ detail: verdict.reason,
1876
+ evidenceRefs: [observationId],
1877
+ });
1878
+ }
1879
+ } catch {
1880
+ graderError = true;
1881
+ diagnostic = {
1882
+ code: "sevro.grader.error",
1883
+ message: "semantic grading did not complete",
1884
+ };
1885
+ stopAdmission();
1886
+ }
1887
+ }
1888
+ }
1889
+ }
1890
+ if (options.extension && !graderError && execution === "completed") {
1891
+ try {
1892
+ const extensionResult = await options.extension.session.evaluate({
1893
+ caseId: options.case.id,
1894
+ execution: { status: execution },
1895
+ observations: [
1896
+ {
1897
+ ...observation,
1898
+ completeness,
1899
+ data: {
1900
+ ...observation.data,
1901
+ ...(hostResult?.finalMessage == null
1902
+ ? {}
1903
+ : { text: hostResult.finalMessage }),
1904
+ },
1905
+ },
1906
+ ...shellObservations,
1907
+ ...gitHeadObservations,
1908
+ ...semanticObservations,
1909
+ ...additionalObservations,
1910
+ ],
1911
+ builtinChecks: checks.map((check) => ({
1912
+ id: check.id,
1913
+ status: check.status,
1914
+ ...(check.detail ? { detail: check.detail } : {}),
1915
+ evidenceRefs: check.evidenceRefs ?? [],
1916
+ })),
1917
+ artifacts: trialArtifactRefs,
1918
+ extensionData,
1919
+ });
1920
+ const declared = new Map(
1921
+ extensionDeclarations.map((check) => [check.id, check.grader]),
1922
+ );
1923
+ if (extensionResult.checks.some((check) => !declared.has(check.id)))
1924
+ throw new Error("extension returned an undeclared check");
1925
+ checks.push(
1926
+ ...extensionResult.checks.map((check) => ({
1927
+ id: check.id,
1928
+ grader: declared.get(check.id)!,
1929
+ status: check.status,
1930
+ ...(check.detail ? { detail: check.detail } : {}),
1931
+ evidenceRefs: check.evidenceRefs,
1932
+ })),
1933
+ );
1934
+ extensionMetrics = extensionResult.metrics;
1935
+ domainOutcomes = extensionResult.domainOutcomes ?? [];
1936
+ taskPolicyRecommendation =
1937
+ extensionResult.taskVerdictRecommendation ?? null;
1938
+ } catch {
1939
+ if (options.signal?.aborted) {
1940
+ execution = "cancelled";
1941
+ diagnostic = {
1942
+ code: "sevro.run.cancelled",
1943
+ message: "run cancelled",
1944
+ };
1945
+ } else {
1946
+ graderError = true;
1947
+ diagnostic = {
1948
+ code: "sevro.grader.error",
1949
+ message: "extension grading did not complete",
1950
+ };
1951
+ stopAdmission();
1952
+ }
1953
+ }
1954
+ }
1955
+ const declaredOrder = new Map(
1956
+ options.case.checks.map((check, index) => [check.id, index]),
1957
+ );
1958
+ checks.sort(
1959
+ (left, right) =>
1960
+ declaredOrder.get(left.id)! - declaredOrder.get(right.id)!,
1961
+ );
1962
+ const defaultAssessment = assessTrial({
1963
+ execution,
1964
+ declaredChecks: options.case.checks
1965
+ .filter((check) => !replaced.has(check.grader))
1966
+ .map((check) => check.id),
1967
+ checks,
1968
+ graderError,
1969
+ requiredEvidenceUnavailable: options.case.requiredEvidence.some(
1970
+ (id) =>
1971
+ !producedArtifacts.some((item) => item.id === id) &&
1972
+ ![
1973
+ observation,
1974
+ ...shellObservations,
1975
+ ...gitHeadObservations,
1976
+ ...semanticObservations,
1977
+ ...additionalObservations,
1978
+ ].some(
1979
+ (item) => item.id === id && item.completeness === "complete",
1980
+ ),
1981
+ ),
1982
+ });
1983
+ const assessment = applyTaskVerdictPolicy(
1984
+ defaultAssessment,
1985
+ taskPolicyRecommendation,
1986
+ Boolean(
1987
+ options.extension?.session.identity.selectedTaskVerdictPolicy,
1988
+ ),
1989
+ );
1990
+ let advisoryReview: {
1991
+ status: "completed" | "failed" | "not_run";
1992
+ assessment: AdvisoryAssessment | null;
1993
+ usage: ReturnType<typeof usage>;
1994
+ rawResult: {
1995
+ source: string;
1996
+ path: string | null;
1997
+ sha256: string | null;
1998
+ };
1999
+ } | null = null;
2000
+ if (options.advisoryHost) {
2001
+ advisoryReview = {
2002
+ status: "not_run",
2003
+ assessment: null,
2004
+ usage: usage(null),
2005
+ rawResult: {
2006
+ source: options.advisoryHost.id,
2007
+ path: null,
2008
+ sha256: null,
2009
+ },
2010
+ };
2011
+ if (execution === "completed" && !options.signal?.aborted) {
2012
+ let reviewWorkspace: string | null = null;
2013
+ try {
2014
+ reviewWorkspace = await buildBlindAdvisoryFixture(workspace, {
2015
+ baseRevision: advisoryRevision!,
2016
+ excludedPaths: options.advisoryExcludedPaths,
2017
+ });
2018
+ const response = await options.advisoryHost.run({
2019
+ prompt: advisoryPrompt(trialPrompt, checks),
2020
+ workspace: reviewWorkspace,
2021
+ condition: "passive",
2022
+ signal: options.signal,
2023
+ });
2024
+ advisoryReview.usage = usage(response);
2025
+ const reviewArtifacts = hostArtifacts(
2026
+ {
2027
+ ...response,
2028
+ artifacts: response.artifacts?.map((item) => ({
2029
+ ...item,
2030
+ id: `sevro.advisory.${item.id}`,
2031
+ })),
2032
+ },
2033
+ new Set([
2034
+ ...trialArtifactRefs.map((item) => item.id),
2035
+ ...options.case.checks.map((item) => item.id),
2036
+ "sevro.advisory.response",
2037
+ ]),
2038
+ );
2039
+ for (const artifact of reviewArtifacts) {
2040
+ const path = join(runDir, `trial-${trial}-${artifact.id}.bin`);
2041
+ try {
2042
+ await writeFile(path, artifact.bytes, {
2043
+ flag: "wx",
2044
+ mode: 0o600,
2045
+ });
2046
+ } catch {
2047
+ throw new Error(
2048
+ `trial persistence failed; fixture retained at ${workspace}`,
2049
+ );
2050
+ }
2051
+ trialArtifactRefs.push({
2052
+ id: artifact.id,
2053
+ path: pathToFileURL(path).href,
2054
+ sha256: sha256(artifact.bytes),
2055
+ });
2056
+ }
2057
+ if (
2058
+ response.finalMessage !== null &&
2059
+ Buffer.byteLength(response.finalMessage, "utf8") <= 64 * 1024
2060
+ ) {
2061
+ const path = join(runDir, `trial-${trial}-advisory.json`);
2062
+ try {
2063
+ await writeFile(path, response.finalMessage, {
2064
+ flag: "wx",
2065
+ mode: 0o600,
2066
+ });
2067
+ } catch {
2068
+ throw new Error(
2069
+ `trial persistence failed; fixture retained at ${workspace}`,
2070
+ );
2071
+ }
2072
+ advisoryReview.rawResult = {
2073
+ source: options.advisoryHost.id,
2074
+ path: pathToFileURL(path).href,
2075
+ sha256: sha256(response.finalMessage),
2076
+ };
2077
+ trialArtifactRefs.push({
2078
+ id: "sevro.advisory.response",
2079
+ path: pathToFileURL(path).href,
2080
+ sha256: sha256(response.finalMessage),
2081
+ });
2082
+ }
2083
+ if (!response.complete || response.finalMessage === null)
2084
+ throw new Error("advisory response is incomplete");
2085
+ advisoryReview.assessment = parseAdvisoryAssessment(
2086
+ response.finalMessage,
2087
+ );
2088
+ advisoryReview.status = "completed";
2089
+ } catch (error) {
2090
+ if (
2091
+ error instanceof Error &&
2092
+ error.message.startsWith("trial persistence failed")
2093
+ )
2094
+ throw error;
2095
+ advisoryReview.status = "failed";
2096
+ } finally {
2097
+ if (reviewWorkspace) {
2098
+ try {
2099
+ await rm(reviewWorkspace, { recursive: true, force: true });
2100
+ } catch {
2101
+ console.warn(
2102
+ `advisory fixture cleanup failed; retained at ${reviewWorkspace}`,
2103
+ );
2104
+ }
2105
+ }
2106
+ }
2107
+ }
2108
+ }
2109
+ await verifyRetainedArtifacts(trialArtifactRefs);
2110
+ const rawPath =
2111
+ hostResult?.finalMessage !== null &&
2112
+ hostResult?.finalMessage !== undefined
2113
+ ? join(runDir, `trial-${trial}-raw.txt`)
2114
+ : null;
2115
+ if (rawPath) {
2116
+ try {
2117
+ await writeFile(rawPath, hostResult!.finalMessage!, {
2118
+ flag: "wx",
2119
+ mode: 0o600,
2120
+ });
2121
+ } catch {
2122
+ throw new Error(
2123
+ `trial persistence failed; fixture retained at ${workspace}`,
2124
+ );
2125
+ }
2126
+ }
2127
+ const evidence = {
2128
+ caseId: options.case.id,
2129
+ trial,
2130
+ executionMode: options.dry ? "dry" : "executed",
2131
+ candidateDurationMs,
2132
+ condition: {
2133
+ requested: options.condition,
2134
+ actual: hostResult?.actualCondition ?? "unknown",
2135
+ appliedInstrumentation,
2136
+ },
2137
+ observationCompleteness: completeness,
2138
+ observations: [
2139
+ observation,
2140
+ ...shellObservations,
2141
+ ...gitHeadObservations,
2142
+ ...semanticObservations,
2143
+ ...additionalObservations,
2144
+ ],
2145
+ metrics: extensionMetrics,
2146
+ domainOutcomes,
2147
+ taskVerdictPolicy: options.extension?.session.identity
2148
+ .selectedTaskVerdictPolicy
2149
+ ? {
2150
+ id: options.extension.session.identity
2151
+ .selectedTaskVerdictPolicy,
2152
+ recommendation: taskPolicyRecommendation,
2153
+ }
2154
+ : null,
2155
+ advisoryReview,
2156
+ routes,
2157
+ usage: usage(hostResult),
2158
+ rawResult: {
2159
+ source: options.host.id,
2160
+ path: rawPath ? pathToFileURL(rawPath).href : null,
2161
+ sha256: rawDigest,
2162
+ },
2163
+ artifactRefs: trialArtifactRefs,
2164
+ };
2165
+ const artifactPath = join(runDir, `trial-${trial}.json`);
2166
+ const trialSummary: TrialSummary = {
2167
+ trial,
2168
+ ...assessment,
2169
+ checks,
2170
+ domainOutcomes,
2171
+ artifactPath,
2172
+ };
2173
+ try {
2174
+ await atomicWriteJson(artifactPath, {
2175
+ format: "sevro.trial-evidence.v1",
2176
+ caseId: options.case.id,
2177
+ trial,
2178
+ result: trialSummary,
2179
+ evidence,
2180
+ });
2181
+ } catch {
2182
+ throw new Error(
2183
+ `trial persistence failed; fixture retained at ${workspace}`,
2184
+ );
2185
+ }
2186
+ persisted = true;
2187
+ trialSummaries.push(trialSummary);
2188
+ trialSummaries.sort((left, right) => left.trial - right.trial);
2189
+ trialEvidence.push(evidence);
2190
+ trialEvidence.sort(
2191
+ (left, right) => Number(left.trial) - Number(right.trial),
2192
+ );
2193
+ if (diagnostic) diagnostics.push({ trial, diagnostic });
2194
+ saveState("active");
2195
+ return !(
2196
+ (execution !== "completed" && execution !== "not_run") ||
2197
+ graderError
2198
+ );
2199
+ } finally {
2200
+ if (persisted) {
2201
+ try {
2202
+ await clearFixtureContents(workspace);
2203
+ retainedWorkspaces.delete(reservation);
2204
+ } catch {
2205
+ console.warn(`fixture cleanup failed; retained at ${workspace}`);
2206
+ }
2207
+ }
2208
+ }
2209
+ };
2210
+ await scheduleTrials(
2211
+ { count: options.trialCount, jobs, signal: options.signal },
2212
+ runTrial,
2213
+ );
2214
+ const diagnostic = diagnostics.sort(
2215
+ (left, right) => left.trial - right.trial,
2216
+ )[0]?.diagnostic;
2217
+
2218
+ const caseAssessment: Assessment =
2219
+ !trialSummaries.length && options.signal?.aborted
2220
+ ? {
2221
+ execution: { status: "cancelled" },
2222
+ grading: { status: "not_requested" },
2223
+ task: { verdict: "not_assessed" },
2224
+ }
2225
+ : summarizeAssessments(trialSummaries, options.passThreshold);
2226
+ const caseResult: CaseSummary = {
2227
+ caseId: options.case.id,
2228
+ ...caseAssessment,
2229
+ trials: trialSummaries,
2230
+ };
2231
+ const runAssessment = summarizeCases([caseResult]);
2232
+ const signalReason = options.signal?.aborted ? options.signal.reason : null;
2233
+ const result: CliResult = {
2234
+ format: "sevro.cli-result.v1",
2235
+ runId,
2236
+ ...runAssessment,
2237
+ exitCode: exitCodeFor(runAssessment, {
2238
+ dryRun: options.dry,
2239
+ signal:
2240
+ signalReason === "SIGINT" || signalReason === "SIGTERM"
2241
+ ? signalReason
2242
+ : undefined,
2243
+ }),
2244
+ evidencePath,
2245
+ cases: [caseResult],
2246
+ };
2247
+ const actualConditions = [
2248
+ ...new Set(
2249
+ trialEvidence.map(
2250
+ (entry) => (entry.condition as { actual: string }).actual,
2251
+ ),
2252
+ ),
2253
+ ];
2254
+ const commonAppliedInstrumentation =
2255
+ trialEvidence.length === options.trialCount &&
2256
+ trialEvidence.every(
2257
+ (entry) =>
2258
+ canonicalJson(
2259
+ (
2260
+ entry.condition as {
2261
+ appliedInstrumentation: InstrumentationRequest[];
2262
+ }
2263
+ ).appliedInstrumentation,
2264
+ ) === canonicalJson(requestedInstrumentation),
2265
+ )
2266
+ ? requestedInstrumentation
2267
+ : [];
2268
+ const runEvidence = {
2269
+ format: "sevro.run-evidence.v1",
2270
+ runId,
2271
+ evaluationIdentity,
2272
+ configuration: {
2273
+ digest: hashJson(redactedConfig),
2274
+ redacted: redactedConfig,
2275
+ },
2276
+ runner,
2277
+ project,
2278
+ extension: options.extension
2279
+ ? {
2280
+ id: options.extension.session.identity.id,
2281
+ version: options.extension.session.identity.version,
2282
+ sourceDigest: options.extension.session.identity.sourceDigest,
2283
+ configurationDigest:
2284
+ options.extension.session.identity.configurationDigest,
2285
+ protocol: options.extension.session.identity.protocol,
2286
+ capabilities: options.extension.session.identity.capabilities,
2287
+ replacements: {
2288
+ graders: replacedBuiltinGraders,
2289
+ taskVerdictPolicy:
2290
+ options.extension.session.identity.selectedTaskVerdictPolicy,
2291
+ },
2292
+ }
2293
+ : null,
2294
+ condition: {
2295
+ requested: options.condition,
2296
+ actual: actualConditions.length === 1 ? actualConditions[0] : "unknown",
2297
+ requestedInstrumentation,
2298
+ appliedInstrumentation: commonAppliedInstrumentation,
2299
+ },
2300
+ graders: {
2301
+ active: activeGraders,
2302
+ replacedDefaults: replacedBuiltinGraders,
2303
+ },
2304
+ routes,
2305
+ result,
2306
+ trials: trialEvidence,
2307
+ ...(diagnostic ? { diagnostic } : {}),
2308
+ };
2309
+ assertCliResult(result);
2310
+ assertRunEvidence(runEvidence);
2311
+ await atomicWriteJson(evidencePath, runEvidence);
2312
+ saveState(options.signal?.aborted ? "interrupted" : "complete");
2313
+ return { result };
2314
+ } catch (error) {
2315
+ try {
2316
+ checkpointRunOwner(
2317
+ owner,
2318
+ trialSummaries.map((trial) => ({
2319
+ trial: trial.trial,
2320
+ artifactPath: trial.artifactPath,
2321
+ })),
2322
+ "diagnostic",
2323
+ );
2324
+ } catch {
2325
+ // Preserve the original failure; retained artifacts remain inspectable.
2326
+ }
2327
+ throw error;
2328
+ } finally {
2329
+ await Promise.all(
2330
+ [...reservedWorkspaces, ...reservedSemanticWorkspaces]
2331
+ .filter((workspace) => !retainedWorkspaces.has(workspace))
2332
+ .map(async (workspace) => {
2333
+ try {
2334
+ await rm(workspace, { recursive: true, force: true });
2335
+ } catch {
2336
+ console.warn(`fixture cleanup failed; retained at ${workspace}`);
2337
+ }
2338
+ }),
2339
+ );
2340
+ }
2341
+ }