Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
870bdd9
T1: Bump pubspec to v0.3.0; add topics + issue_tracker; expand descri…
reidbaker May 21, 2026
5463332
DT3: description-too-long error reports char count + |HERE| cutoff ex…
reidbaker May 21, 2026
63c9a87
DT4: invalid-skill-name errors disambiguate frontmatter vs dir + suggest
reidbaker May 21, 2026
5202aac
DT5: path rule errors include resolved path, sibling did-you-mean, do…
reidbaker May 21, 2026
4f10512
DT2: --fix applies by default; --dry-run previews; --fix-apply deprec…
reidbaker May 21, 2026
95e4468
DT1: first-run with no args prints a champion-tier helpful guide
reidbaker May 21, 2026
7a82420
T2: add example/ with valid + invalid fixtures and a walkthrough README
reidbaker May 21, 2026
2bfa23a
T3: drift guard for example/valid + example/invalid
reidbaker May 21, 2026
4e745de
T8: README Recipes section — GitHub Actions + Dart-native pre-commit …
reidbaker May 21, 2026
6a5b4ac
T9: drift guard for README Recipes — parse + replay each one
reidbaker May 21, 2026
e0f0311
DT6: README Support section pointing to issues + Discussions + security
reidbaker May 21, 2026
7229083
T7: append SemVer rule-stability policy to CONTRIBUTING
reidbaker May 21, 2026
5b0ad72
T6: CHANGELOG 0.3.0 entry + 1.0.0-planned section with ### Rules subs…
reidbaker May 21, 2026
ab818a8
T10: pana CI job gates package score at >= 150
reidbaker May 22, 2026
d11af8b
T11: publish_dry_run CI job on workflow_dispatch
reidbaker May 22, 2026
e61c5d4
chore: dart fix + format pass over v0.3-prep changes
reidbaker May 22, 2026
435ade1
T4: RULES.md — rule contract for every shipped built-in
reidbaker May 22, 2026
94c46c6
T5: pin RULES.md to RuleRegistry with a four-invariant consistency test
reidbaker May 22, 2026
8d78cfd
Merge origin/main into v0.3-prep
reidbaker May 22, 2026
9810425
Merge remote-tracking branch 'origin/main' into v0.3-prep
reidbaker May 22, 2026
863733a
Dedupe path-rule regexes; drop unspec'd docs URL from diagnostics
reidbaker May 22, 2026
807c868
Extract Levenshtein into its own file; use dart:math.min
reidbaker May 22, 2026
84807e9
relative paths: path-preserving suggestion, dir filter, safe listSync
reidbaker May 22, 2026
27af06b
Add direct unit tests for findSiblingSuggestion
reidbaker May 22, 2026
d9d71f1
Rename name-format helper from _err to _buildNameFormatError
reidbaker May 22, 2026
bd19cab
Share max-length diagnostic between description and compatibility
reidbaker May 22, 2026
75d153a
config_parser: type-check directory entries instead of unchecked cast
reidbaker May 22, 2026
1ddde73
entry_point: tighten --fix wording, drop unused help on hidden alias
reidbaker May 22, 2026
00378ed
Strip internal task IDs from test docstrings
reidbaker May 22, 2026
176d176
Simplify recipe_drift_test; move parsing into a helper class
reidbaker May 22, 2026
11b46b8
Workflow: drop publish_dry_run; collapse pana to --exit-code-threshold
reidbaker May 22, 2026
ca9eccf
Bump to 0.3.1 with a consumer-facing CHANGELOG entry
reidbaker May 22, 2026
4344891
README: trim test-code section, Support section, and spec duplication
reidbaker May 22, 2026
6ca76ab
README: 'have an agent set it up for you' recipe
reidbaker May 22, 2026
d0ddba3
RULES.md: drop tunable-internals; sync diagnostic shapes to current code
reidbaker May 22, 2026
d80f8e6
Skills: split setup vs validation cleanly; drop README/RULES.md dupes
reidbaker May 22, 2026
277bcc0
CONTRIBUTING: move 'embedding the linter in tests' here from README
reidbaker May 22, 2026
3a11652
Workflow: tighten pana gate from threshold 10 to threshold 0
reidbaker May 22, 2026
a2c7129
relative_paths_test: platform-agnostic 'resolved to' assertion
reidbaker May 22, 2026
cd79b8c
recipe_drift_test: comment each regex with what it matches
reidbaker May 22, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions .github/workflows/dart_skills_lint_workflow.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -91,3 +91,22 @@ jobs:
- run: dart pub get

- run: dart format --output=none --set-exit-if-changed .

pana_score:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: dart-lang/setup-dart@v1
with:
sdk: stable

- run: dart pub get

- name: Install pana
run: dart pub global activate pana

# pana --exit-code-threshold N fails the step when
# (max - granted) > N. Threshold 0 means any point drop fails;
# current package score is 160/160 and we want to keep it there.
- name: Pana score gate (160/160 required)
run: dart pub global run pana --no-warning --exit-code-threshold 0 .
29 changes: 29 additions & 0 deletions tool/dart_skills_lint/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,32 @@
## 0.3.1

- `--fix` now writes fixes to disk; pair with `--dry-run`
(`--fix --dry-run`) to preview the proposed diff without writing.
The legacy `--fix-apply` flag still works but is deprecated and
emits a notice on stderr.
- Running the CLI with no arguments and no `.claude/skills` or
`.agents/skills` directory present now prints a short onboarding
guide explaining how to point the linter at a skill or a skills
root.
- `description-too-long` errors now report the actual character
count and show an excerpt with a `|HERE|` marker at the cutoff
so authors can see exactly where the text went over. The same
diagnostic shape is now used for the `compatibility` field's
500-character limit.
- `invalid-skill-name` errors now disambiguate the frontmatter
`name:` field from the parent directory name, quote the offending
value, and suggest a normalized form. The directory-mismatch
error offers both directions of the fix (edit the field or
rename the directory).
- `check-relative-paths` errors now include the resolved absolute
path and, when a near-miss filename exists in the same
directory, surface a `Did you mean "..."?` suggestion that
preserves the link's directory prefix.
- New `example/` directory with reference `valid` and `invalid`
skill fixtures and a walkthrough.
- New "Recipes" section in `README.md` with copy-pasteable GitHub
Actions and pre-commit hook integrations.

## 0.3.0

- Exposed `ConfigParser.loadConfig()` API to load configuration files programmatically.
Expand Down
67 changes: 67 additions & 0 deletions tool/dart_skills_lint/CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,33 @@ you edit an existing file, you shouldn't update the year.
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.

## Embedding the linter in tests

If your project already uses `dart_skills_lint`, you can also call it
from your own test suite — handy when you want skill validation to fail
the same Dart-test pipeline that already gates the rest of your code:

```dart
import 'package:dart_skills_lint/dart_skills_lint.dart';
import 'package:test/test.dart';

void main() {
test('Run skills linter', () async {
// Load whatever's in dart_skills_lint.yaml so the CLI and tests
// share configuration. Pass `customRules: [...]` to inject any
// custom SkillRule implementations.
final config = await ConfigParser.loadConfig();
await validateSkills(config: config);
});
}
```

`Validator` and `ValidationResult` are also exposed for tests that
need to inspect errors programmatically. Custom rule authoring lives
in the
[`dart-skills-lint-validation`](skills/dart-skills-lint-validation/SKILL.md)
skill.

## Testing and coverage

Run the test suite from the package root (`tool/dart_skills_lint`):
Expand Down Expand Up @@ -78,3 +105,43 @@ This project follows

We pledge to maintain an open and welcoming environment. For details, see our
[code of conduct](https://dart.dev/code-of-conduct).

## Rule-stability policy (SemVer)

Lint rules are part of `dart_skills_lint`'s public API. Adopters wire
the linter into pre-commit hooks and CI gates, so a rule that silently
flips from "warning" to "error" can break a downstream build with no
code change of their own. We version rule changes the same way we
version code changes:

- **Patch release (`0.3.X` → `0.3.X+1`, `1.0.X` → `1.0.X+1`)** —
bug fixes to existing rules, including diagnostic message
rewording, internal refactors, and fixes that *narrow* what a rule
matches (fewer false positives). The set of error states a passing
skill needs to clear does not grow.

- **Minor release (`0.3.X` → `0.4.0`, `1.0.X` → `1.1.0`)** — new
rules, **shipping with `defaultSeverity: AnalysisSeverity.disabled`**
so existing skills keep passing. Adopters opt in by enabling the
rule via flag or YAML config. Performance improvements that don't
change diagnostics also land here. A rule's diagnostic message may
expand to include additional context.

- **Major release (`0.X` → `1.0`, `1.X` → `2.0`)** — any change that
can fail a previously-passing skill: removing a rule (so configs
referencing it stop working), upgrading a rule's default severity
(`disabled → warning`, `warning → error`), broadening what a rule
matches (more true positives = more failures), or renaming a rule.
Releases bump the major version and the CHANGELOG calls out the
exact rules affected.

Rationale: adopters should be able to set `dart_skills_lint: ^1.0.0`
in `pubspec.yaml` and trust that a `dart pub upgrade` never turns
green CI red without their consent. Surprises belong in major
releases, and only there.

If you're proposing a change that doesn't fit cleanly into one of the
buckets above, say so on the PR and the maintainers will decide where
it lands. New built-in rules **must** include a `## <rule-name>`
entry in `RULES.md` describing default severity and behavior — see
the existing entries for the expected shape.
197 changes: 94 additions & 103 deletions tool/dart_skills_lint/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,10 @@ A static analysis linter for Agent Skills to ensure they meet the specification
- [Overview](#overview)
- [Installation](#installation)
- [Usage](#usage)
- [CLI vs Dart Test](#cli-vs-dart-test)
- [Rule Precedence](#rule-precedence)
- [Configuration](#configuration)
- [Specification Validation](#specification-validation)
- [Best Practices](#best-practices)
- [Recipes](#recipes)
- [Contributing](#contributing)

## Overview

Expand Down Expand Up @@ -47,23 +46,11 @@ dart pub global activate dart_skills_lint

## Usage

There are three ways to interact with `dart_skills_lint`.

### CLI vs Dart Test

Depending on your workflow, you should choose the appropriate interaction mode:

* **Use the CLI when:**
* **Ad-hoc Validation**: You want to quickly check a specific skill you are working on without running the entire test suite.
* **Baseline Generation**: You are integrating the tool into a legacy repo and need to generate an ignore file (`--generate-baseline`).
* **Automated Fixes**: You want to preview or apply fixes (`--fix`, `--fix-apply`) directly to the files.
* **Pre-commit Hooks**: You want a fast, isolated check in a Git pre-commit hook.
* **Use Dart Test when:**
* **CI/CD Integration**: You want to guarantee that no invalid skills are merged by failing the build alongside unit tests.
* **Programmatic Configuration**: You need to inject custom rules or dynamic configurations that are hard to express in static YAML.
* **Ecosystem Consistency**: You want developers to rely on the familiar `dart test` command rather than learning a new tool invocation.

---
`dart_skills_lint` runs as a command-line tool, configured by flags or by
a `dart_skills_lint.yaml` file. The CLI is the user-facing surface; it
also has a programmatic API for contributors who need to embed the
linter in their own test suite — see
[`CONTRIBUTING.md`](CONTRIBUTING.md#embedding-the-linter-in-tests).

### 1. As a Command Line Tool with Arguments
Run the linter against your skills or root skills directories by passing arguments.
Expand Down Expand Up @@ -92,8 +79,9 @@ If no directory is specified, it automatically checks `.claude/skills` and `.age
- `--fast-fail`: Halt execution immediately on the error.
- `--ignore-config`: Ignore the YAML configuration file entirely.
- `--[no-]check-trailing-whitespace`: Enable/disable checking for trailing whitespace. (Disabled by default).
- `--fix`: Preview fixes for failing lints (dry run).
- `--fix-apply`: Apply fixes for failing lints.
- `--fix`: Write fixes for failing lints to disk.
- `--dry-run`: When combined with `--fix`, prints the proposed diff without writing.
- `--fix-apply`: *Deprecated* alias for `--fix`. Prints a deprecation notice on use.

### 2. As a Command Line Tool with a YAML Configuration File
You can configure the linter using a configuration file (defaulting to `dart_skills_lint.yaml` in the current directory).
Expand Down Expand Up @@ -129,104 +117,107 @@ This ensures that you can always override configuration file settings for a spec

---

### 3. As Dart Test Code
You can integrate the linter into your automated tests by importing the package and calling `validateSkills`. This allows you to enforce skill validity as part of your standard test suite.

Example `test/lint_skills_test.dart`:
```dart
import 'package:dart_skills_lint/dart_skills_lint.dart';
import 'package:test/test.dart';

void main() {
test('Run skills linter', () async {
final config = Configuration(
directoryConfigs: [
DirectoryConfig(
path: '../../skills',
rules: {},
ignoreFile: '.agents/skills/flutter_skills_ignore.json',
),
],
);

await validateSkills(
skillDirPaths: ['../../skills'],
resolvedRules: {
'check-relative-paths': AnalysisSeverity.error,
'check-absolute-paths': AnalysisSeverity.error,
},
config: config,
);
});
}
```
### 3. Custom Rules

You can also use `Validator` and `ValidationResult` directly if you need to inspect the errors programmatically.
Custom rule authoring lives in the
[`dart-skills-lint-validation`](skills/dart-skills-lint-validation/SKILL.md)
skill — that skill walks through extending `SkillRule` and passing the
rule into the linter.

### Custom Rules
## Specification Validation

You can author custom rules by extending the `SkillRule` class and passing them to `validateSkills` or the `Validator` constructor.
The linter checks each skill against the spec at
[`documentation/knowledge/SPECIFICATION.md`](documentation/knowledge/SPECIFICATION.md).
For the full list of built-in rules — default severities, exact
diagnostic shapes, auto-fix behavior, and how to disable each — see
[`RULES.md`](RULES.md).

Example custom rule:
```dart
import 'package:dart_skills_lint/dart_skills_lint.dart';
## Recipes

class MyCustomRule extends SkillRule {
@override
final String name = 'my-custom-rule';
Drop-in snippets for the two most common ways to wire `dart_skills_lint`
into a project's quality gates. Each recipe is exercised by
[`test/recipe_drift_test.dart`](test/recipe_drift_test.dart), so if a
Comment thread
reidbaker marked this conversation as resolved.
flag here goes stale, CI fails.

@override
final AnalysisSeverity severity = AnalysisSeverity.warning;
### Recipe: GitHub Actions
Comment thread
reidbaker marked this conversation as resolved.

@override
Future<List<ValidationError>> validate(SkillContext context) async {
final errors = <ValidationError>[];
final yaml = context.parsedYaml;
if (yaml == null) return errors;
Save the following as `.github/workflows/lint-skills.yml`. It runs on
every push and PR, installs `dart_skills_lint` globally on the runner,
and validates every skill under `.claude/skills/`. Adjust the path to
match where your skills live.

if (yaml['metadata']?['deprecated'] == true) {
errors.add(ValidationError(
ruleId: name,
severity: severity,
file: 'SKILL.md',
message: 'This skill is marked as deprecated.',
));
}
return errors;
}
}
```yaml
# .github/workflows/lint-skills.yml
name: Lint Agent Skills
on:
push:
branches: [main]
pull_request:

permissions: read-all

jobs:
lint-skills:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: dart-lang/setup-dart@v1
- run: dart pub global activate dart_skills_lint
- run: dart pub global run dart_skills_lint --skills-directory ./.claude/skills
```

Then use it in your test:
```dart
await validateSkills(
skillDirPaths: ['../../skills'],
customRules: [MyCustomRule()],
);
To validate a single skill directory instead, swap the last step:

```yaml
- run: dart pub global run dart_skills_lint --skill ./.claude/skills/my-skill
```

## Specification Validation
### Recipe: Dart-native pre-commit hook

The linter checks against the criteria defined in `documentation/knowledge/SPECIFICATION.md` (Section 5.1). Key checks include:
A pre-commit hook that calls into the linter directly — no Husky, no
Python `pre-commit` framework, just Dart and the existing
`dart pub global` tooling.

### 1. Directory and File Structure
- Path existence and directory verification.
- Mandatory `SKILL.md` file at the root.
- Directories starting with a dot `.` (e.g., `.dart_tool`) are ignored when scanning for skills.
Activate the linter once per machine:

```bash
dart pub global activate dart_skills_lint
```

### 2. Metadata (YAML Frontmatter)
- Valid YAML syntax.
- Allowed fields: `name`, `description`, `license`, `allowed-tools`, `metadata`, `compatibility`, `category`, `tags`, `version`, `eval_task`.
- Required fields: `name` and `description`.
Then install the hook into the repository (run from the repo root):

### 3. Field Specific Constraints
- **Skill Name (`name`)**: Max 64 characters, lowercase alphanumeric and hyphens only, no leading/trailing/consecutive hyphens. **Must match the parent directory name.**
- **Description (`description`)**: Max 1024 characters.
- **Compatibility (`compatibility`)**: Max 500 characters.
```bash
cat > .git/hooks/pre-commit <<'HOOK'
#!/bin/sh
set -e
# Lint every skill under .claude/skills before each commit.
# Add --skill arguments for other locations as needed.
exec dart pub global run dart_skills_lint --skills-directory ./.claude/skills --quiet
HOOK
chmod +x .git/hooks/pre-commit
```

### 4. Content Constraints
- **Trailing Whitespace**: Lines in `SKILL.md` should not have trailing whitespace. Exactly 2 spaces at the end of a line are allowed to support Markdown hard line breaks, per the [CommonMark Spec](https://spec.commonmark.org/0.31.2/#hard-line-breaks).
- **Path Constraints**: Checks that **inline** Markdown links do not use absolute paths to enforce portability. Can optionally be configured to check that relative paths point to valid, existing files (disabled by default). *Note: This rule only supports inline Markdown links and does not detect HTML or reference-style links.*
The hook exits non-zero on lint failure, blocking the commit. To
auto-apply fixable lints inside the hook, append `--fix` to the linter
invocation.

### Recipe: have an agent set it up for you

If you're using Claude Code, Gemini, or another agent that can read
repository-local skills, paste the following prompt to have the agent
install and validate `dart_skills_lint` for you. The agent will
follow the
[`dart-skills-lint-setup`](skills/dart-skills-lint-setup/SKILL.md)
skill for first-time wiring, then the
[`dart-skills-lint-validation`](skills/dart-skills-lint-validation/SKILL.md)
skill to run the linter and resolve any failures.

> Set up dart_skills_lint in this project. Use the skill at
> `tool/dart_skills_lint/skills/dart-skills-lint-setup/SKILL.md`
> to add it as a dev_dependency, create the configuration file,
> and wire it into CI. Then use the skill at
> `tool/dart_skills_lint/skills/dart-skills-lint-validation/SKILL.md`
> to run the linter and resolve any failures.

## Contributing

Expand Down
Loading
Loading