Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 11 additions & 4 deletions src/converters/claude-to-copilot.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,8 +50,7 @@ function convertAgent(agent: ClaudeAgent, usedNames: Set<string>): CopilotAgent

const frontmatter: Record<string, unknown> = {
description,
tools: ["*"],
infer: true,
"user-invocable": true,
}

let body = transformContentForCopilot(agent.body.trim())
Expand Down Expand Up @@ -123,12 +122,20 @@ export function transformContentForCopilot(body: string): string {
return `/${normalized}`
})

// 3. Rewrite .claude/ paths to .github/ and ~/.claude/ to ~/.copilot/
// 3. Replace plugin colon-namespaced command references (e.g. ce:plan → ce-plan, ce:* → ce-*)
// Scoped to `ce:` prefix which is the compound-engineering plugin namespace.
// The lookbehind ensures we only match at word boundaries or after common delimiters,
// avoiding corruption of URLs, code identifiers, or unrelated namespace:value patterns.
// Note: / is intentionally excluded — slash commands are already handled in step 2.
// Captures colons in the name segment so multi-colon refs like ce:work:beta → ce-work-beta.
result = result.replace(/(?<=^|[\s,.()`'"])ce:([a-z*][a-z0-9_*:-]*)/gim, (_, name: string) => `ce-${name.replace(/:/g, "-")}`)

// 4. Rewrite .claude/ paths to .github/ and ~/.claude/ to ~/.copilot/
result = result
.replace(/~\/\.claude\//g, "~/.copilot/")
.replace(/\.claude\//g, ".github/")

// 4. Transform @agent-name references
// 5. Transform @agent-name references
const agentRefPattern =
/@([a-z][a-z0-9-]*-(?:agent|reviewer|researcher|analyst|specialist|oracle|sentinel|guardian|strategist))/gi
result = result.replace(agentRefPattern, (_match, agentName: string) => {
Expand Down
49 changes: 40 additions & 9 deletions tests/copilot-converter.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,8 +55,9 @@ describe("convertClaudeToCopilot", () => {

const parsed = parseFrontmatter(agent.content)
expect(parsed.data.description).toBe("Security-focused code review agent")
expect(parsed.data.tools).toEqual(["*"])
expect(parsed.data.infer).toBe(true)
expect(parsed.data.tools).toBeUndefined()
expect(parsed.data.infer).toBeUndefined()
expect(parsed.data["user-invocable"]).toBe(true)
expect(parsed.body).toContain("Capabilities")
expect(parsed.body).toContain("Threat modeling")
expect(parsed.body).toContain("Focus on vulnerabilities.")
Expand Down Expand Up @@ -109,20 +110,21 @@ describe("convertClaudeToCopilot", () => {
expect(parsed.data.model).toBeUndefined()
})

test("agent tools defaults to [*]", () => {
test("agent omits tools (Copilot uses defaults when omitted)", () => {
const bundle = convertClaudeToCopilot(fixturePlugin, defaultOptions)
const parsed = parseFrontmatter(bundle.agents[0].content)
expect(parsed.data.tools).toEqual(["*"])
expect(parsed.data.tools).toBeUndefined()
})

test("agent infer defaults to true", () => {
test("agent replaces infer with user-invocable", () => {
const bundle = convertClaudeToCopilot(fixturePlugin, defaultOptions)
const parsed = parseFrontmatter(bundle.agents[0].content)
expect(parsed.data.infer).toBe(true)
expect(parsed.data.infer).toBeUndefined()
expect(parsed.data["user-invocable"]).toBe(true)
})

test("warns when agent body exceeds 30k characters", () => {
const warnSpy = spyOn(console, "warn").mockImplementation(() => {})
const warnSpy = spyOn(console, "warn").mockImplementation(() => { })

const plugin: ClaudePlugin = {
...fixturePlugin,
Expand Down Expand Up @@ -341,7 +343,7 @@ describe("convertClaudeToCopilot", () => {
})

test("warns when hooks are present", () => {
const warnSpy = spyOn(console, "warn").mockImplementation(() => {})
const warnSpy = spyOn(console, "warn").mockImplementation(() => { })

const plugin: ClaudePlugin = {
...fixturePlugin,
Expand All @@ -364,7 +366,7 @@ describe("convertClaudeToCopilot", () => {
})

test("no warning when hooks are absent", () => {
const warnSpy = spyOn(console, "warn").mockImplementation(() => {})
const warnSpy = spyOn(console, "warn").mockImplementation(() => { })

convertClaudeToCopilot(fixturePlugin, defaultOptions)
expect(warnSpy).not.toHaveBeenCalled()
Expand Down Expand Up @@ -468,6 +470,35 @@ Task best-practices-researcher(topic)`
expect(result).not.toContain("@security-sentinel")
})

test("replaces ce: namespace with ce- in body text", () => {
const input = "prefer ce:brainstorm first. Then run ce:plan and ce:review. Use ce:* skills."
const result = transformContentForCopilot(input)
expect(result).toBe("prefer ce-brainstorm first. Then run ce-plan and ce-review. Use ce-* skills.")
expect(result).not.toContain("ce:")
})

test("replaces multi-colon ce: references fully", () => {
const input = "run ce:work:beta and ce:review:deep"
const result = transformContentForCopilot(input)
expect(result).toBe("run ce-work-beta and ce-review-deep")
expect(result).not.toContain(":")
})

test("ce: replacement does not corrupt non-command patterns", () => {
const input = "Use source: explicit and Confidence: high. See https://example.com/ace:thing"
const result = transformContentForCopilot(input)
expect(result).toContain("source: explicit")
expect(result).toContain("Confidence: high")
expect(result).toContain("ace:thing")
})

test("ce: replacement does not corrupt URLs", () => {
const input = "See https://example.com/ce:plan and http://docs.example.com/ce:review/overview"
const result = transformContentForCopilot(input)
expect(result).toContain("https://example.com/ce:plan")
expect(result).toContain("http://docs.example.com/ce:review/overview")
})

test("generated skill deduplicates against sanitized pass-through skill names", () => {
const plugin: ClaudePlugin = {
...fixturePlugin,
Expand Down
2 changes: 1 addition & 1 deletion tests/copilot-writer.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ describe("writeCopilotBundle", () => {
agents: [
{
name: "security-reviewer",
content: "---\ndescription: Security\ntools:\n - '*'\ninfer: true\n---\n\nReview code.",
content: "---\ndescription: Security\nuser-invocable: true\n---\n\nReview code.",
},
],
generatedSkills: [
Expand Down