@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/README.md +120 -12
- package/dist/cli.js +660 -115
- package/dist/cli.js.map +1 -1
- package/package.json +4 -3
- package/schemas/config.schema.json +1 -1
- package/schemas/skillval.schema.json +48 -2
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
|
|
8
|
-
import { homedir as
|
|
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/
|
|
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/
|
|
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 =
|
|
219
|
+
constructor(overrides = {}, realHome = homedir2()) {
|
|
220
|
+
assertEffortSupported("codex", overrides.effort, CODEX_EFFORT_LEVELS);
|
|
221
|
+
this.#overrides = overrides;
|
|
60
222
|
this.#realHome = realHome;
|
|
61
|
-
|
|
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")
|
|
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:
|
|
242
|
+
CODEX_HOME: join3(this.#realHome, ".codex"),
|
|
69
243
|
HOME: request.home
|
|
70
244
|
} : { ...process.env };
|
|
71
|
-
const result =
|
|
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:
|
|
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
|
|
98
|
-
const skillsRoot =
|
|
99
|
-
|
|
100
|
-
|
|
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 =
|
|
103
|
-
const version =
|
|
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
|
|
106
|
-
|
|
107
|
-
)?.[1] ?? "default";
|
|
108
|
-
return {
|
|
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
|
-
|
|
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 ??
|
|
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 =
|
|
497
|
+
function expandRoot(root, home = homedir4()) {
|
|
185
498
|
return resolve(expandHome(root, home));
|
|
186
499
|
}
|
|
187
|
-
function loadConfig(path, home =
|
|
188
|
-
if (!
|
|
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(
|
|
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 =
|
|
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
|
|
219
|
-
import { join as
|
|
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
|
|
223
|
-
import { dirname as dirname2, join as
|
|
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
|
|
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
|
|
232
|
-
import {
|
|
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
|
|
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
|
-
|
|
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 (!
|
|
252
|
-
writeFileSync(
|
|
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
|
-
|
|
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 =
|
|
272
|
-
|
|
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 =
|
|
285
|
-
var nonEmptyStringSchema =
|
|
286
|
-
var stringArraySchema =
|
|
287
|
-
var armSchema =
|
|
288
|
-
var fixtureSchema =
|
|
289
|
-
|
|
290
|
-
path:
|
|
291
|
-
|
|
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:
|
|
298
|
-
|
|
299
|
-
|
|
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 =
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
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
|
-
|
|
313
|
-
|
|
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 =
|
|
318
|
-
|
|
319
|
-
arms:
|
|
320
|
-
|
|
321
|
-
|
|
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:
|
|
327
|
-
fixture:
|
|
806
|
+
assert: Type3.Optional(caseAssertSchema),
|
|
807
|
+
fixture: Type3.Optional(fixtureSchema),
|
|
328
808
|
id: nonEmptyStringSchema,
|
|
329
|
-
mode:
|
|
809
|
+
mode: Type3.Enum(["generation", "trigger"]),
|
|
330
810
|
prompt: nonEmptyStringSchema,
|
|
331
|
-
rule:
|
|
332
|
-
should_trigger:
|
|
333
|
-
trials:
|
|
334
|
-
type:
|
|
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 =
|
|
339
|
-
|
|
340
|
-
cases:
|
|
818
|
+
var skillEvalsSchema = Type3.ReadonlyObject(
|
|
819
|
+
Type3.Object({
|
|
820
|
+
cases: Type3.Readonly(Type3.Array(evalCaseSchema)),
|
|
341
821
|
class: classificationSchema,
|
|
342
|
-
fixture:
|
|
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
|
|
927
|
+
import { spawnSync as spawnSync5 } from "child_process";
|
|
417
928
|
import { createHash as createHash2 } from "crypto";
|
|
418
|
-
import { cpSync, readdirSync as readdirSync2, readFileSync as
|
|
419
|
-
import { basename, join as
|
|
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 :
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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(
|
|
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 =
|
|
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 (!
|
|
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 =
|
|
590
|
-
if (!
|
|
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 =
|
|
640
|
-
if (!
|
|
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
|
|
1192
|
+
import { mkdirSync as mkdirSync4, mkdtempSync, rmSync, writeFileSync as writeFileSync3 } from "fs";
|
|
682
1193
|
import { tmpdir } from "os";
|
|
683
|
-
import { join as
|
|
1194
|
+
import { join as join10 } from "path";
|
|
684
1195
|
|
|
685
1196
|
// src/cache.ts
|
|
686
|
-
import { existsSync as
|
|
687
|
-
import { dirname as dirname3, join as
|
|
688
|
-
var RUNNER_VERSION =
|
|
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 (!
|
|
697
|
-
return { ...JSON.parse(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
1229
|
+
return join9(this.#stateDirectory, "cache", `${key}.json`);
|
|
717
1230
|
}
|
|
718
1231
|
};
|
|
719
1232
|
|
|
720
1233
|
// src/grade.ts
|
|
721
|
-
import { readFileSync as
|
|
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
|
-
${
|
|
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
|
-
|
|
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 =
|
|
815
|
-
const reportPath =
|
|
816
|
-
|
|
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(
|
|
889
|
-
const trialHome = mkdtempSync(
|
|
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.
|
|
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("--
|
|
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
|