Skip to content
Open
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
19 changes: 14 additions & 5 deletions src/specify_cli/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -1165,7 +1165,11 @@ def install_ai_skills(project_path: Path, selected_ai: str, tracker: StepTracker
else:
templates_dir = project_path / commands_subdir

if not templates_dir.exists() or not any(templates_dir.glob("*.md")):
# For Copilot, only consider speckit.*.md templates so that user-authored
# agent files don't prevent the fallback to templates/commands/.
template_glob = "speckit.*.md" if selected_ai == "copilot" else "*.md"

if not templates_dir.exists() or not any(templates_dir.glob(template_glob)):
# Fallback: try the repo-relative path (for running from source checkout)
# This also covers agents whose extracted commands are in a different
# format (e.g. gemini/tabnine use .toml, not .md).
Expand All @@ -1174,14 +1178,14 @@ def install_ai_skills(project_path: Path, selected_ai: str, tracker: StepTracker
if fallback_dir.exists() and any(fallback_dir.glob("*.md")):
templates_dir = fallback_dir

if not templates_dir.exists() or not any(templates_dir.glob("*.md")):
if not templates_dir.exists() or not any(templates_dir.glob(template_glob)):
if tracker:
tracker.error("ai-skills", "command templates not found")
else:
console.print("[yellow]Warning: command templates not found, skipping skills installation[/yellow]")
return False

command_files = sorted(templates_dir.glob("*.md"))
command_files = sorted(templates_dir.glob(template_glob))
if not command_files:
if tracker:
tracker.skip("ai-skills", "no command templates found")
Expand Down Expand Up @@ -1220,11 +1224,14 @@ def install_ai_skills(project_path: Path, selected_ai: str, tracker: StepTracker
body = content

command_name = command_file.stem
# Normalize: extracted commands may be named "speckit.<cmd>.md";
# strip the "speckit." prefix so skill names stay clean and
# Normalize: extracted commands may be named "speckit.<cmd>.md"
# or "speckit.<cmd>.agent.md"; strip the "speckit." prefix and
# any trailing ".agent" suffix so skill names stay clean and
# SKILL_DESCRIPTIONS lookups work.
if command_name.startswith("speckit."):
command_name = command_name[len("speckit."):]
if command_name.endswith(".agent"):
command_name = command_name[:-len(".agent")]
# Kimi CLI discovers skills by directory name and invokes them as
# /skill:<name> — use dot separator to match packaging convention.
if selected_ai == "kimi":
Expand All @@ -1249,6 +1256,8 @@ def install_ai_skills(project_path: Path, selected_ai: str, tracker: StepTracker
source_name = command_file.name
if source_name.startswith("speckit."):
source_name = source_name[len("speckit."):]
if source_name.endswith(".agent.md"):
source_name = source_name[:-len(".agent.md")] + ".md"

frontmatter_data = {
"name": skill_name,
Expand Down
27 changes: 25 additions & 2 deletions tests/test_ai_skills.py
Original file line number Diff line number Diff line change
Expand Up @@ -430,9 +430,12 @@ def test_skills_install_for_all_agents(self, temp_dir, agent_key):

# Place .md templates in the agent's commands directory
agent_folder = AGENT_CONFIG[agent_key]["folder"]
cmds_dir = proj / agent_folder.rstrip("/") / "commands"
commands_subdir = AGENT_CONFIG[agent_key].get("commands_subdir", "commands")
cmds_dir = proj / agent_folder.rstrip("/") / commands_subdir
cmds_dir.mkdir(parents=True)
(cmds_dir / "specify.md").write_text(
# Copilot uses speckit.*.agent.md templates; other agents use plain names
fname = "speckit.specify.agent.md" if agent_key == "copilot" else "specify.md"
Comment on lines +436 to +437
(cmds_dir / fname).write_text(
Comment on lines +436 to +438
"---\ndescription: Test command\n---\n\n# Test\n\nBody.\n"
)

Expand All @@ -448,6 +451,26 @@ def test_skills_install_for_all_agents(self, temp_dir, agent_key):
assert expected_skill_name in skill_dirs
assert (skills_dir / expected_skill_name / "SKILL.md").exists()

def test_copilot_ignores_non_speckit_agents(self, project_dir):
"""Non-speckit markdown in .github/agents/ must not produce skills."""
agents_dir = project_dir / ".github" / "agents"
agents_dir.mkdir(parents=True, exist_ok=True)
(agents_dir / "speckit.plan.agent.md").write_text(
"---\ndescription: Generate implementation plan.\n---\n\n# Plan\n\nBody.\n"
)
(agents_dir / "other-agent.agent.md").write_text(
"---\ndescription: Some other agent\n---\n\n# Other\n\nBody.\n"
)

result = install_ai_skills(project_dir, "copilot")

assert result is True
skills_dir = project_dir / ".github" / "skills"
assert skills_dir.exists()
skill_dirs = [d.name for d in skills_dir.iterdir() if d.is_dir()]
assert "speckit-plan" in skill_dirs
assert "speckit-other-agent.agent" not in skill_dirs



class TestCommandCoexistence:
Expand Down