reach.lint¶
Static validation engine for skill manifests, frontmatter schemas, naming conventions, and listing budgets.
Provide static pre-flight linting for skill definitions and catalogs.
RULES
module-attribute
¶
RULES: dict[str, RuleDefinition] = {
"invalid-yaml": RuleDefinition(
rule="invalid-yaml",
default_severity=Severity.ERROR,
summary="SKILL.md contains missing or unparseable YAML frontmatter",
explanation="Agent runtimes parse frontmatter metadata to discover skills. Malformed YAML prevents the skill from being indexed or loaded.",
remedy="Ensure the file begins with '---' delimiters and contains valid YAML syntax.",
),
"missing-name": RuleDefinition(
rule="missing-name",
default_severity=Severity.ERROR,
summary="Frontmatter does not declare a skill 'name'",
explanation="A skill must have an explicit identifier for agent catalog registration and invocation dispatch.",
remedy="Add a 'name' field to the frontmatter matching the skill's directory name.",
),
"missing-description": RuleDefinition(
rule="missing-description",
default_severity=Severity.ERROR,
summary="Frontmatter has no 'description' or the description is empty",
explanation="Agent models read descriptions to determine whether a skill applies to a user query. A missing description renders the skill unselectable.",
remedy="Add a descriptive 'description' field stating what the skill does and when to use it.",
),
"invalid-name-format": RuleDefinition(
rule="invalid-name-format",
default_severity=Severity.ERROR,
summary="Skill name does not adhere to lowercase kebab-case convention",
explanation="Standard skill runtimes expect lowercase alphanumeric identifiers separated by single hyphens (max 64 chars).",
remedy="Rename the skill to use only lowercase letters, digits, and hyphens (e.g. 'git-workflow').",
),
"name-mismatch": RuleDefinition(
rule="name-mismatch",
default_severity=Severity.ERROR,
summary="Frontmatter 'name' differs from the parent directory name",
explanation="Mismatched directory and manifest names cause discovery anomalies when runtimes load skills by folder name.",
remedy="Align the frontmatter 'name' with the enclosing directory name.",
),
"duplicate-name": RuleDefinition(
rule="duplicate-name",
default_severity=Severity.ERROR,
summary="Multiple skills in the corpus declare the same name",
explanation="Duplicate skill names cause nondeterministic catalog collisions and directory shadowing.",
remedy="Rename conflicting skills so every skill in the corpus has a distinct name.",
),
"duplicate-capability": RuleDefinition(
rule="duplicate-capability",
default_severity=Severity.WARN,
summary="Skill description has high semantic overlap (> 92%) with another skill",
explanation="Descriptions with near-identical semantic vectors create ambiguous attractor basins that lead to misroutes and non-deterministic skill selection.",
remedy="Differentiate the skill descriptions by clarifying distinct trigger boundaries or consolidating redundant skills.",
),
"description-too-short": RuleDefinition(
rule="description-too-short",
default_severity=Severity.WARN,
summary="Description is too brief to provide actionable routing criteria",
explanation="Descriptions under 20 characters lack the context and trigger conditions agent models need to reliably route queries.",
remedy="Expand the description to clearly describe the skill's capabilities and trigger scenarios.",
),
"unresolved-placeholder": RuleDefinition(
rule="unresolved-placeholder",
default_severity=Severity.WARN,
summary="Description contains unresolved template placeholders",
explanation="Markers like TODO, FIXME, or <FILL_IN> in descriptions distract models and degrade selection accuracy.",
remedy="Replace template markers with concrete guidance describing actual skill capabilities.",
),
"reserved-name-collision": RuleDefinition(
rule="reserved-name-collision",
default_severity=Severity.WARN,
summary="Skill name collides with a built-in agent tool or primitive",
explanation="Naming a skill after a built-in command (e.g. 'bash', 'edit', 'read') confuses tool selection routing.",
remedy="Rename the skill to describe the specific domain task (e.g. 'bash-script-runner').",
),
"listing-overflow": RuleDefinition(
rule="listing-overflow",
default_severity=Severity.WARN,
summary="Skill description is unusually long and risks runtime truncation",
explanation="Agent runtimes enforce strict listing budgets on catalog context. Excessively verbose descriptions risk truncation.",
remedy="Condense the description to highlight key triggers, moving extensive documentation into the markdown body.",
),
"unresolved-declared-dependency": RuleDefinition(
rule="unresolved-declared-dependency",
default_severity=Severity.WARN,
summary="Declared dependency skill is missing from catalog",
explanation="A skill declared in metadata.requires_skill or allowed-tools: Skill(X) does not exist in the resident catalog or discovery roots.",
remedy="Ensure the required skill is installed or update the dependency declaration.",
),
"lockfile-drift": RuleDefinition(
rule="lockfile-drift",
default_severity=Severity.WARN,
summary="SKILL.md digest does not match lockfile computedHash",
explanation="The skill contents have changed locally since being pinned in skills-lock.json.",
remedy="Re-run npx skills update or refresh the lockfile hash.",
),
"unbounded-attractor": RuleDefinition(
rule="unbounded-attractor",
default_severity=Severity.WARN,
summary="Description uses greedy or universal phrasing that hijacks queries",
explanation="Descriptions claiming unbounded scope (e.g. 'assist with any task' or 'manage files and run commands') act as greedy attractor sinks in multi-skill catalogs, causing distractor hijacking.",
remedy="Narrow the description to specific domains, tools, and trigger conditions, and add directional disclaimers specifying when not to invoke the skill.",
),
"unknown-skill-reference": RuleDefinition(
rule="unknown-skill-reference",
default_severity=Severity.WARN,
summary="Description hands off to a skill name that does not exist in the catalog",
explanation="Negative routing instructions (e.g. 'Don't use for X — use <other-skill>') that reference a missing or unmerged skill actively repel the router away from the resident skill while the target skill is absent, creating a 0% recall sinkhole.",
remedy="Remove the handoff reference until the target skill is added to the catalog, or correct the referenced skill name.",
),
"missing-mutual-handoff": RuleDefinition(
rule="missing-mutual-handoff",
default_severity=Severity.WARN,
summary="Overlapping neighbor skills lack mutual routing handoffs ('use <other-skill>')",
explanation="When closely related skills share domain vocabulary or one skill defines a one-way boundary without a reciprocal handoff on the neighbor, the unguarded skill acts as a one-way attractor sink and hijacks queries.",
remedy="Add reciprocal 'Don't use for X (use <neighbor-skill>)' handoff clauses to both overlapping skills so each carves out the other's territory.",
),
}
LintIssue ¶
Bases: BaseModel
Represent a single diagnostic finding for a skill file or catalog.
Source code in src/reach/lint.py
LintReport ¶
Bases: BaseModel
Aggregate lint issues across all evaluated skills.
Source code in src/reach/lint.py
LintSettings ¶
Bases: BaseModel
Configuration settings for static skill linting and validation thresholds.
Source code in src/reach/config.py
from_settings
classmethod
¶
from_settings(
settings: Mapping[str, object] | None = None,
overrides: Mapping[str, Any] | None = None,
) -> LintSettings
Construct a LintSettings from loaded reach.toml settings and CLI overrides.
Source code in src/reach/config.py
RuleDefinition ¶
Bases: BaseModel
Describe a static lint rule, its rationale, and recommended remediation.
Source code in src/reach/lint.py
Severity ¶
SkillLintSemantics ¶
Bases: BaseModel
Represent structured routing boundaries and scope attractors for a skill.
Source code in src/reach/_lint_semantics.py
explain_rule ¶
explain_rule(rule_name: str) -> RuleDefinition | None
extract_corpus_semantics ¶
extract_corpus_semantics(
skills: Sequence[Skill],
) -> dict[str, SkillLintSemantics]
Extract routing handoff targets and attractor semantics deterministically across a corpus.
Source code in src/reach/_lint_semantics.py
extract_skill_references ¶
Extract explicit skill references from negative/redirect clauses in a description.
Uses grammatical noun-position boundaries: a kebab-case token after a positive
handoff verb (use, see, prefer, defer to) must either be enclosed in
backticks or stand in terminal noun position (followed by clause punctuation,
instead, first, singular skill, or or/and to another skill). Compound
adjectives modifying a following noun (e.g. use product-specific skills)
are excluded structurally without word blocklists.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
description
|
str
|
Frontmatter description string to inspect. |
required |
self_name
|
str | None
|
Optional name of the skill itself to exclude self-references. |
None
|
Returns:
| Type | Description |
|---|---|
tuple[str, ...]
|
Sorted tuple of unique referenced skill names in kebab-case. |
Source code in src/reach/_lint_semantics.py
find_competing_neighbors ¶
find_competing_neighbors(
modified: set[str],
skills: Sequence[Skill],
*,
settings: LintSettings | None = None,
dense_similarities: Mapping[tuple[str, str], float]
| None = None,
semantics_by_name: Mapping[str, SkillLintSemantics]
| None = None,
) -> set[str]
Identify competing neighbor skills that could be hijacked by modified skills.
Source code in src/reach/lint.py
find_unknown_skill_references ¶
find_unknown_skill_references(
description: str,
known_skills: Sequence[str] | set[str] | frozenset[str],
*,
self_name: str | None = None,
settings: LintSettings | None = None,
extracted_refs: Sequence[str]
| frozenset[str]
| None = None,
) -> tuple[str, ...]
Return referenced skill names in description that are absent from known_skills.
Respects the configured severity for unknown-skill-reference and returns
an empty tuple when the rule is set to ignore.
Source code in src/reach/lint.py
hands_off_to_skill ¶
hands_off_to_skill(
source_description: str,
target_name: str,
extracted_refs: frozenset[str] | None = None,
) -> bool
Return True if source_description explicitly hands off to or disclaims target_name.
Source code in src/reach/lint.py
lint_file ¶
lint_file(
skill_file: Path | str,
config: LintSettings | None = None,
) -> LintReport
Inspect a single SKILL.md manifest file for structural and authoring issues.
Source code in src/reach/lint.py
lint_skills ¶
lint_skills(
skills: Sequence[Path | str],
config: LintSettings | None = None,
) -> LintReport
Lint a specific collection of skill directories or SKILL.md file paths.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
skills
|
Sequence[Path | str]
|
Sequence of paths to skill directories or SKILL.md files. |
required |
config
|
LintSettings | None
|
Optional LintSettings overrides; loads from reach.toml settings if omitted. |
None
|
Returns:
| Type | Description |
|---|---|
LintReport
|
A LintReport containing all detected issues, severities, and skill counts. |
Source code in src/reach/lint.py
lint_tree ¶
lint_tree(
root: Path | str,
config: LintSettings | None = None,
skill_filter: str | None = None,
) -> LintReport
Recursively search for and lint all SKILL.md files under a directory root.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
root
|
Path | str
|
Directory root to search for skills. |
required |
config
|
LintSettings | None
|
Optional LintSettings overrides; loads from reach.toml settings if omitted. |
None
|
skill_filter
|
str | None
|
Optional skill name filter to limit reported diagnostics. |
None
|
Returns:
| Type | Description |
|---|---|
LintReport
|
A LintReport summarizing all issues found across discovered skills. |