@@ -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 "
@@ -430,9 +430,12 @@ def test_skills_install_for_all_agents(self, temp_dir, agent_key):
430430
431431 # Place .md templates in the agent's commands directory
432432 agent_folder = AGENT_CONFIG [agent_key ]["folder" ]
433- cmds_dir = proj / agent_folder .rstrip ("/" ) / "commands"
433+ commands_subdir = AGENT_CONFIG [agent_key ].get ("commands_subdir" , "commands" )
434+ cmds_dir = proj / agent_folder .rstrip ("/" ) / commands_subdir
434435 cmds_dir .mkdir (parents = True )
435- (cmds_dir / "specify.md" ).write_text (
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"
438+ (cmds_dir / fname ).write_text (
436439 "---\n description: Test command\n ---\n \n # Test\n \n Body.\n "
437440 )
438441
@@ -448,7 +451,100 @@ def test_skills_install_for_all_agents(self, temp_dir, agent_key):
448451 assert expected_skill_name in skill_dirs
449452 assert (skills_dir / expected_skill_name / "SKILL.md" ).exists ()
450453
454+ def test_copilot_ignores_non_speckit_agents (self , project_dir ):
455+ """Non-speckit markdown in .github/agents/ must not produce skills."""
456+ agents_dir = project_dir / ".github" / "agents"
457+ agents_dir .mkdir (parents = True , exist_ok = True )
458+ (agents_dir / "speckit.plan.agent.md" ).write_text (
459+ "---\n description: Generate implementation plan.\n ---\n \n # Plan\n \n Body.\n "
460+ )
461+ (agents_dir / "my-custom-agent.agent.md" ).write_text (
462+ "---\n description: A user custom agent\n ---\n \n # Custom\n \n Body.\n "
463+ )
451464
465+ result = install_ai_skills (project_dir , "copilot" )
466+
467+ assert result is True
468+ skills_dir = _get_skills_dir (project_dir , "copilot" )
469+ assert skills_dir .exists ()
470+ skill_dirs = [d .name for d in skills_dir .iterdir () if d .is_dir ()]
471+ assert "speckit-plan" in skill_dirs
472+ assert "speckit-my-custom-agent.agent" not in skill_dirs
473+ assert "speckit-my-custom-agent" not in skill_dirs
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+
505+ def test_copilot_fallback_when_only_non_speckit_agents (self , project_dir ):
506+ """Fallback to templates/commands/ when .github/agents/ has no speckit.*.md files."""
507+ agents_dir = project_dir / ".github" / "agents"
508+ agents_dir .mkdir (parents = True , exist_ok = True )
509+ # Only a user-authored agent, no speckit.* templates
510+ (agents_dir / "my-custom-agent.agent.md" ).write_text (
511+ "---\n description: A user custom agent\n ---\n \n # Custom\n \n Body.\n "
512+ )
513+
514+ result = install_ai_skills (project_dir , "copilot" )
515+
516+ # Should succeed via fallback to templates/commands/
517+ assert result is True
518+ skills_dir = _get_skills_dir (project_dir , "copilot" )
519+ assert skills_dir .exists ()
520+ skill_dirs = [d .name for d in skills_dir .iterdir () if d .is_dir ()]
521+ # Should have skills from fallback templates, not from the custom agent
522+ assert "speckit-plan" in skill_dirs
523+ assert not any ("my-custom" in d for d in skill_dirs )
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 )
452548
453549class TestCommandCoexistence :
454550 """Verify install_ai_skills never touches command files.
@@ -460,14 +556,16 @@ class TestCommandCoexistence:
460556
461557 def test_existing_commands_preserved_claude (self , project_dir , templates_dir , commands_dir_claude ):
462558 """install_ai_skills must NOT remove pre-existing .claude/commands files."""
463- # Verify commands exist before
464- 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
465563
466564 install_ai_skills (project_dir , "claude" )
467565
468566 # Commands must still be there — install_ai_skills never touches them
469567 remaining = list (commands_dir_claude .glob ("speckit.*" ))
470- assert len (remaining ) == 3
568+ assert len (remaining ) == len ( before )
471569
472570 def test_existing_commands_preserved_gemini (self , project_dir , templates_dir , commands_dir_gemini ):
473571 """install_ai_skills must NOT remove pre-existing .gemini/commands files."""
0 commit comments