@every-env/compound-plugin 2.40.1 → 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.
- package/.claude-plugin/marketplace.json +1 -1
- package/CHANGELOG.md +7 -0
- package/README.md +2 -2
- package/docs/solutions/codex-skill-prompt-entrypoints.md +152 -0
- package/package.json +1 -1
- package/plugins/compound-engineering/.claude-plugin/plugin.json +1 -1
- package/plugins/compound-engineering/AGENTS.md +1 -2
- package/plugins/compound-engineering/README.md +0 -2
- 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
|
@@ -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
|
|
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,13 @@ 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
|
+
|
|
10
17
|
## [2.40.1](https://github.com/EveryInc/compound-engineering-plugin/compare/v2.40.0...v2.40.1) (2026-03-17)
|
|
11
18
|
|
|
12
19
|
|
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
|
-
|
|
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` |
|
|
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,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "compound-engineering",
|
|
3
3
|
"version": "2.40.0",
|
|
4
|
-
"description": "AI-powered development tools. 28 agents,
|
|
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.
|
|
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
|
|
15
|
-
const
|
|
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 =
|
|
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) =>
|
|
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(
|
|
43
|
-
|
|
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(
|
|
63
|
-
|
|
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
|
-
|
|
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
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
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
|
-
|
|
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
|
|
156
|
-
const
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
.
|
|
162
|
-
.
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
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 {
|
|
@@ -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
|