refactor(agents): flatten agents directory (#621)
This commit is contained in:
@@ -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:")
|
||||
})
|
||||
|
||||
@@ -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", () => {
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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"),
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
@@ -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 })
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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")
|
||||
|
||||
Reference in New Issue
Block a user