refactor(cli)!: rename all skills and agents to consistent ce- prefix (#503)
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -51,7 +51,7 @@ describe("loadClaudeHome", () => {
|
||||
await fs.mkdir(skillDir, { recursive: true })
|
||||
await fs.writeFile(
|
||||
path.join(skillDir, "SKILL.md"),
|
||||
"---\nname: ce:plan\ndescription: Reviewer skill\nargument-hint: \"[topic]\"\n---\nReview things.\n",
|
||||
"---\nname: ce-plan\ndescription: Reviewer skill\nargument-hint: \"[topic]\"\n---\nReview things.\n",
|
||||
)
|
||||
|
||||
const config = await loadClaudeHome(tempHome)
|
||||
@@ -69,7 +69,7 @@ describe("loadClaudeHome", () => {
|
||||
await fs.mkdir(skillDir, { recursive: true })
|
||||
await fs.writeFile(
|
||||
path.join(skillDir, "SKILL.md"),
|
||||
"---\nname: ce:plan\nfoo: [unterminated\n---\nReview things.\n",
|
||||
"---\nname: ce-plan\nfoo: [unterminated\n---\nReview things.\n",
|
||||
)
|
||||
|
||||
const config = await loadClaudeHome(tempHome)
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import { describe, expect, test } from "bun:test"
|
||||
import { afterEach, describe, expect, test } from "bun:test"
|
||||
import fs from "fs/promises"
|
||||
import os from "os"
|
||||
import path from "path"
|
||||
import { loadClaudePlugin } from "../src/parsers/claude"
|
||||
import { filterSkillsByPlatform } from "../src/types/claude"
|
||||
@@ -9,6 +11,25 @@ const customPathsRoot = path.join(import.meta.dir, "fixtures", "custom-paths")
|
||||
const invalidCommandPathRoot = path.join(import.meta.dir, "fixtures", "invalid-command-path")
|
||||
const invalidHooksPathRoot = path.join(import.meta.dir, "fixtures", "invalid-hooks-path")
|
||||
const invalidMcpPathRoot = path.join(import.meta.dir, "fixtures", "invalid-mcp-path")
|
||||
const tempRoots: string[] = []
|
||||
|
||||
afterEach(async () => {
|
||||
for (const root of tempRoots.splice(0, tempRoots.length)) {
|
||||
await fs.rm(root, { recursive: true, force: true })
|
||||
}
|
||||
})
|
||||
|
||||
async function makeMinimalPluginRoot(): Promise<string> {
|
||||
const root = await fs.mkdtemp(path.join(os.tmpdir(), "claude-parser-agent-md-"))
|
||||
tempRoots.push(root)
|
||||
await fs.mkdir(path.join(root, ".claude-plugin"), { recursive: true })
|
||||
await fs.mkdir(path.join(root, "agents"), { recursive: true })
|
||||
await fs.writeFile(
|
||||
path.join(root, ".claude-plugin", "plugin.json"),
|
||||
JSON.stringify({ name: "test-plugin", version: "1.0.0" }, null, 2),
|
||||
)
|
||||
return root
|
||||
}
|
||||
|
||||
describe("loadClaudePlugin", () => {
|
||||
test("loads manifest, agents, commands, skills, hooks", async () => {
|
||||
@@ -137,4 +158,42 @@ describe("loadClaudePlugin", () => {
|
||||
"Invalid mcpServers path: ../outside-mcp.json. Paths must stay within the plugin root.",
|
||||
)
|
||||
})
|
||||
|
||||
test("loads .agent.md files with explicit frontmatter names", async () => {
|
||||
const root = await makeMinimalPluginRoot()
|
||||
await fs.writeFile(
|
||||
path.join(root, "agents", "repo-research-analyst.agent.md"),
|
||||
`---
|
||||
name: repo-research-analyst
|
||||
description: Research helper
|
||||
---
|
||||
|
||||
Research prompt.
|
||||
`,
|
||||
)
|
||||
|
||||
const plugin = await loadClaudePlugin(root)
|
||||
|
||||
expect(plugin.agents).toHaveLength(1)
|
||||
expect(plugin.agents[0]?.name).toBe("repo-research-analyst")
|
||||
expect(plugin.agents[0]?.sourcePath.endsWith("repo-research-analyst.agent.md")).toBe(true)
|
||||
})
|
||||
|
||||
test("falls back to the filename stem for .agent.md files without name frontmatter", async () => {
|
||||
const root = await makeMinimalPluginRoot()
|
||||
await fs.writeFile(
|
||||
path.join(root, "agents", "cleanup-specialist.agent.md"),
|
||||
`---
|
||||
description: Cleanup helper
|
||||
---
|
||||
|
||||
Cleanup prompt.
|
||||
`,
|
||||
)
|
||||
|
||||
const plugin = await loadClaudePlugin(root)
|
||||
|
||||
expect(plugin.agents).toHaveLength(1)
|
||||
expect(plugin.agents[0]?.name).toBe("cleanup-specialist")
|
||||
})
|
||||
})
|
||||
|
||||
@@ -177,7 +177,7 @@ describe("CLI", () => {
|
||||
expect(stdout).toContain("Installed compound-engineering")
|
||||
// OpenCode global config lives at ~/.config/opencode per XDG spec
|
||||
expect(await exists(path.join(tempRoot, ".config", "opencode", "opencode.json"))).toBe(true)
|
||||
expect(await exists(path.join(tempRoot, ".config", "opencode", "agents", "repo-research-analyst.md"))).toBe(true)
|
||||
expect(await exists(path.join(tempRoot, ".config", "opencode", "agents", "ce-repo-research-analyst.md"))).toBe(true)
|
||||
})
|
||||
|
||||
test("install uses bundled compound-engineering plugin for codex output", async () => {
|
||||
@@ -215,7 +215,6 @@ describe("CLI", () => {
|
||||
|
||||
expect(stdout).toContain("Installed compound-engineering")
|
||||
expect(stdout).toContain(codexRoot)
|
||||
expect(await exists(path.join(codexRoot, "prompts", "ce-plan.md"))).toBe(true)
|
||||
expect(await exists(path.join(codexRoot, "skills", "ce-plan", "SKILL.md"))).toBe(true)
|
||||
expect(await exists(path.join(codexRoot, "AGENTS.md"))).toBe(true)
|
||||
})
|
||||
|
||||
@@ -108,7 +108,7 @@ describe("convertClaudeToCodex", () => {
|
||||
expect(parseFrontmatter(skill!.content).data.model).toBeUndefined()
|
||||
})
|
||||
|
||||
test("generates prompt wrappers for canonical ce workflow skills and omits workflows aliases", () => {
|
||||
test("copies workflow skills as regular skills and omits workflows aliases", () => {
|
||||
const plugin: ClaudePlugin = {
|
||||
...fixturePlugin,
|
||||
manifest: { name: "compound-engineering", version: "1.0.0" },
|
||||
@@ -116,7 +116,7 @@ describe("convertClaudeToCodex", () => {
|
||||
agents: [],
|
||||
skills: [
|
||||
{
|
||||
name: "ce:plan",
|
||||
name: "ce-plan",
|
||||
description: "Planning workflow",
|
||||
argumentHint: "[feature]",
|
||||
sourceDir: "/tmp/plugin/skills/ce-plan",
|
||||
@@ -138,15 +138,11 @@ describe("convertClaudeToCodex", () => {
|
||||
permissions: "none",
|
||||
})
|
||||
|
||||
expect(bundle.prompts).toHaveLength(1)
|
||||
expect(bundle.prompts[0]?.name).toBe("ce-plan")
|
||||
// No prompt wrappers for workflow skills — they're directly invocable as skills
|
||||
expect(bundle.prompts).toHaveLength(0)
|
||||
|
||||
const parsedPrompt = parseFrontmatter(bundle.prompts[0]!.content)
|
||||
expect(parsedPrompt.data.description).toBe("Planning workflow")
|
||||
expect(parsedPrompt.data["argument-hint"]).toBe("[feature]")
|
||||
expect(parsedPrompt.body).toContain("Use the ce:plan skill")
|
||||
|
||||
expect(bundle.skillDirs.map((skill) => skill.name)).toEqual(["ce:plan"])
|
||||
// ce-plan is copied as a regular skill, workflows:plan is omitted
|
||||
expect(bundle.skillDirs.map((skill) => skill.name)).toEqual(["ce-plan"])
|
||||
})
|
||||
|
||||
test("does not apply compound workflow canonicalization to other plugins", () => {
|
||||
@@ -157,7 +153,7 @@ describe("convertClaudeToCodex", () => {
|
||||
agents: [],
|
||||
skills: [
|
||||
{
|
||||
name: "ce:plan",
|
||||
name: "ce-plan",
|
||||
description: "Custom CE-namespaced skill",
|
||||
argumentHint: "[feature]",
|
||||
sourceDir: "/tmp/plugin/skills/ce-plan",
|
||||
@@ -180,7 +176,7 @@ describe("convertClaudeToCodex", () => {
|
||||
})
|
||||
|
||||
expect(bundle.prompts).toHaveLength(0)
|
||||
expect(bundle.skillDirs.map((skill) => skill.name)).toEqual(["ce:plan", "workflows:plan"])
|
||||
expect(bundle.skillDirs.map((skill) => skill.name)).toEqual(["ce-plan", "workflows:plan"])
|
||||
})
|
||||
|
||||
test("passes through MCP servers", () => {
|
||||
@@ -387,7 +383,7 @@ Don't confuse with file paths like /tmp/output.md or /dev/null.`,
|
||||
expect(parsed.body).not.toContain("<script-dir>/prompts:discover-sessions.sh")
|
||||
})
|
||||
|
||||
test("transforms canonical workflow slash commands to Codex prompt references", () => {
|
||||
test("transforms workflow skill slash commands to Codex skill references", () => {
|
||||
const plugin: ClaudePlugin = {
|
||||
...fixturePlugin,
|
||||
manifest: { name: "compound-engineering", version: "1.0.0" },
|
||||
@@ -395,23 +391,23 @@ Don't confuse with file paths like /tmp/output.md or /dev/null.`,
|
||||
{
|
||||
name: "review",
|
||||
description: "Review command",
|
||||
body: `After the brainstorm, run /ce:plan.
|
||||
body: `After the brainstorm, run /ce-plan.
|
||||
|
||||
If planning is complete, continue with /ce:work.`,
|
||||
If planning is complete, continue with /ce-work.`,
|
||||
sourcePath: "/tmp/plugin/commands/review.md",
|
||||
},
|
||||
],
|
||||
agents: [],
|
||||
skills: [
|
||||
{
|
||||
name: "ce:plan",
|
||||
name: "ce-plan",
|
||||
description: "Planning workflow",
|
||||
argumentHint: "[feature]",
|
||||
sourceDir: "/tmp/plugin/skills/ce-plan",
|
||||
skillPath: "/tmp/plugin/skills/ce-plan/SKILL.md",
|
||||
},
|
||||
{
|
||||
name: "ce:work",
|
||||
name: "ce-work",
|
||||
description: "Implementation workflow",
|
||||
argumentHint: "[feature]",
|
||||
sourceDir: "/tmp/plugin/skills/ce-work",
|
||||
@@ -437,9 +433,9 @@ If planning is complete, continue with /ce:work.`,
|
||||
expect(commandSkill).toBeDefined()
|
||||
const parsed = parseFrontmatter(commandSkill!.content)
|
||||
|
||||
expect(parsed.body).toContain("/prompts:ce-plan")
|
||||
expect(parsed.body).toContain("/prompts:ce-work")
|
||||
expect(parsed.body).not.toContain("the ce:plan skill")
|
||||
// Workflow skills are now regular skills, so references use skill syntax
|
||||
expect(parsed.body).toContain("the ce-plan skill")
|
||||
expect(parsed.body).toContain("the ce-work skill")
|
||||
})
|
||||
|
||||
test("excludes commands with disable-model-invocation from prompts and skills", () => {
|
||||
|
||||
@@ -108,6 +108,21 @@ describe("writeCodexBundle", () => {
|
||||
)).toBe(true)
|
||||
})
|
||||
|
||||
test("preserves same-named user prompts during stale prompt cleanup", async () => {
|
||||
const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), "codex-prompts-preserve-"))
|
||||
const codexRoot = path.join(tempRoot, ".codex")
|
||||
const promptsDir = path.join(codexRoot, "prompts")
|
||||
await fs.mkdir(promptsDir, { recursive: true })
|
||||
await fs.writeFile(
|
||||
path.join(promptsDir, "ce-plan.md"),
|
||||
"---\ndescription: \"Project-local ce-plan helper\"\n---\n\nCustom prompt body\n",
|
||||
)
|
||||
|
||||
await writeCodexBundle(codexRoot, { prompts: [], skillDirs: [], generatedSkills: [] })
|
||||
|
||||
expect(await exists(path.join(promptsDir, "ce-plan.md"))).toBe(true)
|
||||
})
|
||||
|
||||
test("preserves existing user config when writing MCP servers", async () => {
|
||||
const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), "codex-backup-"))
|
||||
const codexRoot = path.join(tempRoot, ".codex")
|
||||
@@ -267,31 +282,32 @@ describe("writeCodexBundle", () => {
|
||||
await fs.writeFile(
|
||||
path.join(sourceSkillDir, "SKILL.md"),
|
||||
`---
|
||||
name: ce:brainstorm
|
||||
name: ce-brainstorm
|
||||
description: Brainstorm workflow
|
||||
---
|
||||
|
||||
Continue with /ce:plan when ready.
|
||||
Continue with /ce-plan when ready.
|
||||
Or use /workflows:plan if you're following an older doc.
|
||||
Use /todo-resolve for deeper research.
|
||||
`,
|
||||
)
|
||||
await fs.writeFile(
|
||||
path.join(sourceSkillDir, "notes.md"),
|
||||
"Reference docs still mention /ce:plan here.\n",
|
||||
"Reference docs still mention /ce-plan here.\n",
|
||||
)
|
||||
|
||||
const bundle: CodexBundle = {
|
||||
prompts: [],
|
||||
skillDirs: [{ name: "ce:brainstorm", sourceDir: sourceSkillDir }],
|
||||
skillDirs: [{ name: "ce-brainstorm", sourceDir: sourceSkillDir }],
|
||||
generatedSkills: [],
|
||||
invocationTargets: {
|
||||
promptTargets: {
|
||||
"ce-plan": "ce-plan",
|
||||
"workflows-plan": "ce-plan",
|
||||
"todo-resolve": "todo-resolve",
|
||||
},
|
||||
skillTargets: {},
|
||||
skillTargets: {
|
||||
"ce-plan": "ce-plan",
|
||||
"workflows-plan": "ce-plan",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -301,7 +317,7 @@ Use /todo-resolve for deeper research.
|
||||
path.join(tempRoot, ".codex", "skills", "ce-brainstorm", "SKILL.md"),
|
||||
"utf8",
|
||||
)
|
||||
expect(installedSkill).toContain("/prompts:ce-plan")
|
||||
expect(installedSkill).toContain("the ce-plan skill")
|
||||
expect(installedSkill).not.toContain("/workflows:plan")
|
||||
expect(installedSkill).toContain("/prompts:todo-resolve")
|
||||
|
||||
@@ -309,7 +325,7 @@ Use /todo-resolve for deeper research.
|
||||
path.join(tempRoot, ".codex", "skills", "ce-brainstorm", "notes.md"),
|
||||
"utf8",
|
||||
)
|
||||
expect(notes).toContain("/ce:plan")
|
||||
expect(notes).toContain("/ce-plan")
|
||||
})
|
||||
|
||||
test("transforms namespaced Task calls in copied SKILL.md files", async () => {
|
||||
@@ -319,7 +335,7 @@ Use /todo-resolve for deeper research.
|
||||
await fs.writeFile(
|
||||
path.join(sourceSkillDir, "SKILL.md"),
|
||||
`---
|
||||
name: ce:plan
|
||||
name: ce-plan
|
||||
description: Planning workflow
|
||||
---
|
||||
|
||||
@@ -337,7 +353,7 @@ Also run bare agents:
|
||||
|
||||
const bundle: CodexBundle = {
|
||||
prompts: [],
|
||||
skillDirs: [{ name: "ce:plan", sourceDir: sourceSkillDir }],
|
||||
skillDirs: [{ name: "ce-plan", sourceDir: sourceSkillDir }],
|
||||
generatedSkills: [],
|
||||
invocationTargets: {
|
||||
promptTargets: {},
|
||||
@@ -386,7 +402,7 @@ API examples:
|
||||
- https://www.proofeditor.ai/share/markdown
|
||||
|
||||
Workflow handoff:
|
||||
- /ce:plan
|
||||
- /ce-plan
|
||||
`,
|
||||
)
|
||||
|
||||
@@ -395,10 +411,10 @@ Workflow handoff:
|
||||
skillDirs: [{ name: "proof", sourceDir: sourceSkillDir }],
|
||||
generatedSkills: [],
|
||||
invocationTargets: {
|
||||
promptTargets: {
|
||||
promptTargets: {},
|
||||
skillTargets: {
|
||||
"ce-plan": "ce-plan",
|
||||
},
|
||||
skillTargets: {},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -413,7 +429,7 @@ Workflow handoff:
|
||||
expect(installedSkill).toContain("/settings")
|
||||
expect(installedSkill).toContain("https://www.proofeditor.ai/api/agent/{slug}/state")
|
||||
expect(installedSkill).toContain("https://www.proofeditor.ai/share/markdown")
|
||||
expect(installedSkill).toContain("/prompts:ce-plan")
|
||||
expect(installedSkill).toContain("the ce-plan skill")
|
||||
expect(installedSkill).not.toContain("/prompts:users")
|
||||
expect(installedSkill).not.toContain("/prompts:settings")
|
||||
expect(installedSkill).not.toContain("https://prompts:www.proofeditor.ai")
|
||||
|
||||
@@ -13,7 +13,7 @@ const SHARED_SUPPORT_FILES = [
|
||||
|
||||
const SKILLS_WITH_COPIES = ["ce-compound", "ce-compound-refresh"]
|
||||
|
||||
describe("ce:compound support file drift", () => {
|
||||
describe("ce-compound support file drift", () => {
|
||||
for (const file of SHARED_SUPPORT_FILES) {
|
||||
test(`${file} is identical across ${SKILLS_WITH_COPIES.join(", ")}`, async () => {
|
||||
const contents = await Promise.all(
|
||||
|
||||
@@ -477,9 +477,16 @@ describe("transformSkillContentForOpenCode", () => {
|
||||
expect(transformSkillContentForOpenCode(input)).toBe(input)
|
||||
})
|
||||
|
||||
test("preserves 2-segment plugin:agent names (no category)", () => {
|
||||
test("rewrites 2-segment category:ce-agent refs to flat names", () => {
|
||||
const input = "Dispatch `review:ce-correctness-reviewer` for logic checks."
|
||||
expect(transformSkillContentForOpenCode(input)).toBe(
|
||||
"Dispatch `ce-correctness-reviewer` for logic checks.",
|
||||
)
|
||||
})
|
||||
|
||||
test("preserves 2-segment refs without ce- prefix", () => {
|
||||
const input = "Spawn `compound-engineering:coherence-reviewer` as subagent."
|
||||
// 2-segment names could be skill refs or flat agent refs — not rewritten
|
||||
// 2-segment names without ce- prefix could be skill refs — not rewritten
|
||||
expect(transformSkillContentForOpenCode(input)).toBe(input)
|
||||
})
|
||||
|
||||
|
||||
@@ -172,7 +172,7 @@ describe("writeCopilotBundle", () => {
|
||||
await fs.writeFile(
|
||||
path.join(sourceSkillDir, "SKILL.md"),
|
||||
`---
|
||||
name: ce:plan
|
||||
name: ce-plan
|
||||
description: Planning workflow
|
||||
---
|
||||
|
||||
@@ -187,7 +187,7 @@ Run these research agents:
|
||||
const bundle: CopilotBundle = {
|
||||
agents: [],
|
||||
generatedSkills: [],
|
||||
skillDirs: [{ name: "ce:plan", sourceDir: sourceSkillDir }],
|
||||
skillDirs: [{ name: "ce-plan", sourceDir: sourceSkillDir }],
|
||||
}
|
||||
|
||||
await writeCopilotBundle(tempRoot, bundle)
|
||||
|
||||
@@ -54,7 +54,7 @@ describe("writeDroidBundle", () => {
|
||||
await fs.writeFile(
|
||||
path.join(sourceSkillDir, "SKILL.md"),
|
||||
`---
|
||||
name: ce:plan
|
||||
name: ce-plan
|
||||
description: Planning workflow
|
||||
---
|
||||
|
||||
@@ -69,7 +69,7 @@ Run these research agents:
|
||||
const bundle: DroidBundle = {
|
||||
commands: [],
|
||||
droids: [],
|
||||
skillDirs: [{ name: "ce:plan", sourceDir: sourceSkillDir }],
|
||||
skillDirs: [{ name: "ce-plan", sourceDir: sourceSkillDir }],
|
||||
}
|
||||
|
||||
await writeDroidBundle(tempRoot, bundle)
|
||||
|
||||
@@ -15,6 +15,32 @@ async function exists(filePath: string): Promise<boolean> {
|
||||
}
|
||||
|
||||
describe("writeGeminiBundle", () => {
|
||||
test("removes stale generated agent skill dirs before writing Gemini generated skills", async () => {
|
||||
const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), "gemini-cleanup-"))
|
||||
const legacySkillPath = path.join(tempRoot, ".gemini", "skills", "security-reviewer", "SKILL.md")
|
||||
await fs.mkdir(path.dirname(legacySkillPath), { recursive: true })
|
||||
await fs.writeFile(
|
||||
legacySkillPath,
|
||||
`---\nname: security-reviewer\ndescription: ${JSON.stringify("Conditional code-review persona, selected when the diff touches auth middleware, public endpoints, user input handling, or permission checks. Reviews code for exploitable vulnerabilities.")}\n---\n\nLegacy agent\n`,
|
||||
)
|
||||
|
||||
const bundle: GeminiBundle = {
|
||||
generatedSkills: [
|
||||
{
|
||||
name: "security-reviewer",
|
||||
content: "---\nname: security-reviewer\ndescription: Security\n---\n\nFresh generated skill.",
|
||||
},
|
||||
],
|
||||
skillDirs: [],
|
||||
commands: [],
|
||||
}
|
||||
|
||||
await writeGeminiBundle(tempRoot, bundle)
|
||||
|
||||
const rewritten = await fs.readFile(legacySkillPath, "utf8")
|
||||
expect(rewritten).toContain("Fresh generated skill.")
|
||||
})
|
||||
|
||||
test("writes skills, commands, and settings.json", async () => {
|
||||
const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), "gemini-test-"))
|
||||
const bundle: GeminiBundle = {
|
||||
@@ -73,7 +99,7 @@ describe("writeGeminiBundle", () => {
|
||||
await fs.writeFile(
|
||||
path.join(sourceSkillDir, "SKILL.md"),
|
||||
`---
|
||||
name: ce:plan
|
||||
name: ce-plan
|
||||
description: Planning workflow
|
||||
---
|
||||
|
||||
@@ -87,7 +113,7 @@ Run these research agents:
|
||||
|
||||
const bundle: GeminiBundle = {
|
||||
generatedSkills: [],
|
||||
skillDirs: [{ name: "ce:plan", sourceDir: sourceSkillDir }],
|
||||
skillDirs: [{ name: "ce-plan", sourceDir: sourceSkillDir }],
|
||||
commands: [],
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ import { promises as fs } from "fs"
|
||||
import path from "path"
|
||||
import os from "os"
|
||||
import { writeKiroBundle } from "../src/targets/kiro"
|
||||
import { parseFrontmatter } from "../src/utils/frontmatter"
|
||||
import type { KiroBundle } from "../src/types/kiro"
|
||||
|
||||
async function exists(filePath: string): Promise<boolean> {
|
||||
@@ -14,6 +15,15 @@ async function exists(filePath: string): Promise<boolean> {
|
||||
}
|
||||
}
|
||||
|
||||
async function pluginDescription(relativePath: string): Promise<string> {
|
||||
const raw = await fs.readFile(path.join(import.meta.dir, "..", relativePath), "utf8")
|
||||
const { data } = parseFrontmatter(raw, relativePath)
|
||||
if (typeof data.description !== "string") {
|
||||
throw new Error(`Missing description in ${relativePath}`)
|
||||
}
|
||||
return data.description
|
||||
}
|
||||
|
||||
const emptyBundle: KiroBundle = {
|
||||
agents: [],
|
||||
generatedSkills: [],
|
||||
@@ -23,6 +33,37 @@ const emptyBundle: KiroBundle = {
|
||||
}
|
||||
|
||||
describe("writeKiroBundle", () => {
|
||||
test("removes legacy Kiro agent config and prompt files during rename cleanup", async () => {
|
||||
const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), "kiro-cleanup-"))
|
||||
const kiroRoot = path.join(tempRoot, ".kiro")
|
||||
await fs.mkdir(path.join(kiroRoot, "agents", "prompts"), { recursive: true })
|
||||
const sessionHistorianDescription = await pluginDescription(
|
||||
"plugins/compound-engineering/agents/research/ce-session-historian.agent.md",
|
||||
)
|
||||
|
||||
await fs.writeFile(
|
||||
path.join(kiroRoot, "agents", "session-historian.json"),
|
||||
JSON.stringify({
|
||||
name: "session-historian",
|
||||
description: sessionHistorianDescription,
|
||||
prompt: "file://./prompts/session-historian.md",
|
||||
tools: ["*"],
|
||||
resources: ["file://.kiro/steering/**/*.md", "skill://.kiro/skills/**/SKILL.md"],
|
||||
includeMcpJson: true,
|
||||
welcomeMessage: `Switching to the session-historian agent. ${sessionHistorianDescription}`,
|
||||
}),
|
||||
)
|
||||
await fs.writeFile(
|
||||
path.join(kiroRoot, "agents", "prompts", "session-historian.md"),
|
||||
"Legacy session-historian prompt\n",
|
||||
)
|
||||
|
||||
await writeKiroBundle(kiroRoot, emptyBundle)
|
||||
|
||||
expect(await exists(path.join(kiroRoot, "agents", "session-historian.json"))).toBe(false)
|
||||
expect(await exists(path.join(kiroRoot, "agents", "prompts", "session-historian.md"))).toBe(false)
|
||||
})
|
||||
|
||||
test("writes agents, skills, steering, and mcp.json", async () => {
|
||||
const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), "kiro-test-"))
|
||||
const bundle: KiroBundle = {
|
||||
@@ -106,7 +147,7 @@ describe("writeKiroBundle", () => {
|
||||
await fs.writeFile(
|
||||
path.join(sourceSkillDir, "SKILL.md"),
|
||||
`---
|
||||
name: ce:plan
|
||||
name: ce-plan
|
||||
description: Planning workflow
|
||||
---
|
||||
|
||||
@@ -120,7 +161,7 @@ Run these research agents:
|
||||
|
||||
const bundle: KiroBundle = {
|
||||
...emptyBundle,
|
||||
skillDirs: [{ name: "ce:plan", sourceDir: sourceSkillDir }],
|
||||
skillDirs: [{ name: "ce-plan", sourceDir: sourceSkillDir }],
|
||||
}
|
||||
|
||||
await writeKiroBundle(tempRoot, bundle)
|
||||
|
||||
599
tests/legacy-cleanup.test.ts
Normal file
599
tests/legacy-cleanup.test.ts
Normal file
@@ -0,0 +1,599 @@
|
||||
import { describe, expect, test } from "bun:test"
|
||||
import fs from "fs/promises"
|
||||
import path from "path"
|
||||
import os from "os"
|
||||
import { parseFrontmatter } from "../src/utils/frontmatter"
|
||||
import { cleanupStaleSkillDirs, cleanupStaleAgents, cleanupStalePrompts } from "../src/utils/legacy-cleanup"
|
||||
|
||||
async function createDir(dir: string, content = "placeholder") {
|
||||
await fs.mkdir(dir, { recursive: true })
|
||||
await fs.writeFile(path.join(dir, "SKILL.md"), content)
|
||||
}
|
||||
|
||||
async function createFile(filePath: string, content = "placeholder") {
|
||||
await fs.mkdir(path.dirname(filePath), { recursive: true })
|
||||
await fs.writeFile(filePath, content)
|
||||
}
|
||||
|
||||
async function exists(p: string): Promise<boolean> {
|
||||
try {
|
||||
await fs.stat(p)
|
||||
return true
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
async function pluginDescription(relativePath: string): Promise<string> {
|
||||
const raw = await fs.readFile(path.join(import.meta.dir, "..", relativePath), "utf8")
|
||||
const { data } = parseFrontmatter(raw, relativePath)
|
||||
if (typeof data.description !== "string") {
|
||||
throw new Error(`Missing description in ${relativePath}`)
|
||||
}
|
||||
return data.description
|
||||
}
|
||||
|
||||
function skillContent(name: string, description: string): string {
|
||||
return `---\nname: ${name}\ndescription: ${JSON.stringify(description)}\n---\n\n# ${name}\n`
|
||||
}
|
||||
|
||||
function agentContent(name: string, description: string): string {
|
||||
return `---\nname: ${name}\ndescription: ${JSON.stringify(description)}\n---\n\nBody\n`
|
||||
}
|
||||
|
||||
function promptWrapperContent(skillName: string, description: string, body = "Body") {
|
||||
return `---\ndescription: ${JSON.stringify(description)}\n---\n\nUse the $${skillName} skill for this command and follow its instructions.\n\n${body}\n`
|
||||
}
|
||||
|
||||
function legacyWorkflowPromptContent(skillName: string, description: string) {
|
||||
return `---\ndescription: ${JSON.stringify(description)}\n---\n\nUse the ${skillName} skill for this workflow and follow its instructions exactly.\n\nTreat any text after the prompt name as the workflow context to pass through.\n`
|
||||
}
|
||||
|
||||
function kiroAgentConfigContent(name: string, description: string) {
|
||||
return JSON.stringify({
|
||||
name,
|
||||
description,
|
||||
prompt: `file://./prompts/${name}.md`,
|
||||
tools: ["*"],
|
||||
resources: [
|
||||
"file://.kiro/steering/**/*.md",
|
||||
"skill://.kiro/skills/**/SKILL.md",
|
||||
],
|
||||
includeMcpJson: true,
|
||||
welcomeMessage: `Switching to the ${name} agent. ${description}`,
|
||||
})
|
||||
}
|
||||
|
||||
describe("cleanupStaleSkillDirs", () => {
|
||||
test("removes known stale skill directories", async () => {
|
||||
const root = await fs.mkdtemp(path.join(os.tmpdir(), "cleanup-skills-"))
|
||||
await createDir(
|
||||
path.join(root, "git-commit"),
|
||||
skillContent(
|
||||
"git-commit",
|
||||
await pluginDescription("plugins/compound-engineering/skills/ce-commit/SKILL.md"),
|
||||
),
|
||||
)
|
||||
await createDir(
|
||||
path.join(root, "setup"),
|
||||
skillContent(
|
||||
"setup",
|
||||
await pluginDescription("plugins/compound-engineering/skills/ce-setup/SKILL.md"),
|
||||
),
|
||||
)
|
||||
await createDir(
|
||||
path.join(root, "document-review"),
|
||||
skillContent(
|
||||
"document-review",
|
||||
await pluginDescription("plugins/compound-engineering/skills/ce-doc-review/SKILL.md"),
|
||||
),
|
||||
)
|
||||
|
||||
const removed = await cleanupStaleSkillDirs(root)
|
||||
|
||||
expect(removed).toBe(3)
|
||||
expect(await exists(path.join(root, "git-commit"))).toBe(false)
|
||||
expect(await exists(path.join(root, "setup"))).toBe(false)
|
||||
expect(await exists(path.join(root, "document-review"))).toBe(false)
|
||||
})
|
||||
|
||||
test("preserves non-stale directories", async () => {
|
||||
const root = await fs.mkdtemp(path.join(os.tmpdir(), "cleanup-preserve-"))
|
||||
await createDir(path.join(root, "ce-plan"))
|
||||
await createDir(path.join(root, "ce-commit"))
|
||||
await createDir(path.join(root, "custom-user-skill"))
|
||||
|
||||
const removed = await cleanupStaleSkillDirs(root)
|
||||
|
||||
expect(removed).toBe(0)
|
||||
expect(await exists(path.join(root, "ce-plan"))).toBe(true)
|
||||
expect(await exists(path.join(root, "ce-commit"))).toBe(true)
|
||||
expect(await exists(path.join(root, "custom-user-skill"))).toBe(true)
|
||||
})
|
||||
|
||||
test("removes ce-review and ce-document-review (renamed skills)", async () => {
|
||||
const root = await fs.mkdtemp(path.join(os.tmpdir(), "cleanup-renamed-"))
|
||||
await createDir(
|
||||
path.join(root, "ce-review"),
|
||||
skillContent(
|
||||
"ce-review",
|
||||
await pluginDescription("plugins/compound-engineering/skills/ce-code-review/SKILL.md"),
|
||||
),
|
||||
)
|
||||
await createDir(
|
||||
path.join(root, "ce-document-review"),
|
||||
skillContent(
|
||||
"ce-document-review",
|
||||
await pluginDescription("plugins/compound-engineering/skills/ce-doc-review/SKILL.md"),
|
||||
),
|
||||
)
|
||||
|
||||
const removed = await cleanupStaleSkillDirs(root)
|
||||
|
||||
expect(removed).toBe(2)
|
||||
expect(await exists(path.join(root, "ce-review"))).toBe(false)
|
||||
expect(await exists(path.join(root, "ce-document-review"))).toBe(false)
|
||||
})
|
||||
|
||||
test("returns 0 when directory does not exist", async () => {
|
||||
const removed = await cleanupStaleSkillDirs("/tmp/nonexistent-cleanup-dir-12345")
|
||||
expect(removed).toBe(0)
|
||||
})
|
||||
|
||||
test("preserves same-named user skill directories when content does not match plugin fingerprints", async () => {
|
||||
const root = await fs.mkdtemp(path.join(os.tmpdir(), "cleanup-user-skill-"))
|
||||
await createDir(
|
||||
path.join(root, "setup"),
|
||||
skillContent("setup", "User-owned setup skill unrelated to compound-engineering."),
|
||||
)
|
||||
|
||||
const removed = await cleanupStaleSkillDirs(root)
|
||||
|
||||
expect(removed).toBe(0)
|
||||
expect(await exists(path.join(root, "setup"))).toBe(true)
|
||||
})
|
||||
|
||||
test("removes legacy setup skill even when current description has drifted", async () => {
|
||||
const root = await fs.mkdtemp(path.join(os.tmpdir(), "cleanup-setup-legacy-"))
|
||||
await createDir(
|
||||
path.join(root, "setup"),
|
||||
skillContent(
|
||||
"setup",
|
||||
"Configure project-level settings for compound-engineering workflows. Currently a placeholder — review agent selection is handled automatically by ce:review.",
|
||||
),
|
||||
)
|
||||
|
||||
const removed = await cleanupStaleSkillDirs(root)
|
||||
|
||||
expect(removed).toBe(1)
|
||||
expect(await exists(path.join(root, "setup"))).toBe(false)
|
||||
})
|
||||
|
||||
test("removes legacy-only skills that no longer ship a ce-* replacement", async () => {
|
||||
const root = await fs.mkdtemp(path.join(os.tmpdir(), "cleanup-legacy-only-skills-"))
|
||||
// `feature-video` and `reproduce-bug` were shipped by older plugin versions
|
||||
// but have no current ce-* counterpart. Their fingerprints come from the
|
||||
// LEGACY_ONLY_SKILL_DESCRIPTIONS map, not from a live plugin file.
|
||||
await createDir(
|
||||
path.join(root, "feature-video"),
|
||||
skillContent(
|
||||
"feature-video",
|
||||
"Record a video walkthrough of a feature and add it to the PR description. Use when a PR needs a visual demo for reviewers, when the user asks to demo a feature, create a PR video, record a walkthrough, show what changed visually, or add a video to a pull request.",
|
||||
),
|
||||
)
|
||||
await createDir(
|
||||
path.join(root, "reproduce-bug"),
|
||||
skillContent(
|
||||
"reproduce-bug",
|
||||
"Systematically reproduce and investigate a bug from a GitHub issue. Use when the user provides a GitHub issue number or URL for a bug they want reproduced or investigated.",
|
||||
),
|
||||
)
|
||||
await createDir(
|
||||
path.join(root, "claude-permissions-optimizer"),
|
||||
skillContent(
|
||||
"claude-permissions-optimizer",
|
||||
"Optimize Claude Code permissions by finding safe Bash commands from session history and auto-applying them to settings.json. Can run from any coding agent but targets Claude Code specifically. Use when experiencing permission fatigue, too many permission prompts, wanting to optimize permissions, or needing to set up allowlists. Triggers on \"optimize permissions\", \"reduce permission prompts\", \"allowlist commands\", \"too many permission prompts\", \"permission fatigue\", \"permission setup\", or complaints about clicking approve too often.",
|
||||
),
|
||||
)
|
||||
|
||||
const removed = await cleanupStaleSkillDirs(root)
|
||||
|
||||
expect(removed).toBe(3)
|
||||
expect(await exists(path.join(root, "feature-video"))).toBe(false)
|
||||
expect(await exists(path.join(root, "reproduce-bug"))).toBe(false)
|
||||
expect(await exists(path.join(root, "claude-permissions-optimizer"))).toBe(false)
|
||||
})
|
||||
|
||||
test("preserves same-named user skills for legacy-only entries when content differs", async () => {
|
||||
const root = await fs.mkdtemp(path.join(os.tmpdir(), "cleanup-legacy-only-user-"))
|
||||
await createDir(
|
||||
path.join(root, "reproduce-bug"),
|
||||
skillContent("reproduce-bug", "A project-local reproduce-bug helper unrelated to compound-engineering."),
|
||||
)
|
||||
|
||||
const removed = await cleanupStaleSkillDirs(root)
|
||||
|
||||
expect(removed).toBe(0)
|
||||
expect(await exists(path.join(root, "reproduce-bug"))).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe("cleanupStaleAgents", () => {
|
||||
test("removes flat .md agent files", async () => {
|
||||
const root = await fs.mkdtemp(path.join(os.tmpdir(), "cleanup-agents-md-"))
|
||||
await createFile(
|
||||
path.join(root, "adversarial-reviewer.md"),
|
||||
agentContent(
|
||||
"adversarial-reviewer",
|
||||
await pluginDescription("plugins/compound-engineering/agents/review/ce-adversarial-reviewer.agent.md"),
|
||||
),
|
||||
)
|
||||
await createFile(
|
||||
path.join(root, "learnings-researcher.md"),
|
||||
agentContent(
|
||||
"learnings-researcher",
|
||||
await pluginDescription("plugins/compound-engineering/agents/research/ce-learnings-researcher.agent.md"),
|
||||
),
|
||||
)
|
||||
|
||||
const removed = await cleanupStaleAgents(root, ".md")
|
||||
|
||||
expect(removed).toBe(2)
|
||||
expect(await exists(path.join(root, "adversarial-reviewer.md"))).toBe(false)
|
||||
expect(await exists(path.join(root, "learnings-researcher.md"))).toBe(false)
|
||||
})
|
||||
|
||||
test("removes .agent.md files (Copilot format)", async () => {
|
||||
const root = await fs.mkdtemp(path.join(os.tmpdir(), "cleanup-agents-copilot-"))
|
||||
await createFile(
|
||||
path.join(root, "security-sentinel.agent.md"),
|
||||
agentContent(
|
||||
"security-sentinel",
|
||||
await pluginDescription("plugins/compound-engineering/agents/review/ce-security-sentinel.agent.md"),
|
||||
),
|
||||
)
|
||||
await createFile(
|
||||
path.join(root, "performance-oracle.agent.md"),
|
||||
agentContent(
|
||||
"performance-oracle",
|
||||
await pluginDescription("plugins/compound-engineering/agents/review/ce-performance-oracle.agent.md"),
|
||||
),
|
||||
)
|
||||
|
||||
const removed = await cleanupStaleAgents(root, ".agent.md")
|
||||
|
||||
expect(removed).toBe(2)
|
||||
expect(await exists(path.join(root, "security-sentinel.agent.md"))).toBe(false)
|
||||
})
|
||||
|
||||
test("removes matching Kiro agent configs but preserves same-named user configs", async () => {
|
||||
const root = await fs.mkdtemp(path.join(os.tmpdir(), "cleanup-agents-kiro-"))
|
||||
await createFile(
|
||||
path.join(root, "slack-researcher.json"),
|
||||
kiroAgentConfigContent(
|
||||
"slack-researcher",
|
||||
await pluginDescription("plugins/compound-engineering/agents/research/ce-slack-researcher.agent.md"),
|
||||
),
|
||||
)
|
||||
await createFile(
|
||||
path.join(root, "session-historian.json"),
|
||||
kiroAgentConfigContent(
|
||||
"session-historian",
|
||||
await pluginDescription("plugins/compound-engineering/agents/research/ce-session-historian.agent.md"),
|
||||
),
|
||||
)
|
||||
await createFile(
|
||||
path.join(root, "lint.json"),
|
||||
kiroAgentConfigContent(
|
||||
"lint",
|
||||
"A project-local lint helper unrelated to compound-engineering.",
|
||||
),
|
||||
)
|
||||
|
||||
const removed = await cleanupStaleAgents(root, ".json")
|
||||
|
||||
expect(removed).toBe(2)
|
||||
expect(await exists(path.join(root, "slack-researcher.json"))).toBe(false)
|
||||
expect(await exists(path.join(root, "session-historian.json"))).toBe(false)
|
||||
expect(await exists(path.join(root, "lint.json"))).toBe(true)
|
||||
})
|
||||
|
||||
test("removes agent directories when extension is null", async () => {
|
||||
const root = await fs.mkdtemp(path.join(os.tmpdir(), "cleanup-agents-dir-"))
|
||||
await createDir(
|
||||
path.join(root, "code-simplicity-reviewer"),
|
||||
skillContent(
|
||||
"code-simplicity-reviewer",
|
||||
await pluginDescription("plugins/compound-engineering/agents/review/ce-code-simplicity-reviewer.agent.md"),
|
||||
),
|
||||
)
|
||||
await createDir(
|
||||
path.join(root, "repo-research-analyst"),
|
||||
skillContent(
|
||||
"repo-research-analyst",
|
||||
await pluginDescription("plugins/compound-engineering/agents/research/ce-repo-research-analyst.agent.md"),
|
||||
),
|
||||
)
|
||||
|
||||
const removed = await cleanupStaleAgents(root, null)
|
||||
|
||||
expect(removed).toBe(2)
|
||||
expect(await exists(path.join(root, "code-simplicity-reviewer"))).toBe(false)
|
||||
expect(await exists(path.join(root, "repo-research-analyst"))).toBe(false)
|
||||
})
|
||||
|
||||
test("preserves ce-prefixed agent files", async () => {
|
||||
const root = await fs.mkdtemp(path.join(os.tmpdir(), "cleanup-agents-keep-"))
|
||||
await createFile(path.join(root, "ce-adversarial-reviewer.md"), agentContent("ce-adversarial-reviewer", "custom"))
|
||||
await createFile(path.join(root, "ce-learnings-researcher.md"), agentContent("ce-learnings-researcher", "custom"))
|
||||
|
||||
const removed = await cleanupStaleAgents(root, ".md")
|
||||
|
||||
expect(removed).toBe(0)
|
||||
expect(await exists(path.join(root, "ce-adversarial-reviewer.md"))).toBe(true)
|
||||
})
|
||||
|
||||
test("preserves same-named user agent files when content does not match plugin fingerprints", async () => {
|
||||
const root = await fs.mkdtemp(path.join(os.tmpdir(), "cleanup-agents-user-"))
|
||||
await createFile(
|
||||
path.join(root, "lint.md"),
|
||||
agentContent("lint", "A project-local lint helper unrelated to compound-engineering."),
|
||||
)
|
||||
|
||||
const removed = await cleanupStaleAgents(root, ".md")
|
||||
|
||||
expect(removed).toBe(0)
|
||||
expect(await exists(path.join(root, "lint.md"))).toBe(true)
|
||||
})
|
||||
|
||||
test("removes legacy-only agents that no longer ship a ce-* replacement", async () => {
|
||||
const root = await fs.mkdtemp(path.join(os.tmpdir(), "cleanup-agents-legacy-only-"))
|
||||
// `lint` and `bug-reproduction-validator` were removed in an older plugin
|
||||
// release with no ce-* successor. Their fingerprints live in
|
||||
// LEGACY_ONLY_AGENT_DESCRIPTIONS so upgrades from pre-removal installs
|
||||
// still clean them up.
|
||||
await createFile(
|
||||
path.join(root, "lint.md"),
|
||||
agentContent(
|
||||
"lint",
|
||||
"Use this agent when you need to run linting and code quality checks on Ruby and ERB files. Run before pushing to origin.",
|
||||
),
|
||||
)
|
||||
await createFile(
|
||||
path.join(root, "bug-reproduction-validator.md"),
|
||||
agentContent(
|
||||
"bug-reproduction-validator",
|
||||
"Systematically reproduces and validates bug reports to confirm whether reported behavior is an actual bug. Use when you receive a bug report or issue that needs verification.",
|
||||
),
|
||||
)
|
||||
|
||||
const removed = await cleanupStaleAgents(root, ".md")
|
||||
|
||||
expect(removed).toBe(2)
|
||||
expect(await exists(path.join(root, "lint.md"))).toBe(false)
|
||||
expect(await exists(path.join(root, "bug-reproduction-validator.md"))).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe("cleanupStalePrompts", () => {
|
||||
test("removes old workflow prompt wrappers", async () => {
|
||||
const root = await fs.mkdtemp(path.join(os.tmpdir(), "cleanup-prompts-"))
|
||||
await createFile(
|
||||
path.join(root, "ce-plan.md"),
|
||||
promptWrapperContent(
|
||||
"ce-plan",
|
||||
await pluginDescription("plugins/compound-engineering/skills/ce-plan/SKILL.md"),
|
||||
),
|
||||
)
|
||||
await createFile(
|
||||
path.join(root, "ce-review.md"),
|
||||
promptWrapperContent(
|
||||
"ce-review",
|
||||
await pluginDescription("plugins/compound-engineering/skills/ce-code-review/SKILL.md"),
|
||||
),
|
||||
)
|
||||
await createFile(
|
||||
path.join(root, "ce-brainstorm.md"),
|
||||
promptWrapperContent(
|
||||
"ce-brainstorm",
|
||||
await pluginDescription("plugins/compound-engineering/skills/ce-brainstorm/SKILL.md"),
|
||||
),
|
||||
)
|
||||
|
||||
const removed = await cleanupStalePrompts(root)
|
||||
|
||||
expect(removed).toBe(3)
|
||||
expect(await exists(path.join(root, "ce-plan.md"))).toBe(false)
|
||||
expect(await exists(path.join(root, "ce-review.md"))).toBe(false)
|
||||
})
|
||||
|
||||
test("preserves non-stale prompt files", async () => {
|
||||
const root = await fs.mkdtemp(path.join(os.tmpdir(), "cleanup-prompts-keep-"))
|
||||
await createFile(path.join(root, "my-custom-prompt.md"))
|
||||
await createFile(path.join(root, "review-command.md"))
|
||||
|
||||
const removed = await cleanupStalePrompts(root)
|
||||
|
||||
expect(removed).toBe(0)
|
||||
expect(await exists(path.join(root, "my-custom-prompt.md"))).toBe(true)
|
||||
})
|
||||
|
||||
test("preserves same-named user prompt files when content does not match plugin fingerprints", async () => {
|
||||
const root = await fs.mkdtemp(path.join(os.tmpdir(), "cleanup-prompts-user-"))
|
||||
await createFile(
|
||||
path.join(root, "ce-plan.md"),
|
||||
"---\ndescription: \"A project-local ce-plan helper\"\n---\n\nCustom prompt body\n",
|
||||
)
|
||||
|
||||
const removed = await cleanupStalePrompts(root)
|
||||
|
||||
expect(removed).toBe(0)
|
||||
expect(await exists(path.join(root, "ce-plan.md"))).toBe(true)
|
||||
})
|
||||
|
||||
test("removes pre-rename workflow prompt wrappers with ce:* references", async () => {
|
||||
const root = await fs.mkdtemp(path.join(os.tmpdir(), "cleanup-prompts-legacy-workflow-"))
|
||||
await createFile(
|
||||
path.join(root, "ce-plan.md"),
|
||||
legacyWorkflowPromptContent(
|
||||
"ce:plan",
|
||||
(await pluginDescription("plugins/compound-engineering/skills/ce-plan/SKILL.md"))
|
||||
.replaceAll("ce-", "ce:"),
|
||||
),
|
||||
)
|
||||
await createFile(
|
||||
path.join(root, "ce-work-beta.md"),
|
||||
legacyWorkflowPromptContent(
|
||||
"ce:work-beta",
|
||||
(await pluginDescription("plugins/compound-engineering/skills/ce-work-beta/SKILL.md"))
|
||||
.replaceAll("ce-", "ce:"),
|
||||
),
|
||||
)
|
||||
|
||||
const removed = await cleanupStalePrompts(root)
|
||||
|
||||
expect(removed).toBe(2)
|
||||
expect(await exists(path.join(root, "ce-plan.md"))).toBe(false)
|
||||
expect(await exists(path.join(root, "ce-work-beta.md"))).toBe(false)
|
||||
})
|
||||
|
||||
test("removes wrappers whose description has drifted (matches a known historical alias)", async () => {
|
||||
// Regression: across shipped plugin versions the ce-plan / ce-work /
|
||||
// ce-work-beta descriptions have been reworded multiple times. Requiring
|
||||
// an exact match against the live skill description left pre-upgrade
|
||||
// wrappers in place, so users kept a prompt entrypoint that still
|
||||
// targeted the pre-rename skill.
|
||||
//
|
||||
// Cleanup now accepts any description that appears in the plugin's
|
||||
// `LEGACY_PROMPT_DESCRIPTION_ALIASES` list for that file (in addition to
|
||||
// the current shipped description). The strings below are real
|
||||
// descriptions compound-engineering has shipped in prior releases, so
|
||||
// they must be recognized as owned.
|
||||
const root = await fs.mkdtemp(path.join(os.tmpdir(), "cleanup-prompts-drifted-desc-"))
|
||||
|
||||
// v2.66.1-style ce-plan description (no trailing ce-brainstorm guidance).
|
||||
await createFile(
|
||||
path.join(root, "ce-plan.md"),
|
||||
promptWrapperContent(
|
||||
"ce-plan",
|
||||
"Create structured plans for any multi-step task -- software features, research workflows, events, study plans, or any goal that benefits from structured breakdown. Also deepen existing plans with interactive review of sub-agent findings.",
|
||||
),
|
||||
)
|
||||
// v2.55-era ce-work description with a completely different opening.
|
||||
await createFile(
|
||||
path.join(root, "ce-work.md"),
|
||||
promptWrapperContent(
|
||||
"ce-work",
|
||||
"Transform feature descriptions or requirements into implementation plans grounded in repo patterns and research.",
|
||||
),
|
||||
)
|
||||
// Pre-rename ce-work-beta description still referencing the ce:work
|
||||
// skill name. Normalization must still accept it.
|
||||
await createFile(
|
||||
path.join(root, "ce-work-beta.md"),
|
||||
promptWrapperContent(
|
||||
"ce-work-beta",
|
||||
"[BETA] Execute work with external delegate support. Same as ce:work but includes experimental Codex delegation mode for token-conserving code implementation.",
|
||||
),
|
||||
)
|
||||
|
||||
const removed = await cleanupStalePrompts(root)
|
||||
|
||||
expect(removed).toBe(3)
|
||||
expect(await exists(path.join(root, "ce-plan.md"))).toBe(false)
|
||||
expect(await exists(path.join(root, "ce-work.md"))).toBe(false)
|
||||
expect(await exists(path.join(root, "ce-work-beta.md"))).toBe(false)
|
||||
})
|
||||
|
||||
test("preserves wrappers whose description was never shipped by compound-engineering", async () => {
|
||||
// Defense-in-depth against a sibling plugin installed into the same
|
||||
// `~/.codex/prompts/` directory. `renderPrompt` in
|
||||
// `src/converters/claude-to-codex.ts` emits the instruction sentence for
|
||||
// every plugin that ships invocable commands, so body alone is not proof
|
||||
// of ownership — a third-party plugin whose skill happens to be named
|
||||
// `ce-plan` / `ce-work` (for example a compound-engineering fork keeping
|
||||
// the `ce-*` namespace) would produce a wrapper whose body matches ours
|
||||
// verbatim.
|
||||
//
|
||||
// Cleanup must leave those wrappers alone. The additional ownership
|
||||
// signal is the frontmatter description: if it is not one
|
||||
// compound-engineering has ever shipped, the file belongs to somebody
|
||||
// else and we refuse to delete it.
|
||||
const root = await fs.mkdtemp(path.join(os.tmpdir(), "cleanup-prompts-foreign-desc-"))
|
||||
await createFile(
|
||||
path.join(root, "ce-plan.md"),
|
||||
promptWrapperContent(
|
||||
"ce-plan",
|
||||
"A sibling plugin's ce-plan wrapper. This description has never been shipped by compound-engineering, so cleanup must preserve the file.",
|
||||
),
|
||||
)
|
||||
await createFile(
|
||||
path.join(root, "ce-brainstorm.md"),
|
||||
promptWrapperContent(
|
||||
"ce-brainstorm",
|
||||
"Fork-specific brainstorm wrapper with a description compound-engineering has never shipped.",
|
||||
),
|
||||
)
|
||||
await createFile(
|
||||
path.join(root, "ce-work.md"),
|
||||
promptWrapperContent(
|
||||
"ce-work",
|
||||
"Another plugin's ce-work prompt wrapper; keeps the ce-* namespace but has its own wording.",
|
||||
),
|
||||
)
|
||||
|
||||
const removed = await cleanupStalePrompts(root)
|
||||
|
||||
expect(removed).toBe(0)
|
||||
expect(await exists(path.join(root, "ce-plan.md"))).toBe(true)
|
||||
expect(await exists(path.join(root, "ce-brainstorm.md"))).toBe(true)
|
||||
expect(await exists(path.join(root, "ce-work.md"))).toBe(true)
|
||||
})
|
||||
|
||||
test("preserves user files whose body is not the plugin-generated boilerplate", async () => {
|
||||
// Independent of the description check, cleanup must refuse to delete
|
||||
// user-authored prompts that happen to share a stale file name but do
|
||||
// not carry the plugin-generated instruction sentence in their body.
|
||||
const root = await fs.mkdtemp(path.join(os.tmpdir(), "cleanup-prompts-user-body-"))
|
||||
await createFile(
|
||||
path.join(root, "ce-plan.md"),
|
||||
`---\ndescription: "User-authored ce-plan helper"\n---\n\nThis prompt does not invoke the ce-plan skill — it is a private workflow.\n`,
|
||||
)
|
||||
await createFile(
|
||||
path.join(root, "ce-work.md"),
|
||||
`---\ndescription: "Execute work efficiently while maintaining quality and finishing features"\n---\n\nCustom body that mentions the ce-work skill but not via the plugin's instruction boilerplate.\n`,
|
||||
)
|
||||
|
||||
const removed = await cleanupStalePrompts(root)
|
||||
|
||||
expect(removed).toBe(0)
|
||||
expect(await exists(path.join(root, "ce-plan.md"))).toBe(true)
|
||||
expect(await exists(path.join(root, "ce-work.md"))).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe("idempotency", () => {
|
||||
test("running cleanup twice returns 0 on second run", async () => {
|
||||
const root = await fs.mkdtemp(path.join(os.tmpdir(), "cleanup-idempotent-"))
|
||||
await createDir(
|
||||
path.join(root, "git-commit"),
|
||||
skillContent(
|
||||
"git-commit",
|
||||
await pluginDescription("plugins/compound-engineering/skills/ce-commit/SKILL.md"),
|
||||
),
|
||||
)
|
||||
await createFile(
|
||||
path.join(root, "adversarial-reviewer.md"),
|
||||
agentContent(
|
||||
"adversarial-reviewer",
|
||||
await pluginDescription("plugins/compound-engineering/agents/review/ce-adversarial-reviewer.agent.md"),
|
||||
),
|
||||
)
|
||||
|
||||
const first = await cleanupStaleSkillDirs(root) + await cleanupStaleAgents(root, ".md")
|
||||
expect(first).toBe(2)
|
||||
|
||||
const second = await cleanupStaleSkillDirs(root) + await cleanupStaleAgents(root, ".md")
|
||||
expect(second).toBe(0)
|
||||
})
|
||||
})
|
||||
@@ -249,7 +249,7 @@ describe("convertClaudeToOpenClaw", () => {
|
||||
...fixturePlugin,
|
||||
skills: [
|
||||
{
|
||||
name: "ce:plan",
|
||||
name: "ce-plan",
|
||||
description: "Planning skill",
|
||||
sourceDir: "/tmp/plugin/skills/ce-plan",
|
||||
skillPath: "/tmp/plugin/skills/ce-plan/SKILL.md",
|
||||
|
||||
@@ -3,8 +3,31 @@ import { promises as fs } from "fs"
|
||||
import os from "os"
|
||||
import path from "path"
|
||||
import { writeOpenClawBundle } from "../src/targets/openclaw"
|
||||
import { parseFrontmatter } from "../src/utils/frontmatter"
|
||||
import type { OpenClawBundle } from "../src/types/openclaw"
|
||||
|
||||
async function exists(targetPath: string): Promise<boolean> {
|
||||
try {
|
||||
await fs.stat(targetPath)
|
||||
return true
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
async function pluginDescription(relativePath: string): Promise<string> {
|
||||
const raw = await fs.readFile(path.join(import.meta.dir, "..", relativePath), "utf8")
|
||||
const { data } = parseFrontmatter(raw, relativePath)
|
||||
if (typeof data.description !== "string") {
|
||||
throw new Error(`Missing description in ${relativePath}`)
|
||||
}
|
||||
return data.description
|
||||
}
|
||||
|
||||
function legacyAgentSkillContent(name: string, description: string): string {
|
||||
return `---\nname: ${name}\ndescription: ${JSON.stringify(description)}\n---\n\n# ${name}\n`
|
||||
}
|
||||
|
||||
describe("writeOpenClawBundle", () => {
|
||||
test("writes openclaw.plugin.json with a configSchema", async () => {
|
||||
const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-writer-"))
|
||||
@@ -40,4 +63,42 @@ describe("writeOpenClawBundle", () => {
|
||||
properties: {},
|
||||
})
|
||||
})
|
||||
|
||||
test("removes stale legacy OpenClaw agent skill directories before writing", async () => {
|
||||
const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-writer-cleanup-"))
|
||||
const staleDir = path.join(tempRoot, "skills", "agent-adversarial-reviewer")
|
||||
await fs.mkdir(staleDir, { recursive: true })
|
||||
await fs.writeFile(
|
||||
path.join(staleDir, "SKILL.md"),
|
||||
legacyAgentSkillContent(
|
||||
"adversarial-reviewer",
|
||||
await pluginDescription("plugins/compound-engineering/agents/review/ce-adversarial-reviewer.agent.md"),
|
||||
),
|
||||
)
|
||||
|
||||
const bundle: OpenClawBundle = {
|
||||
manifest: {
|
||||
id: "compound-engineering",
|
||||
name: "Compound Engineering",
|
||||
kind: "tool",
|
||||
configSchema: {
|
||||
type: "object",
|
||||
properties: {},
|
||||
},
|
||||
skills: [],
|
||||
},
|
||||
packageJson: {
|
||||
name: "openclaw-compound-engineering",
|
||||
version: "1.0.0",
|
||||
},
|
||||
entryPoint: "export default async function register() {}",
|
||||
skills: [],
|
||||
skillDirCopies: [],
|
||||
commands: [],
|
||||
}
|
||||
|
||||
await writeOpenClawBundle(tempRoot, bundle)
|
||||
|
||||
expect(await exists(staleDir)).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -7,8 +7,15 @@ const pluginRoot = path.join(process.cwd(), "plugins", "compound-engineering")
|
||||
|
||||
describe("sanitizePathName", () => {
|
||||
test("replaces colons with hyphens", () => {
|
||||
expect(sanitizePathName("ce:brainstorm")).toBe("ce-brainstorm")
|
||||
expect(sanitizePathName("ce:plan")).toBe("ce-plan")
|
||||
expect(sanitizePathName("other:skill")).toBe("other-skill")
|
||||
expect(sanitizePathName("other:tool")).toBe("other-tool")
|
||||
})
|
||||
|
||||
test("no CE skill name contains a colon", async () => {
|
||||
const plugin = await loadClaudePlugin(pluginRoot)
|
||||
for (const skill of plugin.skills) {
|
||||
expect(skill.name).not.toContain(":")
|
||||
}
|
||||
})
|
||||
|
||||
test("passes through names without colons", () => {
|
||||
|
||||
@@ -82,7 +82,7 @@ describe("convertClaudeToPi", () => {
|
||||
expect(parsedPrompt.body).toContain("ask_user_question")
|
||||
expect(parsedPrompt.body).toContain("/workflows-work")
|
||||
expect(parsedPrompt.body).toContain("/todo-resolve")
|
||||
expect(parsedPrompt.body).toContain("file-based todos (todos/ + /skill:todo-create)")
|
||||
expect(parsedPrompt.body).toContain("file-based todos (todos/ + /skill:ce-todo-create)")
|
||||
})
|
||||
|
||||
test("transforms namespaced Task agent calls using final segment", () => {
|
||||
|
||||
@@ -3,6 +3,7 @@ import { promises as fs } from "fs"
|
||||
import path from "path"
|
||||
import os from "os"
|
||||
import { writePiBundle } from "../src/targets/pi"
|
||||
import { parseFrontmatter } from "../src/utils/frontmatter"
|
||||
import type { PiBundle } from "../src/types/pi"
|
||||
|
||||
async function exists(filePath: string): Promise<boolean> {
|
||||
@@ -14,7 +15,45 @@ async function exists(filePath: string): Promise<boolean> {
|
||||
}
|
||||
}
|
||||
|
||||
async function pluginDescription(relativePath: string): Promise<string> {
|
||||
const raw = await fs.readFile(path.join(import.meta.dir, "..", relativePath), "utf8")
|
||||
const { data } = parseFrontmatter(raw, relativePath)
|
||||
if (typeof data.description !== "string") {
|
||||
throw new Error(`Missing description in ${relativePath}`)
|
||||
}
|
||||
return data.description
|
||||
}
|
||||
|
||||
describe("writePiBundle", () => {
|
||||
test("removes stale generated agent skills without touching prompt files", async () => {
|
||||
const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), "pi-cleanup-targets-"))
|
||||
const outputRoot = path.join(tempRoot, ".pi")
|
||||
|
||||
const sessionHistorianDescription = await pluginDescription(
|
||||
"plugins/compound-engineering/agents/research/ce-session-historian.agent.md",
|
||||
)
|
||||
|
||||
await fs.mkdir(path.join(outputRoot, "skills", "session-historian"), { recursive: true })
|
||||
await fs.writeFile(
|
||||
path.join(outputRoot, "skills", "session-historian", "SKILL.md"),
|
||||
`---\nname: session-historian\ndescription: ${JSON.stringify(sessionHistorianDescription)}\n---\n\nLegacy agent\n`,
|
||||
)
|
||||
await fs.mkdir(path.join(outputRoot, "prompts"), { recursive: true })
|
||||
await fs.writeFile(path.join(outputRoot, "prompts", "session-historian.md"), "user-owned prompt")
|
||||
|
||||
const bundle: PiBundle = {
|
||||
prompts: [],
|
||||
skillDirs: [],
|
||||
generatedSkills: [],
|
||||
extensions: [],
|
||||
}
|
||||
|
||||
await writePiBundle(outputRoot, bundle)
|
||||
|
||||
expect(await exists(path.join(outputRoot, "skills", "session-historian"))).toBe(false)
|
||||
expect(await exists(path.join(outputRoot, "prompts", "session-historian.md"))).toBe(true)
|
||||
})
|
||||
|
||||
test("writes prompts, skills, extensions, mcporter config, and AGENTS.md block", async () => {
|
||||
const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), "pi-writer-"))
|
||||
const outputRoot = path.join(tempRoot, ".pi")
|
||||
@@ -58,7 +97,7 @@ describe("writePiBundle", () => {
|
||||
await fs.writeFile(
|
||||
path.join(sourceSkillDir, "SKILL.md"),
|
||||
`---
|
||||
name: ce:plan
|
||||
name: ce-plan
|
||||
description: Planning workflow
|
||||
---
|
||||
|
||||
@@ -72,7 +111,7 @@ Run these research agents:
|
||||
|
||||
const bundle: PiBundle = {
|
||||
prompts: [],
|
||||
skillDirs: [{ name: "ce:plan", sourceDir: sourceSkillDir }],
|
||||
skillDirs: [{ name: "ce-plan", sourceDir: sourceSkillDir }],
|
||||
generatedSkills: [],
|
||||
extensions: [],
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ async function readRepoFile(relativePath: string): Promise<string> {
|
||||
return readFile(path.join(process.cwd(), relativePath), "utf8")
|
||||
}
|
||||
|
||||
describe("ce:work review contract", () => {
|
||||
describe("ce-work review contract", () => {
|
||||
test("requires code review before shipping", async () => {
|
||||
const content = await readRepoFile("plugins/compound-engineering/skills/ce-work/SKILL.md")
|
||||
// Review content extracted to references/shipping-workflow.md
|
||||
@@ -23,11 +23,11 @@ describe("ce:work review contract", () => {
|
||||
// Two-tier rubric in reference file
|
||||
expect(shipping).toContain("**Tier 1: Inline self-review**")
|
||||
expect(shipping).toContain("**Tier 2: Full review (default)**")
|
||||
expect(shipping).toContain("ce:review")
|
||||
expect(shipping).toContain("ce-code-review")
|
||||
expect(shipping).toContain("mode:autofix")
|
||||
|
||||
// Quality checklist includes review
|
||||
expect(shipping).toContain("Code review completed (inline self-review or full `ce:review`)")
|
||||
expect(shipping).toContain("Code review completed (inline self-review or full `ce-code-review`)")
|
||||
})
|
||||
|
||||
test("delegates commit and PR to dedicated skills", async () => {
|
||||
@@ -35,23 +35,23 @@ describe("ce:work review contract", () => {
|
||||
// Commit/PR delegation content extracted to references/shipping-workflow.md
|
||||
const shipping = await readRepoFile("plugins/compound-engineering/skills/ce-work/references/shipping-workflow.md")
|
||||
|
||||
expect(shipping).toContain("`git-commit-push-pr` skill")
|
||||
expect(shipping).toContain("`git-commit` skill")
|
||||
expect(shipping).toContain("`ce-commit-push-pr` skill")
|
||||
expect(shipping).toContain("`ce-commit` skill")
|
||||
|
||||
// Should not contain inline PR templates or attribution placeholders
|
||||
expect(content).not.toContain("gh pr create")
|
||||
expect(content).not.toContain("[HARNESS_URL]")
|
||||
})
|
||||
|
||||
test("ce:work-beta mirrors review and commit delegation", async () => {
|
||||
test("ce-work-beta mirrors review and commit delegation", async () => {
|
||||
const beta = await readRepoFile("plugins/compound-engineering/skills/ce-work-beta/SKILL.md")
|
||||
// Review/commit content extracted to references/shipping-workflow.md
|
||||
const shipping = await readRepoFile("plugins/compound-engineering/skills/ce-work-beta/references/shipping-workflow.md")
|
||||
|
||||
// Extracted content in reference file
|
||||
expect(shipping).toContain("2. **Code Review**")
|
||||
expect(shipping).toContain("`git-commit-push-pr` skill")
|
||||
expect(shipping).toContain("`git-commit` skill")
|
||||
expect(shipping).toContain("`ce-commit-push-pr` skill")
|
||||
expect(shipping).toContain("`ce-commit` skill")
|
||||
|
||||
// Negative assertions stay on SKILL.md
|
||||
expect(beta).not.toContain("Consider Code Review")
|
||||
@@ -86,7 +86,7 @@ describe("ce:work review contract", () => {
|
||||
expect(shipping).not.toContain("Tests pass (run project's test command)")
|
||||
})
|
||||
|
||||
test("ce:work-beta mirrors testing deliberation and checklist changes", async () => {
|
||||
test("ce-work-beta mirrors testing deliberation and checklist changes", async () => {
|
||||
const beta = await readRepoFile("plugins/compound-engineering/skills/ce-work-beta/SKILL.md")
|
||||
// Checklist extracted to references/shipping-workflow.md
|
||||
const shipping = await readRepoFile("plugins/compound-engineering/skills/ce-work-beta/references/shipping-workflow.md")
|
||||
@@ -162,8 +162,8 @@ describe("ce:work-beta codex delegation contract", () => {
|
||||
const content = await readRepoFile("plugins/compound-engineering/skills/ce-work-beta/SKILL.md")
|
||||
|
||||
expect(content).toContain("disable-model-invocation: true")
|
||||
expect(content).toContain("Invoke `ce:work-beta` manually")
|
||||
expect(content).toContain("planning and workflow handoffs remain pointed at stable `ce:work`")
|
||||
expect(content).toContain("Invoke `ce-work-beta` manually")
|
||||
expect(content).toContain("planning and workflow handoffs remain pointed at stable `ce-work`")
|
||||
})
|
||||
|
||||
test("SKILL.md has delegation routing stub pointing to reference", async () => {
|
||||
@@ -253,7 +253,7 @@ describe("ce:work-beta codex delegation contract", () => {
|
||||
const content = await readRepoFile("plugins/compound-engineering/skills/ce-work-beta/SKILL.md")
|
||||
|
||||
expect(content).toContain("**Frontend Design Guidance**")
|
||||
expect(content).toContain("`frontend-design` skill")
|
||||
expect(content).toContain("`ce-frontend-design` skill")
|
||||
})
|
||||
})
|
||||
|
||||
@@ -272,28 +272,28 @@ describe("ce:plan remains neutral during ce:work-beta rollout", () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe("ce:brainstorm review contract", () => {
|
||||
describe("ce-brainstorm review contract", () => {
|
||||
test("requires document review before handoff", async () => {
|
||||
const content = await readRepoFile("plugins/compound-engineering/skills/ce-brainstorm/SKILL.md")
|
||||
|
||||
// Phase 3.5 exists and runs document-review
|
||||
expect(content).toContain("### Phase 3.5: Document Review")
|
||||
expect(content).toContain("`document-review` skill")
|
||||
expect(content).toContain("`ce-doc-review` skill")
|
||||
|
||||
// Phase 3 and Phase 4 are extracted to references for token optimization
|
||||
expect(content).toContain("`references/requirements-capture.md`")
|
||||
expect(content).toContain("`references/handoff.md`")
|
||||
|
||||
// Additional review passes are surfaced contextually (not as a menu fixture) and still
|
||||
// route through the document-review skill when requested
|
||||
// route through the ce-doc-review skill when requested
|
||||
const handoff = await readRepoFile("plugins/compound-engineering/skills/ce-brainstorm/references/handoff.md")
|
||||
expect(handoff).toContain("Surface additional document review contextually")
|
||||
expect(handoff).toContain("Load the `document-review` skill")
|
||||
expect(handoff).toContain("Load the `ce-doc-review` skill")
|
||||
expect(handoff).not.toContain("**Review and refine**")
|
||||
})
|
||||
})
|
||||
|
||||
describe("ce:plan testing contract", () => {
|
||||
describe("ce-plan testing contract", () => {
|
||||
test("flags blank test scenarios on feature-bearing units as incomplete", async () => {
|
||||
const content = await readRepoFile("plugins/compound-engineering/skills/ce-plan/SKILL.md")
|
||||
|
||||
@@ -306,14 +306,14 @@ describe("ce:plan testing contract", () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe("ce:plan review contract", () => {
|
||||
describe("ce-plan review contract", () => {
|
||||
test("requires document review after confidence check", async () => {
|
||||
// Document review instructions extracted to references/plan-handoff.md
|
||||
const content = await readRepoFile("plugins/compound-engineering/skills/ce-plan/references/plan-handoff.md")
|
||||
|
||||
// Phase 5.3.8 runs document-review before final checks (5.3.9)
|
||||
expect(content).toContain("## 5.3.8 Document Review")
|
||||
expect(content).toContain("`document-review` skill")
|
||||
expect(content).toContain("`ce-doc-review` skill")
|
||||
|
||||
// Document review must come before final checks so auto-applied edits are validated
|
||||
const docReviewIdx = content.indexOf("5.3.8 Document Review")
|
||||
@@ -333,23 +333,23 @@ describe("ce:plan review contract", () => {
|
||||
const content = await readRepoFile("plugins/compound-engineering/skills/ce-plan/references/plan-handoff.md")
|
||||
|
||||
// Pipeline mode runs document-review headlessly, not skipping it
|
||||
expect(content).toContain("document-review` with `mode:headless`")
|
||||
expect(content).toContain("ce-doc-review` with `mode:headless`")
|
||||
expect(content).not.toContain("skip document-review and return control")
|
||||
})
|
||||
|
||||
test("handoff options recommend ce:work after review", async () => {
|
||||
test("handoff options recommend ce-work after review", async () => {
|
||||
const content = await readRepoFile("plugins/compound-engineering/skills/ce-plan/references/plan-handoff.md")
|
||||
|
||||
// ce:work is recommended (review already happened)
|
||||
expect(content).toContain("**Start `/ce:work`** (recommended) - Begin implementing this plan in the current session")
|
||||
// ce-work is recommended (review already happened)
|
||||
expect(content).toContain("**Start `/ce-work`** (recommended) - Begin implementing this plan in the current session")
|
||||
|
||||
// Additional review passes are surfaced contextually (not as a menu fixture) and still
|
||||
// route through the document-review skill when requested
|
||||
// route through the ce-doc-review skill when requested
|
||||
expect(content).toContain("Surface additional document review contextually")
|
||||
expect(content).toContain("Load the `document-review` skill")
|
||||
expect(content).toContain("Load the `ce-doc-review` skill")
|
||||
|
||||
// No conditional ordering based on plan depth (review already ran)
|
||||
expect(content).not.toContain("**Options when document-review is recommended:**")
|
||||
expect(content).not.toContain("**Options when ce-doc-review is recommended:**")
|
||||
expect(content).not.toContain("**Options for Standard or Lightweight plans:**")
|
||||
})
|
||||
})
|
||||
|
||||
@@ -21,7 +21,29 @@ function makeBundle(mcpServers?: Record<string, { command: string }>): QwenBundl
|
||||
}
|
||||
}
|
||||
|
||||
const LEGACY_LINT_DESCRIPTION = "Use this agent when you need to run linting and code quality checks on Ruby and ERB files. Run before pushing to origin."
|
||||
|
||||
describe("writeQwenBundle", () => {
|
||||
test("cleans legacy agents before writing new agent files", async () => {
|
||||
const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), "qwen-agent-cleanup-order-"))
|
||||
|
||||
const bundle: QwenBundle = {
|
||||
...makeBundle(),
|
||||
agents: [
|
||||
{
|
||||
name: "lint",
|
||||
format: "markdown",
|
||||
content: `---\nname: lint\ndescription: ${JSON.stringify(LEGACY_LINT_DESCRIPTION)}\n---\n\nReplacement agent\n`,
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
await writeQwenBundle(tempRoot, bundle)
|
||||
|
||||
const lintPath = path.join(tempRoot, "agents", "lint.md")
|
||||
expect(await fs.readFile(lintPath, "utf8")).toContain("Replacement agent")
|
||||
})
|
||||
|
||||
test("removes stale plugin MCP servers on re-install", async () => {
|
||||
const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), "qwen-converge-"))
|
||||
|
||||
|
||||
@@ -61,7 +61,7 @@ describe("release intent parsing", () => {
|
||||
})
|
||||
|
||||
test("supports conventional titles without scope", () => {
|
||||
const parsed = parseReleaseIntent("fix: adjust ce:plan-beta wording")
|
||||
const parsed = parseReleaseIntent("fix: adjust ce-plan wording")
|
||||
expect(parsed.type).toBe("fix")
|
||||
expect(parsed.scope).toBeNull()
|
||||
expect(parsed.breaking).toBe(false)
|
||||
|
||||
@@ -47,7 +47,7 @@ async function makeFixtureRoot(): Promise<string> {
|
||||
)
|
||||
await writeFile(
|
||||
path.join(root, "plugins", "compound-engineering", "skills", "ce-plan", "SKILL.md"),
|
||||
"# ce:plan\n",
|
||||
"# ce-plan\n",
|
||||
)
|
||||
await writeFile(
|
||||
path.join(root, "plugins", "compound-engineering", ".mcp.json"),
|
||||
|
||||
@@ -5,8 +5,8 @@ describe("release preview", () => {
|
||||
test("uses changed files to determine affected components and next versions", async () => {
|
||||
const versions = await loadCurrentVersions()
|
||||
const preview = await buildReleasePreview({
|
||||
title: "fix: adjust ce:plan-beta wording",
|
||||
files: ["plugins/compound-engineering/skills/ce-plan-beta/SKILL.md"],
|
||||
title: "fix: adjust ce-plan wording",
|
||||
files: ["plugins/compound-engineering/skills/ce-plan/SKILL.md"],
|
||||
})
|
||||
|
||||
expect(preview.components).toHaveLength(1)
|
||||
|
||||
@@ -18,7 +18,7 @@ const resolveBaseScript = path.join(
|
||||
"plugins",
|
||||
"compound-engineering",
|
||||
"skills",
|
||||
"ce-review",
|
||||
"ce-code-review",
|
||||
"references",
|
||||
"resolve-base.sh",
|
||||
)
|
||||
|
||||
@@ -7,15 +7,15 @@ async function readRepoFile(relativePath: string): Promise<string> {
|
||||
return readFile(path.join(process.cwd(), relativePath), "utf8")
|
||||
}
|
||||
|
||||
describe("ce-review contract", () => {
|
||||
describe("ce-code-review contract", () => {
|
||||
test("documents explicit modes and orchestration boundaries", async () => {
|
||||
const content = await readRepoFile("plugins/compound-engineering/skills/ce-review/SKILL.md")
|
||||
const content = await readRepoFile("plugins/compound-engineering/skills/ce-code-review/SKILL.md")
|
||||
|
||||
expect(content).toContain("## Mode Detection")
|
||||
expect(content).toContain("mode:autofix")
|
||||
expect(content).toContain("mode:report-only")
|
||||
expect(content).toContain("mode:headless")
|
||||
expect(content).toContain(".context/compound-engineering/ce-review/<run-id>/")
|
||||
expect(content).toContain(".context/compound-engineering/ce-code-review/<run-id>/")
|
||||
expect(content).toContain("Do not create residual todos or `.context` artifacts.")
|
||||
expect(content).toContain(
|
||||
"Do not start a mutating review round concurrently with browser testing on the same checkout.",
|
||||
@@ -27,7 +27,7 @@ describe("ce-review contract", () => {
|
||||
})
|
||||
|
||||
test("documents headless mode contract for programmatic callers", async () => {
|
||||
const content = await readRepoFile("plugins/compound-engineering/skills/ce-review/SKILL.md")
|
||||
const content = await readRepoFile("plugins/compound-engineering/skills/ce-code-review/SKILL.md")
|
||||
|
||||
// Headless mode has its own rules section
|
||||
expect(content).toContain("### Headless mode rules")
|
||||
@@ -70,7 +70,7 @@ describe("ce-review contract", () => {
|
||||
})
|
||||
|
||||
test("documents policy-driven routing and residual handoff", async () => {
|
||||
const content = await readRepoFile("plugins/compound-engineering/skills/ce-review/SKILL.md")
|
||||
const content = await readRepoFile("plugins/compound-engineering/skills/ce-code-review/SKILL.md")
|
||||
|
||||
// Routing taxonomy and fixer queue semantics
|
||||
expect(content).toContain("## Action Routing")
|
||||
@@ -108,7 +108,7 @@ describe("ce-review contract", () => {
|
||||
|
||||
// Tracker fallback chain explicitly forbids extending the internal todos system.
|
||||
const trackerDefer = await readRepoFile(
|
||||
"plugins/compound-engineering/skills/ce-review/references/tracker-defer.md",
|
||||
"plugins/compound-engineering/skills/ce-code-review/references/tracker-defer.md",
|
||||
)
|
||||
expect(trackerDefer).toContain(".context/compound-engineering/todos/")
|
||||
expect(trackerDefer).toMatch(/Never fall back to `\.context\/compound-engineering\/todos\//)
|
||||
@@ -117,7 +117,7 @@ describe("ce-review contract", () => {
|
||||
// rejected synthesis-time rewrite pass. Assert presence of the observable-behavior
|
||||
// rule and the required-field reminder without pinning exact prose.
|
||||
const subagentTemplate = await readRepoFile(
|
||||
"plugins/compound-engineering/skills/ce-review/references/subagent-template.md",
|
||||
"plugins/compound-engineering/skills/ce-code-review/references/subagent-template.md",
|
||||
)
|
||||
expect(subagentTemplate).toMatch(/observable behavior/i)
|
||||
expect(subagentTemplate).toMatch(/required/i)
|
||||
@@ -127,7 +127,7 @@ describe("ce-review contract", () => {
|
||||
// breaks the test. Exact label wording may be refined for clarity — these assertions
|
||||
// check the structural contract, not the prose.
|
||||
const walkthrough = await readRepoFile(
|
||||
"plugins/compound-engineering/skills/ce-review/references/walkthrough.md",
|
||||
"plugins/compound-engineering/skills/ce-code-review/references/walkthrough.md",
|
||||
)
|
||||
expect(walkthrough).toContain("Apply the proposed fix")
|
||||
expect(walkthrough).toContain("Defer — file a [TRACKER] ticket")
|
||||
@@ -136,7 +136,7 @@ describe("ce-review contract", () => {
|
||||
|
||||
// bulk-preview.md contract: exactly Proceed / Cancel, no third option.
|
||||
const bulkPreview = await readRepoFile(
|
||||
"plugins/compound-engineering/skills/ce-review/references/bulk-preview.md",
|
||||
"plugins/compound-engineering/skills/ce-code-review/references/bulk-preview.md",
|
||||
)
|
||||
expect(bulkPreview).toContain("Proceed")
|
||||
expect(bulkPreview).toContain("Cancel")
|
||||
@@ -153,7 +153,7 @@ describe("ce-review contract", () => {
|
||||
|
||||
test("keeps findings schema and downstream docs aligned", async () => {
|
||||
const rawSchema = await readRepoFile(
|
||||
"plugins/compound-engineering/skills/ce-review/references/findings-schema.json",
|
||||
"plugins/compound-engineering/skills/ce-code-review/references/findings-schema.json",
|
||||
)
|
||||
const schema = JSON.parse(rawSchema) as {
|
||||
_meta: { confidence_thresholds: { suppress: string } }
|
||||
@@ -189,27 +189,27 @@ describe("ce-review contract", () => {
|
||||
expect(schema.properties.findings.items.properties.requires_verification.type).toBe("boolean")
|
||||
expect(schema._meta.confidence_thresholds.suppress).toContain("0.60")
|
||||
|
||||
const fileTodos = await readRepoFile("plugins/compound-engineering/skills/todo-create/SKILL.md")
|
||||
expect(fileTodos).toContain("/ce:review mode:autofix")
|
||||
expect(fileTodos).toContain("/todo-resolve")
|
||||
const fileTodos = await readRepoFile("plugins/compound-engineering/skills/ce-todo-create/SKILL.md")
|
||||
expect(fileTodos).toContain("/ce-code-review mode:autofix")
|
||||
expect(fileTodos).toContain("/ce-todo-resolve")
|
||||
|
||||
const resolveTodos = await readRepoFile("plugins/compound-engineering/skills/todo-resolve/SKILL.md")
|
||||
expect(resolveTodos).toContain("ce:review mode:autofix")
|
||||
const resolveTodos = await readRepoFile("plugins/compound-engineering/skills/ce-todo-resolve/SKILL.md")
|
||||
expect(resolveTodos).toContain("ce-code-review mode:autofix")
|
||||
expect(resolveTodos).toContain("safe_auto")
|
||||
})
|
||||
|
||||
test("documents stack-specific conditional reviewers for the JSON pipeline", async () => {
|
||||
const content = await readRepoFile("plugins/compound-engineering/skills/ce-review/SKILL.md")
|
||||
const content = await readRepoFile("plugins/compound-engineering/skills/ce-code-review/SKILL.md")
|
||||
const catalog = await readRepoFile(
|
||||
"plugins/compound-engineering/skills/ce-review/references/persona-catalog.md",
|
||||
"plugins/compound-engineering/skills/ce-code-review/references/persona-catalog.md",
|
||||
)
|
||||
|
||||
for (const agent of [
|
||||
"compound-engineering:review:dhh-rails-reviewer",
|
||||
"compound-engineering:review:kieran-rails-reviewer",
|
||||
"compound-engineering:review:kieran-python-reviewer",
|
||||
"compound-engineering:review:kieran-typescript-reviewer",
|
||||
"compound-engineering:review:julik-frontend-races-reviewer",
|
||||
"review:ce-dhh-rails-reviewer",
|
||||
"review:ce-kieran-rails-reviewer",
|
||||
"review:ce-kieran-python-reviewer",
|
||||
"review:ce-kieran-typescript-reviewer",
|
||||
"review:ce-julik-frontend-races-reviewer",
|
||||
]) {
|
||||
expect(content).toContain(agent)
|
||||
expect(catalog).toContain(agent)
|
||||
@@ -222,23 +222,23 @@ describe("ce-review contract", () => {
|
||||
test("stack-specific reviewer agents follow the structured findings contract", async () => {
|
||||
const reviewers = [
|
||||
{
|
||||
path: "plugins/compound-engineering/agents/review/dhh-rails-reviewer.md",
|
||||
path: "plugins/compound-engineering/agents/review/ce-dhh-rails-reviewer.agent.md",
|
||||
reviewer: "dhh-rails",
|
||||
},
|
||||
{
|
||||
path: "plugins/compound-engineering/agents/review/kieran-rails-reviewer.md",
|
||||
path: "plugins/compound-engineering/agents/review/ce-kieran-rails-reviewer.agent.md",
|
||||
reviewer: "kieran-rails",
|
||||
},
|
||||
{
|
||||
path: "plugins/compound-engineering/agents/review/kieran-python-reviewer.md",
|
||||
path: "plugins/compound-engineering/agents/review/ce-kieran-python-reviewer.agent.md",
|
||||
reviewer: "kieran-python",
|
||||
},
|
||||
{
|
||||
path: "plugins/compound-engineering/agents/review/kieran-typescript-reviewer.md",
|
||||
path: "plugins/compound-engineering/agents/review/ce-kieran-typescript-reviewer.agent.md",
|
||||
reviewer: "kieran-typescript",
|
||||
},
|
||||
{
|
||||
path: "plugins/compound-engineering/agents/review/julik-frontend-races-reviewer.md",
|
||||
path: "plugins/compound-engineering/agents/review/ce-julik-frontend-races-reviewer.agent.md",
|
||||
reviewer: "julik-frontend-races",
|
||||
},
|
||||
]
|
||||
@@ -262,7 +262,7 @@ describe("ce-review contract", () => {
|
||||
|
||||
test("leaves data-migration-expert as the unstructured review format", async () => {
|
||||
const content = await readRepoFile(
|
||||
"plugins/compound-engineering/agents/review/data-migration-expert.md",
|
||||
"plugins/compound-engineering/agents/review/ce-data-migration-expert.agent.md",
|
||||
)
|
||||
|
||||
expect(content).toContain("## Reviewer Checklist")
|
||||
@@ -271,7 +271,7 @@ describe("ce-review contract", () => {
|
||||
})
|
||||
|
||||
test("fails closed when merge-base is unresolved instead of falling back to git diff HEAD", async () => {
|
||||
const content = await readRepoFile("plugins/compound-engineering/skills/ce-review/SKILL.md")
|
||||
const content = await readRepoFile("plugins/compound-engineering/skills/ce-code-review/SKILL.md")
|
||||
|
||||
// No scope path should fall back to `git diff HEAD` or `git diff --cached` — those only
|
||||
// show uncommitted changes and silently produce empty diffs on clean feature branches.
|
||||
@@ -286,7 +286,7 @@ describe("ce-review contract", () => {
|
||||
// The script itself emits ERROR: when the base is unresolved.
|
||||
expect(content).toContain("references/resolve-base.sh")
|
||||
const resolveScript = await readRepoFile(
|
||||
"plugins/compound-engineering/skills/ce-review/references/resolve-base.sh",
|
||||
"plugins/compound-engineering/skills/ce-code-review/references/resolve-base.sh",
|
||||
)
|
||||
expect(resolveScript).toContain("ERROR:")
|
||||
|
||||
@@ -298,14 +298,13 @@ describe("ce-review contract", () => {
|
||||
|
||||
test("orchestration callers pass explicit mode flags", async () => {
|
||||
const lfg = await readRepoFile("plugins/compound-engineering/skills/lfg/SKILL.md")
|
||||
expect(lfg).toContain("/ce:review mode:autofix")
|
||||
|
||||
expect(lfg).toContain("/ce-code-review mode:autofix")
|
||||
})
|
||||
})
|
||||
|
||||
describe("testing-reviewer contract", () => {
|
||||
test("includes behavioral-changes-with-no-test-additions check", async () => {
|
||||
const content = await readRepoFile("plugins/compound-engineering/agents/review/testing-reviewer.md")
|
||||
const content = await readRepoFile("plugins/compound-engineering/agents/review/ce-testing-reviewer.agent.md")
|
||||
|
||||
// New check exists in "What you're hunting for" section
|
||||
expect(content).toContain("Behavioral changes with no test additions")
|
||||
|
||||
@@ -601,7 +601,7 @@ describe("convertClaudeToWindsurf dedupe", () => {
|
||||
manifest: { name: "fixture", version: "1.0.0" },
|
||||
agents: [
|
||||
{
|
||||
name: "ce:plan",
|
||||
name: "ce-plan",
|
||||
description: "Planning agent",
|
||||
body: "Plan things.",
|
||||
sourcePath: "/tmp/plugin/agents/ce-plan.md",
|
||||
@@ -610,7 +610,7 @@ describe("convertClaudeToWindsurf dedupe", () => {
|
||||
commands: [],
|
||||
skills: [
|
||||
{
|
||||
name: "ce:plan",
|
||||
name: "ce-plan",
|
||||
description: "Planning skill",
|
||||
sourceDir: "/tmp/plugin/skills/ce-plan",
|
||||
skillPath: "/tmp/plugin/skills/ce-plan/SKILL.md",
|
||||
@@ -626,7 +626,7 @@ describe("convertClaudeToWindsurf dedupe", () => {
|
||||
permissions: "none" as const,
|
||||
})
|
||||
|
||||
// The agent skill should get a deduplicated name since "ce:plan" normalizes
|
||||
// The agent skill should get a deduplicated name since "ce-plan" normalizes
|
||||
// to "ce-plan" which collides with the pass-through skill on disk
|
||||
expect(bundle.agentSkills[0].name).not.toBe("ce-plan")
|
||||
})
|
||||
|
||||
@@ -92,7 +92,7 @@ describe("writeWindsurfBundle", () => {
|
||||
await fs.writeFile(
|
||||
path.join(sourceSkillDir, "SKILL.md"),
|
||||
`---
|
||||
name: ce:plan
|
||||
name: ce-plan
|
||||
description: Planning workflow
|
||||
---
|
||||
|
||||
@@ -106,7 +106,7 @@ Run these research agents:
|
||||
|
||||
const bundle: WindsurfBundle = {
|
||||
...emptyBundle,
|
||||
skillDirs: [{ name: "ce:plan", sourceDir: sourceSkillDir }],
|
||||
skillDirs: [{ name: "ce-plan", sourceDir: sourceSkillDir }],
|
||||
}
|
||||
|
||||
await writeWindsurfBundle(tempRoot, bundle)
|
||||
|
||||
Reference in New Issue
Block a user