refactor(agents): flatten agents directory (#621)
Some checks failed
CI / pr-title (push) Has been cancelled
CI / test (push) Has been cancelled
Release PR / release-pr (push) Has been cancelled
Release PR / publish-cli (push) Has been cancelled

This commit is contained in:
Trevin Chow
2026-04-21 02:35:21 -07:00
committed by GitHub
parent d359cc7e2f
commit 4c57508c1a
74 changed files with 355 additions and 165 deletions

View File

@@ -326,14 +326,59 @@ Task compound-engineering:review:ce-security-reviewer(code_diff)`,
name: "ce-repo-research-analyst",
description: "Repo research",
body: "Research repositories.",
sourcePath: "/tmp/plugin/agents/research/ce-repo-research-analyst.agent.md",
sourcePath: "/tmp/plugin/agents/ce-repo-research-analyst.agent.md",
},
{
name: "ce-learnings-researcher",
description: "Learning research",
body: "Search learnings.",
sourcePath: "/tmp/plugin/agents/research/ce-learnings-researcher.agent.md",
sourcePath: "/tmp/plugin/agents/ce-learnings-researcher.agent.md",
},
{
name: "ce-security-reviewer",
description: "Security review",
body: "Review security.",
sourcePath: "/tmp/plugin/agents/ce-security-reviewer.agent.md",
},
],
skills: [],
}
const bundle = convertClaudeToCodex(plugin, {
agentMode: "subagent",
inferTemperature: false,
permissions: "none",
codexIncludeSkills: true,
})
const commandSkill = bundle.generatedSkills.find((s) => s.name === "plan")
expect(commandSkill).toBeDefined()
const parsed = parseFrontmatter(commandSkill!.content)
expect(parsed.body).toContain("Spawn the custom agent `ce-repo-research-analyst` with task: feature_description")
expect(parsed.body).toContain("Spawn the custom agent `ce-learnings-researcher` with task: feature_description")
expect(parsed.body).toContain("Spawn the custom agent `ce-security-reviewer` with task: code_diff")
// Original namespaced Task syntax should not remain
expect(parsed.body).not.toContain("Task compound-engineering:")
})
test("retains <category>-<agent> naming for nested-layout plugins (dead-code fallback)", () => {
// This test pins the behavior of getAgentCategory() for any third-party
// plugin that still uses agents/<category>/<name>.md layout. The
// compound-engineering plugin itself is flat, but the converter must keep
// working for other plugins passed through the CLI.
const plugin: ClaudePlugin = {
...fixturePlugin,
commands: [
{
name: "plan",
description: "Planning with agents from a nested-layout plugin",
body: `- Task compound-engineering:review:ce-security-reviewer(code_diff)`,
sourcePath: "/tmp/plugin/commands/plan.md",
},
],
agents: [
{
name: "ce-security-reviewer",
description: "Security review",
@@ -355,12 +400,7 @@ Task compound-engineering:review:ce-security-reviewer(code_diff)`,
expect(commandSkill).toBeDefined()
const parsed = parseFrontmatter(commandSkill!.content)
expect(parsed.body).toContain("Spawn the custom agent `research-ce-repo-research-analyst` with task: feature_description")
expect(parsed.body).toContain("Spawn the custom agent `research-ce-learnings-researcher` with task: feature_description")
expect(parsed.body).toContain("Spawn the custom agent `review-ce-security-reviewer` with task: code_diff")
// Original namespaced Task syntax should not remain
expect(parsed.body).not.toContain("Task compound-engineering:")
})
test("transforms zero-argument Task calls", () => {
@@ -379,7 +419,7 @@ Task compound-engineering:review:ce-security-reviewer(code_diff)`,
name: "ce-code-simplicity-reviewer",
description: "Simplicity review",
body: "Review simplicity.",
sourcePath: "/tmp/plugin/agents/review/ce-code-simplicity-reviewer.agent.md",
sourcePath: "/tmp/plugin/agents/ce-code-simplicity-reviewer.agent.md",
},
],
skills: [],
@@ -395,7 +435,7 @@ Task compound-engineering:review:ce-security-reviewer(code_diff)`,
const commandSkill = bundle.generatedSkills.find((s) => s.name === "review")
expect(commandSkill).toBeDefined()
const parsed = parseFrontmatter(commandSkill!.content)
expect(parsed.body).toContain("Spawn the custom agent `review-ce-code-simplicity-reviewer`")
expect(parsed.body).toContain("Spawn the custom agent `ce-code-simplicity-reviewer`")
expect(parsed.body).not.toContain("compound-engineering:")
expect(parsed.body).not.toContain("skill to:")
})

View File

@@ -395,6 +395,60 @@ describe("writeCodexBundle", () => {
}
})
test("sweeps flat-alias skill dir left by a prior layout when the new bundle's agent name has embedded -ce-", async () => {
// Third-party plugins with nested agent directories (e.g. agents/review/ce-foo.md)
// produce Codex agent names like `review-ce-foo`. If the same logical agent
// was previously installed under a flat layout (raw codex name `ce-foo`),
// the now-orphaned skill dir at `.codex/skills/<plugin>/ce-foo/` should be
// moved into legacy-backup on the next install. This is the only cleanup
// path available for third-party plugins, which have no entry in the
// historical allow-list used by getLegacyCodexArtifacts.
const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), "codex-nested-xmigrate-"))
const codexRoot = path.join(tempRoot, ".codex")
const pluginName = "third-party-nested"
const managedSkillsRoot = path.join(codexRoot, "skills", pluginName)
// Simulate orphan flat-alias skill dir from the earlier layout.
await fs.mkdir(path.join(managedSkillsRoot, "ce-foo"), { recursive: true })
await fs.writeFile(
path.join(managedSkillsRoot, "ce-foo", "SKILL.md"),
"stale flat-alias skill from prior install",
)
await writeCodexBundle(codexRoot, {
pluginName,
prompts: [],
skillDirs: [],
generatedSkills: [],
agents: [
{
name: "review-ce-foo",
description: "Nested-layout agent",
instructions: "Do review work on foo.",
},
],
})
// The current install writes the nested-layout agent, not a same-named skill dir.
expect(await exists(path.join(codexRoot, "agents", pluginName, "review-ce-foo.toml"))).toBe(true)
// The orphan flat-alias skill dir should have been relocated.
expect(await exists(path.join(managedSkillsRoot, "ce-foo"))).toBe(false)
// And should be reachable under legacy-backup.
const backupRoot = path.join(codexRoot, pluginName, "legacy-backup")
expect(await exists(backupRoot)).toBe(true)
const timestamps = await fs.readdir(backupRoot)
let foundBackup = false
for (const ts of timestamps) {
const skillsBackup = path.join(backupRoot, ts, "skills")
if (!(await exists(skillsBackup))) continue
const backed = await fs.readdir(skillsBackup)
if (backed.includes("ce-foo")) foundBackup = true
}
expect(foundBackup).toBe(true)
})
test("agents-only install preserves namespaced skills previously installed via Codex native plugin flow", async () => {
// Regression for the bug where re-running `install --to codex` after a
// native `/plugins` install moved currently-active namespaced skills
@@ -765,6 +819,87 @@ Workflow handoff:
expect(installedSkill).not.toContain("/prompts:settings")
expect(installedSkill).not.toContain("https://prompts:www.proofeditor.ai")
})
test("removes orphan sidecar dir when retained agent declares no sidecars", async () => {
const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), "codex-test-"))
const agentsRoot = path.join(tempRoot, ".codex", "agents")
const orphanDir = path.join(agentsRoot, "ce-foo", "stale-content")
await fs.mkdir(orphanDir, { recursive: true })
await fs.writeFile(path.join(orphanDir, "leftover.txt"), "stale", "utf8")
await fs.writeFile(path.join(agentsRoot, "ce-foo.toml"), "old-toml", "utf8")
const bundle: CodexBundle = {
prompts: [],
skillDirs: [],
generatedSkills: [],
agents: [
{
name: "ce-foo",
description: "Foo agent",
instructions: "Do foo.",
},
],
mcpServers: {},
}
await writeCodexBundle(tempRoot, bundle)
expect(await entryExists(path.join(agentsRoot, "ce-foo"))).toBe(false)
expect(await exists(path.join(agentsRoot, "ce-foo.toml"))).toBe(true)
})
test("keeps sidecar dir when retained agent declares sidecars", async () => {
const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), "codex-test-"))
const sidecarSource = await fs.mkdtemp(path.join(os.tmpdir(), "codex-sidecar-src-"))
await fs.writeFile(path.join(sidecarSource, "script.sh"), "#!/bin/sh\necho hi\n", "utf8")
const bundle: CodexBundle = {
prompts: [],
skillDirs: [],
generatedSkills: [],
agents: [
{
name: "ce-foo",
description: "Foo agent",
instructions: "Do foo.",
sidecarDirs: [{ sourceDir: sidecarSource, targetName: "scripts" }],
},
],
mcpServers: {},
}
await writeCodexBundle(tempRoot, bundle)
const agentsRoot = path.join(tempRoot, ".codex", "agents")
expect(await exists(path.join(agentsRoot, "ce-foo.toml"))).toBe(true)
expect(await exists(path.join(agentsRoot, "ce-foo", "scripts", "script.sh"))).toBe(true)
})
test("leaves unrelated directories under agentsRoot alone", async () => {
const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), "codex-test-"))
const agentsRoot = path.join(tempRoot, ".codex", "agents")
const unrelatedDir = path.join(agentsRoot, "ce-bar-extra")
await fs.mkdir(unrelatedDir, { recursive: true })
await fs.writeFile(path.join(unrelatedDir, "keep-me.txt"), "keep", "utf8")
const bundle: CodexBundle = {
prompts: [],
skillDirs: [],
generatedSkills: [],
agents: [
{
name: "ce-foo",
description: "Foo agent",
instructions: "Do foo.",
},
],
mcpServers: {},
}
await writeCodexBundle(tempRoot, bundle)
expect(await exists(path.join(unrelatedDir, "keep-me.txt"))).toBe(true)
})
})
describe("renderCodexConfig", () => {

View File

@@ -38,7 +38,7 @@ describe("writeKiroBundle", () => {
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",
"plugins/compound-engineering/agents/ce-session-historian.agent.md",
)
await fs.writeFile(

View File

@@ -257,14 +257,14 @@ describe("cleanupStaleAgents", () => {
path.join(root, "adversarial-reviewer.md"),
agentContent(
"adversarial-reviewer",
await pluginDescription("plugins/compound-engineering/agents/review/ce-adversarial-reviewer.agent.md"),
await pluginDescription("plugins/compound-engineering/agents/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"),
await pluginDescription("plugins/compound-engineering/agents/ce-learnings-researcher.agent.md"),
),
)
@@ -281,14 +281,14 @@ describe("cleanupStaleAgents", () => {
path.join(root, "security-sentinel.agent.md"),
agentContent(
"security-sentinel",
await pluginDescription("plugins/compound-engineering/agents/review/ce-security-sentinel.agent.md"),
await pluginDescription("plugins/compound-engineering/agents/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"),
await pluginDescription("plugins/compound-engineering/agents/ce-performance-oracle.agent.md"),
),
)
@@ -304,14 +304,14 @@ describe("cleanupStaleAgents", () => {
path.join(root, "slack-researcher.json"),
kiroAgentConfigContent(
"slack-researcher",
await pluginDescription("plugins/compound-engineering/agents/research/ce-slack-researcher.agent.md"),
await pluginDescription("plugins/compound-engineering/agents/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 pluginDescription("plugins/compound-engineering/agents/ce-session-historian.agent.md"),
),
)
await createFile(
@@ -336,14 +336,14 @@ describe("cleanupStaleAgents", () => {
path.join(root, "code-simplicity-reviewer"),
skillContent(
"code-simplicity-reviewer",
await pluginDescription("plugins/compound-engineering/agents/review/ce-code-simplicity-reviewer.agent.md"),
await pluginDescription("plugins/compound-engineering/agents/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"),
await pluginDescription("plugins/compound-engineering/agents/ce-repo-research-analyst.agent.md"),
),
)
@@ -618,7 +618,7 @@ describe("idempotency", () => {
path.join(root, "adversarial-reviewer.md"),
agentContent(
"adversarial-reviewer",
await pluginDescription("plugins/compound-engineering/agents/review/ce-adversarial-reviewer.agent.md"),
await pluginDescription("plugins/compound-engineering/agents/ce-adversarial-reviewer.agent.md"),
),
)

View File

@@ -32,7 +32,7 @@ describe("writePiBundle", () => {
const outputRoot = path.join(tempRoot, ".pi")
const sessionHistorianDescription = await pluginDescription(
"plugins/compound-engineering/agents/research/ce-session-historian.agent.md",
"plugins/compound-engineering/agents/ce-session-historian.agent.md",
)
await fs.mkdir(path.join(outputRoot, "skills", "session-historian"), { recursive: true })

View File

@@ -597,7 +597,7 @@ describe("ce-compound frontmatter schema expansion contract", () => {
describe("ce-learnings-researcher domain-agnostic contract", () => {
test("agent prompt frames as domain-agnostic not bug-focused", async () => {
const agent = await readRepoFile(
"plugins/compound-engineering/agents/research/ce-learnings-researcher.agent.md"
"plugins/compound-engineering/agents/ce-learnings-researcher.agent.md"
)
// Domain-agnostic identity framing

View File

@@ -205,11 +205,11 @@ describe("ce-code-review contract", () => {
)
for (const agent of [
"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",
"ce-dhh-rails-reviewer",
"ce-kieran-rails-reviewer",
"ce-kieran-python-reviewer",
"ce-kieran-typescript-reviewer",
"ce-julik-frontend-races-reviewer",
]) {
expect(content).toContain(agent)
expect(catalog).toContain(agent)
@@ -222,23 +222,23 @@ describe("ce-code-review contract", () => {
test("stack-specific reviewer agents follow the structured findings contract", async () => {
const reviewers = [
{
path: "plugins/compound-engineering/agents/review/ce-dhh-rails-reviewer.agent.md",
path: "plugins/compound-engineering/agents/ce-dhh-rails-reviewer.agent.md",
reviewer: "dhh-rails",
},
{
path: "plugins/compound-engineering/agents/review/ce-kieran-rails-reviewer.agent.md",
path: "plugins/compound-engineering/agents/ce-kieran-rails-reviewer.agent.md",
reviewer: "kieran-rails",
},
{
path: "plugins/compound-engineering/agents/review/ce-kieran-python-reviewer.agent.md",
path: "plugins/compound-engineering/agents/ce-kieran-python-reviewer.agent.md",
reviewer: "kieran-python",
},
{
path: "plugins/compound-engineering/agents/review/ce-kieran-typescript-reviewer.agent.md",
path: "plugins/compound-engineering/agents/ce-kieran-typescript-reviewer.agent.md",
reviewer: "kieran-typescript",
},
{
path: "plugins/compound-engineering/agents/review/ce-julik-frontend-races-reviewer.agent.md",
path: "plugins/compound-engineering/agents/ce-julik-frontend-races-reviewer.agent.md",
reviewer: "julik-frontend-races",
},
]
@@ -262,7 +262,7 @@ describe("ce-code-review contract", () => {
test("leaves data-migration-expert as the unstructured review format", async () => {
const content = await readRepoFile(
"plugins/compound-engineering/agents/review/ce-data-migration-expert.agent.md",
"plugins/compound-engineering/agents/ce-data-migration-expert.agent.md",
)
expect(content).toContain("## Reviewer Checklist")
@@ -304,7 +304,7 @@ describe("ce-code-review contract", () => {
describe("testing-reviewer contract", () => {
test("includes behavioral-changes-with-no-test-additions check", async () => {
const content = await readRepoFile("plugins/compound-engineering/agents/review/ce-testing-reviewer.agent.md")
const content = await readRepoFile("plugins/compound-engineering/agents/ce-testing-reviewer.agent.md")
// New check exists in "What you're hunting for" section
expect(content).toContain("Behavioral changes with no test additions")