@uinaf/skillcheck 1.0.0 → 1.0.2
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 +52 -16
- package/dist/grok-provider.js +5 -0
- package/dist/scenario.js +16 -3
- package/docs/scenarios.md +3 -1
- package/docs/usage.md +12 -5
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { lintSkills } from "./lint.js";
|
|
3
|
-
import { generateRun, requiredEvalPackages, resolvePackageDir, runNameFor } from "./scenario.js";
|
|
3
|
+
import { encodeRunNamePart, generateRun, requiredEvalPackages, resolvePackageDir, runNameFor } from "./scenario.js";
|
|
4
4
|
import { execFileSync, spawnSync } from "node:child_process";
|
|
5
5
|
import fs from "node:fs";
|
|
6
6
|
import path from "node:path";
|
|
@@ -134,14 +134,19 @@ function attemptPath(resultPath) {
|
|
|
134
134
|
}
|
|
135
135
|
function runScenario(scenarioDir, opts, root) {
|
|
136
136
|
const dirs = stateDirs(root);
|
|
137
|
-
const { name, configPath } = generateRun(path.resolve(scenarioDir), opts, {
|
|
137
|
+
const { name, configPath, skill, scenario } = generateRun(path.resolve(scenarioDir), opts, {
|
|
138
138
|
scratchDir: dirs.scratch,
|
|
139
139
|
transformPath: path.join(here, `transform${selfExt}`),
|
|
140
140
|
grokProviderPath: path.join(here, `grok-provider${selfExt}`)
|
|
141
141
|
});
|
|
142
142
|
fs.mkdirSync(dirs.results, { recursive: true });
|
|
143
143
|
const resultPath = path.join(dirs.results, `${name}.json`);
|
|
144
|
-
|
|
144
|
+
const identity = {
|
|
145
|
+
skill,
|
|
146
|
+
scenario,
|
|
147
|
+
harness: opts.harness
|
|
148
|
+
};
|
|
149
|
+
fs.writeFileSync(attemptPath(resultPath), JSON.stringify(identity) + "\n");
|
|
145
150
|
fs.rmSync(resultPath, { force: true });
|
|
146
151
|
fs.rmSync(metaPath(resultPath), { force: true });
|
|
147
152
|
const sha = gitHead(root);
|
|
@@ -182,7 +187,7 @@ function runScenario(scenarioDir, opts, root) {
|
|
|
182
187
|
if (verdict.score !== void 0) {
|
|
183
188
|
fs.writeFileSync(metaPath(resultPath), JSON.stringify({
|
|
184
189
|
skills_tree_sha: sha,
|
|
185
|
-
|
|
190
|
+
...identity,
|
|
186
191
|
ran_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
187
192
|
tool_version: toolVersion()
|
|
188
193
|
}, null, 2) + "\n");
|
|
@@ -235,11 +240,13 @@ function cmdSweep(argv) {
|
|
|
235
240
|
for (const dir of discoverScenarios(root)) {
|
|
236
241
|
const name = runNameFor(dir, opts.harness);
|
|
237
242
|
const resultPath = path.join(resultsDir, `${name}.json`);
|
|
238
|
-
if (!all && fs.existsSync(resultPath)) {
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
+
if (!all && fs.existsSync(resultPath) && !fs.existsSync(attemptPath(resultPath))) try {
|
|
244
|
+
if (classifyResult(JSON.parse(fs.readFileSync(resultPath, "utf8"))).score !== void 0) {
|
|
245
|
+
skipped++;
|
|
246
|
+
console.log(`SKIP ${name} (results exist; use --all to rerun)`);
|
|
247
|
+
continue;
|
|
248
|
+
}
|
|
249
|
+
} catch {}
|
|
243
250
|
const o = runScenario(dir, opts, root);
|
|
244
251
|
if (o.score === void 0) {
|
|
245
252
|
errored++;
|
|
@@ -255,20 +262,39 @@ function cmdSweep(argv) {
|
|
|
255
262
|
console.log(`\nsweep: ${passed} passed, ${failed} failed, ${errored} errored, ${skipped} skipped`);
|
|
256
263
|
process.exit(errored > 0 ? 2 : failed > 0 ? 1 : 0);
|
|
257
264
|
}
|
|
258
|
-
function resultIdentity(file) {
|
|
265
|
+
function resultIdentity(file, dir) {
|
|
259
266
|
const base = file.replace(/\.json$/, "");
|
|
267
|
+
for (const sidecar of [`${base}.meta.json`, `${file}.attempt`]) try {
|
|
268
|
+
const identity = JSON.parse(fs.readFileSync(path.join(dir, sidecar), "utf8"));
|
|
269
|
+
if (identity !== null && typeof identity === "object" && "skill" in identity && typeof identity.skill === "string" && "scenario" in identity && typeof identity.scenario === "string" && "harness" in identity && (identity.harness === "claude" || identity.harness === "codex" || identity.harness === "grok")) return {
|
|
270
|
+
skill: identity.skill,
|
|
271
|
+
scenario: identity.scenario,
|
|
272
|
+
harness: identity.harness
|
|
273
|
+
};
|
|
274
|
+
} catch {}
|
|
275
|
+
if (/^~v3~[0-9a-f]{64}$/.test(base)) throw new Error(`missing identity metadata for ${file}`);
|
|
276
|
+
const decode = (part) => {
|
|
277
|
+
if (!part.startsWith("~v2~")) return part;
|
|
278
|
+
try {
|
|
279
|
+
const decoded = decodeURIComponent(part.slice(4));
|
|
280
|
+
return encodeRunNamePart(decoded) === part ? decoded : part;
|
|
281
|
+
} catch {
|
|
282
|
+
return part;
|
|
283
|
+
}
|
|
284
|
+
};
|
|
260
285
|
const suffix = base.match(/--(codex|grok|cursor)$/);
|
|
261
286
|
const harness = suffix === null ? "claude" : suffix[1];
|
|
262
287
|
const [skill, ...rest] = base.replace(/--(codex|grok|cursor)$/, "").split("--");
|
|
263
288
|
return {
|
|
264
|
-
skill,
|
|
265
|
-
scenario: rest.join("--"),
|
|
289
|
+
skill: decode(skill),
|
|
290
|
+
scenario: decode(rest.join("--")),
|
|
266
291
|
harness
|
|
267
292
|
};
|
|
268
293
|
}
|
|
269
294
|
function reduceResults(dir, allowMixed) {
|
|
270
295
|
const entries = [];
|
|
271
296
|
const skipped = [];
|
|
297
|
+
const gradedAt = /* @__PURE__ */ new Map();
|
|
272
298
|
const shas = /* @__PURE__ */ new Set();
|
|
273
299
|
const files = fs.readdirSync(dir);
|
|
274
300
|
const incomplete = new Set(files.filter((f) => f.endsWith(".json.attempt")).map((f) => f.replace(/\.attempt$/, "")));
|
|
@@ -300,7 +326,13 @@ function reduceResults(dir, allowMixed) {
|
|
|
300
326
|
const provider = raw.config?.providers?.[0];
|
|
301
327
|
const judge = raw.config?.defaultTest?.options?.provider;
|
|
302
328
|
const base = f.replace(/\.json$/, "");
|
|
303
|
-
const { skill, scenario, harness } = resultIdentity(f);
|
|
329
|
+
const { skill, scenario, harness } = resultIdentity(f, dir);
|
|
330
|
+
const key = entryKey({
|
|
331
|
+
skill,
|
|
332
|
+
scenario,
|
|
333
|
+
harness
|
|
334
|
+
});
|
|
335
|
+
gradedAt.set(key, Math.max(gradedAt.get(key) ?? 0, fs.statSync(path.join(dir, f)).mtimeMs));
|
|
304
336
|
let sha = "unattested";
|
|
305
337
|
try {
|
|
306
338
|
sha = JSON.parse(fs.readFileSync(path.join(dir, `${base}.meta.json`), "utf8")).skills_tree_sha ?? "unattested";
|
|
@@ -323,7 +355,8 @@ function reduceResults(dir, allowMixed) {
|
|
|
323
355
|
return {
|
|
324
356
|
treeSha: shas.size === 1 ? [...shas][0] : shas.size === 0 ? "none" : "mixed",
|
|
325
357
|
entries,
|
|
326
|
-
skipped
|
|
358
|
+
skipped,
|
|
359
|
+
gradedAt
|
|
327
360
|
};
|
|
328
361
|
}
|
|
329
362
|
function entryKey(e) {
|
|
@@ -367,11 +400,14 @@ function cmdSummarize(argv) {
|
|
|
367
400
|
if (positional.length > 0) fail("usage: skillcheck summarize [--root DIR] [--allow-mixed]");
|
|
368
401
|
const dirs = stateDirs(resolveRoot(flags));
|
|
369
402
|
if (!fs.existsSync(dirs.results)) fail(`no results directory at ${dirs.results}; run some evals first`);
|
|
370
|
-
const { entries, skipped } = reduceResults(dirs.results, flags.get("--allow-mixed") === true);
|
|
403
|
+
const { entries, skipped, gradedAt } = reduceResults(dirs.results, flags.get("--allow-mixed") === true);
|
|
371
404
|
fs.mkdirSync(dirs.scorecards, { recursive: true });
|
|
372
405
|
const out = path.join(dirs.scorecards, `${(/* @__PURE__ */ new Date()).toISOString().slice(0, 10)}.json`);
|
|
373
406
|
const existing = readExistingScorecard(out);
|
|
374
|
-
const skippedKeys = new Set(skipped.map((file) =>
|
|
407
|
+
const skippedKeys = new Set(skipped.map((file) => ({
|
|
408
|
+
key: entryKey(resultIdentity(file, dirs.results)),
|
|
409
|
+
modifiedAt: fs.statSync(fs.existsSync(path.join(dirs.results, `${file}.attempt`)) ? path.join(dirs.results, `${file}.attempt`) : path.join(dirs.results, file)).mtimeMs
|
|
410
|
+
})).filter(({ key, modifiedAt }) => (gradedAt.get(key) ?? 0) <= modifiedAt).map(({ key }) => key));
|
|
375
411
|
if (existing.some((entry) => skippedKeys.has(entryKey(entry)))) throw new Error("skipped rerun matches an existing score; refusing to carry it or overwrite the scorecard");
|
|
376
412
|
const merged = mergeScorecard(existing, entries);
|
|
377
413
|
const treeSha = treeShaOf(merged.entries);
|
package/dist/grok-provider.js
CHANGED
|
@@ -113,6 +113,11 @@ var GrokProvider = class {
|
|
|
113
113
|
const abort = () => stop("grok run aborted");
|
|
114
114
|
callOptions?.abortSignal?.addEventListener("abort", abort, { once: true });
|
|
115
115
|
if (callOptions?.abortSignal?.aborted) abort();
|
|
116
|
+
child.on("exit", () => {
|
|
117
|
+
if (process.platform !== "win32" && child.pid !== void 0) try {
|
|
118
|
+
process.kill(-child.pid, "SIGKILL");
|
|
119
|
+
} catch {}
|
|
120
|
+
});
|
|
116
121
|
child.on("close", (code, signal) => {
|
|
117
122
|
clearTimeout(timer);
|
|
118
123
|
callOptions?.abortSignal?.removeEventListener("abort", abort);
|
package/dist/scenario.js
CHANGED
|
@@ -32,10 +32,21 @@ function loadScenario(scenarioDir) {
|
|
|
32
32
|
criteria
|
|
33
33
|
};
|
|
34
34
|
}
|
|
35
|
+
function encodeRunNamePart(part) {
|
|
36
|
+
if (!part.includes("--") && !part.startsWith("-") && !part.endsWith("-") && !part.startsWith("~v2~")) return part;
|
|
37
|
+
return `~v2~${encodeURIComponent(part).replaceAll("-", "%2D").replaceAll("~", "%7E")}`;
|
|
38
|
+
}
|
|
35
39
|
function runNameFor(scenarioDir, harness) {
|
|
36
40
|
const m = path.resolve(scenarioDir).match(/skills\/([^/]+)\/evals\/([^/]+)$/);
|
|
37
41
|
if (!m) throw new Error(`not a scenario dir (want .../skills/<skill>/evals/<scenario>): ${scenarioDir}`);
|
|
38
|
-
|
|
42
|
+
const name = `${encodeRunNamePart(m[1])}--${encodeRunNamePart(m[2])}`;
|
|
43
|
+
const full = harness === "claude" ? name : `${name}--${harness}`;
|
|
44
|
+
if (Buffer.byteLength(`${full}.json.attempt`) <= 255) return full;
|
|
45
|
+
return `~v3~${createHash("sha256").update(JSON.stringify([
|
|
46
|
+
m[1],
|
|
47
|
+
m[2],
|
|
48
|
+
harness
|
|
49
|
+
])).digest("hex")}`;
|
|
39
50
|
}
|
|
40
51
|
function frontmatterRange(text) {
|
|
41
52
|
const lines = text.split("\n");
|
|
@@ -254,8 +265,10 @@ function generateRun(scenarioDir, opts, paths) {
|
|
|
254
265
|
fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
|
|
255
266
|
return {
|
|
256
267
|
name,
|
|
257
|
-
configPath
|
|
268
|
+
configPath,
|
|
269
|
+
skill: s.skill,
|
|
270
|
+
scenario: s.scenario
|
|
258
271
|
};
|
|
259
272
|
}
|
|
260
273
|
//#endregion
|
|
261
|
-
export { buildConfig, generateRun, isHiddenSkill, loadScenario, materialize, requiredEvalPackages, resolvePackageDir, runNameFor, sdkNodeModulesDir, stripHiddenFlag };
|
|
274
|
+
export { buildConfig, encodeRunNamePart, generateRun, isHiddenSkill, loadScenario, materialize, requiredEvalPackages, resolvePackageDir, runNameFor, sdkNodeModulesDir, stripHiddenFlag };
|
package/docs/scenarios.md
CHANGED
|
@@ -9,7 +9,9 @@ A scenario is two files in a frozen location:
|
|
|
9
9
|
|
|
10
10
|
The path is the identity: `<skill>--<scenario>` names the run, the result file,
|
|
11
11
|
and the scorecard entry. Codex and Grok results gain `--codex` and `--grok`
|
|
12
|
-
suffixes, so harnesses can hold results side by side.
|
|
12
|
+
suffixes, so harnesses can hold results side by side. Double hyphens and
|
|
13
|
+
leading or trailing hyphens in a skill or scenario name are escaped in the run
|
|
14
|
+
and result filename; the scorecard keeps the original name.
|
|
13
15
|
A directory missing either file is not discovered.
|
|
14
16
|
|
|
15
17
|
## task.md
|
package/docs/usage.md
CHANGED
|
@@ -77,7 +77,7 @@ provider-qualified judge; the Anthropic judge does not take one.
|
|
|
77
77
|
## Sweep
|
|
78
78
|
|
|
79
79
|
```sh
|
|
80
|
-
skillcheck sweep #
|
|
80
|
+
skillcheck sweep # scenarios without completed results
|
|
81
81
|
skillcheck sweep --all # rerun everything
|
|
82
82
|
```
|
|
83
83
|
|
|
@@ -91,8 +91,8 @@ within one scenario, not across them.
|
|
|
91
91
|
One known failure mode: judge calls through a gateway can drop at the transport
|
|
92
92
|
layer ([uinaf/zebroid-infra#44](https://github.com/uinaf/zebroid-infra/issues/44)).
|
|
93
93
|
That surfaces as an ERROR with no usable result, not as a graded FAIL, and the
|
|
94
|
-
mitigation is a rerun. `sweep` without `--all` resumes, so a rerun
|
|
95
|
-
|
|
94
|
+
mitigation is a rerun. `sweep` without `--all` resumes, so a rerun picks up
|
|
95
|
+
missing or ungraded results.
|
|
96
96
|
|
|
97
97
|
## Summarize
|
|
98
98
|
|
|
@@ -115,14 +115,19 @@ Files that are not promptfoo results and ungraded transport errors are skipped
|
|
|
115
115
|
with a warning rather than failing the reduction. Graded assertion failures
|
|
116
116
|
remain scored results. If a skipped file matches an existing scorecard row,
|
|
117
117
|
summary generation fails and leaves the scorecard unchanged, so an errored rerun
|
|
118
|
-
cannot carry forward its old score.
|
|
118
|
+
cannot carry forward its old score. A graded result for the same identity
|
|
119
|
+
supersedes a skipped attempt only when the result file is newer. This also
|
|
120
|
+
applies with `--allow-mixed`.
|
|
119
121
|
Results from the retired Cursor harness are skipped with their original identity,
|
|
120
122
|
so they cannot become Claude scores or silently carry an old Cursor row.
|
|
121
123
|
Runs keep a `<name>.json.attempt` marker until a graded result and its provenance
|
|
122
|
-
are written. An outstanding marker makes `summarize` skip that identity even
|
|
124
|
+
are written. The marker records the original skill, scenario, and harness. An outstanding marker makes `summarize` skip that identity even
|
|
123
125
|
when the child produced no result file or left partial output. The marker does
|
|
124
126
|
not count as a result for the sweep's existence check, so no-output failures
|
|
125
127
|
remain eligible for retry.
|
|
128
|
+
Long escaped names use a short hashed filename; the original identity is kept
|
|
129
|
+
in the attempt marker and result sidecar. `sweep` retries existing results that
|
|
130
|
+
contain no grade, including results written by older versions without a marker.
|
|
126
131
|
|
|
127
132
|
## Provenance
|
|
128
133
|
|
|
@@ -131,6 +136,8 @@ Each successful run writes a `<name>.meta.json` sidecar next to its result:
|
|
|
131
136
|
```json
|
|
132
137
|
{
|
|
133
138
|
"skills_tree_sha": "<root repo HEAD at run time>",
|
|
139
|
+
"skill": "<skill directory name>",
|
|
140
|
+
"scenario": "<scenario directory name>",
|
|
134
141
|
"harness": "claude",
|
|
135
142
|
"ran_at": "<ISO timestamp>",
|
|
136
143
|
"tool_version": "<skillcheck version>"
|