fix(converters): remove invalid tools/infer from Copilot agent frontmatter (#493)
This commit is contained in:
@@ -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())
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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.")
|
||||
@@ -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,
|
||||
@@ -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,
|
||||
@@ -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()
|
||||
@@ -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,
|
||||
|
||||
@@ -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: [
|
||||
|
||||
Reference in New Issue
Block a user