@uinaf/skillcheck 1.4.1 → 1.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +5 -4
- package/dist/grok-provider.js +4 -2
- package/dist/lint.js +21 -3
- package/dist/scenario.js +3 -2
- package/docs/adoption.md +6 -2
- package/docs/releasing.md +5 -7
- package/docs/scenarios.md +2 -1
- package/docs/usage.md +3 -0
- package/package.json +2 -2
package/dist/cli.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { lintSkills } from "./lint.js";
|
|
3
2
|
import { encodeRunNamePart, generateRun, requiredEvalPackages, resolvePackageDir, runNameFor } from "./scenario.js";
|
|
3
|
+
import { lintSkills } from "./lint.js";
|
|
4
4
|
import { execFileSync, spawnSync } from "node:child_process";
|
|
5
5
|
import { createHash } from "node:crypto";
|
|
6
6
|
import fs from "node:fs";
|
|
@@ -61,12 +61,13 @@ function parseArgs(argv) {
|
|
|
61
61
|
function resolveRoot(flags) {
|
|
62
62
|
return path.resolve(flags.get("--root") ?? process.cwd());
|
|
63
63
|
}
|
|
64
|
-
function stateDirs(root) {
|
|
64
|
+
function stateDirs(root, uid = process.getuid?.()) {
|
|
65
65
|
const base = path.join(root, ".skillcheck");
|
|
66
66
|
const id = createHash("sha256").update(path.resolve(root)).digest("hex").slice(0, 12);
|
|
67
|
+
const owner = uid === void 0 ? "" : `${uid}-`;
|
|
67
68
|
return {
|
|
68
69
|
results: path.join(base, "results"),
|
|
69
|
-
scratch: path.join(fs.realpathSync(os.tmpdir()), `skillcheck-${id}`),
|
|
70
|
+
scratch: path.join(fs.realpathSync(os.tmpdir()), `skillcheck-${owner}${id}`),
|
|
70
71
|
scorecards: path.join(base, "scorecards")
|
|
71
72
|
};
|
|
72
73
|
}
|
|
@@ -137,7 +138,7 @@ function assertUniformConfig(entries, allowMixed) {
|
|
|
137
138
|
for (const [harness, configs] of byHarness) if (configs.size > 1) throw new Error(`${harness} results span multiple run configurations (${[...configs.values()].map(describeConfig).join("; ")}); rerun them to match or pass --allow-mixed`);
|
|
138
139
|
}
|
|
139
140
|
function ensureEvalPackages(opts) {
|
|
140
|
-
const missing = requiredEvalPackages(opts, process.env.ANTHROPIC_API_KEY
|
|
141
|
+
const missing = requiredEvalPackages(opts, Boolean(process.env.ANTHROPIC_API_KEY)).filter((pkg) => resolvePackageDir(pkg) === void 0);
|
|
141
142
|
if (missing.length === 0) return;
|
|
142
143
|
const peers = JSON.parse(fs.readFileSync(path.join(packageDir, "package.json"), "utf8")).peerDependencies ?? {};
|
|
143
144
|
const specs = missing.map((pkg) => `"${pkg}@${peers[pkg] ?? "latest"}"`).join(" ");
|
package/dist/grok-provider.js
CHANGED
|
@@ -89,8 +89,10 @@ var GrokProvider = class {
|
|
|
89
89
|
cost = event.total_cost_usd;
|
|
90
90
|
}
|
|
91
91
|
};
|
|
92
|
+
child.stdout?.setEncoding("utf8");
|
|
93
|
+
child.stderr?.setEncoding("utf8");
|
|
92
94
|
child.stdout?.on("data", (chunk) => {
|
|
93
|
-
stdout += chunk
|
|
95
|
+
stdout += chunk;
|
|
94
96
|
if (stdout.length > OUTPUT_CAP) {
|
|
95
97
|
stop("grok event stream exceeded limit");
|
|
96
98
|
return;
|
|
@@ -103,7 +105,7 @@ var GrokProvider = class {
|
|
|
103
105
|
}
|
|
104
106
|
});
|
|
105
107
|
child.stderr?.on("data", (chunk) => {
|
|
106
|
-
stderr = (stderr + chunk
|
|
108
|
+
stderr = (stderr + chunk).slice(-4e3);
|
|
107
109
|
});
|
|
108
110
|
child.on("error", (cause) => {
|
|
109
111
|
error ??= `grok could not start: ${cause.message}`;
|
package/dist/lint.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { FILE_BLOCK } from "./scenario.js";
|
|
1
2
|
import fs from "node:fs";
|
|
2
3
|
import path from "node:path";
|
|
3
4
|
//#region src/lint.ts
|
|
@@ -61,19 +62,36 @@ function lintSkill(dir, root, errors) {
|
|
|
61
62
|
function lintSkills(root) {
|
|
62
63
|
const roots = ["skills", ...fs.globSync("cli/*/skills", { cwd: root })].map((r) => path.join(root, r));
|
|
63
64
|
const errors = [];
|
|
64
|
-
|
|
65
|
+
const skills = [];
|
|
65
66
|
for (const dir of roots) {
|
|
66
67
|
if (!fs.existsSync(dir)) continue;
|
|
67
68
|
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
68
69
|
if (!entry.isDirectory() || entry.name.startsWith(".")) continue;
|
|
69
70
|
lintSkill(path.join(dir, entry.name), root, errors);
|
|
70
|
-
|
|
71
|
+
skills.push(path.join(dir, entry.name));
|
|
71
72
|
}
|
|
72
73
|
}
|
|
74
|
+
lintTasks(skills, root, errors);
|
|
73
75
|
return {
|
|
74
76
|
errors,
|
|
75
|
-
count
|
|
77
|
+
count: skills.length
|
|
76
78
|
};
|
|
77
79
|
}
|
|
80
|
+
function lintTasks(skills, root, errors) {
|
|
81
|
+
const names = skills.map((dir) => path.basename(dir));
|
|
82
|
+
for (const dir of skills) {
|
|
83
|
+
const evals = path.join(dir, "evals");
|
|
84
|
+
if (!fs.existsSync(evals)) continue;
|
|
85
|
+
for (const scenario of fs.readdirSync(evals, { withFileTypes: true })) {
|
|
86
|
+
const file = path.join(evals, scenario.name, "task.md");
|
|
87
|
+
if (!scenario.isDirectory() || !fs.existsSync(file)) continue;
|
|
88
|
+
const text = fs.readFileSync(file, "utf8").replace(FILE_BLOCK, "");
|
|
89
|
+
for (const name of names) {
|
|
90
|
+
const escaped = name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
91
|
+
if (new RegExp(`(?<![\\w-])${escaped}(?![\\w-])`, "i").test(text)) errors.push(`${path.relative(root, file)}: task names skill ${name}`);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
78
96
|
//#endregion
|
|
79
97
|
export { lintSkills };
|
package/dist/scenario.js
CHANGED
|
@@ -4,6 +4,7 @@ import fs from "node:fs";
|
|
|
4
4
|
import os from "node:os";
|
|
5
5
|
import path from "node:path";
|
|
6
6
|
//#region src/scenario.ts
|
|
7
|
+
const FILE_BLOCK = /^=+ FILE: (.+?) =+\n([\s\S]*?)\n=+ END FILE =+$/gm;
|
|
7
8
|
const DEFAULT_CLAUDE_AGENT = "claude-opus-5";
|
|
8
9
|
function loadScenario(scenarioDir) {
|
|
9
10
|
const match = scenarioDir.match(/skills\/([^/]+)\/evals\/([^/]+)$/);
|
|
@@ -15,7 +16,7 @@ function loadScenario(scenarioDir) {
|
|
|
15
16
|
if (criteria.skill_use !== void 0 && criteria.skill_use !== "required" && criteria.skill_use !== "optional") throw new Error(`skill_use must be "required" or "optional" in ${scenarioDir}`);
|
|
16
17
|
for (const item of criteria.checklist) if (!(typeof item?.name === "string" && item.name.trim() !== "" && typeof item?.description === "string" && item.description.trim() !== "" && Number.isFinite(item?.max_score) && item.max_score > 0)) throw new Error(`invalid checklist item in ${scenarioDir}: ${JSON.stringify(item)}`);
|
|
17
18
|
const files = [];
|
|
18
|
-
const task = taskMd.replace(
|
|
19
|
+
const task = taskMd.replace(FILE_BLOCK, (_, name, content) => {
|
|
19
20
|
files.push({
|
|
20
21
|
name: name.trim(),
|
|
21
22
|
content: content + "\n"
|
|
@@ -322,4 +323,4 @@ function generateRun(scenarioDir, opts, paths) {
|
|
|
322
323
|
};
|
|
323
324
|
}
|
|
324
325
|
//#endregion
|
|
325
|
-
export { DEFAULT_CLAUDE_AGENT, buildConfig, encodeRunNamePart, generateRun, isHiddenSkill, loadScenario, materialize, privateCodexHome, requiredEvalPackages, resolvePackageDir, runNameFor, sdkNodeModulesDir, stripHiddenFlag, trialLabel };
|
|
326
|
+
export { DEFAULT_CLAUDE_AGENT, FILE_BLOCK, buildConfig, encodeRunNamePart, generateRun, isHiddenSkill, loadScenario, materialize, privateCodexHome, requiredEvalPackages, resolvePackageDir, runNameFor, sdkNodeModulesDir, stripHiddenFlag, trialLabel };
|
package/docs/adoption.md
CHANGED
|
@@ -10,7 +10,9 @@ pnpm add -D @uinaf/skillcheck
|
|
|
10
10
|
```
|
|
11
11
|
|
|
12
12
|
Runners that install with `--ignore-scripts` are fine: the package ships
|
|
13
|
-
compiled ESM and has no install
|
|
13
|
+
compiled ESM and has no install or postinstall script. Its `prepare` script
|
|
14
|
+
only wires this repository's own commit hook; package managers do not run it
|
|
15
|
+
for a registry install.
|
|
14
16
|
|
|
15
17
|
```json
|
|
16
18
|
{ "scripts": { "skills:lint": "skillcheck lint" } }
|
|
@@ -25,6 +27,7 @@ jobs:
|
|
|
25
27
|
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
26
28
|
with:
|
|
27
29
|
persist-credentials: false
|
|
30
|
+
- uses: pnpm/action-setup@ea17c68df8912ef543352723c149a84f56e3d413 # v6.1.0
|
|
28
31
|
- uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6.5.0
|
|
29
32
|
with:
|
|
30
33
|
node-version: "24"
|
|
@@ -33,7 +36,8 @@ jobs:
|
|
|
33
36
|
```
|
|
34
37
|
|
|
35
38
|
The job needs no secrets and no network beyond the install. Node 24 is the
|
|
36
|
-
floor.
|
|
39
|
+
floor. `pnpm/action-setup` installs the pnpm version pinned in the repo's
|
|
40
|
+
`packageManager` field; hosted runners do not ship pnpm.
|
|
37
41
|
|
|
38
42
|
Run it through the script rather than a bare `npx skillcheck`: the script
|
|
39
43
|
resolves the version the repo pinned, and `npx` would resolve the latest one on
|
package/docs/releasing.md
CHANGED
|
@@ -5,15 +5,13 @@
|
|
|
5
5
|
A push to `main` runs one workflow, `.github/workflows/release.yml`:
|
|
6
6
|
|
|
7
7
|
```text
|
|
8
|
-
verify
|
|
9
|
-
├──> release npm publish, OIDC + uinaf-ci (release environment)
|
|
10
|
-
scan ────┘
|
|
8
|
+
verify ──> release npm publish, OIDC + uinaf-ci (release environment)
|
|
11
9
|
```
|
|
12
10
|
|
|
13
|
-
`verify`
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
11
|
+
`verify` is the one gate, called from `verify.yml`; its last step runs the
|
|
12
|
+
shared push-time scan from `uinaf/.github` over the pushed range. Keep it that
|
|
13
|
+
way: a second copy of the gate on a push-to-`main` workflow races this one over
|
|
14
|
+
the same commit.
|
|
17
15
|
|
|
18
16
|
The file name `release.yml` is load-bearing. See below.
|
|
19
17
|
|
package/docs/scenarios.md
CHANGED
|
@@ -35,6 +35,7 @@ own examiner or embed generated dependencies.
|
|
|
35
35
|
|
|
36
36
|
Write the task the way a user would write it. Do not name the skill, describe
|
|
37
37
|
its steps, or hint at the checklist: routing is part of what is being measured.
|
|
38
|
+
`skillcheck lint` rejects a task whose prose names any skill in the root.
|
|
38
39
|
|
|
39
40
|
## criteria.json
|
|
40
41
|
|
|
@@ -108,7 +109,7 @@ frontmatter block; body text mentioning the key does not count.
|
|
|
108
109
|
## The workdir
|
|
109
110
|
|
|
110
111
|
Per run, under a scratch directory in the system temp dir
|
|
111
|
-
(`skillcheck-<hash of the root>/<name>/trial-<n>/`), rebuilt from scratch each
|
|
112
|
+
(`skillcheck-<uid>-<hash of the root>/<name>/trial-<n>/`), rebuilt from scratch each
|
|
112
113
|
time. It stays outside the root so the agent cannot reach the skill's source,
|
|
113
114
|
its evals, or the repository's own agent guidance. The skill under test is installed where the harness discovers skills:
|
|
114
115
|
`.claude/skills/<skill>/`, plus `.agents/skills/<skill>/` on codex, or
|
package/docs/usage.md
CHANGED
|
@@ -20,6 +20,9 @@ Checks each `<root>/skills/<skill>/`:
|
|
|
20
20
|
- `disable-model-invocation`, when present, is the bare YAML boolean `true`.
|
|
21
21
|
A quoted `"true"` is an error
|
|
22
22
|
- Relative links in the body resolve on disk
|
|
23
|
+
- No eval `task.md` names a skill from the root, outside its inline input
|
|
24
|
+
files. A named skill gives routing away, and a no-skill control can read
|
|
25
|
+
the operator's installed copy
|
|
23
26
|
|
|
24
27
|
Code spans and fenced blocks are stripped before links are checked, so example
|
|
25
28
|
links never fail. External schemes and `#anchors` pass. Dot-directories under
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@uinaf/skillcheck",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.5.1",
|
|
4
4
|
"description": "Lint and eval harness for agent skills",
|
|
5
5
|
"homepage": "https://github.com/uinaf/skillcheck#readme",
|
|
6
6
|
"bugs": {
|
|
@@ -66,5 +66,5 @@
|
|
|
66
66
|
"engines": {
|
|
67
67
|
"node": ">=24"
|
|
68
68
|
},
|
|
69
|
-
"packageManager": "pnpm@12.
|
|
69
|
+
"packageManager": "pnpm@12.5.1"
|
|
70
70
|
}
|