@uinaf/skillcheck 1.4.1 → 1.5.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/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";
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
- let count = 0;
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
- count++;
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(/^=+ FILE: (.+?) =+\n([\s\S]*?)\n=+ END FILE =+$/gm, (_, name, content) => {
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/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
 
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.4.1",
3
+ "version": "1.5.0",
4
4
  "description": "Lint and eval harness for agent skills",
5
5
  "homepage": "https://github.com/uinaf/skillcheck#readme",
6
6
  "bugs": {