@weareikko/eslint-plugin-skill 0.0.1 → 0.1.1

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.
Files changed (105) hide show
  1. package/README.md +162 -2
  2. package/configs/check-refs.workflow.yml +25 -0
  3. package/configs/eslint.config.example.js +23 -0
  4. package/configs/lint-skills.workflow.yml +26 -0
  5. package/dist/dedup/analyze.d.ts +26 -0
  6. package/dist/dedup/analyze.d.ts.map +1 -0
  7. package/dist/dedup/cli.d.ts +10 -0
  8. package/dist/dedup/cli.d.ts.map +1 -0
  9. package/dist/dedup/cli.js +37 -0
  10. package/dist/dedup/cli.js.map +1 -0
  11. package/dist/dedup/discover.d.ts +16 -0
  12. package/dist/dedup/discover.d.ts.map +1 -0
  13. package/dist/dedup/hash.d.ts +17 -0
  14. package/dist/dedup/hash.d.ts.map +1 -0
  15. package/dist/dedup/index.d.ts +4 -0
  16. package/dist/dedup/index.d.ts.map +1 -0
  17. package/dist/dedup/index.js +2 -0
  18. package/dist/dedup/report.d.ts +6 -0
  19. package/dist/dedup/report.d.ts.map +1 -0
  20. package/dist/dedup/types.d.ts +92 -0
  21. package/dist/dedup/types.d.ts.map +1 -0
  22. package/dist/index.d.ts +109 -0
  23. package/dist/index.d.ts.map +1 -0
  24. package/dist/index.js +1236 -0
  25. package/dist/index.js.map +1 -0
  26. package/dist/report-ClPKWYJy.js +266 -0
  27. package/dist/report-ClPKWYJy.js.map +1 -0
  28. package/dist/rules/body-max-length.d.ts +16 -0
  29. package/dist/rules/body-max-length.d.ts.map +1 -0
  30. package/dist/rules/description-has-trigger.d.ts +16 -0
  31. package/dist/rules/description-has-trigger.d.ts.map +1 -0
  32. package/dist/rules/description-length.d.ts +16 -0
  33. package/dist/rules/description-length.d.ts.map +1 -0
  34. package/dist/rules/description-no-html.d.ts +13 -0
  35. package/dist/rules/description-no-html.d.ts.map +1 -0
  36. package/dist/rules/descriptive-reference-filenames.d.ts +15 -0
  37. package/dist/rules/descriptive-reference-filenames.d.ts.map +1 -0
  38. package/dist/rules/h1-matches-name.d.ts +13 -0
  39. package/dist/rules/h1-matches-name.d.ts.map +1 -0
  40. package/dist/rules/large-reference-toc.d.ts +16 -0
  41. package/dist/rules/large-reference-toc.d.ts.map +1 -0
  42. package/dist/rules/meta-prefers-dispatch-table.d.ts +19 -0
  43. package/dist/rules/meta-prefers-dispatch-table.d.ts.map +1 -0
  44. package/dist/rules/multi-domain-has-variants.d.ts +16 -0
  45. package/dist/rules/multi-domain-has-variants.d.ts.map +1 -0
  46. package/dist/rules/name-kebab-case.d.ts +10 -0
  47. package/dist/rules/name-kebab-case.d.ts.map +1 -0
  48. package/dist/rules/name-matches-dir.d.ts +10 -0
  49. package/dist/rules/name-matches-dir.d.ts.map +1 -0
  50. package/dist/rules/no-broken-relative-links.d.ts +14 -0
  51. package/dist/rules/no-broken-relative-links.d.ts.map +1 -0
  52. package/dist/rules/no-multiple-h1.d.ts +15 -0
  53. package/dist/rules/no-multiple-h1.d.ts.map +1 -0
  54. package/dist/rules/no-orphan-references.d.ts +15 -0
  55. package/dist/rules/no-orphan-references.d.ts.map +1 -0
  56. package/dist/rules/no-unknown-frontmatter-keys.d.ts +18 -0
  57. package/dist/rules/no-unknown-frontmatter-keys.d.ts.map +1 -0
  58. package/dist/rules/procedural-has-steps.d.ts +15 -0
  59. package/dist/rules/procedural-has-steps.d.ts.map +1 -0
  60. package/dist/rules/reference-has-content.d.ts +15 -0
  61. package/dist/rules/reference-has-content.d.ts.map +1 -0
  62. package/dist/rules/refs-one-level-deep.d.ts +15 -0
  63. package/dist/rules/refs-one-level-deep.d.ts.map +1 -0
  64. package/dist/rules/require-frontmatter.d.ts +11 -0
  65. package/dist/rules/require-frontmatter.d.ts.map +1 -0
  66. package/dist/rules/valid-allowed-tools.d.ts +15 -0
  67. package/dist/rules/valid-allowed-tools.d.ts.map +1 -0
  68. package/dist/types.d.ts +15 -0
  69. package/dist/types.d.ts.map +1 -0
  70. package/dist/utils/ast.d.ts +21 -0
  71. package/dist/utils/ast.d.ts.map +1 -0
  72. package/dist/utils/frontmatter.d.ts +24 -0
  73. package/dist/utils/frontmatter.d.ts.map +1 -0
  74. package/dist/utils/kind.d.ts +41 -0
  75. package/dist/utils/kind.d.ts.map +1 -0
  76. package/dist/utils/links.d.ts +23 -0
  77. package/dist/utils/links.d.ts.map +1 -0
  78. package/dist/utils/references.d.ts +7 -0
  79. package/dist/utils/references.d.ts.map +1 -0
  80. package/dist/utils/skill-file.d.ts +32 -0
  81. package/dist/utils/skill-file.d.ts.map +1 -0
  82. package/dist/utils/skill-fs.d.ts +30 -0
  83. package/dist/utils/skill-fs.d.ts.map +1 -0
  84. package/docs/check-refs.md +60 -0
  85. package/docs/rules/body-max-length.md +23 -0
  86. package/docs/rules/description-has-trigger.md +28 -0
  87. package/docs/rules/description-length.md +27 -0
  88. package/docs/rules/description-no-html.md +22 -0
  89. package/docs/rules/descriptive-reference-filenames.md +26 -0
  90. package/docs/rules/h1-matches-name.md +36 -0
  91. package/docs/rules/large-reference-toc.md +26 -0
  92. package/docs/rules/meta-prefers-dispatch-table.md +30 -0
  93. package/docs/rules/multi-domain-has-variants.md +26 -0
  94. package/docs/rules/name-kebab-case.md +31 -0
  95. package/docs/rules/name-matches-dir.md +29 -0
  96. package/docs/rules/no-broken-relative-links.md +25 -0
  97. package/docs/rules/no-multiple-h1.md +31 -0
  98. package/docs/rules/no-orphan-references.md +25 -0
  99. package/docs/rules/no-unknown-frontmatter-keys.md +37 -0
  100. package/docs/rules/procedural-has-steps.md +43 -0
  101. package/docs/rules/reference-has-content.md +26 -0
  102. package/docs/rules/refs-one-level-deep.md +26 -0
  103. package/docs/rules/require-frontmatter.md +34 -0
  104. package/docs/rules/valid-allowed-tools.md +27 -0
  105. package/package.json +55 -3
@@ -0,0 +1,28 @@
1
+ # skill/description-has-trigger
2
+
3
+ Require the `description` to express *when* the skill applies, not just what it is.
4
+
5
+ An agent routes to a skill purely from its description. A description that only states what the skill does, with no cue about when to reach for it, routes poorly. This rule checks for a trigger phrase such as "use when", "when the user", or "trigger". It is a `warn` by default because the phrasing is heuristic.
6
+
7
+ ## Fails
8
+
9
+ ```yaml
10
+ description: A tool that formats code nicely.
11
+ ```
12
+
13
+ ## Passes
14
+
15
+ ```yaml
16
+ description: Formats code. Use when the user asks to format a file.
17
+ description: Runs the deploy. Trigger on deploy requests.
18
+ ```
19
+
20
+ ## Options
21
+
22
+ | Option | Type | Default | Description |
23
+ | --- | --- | --- | --- |
24
+ | `patterns` | string[] | `["use when", "use this when", "use for", "used when", "used for", "when the user", "when you", "when asked", "trigger", "invoke", "for when"]` | Case-insensitive substrings; the description must contain at least one. |
25
+
26
+ ```js
27
+ "skill/description-has-trigger": ["warn", { patterns: ["use when", "trigger"] }]
28
+ ```
@@ -0,0 +1,27 @@
1
+ # skill/description-length
2
+
3
+ Cap the length of the skill `description`.
4
+
5
+ The description is loaded into context for every routing decision. A bloated description wastes tokens and buries the signal that tells an agent when to use the skill. The default ceiling of 1024 characters mirrors the documented frontmatter budget.
6
+
7
+ ## Fails
8
+
9
+ ```yaml
10
+ description: <a description longer than the configured maximum…>
11
+ ```
12
+
13
+ ## Passes
14
+
15
+ ```yaml
16
+ description: Formats code. Use when the user asks to format a file.
17
+ ```
18
+
19
+ ## Options
20
+
21
+ | Option | Type | Default | Description |
22
+ | --- | --- | --- | --- |
23
+ | `max` | integer ≥ 1 | `1024` | Maximum allowed character count. |
24
+
25
+ ```js
26
+ "skill/description-length": ["error", { max: 800 }]
27
+ ```
@@ -0,0 +1,22 @@
1
+ # skill/description-no-html
2
+
3
+ Disallow HTML/XML tags in the skill `description`.
4
+
5
+ Descriptions are consumed as plain routing text. Embedded tags such as `<b>` or `<role>` add noise and can interfere with how the description is parsed and displayed. Plain comparisons like `3 < 5` are fine — the rule only flags actual tags (`<` immediately followed by a letter or `/`).
6
+
7
+ ## Fails
8
+
9
+ ```yaml
10
+ description: Uses <b>bold</b> text. Use when needed.
11
+ description: Self-closing <br/> tag.
12
+ ```
13
+
14
+ ## Passes
15
+
16
+ ```yaml
17
+ description: Compares values where 3 < 5. Use when needed.
18
+ ```
19
+
20
+ ## Options
21
+
22
+ None.
@@ -0,0 +1,26 @@
1
+ # skill/descriptive-reference-filenames
2
+
3
+ Require reference files (`.md` under `references/`) to have descriptive names.
4
+
5
+ Claude decides whether to open a reference file partly from its filename, so a generic name like `doc2.md` or `untitled.md` carries no signal. Official guidance: name files descriptively — `form_validation_rules.md`, not `doc2.md`.
6
+
7
+ Flags generic stems (`doc`, `ref`, `reference`, `file`, `page`, `untitled`, `temp`, `tmp`, `new`, `copy`, `note(s)`, `document`, optionally numbered) and bare numeric names (`1.md`).
8
+
9
+ ## Fails
10
+
11
+ ```
12
+ references/doc2.md
13
+ references/untitled.md
14
+ references/1.md
15
+ ```
16
+
17
+ ## Passes
18
+
19
+ ```
20
+ references/form_validation_rules.md
21
+ references/ga4-setup.md
22
+ ```
23
+
24
+ ## Options
25
+
26
+ None.
@@ -0,0 +1,36 @@
1
+ # skill/h1-matches-name
2
+
3
+ Require the body H1 to correspond to the skill `name`.
4
+
5
+ > **Opt-in.** This rule is not part of the `recommended` or `strict` presets. The standard skill format does not require an H1, and the frontmatter `name` is a display label rather than the body title, so this convention is left to teams that deliberately adopt it.
6
+
7
+ The title a reader sees should match the skill's identity. The comparison is normalized — lowercased, with runs of non-alphanumeric characters treated as spaces — so both `# my-skill` and `# My Skill` satisfy `name: my-skill`. Enable it explicitly:
8
+
9
+ ```js
10
+ "skill/h1-matches-name": "warn"
11
+ ```
12
+
13
+ ## Fails
14
+
15
+ ```yaml
16
+ name: my-skill
17
+ ```
18
+ ```md
19
+ # Totally Different Title
20
+ ```
21
+
22
+ ## Passes
23
+
24
+ ```yaml
25
+ name: my-skill
26
+ ```
27
+ ```md
28
+ # my-skill
29
+ ```
30
+ ```md
31
+ # My Skill
32
+ ```
33
+
34
+ ## Options
35
+
36
+ None.
@@ -0,0 +1,26 @@
1
+ # skill/large-reference-toc
2
+
3
+ Require large reference files to include a table of contents.
4
+
5
+ Official guidance: reference files over ~300 lines should carry a table of contents so Claude can jump to the relevant section instead of reading (or partially reading) the whole file.
6
+
7
+ A file satisfies the rule if it has a `Contents` / `Table of Contents` heading **or** a cluster of in-page anchor links (≥ 3 links to `#...`).
8
+
9
+ ## Fails
10
+
11
+ A `references/*.md` file longer than `maxLines` with no ToC heading and fewer than 3 anchor links.
12
+
13
+ ## Passes
14
+
15
+ - A reference file at or under `maxLines`.
16
+ - A large file with a `## Contents` heading or an anchor-link ToC.
17
+
18
+ ## Options
19
+
20
+ | Option | Type | Default | Description |
21
+ | --- | --- | --- | --- |
22
+ | `maxLines` | integer ≥ 1 | `300` | Line count above which a ToC is required. |
23
+
24
+ ```js
25
+ "skill/large-reference-toc": ["warn", { maxLines: 250 }]
26
+ ```
@@ -0,0 +1,30 @@
1
+ # skill/meta-prefers-dispatch-table
2
+
3
+ Suggest a markdown table for a `meta`-kind skill's dispatch/routing list.
4
+
5
+ > **Opt-in, and evidence-lighter than the plugin's other rules.** Not part of any preset. Backed by a single empirical test — see [`docs/research/meta-dispatch-table-vs-prose.md`](../research/meta-dispatch-table-vs-prose.md) — not the multiply-corroborated evidence behind most other rules here.
6
+
7
+ This rule only fires when the skill declares itself explicitly:
8
+
9
+ ```yaml
10
+ metadata:
11
+ kind: meta
12
+ ```
13
+
14
+ A meta (router) skill exists to dispatch requests to other skills. A one-scenario test (5 fictional skills, a fictional router, 6 blind trials) found a markdown table cost **~21% fewer tokens** than equivalent prose for this kind of dispatch content, with no measured routing-accuracy difference. This rule only checks whether the body contains a markdown table at all — it does not try to judge whether a table is semantically "needed," to avoid the false-positive risk of guessing at content meaning.
15
+
16
+ ## Fails
17
+
18
+ A `metadata.kind: meta` skill whose body has no markdown table anywhere.
19
+
20
+ ## Passes
21
+
22
+ A `metadata.kind: meta` skill whose body contains at least one markdown table.
23
+
24
+ ## Options
25
+
26
+ None.
27
+
28
+ ## Why this rule is opt-in
29
+
30
+ Every other kind-specific rule in this plugin traces to either an official, multiply-corroborated source or a comprehension test with an adversarial, multi-trial design. This one rests on a single router scenario in one domain pairing — real signal, but not yet replicated. Treat it as a cost-saving suggestion worth trying, not an established convention.
@@ -0,0 +1,26 @@
1
+ # skill/multi-domain-has-variants
2
+
3
+ Expect a `multi-domain`-kind skill to carry at least two variant reference files.
4
+
5
+ > **Opt-in.** Not part of any preset — see [Skill kinds](../design/skill-taxonomy-and-rules.md) for the taxonomy this rule is part of.
6
+
7
+ This rule only fires when the skill declares itself explicitly:
8
+
9
+ ```yaml
10
+ metadata:
11
+ kind: multi-domain
12
+ ```
13
+
14
+ The multi-domain pattern (`SKILL.md` holding selection logic over `references/{aws,gcp,azure}.md`) only earns its complexity when there is more than one variant to select between. Fewer than two reference files means there's nothing to select — it's just a `reference` skill.
15
+
16
+ ## Fails
17
+
18
+ A skill with `metadata.kind: multi-domain` and fewer than 2 files under `references/`.
19
+
20
+ ## Passes
21
+
22
+ A skill with `metadata.kind: multi-domain` and 2 or more files under `references/` (e.g. `aws.md`, `gcp.md`).
23
+
24
+ ## Options
25
+
26
+ None.
@@ -0,0 +1,31 @@
1
+ # skill/name-kebab-case
2
+
3
+ Require the skill `name` to be a valid Agent Skills identifier:
4
+
5
+ - **Kebab-case**, matching `^[a-z][a-z0-9]*(-[a-z0-9]+)*$` (letter-start, lowercase, no underscores, no leading/trailing/consecutive hyphens).
6
+ - **At most 64 characters** (the spec's hard limit).
7
+ - **No reserved words** — the name must not contain `anthropic` or `claude`.
8
+
9
+ Skill names double as directory names and identifiers. Keeping them lowercase-hyphenated makes them portable across filesystems and predictable to reference; the length and reserved-word limits come from the Agent Skills standard.
10
+
11
+ ## Fails
12
+
13
+ ```yaml
14
+ name: Bad_Name # underscores, uppercase
15
+ name: 1-leading # starts with a digit
16
+ name: trailing- # trailing hyphen
17
+ name: claude-helper # contains a reserved word
18
+ name: aaaaaa…(>64) # exceeds 64 characters
19
+ ```
20
+
21
+ ## Passes
22
+
23
+ ```yaml
24
+ name: my-skill
25
+ name: skill2-v2
26
+ name: a
27
+ ```
28
+
29
+ ## Options
30
+
31
+ None.
@@ -0,0 +1,29 @@
1
+ # skill/name-matches-dir
2
+
3
+ Require the skill `name` to equal the name of the directory that contains its `SKILL.md`.
4
+
5
+ Claude Code discovers a skill by its folder. If the frontmatter `name` and the folder disagree, tooling and humans reference two different identities for the same skill.
6
+
7
+ The rule is a no-op when the file has no resolvable directory (e.g. a bare `SKILL.md` with no parent folder).
8
+
9
+ ## Fails
10
+
11
+ ```
12
+ skills/my-skill/SKILL.md
13
+ ```
14
+ ```yaml
15
+ name: other-name
16
+ ```
17
+
18
+ ## Passes
19
+
20
+ ```
21
+ skills/my-skill/SKILL.md
22
+ ```
23
+ ```yaml
24
+ name: my-skill
25
+ ```
26
+
27
+ ## Options
28
+
29
+ None.
@@ -0,0 +1,25 @@
1
+ # skill/no-broken-relative-links
2
+
3
+ Flag relative links and images in the body whose target does not exist on disk, relative to the skill's directory.
4
+
5
+ Skills rely on referenced files for progressive disclosure. A dead relative link means the agent will try to open something that isn't there. Absolute URLs (`https:`, `mailto:`, `//host`) and pure anchors (`#section`) are ignored; URL fragments and query strings are stripped before the file is resolved.
6
+
7
+ This is the only rule that touches the filesystem (`node:fs`), and it is a no-op when the file has no resolvable path.
8
+
9
+ ## Fails
10
+
11
+ ```md
12
+ See [the guide](./missing.md).
13
+ ![diagram](./assets/gone.png)
14
+ ```
15
+
16
+ ## Passes
17
+
18
+ ```md
19
+ See [the guide](./reference.md).
20
+ [external](https://example.com) and [jump](#steps).
21
+ ```
22
+
23
+ ## Options
24
+
25
+ None.
@@ -0,0 +1,31 @@
1
+ # skill/no-multiple-h1
2
+
3
+ Disallow more than one top-level `#` H1 in the skill body.
4
+
5
+ The standard skill format does **not** require an H1 — the official examples use `##`-first bodies — so this rule does not demand a title. What it catches is *two or more* `#` H1s in one body, which usually means separate documents were merged or headings were over-leveled. Each H1 after the first is reported.
6
+
7
+ ## Fails
8
+
9
+ ```md
10
+ # one
11
+
12
+ # two
13
+ ```
14
+
15
+ ## Passes
16
+
17
+ ```md
18
+ # my-skill
19
+
20
+ ## a section
21
+ ```
22
+
23
+ ```md
24
+ ## Overview
25
+
26
+ No H1 at all is fine.
27
+ ```
28
+
29
+ ## Options
30
+
31
+ None.
@@ -0,0 +1,25 @@
1
+ # skill/no-orphan-references
2
+
3
+ Flag `.md` files under `references/` that nothing in `SKILL.md` links to.
4
+
5
+ Claude loads reference files by following the pointers in `SKILL.md`, so a file in `references/` that isn't linked is never read — dead weight. It's usually a leftover, a rename that broke the link, or a pointer someone forgot to add. This is the inverse of [`no-broken-relative-links`](no-broken-relative-links.md) (which flags links with no file); this flags files with no link.
6
+
7
+ Only runs when the skill has a `references/` directory on disk and a resolvable path.
8
+
9
+ ## Fails
10
+
11
+ ```
12
+ my-skill/
13
+ ├── SKILL.md # links references/setup.md only
14
+ └── references/
15
+ ├── setup.md # ok
16
+ └── leftover.md # flagged — never linked
17
+ ```
18
+
19
+ ## Passes
20
+
21
+ Every file under `references/` is linked from `SKILL.md`.
22
+
23
+ ## Options
24
+
25
+ None.
@@ -0,0 +1,37 @@
1
+ # skill/no-unknown-frontmatter-keys
2
+
3
+ Flag unknown top-level frontmatter keys on a skill.
4
+
5
+ The Agent Skills standard defines a fixed set of frontmatter fields. An unexpected key is almost always a typo (`descriptoin:`) or a misplaced field — and because the runtime silently ignores unknown keys, the mistake never takes effect and is easy to miss. This rule surfaces it.
6
+
7
+ Recognized keys: `name`, `description`, `license`, `compatibility`, `metadata`, `allowed-tools`, `when_to_use`, `disable-model-invocation`, plus the Claude Code extensions `argument-hint`, `model`, `context`.
8
+
9
+ ## Fails
10
+
11
+ ```yaml
12
+ ---
13
+ name: demo
14
+ descriptoin: a thing # typo — silently ignored by the runtime
15
+ ---
16
+ ```
17
+
18
+ ## Passes
19
+
20
+ ```yaml
21
+ ---
22
+ name: demo
23
+ description: Does a thing. Use when needed.
24
+ allowed-tools: [Bash]
25
+ disable-model-invocation: true
26
+ ---
27
+ ```
28
+
29
+ ## Options
30
+
31
+ | Option | Type | Default | Description |
32
+ | --- | --- | --- | --- |
33
+ | `allow` | string[] | `[]` | Additional frontmatter keys to permit (e.g. a house-style field). |
34
+
35
+ ```js
36
+ "skill/no-unknown-frontmatter-keys": ["error", { allow: ["audience"] }]
37
+ ```
@@ -0,0 +1,43 @@
1
+ # skill/procedural-has-steps
2
+
3
+ Expect a `procedural`-kind skill's body to contain ordered steps.
4
+
5
+ > **Opt-in.** Not part of any preset — see [Skill kinds](../design/skill-taxonomy-and-rules.md) for the taxonomy this rule is part of.
6
+
7
+ This rule only fires when the skill declares itself explicitly:
8
+
9
+ ```yaml
10
+ metadata:
11
+ kind: procedural
12
+ ```
13
+
14
+ A procedural (task/workflow) skill exists to walk the agent through a sequence of actions. A declared procedural skill whose body has no ordered (numbered) list at all is either mis-declared or missing its steps — a bullet list doesn't count, since order matters for a procedure.
15
+
16
+ ## Fails
17
+
18
+ ```yaml
19
+ metadata:
20
+ kind: procedural
21
+ ```
22
+ ```md
23
+ # demo
24
+
25
+ Just prose, no steps.
26
+ ```
27
+
28
+ ## Passes
29
+
30
+ ```yaml
31
+ metadata:
32
+ kind: procedural
33
+ ```
34
+ ```md
35
+ # demo
36
+
37
+ 1. Do the first thing.
38
+ 2. Do the second thing.
39
+ ```
40
+
41
+ ## Options
42
+
43
+ None.
@@ -0,0 +1,26 @@
1
+ # skill/reference-has-content
2
+
3
+ Expect a `reference`-kind skill to carry at least one reference file.
4
+
5
+ > **Opt-in.** Not part of any preset — see [Skill kinds](../design/skill-taxonomy-and-rules.md) for the taxonomy this rule is part of.
6
+
7
+ This rule only fires when the skill declares itself explicitly:
8
+
9
+ ```yaml
10
+ metadata:
11
+ kind: reference
12
+ ```
13
+
14
+ A reference (domain-knowledge) skill exists to supply background material on demand. A declared reference skill with an empty or missing `references/` directory has nothing to supply.
15
+
16
+ ## Fails
17
+
18
+ A skill with `metadata.kind: reference` and no `.md` files under `references/`.
19
+
20
+ ## Passes
21
+
22
+ A skill with `metadata.kind: reference` and at least one file under `references/`.
23
+
24
+ ## Options
25
+
26
+ None.
@@ -0,0 +1,26 @@
1
+ # skill/refs-one-level-deep
2
+
3
+ Require reference links in `SKILL.md` to stay one level deep.
4
+
5
+ Official guidance: every reference file should link **directly** from `SKILL.md` and live at most one directory below it. Deeply nested reference chains are an anti-pattern — when Claude reaches a file indirectly it may only partially read it (e.g. `head -100`), so it can act on incomplete information.
6
+
7
+ Only relative links/images are checked; external URLs and `#anchors` are ignored.
8
+
9
+ ## Fails
10
+
11
+ ```md
12
+ [deep](references/sub/setup.md) # more than one directory deep
13
+ [up](../shared/setup.md) # points outside the skill directory
14
+ ```
15
+
16
+ ## Passes
17
+
18
+ ```md
19
+ [ref](references/setup.md)
20
+ [sibling](./notes.md)
21
+ [external](https://example.com)
22
+ ```
23
+
24
+ ## Options
25
+
26
+ None.
@@ -0,0 +1,34 @@
1
+ # skill/require-frontmatter
2
+
3
+ Require every `SKILL.md` to begin with YAML frontmatter that defines a non-empty `name` and `description`.
4
+
5
+ These two fields are the contract of a skill: `name` identifies it and `description` is the only text an agent reads when deciding whether to load it. A skill missing either can't be routed to reliably.
6
+
7
+ ## Fails
8
+
9
+ ```md
10
+ # just a heading, no frontmatter
11
+ ```
12
+
13
+ ```md
14
+ ---
15
+ name: my-skill
16
+ ---
17
+
18
+ # my-skill
19
+ ```
20
+
21
+ ## Passes
22
+
23
+ ```md
24
+ ---
25
+ name: my-skill
26
+ description: Formats code. Use when the user asks to format a file.
27
+ ---
28
+
29
+ # my-skill
30
+ ```
31
+
32
+ ## Options
33
+
34
+ None.
@@ -0,0 +1,27 @@
1
+ # skill/valid-allowed-tools
2
+
3
+ Validate the shape of the optional `allowed-tools` frontmatter field (a Claude Code extension to the Agent Skills standard).
4
+
5
+ When present, `allowed-tools` must be a non-empty array of non-empty strings. An empty array grants the skill no tools at all — almost certainly not the intent, and easy to fix by just omitting the field. A non-array value is silently ignored by the runtime.
6
+
7
+ ## Fails
8
+
9
+ ```yaml
10
+ allowed-tools: Bash # not an array
11
+ allowed-tools: [] # empty — grants nothing
12
+ allowed-tools: [Bash, ""] # empty entry
13
+ ```
14
+
15
+ ## Passes
16
+
17
+ ```yaml
18
+ allowed-tools: [Bash, Read]
19
+ ```
20
+
21
+ ```yaml
22
+ # field omitted entirely — unrestricted
23
+ ```
24
+
25
+ ## Options
26
+
27
+ None.
package/package.json CHANGED
@@ -1,15 +1,67 @@
1
1
  {
2
2
  "name": "@weareikko/eslint-plugin-skill",
3
- "version": "0.0.1",
4
- "description": "Placeholder release to set up npm trusted publishing. The first real release is 0.1.0.",
3
+ "version": "0.1.1",
4
+ "description": "ESLint plugin that lints Claude Code Agent Skills (SKILL.md) for quality — portable to oxlint.",
5
+ "keywords": [
6
+ "eslint",
7
+ "eslintplugin",
8
+ "eslint-plugin",
9
+ "oxlint",
10
+ "claude",
11
+ "claude-code",
12
+ "skills",
13
+ "skill",
14
+ "markdown"
15
+ ],
16
+ "homepage": "https://github.com/weareikko/motto/tree/main/packages/eslint-plugin-skill#readme",
17
+ "bugs": {
18
+ "url": "https://github.com/weareikko/motto/issues"
19
+ },
5
20
  "license": "MIT",
6
21
  "author": "weareikko",
7
22
  "repository": {
8
23
  "type": "git",
9
- "url": "git+https://github.com/weareikko/motto.git",
24
+ "url": "git+ssh://git@github.com/weareikko/motto.git",
10
25
  "directory": "packages/eslint-plugin-skill"
11
26
  },
27
+ "type": "module",
28
+ "exports": {
29
+ ".": {
30
+ "types": "./dist/index.d.ts",
31
+ "default": "./dist/index.js"
32
+ },
33
+ "./dedup": {
34
+ "types": "./dist/dedup/index.d.ts",
35
+ "default": "./dist/dedup/index.js"
36
+ }
37
+ },
38
+ "main": "./dist/index.js",
39
+ "types": "./dist/index.d.ts",
40
+ "bin": {
41
+ "skill-check-refs": "dist/dedup/cli.js"
42
+ },
43
+ "files": [
44
+ "dist",
45
+ "configs",
46
+ "docs",
47
+ "README.md"
48
+ ],
12
49
  "publishConfig": {
13
50
  "access": "public"
51
+ },
52
+ "scripts": {
53
+ "build": "vite build && tsc -p tsconfig.build.json --emitDeclarationOnly && chmod +x dist/dedup/cli.js",
54
+ "typecheck": "tsc --noEmit && tsc -p tsconfig.test.json --noEmit",
55
+ "prepublishOnly": "npm run build && npm pack --dry-run"
56
+ },
57
+ "engines": {
58
+ "node": ">=20"
59
+ },
60
+ "peerDependencies": {
61
+ "eslint": ">=9.0.0"
62
+ },
63
+ "dependencies": {
64
+ "@eslint/markdown": "^8.0.3",
65
+ "yaml": "^2.9.0"
14
66
  }
15
67
  }