55- Each command gets a companion ``.prompt.md`` file in ``.github/prompts/``
66- Installs ``.vscode/settings.json`` with prompt file recommendations
77- Context file lives at ``.github/copilot-instructions.md``
8+
9+ When ``--skills`` is passed via ``--integration-options``, Copilot scaffolds
10+ commands as ``speckit-<name>/SKILL.md`` directories under ``.github/skills/``
11+ instead. The two modes are mutually exclusive.
812"""
913
1014from __future__ import annotations
1620from pathlib import Path
1721from typing import Any
1822
19- from ..base import IntegrationBase
23+ from ..base import IntegrationBase , IntegrationOption , SkillsIntegration
2024from ..manifest import IntegrationManifest
2125
2226
@@ -44,12 +48,40 @@ def _allow_all() -> bool:
4448 return True
4549
4650
51+ class _CopilotSkillsHelper (SkillsIntegration ):
52+ """Internal helper used when Copilot is scaffolded in skills mode.
53+
54+ Not registered in the integration registry — only used as a delegate
55+ by ``CopilotIntegration`` when ``--skills`` is passed.
56+ """
57+
58+ key = "copilot"
59+ config = {
60+ "name" : "GitHub Copilot" ,
61+ "folder" : ".github/" ,
62+ "commands_subdir" : "skills" ,
63+ "install_url" : "https://docs.github.com/en/copilot/concepts/agents/copilot-cli/about-copilot-cli" ,
64+ "requires_cli" : False ,
65+ }
66+ registrar_config = {
67+ "dir" : ".github/skills" ,
68+ "format" : "markdown" ,
69+ "args" : "$ARGUMENTS" ,
70+ "extension" : "/SKILL.md" ,
71+ }
72+ context_file = ".github/copilot-instructions.md"
73+
74+
4775class CopilotIntegration (IntegrationBase ):
4876 """Integration for GitHub Copilot (VS Code IDE + CLI).
4977
5078 The IDE integration (``requires_cli: False``) installs ``.agent.md``
5179 command files. Workflow dispatch additionally requires the
5280 ``copilot`` CLI to be installed separately.
81+
82+ When ``--skills`` is passed via ``--integration-options``, commands
83+ are scaffolded as ``speckit-<name>/SKILL.md`` under ``.github/skills/``
84+ instead of the default ``.agent.md`` + ``.prompt.md`` layout.
5385 """
5486
5587 key = "copilot"
@@ -68,6 +100,20 @@ class CopilotIntegration(IntegrationBase):
68100 }
69101 context_file = ".github/copilot-instructions.md"
70102
103+ # Mutable flag set by setup() — indicates the active scaffolding mode.
104+ _skills_mode : bool = False
105+
106+ @classmethod
107+ def options (cls ) -> list [IntegrationOption ]:
108+ return [
109+ IntegrationOption (
110+ "--skills" ,
111+ is_flag = True ,
112+ default = False ,
113+ help = "Scaffold commands as agent skills (speckit-<name>/SKILL.md) instead of .agent.md files" ,
114+ ),
115+ ]
116+
71117 def build_exec_args (
72118 self ,
73119 prompt : str ,
@@ -92,7 +138,19 @@ def build_exec_args(
92138 return args
93139
94140 def build_command_invocation (self , command_name : str , args : str = "" ) -> str :
95- """Copilot agents are not slash-commands — just return the args as prompt."""
141+ """Build the native invocation for a Copilot command.
142+
143+ Default mode: agents are not slash-commands — return args as prompt.
144+ Skills mode: ``/speckit-<stem>`` slash-command dispatch.
145+ """
146+ if self ._skills_mode :
147+ stem = command_name
148+ if "." in stem :
149+ stem = stem .rsplit ("." , 1 )[- 1 ]
150+ invocation = f"/speckit-{ stem } "
151+ if args :
152+ invocation = f"{ invocation } { args } "
153+ return invocation
96154 return args or ""
97155
98156 def dispatch_command (
@@ -110,19 +168,32 @@ def dispatch_command(
110168 Copilot ``.agent.md`` files are agents, not skills. The CLI
111169 selects them with ``--agent <name>`` and the prompt is just
112170 the user's arguments.
171+
172+ In skills mode, the prompt includes the skill invocation
173+ (``/speckit-<stem>``).
113174 """
114175 import subprocess
115176
116177 stem = command_name
117178 if "." in stem :
118179 stem = stem .rsplit ("." , 1 )[- 1 ]
119- agent_name = f"speckit.{ stem } "
120180
121- prompt = args or ""
122- cli_args = [
123- "copilot" , "-p" , prompt ,
124- "--agent" , agent_name ,
125- ]
181+ # Detect skills mode from project layout when not set via setup()
182+ skills_mode = self ._skills_mode
183+ if not skills_mode and project_root :
184+ skills_dir = project_root / ".github" / "skills"
185+ if skills_dir .is_dir () and any (skills_dir .iterdir ()):
186+ skills_mode = True
187+
188+ if skills_mode :
189+ prompt = self .build_command_invocation (command_name , args )
190+ else :
191+ agent_name = f"speckit.{ stem } "
192+ prompt = args or ""
193+
194+ cli_args = ["copilot" , "-p" , prompt ]
195+ if not skills_mode :
196+ cli_args .extend (["--agent" , f"speckit.{ stem } " ])
126197 if _allow_all ():
127198 cli_args .append ("--yolo" )
128199 if model :
@@ -168,6 +239,59 @@ def command_filename(self, template_name: str) -> str:
168239 """Copilot commands use ``.agent.md`` extension."""
169240 return f"speckit.{ template_name } .agent.md"
170241
242+ def post_process_skill_content (self , content : str ) -> str :
243+ """Inject Copilot-specific ``mode:`` field into SKILL.md frontmatter.
244+
245+ Inserts ``mode: "speckit.<stem>"`` before the closing ``---`` so
246+ Copilot can associate the skill with its agent mode.
247+ """
248+ lines = content .splitlines (keepends = True )
249+
250+ # Extract skill name from frontmatter to derive the mode value
251+ dash_count = 0
252+ skill_name = ""
253+ for line in lines :
254+ stripped = line .rstrip ("\n \r " )
255+ if stripped == "---" :
256+ dash_count += 1
257+ if dash_count == 2 :
258+ break
259+ continue
260+ if dash_count == 1 :
261+ if stripped .startswith ("mode:" ):
262+ return content # already present
263+ if stripped .startswith ("name:" ):
264+ # Parse: name: "speckit-plan" → speckit.plan
265+ val = stripped .split (":" , 1 )[1 ].strip ().strip ('"' ).strip ("'" )
266+ # Convert speckit-plan → speckit.plan
267+ if val .startswith ("speckit-" ):
268+ skill_name = "speckit." + val [len ("speckit-" ):]
269+ else :
270+ skill_name = val
271+
272+ if not skill_name :
273+ return content
274+
275+ # Inject mode: before the closing --- of frontmatter
276+ out : list [str ] = []
277+ dash_count = 0
278+ injected = False
279+ for line in lines :
280+ stripped = line .rstrip ("\n \r " )
281+ if stripped == "---" :
282+ dash_count += 1
283+ if dash_count == 2 and not injected :
284+ if line .endswith ("\r \n " ):
285+ eol = "\r \n "
286+ elif line .endswith ("\n " ):
287+ eol = "\n "
288+ else :
289+ eol = ""
290+ out .append (f"mode: { skill_name } { eol } " )
291+ injected = True
292+ out .append (line )
293+ return "" .join (out )
294+
171295 def setup (
172296 self ,
173297 project_root : Path ,
@@ -177,10 +301,24 @@ def setup(
177301 ) -> list [Path ]:
178302 """Install copilot commands, companion prompts, and VS Code settings.
179303
180- Uses base class primitives to: read templates, process them
181- (replace placeholders, strip script blocks, rewrite paths),
182- write as ``.agent.md``, then add companion prompts and VS Code settings .
304+ When ``parsed_options["skills"]`` is truthy, delegates to skills
305+ scaffolding (``speckit-<name>/SKILL.md`` under ``.github/skills/``).
306+ Otherwise uses the default ``.agent.md`` + ``.prompt.md`` layout .
183307 """
308+ parsed_options = parsed_options or {}
309+ if parsed_options .get ("skills" ):
310+ self ._skills_mode = True
311+ return self ._setup_skills (project_root , manifest , parsed_options , ** opts )
312+ return self ._setup_default (project_root , manifest , parsed_options , ** opts )
313+
314+ def _setup_default (
315+ self ,
316+ project_root : Path ,
317+ manifest : IntegrationManifest ,
318+ parsed_options : dict [str , Any ] | None = None ,
319+ ** opts : Any ,
320+ ) -> list [Path ]:
321+ """Default mode: .agent.md + .prompt.md + VS Code settings merge."""
184322 project_root_resolved = project_root .resolve ()
185323 if manifest .project_root != project_root_resolved :
186324 raise ValueError (
@@ -252,6 +390,37 @@ def setup(
252390
253391 return created
254392
393+ def _setup_skills (
394+ self ,
395+ project_root : Path ,
396+ manifest : IntegrationManifest ,
397+ parsed_options : dict [str , Any ] | None = None ,
398+ ** opts : Any ,
399+ ) -> list [Path ]:
400+ """Skills mode: delegate to ``_CopilotSkillsHelper`` then post-process."""
401+ helper = _CopilotSkillsHelper ()
402+ created = SkillsIntegration .setup (
403+ helper , project_root , manifest , parsed_options , ** opts
404+ )
405+
406+ # Post-process generated skill files with Copilot-specific frontmatter
407+ skills_dir = helper .skills_dest (project_root ).resolve ()
408+ for path in created :
409+ try :
410+ path .resolve ().relative_to (skills_dir )
411+ except ValueError :
412+ continue
413+ if path .name != "SKILL.md" :
414+ continue
415+
416+ content = path .read_text (encoding = "utf-8" )
417+ updated = self .post_process_skill_content (content )
418+ if updated != content :
419+ path .write_bytes (updated .encode ("utf-8" ))
420+ self .record_file_in_manifest (path , project_root , manifest )
421+
422+ return created
423+
255424 def _vscode_settings_path (self ) -> Path | None :
256425 """Return path to the bundled vscode-settings.json template."""
257426 tpl_dir = self .shared_templates_dir ()
0 commit comments