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
11 changes: 11 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,17 @@ Requires Python 3.14+. Run `make install` before first use to set up the virtual

The LLM tests read two environment variables: `OLLAMA_MODEL_NAME` (the DeepEval judge model, defaults to `gemma4`) and `SLACK_MCP_TOKEN` (a Slack MCP bearer token; the MCP tool-selection test is skipped when it's unset). Copy `.env.example` to `.env` and fill in values — the `Makefile` auto-loads `.env` — or pass them inline, e.g. `OLLAMA_MODEL_NAME=<model> make test-eval`.

## Cross-Skill References

When one `SKILL.md` references another skill (e.g., to delegate a step instead of duplicating content), follow these rules:

- Use the backticked `plugin:skill` form, e.g. `` `slack:slack-cli` ``.
- When pointing at a specific step, include the step's heading text, not just the number — references survive future reordering.
- Add a sentence of prose explaining what the referenced section does and why you're delegating to it.
- Don't use markdown anchor links (`[text](#step-1)`), `@`-include syntax (`@path/to/SKILL.md`), or bare file paths — none are idiomatic in installed skills, and `@`-includes force-load context.

See `skills/create-slack-app/SKILL.md` Step 1a for an example.

## Testing

Two test layers validate skills:
Expand Down
5 changes: 5 additions & 0 deletions tests/config.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,14 @@
import json
import os
from pathlib import Path

# Filesystem
SKILLS_ROOT = Path(__file__).parent.parent / "skills"

# Plugin namespace (single source of truth: the plugin manifest)
PLUGIN_MANIFEST = Path(__file__).parent.parent / ".claude-plugin" / "plugin.json"
PLUGIN_NAME = json.loads(PLUGIN_MANIFEST.read_text())["name"]

# Skill inventory (single source of truth)
EXPECTED_SKILLS = ("create-slack-app", "block-kit", "slack-api", "slack-cli")

Expand Down
45 changes: 45 additions & 0 deletions tests/unit/test_cross_skill_references.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import re

from tests.config import PLUGIN_NAME
from tests.skill import discover_skills


class TestCrossSkillReferences:
def setup_method(self):
self.skills = discover_skills()
self.skill_names = {skill.metadata.name for skill in self.skills}

def test_plugin_skill_references_target_real_skills(self):
pattern = re.compile(rf"`{re.escape(PLUGIN_NAME)}:([a-z0-9-]+)`")
for skill in self.skills:
for target in pattern.findall(skill.body):
assert (
target in self.skill_names
), f"{skill.path} references unknown skill `{PLUGIN_NAME}:{target}`"

def test_no_markdown_anchor_links(self):
for skill in self.skills:
# .find() returns -1 if the substring is not found
anchor_index = skill.body.find("](#")

snippet = skill.body[anchor_index : anchor_index + 30]
assert anchor_index == -1, (
f"'{skill.path}' uses a markdown anchor link near "
f'"{snippet}"; '
"cross-skill references must not use `[text](#anchor)` links"
)

def test_no_bare_skill_file_paths(self):
for skill in self.skills:
# .find() returns -1 if the substring is not found
path_index = skill.body.find("SKILL.md")

# A bare path precedes "SKILL.md" (e.g. "skills/foo/SKILL.md"), so
# the error window reaches back to show that leading context.
start = max(0, path_index - 30)
snippet = skill.body[start : path_index + 8]
assert path_index == -1, (
f"'{skill.path}' references a SKILL.md path near "
f'"{snippet}"; '
"reference skills by the backticked `plugin:skill` form instead"
)