@uinaf/skillcheck 1.5.0 → 1.5.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 +4 -3
- package/dist/grok-provider.js +4 -2
- package/dist/scenario.js +75 -8
- package/dist/transform.js +10 -2
- package/docs/adoption.md +6 -2
- package/docs/releasing.md +5 -7
- package/docs/scenarios.md +13 -2
- package/package.json +2 -2
package/dist/cli.js
CHANGED
|
@@ -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/scenario.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { SKIPPED_DIRS } from "./transform.js";
|
|
1
2
|
import { createRequire } from "node:module";
|
|
2
3
|
import { createHash } from "node:crypto";
|
|
3
4
|
import fs from "node:fs";
|
|
@@ -74,6 +75,41 @@ function stripHiddenFlag(skillMd) {
|
|
|
74
75
|
if (!range) return skillMd;
|
|
75
76
|
return skillMd.split("\n").filter((l, i) => !(i >= range[0] && i < range[1] && l.startsWith("disable-model-invocation:"))).join("\n");
|
|
76
77
|
}
|
|
78
|
+
const MARKDOWN_LINK = /\]\(\s*<([^>]+)>|\]\(\s*([^)\s]+)|^ {0,3}\[[^\]]+\]:\s*<?([^\s>]+)/gm;
|
|
79
|
+
function linkedSiblings(skillDir) {
|
|
80
|
+
const skillsRoot = path.dirname(skillDir);
|
|
81
|
+
const realRoot = fs.realpathSync(skillsRoot);
|
|
82
|
+
const self = path.basename(skillDir);
|
|
83
|
+
const found = /* @__PURE__ */ new Set([self]);
|
|
84
|
+
const pending = [skillDir];
|
|
85
|
+
for (let dir = pending.pop(); dir !== void 0; dir = pending.pop()) for (const file of skillMarkdown(dir)) for (const m of fs.readFileSync(file, "utf8").matchAll(MARKDOWN_LINK)) {
|
|
86
|
+
const href = m[1] ?? m[2] ?? m[3];
|
|
87
|
+
if (href.startsWith("#") || /^[a-z][a-z0-9+.-]*:/i.test(href)) continue;
|
|
88
|
+
const target = path.resolve(path.dirname(file), href.split("#")[0]);
|
|
89
|
+
const rel = path.relative(skillsRoot, target);
|
|
90
|
+
const name = rel.split(path.sep)[0];
|
|
91
|
+
if (name === "" || name === ".." || path.isAbsolute(rel) || found.has(name)) continue;
|
|
92
|
+
if (!fs.existsSync(path.join(skillsRoot, name, "SKILL.md"))) continue;
|
|
93
|
+
if (path.dirname(fs.realpathSync(path.join(skillsRoot, name))) !== realRoot) continue;
|
|
94
|
+
found.add(name);
|
|
95
|
+
pending.push(path.join(skillsRoot, name));
|
|
96
|
+
}
|
|
97
|
+
found.delete(self);
|
|
98
|
+
return [...found].sort();
|
|
99
|
+
}
|
|
100
|
+
function skillMarkdown(dir) {
|
|
101
|
+
const out = [];
|
|
102
|
+
const walk = (d) => {
|
|
103
|
+
for (const e of fs.readdirSync(d, { withFileTypes: true })) {
|
|
104
|
+
const p = path.join(d, e.name);
|
|
105
|
+
if (e.isDirectory()) {
|
|
106
|
+
if (!(d === dir && e.name === "evals")) walk(p);
|
|
107
|
+
} else if (e.isFile() && e.name.endsWith(".md")) out.push(p);
|
|
108
|
+
}
|
|
109
|
+
};
|
|
110
|
+
walk(dir);
|
|
111
|
+
return out;
|
|
112
|
+
}
|
|
77
113
|
const RESERVED = /* @__PURE__ */ new Set([
|
|
78
114
|
".claude",
|
|
79
115
|
".agents",
|
|
@@ -105,10 +141,15 @@ function materialize(s, runDir, harness, control = false) {
|
|
|
105
141
|
fs.writeFileSync(dest, content);
|
|
106
142
|
}
|
|
107
143
|
const roots = control ? [] : harness === "codex" ? [".claude", ".agents"] : harness === "grok" ? [".grok"] : [".claude"];
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
144
|
+
const skillsRoot = path.dirname(s.skillDir);
|
|
145
|
+
const installed = control ? [] : [s.skill, ...linkedSiblings(s.skillDir)];
|
|
146
|
+
for (const root of roots) for (const name of installed) {
|
|
147
|
+
const from = path.join(skillsRoot, name);
|
|
148
|
+
fs.cpSync(from, path.join(workdir, root, "skills", name), {
|
|
149
|
+
recursive: true,
|
|
150
|
+
filter: (src) => src !== path.join(from, "evals")
|
|
151
|
+
});
|
|
152
|
+
}
|
|
112
153
|
for (const root of roots) {
|
|
113
154
|
const skillMd = path.join(workdir, root, "skills", s.skill, "SKILL.md");
|
|
114
155
|
const text = fs.readFileSync(skillMd, "utf8");
|
|
@@ -120,7 +161,7 @@ function materialize(s, runDir, harness, control = false) {
|
|
|
120
161
|
for (const e of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
121
162
|
const p = path.join(dir, e.name);
|
|
122
163
|
if (e.isDirectory()) {
|
|
123
|
-
if (
|
|
164
|
+
if (!SKIPPED_DIRS.has(e.name)) walk(p);
|
|
124
165
|
} else manifest[path.relative(workdir, p)] = createHash("sha256").update(fs.readFileSync(p)).digest("hex");
|
|
125
166
|
}
|
|
126
167
|
};
|
|
@@ -151,7 +192,10 @@ function agentProvider(opts, workdir, skill, paths) {
|
|
|
151
192
|
sandbox_mode: "workspace-write",
|
|
152
193
|
network_access_enabled: true,
|
|
153
194
|
web_search_enabled: true,
|
|
154
|
-
cli_env: {
|
|
195
|
+
cli_env: {
|
|
196
|
+
CODEX_HOME: path.join(workdir, "..", "..", "codex-home"),
|
|
197
|
+
HOME: path.join(workdir, "..", "..", "home")
|
|
198
|
+
}
|
|
155
199
|
}
|
|
156
200
|
};
|
|
157
201
|
return {
|
|
@@ -293,6 +337,26 @@ function privateCodexHome(dir) {
|
|
|
293
337
|
if (fs.existsSync(from)) fs.symlinkSync(from, path.join(dir, file));
|
|
294
338
|
}
|
|
295
339
|
}
|
|
340
|
+
const HIDDEN_HOME_ENTRIES = /* @__PURE__ */ new Set([
|
|
341
|
+
".agents",
|
|
342
|
+
".claude",
|
|
343
|
+
".codex"
|
|
344
|
+
]);
|
|
345
|
+
function privateHome(dir) {
|
|
346
|
+
const source = os.homedir();
|
|
347
|
+
fs.rmSync(dir, {
|
|
348
|
+
recursive: true,
|
|
349
|
+
force: true
|
|
350
|
+
});
|
|
351
|
+
fs.mkdirSync(dir, {
|
|
352
|
+
recursive: true,
|
|
353
|
+
mode: 448
|
|
354
|
+
});
|
|
355
|
+
for (const name of fs.readdirSync(source)) {
|
|
356
|
+
if (HIDDEN_HOME_ENTRIES.has(name)) continue;
|
|
357
|
+
fs.symlinkSync(path.join(source, name), path.join(dir, name));
|
|
358
|
+
}
|
|
359
|
+
}
|
|
296
360
|
function generateRun(scenarioDir, opts, paths) {
|
|
297
361
|
const s = loadScenario(scenarioDir);
|
|
298
362
|
const name = runNameFor(scenarioDir, opts.harness, opts.control);
|
|
@@ -302,7 +366,10 @@ function generateRun(scenarioDir, opts, paths) {
|
|
|
302
366
|
force: true
|
|
303
367
|
});
|
|
304
368
|
const trials = Array.from({ length: opts.trials ?? 1 }, (_, i) => materialize(s, path.join(runDir, trialLabel(i)), opts.harness, opts.control));
|
|
305
|
-
if (opts.harness === "codex")
|
|
369
|
+
if (opts.harness === "codex") {
|
|
370
|
+
privateCodexHome(path.join(runDir, "codex-home"));
|
|
371
|
+
privateHome(path.join(runDir, "home"));
|
|
372
|
+
}
|
|
306
373
|
const sdkDir = sdkNodeModulesDir();
|
|
307
374
|
if (sdkDir !== void 0) {
|
|
308
375
|
const link = path.join(runDir, "node_modules");
|
|
@@ -323,4 +390,4 @@ function generateRun(scenarioDir, opts, paths) {
|
|
|
323
390
|
};
|
|
324
391
|
}
|
|
325
392
|
//#endregion
|
|
326
|
-
export { DEFAULT_CLAUDE_AGENT, FILE_BLOCK, buildConfig, encodeRunNamePart, generateRun, isHiddenSkill, loadScenario, materialize, privateCodexHome, requiredEvalPackages, resolvePackageDir, runNameFor, sdkNodeModulesDir, stripHiddenFlag, trialLabel };
|
|
393
|
+
export { DEFAULT_CLAUDE_AGENT, FILE_BLOCK, buildConfig, encodeRunNamePart, generateRun, isHiddenSkill, linkedSiblings, loadScenario, materialize, privateCodexHome, privateHome, requiredEvalPackages, resolvePackageDir, runNameFor, sdkNodeModulesDir, stripHiddenFlag, trialLabel };
|
package/dist/transform.js
CHANGED
|
@@ -14,6 +14,14 @@ function capped(rel, text) {
|
|
|
14
14
|
if (text.length <= PER_FILE_CAP) return `=== OUTPUT FILE: ${rel} ===\n${text}\n=== END OUTPUT FILE ===`;
|
|
15
15
|
return `=== OUTPUT FILE: ${rel} (truncated: showing ${PER_FILE_CAP} of ${text.length} chars) ===\n${safeSlice(text, PER_FILE_CAP)}\n=== END OUTPUT FILE ===`;
|
|
16
16
|
}
|
|
17
|
+
const SKIPPED_DIRS = /* @__PURE__ */ new Set([
|
|
18
|
+
".claude",
|
|
19
|
+
".grok",
|
|
20
|
+
"node_modules",
|
|
21
|
+
".cache",
|
|
22
|
+
".npm",
|
|
23
|
+
".pnpm-store"
|
|
24
|
+
]);
|
|
17
25
|
function transform(output, context) {
|
|
18
26
|
const workdir = context.vars.workdir;
|
|
19
27
|
const manifest = JSON.parse(fs.readFileSync(context.vars.manifest, "utf8"));
|
|
@@ -23,7 +31,7 @@ function transform(output, context) {
|
|
|
23
31
|
for (const e of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
24
32
|
const p = path.join(dir, e.name);
|
|
25
33
|
if (e.isDirectory()) {
|
|
26
|
-
if (
|
|
34
|
+
if (!SKIPPED_DIRS.has(e.name)) walk(p);
|
|
27
35
|
} else if (e.isFile()) {
|
|
28
36
|
const rel = path.relative(workdir, p);
|
|
29
37
|
visited.add(rel);
|
|
@@ -64,4 +72,4 @@ function transform(output, context) {
|
|
|
64
72
|
return `${output}\n\n${appended}`;
|
|
65
73
|
}
|
|
66
74
|
//#endregion
|
|
67
|
-
export { transform as default };
|
|
75
|
+
export { SKIPPED_DIRS, transform as default };
|
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
|
@@ -109,12 +109,23 @@ frontmatter block; body text mentioning the key does not count.
|
|
|
109
109
|
## The workdir
|
|
110
110
|
|
|
111
111
|
Per run, under a scratch directory in the system temp dir
|
|
112
|
-
(`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
|
|
113
113
|
time. It stays outside the root so the agent cannot reach the skill's source,
|
|
114
114
|
its evals, or the repository's own agent guidance. The skill under test is installed where the harness discovers skills:
|
|
115
115
|
`.claude/skills/<skill>/`, plus `.agents/skills/<skill>/` on codex, or
|
|
116
116
|
`.grok/skills/<skill>/` on Grok, with its `evals/` directory excluded, so
|
|
117
|
-
criteria never leak into the agent's context.
|
|
117
|
+
criteria never leak into the agent's context. Sibling skills under the same root that the skill links to (inline, angle-bracket, or reference links, followed transitively; a symlink out of the root is skipped) are installed beside it
|
|
118
|
+
the same way, so links such as `../other/SKILL.md` resolve as they do when a
|
|
119
|
+
plugin installs the set; a control run installs none of them.
|
|
120
|
+
|
|
121
|
+
A Codex run gets a private `CODEX_HOME` holding only the operator's
|
|
122
|
+
`config.toml` and `auth.json`, and a private `HOME` that links every entry of
|
|
123
|
+
the operator's home except `.agents`, `.claude`, and `.codex`. Auth helpers that read
|
|
124
|
+
`$HOME` keep working; the operator's user-level skills and global guidance are
|
|
125
|
+
not visible.
|
|
126
|
+
|
|
127
|
+
Graded output skips agent config roots, `node_modules`, and tool caches
|
|
128
|
+
(`.cache`, `.npm`, `.pnpm-store`), so a cache cannot crowd deliverables out.
|
|
118
129
|
|
|
119
130
|
Scenario quality is behavioral proof; [authoring](authoring.md) covers the
|
|
120
131
|
judgment layer lint and evals cannot grade.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@uinaf/skillcheck",
|
|
3
|
-
"version": "1.5.
|
|
3
|
+
"version": "1.5.2",
|
|
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.6.0"
|
|
70
70
|
}
|