@every-env/compound-plugin 2.40.0 → 2.40.2

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.
@@ -11,7 +11,7 @@
11
11
  "plugins": [
12
12
  {
13
13
  "name": "compound-engineering",
14
- "description": "AI-powered development tools that get smarter with every use. Make each unit of engineering work easier than the last. Includes 28 specialized agents and 46 skills.",
14
+ "description": "AI-powered development tools that get smarter with every use. Make each unit of engineering work easier than the last. Includes 28 specialized agents and 41 skills.",
15
15
  "version": "2.40.0",
16
16
  "author": {
17
17
  "name": "Kieran Klaassen",
package/CHANGELOG.md CHANGED
@@ -7,6 +7,20 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  Release numbering now follows the repository `v*` tag line. Starting at `v2.34.0`, the root CLI package and this changelog stay on that shared version stream. Older entries below retain the previous `0.x` CLI numbering.
9
9
 
10
+ ## [2.40.2](https://github.com/EveryInc/compound-engineering-plugin/compare/v2.40.1...v2.40.2) (2026-03-17)
11
+
12
+
13
+ ### Bug Fixes
14
+
15
+ * harden codex copied skill rewriting ([#285](https://github.com/EveryInc/compound-engineering-plugin/issues/285)) ([6f561f9](https://github.com/EveryInc/compound-engineering-plugin/commit/6f561f94b4397ca6df2ab163e6f1253817bd7cea))
16
+
17
+ ## [2.40.1](https://github.com/EveryInc/compound-engineering-plugin/compare/v2.40.0...v2.40.1) (2026-03-17)
18
+
19
+
20
+ ### Bug Fixes
21
+
22
+ * **kiro:** parse .mcp.json wrapper key and support remote MCP servers ([#259](https://github.com/EveryInc/compound-engineering-plugin/issues/259)) ([dfff20e](https://github.com/EveryInc/compound-engineering-plugin/commit/dfff20e1adab891b4645a53d0581d4b20577e3f1))
23
+
10
24
  # [2.40.0](https://github.com/EveryInc/compound-engineering-plugin/compare/v2.39.0...v2.40.0) (2026-03-17)
11
25
 
12
26
 
package/README.md CHANGED
@@ -82,7 +82,7 @@ Then run `claude-dev-ce` instead of `claude` to test your changes. Your producti
82
82
  **Codex** — point the install command at your local path:
83
83
 
84
84
  ```bash
85
- bunx @every-env/compound-plugin install ./plugins/compound-engineering --to codex
85
+ bun run src/index.ts install ./plugins/compound-engineering --to codex
86
86
  ```
87
87
 
88
88
  **Other targets** — same pattern, swap the target:
@@ -97,7 +97,7 @@ bun run src/index.ts install ./plugins/compound-engineering --to opencode
97
97
  | Target | Output path | Notes |
98
98
  |--------|------------|-------|
99
99
  | `opencode` | `~/.config/opencode/` | Commands as `.md` files; `opencode.json` MCP config deep-merged; backups made before overwriting |
100
- | `codex` | `~/.codex/prompts` + `~/.codex/skills` | Each command becomes a prompt + skill pair; descriptions truncated to 1024 chars |
100
+ | `codex` | `~/.codex/prompts` + `~/.codex/skills` | Claude commands become prompt + skill pairs; canonical `ce:*` workflow skills also get prompt wrappers; deprecated `workflows:*` aliases are omitted |
101
101
  | `droid` | `~/.factory/` | Tool names mapped (`Bash`→`Execute`, `Write`→`Create`); namespace prefixes stripped |
102
102
  | `pi` | `~/.pi/agent/` | Prompts, skills, extensions, and `mcporter.json` for MCPorter interoperability |
103
103
  | `gemini` | `.gemini/` | Skills from agents; commands as `.toml`; namespaced commands become directories (`workflows:plan` → `commands/workflows/plan.toml`) |
@@ -0,0 +1,152 @@
1
+ ---
2
+ title: Codex Conversion Skills, Prompts, and Canonical Entry Points
3
+ category: architecture
4
+ tags: [codex, converter, skills, prompts, workflows, deprecation]
5
+ created: 2026-03-15
6
+ severity: medium
7
+ component: codex-target
8
+ problem_type: best_practice
9
+ root_cause: outdated_target_model
10
+ ---
11
+
12
+ # Codex Conversion Skills, Prompts, and Canonical Entry Points
13
+
14
+ ## Problem
15
+
16
+ The Codex target had two conflicting assumptions:
17
+
18
+ 1. Compound workflow entrypoints like `ce:brainstorm` and `ce:plan` were treated in docs as slash-command-style surfaces.
19
+ 2. The Codex converter installed those entries as copied skills, not as generated prompts.
20
+
21
+ That created an inconsistent runtime for cross-workflow handoffs. Copied skill content still contained Claude-style references like `/ce:plan`, but no Codex-native translation was applied to copied `SKILL.md` files, and there was no clear canonical Codex entrypoint model for those workflow skills.
22
+
23
+ ## What We Learned
24
+
25
+ ### 1. Codex supports both skills and prompts, and they are different surfaces
26
+
27
+ - Skills are loaded from skill roots such as `~/.codex/skills`, and newer Codex code also supports `.agents/skills`.
28
+ - Prompts are a separate explicit entrypoint surface under `.codex/prompts`.
29
+ - A skill is not automatically a prompt, and a prompt is not automatically a skill.
30
+
31
+ For this repo, that means a copied skill like `ce:plan` is only a skill unless the converter also generates a prompt wrapper for it.
32
+
33
+ ### 2. Codex skill names come from the directory name
34
+
35
+ Codex derives the skill name from the skill directory basename, not from our normalized hyphenated converter name.
36
+
37
+ Implication:
38
+
39
+ - `~/.codex/skills/ce:plan` loads as the skill `ce:plan`
40
+ - Rewriting that to `ce-plan` is wrong for skill-to-skill references
41
+
42
+ ### 3. The original bug was structural, not just wording
43
+
44
+ The issue was not that `ce:brainstorm` needed slightly different prose. The real problem was:
45
+
46
+ - copied skills bypassed Codex-specific transformation
47
+ - workflow handoffs referenced a surface that was not clearly represented in installed Codex artifacts
48
+
49
+ ### 4. Deprecated `workflows:*` aliases add noise in Codex
50
+
51
+ The `workflows:*` names exist only for backward compatibility in Claude.
52
+
53
+ Copying them into Codex would:
54
+
55
+ - duplicate user-facing entrypoints
56
+ - complicate handoff rewriting
57
+ - increase ambiguity around which name is canonical
58
+
59
+ For Codex, the simpler model is to treat `ce:*` as the only canonical workflow namespace and omit `workflows:*` aliases from installed output.
60
+
61
+ ## Recommended Codex Model
62
+
63
+ Use a two-layer mapping for workflow entrypoints:
64
+
65
+ 1. **Skills remain the implementation units**
66
+ - Copy the canonical workflow skills using their exact names, such as `ce:plan`
67
+ - Preserve exact skill names for any Codex skill references
68
+
69
+ 2. **Prompts are the explicit entrypoint layer**
70
+ - Generate prompt wrappers for canonical user-facing workflow entrypoints
71
+ - Use Codex-safe prompt slugs such as `ce-plan`, `ce-work`, `ce-review`
72
+ - Prompt wrappers delegate to the exact underlying skill name, such as `ce:plan`
73
+
74
+ This gives Codex one clear manual invocation surface while preserving the real loaded skill names internally.
75
+
76
+ ## Rewrite Rules
77
+
78
+ When converting copied `SKILL.md` content for Codex:
79
+
80
+ - References to canonical workflow entrypoints should point to generated prompt wrappers
81
+ - `/ce:plan` -> `/prompts:ce-plan`
82
+ - `/ce:work` -> `/prompts:ce-work`
83
+ - References to deprecated aliases should canonicalize to the modern `ce:*` prompt
84
+ - `/workflows:plan` -> `/prompts:ce-plan`
85
+ - References to non-entrypoint skills should use the exact skill name, not a normalized alias
86
+ - Actual Claude commands that are converted to Codex prompts can continue using `/prompts:...`
87
+
88
+ ### Regression hardening
89
+
90
+ When rewriting copied `SKILL.md` files, only known workflow and command references should be rewritten.
91
+
92
+ Do not rewrite arbitrary slash-shaped text such as:
93
+
94
+ - application routes like `/users` or `/settings`
95
+ - API path segments like `/state` or `/ops`
96
+ - URLs such as `https://www.proofeditor.ai/...`
97
+
98
+ Unknown slash references should remain unchanged in copied skill content. Otherwise Codex installs silently corrupt unrelated skills while trying to canonicalize workflow handoffs.
99
+
100
+ Personal skills loaded from `~/.claude/skills` also need tolerant metadata parsing:
101
+
102
+ - malformed YAML frontmatter should not cause the entire skill to disappear
103
+ - keep the directory name as the stable skill name
104
+ - treat frontmatter metadata as best-effort only
105
+
106
+ ## Future Entry Points
107
+
108
+ Do not hard-code an allowlist of workflow names in the converter.
109
+
110
+ Instead, use a stable rule:
111
+
112
+ - `ce:*` = canonical workflow entrypoint
113
+ - auto-generate a prompt wrapper
114
+ - `workflows:*` = deprecated alias
115
+ - omit from Codex output
116
+ - rewrite references to the canonical `ce:*` target
117
+ - non-`ce:*` skills = skill-only by default
118
+ - if a non-`ce:*` skill should also be a prompt entrypoint, mark it explicitly with Codex-specific metadata
119
+
120
+ This means future skills like `ce:ideate` should work without manual converter changes.
121
+
122
+ ## Implementation Guidance
123
+
124
+ For the Codex target:
125
+
126
+ 1. Parse enough skill frontmatter to distinguish command-like entrypoint skills from background skills
127
+ 2. Filter deprecated `workflows:*` alias skills out of Codex installation
128
+ 3. Generate prompt wrappers for canonical `ce:*` workflow skills
129
+ 4. Apply Codex-specific transformation to copied `SKILL.md` files
130
+ 5. Preserve exact Codex skill names internally
131
+ 6. Update README language so Codex entrypoints are documented as Codex-native surfaces, not assumed to be identical to Claude slash commands
132
+
133
+ ## Prevention
134
+
135
+ Before changing the Codex converter again:
136
+
137
+ 1. Verify whether the target surface is a skill, a prompt, or both
138
+ 2. Check how Codex derives names from installed artifacts
139
+ 3. Decide which names are canonical before copying deprecated aliases
140
+ 4. Add tests for copied skill content, not just generated prompt content
141
+
142
+ ## Related Files
143
+
144
+ - `src/converters/claude-to-codex.ts`
145
+ - `src/targets/codex.ts`
146
+ - `src/types/codex.ts`
147
+ - `tests/codex-converter.test.ts`
148
+ - `tests/codex-writer.test.ts`
149
+ - `README.md`
150
+ - `plugins/compound-engineering/skills/ce-brainstorm/SKILL.md`
151
+ - `plugins/compound-engineering/skills/ce-plan/SKILL.md`
152
+ - `docs/solutions/adding-converter-target-providers.md`
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@every-env/compound-plugin",
3
- "version": "2.40.0",
3
+ "version": "2.40.2",
4
4
  "type": "module",
5
5
  "private": false,
6
6
  "bin": {
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "compound-engineering",
3
3
  "version": "2.40.0",
4
- "description": "AI-powered development tools. 28 agents, 46 skills, 1 MCP server for code review, research, design, and workflow automation.",
4
+ "description": "AI-powered development tools. 28 agents, 41 skills, 1 MCP server for code review, research, design, and workflow automation.",
5
5
  "author": {
6
6
  "name": "Kieran Klaassen",
7
7
  "email": "kieran@every.to",
@@ -40,7 +40,6 @@ agents/
40
40
 
41
41
  skills/
42
42
  ├── ce-*/ # Core workflow skills (ce:plan, ce:review, etc.)
43
- ├── workflows-*/ # Deprecated aliases for ce:* skills
44
43
  └── */ # All other skills
45
44
  ```
46
45
 
@@ -57,7 +56,7 @@ skills/
57
56
  - `/ce:work` - Execute work items systematically
58
57
  - `/ce:compound` - Document solved problems
59
58
 
60
- **Why `ce:`?** Claude Code has built-in `/plan` and `/review` commands. The `ce:` namespace (short for compound-engineering) makes it immediately clear these commands belong to this plugin. The legacy `workflows:` prefix is still supported as deprecated aliases that forward to the `ce:*` equivalents.
59
+ **Why `ce:`?** Claude Code has built-in `/plan` and `/review` commands. The `ce:` namespace (short for compound-engineering) makes it immediately clear these commands belong to this plugin.
61
60
 
62
61
  ## Skill Compliance Checklist
63
62
 
@@ -83,8 +83,6 @@ Core workflow commands use `ce:` prefix to unambiguously identify them as compou
83
83
  | `/ce:compound` | Document solved problems to compound team knowledge |
84
84
  | `/ce:compound-refresh` | Refresh stale or drifting learnings and decide whether to keep, update, replace, or archive them |
85
85
 
86
- > **Deprecated aliases:** `/workflows:plan`, `/workflows:work`, `/workflows:review`, `/workflows:brainstorm`, `/workflows:compound` still work but show a deprecation warning. Use `ce:*` equivalents.
87
-
88
86
  ### Utility Commands
89
87
 
90
88
  | Command | Description |
@@ -1,7 +1,12 @@
1
1
  import { formatFrontmatter } from "../utils/frontmatter"
2
- import type { ClaudeAgent, ClaudeCommand, ClaudePlugin } from "../types/claude"
2
+ import type { ClaudeAgent, ClaudeCommand, ClaudePlugin, ClaudeSkill } from "../types/claude"
3
3
  import type { CodexBundle, CodexGeneratedSkill } from "../types/codex"
4
4
  import type { ClaudeToOpenCodeOptions } from "./claude-to-opencode"
5
+ import {
6
+ normalizeCodexName,
7
+ transformContentForCodex,
8
+ type CodexInvocationTargets,
9
+ } from "../utils/codex-content"
5
10
 
6
11
  export type ClaudeToCodexOptions = ClaudeToOpenCodeOptions
7
12
 
@@ -11,42 +16,102 @@ export function convertClaudeToCodex(
11
16
  plugin: ClaudePlugin,
12
17
  _options: ClaudeToCodexOptions,
13
18
  ): CodexBundle {
14
- const promptNames = new Set<string>()
15
- const skillDirs = plugin.skills.map((skill) => ({
19
+ const invocableCommands = plugin.commands.filter((command) => !command.disableModelInvocation)
20
+ const applyCompoundWorkflowModel = shouldApplyCompoundWorkflowModel(plugin)
21
+ const canonicalWorkflowSkills = applyCompoundWorkflowModel
22
+ ? plugin.skills.filter((skill) => isCanonicalCodexWorkflowSkill(skill.name))
23
+ : []
24
+ const deprecatedWorkflowAliases = applyCompoundWorkflowModel
25
+ ? plugin.skills.filter((skill) => isDeprecatedCodexWorkflowAlias(skill.name))
26
+ : []
27
+ const copiedSkills = applyCompoundWorkflowModel
28
+ ? plugin.skills.filter((skill) => !isDeprecatedCodexWorkflowAlias(skill.name))
29
+ : plugin.skills
30
+ const skillDirs = copiedSkills.map((skill) => ({
16
31
  name: skill.name,
17
32
  sourceDir: skill.sourceDir,
18
33
  }))
34
+ const promptNames = new Set<string>()
35
+ const usedSkillNames = new Set<string>(skillDirs.map((skill) => normalizeCodexName(skill.name)))
36
+
37
+ const commandPromptNames = new Map<string, string>()
38
+ for (const command of invocableCommands) {
39
+ commandPromptNames.set(
40
+ command.name,
41
+ uniqueName(normalizeCodexName(command.name), promptNames),
42
+ )
43
+ }
44
+
45
+ const workflowPromptNames = new Map<string, string>()
46
+ for (const skill of canonicalWorkflowSkills) {
47
+ workflowPromptNames.set(
48
+ skill.name,
49
+ uniqueName(normalizeCodexName(skill.name), promptNames),
50
+ )
51
+ }
52
+
53
+ const promptTargets: Record<string, string> = {}
54
+ for (const [commandName, promptName] of commandPromptNames) {
55
+ promptTargets[normalizeCodexName(commandName)] = promptName
56
+ }
57
+ for (const [skillName, promptName] of workflowPromptNames) {
58
+ promptTargets[normalizeCodexName(skillName)] = promptName
59
+ }
60
+ for (const alias of deprecatedWorkflowAliases) {
61
+ const canonicalName = toCanonicalWorkflowSkillName(alias.name)
62
+ const promptName = canonicalName ? workflowPromptNames.get(canonicalName) : undefined
63
+ if (promptName) {
64
+ promptTargets[normalizeCodexName(alias.name)] = promptName
65
+ }
66
+ }
67
+
68
+ const skillTargets: Record<string, string> = {}
69
+ for (const skill of copiedSkills) {
70
+ if (applyCompoundWorkflowModel && isCanonicalCodexWorkflowSkill(skill.name)) continue
71
+ skillTargets[normalizeCodexName(skill.name)] = skill.name
72
+ }
73
+
74
+ const invocationTargets: CodexInvocationTargets = { promptTargets, skillTargets }
19
75
 
20
- const usedSkillNames = new Set<string>(skillDirs.map((skill) => normalizeName(skill.name)))
21
76
  const commandSkills: CodexGeneratedSkill[] = []
22
- const invocableCommands = plugin.commands.filter((command) => !command.disableModelInvocation)
23
77
  const prompts = invocableCommands.map((command) => {
24
- const promptName = uniqueName(normalizeName(command.name), promptNames)
25
- const commandSkill = convertCommandSkill(command, usedSkillNames)
78
+ const promptName = commandPromptNames.get(command.name)!
79
+ const commandSkill = convertCommandSkill(command, usedSkillNames, invocationTargets)
26
80
  commandSkills.push(commandSkill)
27
- const content = renderPrompt(command, commandSkill.name)
81
+ const content = renderPrompt(command, commandSkill.name, invocationTargets)
28
82
  return { name: promptName, content }
29
83
  })
84
+ const workflowPrompts = canonicalWorkflowSkills.map((skill) => ({
85
+ name: workflowPromptNames.get(skill.name)!,
86
+ content: renderWorkflowPrompt(skill),
87
+ }))
30
88
 
31
- const agentSkills = plugin.agents.map((agent) => convertAgent(agent, usedSkillNames))
89
+ const agentSkills = plugin.agents.map((agent) =>
90
+ convertAgent(agent, usedSkillNames, invocationTargets),
91
+ )
32
92
  const generatedSkills = [...commandSkills, ...agentSkills]
33
93
 
34
94
  return {
35
- prompts,
95
+ prompts: [...prompts, ...workflowPrompts],
36
96
  skillDirs,
37
97
  generatedSkills,
98
+ invocationTargets,
38
99
  mcpServers: plugin.mcpServers,
39
100
  }
40
101
  }
41
102
 
42
- function convertAgent(agent: ClaudeAgent, usedNames: Set<string>): CodexGeneratedSkill {
43
- const name = uniqueName(normalizeName(agent.name), usedNames)
103
+ function convertAgent(
104
+ agent: ClaudeAgent,
105
+ usedNames: Set<string>,
106
+ invocationTargets: CodexInvocationTargets,
107
+ ): CodexGeneratedSkill {
108
+ const name = uniqueName(normalizeCodexName(agent.name), usedNames)
44
109
  const description = sanitizeDescription(
45
110
  agent.description ?? `Converted from Claude agent ${agent.name}`,
46
111
  )
47
112
  const frontmatter: Record<string, unknown> = { name, description }
48
113
 
49
- let body = transformContentForCodex(agent.body.trim())
114
+ let body = transformContentForCodex(agent.body.trim(), invocationTargets)
50
115
  if (agent.capabilities && agent.capabilities.length > 0) {
51
116
  const capabilities = agent.capabilities.map((capability) => `- ${capability}`).join("\n")
52
117
  body = `## Capabilities\n${capabilities}\n\n${body}`.trim()
@@ -59,8 +124,12 @@ function convertAgent(agent: ClaudeAgent, usedNames: Set<string>): CodexGenerate
59
124
  return { name, content }
60
125
  }
61
126
 
62
- function convertCommandSkill(command: ClaudeCommand, usedNames: Set<string>): CodexGeneratedSkill {
63
- const name = uniqueName(normalizeName(command.name), usedNames)
127
+ function convertCommandSkill(
128
+ command: ClaudeCommand,
129
+ usedNames: Set<string>,
130
+ invocationTargets: CodexInvocationTargets,
131
+ ): CodexGeneratedSkill {
132
+ const name = uniqueName(normalizeCodexName(command.name), usedNames)
64
133
  const frontmatter: Record<string, unknown> = {
65
134
  name,
66
135
  description: sanitizeDescription(
@@ -74,95 +143,55 @@ function convertCommandSkill(command: ClaudeCommand, usedNames: Set<string>): Co
74
143
  if (command.allowedTools && command.allowedTools.length > 0) {
75
144
  sections.push(`## Allowed tools\n${command.allowedTools.map((tool) => `- ${tool}`).join("\n")}`)
76
145
  }
77
- // Transform Task agent calls to Codex skill references
78
- const transformedBody = transformTaskCalls(command.body.trim())
146
+ const transformedBody = transformContentForCodex(command.body.trim(), invocationTargets)
79
147
  sections.push(transformedBody)
80
148
  const body = sections.filter(Boolean).join("\n\n").trim()
81
149
  const content = formatFrontmatter(frontmatter, body.length > 0 ? body : command.body)
82
150
  return { name, content }
83
151
  }
84
152
 
85
- /**
86
- * Transform Claude Code content to Codex-compatible content.
87
- *
88
- * Handles multiple syntax differences:
89
- * 1. Task agent calls: Task agent-name(args) → Use the $agent-name skill to: args
90
- * 2. Slash commands: /command-name → /prompts:command-name
91
- * 3. Agent references: @agent-name → $agent-name skill
92
- *
93
- * This bridges the gap since Claude Code and Codex have different syntax
94
- * for invoking commands, agents, and skills.
95
- */
96
- function transformContentForCodex(body: string): string {
97
- let result = body
98
-
99
- // 1. Transform Task agent calls
100
- // Match: Task repo-research-analyst(feature_description)
101
- // Match: - Task learnings-researcher(args)
102
- const taskPattern = /^(\s*-?\s*)Task\s+([a-z][a-z0-9-]*)\(([^)]+)\)/gm
103
- result = result.replace(taskPattern, (_match, prefix: string, agentName: string, args: string) => {
104
- const skillName = normalizeName(agentName)
105
- const trimmedArgs = args.trim()
106
- return `${prefix}Use the $${skillName} skill to: ${trimmedArgs}`
107
- })
108
-
109
- // 2. Transform slash command references
110
- // Match: /command-name or /workflows:command but NOT /path/to/file or URLs
111
- // Look for slash commands in contexts like "Run /command", "use /command", etc.
112
- // Avoid matching file paths (contain multiple slashes) or URLs (contain ://)
113
- const slashCommandPattern = /(?<![:\w])\/([a-z][a-z0-9_:-]*?)(?=[\s,."')\]}`]|$)/gi
114
- result = result.replace(slashCommandPattern, (match, commandName: string) => {
115
- // Skip if it looks like a file path (contains /)
116
- if (commandName.includes('/')) return match
117
- // Skip common non-command patterns
118
- if (['dev', 'tmp', 'etc', 'usr', 'var', 'bin', 'home'].includes(commandName)) return match
119
- // Transform to Codex prompt syntax
120
- const normalizedName = normalizeName(commandName)
121
- return `/prompts:${normalizedName}`
122
- })
123
-
124
- // 3. Rewrite .claude/ paths to .codex/
125
- result = result
126
- .replace(/~\/\.claude\//g, "~/.codex/")
127
- .replace(/\.claude\//g, ".codex/")
128
-
129
- // 4. Transform @agent-name references
130
- // Match: @agent-name in text (not emails)
131
- const agentRefPattern = /@([a-z][a-z0-9-]*-(?:agent|reviewer|researcher|analyst|specialist|oracle|sentinel|guardian|strategist))/gi
132
- result = result.replace(agentRefPattern, (_match, agentName: string) => {
133
- const skillName = normalizeName(agentName)
134
- return `$${skillName} skill`
135
- })
136
-
137
- return result
138
- }
139
-
140
- // Alias for backward compatibility
141
- const transformTaskCalls = transformContentForCodex
142
-
143
- function renderPrompt(command: ClaudeCommand, skillName: string): string {
153
+ function renderPrompt(
154
+ command: ClaudeCommand,
155
+ skillName: string,
156
+ invocationTargets: CodexInvocationTargets,
157
+ ): string {
144
158
  const frontmatter: Record<string, unknown> = {
145
159
  description: command.description,
146
160
  "argument-hint": command.argumentHint,
147
161
  }
148
162
  const instructions = `Use the $${skillName} skill for this command and follow its instructions.`
149
- // Transform Task calls in prompt body too (not just skill body)
150
- const transformedBody = transformTaskCalls(command.body)
163
+ const transformedBody = transformContentForCodex(command.body, invocationTargets)
151
164
  const body = [instructions, "", transformedBody].join("\n").trim()
152
165
  return formatFrontmatter(frontmatter, body)
153
166
  }
154
167
 
155
- function normalizeName(value: string): string {
156
- const trimmed = value.trim()
157
- if (!trimmed) return "item"
158
- const normalized = trimmed
159
- .toLowerCase()
160
- .replace(/[\\/]+/g, "-")
161
- .replace(/[:\s]+/g, "-")
162
- .replace(/[^a-z0-9_-]+/g, "-")
163
- .replace(/-+/g, "-")
164
- .replace(/^-+|-+$/g, "")
165
- return normalized || "item"
168
+ function renderWorkflowPrompt(skill: ClaudeSkill): string {
169
+ const frontmatter: Record<string, unknown> = {
170
+ description: skill.description,
171
+ "argument-hint": skill.argumentHint,
172
+ }
173
+ const body = [
174
+ `Use the ${skill.name} skill for this workflow and follow its instructions exactly.`,
175
+ "Treat any text after the prompt name as the workflow context to pass through.",
176
+ ].join("\n\n")
177
+ return formatFrontmatter(frontmatter, body)
178
+ }
179
+
180
+ function isCanonicalCodexWorkflowSkill(name: string): boolean {
181
+ return name.startsWith("ce:")
182
+ }
183
+
184
+ function isDeprecatedCodexWorkflowAlias(name: string): boolean {
185
+ return name.startsWith("workflows:")
186
+ }
187
+
188
+ function toCanonicalWorkflowSkillName(name: string): string | null {
189
+ if (!isDeprecatedCodexWorkflowAlias(name)) return null
190
+ return `ce:${name.slice("workflows:".length)}`
191
+ }
192
+
193
+ function shouldApplyCompoundWorkflowModel(plugin: ClaudePlugin): boolean {
194
+ return plugin.manifest.name === "compound-engineering"
166
195
  }
167
196
 
168
197
  function sanitizeDescription(value: string, maxLength = CODEX_DESCRIPTION_MAX_LENGTH): string {
@@ -53,7 +53,7 @@ export function convertClaudeToKiro(
53
53
  convertCommandToSkill(command, usedSkillNames, agentNames),
54
54
  )
55
55
 
56
- // Convert MCP servers (stdio only)
56
+ // Convert MCP servers (stdio and remote)
57
57
  const mcpServers = convertMcpServers(plugin.mcpServers)
58
58
 
59
59
  // Build steering files from CLAUDE.md
@@ -177,19 +177,20 @@ function convertMcpServers(
177
177
 
178
178
  const result: Record<string, KiroMcpServer> = {}
179
179
  for (const [name, server] of Object.entries(servers)) {
180
- if (!server.command) {
180
+ if (server.command) {
181
+ const entry: KiroMcpServer = { command: server.command }
182
+ if (server.args && server.args.length > 0) entry.args = server.args
183
+ if (server.env && Object.keys(server.env).length > 0) entry.env = server.env
184
+ result[name] = entry
185
+ } else if (server.url) {
186
+ const entry: KiroMcpServer = { url: server.url }
187
+ if (server.headers && Object.keys(server.headers).length > 0) entry.headers = server.headers
188
+ result[name] = entry
189
+ } else {
181
190
  console.warn(
182
- `Warning: MCP server "${name}" has no command (HTTP/SSE transport). Kiro only supports stdio. Skipping.`,
191
+ `Warning: MCP server "${name}" has no command or url. Skipping.`,
183
192
  )
184
- continue
185
193
  }
186
-
187
- const entry: KiroMcpServer = { command: server.command }
188
- if (server.args && server.args.length > 0) entry.args = server.args
189
- if (server.env && Object.keys(server.env).length > 0) entry.env = server.env
190
-
191
- console.log(`MCP server "${name}" will execute: ${server.command}${server.args ? " " + server.args.join(" ") : ""}`)
192
- result[name] = entry
193
194
  }
194
195
  return result
195
196
  }
@@ -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
  })
@@ -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,
@@ -158,7 +159,8 @@ async function loadMcpServers(
158
159
 
159
160
  const mcpPath = path.join(root, ".mcp.json")
160
161
  if (await pathExists(mcpPath)) {
161
- return readJson<Record<string, ClaudeMcpServer>>(mcpPath)
162
+ const raw = await readJson<Record<string, unknown>>(mcpPath)
163
+ return unwrapMcpServers(raw)
162
164
  }
163
165
 
164
166
  return undefined
@@ -232,12 +234,20 @@ async function loadMcpPaths(
232
234
  for (const entry of toPathList(value)) {
233
235
  const resolved = resolveWithinRoot(root, entry, "mcpServers path")
234
236
  if (await pathExists(resolved)) {
235
- configs.push(await readJson<Record<string, ClaudeMcpServer>>(resolved))
237
+ const raw = await readJson<Record<string, unknown>>(resolved)
238
+ configs.push(unwrapMcpServers(raw))
236
239
  }
237
240
  }
238
241
  return configs
239
242
  }
240
243
 
244
+ function unwrapMcpServers(raw: Record<string, unknown>): Record<string, ClaudeMcpServer> {
245
+ if (raw.mcpServers && typeof raw.mcpServers === "object") {
246
+ return raw.mcpServers as Record<string, ClaudeMcpServer>
247
+ }
248
+ return raw as Record<string, ClaudeMcpServer>
249
+ }
250
+
241
251
  function mergeMcpConfigs(configs: Record<string, ClaudeMcpServer>[]): Record<string, ClaudeMcpServer> {
242
252
  return configs.reduce((acc, config) => ({ ...acc, ...config }), {})
243
253
  }
@@ -1,7 +1,9 @@
1
+ import { promises as fs } from "fs"
1
2
  import path from "path"
2
- import { backupFile, copyDir, ensureDir, writeText } from "../utils/files"
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 copyDir(skill.sourceDir, path.join(skillsRoot, skill.name))
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
  }
@@ -47,6 +47,7 @@ export type ClaudeCommand = {
47
47
  export type ClaudeSkill = {
48
48
  name: string
49
49
  description?: string
50
+ argumentHint?: string
50
51
  disableModelInvocation?: boolean
51
52
  sourceDir: string
52
53
  skillPath: string
@@ -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
  })
@@ -174,7 +174,24 @@ describe("convertClaudeToKiro", () => {
174
174
  expect(bundle.mcpServers.local.args).toEqual(["hello"])
175
175
  })
176
176
 
177
- test("MCP HTTP servers skipped with warning", () => {
177
+ test("MCP HTTP servers converted with url", () => {
178
+ const plugin: ClaudePlugin = {
179
+ ...fixturePlugin,
180
+ mcpServers: {
181
+ httpServer: { url: "https://example.com/mcp" },
182
+ },
183
+ agents: [],
184
+ commands: [],
185
+ skills: [],
186
+ }
187
+
188
+ const bundle = convertClaudeToKiro(plugin, defaultOptions)
189
+
190
+ expect(Object.keys(bundle.mcpServers)).toHaveLength(1)
191
+ expect(bundle.mcpServers.httpServer).toEqual({ url: "https://example.com/mcp" })
192
+ })
193
+
194
+ test("MCP servers with no command or url skipped with warning", () => {
178
195
  const warnings: string[] = []
179
196
  const originalWarn = console.warn
180
197
  console.warn = (msg: string) => warnings.push(msg)
@@ -182,7 +199,7 @@ describe("convertClaudeToKiro", () => {
182
199
  const plugin: ClaudePlugin = {
183
200
  ...fixturePlugin,
184
201
  mcpServers: {
185
- httpServer: { url: "https://example.com/mcp" },
202
+ broken: {} as any,
186
203
  },
187
204
  agents: [],
188
205
  commands: [],
@@ -193,7 +210,7 @@ describe("convertClaudeToKiro", () => {
193
210
  console.warn = originalWarn
194
211
 
195
212
  expect(Object.keys(bundle.mcpServers)).toHaveLength(0)
196
- expect(warnings.some((w) => w.includes("no command") || w.includes("HTTP"))).toBe(true)
213
+ expect(warnings.some((w) => w.includes("no command or url"))).toBe(true)
197
214
  })
198
215
 
199
216
  test("plugin with zero agents produces empty agents array", () => {
@@ -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