fix(converters): remove invalid tools/infer from Copilot agent frontmatter (#493)

This commit is contained in:
David Torres
2026-04-02 16:23:40 -04:00
committed by GitHub
parent 184724276a
commit 6dcb4a3c55
3 changed files with 52 additions and 14 deletions

View File

@@ -50,8 +50,7 @@ function convertAgent(agent: ClaudeAgent, usedNames: Set<string>): CopilotAgent
const frontmatter: Record<string, unknown> = { const frontmatter: Record<string, unknown> = {
description, description,
tools: ["*"], "user-invocable": true,
infer: true,
} }
let body = transformContentForCopilot(agent.body.trim()) let body = transformContentForCopilot(agent.body.trim())
@@ -123,12 +122,20 @@ export function transformContentForCopilot(body: string): string {
return `/${normalized}` 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 result = result
.replace(/~\/\.claude\//g, "~/.copilot/") .replace(/~\/\.claude\//g, "~/.copilot/")
.replace(/\.claude\//g, ".github/") .replace(/\.claude\//g, ".github/")
// 4. Transform @agent-name references // 5. Transform @agent-name references
const agentRefPattern = const agentRefPattern =
/@([a-z][a-z0-9-]*-(?:agent|reviewer|researcher|analyst|specialist|oracle|sentinel|guardian|strategist))/gi /@([a-z][a-z0-9-]*-(?:agent|reviewer|researcher|analyst|specialist|oracle|sentinel|guardian|strategist))/gi
result = result.replace(agentRefPattern, (_match, agentName: string) => { result = result.replace(agentRefPattern, (_match, agentName: string) => {

View File

@@ -55,8 +55,9 @@ describe("convertClaudeToCopilot", () => {
const parsed = parseFrontmatter(agent.content) const parsed = parseFrontmatter(agent.content)
expect(parsed.data.description).toBe("Security-focused code review agent") expect(parsed.data.description).toBe("Security-focused code review agent")
expect(parsed.data.tools).toEqual(["*"]) expect(parsed.data.tools).toBeUndefined()
expect(parsed.data.infer).toBe(true) expect(parsed.data.infer).toBeUndefined()
expect(parsed.data["user-invocable"]).toBe(true)
expect(parsed.body).toContain("Capabilities") expect(parsed.body).toContain("Capabilities")
expect(parsed.body).toContain("Threat modeling") expect(parsed.body).toContain("Threat modeling")
expect(parsed.body).toContain("Focus on vulnerabilities.") expect(parsed.body).toContain("Focus on vulnerabilities.")
@@ -109,20 +110,21 @@ describe("convertClaudeToCopilot", () => {
expect(parsed.data.model).toBeUndefined() 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 bundle = convertClaudeToCopilot(fixturePlugin, defaultOptions)
const parsed = parseFrontmatter(bundle.agents[0].content) 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 bundle = convertClaudeToCopilot(fixturePlugin, defaultOptions)
const parsed = parseFrontmatter(bundle.agents[0].content) 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", () => { test("warns when agent body exceeds 30k characters", () => {
const warnSpy = spyOn(console, "warn").mockImplementation(() => {}) const warnSpy = spyOn(console, "warn").mockImplementation(() => { })
const plugin: ClaudePlugin = { const plugin: ClaudePlugin = {
...fixturePlugin, ...fixturePlugin,
@@ -341,7 +343,7 @@ describe("convertClaudeToCopilot", () => {
}) })
test("warns when hooks are present", () => { test("warns when hooks are present", () => {
const warnSpy = spyOn(console, "warn").mockImplementation(() => {}) const warnSpy = spyOn(console, "warn").mockImplementation(() => { })
const plugin: ClaudePlugin = { const plugin: ClaudePlugin = {
...fixturePlugin, ...fixturePlugin,
@@ -364,7 +366,7 @@ describe("convertClaudeToCopilot", () => {
}) })
test("no warning when hooks are absent", () => { test("no warning when hooks are absent", () => {
const warnSpy = spyOn(console, "warn").mockImplementation(() => {}) const warnSpy = spyOn(console, "warn").mockImplementation(() => { })
convertClaudeToCopilot(fixturePlugin, defaultOptions) convertClaudeToCopilot(fixturePlugin, defaultOptions)
expect(warnSpy).not.toHaveBeenCalled() expect(warnSpy).not.toHaveBeenCalled()
@@ -468,6 +470,35 @@ Task best-practices-researcher(topic)`
expect(result).not.toContain("@security-sentinel") 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", () => { test("generated skill deduplicates against sanitized pass-through skill names", () => {
const plugin: ClaudePlugin = { const plugin: ClaudePlugin = {
...fixturePlugin, ...fixturePlugin,

View File

@@ -21,7 +21,7 @@ describe("writeCopilotBundle", () => {
agents: [ agents: [
{ {
name: "security-reviewer", 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: [ generatedSkills: [