@@ -62,7 +62,7 @@ def templates_dir(project_dir):
6262 tpl_root .mkdir (parents = True , exist_ok = True )
6363
6464 # Template with valid YAML frontmatter
65- (tpl_root / "specify.md" ).write_text (
65+ (tpl_root / "speckit. specify.md" ).write_text (
6666 "---\n "
6767 "description: Create or update the feature specification.\n "
6868 "handoffs:\n "
@@ -79,7 +79,7 @@ def templates_dir(project_dir):
7979 )
8080
8181 # Template with minimal frontmatter
82- (tpl_root / "plan.md" ).write_text (
82+ (tpl_root / "speckit. plan.md" ).write_text (
8383 "---\n "
8484 "description: Generate implementation plan.\n "
8585 "---\n "
@@ -91,15 +91,15 @@ def templates_dir(project_dir):
9191 )
9292
9393 # Template with no frontmatter
94- (tpl_root / "tasks.md" ).write_text (
94+ (tpl_root / "speckit. tasks.md" ).write_text (
9595 "# Tasks Command\n "
9696 "\n "
9797 "Body without frontmatter.\n " ,
9898 encoding = "utf-8" ,
9999 )
100100
101101 # Template with empty YAML frontmatter (yaml.safe_load returns None)
102- (tpl_root / "empty_fm.md" ).write_text (
102+ (tpl_root / "speckit. empty_fm.md" ).write_text (
103103 "---\n "
104104 "---\n "
105105 "\n "
@@ -337,7 +337,7 @@ def test_malformed_yaml_frontmatter(self, project_dir):
337337 cmds_dir = project_dir / ".claude" / "commands"
338338 cmds_dir .mkdir (parents = True )
339339
340- (cmds_dir / "broken.md" ).write_text (
340+ (cmds_dir / "speckit. broken.md" ).write_text (
341341 "---\n "
342342 "description: [unclosed bracket\n "
343343 " invalid: yaml: content: here\n "
@@ -433,8 +433,8 @@ def test_skills_install_for_all_agents(self, temp_dir, agent_key):
433433 commands_subdir = AGENT_CONFIG [agent_key ].get ("commands_subdir" , "commands" )
434434 cmds_dir = proj / agent_folder .rstrip ("/" ) / commands_subdir
435435 cmds_dir .mkdir (parents = True )
436- # In this test, Copilot uses speckit.*.agent.md templates; other agents use a simple 'specify.md' name
437- fname = "speckit.specify.agent.md" if agent_key == "copilot" else "specify.md"
436+ # Copilot uses speckit.*.agent.md templates; other agents use speckit.*.md
437+ fname = "speckit.specify.agent.md" if agent_key == "copilot" else "speckit. specify.md"
438438 (cmds_dir / fname ).write_text (
439439 "---\n description: Test command\n ---\n \n # Test\n \n Body.\n "
440440 )
@@ -471,7 +471,37 @@ def test_copilot_ignores_non_speckit_agents(self, project_dir):
471471 assert "speckit-plan" in skill_dirs
472472 assert "speckit-my-custom-agent.agent" not in skill_dirs
473473 assert "speckit-my-custom-agent" not in skill_dirs
474-
474+
475+ @pytest .mark .parametrize ("agent_key,custom_file" , [
476+ ("claude" , "review.md" ),
477+ ("cursor-agent" , "deploy.md" ),
478+ ("qwen" , "my-workflow.md" ),
479+ ])
480+ def test_non_speckit_commands_ignored_for_all_agents (self , temp_dir , agent_key , custom_file ):
481+ """User-authored command files must not produce skills for any agent."""
482+ proj = temp_dir / f"proj-{ agent_key } "
483+ proj .mkdir ()
484+
485+ agent_folder = AGENT_CONFIG [agent_key ]["folder" ]
486+ commands_subdir = AGENT_CONFIG [agent_key ].get ("commands_subdir" , "commands" )
487+ cmds_dir = proj / agent_folder .rstrip ("/" ) / commands_subdir
488+ cmds_dir .mkdir (parents = True )
489+ (cmds_dir / "speckit.specify.md" ).write_text (
490+ "---\n description: Create spec.\n ---\n \n # Specify\n \n Body.\n "
491+ )
492+ (cmds_dir / custom_file ).write_text (
493+ "---\n description: User custom command\n ---\n \n # Custom\n \n Body.\n "
494+ )
495+
496+ result = install_ai_skills (proj , agent_key )
497+
498+ assert result is True
499+ skills_dir = _get_skills_dir (proj , agent_key )
500+ skill_dirs = [d .name for d in skills_dir .iterdir () if d .is_dir ()]
501+ assert "speckit-specify" in skill_dirs
502+ custom_stem = Path (custom_file ).stem
503+ assert f"speckit-{ custom_stem } " not in skill_dirs
504+
475505 def test_copilot_fallback_when_only_non_speckit_agents (self , project_dir ):
476506 """Fallback to templates/commands/ when .github/agents/ has no speckit.*.md files."""
477507 agents_dir = project_dir / ".github" / "agents"
@@ -491,7 +521,30 @@ def test_copilot_fallback_when_only_non_speckit_agents(self, project_dir):
491521 # Should have skills from fallback templates, not from the custom agent
492522 assert "speckit-plan" in skill_dirs
493523 assert not any ("my-custom" in d for d in skill_dirs )
494-
524+
525+ @pytest .mark .parametrize ("agent_key" , ["claude" , "cursor-agent" , "qwen" ])
526+ def test_fallback_when_only_non_speckit_commands (self , temp_dir , agent_key ):
527+ """Fallback to templates/commands/ when agent dir has no speckit.*.md files."""
528+ proj = temp_dir / f"proj-{ agent_key } "
529+ proj .mkdir ()
530+
531+ agent_folder = AGENT_CONFIG [agent_key ]["folder" ]
532+ commands_subdir = AGENT_CONFIG [agent_key ].get ("commands_subdir" , "commands" )
533+ cmds_dir = proj / agent_folder .rstrip ("/" ) / commands_subdir
534+ cmds_dir .mkdir (parents = True )
535+ # Only a user-authored command, no speckit.* templates
536+ (cmds_dir / "my-custom-command.md" ).write_text (
537+ "---\n description: User custom command\n ---\n \n # Custom\n \n Body.\n "
538+ )
539+
540+ result = install_ai_skills (proj , agent_key )
541+
542+ # Should succeed via fallback to templates/commands/
543+ assert result is True
544+ skills_dir = _get_skills_dir (proj , agent_key )
545+ assert skills_dir .exists ()
546+ skill_dirs = [d .name for d in skills_dir .iterdir () if d .is_dir ()]
547+ assert not any ("my-custom" in d for d in skill_dirs )
495548
496549class TestCommandCoexistence :
497550 """Verify install_ai_skills never touches command files.
@@ -503,14 +556,16 @@ class TestCommandCoexistence:
503556
504557 def test_existing_commands_preserved_claude (self , project_dir , templates_dir , commands_dir_claude ):
505558 """install_ai_skills must NOT remove pre-existing .claude/commands files."""
506- # Verify commands exist before
507- assert len (list (commands_dir_claude .glob ("speckit.*" ))) == 3
559+ # Verify commands exist before (templates_dir adds 4 speckit.* files,
560+ # commands_dir_claude overlaps with 3 of them)
561+ before = list (commands_dir_claude .glob ("speckit.*" ))
562+ assert len (before ) >= 3
508563
509564 install_ai_skills (project_dir , "claude" )
510565
511566 # Commands must still be there — install_ai_skills never touches them
512567 remaining = list (commands_dir_claude .glob ("speckit.*" ))
513- assert len (remaining ) == 3
568+ assert len (remaining ) == len ( before )
514569
515570 def test_existing_commands_preserved_gemini (self , project_dir , templates_dir , commands_dir_gemini ):
516571 """install_ai_skills must NOT remove pre-existing .gemini/commands files."""
0 commit comments