feat: generate copilot artifacts directly to ~/.copilot/ instead of .github/

- CopilotAdapter now writes to ~/.copilot/agents/ and ~/.copilot/skills/
- Updated generate.py to pass ~/.copilot/ as output_root for copilot
- Added copilot stale artifact detection to doc_gardener.py
- Updated validate_generated.py to check ~/.copilot/agents/ (with fallback)
- Removed old copilot gitignore entries and cleaned up stale artifacts
- No install step needed — make generate HARNESS=copilot writes directly
This commit is contained in:
GitHub Copilot
2026-05-25 07:35:53 -05:00
parent 269a64af36
commit 4691655d3c
5 changed files with 40 additions and 26 deletions
+1 -5
View File
@@ -28,10 +28,6 @@ node_modules
# Generated harness artifacts (all gitignored — regenerate via `make generate HARNESS=<x>`)
# 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
+11 -9
View File
@@ -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/<plugin>__<agent>.agent.md``, skills to
``.github/skills/<plugin>__<skill>/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/<plugin>__<agent>.agent.md``, skills to
``skills/<plugin>__<skill>/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))
+22
View File
@@ -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.
+3 -9
View File
@@ -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 (
+3 -3
View File
@@ -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"