Development
Architecture
src/skillseal/
├── models.py # pydantic models: Skill, Finding, SkillReport, routing models
├── parser.py # discover_skills(), parse_skill() — never raises on bad YAML
├── linter.py # ties parser + rules + scoring together
├── scoring.py # deterministic 0-100 scoring
├── config.py # skillseal.toml: threshold overrides
├── conflicts.py # cross-skill: duplicate names, routing-overlap (Jaccard)
├── diff.py # score/finding delta between two versions of a skill
├── rules/
│ ├── base.py # Rule protocol, FuncRule, registry, text helpers
│ ├── metadata.py # SPECIFICATION rules
│ ├── quality.py # QUALITY rules
│ ├── security.py # SECURITY rules
│ └── portability.py # PORTABILITY rules
├── routing/
│ ├── evaluator.py # HeuristicRoutingEvaluator, LLMRoutingEvaluator, LLMProvider
│ └── runner.py # loads skillseal.yaml, runs cases
├── reporters/
│ ├── terminal.py # Rich terminal output
│ └── json_reporter.py # stable JSON schema
└── cli.py # typer app: check, test, conflicts, diff
A Rule is id, category, severity, description, and
check(skill, config) -> list[Finding]. Most rules are built with
FuncRule, which wraps a plain function so adding a check doesn't require a
new class — see AGENTS.md
for the exact steps to add one.
Routing evaluation is behind a RoutingEvaluator protocol with two
implementations:
HeuristicRoutingEvaluator(default): fully offline, no API key needed. Scores how much of a prompt's distinctive vocabulary (after stopword removal and light suffix stripping) is covered by the skill's own name, description, andkeywords:. It's deliberately simple — not real NLP — which is also why it's fast, free, and explainable ("Matched terms: ...").LLMRoutingEvaluator: delegates the trigger/no-trigger decision to anLLMProvider(complete(prompt) -> str).OpenAICompatibleProviderimplements this against any OpenAI-compatible/chat/completionsendpoint, configured viaSKILLSEAL_BASE_URL,SKILLSEAL_API_KEY, andSKILLSEAL_MODEL. Use--provider llmto opt in — it's never required.
Quality gates
Before committing:
uv run pytest -q
uv run ruff check .
uv run ruff format --check .
uv run mypy src
These are exactly what CI runs, plus a smoke test against examples/.
Releasing
Publishing is automatic — there is no manual tagging step for a normal release:
- Bump
versioninpyproject.tomland commit it tomain(as its own commit or as part of a larger one). - Once CI passes on that commit,
auto-release.ymlnoticespyproject.toml's version has no matching git tag yet, createsvX.Y.Z, and pushes it. - That same workflow then dispatches
release.ymlvia the API (gh workflow run, i.e.workflow_dispatch) — deliberately not by relying on the tag push to re-trigger it (a tag pushed with the defaultGITHUB_TOKENdoesn't trigger other workflows) and not viaworkflow_call(PyPI Trusted Publishing explicitly rejects the OIDC exchange for reusable-workflow invocations — this broke a real release before landing onworkflow_dispatchinstead).release.ymlthen builds the sdist and wheel, signs a SLSA build provenance attestation, publishes to PyPI via Trusted Publishing (OIDC, no stored token), and creates the GitHub Release with auto-generated notes.
Gated on CI, not run in parallel with it: auto-release.yml triggers on the
CI workflow's completion, not directly on push, so a version bump can
never publish before its own tests have actually passed.
Manual escape hatch, for re-running or backfilling a release:
git tag vX.Y.Z && git push origin vX.Y.Z
release.yml also listens for a direct tag push (its original trigger,
before automation was added), so this still works standalone.
To verify a release artifact was actually built by this repo's workflow (not hand-uploaded) before installing it:
gh attestation verify dist/skillseal-*.whl --owner pespinel
Why not a bigger tool like release-please?
Considered and skipped for now: release-please
and similar tools automate versioning and changelog generation from
Conventional Commits, but via a "Release PR" you merge to cut a release —
a workflow shape (PR-based merges to main) this repo doesn't currently
use, since most changes land as direct pushes. The lighter approach above
gets the automation the maintainer actually asked for (no manual tag/release
step) without changing how the repo is worked in day to day. Worth
revisiting if the project moves to a PR-based workflow later.