Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

## [1.2.1] - 2026-05-03

### Fixed

- `description.quality-score` no longer flags verb-led descriptions starting with `investigate`, `diagnose`, `triage`, `troubleshoot`, `examine`, `audit`, `inspect`, `compare`, `capture`, `normalize`, or `refactor`. Expanded `_ACTION_VERBS` from 43 to 170 entries to cover investigation, inspection, search, code-work, output, comparison, logging, and normalization clusters. Closes #2.

## [1.2.0] - 2026-04-29

Backward compatibility: previously-passing skills still pass. Some previously-failing skills now warn instead of error and produce exit code 0 instead of 1.
Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,11 @@

<img src="https://img.shields.io/pypi/v/skillcheck?style=flat-square" alt="PyPI version"> <img src="https://img.shields.io/pypi/pyversions/skillcheck?style=flat-square" alt="Python"> <img src="https://img.shields.io/github/actions/workflow/status/moonrunnerkc/skillcheck/ci.yml?style=flat-square" alt="CI status"> <img src="https://img.shields.io/github/license/moonrunnerkc/skillcheck?style=flat-square" alt="License">

**v1.2.0 · 683 tests cover all rule modules · production**
**v1.2.1 · 687 tests cover all rule modules · production**

</div>

683 tests cover all rule modules, 0 known false positives.
687 tests cover all rule modules, 0 known false positives.

---

Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ build-backend = "hatchling.build"

[project]
name = "skillcheck"
version = "1.2.0"
version = "1.2.1"
description = "Cross-agent skill quality gate for SKILL.md files conforming to the agentskills.io specification"
readme = "README.md"
license = { text = "MIT" }
Expand Down
2 changes: 1 addition & 1 deletion skills/skillcheck/SKILL.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
---
name: skillcheck
description: Validates and scores SKILL.md files against the agentskills.io specification; use when linting skills for cross-agent compatibility, description quality, or capability graph structure.
version: "1.2.0"
version: "1.2.1"
author: brad
---

Expand Down
2 changes: 1 addition & 1 deletion src/skillcheck/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
from skillcheck.parser import ParsedSkill, ParseError
from skillcheck.result import Diagnostic, Severity, ValidationResult

__version__ = "1.2.0"
__version__ = "1.2.1"

__all__ = [
"validate",
Expand Down
70 changes: 43 additions & 27 deletions src/skillcheck/rules/description.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,24 +23,40 @@
# Action-verb base forms. 3rd-person singular ("generates", "identifies") is
# matched via stem normalization in `_is_action_verb`, so we only store one
# canonical form per verb here.
#
# Excluded: "handle"/"handles" (already in _VAGUE_WORDS), "trigger"/"triggers"
# (would double-count against the trigger-phrase scorer).
_ACTION_VERBS = frozenset({
"generate", "analyze", "validate", "deploy", "process",
"create", "build", "convert", "extract", "format",
"monitor", "scan", "parse", "transform", "compile",
"test", "check", "lint", "run", "execute",
"fetch", "send", "upload", "download",
"configure", "set", "update", "install", "remove",
"detect", "identify", "classify", "score", "rank",
"summarize", "translate", "encrypt", "decrypt",
"automate", "scaffold", "provision", "migrate", "sync",
"analyze", "apply", "assemble", "audit", "automate", "benchmark", "bind", "build",
"capture", "catalog", "check", "classify", "clean", "cleanse", "clone", "cluster",
"compare", "compile", "compose", "compute", "configure", "connect", "construct", "containerize",
"convert", "copy", "correlate", "count", "cover", "create", "crop", "curate",
"decode", "decrypt", "deduplicate", "delegate", "deliver", "demonstrate", "deploy", "derive",
"describe", "detach", "detect", "develop", "diagnose", "diff", "digest", "direct",
"disable", "discover", "distribute", "document", "download", "edit", "enable", "encrypt",
"enrich", "enumerate", "evaluate", "evolve", "examine", "execute", "expand", "export",
"extend", "extract", "facilitate", "fetch", "filter", "finalize", "find", "flatten",
"format", "forward", "freeze", "gauge", "generate", "guard", "identify", "import",
"initialize", "inject", "inspect", "install", "investigate", "iterate", "join", "label",
"link", "lint", "log", "maintain", "measure", "merge", "migrate", "monitor",
"navigate", "normalize", "optimize", "orchestrate", "pair", "parse", "populate", "predict",
"present", "print", "probe", "process", "project", "protect", "provision", "publish",
"push", "query", "rank", "read", "rebuild", "refactor", "register", "release",
"remove", "render", "replace", "report", "request", "resolve", "restore", "retrieve",
"rewrite", "route", "run", "sanitize", "save", "scaffold", "scan", "score",
"screen", "search", "select", "send", "serialize", "set", "share", "shield",
"sign", "signal", "simplify", "snapshot", "sort", "stage", "store", "stream",
"structure", "summarize", "sync", "synchronize", "tag", "target", "test", "transform",
"translate", "transport", "treat", "triage", "troubleshoot", "update", "upload", "validate",
"visualize", "wrap",
})


def _is_action_verb(word: str) -> bool:
"""Match `word` against `_ACTION_VERBS`, normalizing 3rd-person singular forms.

Handles three patterns: bare base ("generate"), -s ("scans"), and -ies -y
("identifies" "identify"). The cheaper -s strip is tried first so most
Handles three patterns: bare base ("generate"), -s ("scans"), and -ies -> -y
("identifies" -> "identify"). The cheaper -s strip is tried first so most
third-person verbs resolve in one extra lookup.
"""
w = word.lower()
Expand Down Expand Up @@ -75,25 +91,25 @@ def _is_action_verb(word: str) -> bool:
# if they often appear as marketing fluff. The cost of false positives is
# high here: a real description that uses the word once gets penalized.
_VAGUE_WORDS = frozenset({
"tool", "helper", "utility", "stuff", "things", "various",
"general", "generic", "simple", "basic", "easy", "nice",
"good", "great", "awesome", "cool", "helpful", "useful",
"important", "powerful", "efficient", "effective", "handles",
"tool", "helper", "utility", "stuff", "things", "various",
"general", "generic", "simple", "basic", "easy", "nice",
"good", "great", "awesome", "cool", "helpful", "useful",
"important", "powerful", "efficient", "effective", "handles",
})

# Common stop words excluded from keyword density calculation.
_STOP_WORDS = frozenset({
"a", "an", "the", "is", "are", "was", "were", "be", "been",
"being", "have", "has", "had", "do", "does", "did", "will",
"would", "shall", "should", "may", "might", "must", "can",
"could", "of", "in", "to", "for", "with", "on", "at", "from",
"by", "about", "as", "into", "through", "during", "before",
"after", "above", "below", "between", "under", "again",
"further", "then", "once", "and", "but", "or", "nor", "not",
"so", "yet", "both", "each", "few", "more", "most", "other",
"some", "such", "no", "only", "own", "same", "than", "too",
"very", "just", "because", "if", "when", "where", "how",
"all", "any", "this", "that", "these", "those", "it", "its",
"a", "an", "the", "is", "are", "was", "were", "be", "been",
"being", "have", "has", "had", "do", "does", "did", "will",
"would", "shall", "should", "may", "might", "must", "can",
"could", "of", "in", "to", "for", "with", "on", "at", "from",
"by", "about", "as", "into", "through", "during", "before",
"after", "above", "below", "between", "under", "again",
"further", "then", "once", "and", "but", "or", "nor", "not",
"so", "yet", "both", "each", "few", "more", "most", "other",
"some", "such", "no", "only", "own", "same", "than", "too",
"very", "just", "because", "if", "when", "where", "how",
"all", "any", "this", "that", "these", "those", "it", "its",
})


Expand Down Expand Up @@ -215,7 +231,7 @@ def check_description_quality(skill: ParsedSkill) -> list[Diagnostic]:
return []
desc = skill.frontmatter.get("description")
if not desc or not isinstance(desc, str) or not desc.strip():
return [] # Missing/empty descriptions are handled by frontmatter rules.
return [] # Missing/empty descriptions are handled by frontmatter rules.

score, suggestions = score_description(desc)

Expand Down
72 changes: 72 additions & 0 deletions tests/test_description.py
Original file line number Diff line number Diff line change
Expand Up @@ -161,3 +161,75 @@ def test_third_person_verb_forms_count_via_stem_normalization():
base = "Identifies cyclic dependencies in plan graphs."
score, _ = score_description(base)
assert score >= 30, f"Expected leading verb to register, got {score}"


# ---------------------------------------------------------------------------
# Issue #2: action-verb allowlist expansion (1.0.2)
# ---------------------------------------------------------------------------

from skillcheck.rules.description import _ACTION_VERBS, _score_action_verbs


def test_newly_added_verbs_all_count_as_action_verbs():
"""Every verb added in the #2 expansion (170 total) must register as an
action verb when used as the first word of a description.
Regression: issue #2 -- "Investigate..." was falsely flagged.
"""
_ORIGINAL = frozenset({
"generate", "analyze", "validate", "deploy", "process",
"create", "build", "convert", "extract", "format",
"monitor", "scan", "parse", "transform", "compile",
"test", "check", "lint", "run", "execute",
"fetch", "send", "upload", "download",
"configure", "set", "update", "install", "remove",
"detect", "identify", "classify", "score", "rank",
"summarize", "translate", "encrypt", "decrypt",
"automate", "scaffold", "provision", "migrate", "sync",
})
newly_added = sorted(_ACTION_VERBS - _ORIGINAL)
assert len(newly_added) == 127, f"Expected 127 new verbs, got {len(newly_added)}"
for verb in newly_added:
score, suggestions = _score_action_verbs(f"{verb.title()} something.")
assert score >= 20, (
f"Verb '{verb}' should count as action verb at start "
f"(got score={score}, suggestions={suggestions})"
)
assert not any(
"action verb" in s.lower() for s in (suggestions or [])
), f"Verb '{verb}' triggered false-positive action-verb suggestion"


def test_issue_2_regression_investigate_description():
"""Issue #2: "Investigate failing GitLab CI test jobs..." must NOT trigger
the "Start the description with an action verb" suggestion.
"""
desc = (
"Investigate failing GitLab CI test jobs by fetching traces, "
"mapping failures to local files, and isolating the smallest "
"rerun only when needed before proposing a fix. Use when given "
"a failing GitLab job URL or job ID, especially for "
"dashboard-api-automation test failures."
)
score, suggestions = score_description(desc)
assert not any(
"action verb" in s.lower() for s in (suggestions or [])
), f'Issue #2 description should not trigger action-verb suggestion: {suggestions}'
assert score >= 90, f"Expected high score for issue #2 description, got {score}"


def test_negative_handles_still_excluded():
"""Handles must NOT count as action verb (deliberate exclusion:
already in _VAGUE_WORDS)."""
s, _ = _score_action_verbs("Handles X.")
assert s != 25 and s != 20, (
f"'Handles' should not count as action verb, got score={s}"
)


def test_negative_triggers_still_excluded():
"""Triggers must NOT count as action verb (deliberate exclusion:
would double-count against trigger-phrase scorer)."""
s, _ = _score_action_verbs("Triggers X.")
assert s != 25 and s != 20, (
f"'Triggers' should not count as action verb, got score={s}"
)
Loading