Skip to content
Merged
20 changes: 15 additions & 5 deletions src/specify_cli/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -1165,23 +1165,28 @@ 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)):
Comment thread
darkglow-net marked this conversation as resolved.
Outdated
# 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).
script_dir = Path(__file__).parent.parent.parent # up from src/specify_cli/
fallback_dir = script_dir / "templates" / "commands"
if fallback_dir.exists() and any(fallback_dir.glob("*.md")):
templates_dir = fallback_dir
template_glob = "*.md"

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 +1225,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 +1257,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
Comment thread
darkglow-net marked this conversation as resolved.
Outdated
fname = "speckit.specify.agent.md" if agent_key == "copilot" else "specify.md"
Comment thread
darkglow-net marked this conversation as resolved.
Outdated
(cmds_dir / fname).write_text(
Comment thread
darkglow-net marked this conversation as resolved.
Outdated
"---\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"
Comment thread
darkglow-net marked this conversation as resolved.
Outdated
assert skills_dir.exists()
skill_dirs = [d.name for d in skills_dir.iterdir() if d.is_dir()]
assert "speckit-plan" in skill_dirs
Comment thread
darkglow-net marked this conversation as resolved.
assert "speckit-other-agent.agent" not in skill_dirs
Comment thread
darkglow-net marked this conversation as resolved.
Outdated



class TestCommandCoexistence:
Expand Down
Loading