Skip to content

Commit 7fc4491

Browse files
committed
fix: correct Copilot extension command registration (#copilot)
- Use .agent.md extension for commands in .github/agents/ - Generate companion .prompt.md files in .github/prompts/ - Clean up .prompt.md files on extension removal - Add tests for Copilot-specific registration behavior Bumps version to 0.1.7.
1 parent bfaca2c commit 7fc4491

File tree

4 files changed

+184
-2
lines changed

4 files changed

+184
-2
lines changed

CHANGELOG.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,15 @@ Recent changes to the Specify CLI and templates are documented here.
77
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
88
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
99

10+
## [0.1.13] - 2026-03-03
11+
12+
### Fixed
13+
14+
- **Copilot Extension Commands Not Visible**: Fixed extension commands not appearing in GitHub Copilot when installed via `specify extension add --dev`
15+
- Changed Copilot file extension from `.md` to `.agent.md` in `CommandRegistrar.AGENT_CONFIGS` so Copilot recognizes agent files
16+
- Added generation of companion `.prompt.md` files in `.github/prompts/` during extension command registration, matching the release packaging behavior
17+
- Added cleanup of `.prompt.md` companion files when removing extensions via `specify extension remove`
18+
1019
## [0.1.12] - 2026-03-02
1120

1221
### Changed

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[project]
22
name = "specify-cli"
3-
version = "0.1.12"
3+
version = "0.1.13"
44
description = "Specify CLI, part of GitHub Spec Kit. A tool to bootstrap your projects for Spec-Driven Development (SDD)."
55
requires-python = ">=3.11"
66
dependencies = [

src/specify_cli/extensions.py

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -455,6 +455,12 @@ def remove(self, extension_id: str, keep_config: bool = False) -> bool:
455455
if cmd_file.exists():
456456
cmd_file.unlink()
457457

458+
# Also remove companion .prompt.md for Copilot
459+
if agent_name == "copilot":
460+
prompt_file = self.project_root / ".github" / "prompts" / f"{cmd_name}.prompt.md"
461+
if prompt_file.exists():
462+
prompt_file.unlink()
463+
458464
if keep_config:
459465
# Preserve config files, only remove non-config files
460466
if extension_dir.exists():
@@ -597,7 +603,7 @@ class CommandRegistrar:
597603
"dir": ".github/agents",
598604
"format": "markdown",
599605
"args": "$ARGUMENTS",
600-
"extension": ".md"
606+
"extension": ".agent.md"
601607
},
602608
"cursor": {
603609
"dir": ".cursor/commands",
@@ -871,16 +877,40 @@ def register_commands_for_agent(
871877
dest_file = commands_dir / f"{cmd_name}{agent_config['extension']}"
872878
dest_file.write_text(output)
873879

880+
# Generate companion .prompt.md for Copilot agents
881+
if agent_name == "copilot":
882+
self._write_copilot_prompt(project_root, cmd_name)
883+
874884
registered.append(cmd_name)
875885

876886
# Register aliases
877887
for alias in cmd_info.get("aliases", []):
878888
alias_file = commands_dir / f"{alias}{agent_config['extension']}"
879889
alias_file.write_text(output)
890+
# Generate companion .prompt.md for alias too
891+
if agent_name == "copilot":
892+
self._write_copilot_prompt(project_root, alias)
880893
registered.append(alias)
881894

882895
return registered
883896

897+
@staticmethod
898+
def _write_copilot_prompt(project_root: Path, cmd_name: str) -> None:
899+
"""Generate a companion .prompt.md file for a Copilot agent command.
900+
901+
Copilot requires a .prompt.md file in .github/prompts/ that references
902+
the corresponding .agent.md file in .github/agents/ via an ``agent:``
903+
frontmatter field.
904+
905+
Args:
906+
project_root: Path to project root
907+
cmd_name: Command name (used as the file stem, e.g. 'speckit.my-ext.example')
908+
"""
909+
prompts_dir = project_root / ".github" / "prompts"
910+
prompts_dir.mkdir(parents=True, exist_ok=True)
911+
prompt_file = prompts_dir / f"{cmd_name}.prompt.md"
912+
prompt_file.write_text(f"---\nagent: {cmd_name}\n---\n")
913+
884914
def register_commands_for_all_agents(
885915
self,
886916
manifest: ExtensionManifest,

tests/test_extensions.py

Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -520,6 +520,149 @@ def test_command_with_aliases(self, project_dir, temp_dir):
520520
assert (claude_dir / "speckit.alias.cmd.md").exists()
521521
assert (claude_dir / "speckit.shortcut.md").exists()
522522

523+
def test_register_commands_for_copilot(self, extension_dir, project_dir):
524+
"""Test registering commands for Copilot agent with .agent.md extension."""
525+
# Create .github/agents directory (Copilot project)
526+
agents_dir = project_dir / ".github" / "agents"
527+
agents_dir.mkdir(parents=True)
528+
529+
manifest = ExtensionManifest(extension_dir / "extension.yml")
530+
531+
registrar = CommandRegistrar()
532+
registered = registrar.register_commands_for_agent(
533+
"copilot", manifest, extension_dir, project_dir
534+
)
535+
536+
assert len(registered) == 1
537+
assert "speckit.test.hello" in registered
538+
539+
# Verify command file uses .agent.md extension
540+
cmd_file = agents_dir / "speckit.test.hello.agent.md"
541+
assert cmd_file.exists()
542+
543+
# Verify NO plain .md file was created
544+
plain_md_file = agents_dir / "speckit.test.hello.md"
545+
assert not plain_md_file.exists()
546+
547+
content = cmd_file.read_text()
548+
assert "description: Test hello command" in content
549+
assert "<!-- Extension: test-ext -->" in content
550+
551+
def test_copilot_companion_prompt_created(self, extension_dir, project_dir):
552+
"""Test that companion .prompt.md files are created in .github/prompts/."""
553+
agents_dir = project_dir / ".github" / "agents"
554+
agents_dir.mkdir(parents=True)
555+
556+
manifest = ExtensionManifest(extension_dir / "extension.yml")
557+
558+
registrar = CommandRegistrar()
559+
registrar.register_commands_for_agent(
560+
"copilot", manifest, extension_dir, project_dir
561+
)
562+
563+
# Verify companion .prompt.md file exists
564+
prompt_file = project_dir / ".github" / "prompts" / "speckit.test.hello.prompt.md"
565+
assert prompt_file.exists()
566+
567+
# Verify content has correct agent frontmatter
568+
content = prompt_file.read_text()
569+
assert content == "---\nagent: speckit.test.hello\n---\n"
570+
571+
def test_copilot_aliases_get_companion_prompts(self, project_dir, temp_dir):
572+
"""Test that aliases also get companion .prompt.md files for Copilot."""
573+
import yaml
574+
575+
ext_dir = temp_dir / "ext-alias-copilot"
576+
ext_dir.mkdir()
577+
578+
manifest_data = {
579+
"schema_version": "1.0",
580+
"extension": {
581+
"id": "ext-alias-copilot",
582+
"name": "Extension with Alias",
583+
"version": "1.0.0",
584+
"description": "Test",
585+
},
586+
"requires": {"speckit_version": ">=0.1.0"},
587+
"provides": {
588+
"commands": [
589+
{
590+
"name": "speckit.alias-copilot.cmd",
591+
"file": "commands/cmd.md",
592+
"aliases": ["speckit.shortcut-copilot"],
593+
}
594+
]
595+
},
596+
}
597+
598+
with open(ext_dir / "extension.yml", "w") as f:
599+
yaml.dump(manifest_data, f)
600+
601+
(ext_dir / "commands").mkdir()
602+
(ext_dir / "commands" / "cmd.md").write_text(
603+
"---\ndescription: Test\n---\n\nTest"
604+
)
605+
606+
# Set up Copilot project
607+
(project_dir / ".github" / "agents").mkdir(parents=True)
608+
609+
manifest = ExtensionManifest(ext_dir / "extension.yml")
610+
registrar = CommandRegistrar()
611+
registered = registrar.register_commands_for_agent(
612+
"copilot", manifest, ext_dir, project_dir
613+
)
614+
615+
assert len(registered) == 2
616+
617+
# Both primary and alias get companion .prompt.md
618+
prompts_dir = project_dir / ".github" / "prompts"
619+
assert (prompts_dir / "speckit.alias-copilot.cmd.prompt.md").exists()
620+
assert (prompts_dir / "speckit.shortcut-copilot.prompt.md").exists()
621+
622+
def test_non_copilot_agent_no_companion_file(self, extension_dir, project_dir):
623+
"""Test that non-copilot agents do NOT create .prompt.md files."""
624+
claude_dir = project_dir / ".claude" / "commands"
625+
claude_dir.mkdir(parents=True)
626+
627+
manifest = ExtensionManifest(extension_dir / "extension.yml")
628+
629+
registrar = CommandRegistrar()
630+
registrar.register_commands_for_agent(
631+
"claude", manifest, extension_dir, project_dir
632+
)
633+
634+
# No .github/prompts directory should exist
635+
prompts_dir = project_dir / ".github" / "prompts"
636+
assert not prompts_dir.exists()
637+
638+
def test_copilot_cleanup_removes_prompt_files(self, extension_dir, project_dir):
639+
"""Test that removing a Copilot extension also removes .prompt.md files."""
640+
agents_dir = project_dir / ".github" / "agents"
641+
agents_dir.mkdir(parents=True)
642+
643+
manifest = ExtensionManifest(extension_dir / "extension.yml")
644+
645+
registrar = CommandRegistrar()
646+
registrar.register_commands_for_agent(
647+
"copilot", manifest, extension_dir, project_dir
648+
)
649+
650+
# Verify files exist before cleanup
651+
agent_file = agents_dir / "speckit.test.hello.agent.md"
652+
prompt_file = project_dir / ".github" / "prompts" / "speckit.test.hello.prompt.md"
653+
assert agent_file.exists()
654+
assert prompt_file.exists()
655+
656+
# Manually remove command file (simulates what remove() does)
657+
agent_file.unlink()
658+
659+
# Now remove the prompt file the same way remove() does for copilot
660+
if prompt_file.exists():
661+
prompt_file.unlink()
662+
663+
assert not agent_file.exists()
664+
assert not prompt_file.exists()
665+
523666

524667
# ===== Utility Function Tests =====
525668

0 commit comments

Comments
 (0)