@uinaf/skillcheck 1.5.1 → 1.5.3

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/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
- for (const root of roots) fs.cpSync(s.skillDir, path.join(workdir, root, "skills", s.skill), {
109
- recursive: true,
110
- filter: (src) => path.basename(src) !== "evals"
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 (e.name !== ".claude" && e.name !== ".grok" && e.name !== "node_modules") walk(p);
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,11 @@ 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: { CODEX_HOME: path.join(workdir, "..", "..", "codex-home") }
195
+ cli_config: { features: { plugins: false } },
196
+ cli_env: {
197
+ CODEX_HOME: path.join(workdir, "..", "..", "codex-home"),
198
+ HOME: path.join(workdir, "..", "..", "home")
199
+ }
155
200
  }
156
201
  };
157
202
  return {
@@ -293,6 +338,26 @@ function privateCodexHome(dir) {
293
338
  if (fs.existsSync(from)) fs.symlinkSync(from, path.join(dir, file));
294
339
  }
295
340
  }
341
+ const HIDDEN_HOME_ENTRIES = /* @__PURE__ */ new Set([
342
+ ".agents",
343
+ ".claude",
344
+ ".codex"
345
+ ]);
346
+ function privateHome(dir) {
347
+ const source = os.homedir();
348
+ fs.rmSync(dir, {
349
+ recursive: true,
350
+ force: true
351
+ });
352
+ fs.mkdirSync(dir, {
353
+ recursive: true,
354
+ mode: 448
355
+ });
356
+ for (const name of fs.readdirSync(source)) {
357
+ if (HIDDEN_HOME_ENTRIES.has(name)) continue;
358
+ fs.symlinkSync(path.join(source, name), path.join(dir, name));
359
+ }
360
+ }
296
361
  function generateRun(scenarioDir, opts, paths) {
297
362
  const s = loadScenario(scenarioDir);
298
363
  const name = runNameFor(scenarioDir, opts.harness, opts.control);
@@ -302,7 +367,10 @@ function generateRun(scenarioDir, opts, paths) {
302
367
  force: true
303
368
  });
304
369
  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") privateCodexHome(path.join(runDir, "codex-home"));
370
+ if (opts.harness === "codex") {
371
+ privateCodexHome(path.join(runDir, "codex-home"));
372
+ privateHome(path.join(runDir, "home"));
373
+ }
306
374
  const sdkDir = sdkNodeModulesDir();
307
375
  if (sdkDir !== void 0) {
308
376
  const link = path.join(runDir, "node_modules");
@@ -323,4 +391,4 @@ function generateRun(scenarioDir, opts, paths) {
323
391
  };
324
392
  }
325
393
  //#endregion
326
- export { DEFAULT_CLAUDE_AGENT, FILE_BLOCK, buildConfig, encodeRunNamePart, generateRun, isHiddenSkill, loadScenario, materialize, privateCodexHome, requiredEvalPackages, resolvePackageDir, runNameFor, sdkNodeModulesDir, stripHiddenFlag, trialLabel };
394
+ 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 (e.name !== ".claude" && e.name !== ".grok" && e.name !== "node_modules") walk(p);
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/scenarios.md CHANGED
@@ -114,7 +114,19 @@ 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`, with plugins turned off (`features.plugins =
123
+ false`) so a plugin the config enables cannot supply skills, and a private `HOME` that links every entry of
124
+ the operator's home except `.agents`, `.claude`, and `.codex`. Auth helpers that read
125
+ `$HOME` keep working; the operator's user-level skills and global guidance are
126
+ not visible.
127
+
128
+ Graded output skips agent config roots, `node_modules`, and tool caches
129
+ (`.cache`, `.npm`, `.pnpm-store`), so a cache cannot crowd deliverables out.
118
130
 
119
131
  Scenario quality is behavioral proof; [authoring](authoring.md) covers the
120
132
  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.1",
3
+ "version": "1.5.3",
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.5.1"
69
+ "packageManager": "pnpm@12.6.0"
70
70
  }