@dungle-scrubs/skillval 0.1.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -4,8 +4,8 @@
4
4
  import { Command } from "commander";
5
5
 
6
6
  // src/config.ts
7
- import { existsSync, readFileSync as readFileSync3 } from "fs";
8
- import { homedir as homedir2 } from "os";
7
+ import { existsSync as existsSync2, readFileSync as readFileSync5 } from "fs";
8
+ import { homedir as homedir4 } from "os";
9
9
  import { resolve } from "path";
10
10
  import { Check as checkSchema, Errors as schemaErrors } from "typebox/value";
11
11
  import { parse as parseYaml } from "yaml";
@@ -13,9 +13,9 @@ import { parse as parseYaml } from "yaml";
13
13
  // src/config-contract.ts
14
14
  import Type from "typebox";
15
15
 
16
- // src/executors/codex.ts
16
+ // src/executors/claude.ts
17
17
  import { spawnSync } from "child_process";
18
- import { mkdirSync, readFileSync as readFileSync2, symlinkSync } from "fs";
18
+ import { copyFileSync, existsSync, mkdirSync, readFileSync as readFileSync2, symlinkSync } from "fs";
19
19
  import { homedir } from "os";
20
20
  import { join as join2 } from "path";
21
21
 
@@ -51,30 +51,205 @@ function isRecord(value) {
51
51
  return typeof value === "object" && value !== null && !Array.isArray(value);
52
52
  }
53
53
 
54
- // src/executors/codex.ts
54
+ // src/executors/types.ts
55
+ function assertEffortSupported(executor, effort, allowed) {
56
+ if (effort !== void 0 && !allowed.includes(effort)) {
57
+ throw new Error(
58
+ `${executor} does not support effort "${effort}"; valid levels: ${allowed.join(", ")}`
59
+ );
60
+ }
61
+ }
62
+
63
+ // src/executors/claude.ts
55
64
  var TRIAL_TIMEOUT_MS = 15 * 60 * 1e3;
65
+ var CLAUDE_EFFORT_LEVELS = ["low", "medium", "high", "xhigh", "max"];
66
+ var CLAUDE_INVOCATION_DETECTION = "structured";
67
+ var ClaudeExecutor = class {
68
+ metadata;
69
+ #overrides;
70
+ #realConfigDirectory;
71
+ constructor(overrides = {}, realConfigDirectory = defaultConfigDirectory()) {
72
+ assertEffortSupported("claude", overrides.effort, CLAUDE_EFFORT_LEVELS);
73
+ this.#overrides = overrides;
74
+ this.#realConfigDirectory = realConfigDirectory;
75
+ const detected = detectClaude(realConfigDirectory);
76
+ this.metadata = {
77
+ ...detected,
78
+ model: overrides.model ?? detected.model,
79
+ thinking: overrides.effort ?? detected.thinking
80
+ };
81
+ }
82
+ runTrial(request) {
83
+ if (request.arm === "skill") seedSkill(request);
84
+ const selection = [];
85
+ if (this.#overrides.model !== void 0) selection.push("--model", this.#overrides.model);
86
+ if (this.#overrides.effort !== void 0) selection.push("--effort", this.#overrides.effort);
87
+ const permissions = request.evalCase.mode === "generation" ? ["--permission-mode", "acceptEdits"] : ["--permission-mode", "dontAsk", "--allowedTools", "Read,Glob,Grep,Skill"];
88
+ const environment = request.arm === "baseline" ? {
89
+ ...process.env,
90
+ CLAUDE_CONFIG_DIR: prepareBaselineConfig(request.home, this.#realConfigDirectory)
91
+ } : { ...process.env };
92
+ const result = spawnSync(
93
+ "claude",
94
+ [
95
+ "-p",
96
+ request.evalCase.prompt,
97
+ "--output-format",
98
+ "stream-json",
99
+ "--verbose",
100
+ "--no-session-persistence",
101
+ ...selection,
102
+ ...permissions
103
+ ],
104
+ {
105
+ cwd: request.workspace,
106
+ encoding: "utf8",
107
+ env: environment,
108
+ maxBuffer: 64 * 1024 * 1024,
109
+ timeout: TRIAL_TIMEOUT_MS
110
+ }
111
+ );
112
+ if (result.status !== 0) {
113
+ throw new Error(`claude -p exited ${result.status}: ${result.stderr?.slice(-500)}`);
114
+ }
115
+ return parseClaudeTrace(result.stdout, request.skillName);
116
+ }
117
+ };
118
+ function defaultConfigDirectory() {
119
+ return process.env.CLAUDE_CONFIG_DIR ?? join2(homedir(), ".claude");
120
+ }
121
+ function seedSkill(request) {
122
+ const skillsRoot = join2(request.workspace, ".claude/skills");
123
+ mkdirSync(skillsRoot, { recursive: true });
124
+ symlinkSync(request.skillDirectory, join2(skillsRoot, request.skillName));
125
+ }
126
+ function prepareBaselineConfig(home, realConfigDirectory) {
127
+ const configDirectory = join2(home, "claude-config");
128
+ mkdirSync(configDirectory, { recursive: true });
129
+ const credentials = join2(realConfigDirectory, ".credentials.json");
130
+ if (existsSync(credentials)) {
131
+ copyFileSync(credentials, join2(configDirectory, ".credentials.json"));
132
+ }
133
+ return configDirectory;
134
+ }
135
+ function detectClaude(realConfigDirectory = defaultConfigDirectory()) {
136
+ const version = spawnSync("claude", ["--version"], { encoding: "utf8" }).stdout?.trim() ?? "";
137
+ if (version === "") throw new Error("claude CLI not found on PATH");
138
+ let model = "default";
139
+ let thinking = "default";
140
+ try {
141
+ const settings = JSON.parse(
142
+ readFileSync2(join2(realConfigDirectory, "settings.json"), "utf8")
143
+ );
144
+ if (isRecord(settings)) {
145
+ if (typeof settings.model === "string") model = settings.model;
146
+ if (typeof settings.effort === "string") thinking = settings.effort;
147
+ }
148
+ } catch {
149
+ }
150
+ return {
151
+ invocationDetection: CLAUDE_INVOCATION_DETECTION,
152
+ model,
153
+ name: "claude",
154
+ thinking,
155
+ version
156
+ };
157
+ }
158
+ function parseClaudeTrace(stdout, skillName) {
159
+ let completed = false;
160
+ let invoked = false;
161
+ let invocationEvidence = null;
162
+ let resultText = "";
163
+ const texts = [];
164
+ let usage;
165
+ for (const line of stdout.split("\n")) {
166
+ if (line.trim() === "") continue;
167
+ let event;
168
+ try {
169
+ event = JSON.parse(line);
170
+ } catch {
171
+ continue;
172
+ }
173
+ if (!isRecord(event)) continue;
174
+ if (event.type === "assistant" && isRecord(event.message) && Array.isArray(event.message.content)) {
175
+ for (const block of event.message.content) {
176
+ if (!isRecord(block)) continue;
177
+ if (block.type === "text" && typeof block.text === "string") texts.push(block.text);
178
+ if (block.type === "tool_use" && block.name === "Skill" && JSON.stringify(block.input ?? "").includes(skillName)) {
179
+ invoked = true;
180
+ invocationEvidence ??= `Skill tool_use: ${JSON.stringify(block.input ?? null)}`;
181
+ }
182
+ }
183
+ }
184
+ if (event.type === "result") {
185
+ completed = event.is_error === false;
186
+ usage = event.usage;
187
+ if (typeof event.result === "string") resultText = event.result;
188
+ }
189
+ }
190
+ return {
191
+ agentText: texts.length > 0 ? texts.join("\n") : resultText,
192
+ completed,
193
+ invocationEvidence,
194
+ invoked,
195
+ usage
196
+ };
197
+ }
198
+
199
+ // src/executors/codex.ts
200
+ import { spawnSync as spawnSync2 } from "child_process";
201
+ import { mkdirSync as mkdirSync2, readFileSync as readFileSync3, symlinkSync as symlinkSync2 } from "fs";
202
+ import { homedir as homedir2 } from "os";
203
+ import { join as join3 } from "path";
204
+ var TRIAL_TIMEOUT_MS2 = 15 * 60 * 1e3;
205
+ var CODEX_EFFORT_LEVELS = [
206
+ "none",
207
+ "minimal",
208
+ "low",
209
+ "medium",
210
+ "high",
211
+ "xhigh",
212
+ "max"
213
+ ];
214
+ var CODEX_INVOCATION_DETECTION = "heuristic";
56
215
  var CodexExecutor = class {
57
216
  metadata;
217
+ #overrides;
58
218
  #realHome;
59
- constructor(realHome = homedir()) {
219
+ constructor(overrides = {}, realHome = homedir2()) {
220
+ assertEffortSupported("codex", overrides.effort, CODEX_EFFORT_LEVELS);
221
+ this.#overrides = overrides;
60
222
  this.#realHome = realHome;
61
- this.metadata = detectCodex(realHome);
223
+ const detected = detectCodex(realHome);
224
+ this.metadata = {
225
+ ...detected,
226
+ model: overrides.model ?? detected.model,
227
+ thinking: overrides.effort ?? detected.thinking
228
+ };
62
229
  }
63
230
  runTrial(request) {
64
- if (request.arm === "skill") seedSkill(request);
231
+ if (request.arm === "skill") seedSkill2(request);
65
232
  const sandbox = request.evalCase.mode === "generation" ? "workspace-write" : "read-only";
233
+ const selection = [];
234
+ if (this.#overrides.model !== void 0) {
235
+ selection.push("-c", `model=${JSON.stringify(this.#overrides.model)}`);
236
+ }
237
+ if (this.#overrides.effort !== void 0) {
238
+ selection.push("-c", `model_reasoning_effort=${JSON.stringify(this.#overrides.effort)}`);
239
+ }
66
240
  const environment = request.arm === "baseline" ? {
67
241
  ...process.env,
68
- CODEX_HOME: join2(this.#realHome, ".codex"),
242
+ CODEX_HOME: join3(this.#realHome, ".codex"),
69
243
  HOME: request.home
70
244
  } : { ...process.env };
71
- const result = spawnSync(
245
+ const result = spawnSync2(
72
246
  "codex",
73
247
  [
74
248
  "exec",
75
249
  "--json",
76
250
  "--skip-git-repo-check",
77
251
  "--ephemeral",
252
+ ...selection,
78
253
  "-s",
79
254
  sandbox,
80
255
  "-C",
@@ -85,7 +260,7 @@ var CodexExecutor = class {
85
260
  encoding: "utf8",
86
261
  env: environment,
87
262
  maxBuffer: 64 * 1024 * 1024,
88
- timeout: TRIAL_TIMEOUT_MS
263
+ timeout: TRIAL_TIMEOUT_MS2
89
264
  }
90
265
  );
91
266
  if (result.status !== 0) {
@@ -94,22 +269,29 @@ var CodexExecutor = class {
94
269
  return parseCodexTrace(result.stdout, request.skillName);
95
270
  }
96
271
  };
97
- function seedSkill(request) {
98
- const skillsRoot = join2(request.workspace, ".agents/skills");
99
- mkdirSync(skillsRoot, { recursive: true });
100
- symlinkSync(request.skillDirectory, join2(skillsRoot, request.skillName));
272
+ function seedSkill2(request) {
273
+ const skillsRoot = join3(request.workspace, ".agents/skills");
274
+ mkdirSync2(skillsRoot, { recursive: true });
275
+ symlinkSync2(request.skillDirectory, join3(skillsRoot, request.skillName));
101
276
  }
102
- function detectCodex(realHome = homedir()) {
103
- const version = spawnSync("codex", ["--version"], { encoding: "utf8" }).stdout?.trim() ?? "";
277
+ function detectCodex(realHome = homedir2()) {
278
+ const version = spawnSync2("codex", ["--version"], { encoding: "utf8" }).stdout?.trim() ?? "";
104
279
  if (version === "") throw new Error("codex CLI not found on PATH");
105
- const model = readFileSync2(join2(realHome, ".codex/config.toml"), "utf8").match(
106
- /^model\s*=\s*"([^"]+)"/m
107
- )?.[1] ?? "default";
108
- return { model, name: "codex", version };
280
+ const configuration = readFileSync3(join3(realHome, ".codex/config.toml"), "utf8");
281
+ const model = configuration.match(/^model\s*=\s*"([^"]+)"/m)?.[1] ?? "default";
282
+ const thinking = configuration.match(/^model_reasoning_effort\s*=\s*"([^"]+)"/m)?.[1] ?? "default";
283
+ return {
284
+ invocationDetection: CODEX_INVOCATION_DETECTION,
285
+ model,
286
+ name: "codex",
287
+ thinking,
288
+ version
289
+ };
109
290
  }
110
291
  function parseCodexTrace(stdout, skillName) {
111
292
  let completed = false;
112
293
  let invoked = false;
294
+ let invocationEvidence = null;
113
295
  const texts = [];
114
296
  let usage;
115
297
  for (const line of stdout.split("\n")) {
@@ -124,6 +306,7 @@ function parseCodexTrace(stdout, skillName) {
124
306
  const item = isRecord(event.item) ? event.item : void 0;
125
307
  if (item?.type === "command_execution" && typeof item.command === "string" && item.command.includes(`${skillName}/SKILL.md`)) {
126
308
  invoked = true;
309
+ invocationEvidence ??= `command_execution: ${item.command}`;
127
310
  }
128
311
  if (event.type === "item.completed" && item?.type === "agent_message" && typeof item.text === "string") {
129
312
  texts.push(item.text);
@@ -133,16 +316,146 @@ function parseCodexTrace(stdout, skillName) {
133
316
  usage = event.usage;
134
317
  }
135
318
  }
136
- return { agentText: texts.join("\n"), completed, invoked, usage };
319
+ return { agentText: texts.join("\n"), completed, invocationEvidence, invoked, usage };
320
+ }
321
+
322
+ // src/executors/pi.ts
323
+ import { spawnSync as spawnSync3 } from "child_process";
324
+ import { readFileSync as readFileSync4 } from "fs";
325
+ import { homedir as homedir3 } from "os";
326
+ import { join as join4 } from "path";
327
+ var TRIAL_TIMEOUT_MS3 = 15 * 60 * 1e3;
328
+ var PI_EFFORT_LEVELS = [
329
+ "off",
330
+ "minimal",
331
+ "low",
332
+ "medium",
333
+ "high",
334
+ "xhigh"
335
+ ];
336
+ var PI_INVOCATION_DETECTION = "heuristic";
337
+ var PiExecutor = class {
338
+ metadata;
339
+ #overrides;
340
+ constructor(overrides = {}, settingsDirectory = join4(homedir3(), ".pi")) {
341
+ assertEffortSupported("pi", overrides.effort, PI_EFFORT_LEVELS);
342
+ this.#overrides = overrides;
343
+ const detected = detectPi(settingsDirectory);
344
+ this.metadata = {
345
+ ...detected,
346
+ model: overrides.model ?? detected.model,
347
+ thinking: overrides.effort ?? detected.thinking
348
+ };
349
+ }
350
+ runTrial(request) {
351
+ const arm = request.arm === "skill" ? ["--skill", request.skillDirectory] : ["--no-skills"];
352
+ const selection = [];
353
+ if (this.#overrides.model !== void 0) selection.push("--model", this.#overrides.model);
354
+ if (this.#overrides.effort !== void 0) selection.push("--thinking", this.#overrides.effort);
355
+ const tools = request.evalCase.mode === "generation" ? [] : ["-t", "read"];
356
+ const result = spawnSync3(
357
+ "pi",
358
+ [
359
+ "-p",
360
+ "--mode",
361
+ "json",
362
+ "--no-session",
363
+ ...selection,
364
+ ...arm,
365
+ ...tools,
366
+ request.evalCase.prompt
367
+ ],
368
+ {
369
+ cwd: request.workspace,
370
+ encoding: "utf8",
371
+ env: { ...process.env },
372
+ maxBuffer: 64 * 1024 * 1024,
373
+ timeout: TRIAL_TIMEOUT_MS3
374
+ }
375
+ );
376
+ if (result.status !== 0) {
377
+ throw new Error(`pi -p exited ${result.status}: ${result.stderr?.slice(-500)}`);
378
+ }
379
+ const trace = parsePiTrace(result.stdout, request.skillName);
380
+ if (!trace.completed && /no api key/i.test(result.stdout)) {
381
+ throw new Error(
382
+ "pi found no API key for its configured provider; export the provider key (e.g. ZAI_API_KEY) in the environment running skillval"
383
+ );
384
+ }
385
+ return trace;
386
+ }
387
+ };
388
+ function detectPi(settingsDirectory = join4(homedir3(), ".pi")) {
389
+ const version = spawnSync3("pi", ["--version"], { encoding: "utf8" }).stdout?.trim() ?? "";
390
+ if (version === "") throw new Error("pi CLI not found on PATH");
391
+ let model = "default";
392
+ let thinking = "default";
393
+ try {
394
+ const settings = JSON.parse(
395
+ readFileSync4(join4(settingsDirectory, "settings.json"), "utf8")
396
+ );
397
+ if (isRecord(settings)) {
398
+ if (typeof settings.defaultModel === "string") {
399
+ model = typeof settings.defaultProvider === "string" ? `${settings.defaultProvider}/${settings.defaultModel}` : settings.defaultModel;
400
+ }
401
+ if (typeof settings.defaultThinkingLevel === "string") {
402
+ thinking = settings.defaultThinkingLevel;
403
+ }
404
+ }
405
+ } catch {
406
+ }
407
+ return {
408
+ invocationDetection: PI_INVOCATION_DETECTION,
409
+ model,
410
+ name: "pi",
411
+ thinking,
412
+ version
413
+ };
414
+ }
415
+ function parsePiTrace(stdout, skillName) {
416
+ let completed = false;
417
+ let invoked = false;
418
+ let invocationEvidence = null;
419
+ const texts = [];
420
+ let usage;
421
+ for (const line of stdout.split("\n")) {
422
+ if (line.trim() === "") continue;
423
+ let event;
424
+ try {
425
+ event = JSON.parse(line);
426
+ } catch {
427
+ continue;
428
+ }
429
+ if (!isRecord(event)) continue;
430
+ if (event.type !== "agent_end" || !Array.isArray(event.messages)) continue;
431
+ completed = true;
432
+ for (const message of event.messages) {
433
+ if (!isRecord(message) || message.role !== "assistant") continue;
434
+ if (!Array.isArray(message.content)) continue;
435
+ for (const block of message.content) {
436
+ if (!isRecord(block)) continue;
437
+ if (block.type === "text" && typeof block.text === "string") texts.push(block.text);
438
+ if (block.type === "toolCall" && JSON.stringify(block.arguments ?? "").includes(`${skillName}/SKILL.md`)) {
439
+ invoked = true;
440
+ const name = typeof block.name === "string" ? block.name : "toolCall";
441
+ invocationEvidence ??= `${name} toolCall: ${JSON.stringify(block.arguments ?? null)}`;
442
+ }
443
+ }
444
+ usage = message.usage ?? usage;
445
+ }
446
+ }
447
+ return { agentText: texts.join("\n"), completed, invocationEvidence, invoked, usage };
137
448
  }
138
449
 
139
450
  // src/executors/index.ts
140
451
  var executorFactories = {
141
- codex: () => new CodexExecutor()
452
+ claude: (overrides) => new ClaudeExecutor(overrides),
453
+ codex: (overrides) => new CodexExecutor(overrides),
454
+ pi: (overrides) => new PiExecutor(overrides)
142
455
  };
143
456
  var EXECUTOR_NAMES = Object.keys(executorFactories);
144
- function createExecutor(name) {
145
- return executorFactories[name]();
457
+ function createExecutor(name, overrides = {}) {
458
+ return executorFactories[name](overrides);
146
459
  }
147
460
 
148
461
  // src/config-contract.ts
@@ -177,20 +490,20 @@ var ConfigError = class extends Error {
177
490
  };
178
491
  function resolveConfigPath(options = {}) {
179
492
  const environment = options.environment ?? process.env;
180
- const home = options.home ?? homedir2();
493
+ const home = options.home ?? homedir4();
181
494
  const selected = options.cliPath ?? environment.SKILLVAL_CONFIG ?? (environment.XDG_CONFIG_HOME ? resolve(environment.XDG_CONFIG_HOME, "skillval/config.yml") : resolve(home, ".config/skillval/config.yml"));
182
495
  return resolve(expandHome(selected, home));
183
496
  }
184
- function expandRoot(root, home = homedir2()) {
497
+ function expandRoot(root, home = homedir4()) {
185
498
  return resolve(expandHome(root, home));
186
499
  }
187
- function loadConfig(path, home = homedir2()) {
188
- if (!existsSync(path)) {
500
+ function loadConfig(path, home = homedir4()) {
501
+ if (!existsSync2(path)) {
189
502
  throw new ConfigError(`config file not found: ${path}`, "CONFIG_NOT_FOUND");
190
503
  }
191
504
  let parsed;
192
505
  try {
193
- parsed = parseYaml(readFileSync3(path, "utf8"));
506
+ parsed = parseYaml(readFileSync5(path, "utf8"));
194
507
  } catch (error) {
195
508
  const detail = error instanceof Error ? error.message : String(error);
196
509
  throw new ConfigError(`invalid YAML in ${path}: ${detail}`, "CONFIG_YAML_INVALID");
@@ -206,7 +519,7 @@ function loadConfig(path, home = homedir2()) {
206
519
  roots: parsed.roots.map((root) => expandRoot(root, home))
207
520
  };
208
521
  }
209
- function resolveStateDirectory(environment = process.env, home = homedir2()) {
522
+ function resolveStateDirectory(environment = process.env, home = homedir4()) {
210
523
  return environment.XDG_STATE_HOME ? resolve(environment.XDG_STATE_HOME, "skillval") : resolve(home, ".local/state/skillval");
211
524
  }
212
525
  function expandHome(value, home) {
@@ -215,23 +528,64 @@ function expandHome(value, home) {
215
528
  }
216
529
 
217
530
  // src/discovery.ts
218
- import { existsSync as existsSync3, readdirSync as readdirSync3 } from "fs";
219
- import { join as join6 } from "path";
531
+ import { existsSync as existsSync4, readdirSync as readdirSync3 } from "fs";
532
+ import { join as join8 } from "path";
220
533
 
221
534
  // src/case-file.ts
222
- import { readFileSync as readFileSync5, statSync as statSync2 } from "fs";
223
- import { dirname as dirname2, join as join5 } from "path";
535
+ import { readFileSync as readFileSync8, statSync as statSync2 } from "fs";
536
+ import { dirname as dirname2, join as join7 } from "path";
224
537
  import { parse as parseYaml2 } from "yaml";
225
538
 
226
539
  // src/case-contract.ts
227
- import Type2 from "typebox";
540
+ import { isAbsolute, normalize } from "path";
541
+ import Type3 from "typebox";
228
542
  import { Check as checkSchema2, Errors as schemaErrors2 } from "typebox/value";
229
543
 
230
544
  // src/graders.ts
231
- import { spawnSync as spawnSync2 } from "child_process";
232
- import { existsSync as existsSync2, writeFileSync } from "fs";
545
+ import { spawnSync as spawnSync4 } from "child_process";
546
+ import {
547
+ existsSync as existsSync3,
548
+ lstatSync,
549
+ readFileSync as readFileSync6,
550
+ realpathSync,
551
+ writeFileSync
552
+ } from "fs";
233
553
  import { createRequire } from "module";
234
- import { dirname, join as join3 } from "path";
554
+ import { dirname, join as join5, resolve as resolve2, sep } from "path";
555
+ import { Ajv2020 } from "ajv/dist/2020.js";
556
+ import Type2 from "typebox";
557
+ var jsonSchemaGraderSchema = Type2.ReadonlyObject(
558
+ Type2.Object({
559
+ file: Type2.String({
560
+ description: "Produced file, relative to the workspace, parsed as JSON and validated.",
561
+ minLength: 1,
562
+ pattern: String.raw`\S`
563
+ }),
564
+ schema: Type2.Union([Type2.Record(Type2.String(), Type2.Unknown()), Type2.Boolean()], {
565
+ description: "JSON Schema (draft 2020-12) the produced file must satisfy; an object or a boolean schema. Omit $schema, or set it to 2020-12; other declared dialects are rejected."
566
+ })
567
+ }),
568
+ { additionalProperties: false }
569
+ );
570
+ var JSON_SCHEMA_GRADER_MODES = ["generation"];
571
+ var commandExitGraderSchema = Type2.ReadonlyObject(
572
+ Type2.Object({
573
+ command: Type2.String({
574
+ description: "Shell command run in the workspace; the grader passes when it exits as expected. Trusted case input: this is arbitrary shell executed on the grading machine (see the README's Trust model section).",
575
+ minLength: 1,
576
+ pattern: String.raw`\S`
577
+ }),
578
+ expect: Type2.Optional(
579
+ Type2.Integer({
580
+ description: "Exit code the command must produce to pass. Defaults to 0.",
581
+ maximum: 255,
582
+ minimum: 0
583
+ })
584
+ )
585
+ }),
586
+ { additionalProperties: false }
587
+ );
588
+ var COMMAND_EXIT_GRADER_MODES = ["generation"];
235
589
  var packageRequire = createRequire(import.meta.url);
236
590
  var graders = {
237
591
  tsc: {
@@ -245,15 +599,135 @@ function graderSupportsMode(name, mode) {
245
599
  return modes.includes(mode);
246
600
  }
247
601
  function runGraders(evalCase, workspace) {
248
- return (evalCase.assert?.graders ?? []).map((name) => graders[name].run(workspace));
602
+ const checks = [];
603
+ if (evalCase.assert?.json_schema !== void 0) {
604
+ checks.push(gradeJsonSchema(workspace, evalCase.assert.json_schema));
605
+ }
606
+ if (evalCase.assert?.command_exit !== void 0) {
607
+ checks.push(gradeCommandExit(workspace, evalCase.assert.command_exit));
608
+ }
609
+ for (const name of evalCase.assert?.graders ?? []) {
610
+ checks.push(graders[name].run(workspace));
611
+ }
612
+ return checks;
613
+ }
614
+ var COMMAND_EXIT_TIMEOUT_MS = 12e4;
615
+ function gradeCommandExit(workspace, config) {
616
+ const expected = config.expect ?? 0;
617
+ const outcome = spawnSync4(config.command, {
618
+ cwd: workspace,
619
+ encoding: "utf8",
620
+ env: { HOME: workspace, PATH: process.env.PATH ?? "" },
621
+ killSignal: "SIGKILL",
622
+ maxBuffer: 64 * 1024 * 1024,
623
+ shell: true,
624
+ timeout: COMMAND_EXIT_TIMEOUT_MS
625
+ });
626
+ if (outcome.error !== void 0) {
627
+ const timedOut = outcome.error.code === "ETIMEDOUT";
628
+ const reason = timedOut ? `timed out after ${COMMAND_EXIT_TIMEOUT_MS / 1e3}s` : outcome.error.message;
629
+ return {
630
+ detail: `command "${config.command}" failed to run: ${reason}`,
631
+ name: "command_exit",
632
+ pass: false
633
+ };
634
+ }
635
+ if (outcome.signal !== null) {
636
+ return {
637
+ detail: `command "${config.command}" terminated by ${outcome.signal}`,
638
+ name: "command_exit",
639
+ pass: false
640
+ };
641
+ }
642
+ if (outcome.status === expected) {
643
+ return {
644
+ detail: `command "${config.command}" exited ${expected}`,
645
+ name: "command_exit",
646
+ pass: true
647
+ };
648
+ }
649
+ const stderr = outcome.stderr === "" ? "" : `: ${outcome.stderr.slice(0, 300)}`;
650
+ return {
651
+ detail: `command "${config.command}" exited ${outcome.status}, expected ${expected}${stderr}`,
652
+ name: "command_exit",
653
+ pass: false
654
+ };
655
+ }
656
+ function compileSchema(schema) {
657
+ try {
658
+ const ajv = new Ajv2020({ allErrors: false, strict: false });
659
+ return { ok: true, validate: ajv.compile(schema) };
660
+ } catch (error) {
661
+ return { message: error instanceof Error ? error.message : String(error), ok: false };
662
+ }
663
+ }
664
+ function jsonSchemaCompileError(schema) {
665
+ const result = compileSchema(schema);
666
+ return result.ok ? null : result.message;
667
+ }
668
+ function safeRealpath(target) {
669
+ try {
670
+ return realpathSync(target);
671
+ } catch {
672
+ return null;
673
+ }
674
+ }
675
+ function safeLstat(target) {
676
+ try {
677
+ return lstatSync(target);
678
+ } catch {
679
+ return null;
680
+ }
681
+ }
682
+ function gradeJsonSchema(workspace, config) {
683
+ const workspaceRoot = safeRealpath(resolve2(workspace));
684
+ if (workspaceRoot === null) {
685
+ return { detail: "workspace not found", name: "json_schema", pass: false };
686
+ }
687
+ const target = safeRealpath(resolve2(workspaceRoot, config.file));
688
+ if (target === null) {
689
+ return { detail: `file not found: ${config.file}`, name: "json_schema", pass: false };
690
+ }
691
+ if (target !== workspaceRoot && !target.startsWith(workspaceRoot + sep)) {
692
+ return { detail: `file escapes workspace: ${config.file}`, name: "json_schema", pass: false };
693
+ }
694
+ const stats = safeLstat(target);
695
+ if (stats === null || !stats.isFile()) {
696
+ return { detail: `not a regular file: ${config.file}`, name: "json_schema", pass: false };
697
+ }
698
+ let parsed;
699
+ try {
700
+ parsed = JSON.parse(readFileSync6(target, "utf8"));
701
+ } catch (error) {
702
+ const message = error instanceof Error ? error.message : String(error);
703
+ return {
704
+ detail: `invalid JSON in ${config.file}: ${message}`,
705
+ name: "json_schema",
706
+ pass: false
707
+ };
708
+ }
709
+ const compiled = compileSchema(config.schema);
710
+ if (!compiled.ok) {
711
+ return { detail: `invalid json_schema: ${compiled.message}`, name: "json_schema", pass: false };
712
+ }
713
+ if (compiled.validate(parsed)) {
714
+ return { detail: `${config.file} matches schema`, name: "json_schema", pass: true };
715
+ }
716
+ const first = compiled.validate.errors?.[0];
717
+ const location = first?.instancePath === void 0 || first.instancePath === "" ? "(root)" : first.instancePath;
718
+ return {
719
+ detail: `${config.file} ${location} ${first?.message ?? "does not match schema"}`,
720
+ name: "json_schema",
721
+ pass: false
722
+ };
249
723
  }
250
724
  function gradeTsc(workspace) {
251
- if (!existsSync2(join3(workspace, "package.json"))) {
252
- writeFileSync(join3(workspace, "package.json"), '{ "type": "module" }\n');
725
+ if (!existsSync3(join5(workspace, "package.json"))) {
726
+ writeFileSync(join5(workspace, "package.json"), '{ "type": "module" }\n');
253
727
  }
254
728
  const nodeTypesDirectory = dirname(packageRequire.resolve("@types/node/package.json"));
255
729
  writeFileSync(
256
- join3(workspace, "tsconfig.json"),
730
+ join5(workspace, "tsconfig.json"),
257
731
  JSON.stringify({
258
732
  compilerOptions: {
259
733
  lib: ["es2023"],
@@ -268,8 +742,12 @@ function gradeTsc(workspace) {
268
742
  }
269
743
  })
270
744
  );
271
- const typescriptBinary = packageRequire.resolve("typescript/bin/tsc");
272
- const result = spawnSync2(typescriptBinary, ["-p", workspace], {
745
+ const typescriptBinary = join5(
746
+ dirname(packageRequire.resolve("typescript/package.json")),
747
+ "bin",
748
+ "tsc"
749
+ );
750
+ const result = spawnSync4(typescriptBinary, ["-p", workspace], {
273
751
  encoding: "utf8",
274
752
  timeout: 12e4
275
753
  });
@@ -281,65 +759,67 @@ function gradeTsc(workspace) {
281
759
  }
282
760
 
283
761
  // src/case-contract.ts
284
- var classificationSchema = Type2.Enum(["capability", "preference"]);
285
- var nonEmptyStringSchema = Type2.String({ minLength: 1, pattern: String.raw`\S` });
286
- var stringArraySchema = Type2.Readonly(Type2.Array(Type2.String()));
287
- var armSchema = Type2.Enum(["baseline", "skill"]);
288
- var fixtureSchema = Type2.ReadonlyObject(
289
- Type2.Object({
290
- path: Type2.Optional(
291
- Type2.String({
762
+ var classificationSchema = Type3.Enum(["capability", "preference"]);
763
+ var nonEmptyStringSchema = Type3.String({ minLength: 1, pattern: String.raw`\S` });
764
+ var stringArraySchema = Type3.Readonly(Type3.Array(Type3.String()));
765
+ var armSchema = Type3.Enum(["baseline", "skill"]);
766
+ var fixtureSchema = Type3.ReadonlyObject(
767
+ Type3.Object({
768
+ path: Type3.Optional(
769
+ Type3.String({
292
770
  description: "Directory relative to skillval.yml, copied into the workspace.",
293
771
  minLength: 1,
294
772
  pattern: String.raw`\S`
295
773
  })
296
774
  ),
297
- setup: Type2.Optional(
298
- Type2.Readonly(
299
- Type2.Array(nonEmptyStringSchema, {
300
- description: "Shell commands run sequentially inside the workspace after the copy."
775
+ setup: Type3.Optional(
776
+ Type3.Readonly(
777
+ Type3.Array(nonEmptyStringSchema, {
778
+ description: "Shell commands run sequentially inside the workspace after the copy. Trusted case input: arbitrary shell executed on the grading machine (see the README's Trust model section)."
301
779
  })
302
780
  )
303
781
  )
304
782
  }),
305
783
  { additionalProperties: false, minProperties: 1 }
306
784
  );
307
- var caseAssertSchema = Type2.ReadonlyObject(
308
- Type2.Object({
309
- graders: Type2.Optional(
310
- Type2.Readonly(Type2.Array(Type2.Enum(GRADER_NAMES), { uniqueItems: true }))
785
+ var caseAssertSchema = Type3.ReadonlyObject(
786
+ Type3.Object({
787
+ command_exit: Type3.Optional(commandExitGraderSchema),
788
+ graders: Type3.Optional(
789
+ Type3.Readonly(Type3.Array(Type3.Enum(GRADER_NAMES), { uniqueItems: true }))
311
790
  ),
312
- must_match: Type2.Optional(stringArraySchema),
313
- must_not_match: Type2.Optional(stringArraySchema)
791
+ json_schema: Type3.Optional(jsonSchemaGraderSchema),
792
+ must_match: Type3.Optional(stringArraySchema),
793
+ must_not_match: Type3.Optional(stringArraySchema)
314
794
  }),
315
795
  { additionalProperties: false }
316
796
  );
317
- var evalCaseSchema = Type2.ReadonlyObject(
318
- Type2.Object({
319
- arms: Type2.Optional(
320
- Type2.Readonly(
321
- Type2.Array(armSchema, {
797
+ var evalCaseSchema = Type3.ReadonlyObject(
798
+ Type3.Object({
799
+ arms: Type3.Optional(
800
+ Type3.Readonly(
801
+ Type3.Array(armSchema, {
322
802
  uniqueItems: true
323
803
  })
324
804
  )
325
805
  ),
326
- assert: Type2.Optional(caseAssertSchema),
327
- fixture: Type2.Optional(fixtureSchema),
806
+ assert: Type3.Optional(caseAssertSchema),
807
+ fixture: Type3.Optional(fixtureSchema),
328
808
  id: nonEmptyStringSchema,
329
- mode: Type2.Enum(["generation", "trigger"]),
809
+ mode: Type3.Enum(["generation", "trigger"]),
330
810
  prompt: nonEmptyStringSchema,
331
- rule: Type2.Optional(Type2.String()),
332
- should_trigger: Type2.Optional(Type2.Boolean()),
333
- trials: Type2.Optional(Type2.Integer({ maximum: 5, minimum: 1 })),
334
- type: Type2.Optional(classificationSchema)
811
+ rule: Type3.Optional(Type3.String()),
812
+ should_trigger: Type3.Optional(Type3.Boolean()),
813
+ trials: Type3.Optional(Type3.Integer({ maximum: 5, minimum: 1 })),
814
+ type: Type3.Optional(classificationSchema)
335
815
  }),
336
816
  { additionalProperties: false }
337
817
  );
338
- var skillEvalsSchema = Type2.ReadonlyObject(
339
- Type2.Object({
340
- cases: Type2.Readonly(Type2.Array(evalCaseSchema)),
818
+ var skillEvalsSchema = Type3.ReadonlyObject(
819
+ Type3.Object({
820
+ cases: Type3.Readonly(Type3.Array(evalCaseSchema)),
341
821
  class: classificationSchema,
342
- fixture: Type2.Optional(fixtureSchema),
822
+ fixture: Type3.Optional(fixtureSchema),
343
823
  skill: nonEmptyStringSchema
344
824
  }),
345
825
  {
@@ -384,9 +864,40 @@ function parseCaseValue(value, path, expectedSkill) {
384
864
  ids.add(evalCase.id);
385
865
  validatePatterns(evalCase, path);
386
866
  validateGraders(evalCase, path);
867
+ validateJsonSchemaGrader(evalCase, path);
868
+ validateCommandExitGrader(evalCase, path);
387
869
  }
388
870
  return value;
389
871
  }
872
+ function validateCommandExitGrader(evalCase, path) {
873
+ if (evalCase.assert?.command_exit === void 0) return;
874
+ if (!COMMAND_EXIT_GRADER_MODES.includes(evalCase.mode)) {
875
+ throw new CaseContractError(
876
+ `${path} case "${evalCase.id}" grader "command_exit" does not support ${evalCase.mode} mode`
877
+ );
878
+ }
879
+ }
880
+ function validateJsonSchemaGrader(evalCase, path) {
881
+ const config = evalCase.assert?.json_schema;
882
+ if (config === void 0) return;
883
+ if (!JSON_SCHEMA_GRADER_MODES.includes(evalCase.mode)) {
884
+ throw new CaseContractError(
885
+ `${path} case "${evalCase.id}" grader "json_schema" does not support ${evalCase.mode} mode`
886
+ );
887
+ }
888
+ const segments = normalize(config.file).split(/[/\\]/);
889
+ if (isAbsolute(config.file) || segments.includes("..")) {
890
+ throw new CaseContractError(
891
+ `${path} case "${evalCase.id}" json_schema file "${config.file}" must be a path inside the workspace`
892
+ );
893
+ }
894
+ const schemaError = jsonSchemaCompileError(config.schema);
895
+ if (schemaError !== null) {
896
+ throw new CaseContractError(
897
+ `${path} case "${evalCase.id}" has an invalid json_schema: ${schemaError}`
898
+ );
899
+ }
900
+ }
390
901
  function validateGraders(evalCase, path) {
391
902
  for (const grader of evalCase.assert?.graders ?? []) {
392
903
  if (!graderSupportsMode(grader, evalCase.mode)) {
@@ -413,10 +924,10 @@ function validatePatterns(evalCase, path) {
413
924
  }
414
925
 
415
926
  // src/fixture.ts
416
- import { spawnSync as spawnSync3 } from "child_process";
927
+ import { spawnSync as spawnSync5 } from "child_process";
417
928
  import { createHash as createHash2 } from "crypto";
418
- import { cpSync, readdirSync as readdirSync2, readFileSync as readFileSync4, statSync } from "fs";
419
- import { basename, join as join4 } from "path";
929
+ import { cpSync, readdirSync as readdirSync2, readFileSync as readFileSync7, statSync } from "fs";
930
+ import { basename, join as join6 } from "path";
420
931
  var SETUP_COMMAND_TIMEOUT_MS = 6e4;
421
932
  var FixtureSetupError = class extends Error {
422
933
  results;
@@ -431,7 +942,7 @@ function selectFixture(caseFixture, suiteFixture) {
431
942
  }
432
943
  function resolveFixture(fixture, baseDirectory) {
433
944
  if (fixture === void 0) return void 0;
434
- const directory = fixture.path === void 0 ? void 0 : join4(baseDirectory, fixture.path);
945
+ const directory = fixture.path === void 0 ? void 0 : join6(baseDirectory, fixture.path);
435
946
  const setup = fixture.setup ?? [];
436
947
  return { directory, hash: fixtureIdentityHash(directory, setup), setup };
437
948
  }
@@ -443,7 +954,7 @@ function walkFixtureEntries(directory) {
443
954
  );
444
955
  for (const dirent of dirents) {
445
956
  if (SKIPPED_DIRECTORIES.has(dirent.name)) continue;
446
- const absolutePath = join4(current, dirent.name);
957
+ const absolutePath = join6(current, dirent.name);
447
958
  const path = prefix === "" ? dirent.name : `${prefix}/${dirent.name}`;
448
959
  if (dirent.isSymbolicLink()) {
449
960
  throw new Error(
@@ -472,7 +983,7 @@ function fixtureIdentityHash(directory, setup) {
472
983
  hash.update(`D\0${entry.path}\0`);
473
984
  continue;
474
985
  }
475
- const content = readFileSync4(entry.absolutePath);
986
+ const content = readFileSync7(entry.absolutePath);
476
987
  hash.update(`F\0${entry.path}\0${entry.executable ? "x" : "-"}\0${content.byteLength}\0`);
477
988
  hash.update(content);
478
989
  }
@@ -490,7 +1001,7 @@ function applyFixture(fixture, workspace, home) {
490
1001
  }
491
1002
  const results = [];
492
1003
  for (const command of fixture.setup) {
493
- const outcome = spawnSync3(command, {
1004
+ const outcome = spawnSync5(command, {
494
1005
  cwd: workspace,
495
1006
  encoding: "utf8",
496
1007
  env: { HOME: home, PATH: process.env.PATH ?? "" },
@@ -522,7 +1033,7 @@ var CaseFileError = class extends CaseContractError {
522
1033
  }
523
1034
  };
524
1035
  function readCaseFile(path, expectedSkill) {
525
- const evals = parseCaseFile(readFileSync5(path, "utf8"), path, expectedSkill);
1036
+ const evals = parseCaseFile(readFileSync8(path, "utf8"), path, expectedSkill);
526
1037
  const baseDirectory = dirname2(path);
527
1038
  assertFixtureDirectory(evals.fixture, `${path} suite fixture`, baseDirectory);
528
1039
  for (const evalCase of evals.cases) {
@@ -536,7 +1047,7 @@ function readCaseFile(path, expectedSkill) {
536
1047
  }
537
1048
  function assertFixtureDirectory(fixture, subject, baseDirectory) {
538
1049
  if (fixture?.path === void 0) return;
539
- const directory = join5(baseDirectory, fixture.path);
1050
+ const directory = join7(baseDirectory, fixture.path);
540
1051
  const stats = statSync2(directory, { throwIfNoEntry: false });
541
1052
  if (stats === void 0) {
542
1053
  throw new CaseFileError(
@@ -580,14 +1091,14 @@ function discoverSkills(roots) {
580
1091
  const missingRoots = [];
581
1092
  const skills = [];
582
1093
  for (const root of roots) {
583
- if (!existsSync3(root)) {
1094
+ if (!existsSync4(root)) {
584
1095
  missingRoots.push(root);
585
1096
  continue;
586
1097
  }
587
1098
  const entries = readdirSync3(root, { withFileTypes: true }).filter((entry) => entry.isDirectory()).sort((left, right) => left.name.localeCompare(right.name));
588
1099
  for (const entry of entries) {
589
- const skillDirectory = join6(root, entry.name);
590
- if (!existsSync3(join6(skillDirectory, "SKILL.md"))) continue;
1100
+ const skillDirectory = join8(root, entry.name);
1101
+ if (!existsSync4(join8(skillDirectory, "SKILL.md"))) continue;
591
1102
  skills.push(describeSkill(entry.name, root, skillDirectory));
592
1103
  }
593
1104
  }
@@ -636,8 +1147,8 @@ function selectSkills(discovery, requestedNames) {
636
1147
  });
637
1148
  }
638
1149
  function describeSkill(name, root, skillDirectory) {
639
- const caseFilePath = join6(skillDirectory, "skillval.yml");
640
- if (!existsSync3(caseFilePath)) {
1150
+ const caseFilePath = join8(skillDirectory, "skillval.yml");
1151
+ if (!existsSync4(caseFilePath)) {
641
1152
  return {
642
1153
  caseCount: 0,
643
1154
  class: void 0,
@@ -678,14 +1189,14 @@ function describeSkill(name, root, skillDirectory) {
678
1189
  }
679
1190
 
680
1191
  // src/runner.ts
681
- import { mkdirSync as mkdirSync3, mkdtempSync, rmSync, writeFileSync as writeFileSync3 } from "fs";
1192
+ import { mkdirSync as mkdirSync4, mkdtempSync, rmSync, writeFileSync as writeFileSync3 } from "fs";
682
1193
  import { tmpdir } from "os";
683
- import { join as join8 } from "path";
1194
+ import { join as join10 } from "path";
684
1195
 
685
1196
  // src/cache.ts
686
- import { existsSync as existsSync4, mkdirSync as mkdirSync2, readFileSync as readFileSync6, writeFileSync as writeFileSync2 } from "fs";
687
- import { dirname as dirname3, join as join7 } from "path";
688
- var RUNNER_VERSION = 6;
1197
+ import { existsSync as existsSync5, mkdirSync as mkdirSync3, readFileSync as readFileSync9, writeFileSync as writeFileSync2 } from "fs";
1198
+ import { dirname as dirname3, join as join9 } from "path";
1199
+ var RUNNER_VERSION = 9;
689
1200
  var ArmCache = class {
690
1201
  #stateDirectory;
691
1202
  constructor(stateDirectory = resolveStateDirectory()) {
@@ -693,32 +1204,34 @@ var ArmCache = class {
693
1204
  }
694
1205
  lookup(identity) {
695
1206
  const path = this.#path(identity);
696
- if (!existsSync4(path)) return void 0;
697
- return { ...JSON.parse(readFileSync6(path, "utf8")), cached: true };
1207
+ if (!existsSync5(path)) return void 0;
1208
+ return { ...JSON.parse(readFileSync9(path, "utf8")), cached: true };
698
1209
  }
699
1210
  store(identity, result) {
700
1211
  const path = this.#path(identity);
701
- mkdirSync2(dirname3(path), { recursive: true });
1212
+ mkdirSync3(dirname3(path), { recursive: true });
702
1213
  writeFileSync2(path, JSON.stringify(result));
703
1214
  }
704
1215
  #path(identity) {
1216
+ const skillComponent = identity.arm === "skill" ? identity.skillHash : "";
705
1217
  const parts = [
706
1218
  String(RUNNER_VERSION),
707
- identity.skillHash,
1219
+ skillComponent,
708
1220
  JSON.stringify(identity.evalCase),
709
1221
  identity.arm,
710
1222
  identity.executor.name,
711
1223
  identity.executor.version,
712
- identity.executor.model
1224
+ identity.executor.model,
1225
+ identity.executor.thinking
713
1226
  ];
714
1227
  if (identity.fixtureHash !== void 0) parts.push(identity.fixtureHash);
715
1228
  const key = sha256(parts.join("\0"));
716
- return join7(this.#stateDirectory, "cache", `${key}.json`);
1229
+ return join9(this.#stateDirectory, "cache", `${key}.json`);
717
1230
  }
718
1231
  };
719
1232
 
720
1233
  // src/grade.ts
721
- import { readFileSync as readFileSync7 } from "fs";
1234
+ import { readFileSync as readFileSync10 } from "fs";
722
1235
  import { relative as relative2 } from "path";
723
1236
  var INJECTED_FILES = /* @__PURE__ */ new Set(["package.json", "tsconfig.json"]);
724
1237
  function gradeTrial(evalCase, arm, trace, workspace) {
@@ -729,14 +1242,15 @@ function gradeTrial(evalCase, arm, trace, workspace) {
729
1242
  pass: trace.completed
730
1243
  });
731
1244
  if (evalCase.should_trigger !== void 0 && arm === "skill") {
1245
+ const evidence = trace.invocationEvidence === null ? "none" : trace.invocationEvidence;
732
1246
  checks.push({
733
- detail: `invoked=${trace.invoked}, expected=${evalCase.should_trigger}`,
1247
+ detail: `invoked=${trace.invoked}, expected=${evalCase.should_trigger}, evidence=${evidence}`,
734
1248
  name: "trigger",
735
1249
  pass: trace.invoked === evalCase.should_trigger
736
1250
  });
737
1251
  }
738
1252
  const gradedText = evalCase.mode === "generation" ? walkFiles(workspace).filter((file) => !INJECTED_FILES.has(relative2(workspace, file))).map((file) => `=== ${relative2(workspace, file)} ===
739
- ${readFileSync7(file, "utf8")}`).join("\n") : trace.agentText;
1253
+ ${readFileSync10(file, "utf8")}`).join("\n") : trace.agentText;
740
1254
  for (const pattern of evalCase.assert?.must_match ?? []) {
741
1255
  const pass = new RegExp(pattern, "m").test(gradedText);
742
1256
  checks.push({
@@ -772,7 +1286,19 @@ function shouldEscalate(trials) {
772
1286
  function runEvaluation(config, options, log) {
773
1287
  const discovery = discoverSkills(config.roots);
774
1288
  const selectedSkills = selectSkills(discovery, options.requestedSkills);
775
- const executor = createExecutor(config.executor);
1289
+ assertPiGenerationAcknowledged(
1290
+ config.executor,
1291
+ selectedSkills,
1292
+ options.caseFilter,
1293
+ options.allowUnsandboxedPi
1294
+ );
1295
+ const executor = createExecutor(config.executor, {
1296
+ effort: options.effort,
1297
+ model: options.model
1298
+ });
1299
+ log(
1300
+ `executor: ${executor.metadata.name} ${executor.metadata.version} (model ${executor.metadata.model}, thinking ${executor.metadata.thinking}, invocation detection ${executor.metadata.invocationDetection})`
1301
+ );
776
1302
  const stateDirectory = resolveStateDirectory();
777
1303
  const cache = new ArmCache(stateDirectory);
778
1304
  const skillInputs = selectedSkills.map((skill) => ({
@@ -811,12 +1337,25 @@ function runEvaluation(config, options, log) {
811
1337
  runHash,
812
1338
  skills: skillReports
813
1339
  };
814
- const reportDirectory = join8(stateDirectory, "reports");
815
- const reportPath = join8(reportDirectory, `${runHash}.json`);
816
- mkdirSync3(reportDirectory, { recursive: true });
1340
+ const reportDirectory = join10(stateDirectory, "reports");
1341
+ const reportPath = join10(reportDirectory, `${runHash}.json`);
1342
+ mkdirSync4(reportDirectory, { recursive: true });
817
1343
  writeFileSync3(reportPath, JSON.stringify(report, null, 2));
818
1344
  return { failures, noops, report, reportPath };
819
1345
  }
1346
+ function assertPiGenerationAcknowledged(executorName, skills, caseFilter, allow) {
1347
+ if (executorName !== "pi" || allow) return;
1348
+ for (const skill of skills) {
1349
+ for (const evalCase of skill.evals.cases) {
1350
+ if (caseFilter !== void 0 && evalCase.id !== caseFilter) continue;
1351
+ if (evalCase.mode === "generation") {
1352
+ throw new Error(
1353
+ `pi has no OS sandbox, so generation case "${evalCase.id}" (skill "${skill.name}") would run agent writes without enforced isolation. Re-run with --allow-unsandboxed-pi to acknowledge, or use codex or claude for generation cases.`
1354
+ );
1355
+ }
1356
+ }
1357
+ }
1358
+ }
820
1359
  function participatingSkillsHash(skills) {
821
1360
  return sha256(
822
1361
  [...skills].sort((left, right) => left.name.localeCompare(right.name)).map(({ contentHash, name }) => `${name}\0${contentHash}`).join("\0")
@@ -885,8 +1424,8 @@ function runArm(context, arm) {
885
1424
  return result;
886
1425
  }
887
1426
  function runTrial(context, arm) {
888
- const workspace = mkdtempSync(join8(tmpdir(), `skillval-${context.evalCase.id}-`));
889
- const trialHome = mkdtempSync(join8(tmpdir(), "skillval-home-"));
1427
+ const workspace = mkdtempSync(join10(tmpdir(), `skillval-${context.evalCase.id}-`));
1428
+ const trialHome = mkdtempSync(join10(tmpdir(), "skillval-home-"));
890
1429
  try {
891
1430
  const fixtureSetup = context.fixture === void 0 ? void 0 : applyFixture(context.fixture, workspace, trialHome);
892
1431
  const trace = context.executor.runTrial({
@@ -927,17 +1466,23 @@ function runTrial(context, arm) {
927
1466
 
928
1467
  // src/cli.ts
929
1468
  var program = new Command();
930
- program.name("skillval").description("Evaluate agent skills with deterministic graders").configureHelp({ showGlobalOptions: true }).version("0.1.0").option("--config <path>", "read configuration from this path");
1469
+ program.name("skillval").description("Evaluate agent skills with deterministic graders").configureHelp({ showGlobalOptions: true }).version("0.3.0").option("--config <path>", "read configuration from this path");
931
1470
  program.command("run").description(
932
1471
  "Run selected cases and return a report whose exit status fails when any skill arm fails"
933
- ).argument("[skill...]", "skill names; omit to run every skill with skillval.yml").option("--case <id>", "run only the case with this id").option("--no-cache", "ignore cached arm results").option("--skip-baseline", "do not run baseline arms").option("--json", "return the complete report as JSON").action((skills, options, command) => {
1472
+ ).argument("[skill...]", "skill names; omit to run every skill with skillval.yml").option("--case <id>", "run only the case with this id").option("--model <model>", "model for the executor to use this run").option("--effort <level>", "effort/thinking level for the executor to use this run").option("--no-cache", "ignore cached arm results").option("--skip-baseline", "do not run baseline arms").option(
1473
+ "--allow-unsandboxed-pi",
1474
+ "acknowledge that pi generation trials run without an OS sandbox"
1475
+ ).option("--json", "return the complete report as JSON").action((skills, options, command) => {
934
1476
  const globalOptions = command.optsWithGlobals();
935
1477
  const configPath = resolveConfigPath({ cliPath: globalOptions.config });
936
1478
  const config = loadConfig(configPath);
937
1479
  const outcome = runEvaluation(
938
1480
  config,
939
1481
  {
1482
+ allowUnsandboxedPi: options.allowUnsandboxedPi === true,
940
1483
  caseFilter: options.case,
1484
+ effort: options.effort,
1485
+ model: options.model,
941
1486
  requestedSkills: skills,
942
1487
  skipBaseline: options.skipBaseline === true,
943
1488
  useCache: options.cache