Add Copilot as the 6th converter target, transforming Claude Code plugins
into Copilot's native format: custom agents (.agent.md), agent skills
(SKILL.md), and MCP server configuration JSON.
Component mapping:
- Agents → .github/agents/{name}.agent.md (with Copilot frontmatter)
- Commands → .github/skills/{name}/SKILL.md
- Skills → .github/skills/{name}/ (copied as-is)
- MCP servers → .github/copilot-mcp-config.json
- Hooks → skipped with warning
Also adds `compound sync copilot` support and fixes YAML quoting for
the `*` character in frontmatter serialization.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
329 lines
11 KiB
Markdown
329 lines
11 KiB
Markdown
---
|
|
title: "feat: Add GitHub Copilot converter target"
|
|
type: feat
|
|
date: 2026-02-14
|
|
status: complete
|
|
---
|
|
|
|
# feat: Add GitHub Copilot Converter Target
|
|
|
|
## Overview
|
|
|
|
Add GitHub Copilot as a converter target following the established `TargetHandler` pattern. This converts the compound-engineering Claude Code plugin into Copilot's native format: custom agents (`.agent.md`), agent skills (`SKILL.md`), and MCP server configuration JSON.
|
|
|
|
**Brainstorm:** `docs/brainstorms/2026-02-14-copilot-converter-target-brainstorm.md`
|
|
|
|
## Problem Statement
|
|
|
|
The CLI tool (`compound`) already supports converting Claude Code plugins to 5 target formats (OpenCode, Codex, Droid, Cursor, Pi). GitHub Copilot is a widely-used AI coding assistant that now supports custom agents, skills, and MCP servers — but there's no converter target for it.
|
|
|
|
## Proposed Solution
|
|
|
|
Follow the existing converter pattern exactly:
|
|
|
|
1. Define types (`src/types/copilot.ts`)
|
|
2. Implement converter (`src/converters/claude-to-copilot.ts`)
|
|
3. Implement writer (`src/targets/copilot.ts`)
|
|
4. Register target (`src/targets/index.ts`)
|
|
5. Add sync support (`src/sync/copilot.ts`, `src/commands/sync.ts`)
|
|
6. Write tests and documentation
|
|
|
|
### Component Mapping
|
|
|
|
| Claude Code | Copilot | Output Path |
|
|
|-------------|---------|-------------|
|
|
| Agents (`.md`) | Custom Agents (`.agent.md`) | `.github/agents/{name}.agent.md` |
|
|
| Commands (`.md`) | Agent Skills (`SKILL.md`) | `.github/skills/{name}/SKILL.md` |
|
|
| Skills (`SKILL.md`) | Agent Skills (`SKILL.md`) | `.github/skills/{name}/SKILL.md` |
|
|
| MCP Servers | Config JSON | `.github/copilot-mcp-config.json` |
|
|
| Hooks | Skipped | Warning to stderr |
|
|
|
|
## Technical Approach
|
|
|
|
### Phase 1: Types
|
|
|
|
**File:** `src/types/copilot.ts`
|
|
|
|
```typescript
|
|
export type CopilotAgent = {
|
|
name: string
|
|
content: string // Full .agent.md content with frontmatter
|
|
}
|
|
|
|
export type CopilotGeneratedSkill = {
|
|
name: string
|
|
content: string // SKILL.md content with frontmatter
|
|
}
|
|
|
|
export type CopilotSkillDir = {
|
|
name: string
|
|
sourceDir: string
|
|
}
|
|
|
|
export type CopilotMcpServer = {
|
|
type: string
|
|
command?: string
|
|
args?: string[]
|
|
url?: string
|
|
tools: string[]
|
|
env?: Record<string, string>
|
|
headers?: Record<string, string>
|
|
}
|
|
|
|
export type CopilotBundle = {
|
|
agents: CopilotAgent[]
|
|
generatedSkills: CopilotGeneratedSkill[]
|
|
skillDirs: CopilotSkillDir[]
|
|
mcpConfig?: Record<string, CopilotMcpServer>
|
|
}
|
|
```
|
|
|
|
### Phase 2: Converter
|
|
|
|
**File:** `src/converters/claude-to-copilot.ts`
|
|
|
|
**Agent conversion:**
|
|
- Frontmatter: `description` (required, fallback to `"Converted from Claude agent {name}"`), `tools: ["*"]`, `infer: true`
|
|
- Pass through `model` if present
|
|
- Fold `capabilities` into body as `## Capabilities` section (same as Cursor)
|
|
- Use `formatFrontmatter()` utility
|
|
- Warn if body exceeds 30,000 characters (`.length`)
|
|
|
|
**Command → Skill conversion:**
|
|
- Convert to SKILL.md format with frontmatter: `name`, `description`
|
|
- Flatten namespaced names: `workflows:plan` → `plan`
|
|
- Drop `allowed-tools`, `model`, `disable-model-invocation` silently
|
|
- Include `argument-hint` as `## Arguments` section in body
|
|
|
|
**Skill pass-through:**
|
|
- Map to `CopilotSkillDir` as-is (same as Cursor)
|
|
|
|
**MCP server conversion:**
|
|
- Transform env var names: `API_KEY` → `COPILOT_MCP_API_KEY`
|
|
- Skip vars already prefixed with `COPILOT_MCP_`
|
|
- Add `type: "local"` for command-based servers, `type: "sse"` for URL-based
|
|
- Set `tools: ["*"]` for all servers
|
|
|
|
**Content transformation (`transformContentForCopilot`):**
|
|
|
|
| Pattern | Input | Output |
|
|
|---------|-------|--------|
|
|
| Task calls | `Task repo-research-analyst(desc)` | `Use the repo-research-analyst skill to: desc` |
|
|
| Slash commands | `/workflows:plan` | `/plan` |
|
|
| Path rewriting | `.claude/` | `.github/` |
|
|
| Home path rewriting | `~/.claude/` | `~/.copilot/` |
|
|
| Agent references | `@security-sentinel` | `the security-sentinel agent` |
|
|
|
|
**Hooks:** Warn to stderr if present, skip.
|
|
|
|
### Phase 3: Writer
|
|
|
|
**File:** `src/targets/copilot.ts`
|
|
|
|
**Path resolution:**
|
|
- If `outputRoot` basename is `.github`, write directly into it (avoid `.github/.github/` double-nesting)
|
|
- Otherwise, nest under `.github/`
|
|
|
|
**Write operations:**
|
|
- Agents → `.github/agents/{name}.agent.md` (note: `.agent.md` extension)
|
|
- Generated skills (from commands) → `.github/skills/{name}/SKILL.md`
|
|
- Skill dirs → `.github/skills/{name}/` (copy via `copyDir`)
|
|
- MCP config → `.github/copilot-mcp-config.json` (backup existing with `backupFile`)
|
|
|
|
### Phase 4: Target Registration
|
|
|
|
**File:** `src/targets/index.ts`
|
|
|
|
Add import and register:
|
|
|
|
```typescript
|
|
import { convertClaudeToCopilot } from "../converters/claude-to-copilot"
|
|
import { writeCopilotBundle } from "./copilot"
|
|
|
|
// In targets record:
|
|
copilot: {
|
|
name: "copilot",
|
|
implemented: true,
|
|
convert: convertClaudeToCopilot as TargetHandler<CopilotBundle>["convert"],
|
|
write: writeCopilotBundle as TargetHandler<CopilotBundle>["write"],
|
|
},
|
|
```
|
|
|
|
### Phase 5: Sync Support
|
|
|
|
**File:** `src/sync/copilot.ts`
|
|
|
|
Follow the Cursor sync pattern (`src/sync/cursor.ts`):
|
|
- Symlink skills to `.github/skills/` using `forceSymlink`
|
|
- Validate skill names with `isValidSkillName`
|
|
- Convert MCP servers with `COPILOT_MCP_` prefix transformation
|
|
- Merge MCP config into existing `.github/copilot-mcp-config.json`
|
|
|
|
**File:** `src/commands/sync.ts`
|
|
|
|
- Add `"copilot"` to `validTargets` array
|
|
- Add case in `resolveOutputRoot()`: `case "copilot": return path.join(process.cwd(), ".github")`
|
|
- Add import and switch case for `syncToCopilot`
|
|
- Update meta description to include "Copilot"
|
|
|
|
### Phase 6: Tests
|
|
|
|
**File:** `tests/copilot-converter.test.ts`
|
|
|
|
Test cases (following `tests/cursor-converter.test.ts` pattern):
|
|
|
|
```
|
|
describe("convertClaudeToCopilot")
|
|
✓ converts agents to .agent.md with Copilot frontmatter
|
|
✓ agent description is required, fallback generated if missing
|
|
✓ agent with empty body gets default body
|
|
✓ agent capabilities are prepended to body
|
|
✓ agent model field is passed through
|
|
✓ agent tools defaults to ["*"]
|
|
✓ agent infer defaults to true
|
|
✓ warns when agent body exceeds 30k characters
|
|
✓ converts commands to skills with SKILL.md format
|
|
✓ flattens namespaced command names
|
|
✓ command name collision after flattening is deduplicated
|
|
✓ command allowedTools is silently dropped
|
|
✓ command with argument-hint gets Arguments section
|
|
✓ passes through skill directories
|
|
✓ skill and generated skill name collision is deduplicated
|
|
✓ converts MCP servers with COPILOT_MCP_ prefix
|
|
✓ MCP env vars already prefixed are not double-prefixed
|
|
✓ MCP servers get type field (local vs sse)
|
|
✓ warns when hooks are present
|
|
✓ no warning when hooks are absent
|
|
✓ plugin with zero agents produces empty agents array
|
|
✓ plugin with only skills works
|
|
|
|
describe("transformContentForCopilot")
|
|
✓ rewrites .claude/ paths to .github/
|
|
✓ rewrites ~/.claude/ paths to ~/.copilot/
|
|
✓ transforms Task agent calls to skill references
|
|
✓ flattens slash commands
|
|
✓ transforms @agent references to agent references
|
|
```
|
|
|
|
**File:** `tests/copilot-writer.test.ts`
|
|
|
|
Test cases (following `tests/cursor-writer.test.ts` pattern):
|
|
|
|
```
|
|
describe("writeCopilotBundle")
|
|
✓ writes agents, generated skills, copied skills, and MCP config
|
|
✓ agents use .agent.md file extension
|
|
✓ writes directly into .github output root without double-nesting
|
|
✓ handles empty bundles gracefully
|
|
✓ writes multiple agents as separate .agent.md files
|
|
✓ backs up existing copilot-mcp-config.json before overwriting
|
|
✓ creates skill directories with SKILL.md
|
|
```
|
|
|
|
**File:** `tests/sync-copilot.test.ts`
|
|
|
|
Test cases (following `tests/sync-cursor.test.ts` pattern):
|
|
|
|
```
|
|
describe("syncToCopilot")
|
|
✓ symlinks skills to .github/skills/
|
|
✓ skips skills with invalid names
|
|
✓ merges MCP config with existing file
|
|
✓ transforms MCP env var names to COPILOT_MCP_ prefix
|
|
✓ writes MCP config with restricted permissions (0o600)
|
|
```
|
|
|
|
### Phase 7: Documentation
|
|
|
|
**File:** `docs/specs/copilot.md`
|
|
|
|
Follow `docs/specs/cursor.md` format:
|
|
- Last verified date
|
|
- Primary sources (GitHub Docs URLs)
|
|
- Config locations table
|
|
- Agents section (`.agent.md` format, frontmatter fields)
|
|
- Skills section (`SKILL.md` format)
|
|
- MCP section (config structure, env var prefix requirement)
|
|
- Character limits (30k agent body)
|
|
|
|
**File:** `README.md`
|
|
|
|
- Add "copilot" to the list of supported targets
|
|
- Add usage example: `compound convert --to copilot ./plugins/compound-engineering`
|
|
- Add sync example: `compound sync copilot`
|
|
|
|
## Acceptance Criteria
|
|
|
|
### Converter
|
|
- [x] Agents convert to `.agent.md` with `description`, `tools: ["*"]`, `infer: true`
|
|
- [x] Agent `model` passes through when present
|
|
- [x] Agent `capabilities` fold into body as `## Capabilities`
|
|
- [x] Missing description generates fallback
|
|
- [x] Empty body generates fallback
|
|
- [x] Body exceeding 30k chars triggers stderr warning
|
|
- [x] Commands convert to SKILL.md format
|
|
- [x] Command names flatten (`workflows:plan` → `plan`)
|
|
- [x] Name collisions deduplicated with `-2`, `-3` suffix
|
|
- [x] Command `allowed-tools` dropped silently
|
|
- [x] Skills pass through as `CopilotSkillDir`
|
|
- [x] MCP env vars prefixed with `COPILOT_MCP_`
|
|
- [x] Already-prefixed env vars not double-prefixed
|
|
- [x] MCP servers get `type` field (`local` or `sse`)
|
|
- [x] Hooks trigger warning, skip conversion
|
|
- [x] Content transformation: Task calls, slash commands, paths, @agent refs
|
|
|
|
### Writer
|
|
- [x] Agents written to `.github/agents/{name}.agent.md`
|
|
- [x] Generated skills written to `.github/skills/{name}/SKILL.md`
|
|
- [x] Skill dirs copied to `.github/skills/{name}/`
|
|
- [x] MCP config written to `.github/copilot-mcp-config.json`
|
|
- [x] Existing MCP config backed up before overwrite
|
|
- [x] No double-nesting when outputRoot is `.github`
|
|
- [x] Empty bundles handled gracefully
|
|
|
|
### CLI Integration
|
|
- [x] `compound convert --to copilot` works
|
|
- [x] `compound sync copilot` works
|
|
- [x] Copilot registered in `src/targets/index.ts`
|
|
- [x] Sync resolves output to `.github/` in current directory
|
|
|
|
### Tests
|
|
- [x] `tests/copilot-converter.test.ts` — all converter tests pass
|
|
- [x] `tests/copilot-writer.test.ts` — all writer tests pass
|
|
- [x] `tests/sync-copilot.test.ts` — all sync tests pass
|
|
|
|
### Documentation
|
|
- [x] `docs/specs/copilot.md` — format specification
|
|
- [x] `README.md` — updated with copilot target
|
|
|
|
## Files to Create
|
|
|
|
| File | Purpose |
|
|
|------|---------|
|
|
| `src/types/copilot.ts` | Type definitions |
|
|
| `src/converters/claude-to-copilot.ts` | Converter logic |
|
|
| `src/targets/copilot.ts` | Writer logic |
|
|
| `src/sync/copilot.ts` | Sync handler |
|
|
| `tests/copilot-converter.test.ts` | Converter tests |
|
|
| `tests/copilot-writer.test.ts` | Writer tests |
|
|
| `tests/sync-copilot.test.ts` | Sync tests |
|
|
| `docs/specs/copilot.md` | Format specification |
|
|
|
|
## Files to Modify
|
|
|
|
| File | Change |
|
|
|------|--------|
|
|
| `src/targets/index.ts` | Register copilot target |
|
|
| `src/commands/sync.ts` | Add copilot to valid targets, output root, switch case |
|
|
| `README.md` | Add copilot to supported targets |
|
|
|
|
## References
|
|
|
|
- [Custom agents configuration - GitHub Docs](https://docs.github.com/en/copilot/reference/custom-agents-configuration)
|
|
- [About Agent Skills - GitHub Docs](https://docs.github.com/en/copilot/concepts/agents/about-agent-skills)
|
|
- [MCP and coding agent - GitHub Docs](https://docs.github.com/en/copilot/concepts/agents/coding-agent/mcp-and-coding-agent)
|
|
- Existing converter: `src/converters/claude-to-cursor.ts`
|
|
- Existing writer: `src/targets/cursor.ts`
|
|
- Existing sync: `src/sync/cursor.ts`
|
|
- Existing tests: `tests/cursor-converter.test.ts`, `tests/cursor-writer.test.ts`
|