@sagargupta1610/skillcheck 0.2.0 → 0.2.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/README.md CHANGED
@@ -1,68 +1,90 @@
1
1
  # skillcheck
2
2
 
3
+ [![npm](https://img.shields.io/npm/v/%40sagargupta1610%2Fskillcheck?label=npm)](https://www.npmjs.com/package/@sagargupta1610/skillcheck)
4
+ [![CI](https://github.com/Sagargupta16/skillcheck/actions/workflows/ci.yml/badge.svg)](https://github.com/Sagargupta16/skillcheck/actions/workflows/ci.yml)
5
+ [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
6
+ [![Node >= 22](https://img.shields.io/badge/node-%3E%3D22-brightgreen)](package.json)
7
+
3
8
  Conformance suite for [Agent Skills](https://agentskills.io) (`SKILL.md`).
4
9
 
5
- Linters check syntax. skillcheck checks reality: 36 rules with exact [skills-ref](https://github.com/agentskills/agentskills/tree/main/skills-ref) parity (verified against the reference validator's source, message for message), client-extension awareness across 6 runtimes, structure/reference validation, trigger-test scaffolding, SARIF, and a drop-in GitHub Action.
10
+ Linters check syntax. skillcheck checks reality: 36 rules with exact [skills-ref](https://github.com/agentskills/agentskills/tree/main/skills-ref) parity (verified against the reference validator's source, message for message), client-extension awareness across 6 runtimes, structure and reference validation, trigger-test scaffolding, SARIF output, and a drop-in GitHub Action.
11
+
12
+ ```
13
+ $ skillcheck lint ./skills
14
+
15
+ commit-helper ./skills/commit-helper
16
+ ERR [SC013 name-invalid-chars] Skill name 'commit_helper' contains invalid characters. Only letters, digits, and hyphens are allowed.
17
+ WARN [SC301 extension-field] `model` is a client extension (claude-code), not in the Agent Skills spec
18
+ WARN [SC101 broken-relative-reference] referenced file `references/style.md` does not exist in the skill directory
19
+
20
+ 16 skill(s) checked: 1 error(s), 2 warning(s), 0 info
21
+ ```
6
22
 
7
23
  ## Why
8
24
 
9
- The Agent Skills standard is adopted by 40+ runtimes (Claude Code, Codex, Gemini CLI, Copilot, Cursor, OpenCode, ...), and its ecosystem has a built-in tension: the official reference validator (`skills-ref`) is **strict** -- unknown frontmatter fields fail -- while real clients are **lenient** -- they warn and load anyway. Claude Code alone ships 18 extension fields that fail strict validation.
25
+ The Agent Skills standard is adopted by 40+ runtimes (Claude Code, Codex, Gemini CLI, Copilot, Cursor, OpenCode, ...) and its ecosystem has a built-in tension: the official reference validator (`skills-ref`) is **strict** -- unknown frontmatter fields fail -- while real clients are **lenient** -- they warn and load anyway. Claude Code alone ships 18 extension fields that fail strict validation.
10
26
 
11
27
  skillcheck is the only linter that models both tiers:
12
28
 
13
- - **error** = skills-ref 0.1.0 strict parity (exact messages, exact check order, exact i18n semantics -- Chinese and lowercase-Cyrillic names are valid, `skill_name` is not)
14
- - **warning** = client-guide lenient tier + beyond-parity checks (broken references, hidden unicode, portability)
15
- - **info** = advisory, never gates CI
29
+ | Tier | Meaning |
30
+ | --- | --- |
31
+ | **error** | skills-ref 0.1.0 strict parity: exact messages, exact check order, exact i18n semantics |
32
+ | **warning** | client-guide lenient tier + beyond-parity checks (broken references, hidden unicode, portability) |
33
+ | **info** | advisory; never gates CI |
16
34
 
17
- ## Install
35
+ ## Quick start
18
36
 
19
37
  ```bash
38
+ # no install
39
+ npx -y @sagargupta1610/skillcheck lint ./skills
40
+
41
+ # or as a dev dependency
20
42
  pnpm add -D @sagargupta1610/skillcheck
21
- # or run directly
22
- pnpm dlx @sagargupta1610/skillcheck lint ./skills
23
43
  ```
24
44
 
25
- ## Usage
26
-
27
45
  ```bash
28
46
  skillcheck lint ./my-skill # one skill
29
47
  skillcheck lint ./skills # every SKILL.md under a tree
30
48
  skillcheck lint . --profile lenient # client-guide severities
31
- skillcheck lint . --format concise # one line per finding
32
- skillcheck lint . --format json
33
- skillcheck lint . --format github # Actions inline annotations
34
- skillcheck lint . --format sarif # SARIF 2.1.0 to stdout
35
- skillcheck lint . --sarif report.sarif # sidecar SARIF alongside any format
36
- skillcheck lint . --fail-on warning # stricter CI gate
49
+ skillcheck lint . --format concise # one line per finding
50
+ skillcheck lint . --format json # machine-readable
51
+ skillcheck lint . --format sarif # SARIF 2.1.0 to stdout
52
+ skillcheck lint . --sarif report.sarif # sidecar SARIF alongside any format
53
+ skillcheck lint . --fail-on warning # stricter CI gate
37
54
  skillcheck lint . --max-warnings 10
38
55
 
39
56
  skillcheck eval init ./my-skill # scaffold evals/evals.json
40
57
  skillcheck eval check ./my-skill # validate it
41
58
  ```
42
59
 
43
- Exit codes: `0` clean, `1` findings at/above `--fail-on`, `2` usage/internal error.
60
+ Exit codes: `0` clean, `1` findings at or above `--fail-on`, `2` usage or internal error.
44
61
 
45
62
  ## GitHub Action
46
63
 
47
64
  ```yaml
48
- - uses: Sagargupta16/skillcheck@v0.2.0
49
- with:
50
- path: skills/
51
- fail-on: error
52
- sarif-file: skillcheck.sarif # optional
53
-
54
- # optional: upload to code scanning (needs security-events: write)
55
- - uses: github/codeql-action/upload-sarif@v4
56
- with:
57
- sarif_file: skillcheck.sarif
58
- category: skillcheck
65
+ jobs:
66
+ skills:
67
+ runs-on: ubuntu-latest
68
+ steps:
69
+ - uses: actions/checkout@v7
70
+ - uses: Sagargupta16/skillcheck@v0.2.0
71
+ with:
72
+ path: skills/
73
+ fail-on: error
74
+ sarif-file: skillcheck.sarif # optional
75
+
76
+ # optional: upload to code scanning (needs security-events: write)
77
+ - uses: github/codeql-action/upload-sarif@v4
78
+ with:
79
+ sarif_file: skillcheck.sarif
80
+ category: skillcheck
59
81
  ```
60
82
 
61
- Inline PR annotations come free via `--format github`; a full report lands in the job summary automatically.
83
+ Inline PR annotations come free via workflow commands; a full report table lands in the job summary automatically. Action outputs: `error-count`, `warning-count`, `sarif-file`.
62
84
 
63
85
  ## Rules
64
86
 
65
- 36 rules with stable codes (never renumbered or recycled) -- see [docs/rules.md](docs/rules.md) for the full table.
87
+ 36 rules with stable codes (never renumbered or recycled). Full reference: [docs/rules.md](docs/rules.md).
66
88
 
67
89
  | Range | Category | Examples |
68
90
  | --- | --- | --- |
@@ -80,9 +102,9 @@ Parity details worth knowing:
80
102
  - Strict-YAML semantics are enforced on top of the JS parser: flow collections, tags, and duplicate keys are rejected, like `strictyaml`.
81
103
  - The 18-field extension registry knows which runtimes read each field (`model` is Claude Code only; `paths` is Claude Code + Cursor; OpenCode and Gemini ignore all of them) and validates values (`effort: extreme` flags SC302).
82
104
 
83
- ## Config
105
+ ## Configuration
84
106
 
85
- Optional `skillcheck.config.json`:
107
+ Optional `skillcheck.config.json` in your project root:
86
108
 
87
109
  ```json
88
110
  {
@@ -93,16 +115,16 @@ Optional `skillcheck.config.json`:
93
115
  }
94
116
  ```
95
117
 
96
- Codes and aliases are interchangeable. Zero-config works.
118
+ Codes and kebab-case aliases are interchangeable. Zero-config works.
97
119
 
98
120
  ## Evals (trigger tests)
99
121
 
100
- `skillcheck eval init` scaffolds `evals/evals.json` -- a strict superset of Anthropic skill-creator's format (anything skill-creator writes, skillcheck validates unchanged), plus a `triggers` array covering the four trigger classes: explicit, implicit, contextual, and negative (the near-misses that catch over-triggering). `skillcheck lint` validates the file automatically when present. Runtime execution of these tests lands in v0.3.
122
+ `skillcheck eval init` scaffolds `evals/evals.json` -- a strict superset of Anthropic skill-creator's format (anything skill-creator writes, skillcheck validates unchanged) plus a `triggers` array covering four trigger classes: explicit, implicit, contextual, and negative (the near-misses that catch over-triggering). `skillcheck lint` validates the file automatically when present. Runtime execution of these tests lands in v0.3.
101
123
 
102
124
  ## Roadmap
103
125
 
104
126
  - [x] **v0.1** -- static lint, two-tier severity
105
- - [x] **v0.2** -- skills-ref parity (verified message-for-message), rule codes, i18n names, structure/reference rules, extension registry with value validation, SARIF 2.1.0, evals scaffold + validation, GitHub Action, profiles, config
127
+ - [x] **v0.2** -- skills-ref parity (verified message for message), rule codes, i18n names, structure/reference rules, extension registry with value validation, SARIF 2.1.0, evals scaffold + validation, GitHub Action, profiles, config
106
128
  - [ ] **v0.3** -- runtime execution: run trigger tests against Claude Code / OpenCode / Codex headlessly in Docker; `--fix` for safe fixes
107
129
  - [ ] **v1.0** -- public caniuse-style compatibility matrix + README badges
108
130
 
@@ -116,6 +138,8 @@ pnpm lint # biome
116
138
  node dist/cli.js lint fixtures/valid-skill
117
139
  ```
118
140
 
141
+ Contributions welcome -- see [CONTRIBUTING.md](CONTRIBUTING.md).
142
+
119
143
  ## License
120
144
 
121
145
  [MIT](LICENSE)
@@ -1029,6 +1029,9 @@ async function lintSkillDir(dir, options = {}) {
1029
1029
  bundledFiles
1030
1030
  })
1031
1031
  );
1032
+ findings.push(
1033
+ ...await deepReferenceChains(skillDir, parsed.body, bundledFiles)
1034
+ );
1032
1035
  const skillName = parsed.frontmatter && typeof parsed.frontmatter.name === "string" ? parsed.frontmatter.name.trim().normalize("NFKC") : null;
1033
1036
  return summarize(skillDir, skillName, applyOverrides(findings, options));
1034
1037
  }
@@ -1071,6 +1074,50 @@ function applyOverrides(findings, options) {
1071
1074
  }
1072
1075
  return out;
1073
1076
  }
1077
+ function extractReferences(body) {
1078
+ const refs = /* @__PURE__ */ new Set();
1079
+ for (const m of body.matchAll(/\[[^\]]*\]\(([^)]+)\)/g)) {
1080
+ const target = (m[1] ?? "").split(/[#?]/)[0]?.trim() ?? "";
1081
+ if (target) refs.add(target);
1082
+ }
1083
+ for (const m of body.matchAll(
1084
+ /`((?:scripts|references|assets)\/[^\s`]+)`/g
1085
+ )) {
1086
+ refs.add((m[1] ?? "").trim());
1087
+ }
1088
+ return [...refs].filter(
1089
+ (t) => !/^[a-z][a-z0-9+.-]*:\/\//i.test(t) && !t.startsWith("#") && !t.startsWith("mailto:") && !t.includes("${") && !t.includes("{{") && !/^(\/|[A-Za-z]:[\\/]|~\/)/.test(t)
1090
+ );
1091
+ }
1092
+ async function deepReferenceChains(skillDir, body, bundledFiles) {
1093
+ const findings = [];
1094
+ const bundled = new Set(bundledFiles.map((f) => f.replace(/\\/g, "/")));
1095
+ for (const ref of extractReferences(body)) {
1096
+ const normalized = ref.replace(/^\.\//, "").replace(/\\/g, "/");
1097
+ if (!normalized.endsWith(".md") || !bundled.has(normalized)) continue;
1098
+ let childBody;
1099
+ try {
1100
+ childBody = await readFile2(join2(skillDir, normalized), "utf8");
1101
+ } catch {
1102
+ continue;
1103
+ }
1104
+ const childRefs = extractReferences(childBody).filter((t) => {
1105
+ const childDir = normalized.includes("/") ? normalized.slice(0, normalized.lastIndexOf("/")) : "";
1106
+ const resolved = t.startsWith("./") || !t.includes("/") ? `${childDir ? `${childDir}/` : ""}${t.replace(/^\.\//, "")}` : t;
1107
+ return bundled.has(resolved.replace(/\\/g, "/"));
1108
+ });
1109
+ if (childRefs.length > 0) {
1110
+ findings.push({
1111
+ code: "SC103",
1112
+ alias: "deep-reference-chain",
1113
+ severity: "info",
1114
+ message: `\`${normalized}\` references further bundled files (${childRefs.slice(0, 3).join(", ")}${childRefs.length > 3 ? ", ..." : ""}) -- spec recommends keeping references one level deep from SKILL.md`,
1115
+ file: normalized
1116
+ });
1117
+ }
1118
+ }
1119
+ return findings;
1120
+ }
1074
1121
  async function listBundledFiles(skillDir) {
1075
1122
  const files = [];
1076
1123
  async function walk(dir, depth) {
package/dist/cli.js CHANGED
@@ -5,14 +5,14 @@ import {
5
5
  initEvals,
6
6
  lintSkillDir,
7
7
  toSarif
8
- } from "./chunk-MGSKVMAQ.js";
8
+ } from "./chunk-X3UHBJW7.js";
9
9
 
10
10
  // src/cli.ts
11
11
  import { appendFile, readFile, stat, writeFile } from "fs/promises";
12
12
  import { join } from "path";
13
13
  import { Command } from "commander";
14
14
  import { glob } from "glob";
15
- var VERSION = "0.2.0";
15
+ var VERSION = "0.2.2";
16
16
  var program = new Command();
17
17
  program.name("skillcheck").description("Conformance suite for Agent Skills (SKILL.md)").version(VERSION);
18
18
  program.command("lint").description(
@@ -32,14 +32,27 @@ program.command("lint").description(
32
32
  "--fail-on <severity>",
33
33
  "minimum severity that fails: error | warning | never",
34
34
  "error"
35
- ).option("--sarif <path>", "also write a SARIF 2.1.0 report to this path").option("--max-warnings <n>", "fail when warnings exceed this count").action(async (paths, opts) => {
36
- const skillDirs = await expandSkillDirs(paths);
35
+ ).option("--sarif <path>", "also write a SARIF 2.1.0 report to this path").option("--max-warnings <n>", "fail when warnings exceed this count").option(
36
+ "--ignore-pattern <glob...>",
37
+ "glob(s) to exclude during discovery (adds to config ignore)"
38
+ ).action(async (paths, opts) => {
39
+ if (opts.strict && opts.profile === "lenient") {
40
+ console.error("--strict conflicts with --profile lenient; pick one");
41
+ process.exit(2);
42
+ }
43
+ if (opts.strict) {
44
+ console.error(
45
+ "warning: --strict is deprecated; strict is the default (use --profile)"
46
+ );
47
+ }
48
+ const config = await loadConfig();
49
+ const ignore = [...config?.ignore ?? [], ...opts.ignorePattern ?? []];
50
+ const skillDirs = await expandSkillDirs(paths, ignore);
37
51
  if (skillDirs.length === 0) {
38
52
  console.error("no SKILL.md found under the given paths");
39
53
  process.exit(2);
40
54
  }
41
55
  const profile = opts.profile === "lenient" ? "lenient" : "strict";
42
- const config = await loadConfig();
43
56
  const results = [];
44
57
  for (const dir of skillDirs) {
45
58
  const result = await lintSkillDir(dir, {
@@ -103,7 +116,7 @@ async function loadConfig() {
103
116
  return null;
104
117
  }
105
118
  }
106
- async function expandSkillDirs(paths) {
119
+ async function expandSkillDirs(paths, extraIgnore = []) {
107
120
  const dirs = /* @__PURE__ */ new Set();
108
121
  for (const p of paths) {
109
122
  if (await exists(join(p, "SKILL.md")) || await exists(join(p, "skill.md"))) {
@@ -112,7 +125,7 @@ async function expandSkillDirs(paths) {
112
125
  }
113
126
  const matches = await glob("**/{SKILL,skill}.md", {
114
127
  cwd: p,
115
- ignore: ["**/node_modules/**", "**/.git/**"],
128
+ ignore: ["**/node_modules/**", "**/.git/**", ...extraIgnore],
116
129
  absolute: true
117
130
  });
118
131
  for (const m of matches) {
package/dist/index.js CHANGED
@@ -10,7 +10,7 @@ import {
10
10
  lintSkillDir,
11
11
  parseSkillMd,
12
12
  toSarif
13
- } from "./chunk-MGSKVMAQ.js";
13
+ } from "./chunk-X3UHBJW7.js";
14
14
  export {
15
15
  KNOWN_EXTENSIONS,
16
16
  RULES,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sagargupta1610/skillcheck",
3
- "version": "0.2.0",
3
+ "version": "0.2.2",
4
4
  "description": "Conformance suite for Agent Skills: lint SKILL.md against the spec, run it against real agent runtimes, publish a compatibility matrix",
5
5
  "keywords": [
6
6
  "agent-skills",