diff --git a/.gitignore b/.gitignore index 07a05ae..a6e7a5a 100644 --- a/.gitignore +++ b/.gitignore @@ -28,10 +28,6 @@ node_modules # Generated harness artifacts (all gitignored — regenerate via `make generate HARNESS=`) # Source-of-truth lives under plugins/. See docs/harnesses.md. -# Copilot -.github/agents/ -.github/skills/ - # Codex CLI .codex/ # AGENTS.md is committed as the canonical context file (per OpenAI harness engineering). @@ -51,5 +47,5 @@ commands/**/*.toml /commands/ /agents/ /skills/ -# .agent.md files belong in .github/agents/ – guard Gemini output from pollution +# Guard Gemini output from pollution (generated .agent.md files in root agents/) agents/*.agent.md diff --git a/tools/adapters/copilot.py b/tools/adapters/copilot.py index d998413..ba32b55 100644 --- a/tools/adapters/copilot.py +++ b/tools/adapters/copilot.py @@ -61,19 +61,21 @@ def _build_tools_list(agent_tools: list[str]) -> list[str]: class CopilotAdapter(HarnessAdapter): - """Emit Copilot agent profiles (.agent.md) and skills (SKILL.md) to a local output tree. + """Emit Copilot agent profiles (.agent.md) and skills (SKILL.md) to Copilot config dir. - Agents go to ``.github/agents/__.agent.md``, skills to - ``.github/skills/__/SKILL.md``. Tool names are rewritten from - Claude Code CamelCase to Copilot lowercase. Model aliases are mapped to - the GPT-5 family (same as Codex CLI). + Agents go to ``agents/__.agent.md``, skills to + ``skills/__/SKILL.md`` under the Copilot config directory + (default ``~/.copilot/``). Tool names are rewritten from Claude Code + CamelCase to Copilot lowercase. Model aliases are mapped to the GPT-5 + family (same as Codex CLI). """ harness_id = "copilot" + COPILOT_CONFIG_DIR = Path.home() / ".copilot" def __init__(self, output_root: Path | None = None, repo_root: Path | None = None) -> None: - """Set output root (defaults to WORKTREE / .github) and optional repo root.""" - super().__init__(output_root=output_root) + """Set output root (defaults to ~/.copilot) and optional repo root.""" + super().__init__(output_root=output_root or self.COPILOT_CONFIG_DIR) if repo_root is not None: self.repo_root = repo_root @@ -97,7 +99,7 @@ class CopilotAdapter(HarnessAdapter): names, and resolves model aliases before writing. """ agent_id = f"{plugin.name}__{agent.name}" - rel = Path(".github") / "agents" / f"{agent_id}.agent.md" + rel = Path("agents") / f"{agent_id}.agent.md" model, warning = resolve_model("copilot", agent.model) if warning: @@ -125,7 +127,7 @@ class CopilotAdapter(HarnessAdapter): phrases) and body verbatim, wrapped in YAML frontmatter. """ skill_id = f"{plugin.name}__{skill.name}" - skill_dir = Path(".github") / "skills" / skill_id + skill_dir = Path("skills") / skill_id content = _copilot_frontmatter(skill.frontmatter) + "\n\n" + skill.body.rstrip() + "\n" result.written.append(self.write(skill_dir / "SKILL.md", content)) diff --git a/tools/doc_gardener.py b/tools/doc_gardener.py index 15223d9..25f9c9a 100644 --- a/tools/doc_gardener.py +++ b/tools/doc_gardener.py @@ -215,6 +215,28 @@ def check_stale_artifacts(report: Report) -> None: if src.is_file(): pairs.append((src, toml_path)) + # Copilot agents (.agent.md) and skills (SKILL.md) at ~/.copilot/ + copilot_root = Path.home() / ".copilot" + copilot_agents = copilot_root / "agents" + if copilot_agents.is_dir(): + for agent_md in copilot_agents.glob("*.agent.md"): + name = agent_md.stem + if "__" in name: + plugin, agent = name.split("__", 1) + src = PLUGINS_DIR / plugin / "agents" / f"{agent}.md" + if src.is_file(): + pairs.append((src, agent_md)) + + copilot_skills = copilot_root / "skills" + if copilot_skills.is_dir(): + for skill_md in copilot_skills.glob("*/SKILL.md"): + name = skill_md.parent.name + if "__" in name: + plugin, leaf = name.split("__", 1) + src = PLUGINS_DIR / plugin / "skills" / leaf / "SKILL.md" + if src.is_file(): + pairs.append((src, skill_md)) + for src, gen in pairs: if src.stat().st_mtime > gen.stat().st_mtime + 1: # 1s grace # Derive the plugin name correctly regardless of source layout. diff --git a/tools/generate.py b/tools/generate.py index bd63a48..9a23886 100644 --- a/tools/generate.py +++ b/tools/generate.py @@ -34,7 +34,8 @@ _HARNESS_TARGETS = { "cursor": [".cursor", ".cursor-plugin"], "opencode": [".opencode", "opencode.json"], "gemini": ["commands", "agents", "skills"], - "copilot": [".github/agents", ".github/skills"], + # Copilot writes directly to ~/.copilot/ — no repo-local output to clean. + "copilot": [], } @@ -59,7 +60,7 @@ def get_adapter(harness_id: str, output_root: Path) -> HarnessAdapter: if harness_id == "copilot": from tools.adapters.copilot import CopilotAdapter - return CopilotAdapter(output_root=output_root) + return CopilotAdapter(output_root=output_root or Path.home() / ".copilot") raise ValueError(f"Unknown harness: {harness_id}. Supported: {supported_harnesses()}") @@ -160,13 +161,6 @@ def prune_orphans(harness_id: str, output_root: Path, written: set[Path]) -> lis d = output_root / sub if d.is_dir(): candidates.extend(p for p in d.rglob("*") if p.is_file()) - elif harness_id == "copilot": - for sub_path in ( - output_root / ".github" / "agents", - output_root / ".github" / "skills", - ): - if sub_path.is_dir(): - candidates.extend(p for p in sub_path.rglob("*") if p.is_file()) elif harness_id == "cursor": # Both .cursor-plugin/plugins/*.json and .cursor/rules/*.mdc are adapter outputs. for sub_path in ( diff --git a/tools/validate_generated.py b/tools/validate_generated.py index 6a60bdb..80acb07 100644 --- a/tools/validate_generated.py +++ b/tools/validate_generated.py @@ -559,12 +559,12 @@ def validate_gemini(report: Report) -> None: def validate_copilot(report: Report) -> None: - """Validate Copilot agent markdown files under WORKTREE/.github/agents. + """Validate Copilot agent markdown files under ~/.copilot/agents. Checks that every .agent.md file has valid frontmatter with required fields. + Also checks WORKTREE/.github/ as a fallback for local dev / test compatibility. """ - # Validate repository-level agents (WORKTREE/.github/agents). - candidate_roots = [WORKTREE / ".github"] + candidate_roots = [Path.home() / ".copilot", WORKTREE / ".github"] found_any = False for root in candidate_roots: agents_dir = root / "agents"