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> = {
|
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) => {
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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: [
|
||||||
|
|||||||
Reference in New Issue
Block a user