@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 +59 -35
- package/dist/{chunk-MGSKVMAQ.js → chunk-X3UHBJW7.js} +47 -0
- package/dist/cli.js +20 -7
- package/dist/index.js +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,68 +1,90 @@
|
|
|
1
1
|
# skillcheck
|
|
2
2
|
|
|
3
|
+
[](https://www.npmjs.com/package/@sagargupta1610/skillcheck)
|
|
4
|
+
[](https://github.com/Sagargupta16/skillcheck/actions/workflows/ci.yml)
|
|
5
|
+
[](LICENSE)
|
|
6
|
+
[](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
|
|
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, ...)
|
|
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
|
-
|
|
14
|
-
|
|
15
|
-
|
|
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
|
-
##
|
|
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
|
|
32
|
-
skillcheck lint . --format json
|
|
33
|
-
skillcheck lint . --format
|
|
34
|
-
skillcheck lint . --
|
|
35
|
-
skillcheck lint . --
|
|
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
|
|
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
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
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
|
|
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)
|
|
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
|
-
##
|
|
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)
|
|
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
|
|
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-
|
|
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.
|
|
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").
|
|
36
|
-
|
|
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
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sagargupta1610/skillcheck",
|
|
3
|
-
"version": "0.2.
|
|
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",
|