Skip to content

Commit aa88f9c

Browse files
committed
Align Claude skill frontmatter across generators
1 parent ce96610 commit aa88f9c

8 files changed

Lines changed: 63 additions & 58 deletions

File tree

src/specify_cli/__init__.py

Lines changed: 8 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1640,6 +1640,8 @@ def install_ai_skills(
16401640
``True`` if at least one skill was installed or all skills were
16411641
already present (idempotent re-run), ``False`` otherwise.
16421642
"""
1643+
from .agents import CommandRegistrar
1644+
16431645
# Locate command templates in the agent's extracted commands directory.
16441646
# download_and_extract_template() already placed the .md files here.
16451647
agent_config = AGENT_CONFIG.get(selected_ai, {})
@@ -1741,15 +1743,12 @@ def install_ai_skills(
17411743
if source_name.endswith(".agent.md"):
17421744
source_name = source_name[:-len(".agent.md")] + ".md"
17431745

1744-
frontmatter_data = {
1745-
"name": skill_name,
1746-
"description": enhanced_desc,
1747-
"compatibility": "Requires spec-kit project structure with .specify/ directory",
1748-
"metadata": {
1749-
"author": "github-spec-kit",
1750-
"source": f"templates/commands/{source_name}",
1751-
},
1752-
}
1746+
frontmatter_data = CommandRegistrar.build_skill_frontmatter(
1747+
selected_ai,
1748+
skill_name,
1749+
enhanced_desc,
1750+
f"templates/commands/{source_name}",
1751+
)
17531752
frontmatter_text = yaml.safe_dump(frontmatter_data, sort_keys=False).strip()
17541753
skill_content = (
17551754
f"---\n"

src/specify_cli/agents.py

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -364,16 +364,35 @@ def render_skill_command(
364364
body = self.resolve_skill_placeholders(agent_name, frontmatter, body, project_root)
365365

366366
description = frontmatter.get("description", f"Spec-kit workflow command: {skill_name}")
367+
skill_frontmatter = self.build_skill_frontmatter(
368+
agent_name,
369+
skill_name,
370+
description,
371+
f"{source_id}:{source_file}",
372+
)
373+
return self.render_frontmatter(skill_frontmatter) + "\n" + body
374+
375+
@staticmethod
376+
def build_skill_frontmatter(
377+
agent_name: str,
378+
skill_name: str,
379+
description: str,
380+
source: str,
381+
) -> dict:
382+
"""Build consistent SKILL.md frontmatter across all skill generators."""
367383
skill_frontmatter = {
368384
"name": skill_name,
369385
"description": description,
370386
"compatibility": "Requires spec-kit project structure with .specify/ directory",
371387
"metadata": {
372388
"author": "github-spec-kit",
373-
"source": f"{source_id}:{source_file}",
389+
"source": source,
374390
},
375391
}
376-
return self.render_frontmatter(skill_frontmatter) + "\n" + body
392+
if agent_name == "claude":
393+
# Claude skills should only run when explicitly invoked.
394+
skill_frontmatter["disable-model-invocation"] = True
395+
return skill_frontmatter
377396

378397
@staticmethod
379398
def resolve_skill_placeholders(agent_name: str, frontmatter: dict, body: str, project_root: Path) -> str:

src/specify_cli/extensions.py

Lines changed: 6 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -801,15 +801,12 @@ def _register_extension_skills(
801801
original_desc = frontmatter.get("description", "")
802802
description = original_desc or f"Extension command: {cmd_name}"
803803

804-
frontmatter_data = {
805-
"name": skill_name,
806-
"description": description,
807-
"compatibility": "Requires spec-kit project structure with .specify/ directory",
808-
"metadata": {
809-
"author": "github-spec-kit",
810-
"source": f"extension:{manifest.id}",
811-
},
812-
}
804+
frontmatter_data = registrar.build_skill_frontmatter(
805+
selected_ai,
806+
skill_name,
807+
description,
808+
f"extension:{manifest.id}",
809+
)
813810
frontmatter_text = yaml.safe_dump(frontmatter_data, sort_keys=False).strip()
814811

815812
# Derive a human-friendly title from the command name

src/specify_cli/integrations/claude/__init__.py

Lines changed: 6 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -43,17 +43,12 @@ def _render_skill(self, template_name: str, frontmatter: dict[str, Any], body: s
4343
"description",
4444
f"Spec-kit workflow command: {template_name}",
4545
)
46-
skill_frontmatter = {
47-
"name": skill_name,
48-
"description": description,
49-
# Spec-kit workflows should only run when explicitly invoked.
50-
"disable-model-invocation": True,
51-
"compatibility": "Requires spec-kit project structure with .specify/ directory",
52-
"metadata": {
53-
"author": "github-spec-kit",
54-
"source": f"templates/commands/{template_name}.md",
55-
},
56-
}
46+
skill_frontmatter = CommandRegistrar.build_skill_frontmatter(
47+
self.key,
48+
skill_name,
49+
description,
50+
f"templates/commands/{template_name}.md",
51+
)
5752
frontmatter_text = yaml.safe_dump(skill_frontmatter, sort_keys=False).strip()
5853
return f"---\n{frontmatter_text}\n---\n\n{body.strip()}\n"
5954

src/specify_cli/presets.py

Lines changed: 18 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -771,15 +771,12 @@ def _register_skills(
771771
if skill_subdir.exists() and not skill_subdir.is_dir():
772772
continue
773773
skill_subdir.mkdir(parents=True, exist_ok=True)
774-
frontmatter_data = {
775-
"name": target_skill_name,
776-
"description": enhanced_desc,
777-
"compatibility": "Requires spec-kit project structure with .specify/ directory",
778-
"metadata": {
779-
"author": "github-spec-kit",
780-
"source": f"preset:{manifest.id}",
781-
},
782-
}
774+
frontmatter_data = registrar.build_skill_frontmatter(
775+
selected_ai,
776+
target_skill_name,
777+
enhanced_desc,
778+
f"preset:{manifest.id}",
779+
)
783780
frontmatter_text = yaml.safe_dump(frontmatter_data, sort_keys=False).strip()
784781
skill_content = (
785782
f"---\n"
@@ -861,15 +858,12 @@ def _unregister_skills(self, skill_names: List[str], preset_dir: Path) -> None:
861858
original_desc or f"Spec-kit workflow command: {short_name}",
862859
)
863860

864-
frontmatter_data = {
865-
"name": skill_name,
866-
"description": enhanced_desc,
867-
"compatibility": "Requires spec-kit project structure with .specify/ directory",
868-
"metadata": {
869-
"author": "github-spec-kit",
870-
"source": f"templates/commands/{short_name}.md",
871-
},
872-
}
861+
frontmatter_data = registrar.build_skill_frontmatter(
862+
selected_ai if isinstance(selected_ai, str) else "",
863+
skill_name,
864+
enhanced_desc,
865+
f"templates/commands/{short_name}.md",
866+
)
873867
frontmatter_text = yaml.safe_dump(frontmatter_data, sort_keys=False).strip()
874868
skill_title = self._skill_title_from_command(short_name)
875869
skill_content = (
@@ -894,15 +888,12 @@ def _unregister_skills(self, skill_names: List[str], preset_dir: Path) -> None:
894888
command_name = extension_restore["command_name"]
895889
title_name = self._skill_title_from_command(command_name)
896890

897-
frontmatter_data = {
898-
"name": skill_name,
899-
"description": frontmatter.get("description", f"Extension command: {command_name}"),
900-
"compatibility": "Requires spec-kit project structure with .specify/ directory",
901-
"metadata": {
902-
"author": "github-spec-kit",
903-
"source": extension_restore["source"],
904-
},
905-
}
891+
frontmatter_data = registrar.build_skill_frontmatter(
892+
selected_ai if isinstance(selected_ai, str) else "",
893+
skill_name,
894+
frontmatter.get("description", f"Extension command: {command_name}"),
895+
extension_restore["source"],
896+
)
906897
frontmatter_text = yaml.safe_dump(frontmatter_data, sort_keys=False).strip()
907898
skill_content = (
908899
f"---\n"

tests/integrations/test_integration_claude.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -284,6 +284,7 @@ def test_claude_preset_creates_new_skill_without_commands_dir(self, tmp_path):
284284
content = skill_file.read_text(encoding="utf-8")
285285
assert "preset:claude-skill-command" in content
286286
assert "name: speckit-research" in content
287+
assert "disable-model-invocation: true" in content
287288

288289
metadata = manager.registry.get("claude-skill-command")
289290
assert "speckit-research" in metadata.get("registered_skills", [])

tests/test_extension_skills.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -269,6 +269,7 @@ def test_skill_md_has_parseable_yaml(self, skills_project, extension_dir):
269269
assert isinstance(parsed, dict)
270270
assert parsed["name"] == "speckit-test-ext-hello"
271271
assert "description" in parsed
272+
assert parsed["disable-model-invocation"] is True
272273

273274
def test_no_skills_when_ai_skills_disabled(self, no_skills_project, extension_dir):
274275
"""No skills should be created when ai_skills is false."""

tests/test_presets.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1974,6 +1974,7 @@ def test_skill_overridden_on_preset_install(self, project_dir, temp_dir):
19741974
assert skill_file.exists()
19751975
content = skill_file.read_text()
19761976
assert "preset:self-test" in content, "Skill should reference preset source"
1977+
assert "disable-model-invocation: true" in content
19771978

19781979
# Verify it was recorded in registry
19791980
metadata = manager.registry.get("self-test")
@@ -2059,6 +2060,7 @@ def test_skill_restored_on_preset_remove(self, project_dir, temp_dir):
20592060
content = skill_file.read_text()
20602061
assert "preset:self-test" not in content, "Preset content should be gone"
20612062
assert "templates/commands/specify.md" in content, "Should reference core template"
2063+
assert "disable-model-invocation: true" in content
20622064

20632065
def test_skill_restored_on_remove_resolves_script_placeholders(self, project_dir):
20642066
"""Core restore should resolve {SCRIPT}/{ARGS} placeholders like other skill paths."""

0 commit comments

Comments
 (0)