@every-env/compound-plugin 2.40.1 → 2.40.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +1 -1
- package/CHANGELOG.md +14 -0
- package/README.md +2 -2
- package/docs/solutions/codex-skill-prompt-entrypoints.md +152 -0
- package/docs/specs/codex.md +3 -1
- package/package.json +1 -1
- package/plugins/compound-engineering/.claude-plugin/plugin.json +1 -1
- package/plugins/compound-engineering/AGENTS.md +13 -2
- package/plugins/compound-engineering/README.md +0 -2
- package/plugins/compound-engineering/agents/research/best-practices-researcher.md +8 -3
- package/plugins/compound-engineering/agents/research/framework-docs-researcher.md +2 -0
- package/plugins/compound-engineering/agents/research/git-history-analyzer.md +7 -5
- package/plugins/compound-engineering/agents/research/repo-research-analyst.md +5 -10
- package/src/converters/claude-to-codex.ts +118 -89
- package/src/parsers/claude-home.ts +10 -0
- package/src/parsers/claude.ts +1 -0
- package/src/targets/codex.ts +43 -2
- package/src/types/claude.ts +1 -0
- package/src/types/codex.ts +2 -0
- package/src/utils/codex-content.ts +84 -0
- package/tests/claude-home.test.ts +36 -0
- package/tests/codex-converter.test.ts +172 -0
- package/tests/codex-writer.test.ts +154 -0
- package/plugins/compound-engineering/skills/workflows-brainstorm/SKILL.md +0 -10
- package/plugins/compound-engineering/skills/workflows-compound/SKILL.md +0 -10
- package/plugins/compound-engineering/skills/workflows-plan/SKILL.md +0 -10
- package/plugins/compound-engineering/skills/workflows-review/SKILL.md +0 -10
- package/plugins/compound-engineering/skills/workflows-work/SKILL.md +0 -10
|
@@ -41,8 +41,18 @@ async function loadPersonalSkills(skillsDir: string): Promise<ClaudeSkill[]> {
|
|
|
41
41
|
const sourceDir = entry.isSymbolicLink()
|
|
42
42
|
? await fs.realpath(entryPath)
|
|
43
43
|
: entryPath
|
|
44
|
+
let data: Record<string, unknown> = {}
|
|
45
|
+
try {
|
|
46
|
+
const raw = await fs.readFile(skillPath, "utf8")
|
|
47
|
+
data = parseFrontmatter(raw).data
|
|
48
|
+
} catch {
|
|
49
|
+
// Keep syncing the skill even if frontmatter is malformed.
|
|
50
|
+
}
|
|
44
51
|
skills.push({
|
|
45
52
|
name: entry.name,
|
|
53
|
+
description: data.description as string | undefined,
|
|
54
|
+
argumentHint: data["argument-hint"] as string | undefined,
|
|
55
|
+
disableModelInvocation: data["disable-model-invocation"] === true ? true : undefined,
|
|
46
56
|
sourceDir,
|
|
47
57
|
skillPath,
|
|
48
58
|
})
|
package/src/parsers/claude.ts
CHANGED
|
@@ -110,6 +110,7 @@ async function loadSkills(skillsDirs: string[]): Promise<ClaudeSkill[]> {
|
|
|
110
110
|
skills.push({
|
|
111
111
|
name,
|
|
112
112
|
description: data.description as string | undefined,
|
|
113
|
+
argumentHint: data["argument-hint"] as string | undefined,
|
|
113
114
|
disableModelInvocation,
|
|
114
115
|
sourceDir: path.dirname(file),
|
|
115
116
|
skillPath: file,
|
package/src/targets/codex.ts
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
|
+
import { promises as fs } from "fs"
|
|
1
2
|
import path from "path"
|
|
2
|
-
import { backupFile,
|
|
3
|
+
import { backupFile, ensureDir, readText, writeText } from "../utils/files"
|
|
3
4
|
import type { CodexBundle } from "../types/codex"
|
|
4
5
|
import type { ClaudeMcpServer } from "../types/claude"
|
|
6
|
+
import { transformContentForCodex } from "../utils/codex-content"
|
|
5
7
|
|
|
6
8
|
export async function writeCodexBundle(outputRoot: string, bundle: CodexBundle): Promise<void> {
|
|
7
9
|
const codexRoot = resolveCodexRoot(outputRoot)
|
|
@@ -17,7 +19,11 @@ export async function writeCodexBundle(outputRoot: string, bundle: CodexBundle):
|
|
|
17
19
|
if (bundle.skillDirs.length > 0) {
|
|
18
20
|
const skillsRoot = path.join(codexRoot, "skills")
|
|
19
21
|
for (const skill of bundle.skillDirs) {
|
|
20
|
-
await
|
|
22
|
+
await copyCodexSkillDir(
|
|
23
|
+
skill.sourceDir,
|
|
24
|
+
path.join(skillsRoot, skill.name),
|
|
25
|
+
bundle.invocationTargets,
|
|
26
|
+
)
|
|
21
27
|
}
|
|
22
28
|
}
|
|
23
29
|
|
|
@@ -39,6 +45,41 @@ export async function writeCodexBundle(outputRoot: string, bundle: CodexBundle):
|
|
|
39
45
|
}
|
|
40
46
|
}
|
|
41
47
|
|
|
48
|
+
async function copyCodexSkillDir(
|
|
49
|
+
sourceDir: string,
|
|
50
|
+
targetDir: string,
|
|
51
|
+
invocationTargets?: CodexBundle["invocationTargets"],
|
|
52
|
+
): Promise<void> {
|
|
53
|
+
await ensureDir(targetDir)
|
|
54
|
+
const entries = await fs.readdir(sourceDir, { withFileTypes: true })
|
|
55
|
+
|
|
56
|
+
for (const entry of entries) {
|
|
57
|
+
const sourcePath = path.join(sourceDir, entry.name)
|
|
58
|
+
const targetPath = path.join(targetDir, entry.name)
|
|
59
|
+
|
|
60
|
+
if (entry.isDirectory()) {
|
|
61
|
+
await copyCodexSkillDir(sourcePath, targetPath, invocationTargets)
|
|
62
|
+
continue
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (!entry.isFile()) continue
|
|
66
|
+
|
|
67
|
+
if (entry.name === "SKILL.md") {
|
|
68
|
+
const content = await readText(sourcePath)
|
|
69
|
+
await writeText(
|
|
70
|
+
targetPath,
|
|
71
|
+
transformContentForCodex(content, invocationTargets, {
|
|
72
|
+
unknownSlashBehavior: "preserve",
|
|
73
|
+
}),
|
|
74
|
+
)
|
|
75
|
+
continue
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
await ensureDir(path.dirname(targetPath))
|
|
79
|
+
await fs.copyFile(sourcePath, targetPath)
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
42
83
|
function resolveCodexRoot(outputRoot: string): string {
|
|
43
84
|
return path.basename(outputRoot) === ".codex" ? outputRoot : path.join(outputRoot, ".codex")
|
|
44
85
|
}
|
package/src/types/claude.ts
CHANGED
package/src/types/codex.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { ClaudeMcpServer } from "./claude"
|
|
2
|
+
import type { CodexInvocationTargets } from "../utils/codex-content"
|
|
2
3
|
|
|
3
4
|
export type CodexPrompt = {
|
|
4
5
|
name: string
|
|
@@ -19,5 +20,6 @@ export type CodexBundle = {
|
|
|
19
20
|
prompts: CodexPrompt[]
|
|
20
21
|
skillDirs: CodexSkillDir[]
|
|
21
22
|
generatedSkills: CodexGeneratedSkill[]
|
|
23
|
+
invocationTargets?: CodexInvocationTargets
|
|
22
24
|
mcpServers?: Record<string, ClaudeMcpServer>
|
|
23
25
|
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
export type CodexInvocationTargets = {
|
|
2
|
+
promptTargets: Record<string, string>
|
|
3
|
+
skillTargets: Record<string, string>
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
export type CodexTransformOptions = {
|
|
7
|
+
unknownSlashBehavior?: "prompt" | "preserve"
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Transform Claude Code content to Codex-compatible content.
|
|
12
|
+
*
|
|
13
|
+
* Handles multiple syntax differences:
|
|
14
|
+
* 1. Task agent calls: Task agent-name(args) -> Use the $agent-name skill to: args
|
|
15
|
+
* 2. Slash command references:
|
|
16
|
+
* - known prompt entrypoints -> /prompts:prompt-name
|
|
17
|
+
* - known skills -> the exact skill name
|
|
18
|
+
* - unknown slash refs -> /prompts:command-name
|
|
19
|
+
* 3. Agent references: @agent-name -> $agent-name skill
|
|
20
|
+
* 4. Claude config paths: .claude/ -> .codex/
|
|
21
|
+
*/
|
|
22
|
+
export function transformContentForCodex(
|
|
23
|
+
body: string,
|
|
24
|
+
targets?: CodexInvocationTargets,
|
|
25
|
+
options: CodexTransformOptions = {},
|
|
26
|
+
): string {
|
|
27
|
+
let result = body
|
|
28
|
+
const promptTargets = targets?.promptTargets ?? {}
|
|
29
|
+
const skillTargets = targets?.skillTargets ?? {}
|
|
30
|
+
const unknownSlashBehavior = options.unknownSlashBehavior ?? "prompt"
|
|
31
|
+
|
|
32
|
+
const taskPattern = /^(\s*-?\s*)Task\s+([a-z][a-z0-9:-]*)\(([^)]+)\)/gm
|
|
33
|
+
result = result.replace(taskPattern, (_match, prefix: string, agentName: string, args: string) => {
|
|
34
|
+
// For namespaced calls like "compound-engineering:research:repo-research-analyst",
|
|
35
|
+
// use only the final segment as the skill name.
|
|
36
|
+
const finalSegment = agentName.includes(":") ? agentName.split(":").pop()! : agentName
|
|
37
|
+
const skillName = normalizeCodexName(finalSegment)
|
|
38
|
+
const trimmedArgs = args.trim()
|
|
39
|
+
return `${prefix}Use the $${skillName} skill to: ${trimmedArgs}`
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
const slashCommandPattern = /(?<![:\w])\/([a-z][a-z0-9_:-]*?)(?=[\s,."')\]}`]|$)/gi
|
|
43
|
+
result = result.replace(slashCommandPattern, (match, commandName: string) => {
|
|
44
|
+
if (commandName.includes("/")) return match
|
|
45
|
+
if (["dev", "tmp", "etc", "usr", "var", "bin", "home"].includes(commandName)) return match
|
|
46
|
+
|
|
47
|
+
const normalizedName = normalizeCodexName(commandName)
|
|
48
|
+
if (promptTargets[normalizedName]) {
|
|
49
|
+
return `/prompts:${promptTargets[normalizedName]}`
|
|
50
|
+
}
|
|
51
|
+
if (skillTargets[normalizedName]) {
|
|
52
|
+
return `the ${skillTargets[normalizedName]} skill`
|
|
53
|
+
}
|
|
54
|
+
if (unknownSlashBehavior === "preserve") {
|
|
55
|
+
return match
|
|
56
|
+
}
|
|
57
|
+
return `/prompts:${normalizedName}`
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
result = result
|
|
61
|
+
.replace(/~\/\.claude\//g, "~/.codex/")
|
|
62
|
+
.replace(/\.claude\//g, ".codex/")
|
|
63
|
+
|
|
64
|
+
const agentRefPattern = /@([a-z][a-z0-9-]*-(?:agent|reviewer|researcher|analyst|specialist|oracle|sentinel|guardian|strategist))/gi
|
|
65
|
+
result = result.replace(agentRefPattern, (_match, agentName: string) => {
|
|
66
|
+
const skillName = normalizeCodexName(agentName)
|
|
67
|
+
return `$${skillName} skill`
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
return result
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function normalizeCodexName(value: string): string {
|
|
74
|
+
const trimmed = value.trim()
|
|
75
|
+
if (!trimmed) return "item"
|
|
76
|
+
const normalized = trimmed
|
|
77
|
+
.toLowerCase()
|
|
78
|
+
.replace(/[\\/]+/g, "-")
|
|
79
|
+
.replace(/[:\s]+/g, "-")
|
|
80
|
+
.replace(/[^a-z0-9_-]+/g, "-")
|
|
81
|
+
.replace(/-+/g, "-")
|
|
82
|
+
.replace(/^-+|-+$/g, "")
|
|
83
|
+
return normalized || "item"
|
|
84
|
+
}
|
|
@@ -43,4 +43,40 @@ describe("loadClaudeHome", () => {
|
|
|
43
43
|
expect(config.commands?.find((command) => command.name === "custom-command")?.allowedTools).toEqual(["Bash", "Read"])
|
|
44
44
|
expect(config.mcpServers.context7?.url).toBe("https://mcp.context7.com/mcp")
|
|
45
45
|
})
|
|
46
|
+
|
|
47
|
+
test("keeps personal skill directory names stable even when frontmatter name differs", async () => {
|
|
48
|
+
const tempHome = await fs.mkdtemp(path.join(os.tmpdir(), "claude-home-skill-name-"))
|
|
49
|
+
const skillDir = path.join(tempHome, "skills", "reviewer")
|
|
50
|
+
|
|
51
|
+
await fs.mkdir(skillDir, { recursive: true })
|
|
52
|
+
await fs.writeFile(
|
|
53
|
+
path.join(skillDir, "SKILL.md"),
|
|
54
|
+
"---\nname: ce:plan\ndescription: Reviewer skill\nargument-hint: \"[topic]\"\n---\nReview things.\n",
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
const config = await loadClaudeHome(tempHome)
|
|
58
|
+
|
|
59
|
+
expect(config.skills).toHaveLength(1)
|
|
60
|
+
expect(config.skills[0]?.name).toBe("reviewer")
|
|
61
|
+
expect(config.skills[0]?.description).toBe("Reviewer skill")
|
|
62
|
+
expect(config.skills[0]?.argumentHint).toBe("[topic]")
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
test("keeps personal skills when frontmatter is malformed", async () => {
|
|
66
|
+
const tempHome = await fs.mkdtemp(path.join(os.tmpdir(), "claude-home-skill-yaml-"))
|
|
67
|
+
const skillDir = path.join(tempHome, "skills", "reviewer")
|
|
68
|
+
|
|
69
|
+
await fs.mkdir(skillDir, { recursive: true })
|
|
70
|
+
await fs.writeFile(
|
|
71
|
+
path.join(skillDir, "SKILL.md"),
|
|
72
|
+
"---\nname: ce:plan\nfoo: [unterminated\n---\nReview things.\n",
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
const config = await loadClaudeHome(tempHome)
|
|
76
|
+
|
|
77
|
+
expect(config.skills).toHaveLength(1)
|
|
78
|
+
expect(config.skills[0]?.name).toBe("reviewer")
|
|
79
|
+
expect(config.skills[0]?.description).toBeUndefined()
|
|
80
|
+
expect(config.skills[0]?.argumentHint).toBeUndefined()
|
|
81
|
+
})
|
|
46
82
|
})
|
|
@@ -31,6 +31,7 @@ const fixturePlugin: ClaudePlugin = {
|
|
|
31
31
|
{
|
|
32
32
|
name: "existing-skill",
|
|
33
33
|
description: "Existing skill",
|
|
34
|
+
argumentHint: "[ITEM]",
|
|
34
35
|
sourceDir: "/tmp/plugin/skills/existing-skill",
|
|
35
36
|
skillPath: "/tmp/plugin/skills/existing-skill/SKILL.md",
|
|
36
37
|
},
|
|
@@ -78,6 +79,81 @@ describe("convertClaudeToCodex", () => {
|
|
|
78
79
|
expect(parsedSkill.body).toContain("Threat modeling")
|
|
79
80
|
})
|
|
80
81
|
|
|
82
|
+
test("generates prompt wrappers for canonical ce workflow skills and omits workflows aliases", () => {
|
|
83
|
+
const plugin: ClaudePlugin = {
|
|
84
|
+
...fixturePlugin,
|
|
85
|
+
manifest: { name: "compound-engineering", version: "1.0.0" },
|
|
86
|
+
commands: [],
|
|
87
|
+
agents: [],
|
|
88
|
+
skills: [
|
|
89
|
+
{
|
|
90
|
+
name: "ce:plan",
|
|
91
|
+
description: "Planning workflow",
|
|
92
|
+
argumentHint: "[feature]",
|
|
93
|
+
sourceDir: "/tmp/plugin/skills/ce-plan",
|
|
94
|
+
skillPath: "/tmp/plugin/skills/ce-plan/SKILL.md",
|
|
95
|
+
},
|
|
96
|
+
{
|
|
97
|
+
name: "workflows:plan",
|
|
98
|
+
description: "Deprecated planning alias",
|
|
99
|
+
argumentHint: "[feature]",
|
|
100
|
+
sourceDir: "/tmp/plugin/skills/workflows-plan",
|
|
101
|
+
skillPath: "/tmp/plugin/skills/workflows-plan/SKILL.md",
|
|
102
|
+
},
|
|
103
|
+
],
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const bundle = convertClaudeToCodex(plugin, {
|
|
107
|
+
agentMode: "subagent",
|
|
108
|
+
inferTemperature: false,
|
|
109
|
+
permissions: "none",
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
expect(bundle.prompts).toHaveLength(1)
|
|
113
|
+
expect(bundle.prompts[0]?.name).toBe("ce-plan")
|
|
114
|
+
|
|
115
|
+
const parsedPrompt = parseFrontmatter(bundle.prompts[0]!.content)
|
|
116
|
+
expect(parsedPrompt.data.description).toBe("Planning workflow")
|
|
117
|
+
expect(parsedPrompt.data["argument-hint"]).toBe("[feature]")
|
|
118
|
+
expect(parsedPrompt.body).toContain("Use the ce:plan skill")
|
|
119
|
+
|
|
120
|
+
expect(bundle.skillDirs.map((skill) => skill.name)).toEqual(["ce:plan"])
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
test("does not apply compound workflow canonicalization to other plugins", () => {
|
|
124
|
+
const plugin: ClaudePlugin = {
|
|
125
|
+
...fixturePlugin,
|
|
126
|
+
manifest: { name: "other-plugin", version: "1.0.0" },
|
|
127
|
+
commands: [],
|
|
128
|
+
agents: [],
|
|
129
|
+
skills: [
|
|
130
|
+
{
|
|
131
|
+
name: "ce:plan",
|
|
132
|
+
description: "Custom CE-namespaced skill",
|
|
133
|
+
argumentHint: "[feature]",
|
|
134
|
+
sourceDir: "/tmp/plugin/skills/ce-plan",
|
|
135
|
+
skillPath: "/tmp/plugin/skills/ce-plan/SKILL.md",
|
|
136
|
+
},
|
|
137
|
+
{
|
|
138
|
+
name: "workflows:plan",
|
|
139
|
+
description: "Custom workflows-namespaced skill",
|
|
140
|
+
argumentHint: "[feature]",
|
|
141
|
+
sourceDir: "/tmp/plugin/skills/workflows-plan",
|
|
142
|
+
skillPath: "/tmp/plugin/skills/workflows-plan/SKILL.md",
|
|
143
|
+
},
|
|
144
|
+
],
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const bundle = convertClaudeToCodex(plugin, {
|
|
148
|
+
agentMode: "subagent",
|
|
149
|
+
inferTemperature: false,
|
|
150
|
+
permissions: "none",
|
|
151
|
+
})
|
|
152
|
+
|
|
153
|
+
expect(bundle.prompts).toHaveLength(0)
|
|
154
|
+
expect(bundle.skillDirs.map((skill) => skill.name)).toEqual(["ce:plan", "workflows:plan"])
|
|
155
|
+
})
|
|
156
|
+
|
|
81
157
|
test("passes through MCP servers", () => {
|
|
82
158
|
const bundle = convertClaudeToCodex(fixturePlugin, {
|
|
83
159
|
agentMode: "subagent",
|
|
@@ -131,6 +207,47 @@ Task best-practices-researcher(topic)`,
|
|
|
131
207
|
expect(parsed.body).not.toContain("Task learnings-researcher")
|
|
132
208
|
})
|
|
133
209
|
|
|
210
|
+
test("transforms namespaced Task agent calls to skill references using final segment", () => {
|
|
211
|
+
const plugin: ClaudePlugin = {
|
|
212
|
+
...fixturePlugin,
|
|
213
|
+
commands: [
|
|
214
|
+
{
|
|
215
|
+
name: "plan",
|
|
216
|
+
description: "Planning with namespaced agents",
|
|
217
|
+
body: `Run these agents in parallel:
|
|
218
|
+
|
|
219
|
+
- Task compound-engineering:research:repo-research-analyst(feature_description)
|
|
220
|
+
- Task compound-engineering:research:learnings-researcher(feature_description)
|
|
221
|
+
|
|
222
|
+
Then consolidate findings.
|
|
223
|
+
|
|
224
|
+
Task compound-engineering:review:security-reviewer(code_diff)`,
|
|
225
|
+
sourcePath: "/tmp/plugin/commands/plan.md",
|
|
226
|
+
},
|
|
227
|
+
],
|
|
228
|
+
agents: [],
|
|
229
|
+
skills: [],
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
const bundle = convertClaudeToCodex(plugin, {
|
|
233
|
+
agentMode: "subagent",
|
|
234
|
+
inferTemperature: false,
|
|
235
|
+
permissions: "none",
|
|
236
|
+
})
|
|
237
|
+
|
|
238
|
+
const commandSkill = bundle.generatedSkills.find((s) => s.name === "plan")
|
|
239
|
+
expect(commandSkill).toBeDefined()
|
|
240
|
+
const parsed = parseFrontmatter(commandSkill!.content)
|
|
241
|
+
|
|
242
|
+
// Namespaced Task calls should use only the final segment as the skill name
|
|
243
|
+
expect(parsed.body).toContain("Use the $repo-research-analyst skill to: feature_description")
|
|
244
|
+
expect(parsed.body).toContain("Use the $learnings-researcher skill to: feature_description")
|
|
245
|
+
expect(parsed.body).toContain("Use the $security-reviewer skill to: code_diff")
|
|
246
|
+
|
|
247
|
+
// Original namespaced Task syntax should not remain
|
|
248
|
+
expect(parsed.body).not.toContain("Task compound-engineering:")
|
|
249
|
+
})
|
|
250
|
+
|
|
134
251
|
test("transforms slash commands to prompts syntax", () => {
|
|
135
252
|
const plugin: ClaudePlugin = {
|
|
136
253
|
...fixturePlugin,
|
|
@@ -172,6 +289,61 @@ Don't confuse with file paths like /tmp/output.md or /dev/null.`,
|
|
|
172
289
|
expect(parsed.body).toContain("/dev/null")
|
|
173
290
|
})
|
|
174
291
|
|
|
292
|
+
test("transforms canonical workflow slash commands to Codex prompt references", () => {
|
|
293
|
+
const plugin: ClaudePlugin = {
|
|
294
|
+
...fixturePlugin,
|
|
295
|
+
manifest: { name: "compound-engineering", version: "1.0.0" },
|
|
296
|
+
commands: [
|
|
297
|
+
{
|
|
298
|
+
name: "review",
|
|
299
|
+
description: "Review command",
|
|
300
|
+
body: `After the brainstorm, run /ce:plan.
|
|
301
|
+
|
|
302
|
+
If planning is complete, continue with /ce:work.`,
|
|
303
|
+
sourcePath: "/tmp/plugin/commands/review.md",
|
|
304
|
+
},
|
|
305
|
+
],
|
|
306
|
+
agents: [],
|
|
307
|
+
skills: [
|
|
308
|
+
{
|
|
309
|
+
name: "ce:plan",
|
|
310
|
+
description: "Planning workflow",
|
|
311
|
+
argumentHint: "[feature]",
|
|
312
|
+
sourceDir: "/tmp/plugin/skills/ce-plan",
|
|
313
|
+
skillPath: "/tmp/plugin/skills/ce-plan/SKILL.md",
|
|
314
|
+
},
|
|
315
|
+
{
|
|
316
|
+
name: "ce:work",
|
|
317
|
+
description: "Implementation workflow",
|
|
318
|
+
argumentHint: "[feature]",
|
|
319
|
+
sourceDir: "/tmp/plugin/skills/ce-work",
|
|
320
|
+
skillPath: "/tmp/plugin/skills/ce-work/SKILL.md",
|
|
321
|
+
},
|
|
322
|
+
{
|
|
323
|
+
name: "workflows:work",
|
|
324
|
+
description: "Deprecated implementation alias",
|
|
325
|
+
argumentHint: "[feature]",
|
|
326
|
+
sourceDir: "/tmp/plugin/skills/workflows-work",
|
|
327
|
+
skillPath: "/tmp/plugin/skills/workflows-work/SKILL.md",
|
|
328
|
+
},
|
|
329
|
+
],
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
const bundle = convertClaudeToCodex(plugin, {
|
|
333
|
+
agentMode: "subagent",
|
|
334
|
+
inferTemperature: false,
|
|
335
|
+
permissions: "none",
|
|
336
|
+
})
|
|
337
|
+
|
|
338
|
+
const commandSkill = bundle.generatedSkills.find((s) => s.name === "review")
|
|
339
|
+
expect(commandSkill).toBeDefined()
|
|
340
|
+
const parsed = parseFrontmatter(commandSkill!.content)
|
|
341
|
+
|
|
342
|
+
expect(parsed.body).toContain("/prompts:ce-plan")
|
|
343
|
+
expect(parsed.body).toContain("/prompts:ce-work")
|
|
344
|
+
expect(parsed.body).not.toContain("the ce:plan skill")
|
|
345
|
+
})
|
|
346
|
+
|
|
175
347
|
test("excludes commands with disable-model-invocation from prompts and skills", () => {
|
|
176
348
|
const plugin: ClaudePlugin = {
|
|
177
349
|
...fixturePlugin,
|
|
@@ -105,4 +105,158 @@ describe("writeCodexBundle", () => {
|
|
|
105
105
|
const backupContent = await fs.readFile(path.join(codexRoot, backupFileName!), "utf8")
|
|
106
106
|
expect(backupContent).toBe(originalContent)
|
|
107
107
|
})
|
|
108
|
+
|
|
109
|
+
test("transforms copied SKILL.md files using Codex invocation targets", async () => {
|
|
110
|
+
const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), "codex-skill-transform-"))
|
|
111
|
+
const sourceSkillDir = path.join(tempRoot, "source-skill")
|
|
112
|
+
await fs.mkdir(sourceSkillDir, { recursive: true })
|
|
113
|
+
await fs.writeFile(
|
|
114
|
+
path.join(sourceSkillDir, "SKILL.md"),
|
|
115
|
+
`---
|
|
116
|
+
name: ce:brainstorm
|
|
117
|
+
description: Brainstorm workflow
|
|
118
|
+
---
|
|
119
|
+
|
|
120
|
+
Continue with /ce:plan when ready.
|
|
121
|
+
Or use /workflows:plan if you're following an older doc.
|
|
122
|
+
Use /deepen-plan for deeper research.
|
|
123
|
+
`,
|
|
124
|
+
)
|
|
125
|
+
await fs.writeFile(
|
|
126
|
+
path.join(sourceSkillDir, "notes.md"),
|
|
127
|
+
"Reference docs still mention /ce:plan here.\n",
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
const bundle: CodexBundle = {
|
|
131
|
+
prompts: [],
|
|
132
|
+
skillDirs: [{ name: "ce:brainstorm", sourceDir: sourceSkillDir }],
|
|
133
|
+
generatedSkills: [],
|
|
134
|
+
invocationTargets: {
|
|
135
|
+
promptTargets: {
|
|
136
|
+
"ce-plan": "ce-plan",
|
|
137
|
+
"workflows-plan": "ce-plan",
|
|
138
|
+
"deepen-plan": "deepen-plan",
|
|
139
|
+
},
|
|
140
|
+
skillTargets: {},
|
|
141
|
+
},
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
await writeCodexBundle(tempRoot, bundle)
|
|
145
|
+
|
|
146
|
+
const installedSkill = await fs.readFile(
|
|
147
|
+
path.join(tempRoot, ".codex", "skills", "ce:brainstorm", "SKILL.md"),
|
|
148
|
+
"utf8",
|
|
149
|
+
)
|
|
150
|
+
expect(installedSkill).toContain("/prompts:ce-plan")
|
|
151
|
+
expect(installedSkill).not.toContain("/workflows:plan")
|
|
152
|
+
expect(installedSkill).toContain("/prompts:deepen-plan")
|
|
153
|
+
|
|
154
|
+
const notes = await fs.readFile(
|
|
155
|
+
path.join(tempRoot, ".codex", "skills", "ce:brainstorm", "notes.md"),
|
|
156
|
+
"utf8",
|
|
157
|
+
)
|
|
158
|
+
expect(notes).toContain("/ce:plan")
|
|
159
|
+
})
|
|
160
|
+
|
|
161
|
+
test("transforms namespaced Task calls in copied SKILL.md files", async () => {
|
|
162
|
+
const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), "codex-ns-task-"))
|
|
163
|
+
const sourceSkillDir = path.join(tempRoot, "source-skill")
|
|
164
|
+
await fs.mkdir(sourceSkillDir, { recursive: true })
|
|
165
|
+
await fs.writeFile(
|
|
166
|
+
path.join(sourceSkillDir, "SKILL.md"),
|
|
167
|
+
`---
|
|
168
|
+
name: ce:plan
|
|
169
|
+
description: Planning workflow
|
|
170
|
+
---
|
|
171
|
+
|
|
172
|
+
Run these research agents:
|
|
173
|
+
|
|
174
|
+
- Task compound-engineering:research:repo-research-analyst(feature_description)
|
|
175
|
+
- Task compound-engineering:research:learnings-researcher(feature_description)
|
|
176
|
+
|
|
177
|
+
Also run bare agents:
|
|
178
|
+
|
|
179
|
+
- Task best-practices-researcher(topic)
|
|
180
|
+
`,
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
const bundle: CodexBundle = {
|
|
184
|
+
prompts: [],
|
|
185
|
+
skillDirs: [{ name: "ce:plan", sourceDir: sourceSkillDir }],
|
|
186
|
+
generatedSkills: [],
|
|
187
|
+
invocationTargets: {
|
|
188
|
+
promptTargets: {},
|
|
189
|
+
skillTargets: {},
|
|
190
|
+
},
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
await writeCodexBundle(tempRoot, bundle)
|
|
194
|
+
|
|
195
|
+
const installedSkill = await fs.readFile(
|
|
196
|
+
path.join(tempRoot, ".codex", "skills", "ce:plan", "SKILL.md"),
|
|
197
|
+
"utf8",
|
|
198
|
+
)
|
|
199
|
+
|
|
200
|
+
// Namespaced Task calls should be rewritten using the final segment
|
|
201
|
+
expect(installedSkill).toContain("Use the $repo-research-analyst skill to: feature_description")
|
|
202
|
+
expect(installedSkill).toContain("Use the $learnings-researcher skill to: feature_description")
|
|
203
|
+
expect(installedSkill).not.toContain("Task compound-engineering:")
|
|
204
|
+
|
|
205
|
+
// Bare Task calls should still be rewritten
|
|
206
|
+
expect(installedSkill).toContain("Use the $best-practices-researcher skill to: topic")
|
|
207
|
+
expect(installedSkill).not.toContain("Task best-practices-researcher")
|
|
208
|
+
})
|
|
209
|
+
|
|
210
|
+
test("preserves unknown slash text in copied SKILL.md files", async () => {
|
|
211
|
+
const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), "codex-skill-preserve-"))
|
|
212
|
+
const sourceSkillDir = path.join(tempRoot, "source-skill")
|
|
213
|
+
await fs.mkdir(sourceSkillDir, { recursive: true })
|
|
214
|
+
await fs.writeFile(
|
|
215
|
+
path.join(sourceSkillDir, "SKILL.md"),
|
|
216
|
+
`---
|
|
217
|
+
name: proof
|
|
218
|
+
description: Proof skill
|
|
219
|
+
---
|
|
220
|
+
|
|
221
|
+
Route examples:
|
|
222
|
+
- /users
|
|
223
|
+
- /settings
|
|
224
|
+
|
|
225
|
+
API examples:
|
|
226
|
+
- https://www.proofeditor.ai/api/agent/{slug}/state
|
|
227
|
+
- https://www.proofeditor.ai/share/markdown
|
|
228
|
+
|
|
229
|
+
Workflow handoff:
|
|
230
|
+
- /ce:plan
|
|
231
|
+
`,
|
|
232
|
+
)
|
|
233
|
+
|
|
234
|
+
const bundle: CodexBundle = {
|
|
235
|
+
prompts: [],
|
|
236
|
+
skillDirs: [{ name: "proof", sourceDir: sourceSkillDir }],
|
|
237
|
+
generatedSkills: [],
|
|
238
|
+
invocationTargets: {
|
|
239
|
+
promptTargets: {
|
|
240
|
+
"ce-plan": "ce-plan",
|
|
241
|
+
},
|
|
242
|
+
skillTargets: {},
|
|
243
|
+
},
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
await writeCodexBundle(tempRoot, bundle)
|
|
247
|
+
|
|
248
|
+
const installedSkill = await fs.readFile(
|
|
249
|
+
path.join(tempRoot, ".codex", "skills", "proof", "SKILL.md"),
|
|
250
|
+
"utf8",
|
|
251
|
+
)
|
|
252
|
+
|
|
253
|
+
expect(installedSkill).toContain("/users")
|
|
254
|
+
expect(installedSkill).toContain("/settings")
|
|
255
|
+
expect(installedSkill).toContain("https://www.proofeditor.ai/api/agent/{slug}/state")
|
|
256
|
+
expect(installedSkill).toContain("https://www.proofeditor.ai/share/markdown")
|
|
257
|
+
expect(installedSkill).toContain("/prompts:ce-plan")
|
|
258
|
+
expect(installedSkill).not.toContain("/prompts:users")
|
|
259
|
+
expect(installedSkill).not.toContain("/prompts:settings")
|
|
260
|
+
expect(installedSkill).not.toContain("https://prompts:www.proofeditor.ai")
|
|
261
|
+
})
|
|
108
262
|
})
|
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: workflows:brainstorm
|
|
3
|
-
description: "[DEPRECATED] Use /ce:brainstorm instead — renamed for clarity."
|
|
4
|
-
argument-hint: "[feature idea or problem to explore]"
|
|
5
|
-
disable-model-invocation: true
|
|
6
|
-
---
|
|
7
|
-
|
|
8
|
-
NOTE: /workflows:brainstorm is deprecated. Please use /ce:brainstorm instead. This alias will be removed in a future version.
|
|
9
|
-
|
|
10
|
-
/ce:brainstorm $ARGUMENTS
|
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: workflows:compound
|
|
3
|
-
description: "[DEPRECATED] Use /ce:compound instead — renamed for clarity."
|
|
4
|
-
argument-hint: "[optional: brief context about the fix]"
|
|
5
|
-
disable-model-invocation: true
|
|
6
|
-
---
|
|
7
|
-
|
|
8
|
-
NOTE: /workflows:compound is deprecated. Please use /ce:compound instead. This alias will be removed in a future version.
|
|
9
|
-
|
|
10
|
-
/ce:compound $ARGUMENTS
|
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: workflows:plan
|
|
3
|
-
description: "[DEPRECATED] Use /ce:plan instead — renamed for clarity."
|
|
4
|
-
argument-hint: "[feature description, bug report, or improvement idea]"
|
|
5
|
-
disable-model-invocation: true
|
|
6
|
-
---
|
|
7
|
-
|
|
8
|
-
NOTE: /workflows:plan is deprecated. Please use /ce:plan instead. This alias will be removed in a future version.
|
|
9
|
-
|
|
10
|
-
/ce:plan $ARGUMENTS
|
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: workflows:review
|
|
3
|
-
description: "[DEPRECATED] Use /ce:review instead — renamed for clarity."
|
|
4
|
-
argument-hint: "[PR number, GitHub URL, branch name, or latest]"
|
|
5
|
-
disable-model-invocation: true
|
|
6
|
-
---
|
|
7
|
-
|
|
8
|
-
NOTE: /workflows:review is deprecated. Please use /ce:review instead. This alias will be removed in a future version.
|
|
9
|
-
|
|
10
|
-
/ce:review $ARGUMENTS
|
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: workflows:work
|
|
3
|
-
description: "[DEPRECATED] Use /ce:work instead — renamed for clarity."
|
|
4
|
-
argument-hint: "[plan file, specification, or todo file path]"
|
|
5
|
-
disable-model-invocation: true
|
|
6
|
-
---
|
|
7
|
-
|
|
8
|
-
NOTE: /workflows:work is deprecated. Please use /ce:work instead. This alias will be removed in a future version.
|
|
9
|
-
|
|
10
|
-
/ce:work $ARGUMENTS
|