Metadata-Version: 2.5
Name: skill-linter
Version: 0.2.0
Summary: Validate Agent Skills repos: spec checks, shared conventions, per-repo policy, count stamping and plugin release checks
Project-URL: Repository, https://github.com/Jeffallan/skill-linter
Project-URL: Issues, https://github.com/Jeffallan/skill-linter/issues
Author: Jeff Smolinski
License-Expression: MIT
License-File: LICENSE
Requires-Python: >=3.11
Requires-Dist: pyyaml>=6
Description-Content-Type: text/markdown

# skill-linter

skill-linter checks a repo of Agent Skills before you commit or release it, and keeps the counts and version numbers in its docs current. One `[tool.skill-linter]` table in `pyproject.toml` configures it, so a new skill repo gets the same checks as the others without copying `validate-skills.py`, `validate-markdown.py` and `update-docs.py` into it.

It needs Python 3.11 or later. PyYAML is the only dependency, used for workflow YAML.

```sh
uvx skill-linter check        # run without installing
pipx install skill-linter     # or install it once
```

Status: early (0.x). Rule names and config keys may still change between minor versions.

## Commands

```text
skill-linter check   [--only GROUP[,GROUP]] [--skill NAME] [--format text|json] [--root PATH]
skill-linter stamp   [--check] [--dry-run] [--root PATH]
skill-linter release [--format text|json] [--root PATH]
skill-linter rules   [--format text|json]
```

- `check` runs every enabled rule except the release checks.
- `stamp` counts skills, references, workflows and domains, then writes those counts and the version from `version.json` into the files you list. `--check` writes nothing and exits 1 when anything is stale.
- `release` checks the tree the plugin directory will read (`plugin_root` or a `ship` list), then runs `check`.
- `rules` lists every rule with its group, layer and severity after your overrides.

`skill-linter --version` prints the installed version. Exit codes: 0 when there are no errors, 1 when there are errors or stale stamps, 2 for a usage or config error. `python -m skill_linter` runs the same CLI.

## Rules

Rules come in three layers:

1. The [Agent Skills specification](https://agentskills.io/specification) (`SPEC-01` to `SPEC-10`). You can't switch these off.
2. Conventions shared across the skill repos: frontmatter metadata, the description rule, Markdown lint, workflow definitions, hidden conventions such as the docs backlink and title safety, and the changelog format, and the plugin-directory release checks (`CONV`, `DESC`, `MD`, `WF`, `H`, `CL`, `P`). Each one can be switched off or lowered to a warning.
3. Per-repo policy (`POL`, `LINK`, and your own `X-` rules). A policy rule runs only once you give it a value.

Every rule has an ID and a name, such as `POL-03` and `section-order`. Output shows both, and the config accepts either. Run `skill-linter rules` for the full list.

### Cross-skill references

A skill can cite a file in a sibling skill or in a shared folder, written from the skill root: `` `../vue-expert/references/components.md` `` or `` `../_shared/markup-convention.md` ``. The spec wants every reference to stay inside the skill folder, so `SPEC-10` (`reference-depth`) leaves these paths to two other rules:

- `CONV-15` (`cross-skill-references`) checks that the file exists, that the folder is a skill or is listed in `shared_dirs`, and that a cited skill appears in `metadata.related-skills` (a warning when it doesn't).
- `P17` (`cross-skill-targets-ship`) checks that the cited file ships with the plugin. A skill installed on its own won't find its sibling, so these references only work when the whole plugin is installed.

## Configuration

Everything lives under `[tool.skill-linter]` in the repo's `pyproject.toml`. Unknown keys and unknown rule IDs are errors, so a typo can't switch a check off without anyone noticing. `tests/adoption/` holds complete configs for claude-skills and writing-with-agents.

```toml
[tool.skill-linter]
skills_dir = "plugin/skills"
shared_dirs = ["_shared"]
commands_dir = "plugin/commands"
plugin_root = "plugin"
docs_url = "https://example.github.io/my-skills/skills/{domain}/{name}/"

[tool.skill-linter.policy]
workflow_steps = [5, 7]
license = "MIT"

[tool.skill-linter.severity]
plugin-icon = "off"   # or P10

[[tool.skill-linter.rules.custom]]
id = "X-lead-support"
target = "body"
pattern = '(?is)\blead\b.*\bsupport\b'
severity = "warning"
message = "Name a lead and a support role"
```

The changelog rules (`CL-01` to `CL-05`) expect Keep a Changelog headings, the six standard sections in order with `### Contributors` last, an `[Unreleased]` section, and compare links. `[tool.skill-linter.changelog]` takes `path`, `extra_sections` and `since`, the first version they apply to, so older entries can stay as published.

Custom rules are regular expressions applied to a frontmatter field, a section, the last line, the body or the reference files. Config never runs code.

### Stamping

Each `[[tool.skill-linter.stamp.files]]` entry names a file and how to update it:

```toml
[[tool.skill-linter.stamp.files]]
path = "README.md"   # .md, .mdx and .html files get marker stamping by default
patterns = [
  { key = "version", regex = 'version-([\d.]+)-blue\.svg' },
  { key = "referenceFileCount.{skill}", regex = '(?m)^\s+(?P<skill>[\w-]+)/\s+\(SKILL\.md \+ (?P<count>\d+) reference(?P<s>s?)\)' },
]

[[tool.skill-linter.stamp.files]]
path = ".claude-plugin/marketplace.json"
json = { "/metadata/version" = "version", "/plugins/0/version" = "version" }
patterns = [{ key = "skillCount", regex = '(\d+) specialized skills' }]
```

- Markers: `<!-- SKILL_COUNT -->67<!-- /SKILL_COUNT -->`, and likewise `WORKFLOW_COUNT`, `REFERENCE_COUNT`, `DOMAIN_COUNT` and `VERSION`. `SKILL_COUNT:<domain>` counts one domain and `REFERENCE_COUNT:<skill>` counts one skill's references.
- `json` maps a JSON Pointer to a key and replaces only that value, leaving the file's formatting alone.
- `patterns` replace the one group in each regex. A pattern that matches nothing is an error, which catches a sentence reworded after its pattern was written.
- The key `referenceFileCount.{skill}` stamps a count per skill, for places where an HTML comment would show up as text, such as a file tree inside a code block. Its regex names the skill and count groups. The optional `s` group becomes `s` unless the count is 1, and a line that names no skill is an error.

## Development

```sh
uv sync
uv run pytest
uv run ruff check . && uv run ruff format --check .
```

The adoption tests export pinned commits of the two skill repos with `git archive` and skip when those checkouts aren't present. One of them runs each repo's own `update-docs.py` and `skill-linter stamp` on the same change and checks that every file comes out identical.
