From a94a6ab995188d9cd65e7708bf8aae5e7855b844 Mon Sep 17 00:00:00 2001 From: Mo Khan Date: Thu, 21 May 2026 15:37:36 -0500 Subject: [PATCH 1/7] feat: add GitHub Copilot target (#149) --- AGENTS.md | 2 + src/lola/targets/__init__.py | 3 + src/lola/targets/copilot.py | 193 ++++++++++++++++++++++ tests/test_copilot_target.py | 308 +++++++++++++++++++++++++++++++++++ 4 files changed, 506 insertions(+) create mode 100644 src/lola/targets/copilot.py create mode 100644 tests/test_copilot_target.py diff --git a/AGENTS.md b/AGENTS.md index 42aa4a8..a40e18c 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -139,6 +139,7 @@ Defined in `targets.py` TARGETS dict. Each assistant has different output format | Assistant | Skills | Commands | Agents | |-----------|--------|----------|--------| | claude-code | `.claude/skills//SKILL.md` | `.claude/commands/.md` | `.claude/agents/.md` | +| copilot | `.github/skills//SKILL.md` (project) / `~/.copilot/skills//SKILL.md` (user) | `.github/prompts/.prompt.md` | `.github/agents/.agent.md` | | cursor | `.cursor/skills//SKILL.md` | `.cursor/commands/.md` | `.cursor/agents/.md` | | gemini-cli | `GEMINI.md` (managed section) | `.gemini/commands/.toml` | N/A | | openclaw | `~/.openclaw/workspace/skills//SKILL.md` | N/A | N/A | @@ -146,6 +147,7 @@ Defined in `targets.py` TARGETS dict. Each assistant has different output format Agent frontmatter is modified during generation: - Claude Code: `name` (agent name) and `model: inherit` are added +- Copilot: `name` (matching directory name) and `description` are added to skill frontmatter - Cursor: `name` (agent name) and `model: inherit` are added - OpenCode: `mode: subagent` is added diff --git a/src/lola/targets/__init__.py b/src/lola/targets/__init__.py index b9be7bf..1d2577e 100644 --- a/src/lola/targets/__init__.py +++ b/src/lola/targets/__init__.py @@ -29,6 +29,7 @@ # Concrete target implementations from lola.targets.claude_code import ClaudeCodeTarget +from lola.targets.copilot import CopilotTarget from lola.targets.cursor import CursorTarget from lola.targets.gemini import GeminiTarget, _convert_to_gemini_args from lola.targets.openclaw import OpenClawTarget @@ -49,6 +50,7 @@ TARGETS: dict[str, AssistantTarget] = { "claude-code": ClaudeCodeTarget(), + "copilot": CopilotTarget(), "cursor": CursorTarget(), "gemini-cli": GeminiTarget(), "openclaw": OpenClawTarget(), @@ -76,6 +78,7 @@ def get_target(assistant: str) -> AssistantTarget: "MCPSupportMixin", # Concrete targets "ClaudeCodeTarget", + "CopilotTarget", "CursorTarget", "GeminiTarget", "OpenClawTarget", diff --git a/src/lola/targets/copilot.py b/src/lola/targets/copilot.py new file mode 100644 index 0000000..3f76cc5 --- /dev/null +++ b/src/lola/targets/copilot.py @@ -0,0 +1,193 @@ +"""GitHub Copilot target implementation.""" + +from __future__ import annotations + +from pathlib import Path + +import lola.config as config +import lola.frontmatter as fm +from .base import ( + BaseAssistantTarget, + ManagedInstructionsTarget, + MCPSupportMixin, + _generate_passthrough_command, +) + + +class CopilotTarget(MCPSupportMixin, ManagedInstructionsTarget, BaseAssistantTarget): + """Target for GitHub Copilot (VS Code + Visual Studio). + + Copilot supports: + - Skills in .copilot/skills//SKILL.md (with name+description frontmatter) + - Prompt files in .github/prompts/*.prompt.md + - Agents in .github/agents/*.agent.md + - Global instructions in .github/copilot-instructions.md + - MCP servers in .github/copilot/mcp.json + """ + + name = "copilot" + supports_agents = True + INSTRUCTIONS_FILE = "copilot-instructions.md" + + def get_skill_path(self, project_path: str, scope: str = "project") -> Path: + if scope == "user": + return Path.home() / ".copilot" / "skills" + return Path(project_path) / ".github" / "skills" + + def get_command_path(self, project_path: str, scope: str = "project") -> Path: + if scope == "user": + return Path.home() / ".copilot" / "prompts" + return Path(project_path) / ".github" / "prompts" + + def get_agent_path(self, project_path: str, scope: str = "project") -> Path: + if scope == "user": + return Path.home() / ".copilot" / "agents" + return Path(project_path) / ".github" / "agents" + + def get_instructions_path(self, project_path: str, scope: str = "project") -> Path: + base = Path.home() if scope == "user" else Path(project_path) + return base / ".github" / self.INSTRUCTIONS_FILE + + def get_mcp_path(self, project_path: str, scope: str = "project") -> Path: + base = Path.home() if scope == "user" else Path(project_path) + return base / ".github" / "copilot" / "mcp.json" + + def generate_skill( + self, + source_path: Path, + dest_path: Path, + skill_name: str, + project_path: str | None = None, # noqa: ARG002 + ) -> bool: + """Generate SKILL.md in .copilot/skills// directory. + + Copilot skills use a directory-per-skill structure with + name + description in YAML frontmatter. + """ + if not source_path.exists(): + return False + + skill_file = source_path / config.SKILL_FILE + if not skill_file.exists(): + return False + + skill_dir = dest_path / skill_name + skill_dir.mkdir(parents=True, exist_ok=True) + + content = skill_file.read_text() + frontmatter, body = fm.parse(content) + + # Build Copilot-compatible frontmatter (requires name + description) + import yaml + + copilot_fm: dict = {"name": skill_name} + if frontmatter.get("description"): + copilot_fm["description"] = frontmatter["description"] + if frontmatter.get("applyTo"): + copilot_fm["applyTo"] = frontmatter["applyTo"] + elif frontmatter.get("globs"): + copilot_fm["applyTo"] = frontmatter["globs"] + + fm_str = yaml.dump( + copilot_fm, default_flow_style=False, sort_keys=False + ).rstrip() + output = f"---\n{fm_str}\n---\n{body}" + + dest_file = skill_dir / "SKILL.md" + dest_file.write_text(output) + return True + + def remove_skill(self, dest_path: Path, skill_name: str) -> bool: + """Remove a skill's directory.""" + import shutil + + skill_dir = dest_path / skill_name + if skill_dir.exists(): + shutil.rmtree(skill_dir) + return True + # Legacy cleanup: old .instructions.md format + legacy_file = ( + dest_path.parent + / ".github" + / "instructions" + / f"{skill_name}.instructions.md" + ) + if legacy_file.exists(): + legacy_file.unlink() + return True + return False + + def generate_command( + self, + source_path: Path, + dest_dir: Path, + cmd_name: str, + module_name: str, + ) -> bool: + filename = self.get_command_filename(module_name, cmd_name) + return _generate_passthrough_command(source_path, dest_dir, filename) + + def get_command_filename(self, module_name: str, cmd_name: str) -> str: # noqa: ARG002 + """Copilot uses .prompt.md extension for commands.""" + return f"{cmd_name}.prompt.md" + + def generate_agent( + self, + source_path: Path, + dest_dir: Path, + agent_name: str, + module_name: str, + ) -> bool: + """Generate agent file with .agent.md extension. + + Copilot agents use YAML frontmatter with fields like: + - description: when to use this agent + - tools: list of tools the agent can use + """ + if not source_path.exists(): + return False + dest_dir.mkdir(parents=True, exist_ok=True) + + filename = self.get_agent_filename(module_name, agent_name) + content = source_path.read_text() + + (dest_dir / filename).write_text(content) + return True + + def get_agent_filename(self, module_name: str, agent_name: str) -> str: # noqa: ARG002 + """Copilot uses .agent.md extension for agents.""" + return f"{agent_name}.agent.md" + + def remove_command( + self, + dest_dir: Path, + cmd_name: str, + module_name: str, + ) -> bool: + """Delete command file (.prompt.md).""" + filename = self.get_command_filename(module_name, cmd_name) + cmd_file = dest_dir / filename + if cmd_file.exists(): + cmd_file.unlink() + # Legacy cleanup + legacy_file = dest_dir / f"{module_name}.{cmd_name}.prompt.md" + if legacy_file.exists(): + legacy_file.unlink() + return True + + def remove_agent( + self, + dest_dir: Path, + agent_name: str, + module_name: str, + ) -> bool: + """Delete agent file (.agent.md).""" + filename = self.get_agent_filename(module_name, agent_name) + agent_file = dest_dir / filename + if agent_file.exists(): + agent_file.unlink() + # Legacy cleanup + legacy_file = dest_dir / f"{module_name}.{agent_name}.agent.md" + if legacy_file.exists(): + legacy_file.unlink() + return True diff --git a/tests/test_copilot_target.py b/tests/test_copilot_target.py new file mode 100644 index 0000000..e242fbe --- /dev/null +++ b/tests/test_copilot_target.py @@ -0,0 +1,308 @@ +"""Tests for CopilotTarget scope-aware path resolution and file generation.""" + +from pathlib import Path + +from lola.targets.copilot import CopilotTarget + + +# --- Project scope path tests --- + + +def test_copilot_skill_path_project_scope(): + target = CopilotTarget() + path = target.get_skill_path("/home/user/project", "project") + assert path == Path("/home/user/project/.github/skills") + + +def test_copilot_command_path_project_scope(): + target = CopilotTarget() + path = target.get_command_path("/home/user/project", "project") + assert path == Path("/home/user/project/.github/prompts") + + +def test_copilot_agent_path_project_scope(): + target = CopilotTarget() + path = target.get_agent_path("/home/user/project", "project") + assert path == Path("/home/user/project/.github/agents") + + +def test_copilot_instructions_path_project_scope(): + target = CopilotTarget() + path = target.get_instructions_path("/home/user/project", "project") + assert path == Path("/home/user/project/.github/copilot-instructions.md") + + +def test_copilot_mcp_path_project_scope(): + target = CopilotTarget() + path = target.get_mcp_path("/home/user/project", "project") + assert path == Path("/home/user/project/.github/copilot/mcp.json") + + +# --- User scope path tests --- + + +def test_copilot_skill_path_user_scope(): + target = CopilotTarget() + path = target.get_skill_path("/home/user/project", "user") + assert path == Path.home() / ".copilot" / "skills" + + +def test_copilot_command_path_user_scope(): + target = CopilotTarget() + path = target.get_command_path("/home/user/project", "user") + assert path == Path.home() / ".copilot" / "prompts" + + +def test_copilot_agent_path_user_scope(): + target = CopilotTarget() + path = target.get_agent_path("/home/user/project", "user") + assert path == Path.home() / ".copilot" / "agents" + + +def test_copilot_instructions_path_user_scope(): + target = CopilotTarget() + path = target.get_instructions_path("/home/user/project", "user") + assert path == Path.home() / ".github" / "copilot-instructions.md" + + +def test_copilot_mcp_path_user_scope(): + target = CopilotTarget() + path = target.get_mcp_path("/home/user/project", "user") + assert path == Path.home() / ".github" / "copilot" / "mcp.json" + + +# --- Default scope tests --- + + +def test_copilot_skill_path_default_scope(): + target = CopilotTarget() + path = target.get_skill_path("/home/user/project") + assert path == Path("/home/user/project/.github/skills") + + +def test_copilot_command_path_default_scope(): + target = CopilotTarget() + path = target.get_command_path("/home/user/project") + assert path == Path("/home/user/project/.github/prompts") + + +def test_copilot_agent_path_default_scope(): + target = CopilotTarget() + path = target.get_agent_path("/home/user/project") + assert path == Path("/home/user/project/.github/agents") + + +def test_copilot_instructions_path_default_scope(): + target = CopilotTarget() + path = target.get_instructions_path("/home/user/project") + assert path == Path("/home/user/project/.github/copilot-instructions.md") + + +def test_copilot_mcp_path_default_scope(): + target = CopilotTarget() + path = target.get_mcp_path("/home/user/project") + assert path == Path("/home/user/project/.github/copilot/mcp.json") + + +# --- Skill generation tests --- + + +def test_generate_skill_basic(tmp_path): + """Generate SKILL.md in skill directory with name frontmatter.""" + target = CopilotTarget() + source = tmp_path / "my-skill" + source.mkdir() + (source / "SKILL.md").write_text("Do the thing.\n") + + dest = tmp_path / "skills" + result = target.generate_skill(source, dest, "my-skill") + + assert result is True + output_file = dest / "my-skill" / "SKILL.md" + assert output_file.exists() + content = output_file.read_text() + assert "name: my-skill" in content + assert "Do the thing." in content + + +def test_generate_skill_with_apply_to(tmp_path): + """Generate SKILL.md preserving applyTo and description frontmatter.""" + target = CopilotTarget() + source = tmp_path / "tf-skill" + source.mkdir() + (source / "SKILL.md").write_text( + '---\napplyTo: "**/*.tf"\ndescription: Terraform help\n---\n\nTerraform instructions.\n' + ) + + dest = tmp_path / "skills" + result = target.generate_skill(source, dest, "tf-skill") + + assert result is True + output_file = dest / "tf-skill" / "SKILL.md" + content = output_file.read_text() + assert "name: tf-skill" in content + assert "description: Terraform help" in content + assert "applyTo:" in content + assert "**/*.tf" in content + assert "Terraform instructions." in content + + +def test_generate_skill_with_globs_as_apply_to(tmp_path): + """Generate SKILL.md converting globs to applyTo.""" + target = CopilotTarget() + source = tmp_path / "py-skill" + source.mkdir() + (source / "SKILL.md").write_text( + '---\nglobs: "**/*.py"\ndescription: Python help\n---\n\nPython instructions.\n' + ) + + dest = tmp_path / "skills" + result = target.generate_skill(source, dest, "py-skill") + + assert result is True + output_file = dest / "py-skill" / "SKILL.md" + content = output_file.read_text() + assert "applyTo:" in content + assert "**/*.py" in content + + +def test_generate_skill_missing_source(tmp_path): + """Return False if source doesn't exist.""" + target = CopilotTarget() + source = tmp_path / "nonexistent" + dest = tmp_path / "skills" + + result = target.generate_skill(source, dest, "nonexistent") + assert result is False + + +def test_generate_skill_missing_skill_md(tmp_path): + """Return False if SKILL.md doesn't exist in source.""" + target = CopilotTarget() + source = tmp_path / "empty-skill" + source.mkdir() + dest = tmp_path / "skills" + + result = target.generate_skill(source, dest, "empty-skill") + assert result is False + + +# --- Skill removal tests --- + + +def test_remove_skill(tmp_path): + """Remove existing skill directory.""" + target = CopilotTarget() + dest = tmp_path / "skills" + skill_dir = dest / "my-skill" + skill_dir.mkdir(parents=True) + (skill_dir / "SKILL.md").write_text("content") + + result = target.remove_skill(dest, "my-skill") + assert result is True + assert not skill_dir.exists() + + +def test_remove_skill_not_found(tmp_path): + """Return False if skill directory doesn't exist.""" + target = CopilotTarget() + dest = tmp_path / "skills" + dest.mkdir() + + result = target.remove_skill(dest, "missing") + assert result is False + + +# --- Command generation tests --- + + +def test_generate_command(tmp_path): + """Generate .prompt.md command file.""" + target = CopilotTarget() + source = tmp_path / "review.md" + source.write_text("Review the code.\n") + dest = tmp_path / "prompts" + + result = target.generate_command(source, dest, "review", "my-module") + assert result is True + assert (dest / "review.prompt.md").exists() + assert (dest / "review.prompt.md").read_text() == "Review the code.\n" + + +def test_command_filename(): + """Command filename uses .prompt.md extension.""" + target = CopilotTarget() + assert target.get_command_filename("my-module", "review") == "review.prompt.md" + + +# --- Agent generation tests --- + + +def test_generate_agent(tmp_path): + """Generate .agent.md agent file.""" + target = CopilotTarget() + source = tmp_path / "reviewer.md" + source.write_text("---\ndescription: Reviews code\n---\n\nAgent content.\n") + dest = tmp_path / "agents" + + result = target.generate_agent(source, dest, "reviewer", "my-module") + assert result is True + output = dest / "reviewer.agent.md" + assert output.exists() + assert "description: Reviews code" in output.read_text() + + +def test_generate_agent_missing_source(tmp_path): + """Return False if agent source doesn't exist.""" + target = CopilotTarget() + source = tmp_path / "missing.md" + dest = tmp_path / "agents" + + result = target.generate_agent(source, dest, "missing", "my-module") + assert result is False + + +def test_agent_filename(): + """Agent filename uses .agent.md extension.""" + target = CopilotTarget() + assert target.get_agent_filename("my-module", "reviewer") == "reviewer.agent.md" + + +# --- Remove command/agent tests --- + + +def test_remove_command(tmp_path): + """Remove .prompt.md command file.""" + target = CopilotTarget() + dest = tmp_path / "prompts" + dest.mkdir() + (dest / "review.prompt.md").write_text("content") + + result = target.remove_command(dest, "review", "my-module") + assert result is True + assert not (dest / "review.prompt.md").exists() + + +def test_remove_agent(tmp_path): + """Remove .agent.md agent file.""" + target = CopilotTarget() + dest = tmp_path / "agents" + dest.mkdir() + (dest / "reviewer.agent.md").write_text("content") + + result = target.remove_agent(dest, "reviewer", "my-module") + assert result is True + assert not (dest / "reviewer.agent.md").exists() + + +# --- Target metadata --- + + +def test_copilot_target_name(): + target = CopilotTarget() + assert target.name == "copilot" + + +def test_copilot_supports_agents(): + target = CopilotTarget() + assert target.supports_agents is True From 954876705cfda79b817b8a0f0a4de2f59da30642 Mon Sep 17 00:00:00 2001 From: Mo Khan Date: Thu, 21 May 2026 16:14:41 -0500 Subject: [PATCH 2/7] fix: require description in skill frontmatter, fix legacy path --- src/lola/targets/copilot.py | 22 ++++++++++++---------- tests/test_copilot_target.py | 19 +++++++++++++++++-- 2 files changed, 29 insertions(+), 12 deletions(-) diff --git a/src/lola/targets/copilot.py b/src/lola/targets/copilot.py index 3f76cc5..917aa65 100644 --- a/src/lola/targets/copilot.py +++ b/src/lola/targets/copilot.py @@ -71,18 +71,23 @@ def generate_skill( if not skill_file.exists(): return False - skill_dir = dest_path / skill_name - skill_dir.mkdir(parents=True, exist_ok=True) - content = skill_file.read_text() frontmatter, body = fm.parse(content) + description = frontmatter.get("description") + if not description: + return False + + skill_dir = dest_path / skill_name + skill_dir.mkdir(parents=True, exist_ok=True) + # Build Copilot-compatible frontmatter (requires name + description) import yaml - copilot_fm: dict = {"name": skill_name} - if frontmatter.get("description"): - copilot_fm["description"] = frontmatter["description"] + copilot_fm: dict = { + "name": skill_name, + "description": description, + } if frontmatter.get("applyTo"): copilot_fm["applyTo"] = frontmatter["applyTo"] elif frontmatter.get("globs"): @@ -107,10 +112,7 @@ def remove_skill(self, dest_path: Path, skill_name: str) -> bool: return True # Legacy cleanup: old .instructions.md format legacy_file = ( - dest_path.parent - / ".github" - / "instructions" - / f"{skill_name}.instructions.md" + dest_path.parent / "instructions" / f"{skill_name}.instructions.md" ) if legacy_file.exists(): legacy_file.unlink() diff --git a/tests/test_copilot_target.py b/tests/test_copilot_target.py index e242fbe..067fbca 100644 --- a/tests/test_copilot_target.py +++ b/tests/test_copilot_target.py @@ -108,11 +108,13 @@ def test_copilot_mcp_path_default_scope(): def test_generate_skill_basic(tmp_path): - """Generate SKILL.md in skill directory with name frontmatter.""" + """Generate SKILL.md in skill directory with name + description frontmatter.""" target = CopilotTarget() source = tmp_path / "my-skill" source.mkdir() - (source / "SKILL.md").write_text("Do the thing.\n") + (source / "SKILL.md").write_text( + "---\ndescription: Does the thing\n---\n\nDo the thing.\n" + ) dest = tmp_path / "skills" result = target.generate_skill(source, dest, "my-skill") @@ -122,9 +124,22 @@ def test_generate_skill_basic(tmp_path): assert output_file.exists() content = output_file.read_text() assert "name: my-skill" in content + assert "description: Does the thing" in content assert "Do the thing." in content +def test_generate_skill_missing_description(tmp_path): + """Return False if SKILL.md has no description (required by Copilot).""" + target = CopilotTarget() + source = tmp_path / "my-skill" + source.mkdir() + (source / "SKILL.md").write_text("No frontmatter here.\n") + + dest = tmp_path / "skills" + result = target.generate_skill(source, dest, "my-skill") + assert result is False + + def test_generate_skill_with_apply_to(tmp_path): """Generate SKILL.md preserving applyTo and description frontmatter.""" target = CopilotTarget() From d3418094c13fbc4fc8ddb818684d19bb9977e91a Mon Sep 17 00:00:00 2001 From: Mo Khan Date: Thu, 21 May 2026 16:15:12 -0500 Subject: [PATCH 3/7] docs: fix frontmatter section wording for copilot --- AGENTS.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/AGENTS.md b/AGENTS.md index a40e18c..510b358 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -147,7 +147,7 @@ Defined in `targets.py` TARGETS dict. Each assistant has different output format Agent frontmatter is modified during generation: - Claude Code: `name` (agent name) and `model: inherit` are added -- Copilot: `name` (matching directory name) and `description` are added to skill frontmatter +- Copilot: `name` (matching directory name) and `description` are added - Cursor: `name` (agent name) and `model: inherit` are added - OpenCode: `mode: subagent` is added From 412183b8f834029725deb4fa0617c7e73767c67d Mon Sep 17 00:00:00 2001 From: Mo Khan Date: Thu, 21 May 2026 16:28:36 -0500 Subject: [PATCH 4/7] fix: ensure legacy skill cleanup runs even when skill dir exists --- src/lola/targets/copilot.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/lola/targets/copilot.py b/src/lola/targets/copilot.py index 917aa65..8df47ef 100644 --- a/src/lola/targets/copilot.py +++ b/src/lola/targets/copilot.py @@ -106,18 +106,19 @@ def remove_skill(self, dest_path: Path, skill_name: str) -> bool: """Remove a skill's directory.""" import shutil + removed = False skill_dir = dest_path / skill_name if skill_dir.exists(): shutil.rmtree(skill_dir) - return True + removed = True # Legacy cleanup: old .instructions.md format legacy_file = ( dest_path.parent / "instructions" / f"{skill_name}.instructions.md" ) if legacy_file.exists(): legacy_file.unlink() - return True - return False + removed = True + return removed def generate_command( self, From 978b12d59ab046cd528d0b5256902d9bee0ff003 Mon Sep 17 00:00:00 2001 From: Mo Khan Date: Thu, 21 May 2026 16:29:03 -0500 Subject: [PATCH 5/7] test: add regression test for legacy skill cleanup --- tests/test_copilot_target.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/tests/test_copilot_target.py b/tests/test_copilot_target.py index 067fbca..b225cae 100644 --- a/tests/test_copilot_target.py +++ b/tests/test_copilot_target.py @@ -228,6 +228,25 @@ def test_remove_skill_not_found(tmp_path): assert result is False +def test_remove_skill_legacy_instructions(tmp_path): + """Remove both skill dir and legacy .instructions.md during uninstall.""" + target = CopilotTarget() + # dest_path is .github/skills, so parent is .github + dest = tmp_path / ".github" / "skills" + skill_dir = dest / "my-skill" + skill_dir.mkdir(parents=True) + (skill_dir / "SKILL.md").write_text("content") + instructions_dir = tmp_path / ".github" / "instructions" + instructions_dir.mkdir() + legacy_file = instructions_dir / "my-skill.instructions.md" + legacy_file.write_text("old format") + + result = target.remove_skill(dest, "my-skill") + assert result is True + assert not skill_dir.exists() + assert not legacy_file.exists() + + # --- Command generation tests --- From bfd0d4a56de9e7c7fff9f48e8a133d646693668f Mon Sep 17 00:00:00 2001 From: Mo Khan Date: Sun, 24 May 2026 22:26:24 -0500 Subject: [PATCH 6/7] docs: clarify copilot target paths and agent passthrough behavior --- AGENTS.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 510b358..fa60fbe 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -139,7 +139,7 @@ Defined in `targets.py` TARGETS dict. Each assistant has different output format | Assistant | Skills | Commands | Agents | |-----------|--------|----------|--------| | claude-code | `.claude/skills//SKILL.md` | `.claude/commands/.md` | `.claude/agents/.md` | -| copilot | `.github/skills//SKILL.md` (project) / `~/.copilot/skills//SKILL.md` (user) | `.github/prompts/.prompt.md` | `.github/agents/.agent.md` | +| copilot | `.github/skills//SKILL.md` (project) / `~/.copilot/skills//SKILL.md` (user) | `.github/prompts/.prompt.md` (project) / `~/.copilot/prompts/.prompt.md` (user) | `.github/agents/.agent.md` (project) / `~/.copilot/agents/.agent.md` (user) | | cursor | `.cursor/skills//SKILL.md` | `.cursor/commands/.md` | `.cursor/agents/.md` | | gemini-cli | `GEMINI.md` (managed section) | `.gemini/commands/.toml` | N/A | | openclaw | `~/.openclaw/workspace/skills//SKILL.md` | N/A | N/A | @@ -147,7 +147,7 @@ Defined in `targets.py` TARGETS dict. Each assistant has different output format Agent frontmatter is modified during generation: - Claude Code: `name` (agent name) and `model: inherit` are added -- Copilot: `name` (matching directory name) and `description` are added +- Copilot: `generate_agent` is passthrough (content copied as-is); skill frontmatter is rewritten to include `name` and `description` - Cursor: `name` (agent name) and `model: inherit` are added - OpenCode: `mode: subagent` is added From 2e00b4e3c8ad1965456eff2a8304e0d70143d862 Mon Sep 17 00:00:00 2001 From: Mo Khan Date: Sun, 24 May 2026 22:34:26 -0500 Subject: [PATCH 7/7] fix: use ~/.copilot/ for user-scope instructions and MCP paths --- src/lola/targets/copilot.py | 10 ++++++---- tests/test_copilot_target.py | 4 ++-- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/src/lola/targets/copilot.py b/src/lola/targets/copilot.py index 8df47ef..1bd12d1 100644 --- a/src/lola/targets/copilot.py +++ b/src/lola/targets/copilot.py @@ -45,12 +45,14 @@ def get_agent_path(self, project_path: str, scope: str = "project") -> Path: return Path(project_path) / ".github" / "agents" def get_instructions_path(self, project_path: str, scope: str = "project") -> Path: - base = Path.home() if scope == "user" else Path(project_path) - return base / ".github" / self.INSTRUCTIONS_FILE + if scope == "user": + return Path.home() / ".copilot" / self.INSTRUCTIONS_FILE + return Path(project_path) / ".github" / self.INSTRUCTIONS_FILE def get_mcp_path(self, project_path: str, scope: str = "project") -> Path: - base = Path.home() if scope == "user" else Path(project_path) - return base / ".github" / "copilot" / "mcp.json" + if scope == "user": + return Path.home() / ".copilot" / "mcp.json" + return Path(project_path) / ".github" / "copilot" / "mcp.json" def generate_skill( self, diff --git a/tests/test_copilot_target.py b/tests/test_copilot_target.py index b225cae..d1a4c48 100644 --- a/tests/test_copilot_target.py +++ b/tests/test_copilot_target.py @@ -62,13 +62,13 @@ def test_copilot_agent_path_user_scope(): def test_copilot_instructions_path_user_scope(): target = CopilotTarget() path = target.get_instructions_path("/home/user/project", "user") - assert path == Path.home() / ".github" / "copilot-instructions.md" + assert path == Path.home() / ".copilot" / "copilot-instructions.md" def test_copilot_mcp_path_user_scope(): target = CopilotTarget() path = target.get_mcp_path("/home/user/project", "user") - assert path == Path.home() / ".github" / "copilot" / "mcp.json" + assert path == Path.home() / ".copilot" / "mcp.json" # --- Default scope tests ---