@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/cli.ts ADDED
@@ -0,0 +1,837 @@
1
+ #!/usr/bin/env bun
2
+ import { readFile } from "node:fs/promises";
3
+ import { isAbsolute } from "node:path";
4
+ import { fileURLToPath, pathToFileURL } from "node:url";
5
+ import { parseArgs } from "node:util";
6
+ import {
7
+ EvaluationConfigurationError,
8
+ runEvaluation,
9
+ type HostAdapter,
10
+ type ResolvedCase,
11
+ } from "./engine";
12
+ import { createCodexHost } from "./hosts/codex";
13
+ import {
14
+ codexAgentConcurrency,
15
+ configurationRoot,
16
+ } from "./hosts/codex-configuration";
17
+ import { createClaudeHost } from "./hosts/claude";
18
+ import { prepareGeneratedFixture } from "./generated-fixture";
19
+ import { prepareRepositoryFixture } from "./repository-fixture";
20
+ import { prepareInstrumentation } from "./instrumentation";
21
+ import { openExtensionSession, type ExtensionCase } from "./extension-session";
22
+ import { assertCliResult } from "./schema";
23
+ import { reportCommand } from "./report";
24
+ import {
25
+ packageBuildDigest,
26
+ projectIdentityDigest,
27
+ projectProvenance,
28
+ } from "./provenance";
29
+
30
+ class InvocationError extends Error {}
31
+
32
+ function record(value: unknown): value is Record<string, unknown> {
33
+ return Boolean(value && typeof value === "object" && !Array.isArray(value));
34
+ }
35
+
36
+ function requiredOption(value: string | undefined, name: string): string {
37
+ if (!value) throw new InvocationError(`missing ${name}`);
38
+ return value;
39
+ }
40
+
41
+ function absoluteOption(value: string | undefined, name: string): string {
42
+ const path = requiredOption(value, name);
43
+ if (!isAbsolute(path)) throw new InvocationError(`${name} must be absolute`);
44
+ return path;
45
+ }
46
+
47
+ function optionalDigest(value: string | undefined, name: string) {
48
+ if (value !== undefined && !/^[a-f0-9]{64}$/.test(value))
49
+ throw new InvocationError(`${name} must be a 64-character SHA-256 digest`);
50
+ return value;
51
+ }
52
+
53
+ function validFixture(value: unknown): boolean {
54
+ if (!record(value)) return false;
55
+ if (value.kind === "generated") {
56
+ try {
57
+ prepareGeneratedFixture(value);
58
+ return true;
59
+ } catch {
60
+ return false;
61
+ }
62
+ }
63
+ if (value.kind !== undefined) return false;
64
+ if (Object.hasOwn(value, "sourceRef")) {
65
+ try {
66
+ prepareRepositoryFixture({ kind: "repository", ...value });
67
+ return true;
68
+ } catch {
69
+ return false;
70
+ }
71
+ }
72
+ if (Object.hasOwn(value, "files"))
73
+ return (
74
+ !Object.hasOwn(value, "sourceRef") &&
75
+ record(value.files) &&
76
+ Object.values(value.files).every((content) => typeof content === "string")
77
+ );
78
+ return false;
79
+ }
80
+
81
+ function parseCase(value: unknown): ResolvedCase {
82
+ if (
83
+ !record(value) ||
84
+ typeof value.id !== "string" ||
85
+ !value.id ||
86
+ typeof value.prompt !== "string" ||
87
+ !value.prompt ||
88
+ (value.followUpPrompt !== undefined &&
89
+ (typeof value.followUpPrompt !== "string" ||
90
+ !value.followUpPrompt.trim())) ||
91
+ !validFixture(value.fixture) ||
92
+ !Array.isArray(value.checks) ||
93
+ !value.checks.every(
94
+ (check) =>
95
+ record(check) &&
96
+ typeof check.id === "string" &&
97
+ typeof check.grader === "string" &&
98
+ record(check.configuration),
99
+ ) ||
100
+ !Array.isArray(value.requiredEvidence) ||
101
+ !value.requiredEvidence.every((item) => typeof item === "string")
102
+ )
103
+ throw new InvocationError("invalid resolved case file");
104
+ return value as unknown as ResolvedCase;
105
+ }
106
+
107
+ function parseInvocation(argv: string[]) {
108
+ const parsed = (() => {
109
+ try {
110
+ return parseArgs({
111
+ args: argv,
112
+ allowPositionals: true,
113
+ strict: true,
114
+ options: {
115
+ json: { type: "boolean" },
116
+ dry: { type: "boolean" },
117
+ "case-file": { type: "string" },
118
+ "case-id": { type: "string" },
119
+ "extension-command-file": { type: "string" },
120
+ "extension-source-file": { type: "string", multiple: true },
121
+ "extension-configuration-file": { type: "string" },
122
+ "extension-redacted-configuration-file": { type: "string" },
123
+ "task-verdict-policy": { type: "string" },
124
+ "replace-builtin-grader": { type: "string", multiple: true },
125
+ "case-source-root": { type: "string" },
126
+ "case-source-map-file": { type: "string" },
127
+ "adapter-module": { type: "string" },
128
+ "semantic-adapter-module": { type: "string" },
129
+ "semantic-host": { type: "string" },
130
+ "semantic-model": { type: "string" },
131
+ "semantic-effort": { type: "string" },
132
+ "advisory-adapter-module": { type: "string" },
133
+ "advisory-host": { type: "string" },
134
+ "advisory-model": { type: "string" },
135
+ "advisory-effort": { type: "string" },
136
+ "advisory-exclude": { type: "string", multiple: true },
137
+ host: { type: "string" },
138
+ "codex-bin": { type: "string" },
139
+ "codex-auth-file": { type: "string" },
140
+ "claude-bin": { type: "string" },
141
+ "claude-credential-file": { type: "string" },
142
+ "claude-uv-cache-dir": { type: "string" },
143
+ "claude-project-settings": { type: "boolean" },
144
+ "toolchain-bin-dir": { type: "string" },
145
+ model: { type: "string" },
146
+ effort: { type: "string" },
147
+ "shell-isolation": { type: "boolean" },
148
+ "protected-root": { type: "string", multiple: true },
149
+ "project-root": { type: "string" },
150
+ "config-root": { type: "string" },
151
+ "results-root": { type: "string" },
152
+ "run-state-root": { type: "string" },
153
+ "runner-build-digest": { type: "string" },
154
+ "runner-checkout-root": { type: "string" },
155
+ "project-digest": { type: "string" },
156
+ condition: { type: "string" },
157
+ trials: { type: "string" },
158
+ jobs: { type: "string", default: "3" },
159
+ threshold: { type: "string" },
160
+ },
161
+ } as const);
162
+ } catch {
163
+ throw new InvocationError("invalid CLI arguments");
164
+ }
165
+ })();
166
+ if (parsed.positionals.length !== 1 || parsed.positionals[0] !== "run")
167
+ throw new InvocationError("expected the run command");
168
+ const values = parsed.values;
169
+ const condition = requiredOption(values.condition, "--condition");
170
+ if (condition !== "passive" && condition !== "enforced")
171
+ throw new InvocationError("invalid --condition");
172
+ const trialCount = Number(requiredOption(values.trials, "--trials"));
173
+ const jobs = Number(requiredOption(values.jobs, "--jobs"));
174
+ const passThreshold = Number(requiredOption(values.threshold, "--threshold"));
175
+ if (!Number.isSafeInteger(trialCount) || trialCount < 1)
176
+ throw new InvocationError("invalid --trials");
177
+ if (!Number.isSafeInteger(jobs) || jobs < 1)
178
+ throw new InvocationError("invalid --jobs");
179
+ if (
180
+ !Number.isFinite(passThreshold) ||
181
+ passThreshold <= 0 ||
182
+ passThreshold > 1
183
+ )
184
+ throw new InvocationError("invalid --threshold");
185
+ const protectedRoots = values["protected-root"] ?? [];
186
+ if (protectedRoots.some((path) => !isAbsolute(path)))
187
+ throw new InvocationError("--protected-root must be absolute");
188
+ const toolchainBinDir = values["toolchain-bin-dir"]
189
+ ? absoluteOption(values["toolchain-bin-dir"], "--toolchain-bin-dir")
190
+ : undefined;
191
+ const codex = values.host === "codex";
192
+ const claude = values.host === "claude";
193
+ const builtinHost = codex || claude;
194
+ const semanticCodex = values["semantic-host"] === "codex";
195
+ const advisoryCodex = values["advisory-host"] === "codex";
196
+ if (values.host && !builtinHost)
197
+ throw new InvocationError("unsupported --host");
198
+ if (values["semantic-host"] && !semanticCodex)
199
+ throw new InvocationError("unsupported --semantic-host");
200
+ if (values["advisory-host"] && !advisoryCodex)
201
+ throw new InvocationError("unsupported --advisory-host");
202
+ if (semanticCodex && values["semantic-adapter-module"])
203
+ throw new InvocationError(
204
+ "--semantic-host and --semantic-adapter-module are exclusive",
205
+ );
206
+ if (advisoryCodex && values["advisory-adapter-module"])
207
+ throw new InvocationError(
208
+ "--advisory-host and --advisory-adapter-module are exclusive",
209
+ );
210
+ if (builtinHost && values["adapter-module"])
211
+ throw new InvocationError("--host and --adapter-module are exclusive");
212
+ if (!builtinHost && !values["adapter-module"])
213
+ throw new InvocationError("missing --adapter-module or --host");
214
+ if (!builtinHost && (values.model || values.effort))
215
+ throw new InvocationError("--model and --effort require a built-in host");
216
+ if (!semanticCodex && (values["semantic-model"] || values["semantic-effort"]))
217
+ throw new InvocationError(
218
+ "semantic model options require --semantic-host codex",
219
+ );
220
+ if (!advisoryCodex && (values["advisory-model"] || values["advisory-effort"]))
221
+ throw new InvocationError(
222
+ "advisory model options require --advisory-host codex",
223
+ );
224
+ if (
225
+ values["advisory-exclude"]?.length &&
226
+ !advisoryCodex &&
227
+ !values["advisory-adapter-module"]
228
+ )
229
+ throw new InvocationError("--advisory-exclude requires an advisory route");
230
+ if (
231
+ !codex &&
232
+ !semanticCodex &&
233
+ !advisoryCodex &&
234
+ (values["codex-bin"] || values["codex-auth-file"])
235
+ )
236
+ throw new InvocationError("Codex options require a Codex host route");
237
+ if (
238
+ !claude &&
239
+ (values["claude-bin"] ||
240
+ values["claude-credential-file"] ||
241
+ values["claude-uv-cache-dir"] ||
242
+ values["claude-project-settings"])
243
+ )
244
+ throw new InvocationError("Claude options require --host claude");
245
+ if (
246
+ protectedRoots.length &&
247
+ !values["shell-isolation"] &&
248
+ !builtinHost &&
249
+ !semanticCodex &&
250
+ !advisoryCodex
251
+ )
252
+ throw new InvocationError("--protected-root requires isolation");
253
+ const projectRoot = absoluteOption(values["project-root"], "--project-root");
254
+ const configRoot =
255
+ values["config-root"] !== undefined
256
+ ? absoluteOption(values["config-root"], "--config-root")
257
+ : projectRoot;
258
+ const resultsRoot = absoluteOption(values["results-root"], "--results-root");
259
+ const runStateRoot = values["run-state-root"]
260
+ ? absoluteOption(values["run-state-root"], "--run-state-root")
261
+ : resultsRoot;
262
+ const extensionCommandFile = values["extension-command-file"];
263
+ const taskVerdictPolicy = values["task-verdict-policy"];
264
+ const replaceBuiltinGraders = values["replace-builtin-grader"] ?? [];
265
+ if (
266
+ taskVerdictPolicy !== undefined &&
267
+ !/^[a-z][a-z0-9]*(?:[.-][a-z0-9]+)+$/.test(taskVerdictPolicy)
268
+ )
269
+ throw new InvocationError("invalid --task-verdict-policy");
270
+ if (
271
+ new Set(replaceBuiltinGraders).size !== replaceBuiltinGraders.length ||
272
+ replaceBuiltinGraders.some(
273
+ (id) => !/^[a-z][a-z0-9]*(?:[.-][a-z0-9]+)+$/.test(id),
274
+ )
275
+ )
276
+ throw new InvocationError("invalid --replace-builtin-grader");
277
+ if (Boolean(values["case-file"]) === Boolean(extensionCommandFile))
278
+ throw new InvocationError(
279
+ "select exactly one of --case-file or --extension-command-file",
280
+ );
281
+ if (extensionCommandFile) {
282
+ if (!values["case-id"]) throw new InvocationError("missing --case-id");
283
+ if (!values["extension-source-file"]?.length)
284
+ throw new InvocationError("missing --extension-source-file");
285
+ if (values["extension-source-file"].some((path) => !isAbsolute(path)))
286
+ throw new InvocationError("--extension-source-file must be absolute");
287
+ } else if (
288
+ values["case-id"] ||
289
+ values["extension-source-file"] ||
290
+ values["extension-configuration-file"] ||
291
+ values["extension-redacted-configuration-file"] ||
292
+ taskVerdictPolicy !== undefined ||
293
+ replaceBuiltinGraders.length > 0
294
+ )
295
+ throw new InvocationError(
296
+ "extension options require --extension-command-file",
297
+ );
298
+ if (
299
+ Boolean(values["extension-configuration-file"]) !==
300
+ Boolean(values["extension-redacted-configuration-file"])
301
+ )
302
+ throw new InvocationError(
303
+ "extension configuration requires a redacted file",
304
+ );
305
+ if (
306
+ Boolean(values["case-source-root"]) !==
307
+ Boolean(values["case-source-map-file"])
308
+ )
309
+ throw new InvocationError("case sources require a root and map file");
310
+ const privateRoots = [
311
+ configRoot,
312
+ ...protectedRoots,
313
+ ...(values["claude-credential-file"]
314
+ ? [values["claude-credential-file"]]
315
+ : []),
316
+ ...(values["case-file"] ? [values["case-file"]] : []),
317
+ ...(extensionCommandFile ? [extensionCommandFile] : []),
318
+ ...(values["extension-source-file"] ?? []),
319
+ ...(values["extension-configuration-file"]
320
+ ? [values["extension-configuration-file"]]
321
+ : []),
322
+ ...(values["extension-redacted-configuration-file"]
323
+ ? [values["extension-redacted-configuration-file"]]
324
+ : []),
325
+ ...(values["case-source-root"] ? [values["case-source-root"]] : []),
326
+ ...(values["case-source-map-file"] ? [values["case-source-map-file"]] : []),
327
+ ...(values["semantic-adapter-module"]
328
+ ? [values["semantic-adapter-module"]]
329
+ : []),
330
+ ...(values["advisory-adapter-module"]
331
+ ? [values["advisory-adapter-module"]]
332
+ : []),
333
+ ];
334
+ const codexCommon =
335
+ codex || semanticCodex || advisoryCodex
336
+ ? {
337
+ binary: absoluteOption(values["codex-bin"], "--codex-bin"),
338
+ authFile: absoluteOption(
339
+ values["codex-auth-file"],
340
+ "--codex-auth-file",
341
+ ),
342
+ projectRoot,
343
+ resultsRoot,
344
+ additionalProtectedRoots: [...privateRoots, runStateRoot],
345
+ }
346
+ : undefined;
347
+ const claudeOptions = claude
348
+ ? {
349
+ binary: absoluteOption(values["claude-bin"], "--claude-bin"),
350
+ ...(values["claude-credential-file"]
351
+ ? {
352
+ credentialFile: absoluteOption(
353
+ values["claude-credential-file"],
354
+ "--claude-credential-file",
355
+ ),
356
+ }
357
+ : {}),
358
+ ...(values["claude-uv-cache-dir"]
359
+ ? {
360
+ uvCacheDir: absoluteOption(
361
+ values["claude-uv-cache-dir"],
362
+ "--claude-uv-cache-dir",
363
+ ),
364
+ }
365
+ : {}),
366
+ model: requiredOption(values.model, "--model"),
367
+ effort: requiredOption(values.effort, "--effort"),
368
+ toolchainBinDir,
369
+ projectSettings: values["claude-project-settings"] ?? false,
370
+ projectRoot,
371
+ resultsRoot,
372
+ additionalProtectedRoots: [...privateRoots, runStateRoot],
373
+ }
374
+ : undefined;
375
+ return {
376
+ json: values.json ?? false,
377
+ dry: values.dry ?? false,
378
+ caseFile: values["case-file"]
379
+ ? absoluteOption(values["case-file"], "--case-file")
380
+ : undefined,
381
+ extension: extensionCommandFile
382
+ ? {
383
+ commandFile: absoluteOption(
384
+ extensionCommandFile,
385
+ "--extension-command-file",
386
+ ),
387
+ sourceFiles: values["extension-source-file"]!,
388
+ caseId: values["case-id"]!,
389
+ configurationFile: values["extension-configuration-file"]
390
+ ? absoluteOption(
391
+ values["extension-configuration-file"],
392
+ "--extension-configuration-file",
393
+ )
394
+ : undefined,
395
+ redactedConfigurationFile: values[
396
+ "extension-redacted-configuration-file"
397
+ ]
398
+ ? absoluteOption(
399
+ values["extension-redacted-configuration-file"],
400
+ "--extension-redacted-configuration-file",
401
+ )
402
+ : undefined,
403
+ taskVerdictPolicy,
404
+ replaceBuiltinGraders,
405
+ }
406
+ : undefined,
407
+ caseSourceRoot: values["case-source-root"]
408
+ ? absoluteOption(values["case-source-root"], "--case-source-root")
409
+ : undefined,
410
+ caseSourceMapFile: values["case-source-map-file"]
411
+ ? absoluteOption(values["case-source-map-file"], "--case-source-map-file")
412
+ : undefined,
413
+ adapterModule: builtinHost
414
+ ? undefined
415
+ : absoluteOption(values["adapter-module"], "--adapter-module"),
416
+ semanticAdapterModule: values["semantic-adapter-module"]
417
+ ? absoluteOption(
418
+ values["semantic-adapter-module"],
419
+ "--semantic-adapter-module",
420
+ )
421
+ : undefined,
422
+ semanticCodex: semanticCodex
423
+ ? {
424
+ ...codexCommon!,
425
+ model: requiredOption(values["semantic-model"], "--semantic-model"),
426
+ effort: requiredOption(
427
+ values["semantic-effort"],
428
+ "--semantic-effort",
429
+ ),
430
+ }
431
+ : undefined,
432
+ advisoryAdapterModule: values["advisory-adapter-module"]
433
+ ? absoluteOption(
434
+ values["advisory-adapter-module"],
435
+ "--advisory-adapter-module",
436
+ )
437
+ : undefined,
438
+ advisoryCodex: advisoryCodex
439
+ ? {
440
+ ...codexCommon!,
441
+ model: requiredOption(values["advisory-model"], "--advisory-model"),
442
+ effort: requiredOption(
443
+ values["advisory-effort"],
444
+ "--advisory-effort",
445
+ ),
446
+ }
447
+ : undefined,
448
+ advisoryExcludedPaths: values["advisory-exclude"] ?? [],
449
+ codex: codex
450
+ ? {
451
+ ...codexCommon!,
452
+ model: requiredOption(values.model, "--model"),
453
+ effort: requiredOption(values.effort, "--effort"),
454
+ }
455
+ : undefined,
456
+ claude: claudeOptions,
457
+ shellIsolation: values["shell-isolation"]
458
+ ? {
459
+ protectedRoots: privateRoots,
460
+ toolchainBinDir,
461
+ uvRuntimeCache: !!values["claude-uv-cache-dir"],
462
+ }
463
+ : undefined,
464
+ projectRoot,
465
+ configRoot,
466
+ resultsRoot,
467
+ runStateRoot,
468
+ runnerBuildDigest: optionalDigest(
469
+ values["runner-build-digest"],
470
+ "--runner-build-digest",
471
+ ),
472
+ runnerCheckoutRoot: values["runner-checkout-root"]
473
+ ? absoluteOption(values["runner-checkout-root"], "--runner-checkout-root")
474
+ : undefined,
475
+ projectDigest: optionalDigest(values["project-digest"], "--project-digest"),
476
+ condition: condition as "passive" | "enforced",
477
+ trialCount,
478
+ jobs,
479
+ passThreshold,
480
+ };
481
+ }
482
+
483
+ async function loadCase(path: string): Promise<ResolvedCase> {
484
+ try {
485
+ return parseCase(JSON.parse(await readFile(path, "utf8")));
486
+ } catch {
487
+ throw new InvocationError("resolved case file is unreadable or invalid");
488
+ }
489
+ }
490
+
491
+ async function loadJson(path: string, label: string): Promise<unknown> {
492
+ try {
493
+ return JSON.parse(await readFile(path, "utf8"));
494
+ } catch {
495
+ throw new InvocationError(`${label} is unreadable or invalid`);
496
+ }
497
+ }
498
+
499
+ async function loadExtensionOptions(
500
+ selected: NonNullable<ReturnType<typeof parseInvocation>["extension"]>,
501
+ ) {
502
+ const command = await loadJson(
503
+ selected.commandFile,
504
+ "extension command file",
505
+ );
506
+ if (
507
+ !Array.isArray(command) ||
508
+ !command.length ||
509
+ !command.every((part) => typeof part === "string" && part.length) ||
510
+ !isAbsolute(command[0])
511
+ )
512
+ throw new InvocationError(
513
+ "extension command must be an absolute argv array",
514
+ );
515
+ const configuration = selected.configurationFile
516
+ ? await loadJson(selected.configurationFile, "extension configuration file")
517
+ : {};
518
+ const redactedConfiguration = selected.redactedConfigurationFile
519
+ ? await loadJson(
520
+ selected.redactedConfigurationFile,
521
+ "redacted extension configuration file",
522
+ )
523
+ : {};
524
+ if (!record(configuration) || !record(redactedConfiguration))
525
+ throw new InvocationError("extension configuration must be JSON objects");
526
+ return { command: command as string[], configuration, redactedConfiguration };
527
+ }
528
+
529
+ async function loadPreparationSources(
530
+ selected: ReturnType<typeof parseInvocation>,
531
+ ) {
532
+ if (!selected.caseSourceRoot || !selected.caseSourceMapFile) return undefined;
533
+ const refs = await loadJson(
534
+ selected.caseSourceMapFile,
535
+ "case source map file",
536
+ );
537
+ if (
538
+ !record(refs) ||
539
+ !Object.values(refs).every(
540
+ (value) => typeof value === "string" && value.startsWith("file:///"),
541
+ )
542
+ )
543
+ throw new InvocationError("case source map must contain file URLs");
544
+ return {
545
+ root: selected.caseSourceRoot,
546
+ refs: refs as Record<string, string>,
547
+ };
548
+ }
549
+
550
+ function selectExtensionCase(
551
+ cases: ExtensionCase[],
552
+ caseId: string,
553
+ ): { caseData: ResolvedCase; resolvedCase: ExtensionCase } {
554
+ const resolvedCase = cases.find((item) => item.id === caseId);
555
+ if (!resolvedCase)
556
+ throw new InvocationError("extension did not resolve the selected case");
557
+ const caseData = parseCase({
558
+ ...resolvedCase,
559
+ fixture:
560
+ resolvedCase.fixture.kind === "inline"
561
+ ? { files: resolvedCase.fixture.files }
562
+ : resolvedCase.fixture.kind === "repository"
563
+ ? {
564
+ sourceRef: resolvedCase.fixture.sourceRef,
565
+ ...(resolvedCase.fixture.files
566
+ ? { files: resolvedCase.fixture.files }
567
+ : {}),
568
+ ...(resolvedCase.fixture.staged
569
+ ? { staged: resolvedCase.fixture.staged }
570
+ : {}),
571
+ ...(resolvedCase.fixture.commitFiles
572
+ ? { commitFiles: true }
573
+ : {}),
574
+ ...(resolvedCase.fixture.hooks
575
+ ? { hooks: resolvedCase.fixture.hooks }
576
+ : {}),
577
+ ...(resolvedCase.fixture.bin
578
+ ? { bin: resolvedCase.fixture.bin }
579
+ : {}),
580
+ }
581
+ : resolvedCase.fixture,
582
+ });
583
+ return { caseData, resolvedCase };
584
+ }
585
+
586
+ async function loadHost(path: string): Promise<HostAdapter> {
587
+ let module: unknown;
588
+ try {
589
+ module = await import(pathToFileURL(path).href);
590
+ } catch {
591
+ throw new InvocationError("host adapter module could not be loaded");
592
+ }
593
+ const host = record(module) ? module.default : null;
594
+ if (
595
+ !record(host) ||
596
+ typeof host.id !== "string" ||
597
+ typeof host.model !== "string" ||
598
+ typeof host.effort !== "string" ||
599
+ typeof host.run !== "function"
600
+ )
601
+ throw new InvocationError(
602
+ "host adapter module has no valid default adapter",
603
+ );
604
+ try {
605
+ prepareInstrumentation(
606
+ [],
607
+ (host as unknown as HostAdapter).instrumentation ?? [],
608
+ [],
609
+ "passive",
610
+ );
611
+ } catch {
612
+ throw new InvocationError("host adapter instrumentation is invalid");
613
+ }
614
+ if (
615
+ (host as unknown as HostAdapter).hostCapabilities !== undefined &&
616
+ (!Array.isArray((host as unknown as HostAdapter).hostCapabilities) ||
617
+ (host as unknown as HostAdapter).hostCapabilities!.some(
618
+ (id) =>
619
+ typeof id !== "string" ||
620
+ !/^[a-z][a-z0-9]*(?:[.-][a-z0-9]+)+$/.test(id),
621
+ ) ||
622
+ new Set((host as unknown as HostAdapter).hostCapabilities).size !==
623
+ (host as unknown as HostAdapter).hostCapabilities!.length)
624
+ )
625
+ throw new InvocationError("host adapter capabilities are invalid");
626
+ return host as unknown as HostAdapter;
627
+ }
628
+
629
+ function failure(code: 64 | 70, message: string) {
630
+ const result = {
631
+ format: "sevro.cli-result.v1",
632
+ runId: null,
633
+ execution: { status: "not_run" },
634
+ grading: { status: "not_requested" },
635
+ task: { verdict: "not_assessed" },
636
+ exitCode: code,
637
+ evidencePath: null,
638
+ cases: [],
639
+ diagnostic: {
640
+ code: code === 64 ? "sevro.invocation.invalid" : "sevro.runner.error",
641
+ message: message.slice(0, 4096),
642
+ },
643
+ };
644
+ assertCliResult(result);
645
+ return result;
646
+ }
647
+
648
+ function display(value: unknown, json: boolean): void {
649
+ if (json) {
650
+ process.stdout.write(`${JSON.stringify(value)}\n`);
651
+ return;
652
+ }
653
+ const result = value as {
654
+ execution: { status: string };
655
+ grading: { status: string };
656
+ task: { verdict: string };
657
+ evidencePath: string | null;
658
+ cases: Array<{
659
+ caseId: string;
660
+ trials: Array<{
661
+ trial: number;
662
+ domainOutcomes: Array<{ id: string; status: string }>;
663
+ }>;
664
+ }>;
665
+ };
666
+ process.stdout.write(
667
+ `execution=${result.execution.status} grading=${result.grading.status} task=${result.task.verdict}\n`,
668
+ );
669
+ for (const selected of result.cases)
670
+ for (const trial of selected.trials)
671
+ for (const outcome of trial.domainOutcomes)
672
+ process.stdout.write(
673
+ `domain case=${selected.caseId} trial=${trial.trial} outcome=${outcome.id} status=${outcome.status}\n`,
674
+ );
675
+ if (result.evidencePath)
676
+ process.stdout.write(`evidence=${result.evidencePath}\n`);
677
+ }
678
+
679
+ async function main(argv: string[]): Promise<void> {
680
+ const json = argv.includes("--json");
681
+ let invocation: ReturnType<typeof parseInvocation>;
682
+ let caseData: ResolvedCase | undefined;
683
+ let host: HostAdapter;
684
+ let semanticHost: HostAdapter | undefined;
685
+ let advisoryHost: HostAdapter | undefined;
686
+ let preparationSources: Awaited<ReturnType<typeof loadPreparationSources>>;
687
+ try {
688
+ invocation = parseInvocation(argv);
689
+ preparationSources = await loadPreparationSources(invocation);
690
+ const sourceRoots = Object.values(preparationSources?.refs ?? {}).map(
691
+ (url) => fileURLToPath(url),
692
+ );
693
+ for (const native of [
694
+ invocation.codex,
695
+ invocation.claude,
696
+ invocation.semanticCodex,
697
+ invocation.advisoryCodex,
698
+ ]) {
699
+ if (native)
700
+ native.additionalProtectedRoots = [
701
+ ...native.additionalProtectedRoots,
702
+ ...sourceRoots,
703
+ ];
704
+ }
705
+ invocation.shellIsolation?.protectedRoots.push(...sourceRoots);
706
+ const configRoot = await configurationRoot(invocation.configRoot);
707
+ const agentConcurrencyLimit =
708
+ invocation.codex || invocation.semanticCodex || invocation.advisoryCodex
709
+ ? await codexAgentConcurrency(configRoot)
710
+ : null;
711
+ if (invocation.caseFile) caseData = await loadCase(invocation.caseFile);
712
+ host = invocation.codex
713
+ ? createCodexHost({ ...invocation.codex, agentConcurrencyLimit })
714
+ : invocation.claude
715
+ ? createClaudeHost(invocation.claude)
716
+ : await loadHost(invocation.adapterModule!);
717
+ if (invocation.semanticAdapterModule)
718
+ semanticHost = await loadHost(invocation.semanticAdapterModule);
719
+ else if (invocation.semanticCodex)
720
+ semanticHost = createCodexHost({
721
+ ...invocation.semanticCodex,
722
+ agentConcurrencyLimit,
723
+ });
724
+ if (invocation.advisoryAdapterModule)
725
+ advisoryHost = await loadHost(invocation.advisoryAdapterModule);
726
+ else if (invocation.advisoryCodex)
727
+ advisoryHost = createCodexHost({
728
+ ...invocation.advisoryCodex,
729
+ agentConcurrencyLimit,
730
+ });
731
+ } catch (error) {
732
+ const result = failure(
733
+ 64,
734
+ error instanceof Error ? error.message : "invalid invocation",
735
+ );
736
+ display(result, json);
737
+ process.exitCode = 64;
738
+ return;
739
+ }
740
+ const cancellation = new AbortController();
741
+ const interrupt = () => cancellation.abort("SIGINT");
742
+ const terminate = () => cancellation.abort("SIGTERM");
743
+ process.on("SIGINT", interrupt);
744
+ process.on("SIGTERM", terminate);
745
+ try {
746
+ let extension:
747
+ | {
748
+ session: Awaited<ReturnType<typeof openExtensionSession>>;
749
+ resolvedCase: ExtensionCase;
750
+ }
751
+ | undefined;
752
+ if (invocation.extension) {
753
+ const selected = invocation.extension;
754
+ const options = await loadExtensionOptions(selected);
755
+ const hostCapabilities = [
756
+ ...(host.instrumentation ?? []).map((item) => item.id),
757
+ ...(host.hostCapabilities ?? []),
758
+ ];
759
+ const session = await openExtensionSession({
760
+ ...options,
761
+ sourceFiles: selected.sourceFiles,
762
+ engineCapabilities: [
763
+ "sevro.host.exec",
764
+ "sevro.fixture.setup",
765
+ "sevro.case.host-route",
766
+ ],
767
+ hostCapabilities,
768
+ taskVerdictPolicy: selected.taskVerdictPolicy,
769
+ replaceBuiltinGraders: selected.replaceBuiltinGraders,
770
+ signal: cancellation.signal,
771
+ });
772
+ const chosen = selectExtensionCase(
773
+ await session.resolve(
774
+ pathToFileURL(invocation.projectRoot).href,
775
+ { caseIds: [selected.caseId] },
776
+ {
777
+ id: host.id,
778
+ model: host.model,
779
+ effort: host.effort,
780
+ capabilities: hostCapabilities,
781
+ },
782
+ ),
783
+ selected.caseId,
784
+ );
785
+ caseData = chosen.caseData;
786
+ extension = { session, resolvedCase: chosen.resolvedCase };
787
+ }
788
+ const { result } = await runEvaluation({
789
+ projectRoot: invocation.projectRoot,
790
+ resultsRoot: invocation.resultsRoot,
791
+ runStateRoot: invocation.runStateRoot,
792
+ case: caseData!,
793
+ extension,
794
+ preparationSources,
795
+ host,
796
+ semanticHost,
797
+ advisoryHost,
798
+ advisoryExcludedPaths: invocation.advisoryExcludedPaths,
799
+ shellIsolation: invocation.shellIsolation,
800
+ runnerBuildDigest:
801
+ invocation.runnerBuildDigest ?? (await packageBuildDigest()),
802
+ runnerCheckoutRoot: invocation.runnerCheckoutRoot,
803
+ projectDigest:
804
+ invocation.projectDigest ??
805
+ (await projectIdentityDigest(
806
+ invocation.projectRoot,
807
+ await projectProvenance(invocation.projectRoot),
808
+ [invocation.resultsRoot, invocation.runStateRoot],
809
+ )),
810
+ condition: invocation.condition,
811
+ trialCount: invocation.trialCount,
812
+ jobs: invocation.jobs,
813
+ passThreshold: invocation.passThreshold,
814
+ dry: invocation.dry,
815
+ signal: cancellation.signal,
816
+ });
817
+ display(result, invocation.json);
818
+ process.exitCode = result.exitCode;
819
+ } catch (error) {
820
+ const code =
821
+ error instanceof EvaluationConfigurationError ||
822
+ error instanceof InvocationError
823
+ ? 64
824
+ : 70;
825
+ const message = error instanceof Error ? error.message : "runner failure";
826
+ const result = failure(code, message);
827
+ display(result, invocation.json);
828
+ process.exitCode = code;
829
+ } finally {
830
+ process.off("SIGINT", interrupt);
831
+ process.off("SIGTERM", terminate);
832
+ }
833
+ }
834
+
835
+ if (process.argv[2] === "report")
836
+ process.exitCode = await reportCommand(process.argv.slice(2));
837
+ else await main(process.argv.slice(2));