mirror of
https://github.com/wshobson/agents
synced 2026-06-21 14:13:58 +00:00
copilot: symlink command directories to ~/.copilot/<plugin>/commands/
- Commands now symlink to ~/.copilot/comprehensive-review/commands/ instead of ~/.copilot/commands/ - Adapter emits command files to .copilot/commands/<plugin>/ directory structure - Installer symlinks each command directory to ~/.copilot/<plugin>/commands/ - This matches Copilot CLI's discovery path for plugin commands Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"sessionID": "ses_1a0c1bc8affexjd7idgHFAovWT",
|
||||
"updatedAt": "2026-05-25T13:49:53.610Z",
|
||||
"sources": {
|
||||
"background-task": {
|
||||
"state": "idle",
|
||||
"updatedAt": "2026-05-25T13:49:53.610Z"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
aposd
|
||||
aposd-audit
|
||||
aposd-critique
|
||||
brainstorming
|
||||
dispatching-parallel-agents
|
||||
enhance-prompt
|
||||
executing-plans
|
||||
find-skills
|
||||
finishing-a-development-branch
|
||||
gh-cli
|
||||
git-commit
|
||||
impeccable
|
||||
karpathy-guidelines
|
||||
react-components
|
||||
receiving-code-review
|
||||
repo-roast
|
||||
requesting-code-review
|
||||
shadcn-ui
|
||||
stitch-design
|
||||
stitch-loop
|
||||
subagent-driven-development
|
||||
systematic-debugging
|
||||
test-driven-development
|
||||
using-git-worktrees
|
||||
using-superpowers
|
||||
verification-before-completion
|
||||
writing-plans
|
||||
writing-skills
|
||||
+2
-2
@@ -26,7 +26,7 @@ do, and what to avoid, so the work you do for Claude Code translates cleanly eve
|
||||
|---|---|---|---|
|
||||
| `agents/<name>.md` | `name`, `description` | `model`, optional `tools:`, optional `color:` | `tools:` allowlist becomes a per-harness permission block where supported, dropped otherwise. |
|
||||
| `skills/<name>/SKILL.md` | `name`, `description` | (none) | Other Anthropic SKILL.md fields work on Claude Code only. |
|
||||
| `commands/<name>.md` | `description` | `argument-hint:` | Codex converts these to skills (it deprecated `~/.codex/prompts/`). |
|
||||
| `commands/<name>.md` | `description` | `argument-hint:` | Codex converts these to skills; Copilot emits `.copilot/commands/<plugin>/<name>.md` slash-command prompts. |
|
||||
|
||||
**Description triggers.** Include a recognized phrase: `Use when …`, `Use this skill when …`,
|
||||
`Use PROACTIVELY when …`, `Use after …`, `Trigger when …`, `Auto-loads when …`. The
|
||||
@@ -141,7 +141,7 @@ Things that work in Claude Code but degrade across harnesses:
|
||||
| Hooks (`hooks:` frontmatter) | Only Claude Code and OpenCode (via TS plugins). |
|
||||
| `color:` on agents | Cosmetic; dropped everywhere except Claude Code. |
|
||||
| Per-agent tool allowlist | Honored only on Claude Code/Gemini/OpenCode. Cursor and Codex have coarser models. |
|
||||
| Slash commands | Codex converts to skills. Gemini transpiles to TOML. |
|
||||
| Slash commands | Codex converts to skills. Gemini transpiles to TOML. Copilot emits `.copilot/commands/` prompt files. |
|
||||
| Marketplace registry | Only Claude Code and Cursor have one. Gemini installs by URL; Codex/OpenCode have no marketplace. |
|
||||
|
||||
When you must use a feature with no equivalent, the `harness_portability` lint won't fire
|
||||
|
||||
+1
-2
@@ -70,8 +70,7 @@ Claude Code automatically selects and coordinates the appropriate agents based o
|
||||
|
||||
| Command | Description |
|
||||
| ----------------------------------- | -------------------------- |
|
||||
| `/comprehensive-review:full-review` | Multi-perspective analysis |
|
||||
| `/comprehensive-review:pr-enhance` | Enhance pull requests |
|
||||
| `/comprehensive-review:full-review` | Run the comprehensive review workflow |
|
||||
|
||||
### Debugging & Troubleshooting
|
||||
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
brainstorming
|
||||
dispatching-parallel-agents
|
||||
executing-plans
|
||||
finishing-a-development-branch
|
||||
receiving-code-review
|
||||
requesting-code-review
|
||||
subagent-driven-development
|
||||
systematic-debugging
|
||||
test-driven-development
|
||||
using-git-worktrees
|
||||
using-superpowers
|
||||
verification-before-completion
|
||||
writing-plans
|
||||
writing-skills
|
||||
@@ -466,6 +466,7 @@ class HarnessAdapter(ABC):
|
||||
"WebFetch": "fetch",
|
||||
"WebSearch": "search",
|
||||
"TodoWrite": "todo",
|
||||
"AskUserQuestion": "question",
|
||||
}
|
||||
out = body
|
||||
for camel, replacement in replacements.items():
|
||||
|
||||
@@ -94,7 +94,7 @@ CAPABILITIES: dict[str, Capability] = {
|
||||
display_name="GitHub Copilot",
|
||||
skills_native=True,
|
||||
agents_native=True,
|
||||
commands_native=False,
|
||||
commands_native=True,
|
||||
plugin_marketplace=False,
|
||||
parallel_agents=False,
|
||||
tool_allowlist_per_agent=True,
|
||||
@@ -107,7 +107,7 @@ CAPABILITIES: dict[str, Capability] = {
|
||||
skill_body_max_bytes=_NO_CAP,
|
||||
tool_name_case="lowercase",
|
||||
bare_model_aliases=False,
|
||||
notes="Emits Markdown agent profiles to .github/agents/ and SKILL.md skills to .github/skills/. Copilot discovers from any clone (files are committed). Also discovers from ~/.copilot/agents/ (user-level) and .github-private/ (org-level).",
|
||||
notes="Emits Markdown agent profiles to .copilot/agents/, SKILL.md skills to .copilot/skills/, and slash-command prompt files to .copilot/commands/. Copilot discovers from the repo or from ~/.copilot/ at user level.",
|
||||
),
|
||||
"cursor": Capability(
|
||||
harness_id="cursor",
|
||||
|
||||
+4
-1
@@ -34,7 +34,7 @@ _HARNESS_TARGETS = {
|
||||
"cursor": [".cursor", ".cursor-plugin"],
|
||||
"opencode": [".opencode", "opencode.json"],
|
||||
"gemini": ["commands", "agents", "skills"],
|
||||
"copilot": [".copilot/agents", ".copilot/skills"],
|
||||
"copilot": [".copilot/agents", ".copilot/skills", ".copilot/commands"],
|
||||
}
|
||||
|
||||
|
||||
@@ -165,6 +165,9 @@ def prune_orphans(harness_id: str, output_root: Path, written: set[Path]) -> lis
|
||||
d = output_root / ".copilot" / sub
|
||||
if d.is_dir():
|
||||
candidates.extend(p for p in d.rglob("*") if p.is_file())
|
||||
d = output_root / ".copilot" / "commands"
|
||||
if d.is_dir():
|
||||
candidates.extend(p for p in d.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 (
|
||||
|
||||
@@ -13,6 +13,7 @@ GENERATED_ROOT = REPO_ROOT / ".copilot"
|
||||
ARTIFACT_GLOBS = {
|
||||
"agents": "*.agent.md",
|
||||
"skills": "*",
|
||||
"commands": "*",
|
||||
}
|
||||
|
||||
|
||||
@@ -58,7 +59,9 @@ def _generated_artifacts(repo_root: Path) -> list[tuple[str, Path]]:
|
||||
for src in sorted(source_dir.glob(pattern)):
|
||||
if subdir == "skills" and not src.is_dir():
|
||||
continue
|
||||
if subdir != "skills" and not src.is_file():
|
||||
if subdir in {"agents"} and not src.is_file():
|
||||
continue
|
||||
if subdir in {"commands"} and not src.is_dir():
|
||||
continue
|
||||
artifacts.append((subdir, src.resolve()))
|
||||
return artifacts
|
||||
@@ -100,8 +103,15 @@ def install(
|
||||
return report
|
||||
|
||||
for subdir, src in artifacts:
|
||||
dst = config_dir / subdir / src.name
|
||||
_link_one(src, dst, force=force, report=report)
|
||||
if subdir == "commands":
|
||||
# For commands, extract plugin name and symlink to ~/.copilot/<plugin>/commands/
|
||||
# src is like .copilot/commands/comprehensive-review, symlink to ~/.copilot/comprehensive-review/commands
|
||||
plugin_name = src.name
|
||||
dst = config_dir / plugin_name / "commands"
|
||||
_link_one(src, dst, force=force, report=report)
|
||||
else:
|
||||
dst = config_dir / subdir / src.name
|
||||
_link_one(src, dst, force=force, report=report)
|
||||
return report
|
||||
|
||||
|
||||
|
||||
@@ -1015,6 +1015,22 @@ class TestCopilotAdapter:
|
||||
assert fm["description"] == "Use when greeting users."
|
||||
assert "# Hello" in body
|
||||
|
||||
def test_emits_command_prompt_files(self, synthetic_plugin: PluginSource, output_root: Path):
|
||||
CopilotAdapter(output_root=output_root).emit_plugin(synthetic_plugin)
|
||||
|
||||
entry = output_root / ".copilot" / "commands" / "demo" / "index.md"
|
||||
cmd = output_root / ".copilot" / "commands" / "demo" / "say-hi.md"
|
||||
|
||||
assert entry.is_file()
|
||||
assert cmd.is_file()
|
||||
|
||||
entry_fm, entry_body = parse_frontmatter(entry.read_text())
|
||||
cmd_fm, cmd_body = parse_frontmatter(cmd.read_text())
|
||||
assert entry_fm["description"] == "Demo plugin for tests"
|
||||
assert "/demo:say-hi" in entry_body
|
||||
assert cmd_fm["description"] == "Send a greeting"
|
||||
assert "Greet the user named $ARGUMENTS." in cmd_body
|
||||
|
||||
def test_emit_global_returns_empty(
|
||||
self, synthetic_plugin: PluginSource, output_root: Path
|
||||
):
|
||||
|
||||
@@ -10,10 +10,14 @@ from tools.install_copilot import default_config_dir, install, uninstall
|
||||
def _write_generated_copilot(repo_root: Path) -> None:
|
||||
agents = repo_root / ".copilot" / "agents"
|
||||
skills = repo_root / ".copilot" / "skills" / "demo-hello"
|
||||
commands = repo_root / ".copilot" / "commands" / "demo"
|
||||
agents.mkdir(parents=True)
|
||||
skills.mkdir(parents=True)
|
||||
commands.mkdir(parents=True)
|
||||
(agents / "demo__agent.agent.md").write_text("agent\n")
|
||||
(skills / "SKILL.md").write_text("---\nname: demo-hello\n---\n\nBody.\n")
|
||||
(commands / "index.md").write_text("---\ndescription: demo\n---\n\nEntry.\n")
|
||||
(commands / "say-hi.md").write_text("---\ndescription: hi\n---\n\nHi.\n")
|
||||
|
||||
|
||||
def test_default_config_dir_prefers_copilot_config_dir(tmp_path: Path):
|
||||
@@ -39,11 +43,12 @@ def test_install_creates_idempotent_symlinks(tmp_path: Path):
|
||||
second = install(repo_root=repo_root, config_dir=config_dir)
|
||||
|
||||
assert first.ok
|
||||
assert first.linked == 2
|
||||
assert first.linked == 3
|
||||
assert second.ok
|
||||
assert second.unchanged == 2
|
||||
assert second.unchanged == 3
|
||||
assert (config_dir / "agents" / "demo__agent.agent.md").is_symlink()
|
||||
assert (config_dir / "skills" / "demo-hello").is_symlink()
|
||||
assert (config_dir / "commands" / "demo").is_symlink()
|
||||
|
||||
|
||||
def test_install_refuses_to_overwrite_real_files(tmp_path: Path):
|
||||
@@ -95,7 +100,7 @@ def test_uninstall_removes_only_repo_owned_symlinks(tmp_path: Path):
|
||||
report = uninstall(repo_root=repo_root, config_dir=config_dir)
|
||||
|
||||
assert report.ok
|
||||
assert report.removed == 2
|
||||
assert report.removed == 3
|
||||
assert not (config_dir / "agents" / "demo__agent.agent.md").exists()
|
||||
assert unrelated.is_symlink()
|
||||
assert real_file.read_text() == "user\n"
|
||||
|
||||
@@ -297,6 +297,26 @@ class TestCopilotRoundTrip:
|
||||
f"skill count mismatch: source={_source_skill_count()} copilot={n}"
|
||||
)
|
||||
|
||||
def test_copilot_command_count_matches_source(self):
|
||||
n = len(
|
||||
[
|
||||
p
|
||||
for p in (WORKTREE / ".copilot" / "commands").rglob("*.md")
|
||||
if p.name != "index.md"
|
||||
]
|
||||
)
|
||||
assert n == _source_command_count(), (
|
||||
f"command count mismatch: source={_source_command_count()} copilot={n}"
|
||||
)
|
||||
|
||||
def test_copilot_command_entrypoints_exist_for_every_plugin(self):
|
||||
missing = []
|
||||
for plugin_name in list_plugins():
|
||||
entry = WORKTREE / ".copilot" / "commands" / plugin_name / "index.md"
|
||||
if not entry.is_file():
|
||||
missing.append(plugin_name)
|
||||
assert not missing, f"missing Copilot command entrypoints for: {sorted(missing)}"
|
||||
|
||||
def test_every_copilot_agent_has_required_frontmatter(self):
|
||||
required = {"name", "description"}
|
||||
problems = []
|
||||
|
||||
@@ -559,9 +559,10 @@ def validate_gemini(report: Report) -> None:
|
||||
|
||||
|
||||
def validate_copilot(report: Report) -> None:
|
||||
"""Validate Copilot agent markdown files under WORKTREE/.copilot/agents.
|
||||
"""Validate Copilot agent, skill, and command markdown files under WORKTREE/.copilot.
|
||||
|
||||
Checks that every .agent.md file has valid frontmatter with required fields.
|
||||
Checks that generated agent, skill, and command markdown files have valid
|
||||
frontmatter and the minimum metadata Copilot needs to discover them.
|
||||
"""
|
||||
candidate_roots = [WORKTREE / ".copilot"]
|
||||
found_any = False
|
||||
@@ -679,6 +680,39 @@ def validate_copilot(report: Report) -> None:
|
||||
remediation="Set `description` to a non-empty string in the skill frontmatter.",
|
||||
)
|
||||
|
||||
commands_dir = WORKTREE / ".copilot" / "commands"
|
||||
if commands_dir.is_dir():
|
||||
found_any = True
|
||||
for command_md in commands_dir.rglob("*.md"):
|
||||
content = command_md.read_text(encoding="utf-8")
|
||||
fm, body = parse_frontmatter(content)
|
||||
if not fm:
|
||||
report.add(
|
||||
severity="error",
|
||||
harness="copilot",
|
||||
path=command_md,
|
||||
message="missing or invalid frontmatter",
|
||||
remediation="Regenerate via `make generate HARNESS=copilot`.",
|
||||
)
|
||||
continue
|
||||
description = fm.get("description")
|
||||
if not isinstance(description, str) or not description.strip():
|
||||
report.add(
|
||||
severity="error",
|
||||
harness="copilot",
|
||||
path=command_md,
|
||||
message="missing required `description` field in frontmatter",
|
||||
remediation="Copilot command prompt files need a non-empty description.",
|
||||
)
|
||||
if command_md.name != "index.md" and not body.strip():
|
||||
report.add(
|
||||
severity="warning",
|
||||
harness="copilot",
|
||||
path=command_md,
|
||||
message="command body is empty",
|
||||
remediation="Keep the prompt body in the source command markdown.",
|
||||
)
|
||||
|
||||
if not found_any:
|
||||
return
|
||||
|
||||
|
||||
Reference in New Issue
Block a user