chore: remove deprecated workflows:* skill aliases (#284)
* docs: capture codex skill prompt model * fix: align codex workflow conversion * chore: remove deprecated workflows:* skill aliases The workflows:brainstorm, workflows:plan, workflows:work, workflows:review, and workflows:compound aliases have been deprecated long enough. Remove them and update skill counts (46 → 41) across plugin.json, marketplace.json, README, and CLAUDE.md. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Trevin Chow <trevin@trevinchow.com> Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -11,7 +11,7 @@
|
||||
"plugins": [
|
||||
{
|
||||
"name": "compound-engineering",
|
||||
"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.",
|
||||
"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.",
|
||||
"version": "2.40.0",
|
||||
"author": {
|
||||
"name": "Kieran Klaassen",
|
||||
|
||||
@@ -82,7 +82,7 @@ Then run `claude-dev-ce` instead of `claude` to test your changes. Your producti
|
||||
**Codex** — point the install command at your local path:
|
||||
|
||||
```bash
|
||||
bunx @every-env/compound-plugin install ./plugins/compound-engineering --to codex
|
||||
bun run src/index.ts install ./plugins/compound-engineering --to codex
|
||||
```
|
||||
|
||||
**Other targets** — same pattern, swap the target:
|
||||
@@ -97,7 +97,7 @@ bun run src/index.ts install ./plugins/compound-engineering --to opencode
|
||||
| Target | Output path | Notes |
|
||||
|--------|------------|-------|
|
||||
| `opencode` | `~/.config/opencode/` | Commands as `.md` files; `opencode.json` MCP config deep-merged; backups made before overwriting |
|
||||
| `codex` | `~/.codex/prompts` + `~/.codex/skills` | Each command becomes a prompt + skill pair; descriptions truncated to 1024 chars |
|
||||
| `codex` | `~/.codex/prompts` + `~/.codex/skills` | Claude commands become prompt + skill pairs; canonical `ce:*` workflow skills also get prompt wrappers; deprecated `workflows:*` aliases are omitted |
|
||||
| `droid` | `~/.factory/` | Tool names mapped (`Bash`→`Execute`, `Write`→`Create`); namespace prefixes stripped |
|
||||
| `pi` | `~/.pi/agent/` | Prompts, skills, extensions, and `mcporter.json` for MCPorter interoperability |
|
||||
| `gemini` | `.gemini/` | Skills from agents; commands as `.toml`; namespaced commands become directories (`workflows:plan` → `commands/workflows/plan.toml`) |
|
||||
|
||||
134
docs/solutions/codex-skill-prompt-entrypoints.md
Normal file
134
docs/solutions/codex-skill-prompt-entrypoints.md
Normal file
@@ -0,0 +1,134 @@
|
||||
---
|
||||
title: Codex Conversion Skills, Prompts, and Canonical Entry Points
|
||||
category: architecture
|
||||
tags: [codex, converter, skills, prompts, workflows, deprecation]
|
||||
created: 2026-03-15
|
||||
severity: medium
|
||||
component: codex-target
|
||||
problem_type: best_practice
|
||||
root_cause: outdated_target_model
|
||||
---
|
||||
|
||||
# Codex Conversion Skills, Prompts, and Canonical Entry Points
|
||||
|
||||
## Problem
|
||||
|
||||
The Codex target had two conflicting assumptions:
|
||||
|
||||
1. Compound workflow entrypoints like `ce:brainstorm` and `ce:plan` were treated in docs as slash-command-style surfaces.
|
||||
2. The Codex converter installed those entries as copied skills, not as generated prompts.
|
||||
|
||||
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.
|
||||
|
||||
## What We Learned
|
||||
|
||||
### 1. Codex supports both skills and prompts, and they are different surfaces
|
||||
|
||||
- Skills are loaded from skill roots such as `~/.codex/skills`, and newer Codex code also supports `.agents/skills`.
|
||||
- Prompts are a separate explicit entrypoint surface under `.codex/prompts`.
|
||||
- A skill is not automatically a prompt, and a prompt is not automatically a skill.
|
||||
|
||||
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.
|
||||
|
||||
### 2. Codex skill names come from the directory name
|
||||
|
||||
Codex derives the skill name from the skill directory basename, not from our normalized hyphenated converter name.
|
||||
|
||||
Implication:
|
||||
|
||||
- `~/.codex/skills/ce:plan` loads as the skill `ce:plan`
|
||||
- Rewriting that to `ce-plan` is wrong for skill-to-skill references
|
||||
|
||||
### 3. The original bug was structural, not just wording
|
||||
|
||||
The issue was not that `ce:brainstorm` needed slightly different prose. The real problem was:
|
||||
|
||||
- copied skills bypassed Codex-specific transformation
|
||||
- workflow handoffs referenced a surface that was not clearly represented in installed Codex artifacts
|
||||
|
||||
### 4. Deprecated `workflows:*` aliases add noise in Codex
|
||||
|
||||
The `workflows:*` names exist only for backward compatibility in Claude.
|
||||
|
||||
Copying them into Codex would:
|
||||
|
||||
- duplicate user-facing entrypoints
|
||||
- complicate handoff rewriting
|
||||
- increase ambiguity around which name is canonical
|
||||
|
||||
For Codex, the simpler model is to treat `ce:*` as the only canonical workflow namespace and omit `workflows:*` aliases from installed output.
|
||||
|
||||
## Recommended Codex Model
|
||||
|
||||
Use a two-layer mapping for workflow entrypoints:
|
||||
|
||||
1. **Skills remain the implementation units**
|
||||
- Copy the canonical workflow skills using their exact names, such as `ce:plan`
|
||||
- Preserve exact skill names for any Codex skill references
|
||||
|
||||
2. **Prompts are the explicit entrypoint layer**
|
||||
- Generate prompt wrappers for canonical user-facing workflow entrypoints
|
||||
- Use Codex-safe prompt slugs such as `ce-plan`, `ce-work`, `ce-review`
|
||||
- Prompt wrappers delegate to the exact underlying skill name, such as `ce:plan`
|
||||
|
||||
This gives Codex one clear manual invocation surface while preserving the real loaded skill names internally.
|
||||
|
||||
## Rewrite Rules
|
||||
|
||||
When converting copied `SKILL.md` content for Codex:
|
||||
|
||||
- References to canonical workflow entrypoints should point to generated prompt wrappers
|
||||
- `/ce:plan` -> `/prompts:ce-plan`
|
||||
- `/ce:work` -> `/prompts:ce-work`
|
||||
- References to deprecated aliases should canonicalize to the modern `ce:*` prompt
|
||||
- `/workflows:plan` -> `/prompts:ce-plan`
|
||||
- References to non-entrypoint skills should use the exact skill name, not a normalized alias
|
||||
- Actual Claude commands that are converted to Codex prompts can continue using `/prompts:...`
|
||||
|
||||
## Future Entry Points
|
||||
|
||||
Do not hard-code an allowlist of workflow names in the converter.
|
||||
|
||||
Instead, use a stable rule:
|
||||
|
||||
- `ce:*` = canonical workflow entrypoint
|
||||
- auto-generate a prompt wrapper
|
||||
- `workflows:*` = deprecated alias
|
||||
- omit from Codex output
|
||||
- rewrite references to the canonical `ce:*` target
|
||||
- non-`ce:*` skills = skill-only by default
|
||||
- if a non-`ce:*` skill should also be a prompt entrypoint, mark it explicitly with Codex-specific metadata
|
||||
|
||||
This means future skills like `ce:ideate` should work without manual converter changes.
|
||||
|
||||
## Implementation Guidance
|
||||
|
||||
For the Codex target:
|
||||
|
||||
1. Parse enough skill frontmatter to distinguish command-like entrypoint skills from background skills
|
||||
2. Filter deprecated `workflows:*` alias skills out of Codex installation
|
||||
3. Generate prompt wrappers for canonical `ce:*` workflow skills
|
||||
4. Apply Codex-specific transformation to copied `SKILL.md` files
|
||||
5. Preserve exact Codex skill names internally
|
||||
6. Update README language so Codex entrypoints are documented as Codex-native surfaces, not assumed to be identical to Claude slash commands
|
||||
|
||||
## Prevention
|
||||
|
||||
Before changing the Codex converter again:
|
||||
|
||||
1. Verify whether the target surface is a skill, a prompt, or both
|
||||
2. Check how Codex derives names from installed artifacts
|
||||
3. Decide which names are canonical before copying deprecated aliases
|
||||
4. Add tests for copied skill content, not just generated prompt content
|
||||
|
||||
## Related Files
|
||||
|
||||
- `src/converters/claude-to-codex.ts`
|
||||
- `src/targets/codex.ts`
|
||||
- `src/types/codex.ts`
|
||||
- `tests/codex-converter.test.ts`
|
||||
- `tests/codex-writer.test.ts`
|
||||
- `README.md`
|
||||
- `plugins/compound-engineering/skills/ce-brainstorm/SKILL.md`
|
||||
- `plugins/compound-engineering/skills/ce-plan/SKILL.md`
|
||||
- `docs/solutions/adding-converter-target-providers.md`
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "compound-engineering",
|
||||
"version": "2.40.0",
|
||||
"description": "AI-powered development tools. 28 agents, 46 skills, 1 MCP server for code review, research, design, and workflow automation.",
|
||||
"description": "AI-powered development tools. 28 agents, 41 skills, 1 MCP server for code review, research, design, and workflow automation.",
|
||||
"author": {
|
||||
"name": "Kieran Klaassen",
|
||||
"email": "kieran@every.to",
|
||||
|
||||
@@ -40,7 +40,6 @@ agents/
|
||||
|
||||
skills/
|
||||
├── ce-*/ # Core workflow skills (ce:plan, ce:review, etc.)
|
||||
├── workflows-*/ # Deprecated aliases for ce:* skills
|
||||
└── */ # All other skills
|
||||
```
|
||||
|
||||
@@ -57,7 +56,7 @@ skills/
|
||||
- `/ce:work` - Execute work items systematically
|
||||
- `/ce:compound` - Document solved problems
|
||||
|
||||
**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.
|
||||
**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.
|
||||
|
||||
## Skill Compliance Checklist
|
||||
|
||||
|
||||
@@ -83,8 +83,6 @@ Core workflow commands use `ce:` prefix to unambiguously identify them as compou
|
||||
| `/ce:compound` | Document solved problems to compound team knowledge |
|
||||
| `/ce:compound-refresh` | Refresh stale or drifting learnings and decide whether to keep, update, replace, or archive them |
|
||||
|
||||
> **Deprecated aliases:** `/workflows:plan`, `/workflows:work`, `/workflows:review`, `/workflows:brainstorm`, `/workflows:compound` still work but show a deprecation warning. Use `ce:*` equivalents.
|
||||
|
||||
### Utility Commands
|
||||
|
||||
| Command | Description |
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
---
|
||||
name: workflows:brainstorm
|
||||
description: "[DEPRECATED] Use /ce:brainstorm instead — renamed for clarity."
|
||||
argument-hint: "[feature idea or problem to explore]"
|
||||
disable-model-invocation: true
|
||||
---
|
||||
|
||||
NOTE: /workflows:brainstorm is deprecated. Please use /ce:brainstorm instead. This alias will be removed in a future version.
|
||||
|
||||
/ce:brainstorm $ARGUMENTS
|
||||
@@ -1,10 +0,0 @@
|
||||
---
|
||||
name: workflows:compound
|
||||
description: "[DEPRECATED] Use /ce:compound instead — renamed for clarity."
|
||||
argument-hint: "[optional: brief context about the fix]"
|
||||
disable-model-invocation: true
|
||||
---
|
||||
|
||||
NOTE: /workflows:compound is deprecated. Please use /ce:compound instead. This alias will be removed in a future version.
|
||||
|
||||
/ce:compound $ARGUMENTS
|
||||
@@ -1,10 +0,0 @@
|
||||
---
|
||||
name: workflows:plan
|
||||
description: "[DEPRECATED] Use /ce:plan instead — renamed for clarity."
|
||||
argument-hint: "[feature description, bug report, or improvement idea]"
|
||||
disable-model-invocation: true
|
||||
---
|
||||
|
||||
NOTE: /workflows:plan is deprecated. Please use /ce:plan instead. This alias will be removed in a future version.
|
||||
|
||||
/ce:plan $ARGUMENTS
|
||||
@@ -1,10 +0,0 @@
|
||||
---
|
||||
name: workflows:review
|
||||
description: "[DEPRECATED] Use /ce:review instead — renamed for clarity."
|
||||
argument-hint: "[PR number, GitHub URL, branch name, or latest]"
|
||||
disable-model-invocation: true
|
||||
---
|
||||
|
||||
NOTE: /workflows:review is deprecated. Please use /ce:review instead. This alias will be removed in a future version.
|
||||
|
||||
/ce:review $ARGUMENTS
|
||||
@@ -1,10 +0,0 @@
|
||||
---
|
||||
name: workflows:work
|
||||
description: "[DEPRECATED] Use /ce:work instead — renamed for clarity."
|
||||
argument-hint: "[plan file, specification, or todo file path]"
|
||||
disable-model-invocation: true
|
||||
---
|
||||
|
||||
NOTE: /workflows:work is deprecated. Please use /ce:work instead. This alias will be removed in a future version.
|
||||
|
||||
/ce:work $ARGUMENTS
|
||||
@@ -1,7 +1,12 @@
|
||||
import { formatFrontmatter } from "../utils/frontmatter"
|
||||
import type { ClaudeAgent, ClaudeCommand, ClaudePlugin } from "../types/claude"
|
||||
import type { ClaudeAgent, ClaudeCommand, ClaudePlugin, ClaudeSkill } from "../types/claude"
|
||||
import type { CodexBundle, CodexGeneratedSkill } from "../types/codex"
|
||||
import type { ClaudeToOpenCodeOptions } from "./claude-to-opencode"
|
||||
import {
|
||||
normalizeCodexName,
|
||||
transformContentForCodex,
|
||||
type CodexInvocationTargets,
|
||||
} from "../utils/codex-content"
|
||||
|
||||
export type ClaudeToCodexOptions = ClaudeToOpenCodeOptions
|
||||
|
||||
@@ -11,42 +16,102 @@ export function convertClaudeToCodex(
|
||||
plugin: ClaudePlugin,
|
||||
_options: ClaudeToCodexOptions,
|
||||
): CodexBundle {
|
||||
const promptNames = new Set<string>()
|
||||
const skillDirs = plugin.skills.map((skill) => ({
|
||||
const invocableCommands = plugin.commands.filter((command) => !command.disableModelInvocation)
|
||||
const applyCompoundWorkflowModel = shouldApplyCompoundWorkflowModel(plugin)
|
||||
const canonicalWorkflowSkills = applyCompoundWorkflowModel
|
||||
? plugin.skills.filter((skill) => isCanonicalCodexWorkflowSkill(skill.name))
|
||||
: []
|
||||
const deprecatedWorkflowAliases = applyCompoundWorkflowModel
|
||||
? plugin.skills.filter((skill) => isDeprecatedCodexWorkflowAlias(skill.name))
|
||||
: []
|
||||
const copiedSkills = applyCompoundWorkflowModel
|
||||
? plugin.skills.filter((skill) => !isDeprecatedCodexWorkflowAlias(skill.name))
|
||||
: plugin.skills
|
||||
const skillDirs = copiedSkills.map((skill) => ({
|
||||
name: skill.name,
|
||||
sourceDir: skill.sourceDir,
|
||||
}))
|
||||
const promptNames = new Set<string>()
|
||||
const usedSkillNames = new Set<string>(skillDirs.map((skill) => normalizeCodexName(skill.name)))
|
||||
|
||||
const commandPromptNames = new Map<string, string>()
|
||||
for (const command of invocableCommands) {
|
||||
commandPromptNames.set(
|
||||
command.name,
|
||||
uniqueName(normalizeCodexName(command.name), promptNames),
|
||||
)
|
||||
}
|
||||
|
||||
const workflowPromptNames = new Map<string, string>()
|
||||
for (const skill of canonicalWorkflowSkills) {
|
||||
workflowPromptNames.set(
|
||||
skill.name,
|
||||
uniqueName(normalizeCodexName(skill.name), promptNames),
|
||||
)
|
||||
}
|
||||
|
||||
const promptTargets: Record<string, string> = {}
|
||||
for (const [commandName, promptName] of commandPromptNames) {
|
||||
promptTargets[normalizeCodexName(commandName)] = promptName
|
||||
}
|
||||
for (const [skillName, promptName] of workflowPromptNames) {
|
||||
promptTargets[normalizeCodexName(skillName)] = promptName
|
||||
}
|
||||
for (const alias of deprecatedWorkflowAliases) {
|
||||
const canonicalName = toCanonicalWorkflowSkillName(alias.name)
|
||||
const promptName = canonicalName ? workflowPromptNames.get(canonicalName) : undefined
|
||||
if (promptName) {
|
||||
promptTargets[normalizeCodexName(alias.name)] = promptName
|
||||
}
|
||||
}
|
||||
|
||||
const skillTargets: Record<string, string> = {}
|
||||
for (const skill of copiedSkills) {
|
||||
if (applyCompoundWorkflowModel && isCanonicalCodexWorkflowSkill(skill.name)) continue
|
||||
skillTargets[normalizeCodexName(skill.name)] = skill.name
|
||||
}
|
||||
|
||||
const invocationTargets: CodexInvocationTargets = { promptTargets, skillTargets }
|
||||
|
||||
const usedSkillNames = new Set<string>(skillDirs.map((skill) => normalizeName(skill.name)))
|
||||
const commandSkills: CodexGeneratedSkill[] = []
|
||||
const invocableCommands = plugin.commands.filter((command) => !command.disableModelInvocation)
|
||||
const prompts = invocableCommands.map((command) => {
|
||||
const promptName = uniqueName(normalizeName(command.name), promptNames)
|
||||
const commandSkill = convertCommandSkill(command, usedSkillNames)
|
||||
const promptName = commandPromptNames.get(command.name)!
|
||||
const commandSkill = convertCommandSkill(command, usedSkillNames, invocationTargets)
|
||||
commandSkills.push(commandSkill)
|
||||
const content = renderPrompt(command, commandSkill.name)
|
||||
const content = renderPrompt(command, commandSkill.name, invocationTargets)
|
||||
return { name: promptName, content }
|
||||
})
|
||||
const workflowPrompts = canonicalWorkflowSkills.map((skill) => ({
|
||||
name: workflowPromptNames.get(skill.name)!,
|
||||
content: renderWorkflowPrompt(skill),
|
||||
}))
|
||||
|
||||
const agentSkills = plugin.agents.map((agent) => convertAgent(agent, usedSkillNames))
|
||||
const agentSkills = plugin.agents.map((agent) =>
|
||||
convertAgent(agent, usedSkillNames, invocationTargets),
|
||||
)
|
||||
const generatedSkills = [...commandSkills, ...agentSkills]
|
||||
|
||||
return {
|
||||
prompts,
|
||||
prompts: [...prompts, ...workflowPrompts],
|
||||
skillDirs,
|
||||
generatedSkills,
|
||||
invocationTargets,
|
||||
mcpServers: plugin.mcpServers,
|
||||
}
|
||||
}
|
||||
|
||||
function convertAgent(agent: ClaudeAgent, usedNames: Set<string>): CodexGeneratedSkill {
|
||||
const name = uniqueName(normalizeName(agent.name), usedNames)
|
||||
function convertAgent(
|
||||
agent: ClaudeAgent,
|
||||
usedNames: Set<string>,
|
||||
invocationTargets: CodexInvocationTargets,
|
||||
): CodexGeneratedSkill {
|
||||
const name = uniqueName(normalizeCodexName(agent.name), usedNames)
|
||||
const description = sanitizeDescription(
|
||||
agent.description ?? `Converted from Claude agent ${agent.name}`,
|
||||
)
|
||||
const frontmatter: Record<string, unknown> = { name, description }
|
||||
|
||||
let body = transformContentForCodex(agent.body.trim())
|
||||
let body = transformContentForCodex(agent.body.trim(), invocationTargets)
|
||||
if (agent.capabilities && agent.capabilities.length > 0) {
|
||||
const capabilities = agent.capabilities.map((capability) => `- ${capability}`).join("\n")
|
||||
body = `## Capabilities\n${capabilities}\n\n${body}`.trim()
|
||||
@@ -59,8 +124,12 @@ function convertAgent(agent: ClaudeAgent, usedNames: Set<string>): CodexGenerate
|
||||
return { name, content }
|
||||
}
|
||||
|
||||
function convertCommandSkill(command: ClaudeCommand, usedNames: Set<string>): CodexGeneratedSkill {
|
||||
const name = uniqueName(normalizeName(command.name), usedNames)
|
||||
function convertCommandSkill(
|
||||
command: ClaudeCommand,
|
||||
usedNames: Set<string>,
|
||||
invocationTargets: CodexInvocationTargets,
|
||||
): CodexGeneratedSkill {
|
||||
const name = uniqueName(normalizeCodexName(command.name), usedNames)
|
||||
const frontmatter: Record<string, unknown> = {
|
||||
name,
|
||||
description: sanitizeDescription(
|
||||
@@ -74,95 +143,55 @@ function convertCommandSkill(command: ClaudeCommand, usedNames: Set<string>): Co
|
||||
if (command.allowedTools && command.allowedTools.length > 0) {
|
||||
sections.push(`## Allowed tools\n${command.allowedTools.map((tool) => `- ${tool}`).join("\n")}`)
|
||||
}
|
||||
// Transform Task agent calls to Codex skill references
|
||||
const transformedBody = transformTaskCalls(command.body.trim())
|
||||
const transformedBody = transformContentForCodex(command.body.trim(), invocationTargets)
|
||||
sections.push(transformedBody)
|
||||
const body = sections.filter(Boolean).join("\n\n").trim()
|
||||
const content = formatFrontmatter(frontmatter, body.length > 0 ? body : command.body)
|
||||
return { name, content }
|
||||
}
|
||||
|
||||
/**
|
||||
* Transform Claude Code content to Codex-compatible content.
|
||||
*
|
||||
* Handles multiple syntax differences:
|
||||
* 1. Task agent calls: Task agent-name(args) → Use the $agent-name skill to: args
|
||||
* 2. Slash commands: /command-name → /prompts:command-name
|
||||
* 3. Agent references: @agent-name → $agent-name skill
|
||||
*
|
||||
* This bridges the gap since Claude Code and Codex have different syntax
|
||||
* for invoking commands, agents, and skills.
|
||||
*/
|
||||
function transformContentForCodex(body: string): string {
|
||||
let result = body
|
||||
|
||||
// 1. Transform Task agent calls
|
||||
// Match: Task repo-research-analyst(feature_description)
|
||||
// Match: - Task learnings-researcher(args)
|
||||
const taskPattern = /^(\s*-?\s*)Task\s+([a-z][a-z0-9-]*)\(([^)]+)\)/gm
|
||||
result = result.replace(taskPattern, (_match, prefix: string, agentName: string, args: string) => {
|
||||
const skillName = normalizeName(agentName)
|
||||
const trimmedArgs = args.trim()
|
||||
return `${prefix}Use the $${skillName} skill to: ${trimmedArgs}`
|
||||
})
|
||||
|
||||
// 2. Transform slash command references
|
||||
// Match: /command-name or /workflows:command but NOT /path/to/file or URLs
|
||||
// Look for slash commands in contexts like "Run /command", "use /command", etc.
|
||||
// Avoid matching file paths (contain multiple slashes) or URLs (contain ://)
|
||||
const slashCommandPattern = /(?<![:\w])\/([a-z][a-z0-9_:-]*?)(?=[\s,."')\]}`]|$)/gi
|
||||
result = result.replace(slashCommandPattern, (match, commandName: string) => {
|
||||
// Skip if it looks like a file path (contains /)
|
||||
if (commandName.includes('/')) return match
|
||||
// Skip common non-command patterns
|
||||
if (['dev', 'tmp', 'etc', 'usr', 'var', 'bin', 'home'].includes(commandName)) return match
|
||||
// Transform to Codex prompt syntax
|
||||
const normalizedName = normalizeName(commandName)
|
||||
return `/prompts:${normalizedName}`
|
||||
})
|
||||
|
||||
// 3. Rewrite .claude/ paths to .codex/
|
||||
result = result
|
||||
.replace(/~\/\.claude\//g, "~/.codex/")
|
||||
.replace(/\.claude\//g, ".codex/")
|
||||
|
||||
// 4. Transform @agent-name references
|
||||
// Match: @agent-name in text (not emails)
|
||||
const agentRefPattern = /@([a-z][a-z0-9-]*-(?:agent|reviewer|researcher|analyst|specialist|oracle|sentinel|guardian|strategist))/gi
|
||||
result = result.replace(agentRefPattern, (_match, agentName: string) => {
|
||||
const skillName = normalizeName(agentName)
|
||||
return `$${skillName} skill`
|
||||
})
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// Alias for backward compatibility
|
||||
const transformTaskCalls = transformContentForCodex
|
||||
|
||||
function renderPrompt(command: ClaudeCommand, skillName: string): string {
|
||||
function renderPrompt(
|
||||
command: ClaudeCommand,
|
||||
skillName: string,
|
||||
invocationTargets: CodexInvocationTargets,
|
||||
): string {
|
||||
const frontmatter: Record<string, unknown> = {
|
||||
description: command.description,
|
||||
"argument-hint": command.argumentHint,
|
||||
}
|
||||
const instructions = `Use the $${skillName} skill for this command and follow its instructions.`
|
||||
// Transform Task calls in prompt body too (not just skill body)
|
||||
const transformedBody = transformTaskCalls(command.body)
|
||||
const transformedBody = transformContentForCodex(command.body, invocationTargets)
|
||||
const body = [instructions, "", transformedBody].join("\n").trim()
|
||||
return formatFrontmatter(frontmatter, body)
|
||||
}
|
||||
|
||||
function normalizeName(value: string): string {
|
||||
const trimmed = value.trim()
|
||||
if (!trimmed) return "item"
|
||||
const normalized = trimmed
|
||||
.toLowerCase()
|
||||
.replace(/[\\/]+/g, "-")
|
||||
.replace(/[:\s]+/g, "-")
|
||||
.replace(/[^a-z0-9_-]+/g, "-")
|
||||
.replace(/-+/g, "-")
|
||||
.replace(/^-+|-+$/g, "")
|
||||
return normalized || "item"
|
||||
function renderWorkflowPrompt(skill: ClaudeSkill): string {
|
||||
const frontmatter: Record<string, unknown> = {
|
||||
description: skill.description,
|
||||
"argument-hint": skill.argumentHint,
|
||||
}
|
||||
const body = [
|
||||
`Use the ${skill.name} skill for this workflow and follow its instructions exactly.`,
|
||||
"Treat any text after the prompt name as the workflow context to pass through.",
|
||||
].join("\n\n")
|
||||
return formatFrontmatter(frontmatter, body)
|
||||
}
|
||||
|
||||
function isCanonicalCodexWorkflowSkill(name: string): boolean {
|
||||
return name.startsWith("ce:")
|
||||
}
|
||||
|
||||
function isDeprecatedCodexWorkflowAlias(name: string): boolean {
|
||||
return name.startsWith("workflows:")
|
||||
}
|
||||
|
||||
function toCanonicalWorkflowSkillName(name: string): string | null {
|
||||
if (!isDeprecatedCodexWorkflowAlias(name)) return null
|
||||
return `ce:${name.slice("workflows:".length)}`
|
||||
}
|
||||
|
||||
function shouldApplyCompoundWorkflowModel(plugin: ClaudePlugin): boolean {
|
||||
return plugin.manifest.name === "compound-engineering"
|
||||
}
|
||||
|
||||
function sanitizeDescription(value: string, maxLength = CODEX_DESCRIPTION_MAX_LENGTH): string {
|
||||
|
||||
@@ -37,12 +37,17 @@ async function loadPersonalSkills(skillsDir: string): Promise<ClaudeSkill[]> {
|
||||
|
||||
try {
|
||||
await fs.access(skillPath)
|
||||
const raw = await fs.readFile(skillPath, "utf8")
|
||||
const { data } = parseFrontmatter(raw)
|
||||
// Resolve symlink to get the actual source directory
|
||||
const sourceDir = entry.isSymbolicLink()
|
||||
? await fs.realpath(entryPath)
|
||||
: entryPath
|
||||
skills.push({
|
||||
name: entry.name,
|
||||
description: data.description as string | undefined,
|
||||
argumentHint: data["argument-hint"] as string | undefined,
|
||||
disableModelInvocation: data["disable-model-invocation"] === true ? true : undefined,
|
||||
sourceDir,
|
||||
skillPath,
|
||||
})
|
||||
|
||||
@@ -110,6 +110,7 @@ async function loadSkills(skillsDirs: string[]): Promise<ClaudeSkill[]> {
|
||||
skills.push({
|
||||
name,
|
||||
description: data.description as string | undefined,
|
||||
argumentHint: data["argument-hint"] as string | undefined,
|
||||
disableModelInvocation,
|
||||
sourceDir: path.dirname(file),
|
||||
skillPath: file,
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import { promises as fs } from "fs"
|
||||
import path from "path"
|
||||
import { backupFile, copyDir, ensureDir, writeText } from "../utils/files"
|
||||
import { backupFile, ensureDir, readText, writeText } from "../utils/files"
|
||||
import type { CodexBundle } from "../types/codex"
|
||||
import type { ClaudeMcpServer } from "../types/claude"
|
||||
import { transformContentForCodex } from "../utils/codex-content"
|
||||
|
||||
export async function writeCodexBundle(outputRoot: string, bundle: CodexBundle): Promise<void> {
|
||||
const codexRoot = resolveCodexRoot(outputRoot)
|
||||
@@ -17,7 +19,11 @@ export async function writeCodexBundle(outputRoot: string, bundle: CodexBundle):
|
||||
if (bundle.skillDirs.length > 0) {
|
||||
const skillsRoot = path.join(codexRoot, "skills")
|
||||
for (const skill of bundle.skillDirs) {
|
||||
await copyDir(skill.sourceDir, path.join(skillsRoot, skill.name))
|
||||
await copyCodexSkillDir(
|
||||
skill.sourceDir,
|
||||
path.join(skillsRoot, skill.name),
|
||||
bundle.invocationTargets,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,6 +45,36 @@ export async function writeCodexBundle(outputRoot: string, bundle: CodexBundle):
|
||||
}
|
||||
}
|
||||
|
||||
async function copyCodexSkillDir(
|
||||
sourceDir: string,
|
||||
targetDir: string,
|
||||
invocationTargets?: CodexBundle["invocationTargets"],
|
||||
): Promise<void> {
|
||||
await ensureDir(targetDir)
|
||||
const entries = await fs.readdir(sourceDir, { withFileTypes: true })
|
||||
|
||||
for (const entry of entries) {
|
||||
const sourcePath = path.join(sourceDir, entry.name)
|
||||
const targetPath = path.join(targetDir, entry.name)
|
||||
|
||||
if (entry.isDirectory()) {
|
||||
await copyCodexSkillDir(sourcePath, targetPath, invocationTargets)
|
||||
continue
|
||||
}
|
||||
|
||||
if (!entry.isFile()) continue
|
||||
|
||||
if (entry.name === "SKILL.md") {
|
||||
const content = await readText(sourcePath)
|
||||
await writeText(targetPath, transformContentForCodex(content, invocationTargets))
|
||||
continue
|
||||
}
|
||||
|
||||
await ensureDir(path.dirname(targetPath))
|
||||
await fs.copyFile(sourcePath, targetPath)
|
||||
}
|
||||
}
|
||||
|
||||
function resolveCodexRoot(outputRoot: string): string {
|
||||
return path.basename(outputRoot) === ".codex" ? outputRoot : path.join(outputRoot, ".codex")
|
||||
}
|
||||
|
||||
@@ -47,6 +47,7 @@ export type ClaudeCommand = {
|
||||
export type ClaudeSkill = {
|
||||
name: string
|
||||
description?: string
|
||||
argumentHint?: string
|
||||
disableModelInvocation?: boolean
|
||||
sourceDir: string
|
||||
skillPath: string
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { ClaudeMcpServer } from "./claude"
|
||||
import type { CodexInvocationTargets } from "../utils/codex-content"
|
||||
|
||||
export type CodexPrompt = {
|
||||
name: string
|
||||
@@ -19,5 +20,6 @@ export type CodexBundle = {
|
||||
prompts: CodexPrompt[]
|
||||
skillDirs: CodexSkillDir[]
|
||||
generatedSkills: CodexGeneratedSkill[]
|
||||
invocationTargets?: CodexInvocationTargets
|
||||
mcpServers?: Record<string, ClaudeMcpServer>
|
||||
}
|
||||
|
||||
75
src/utils/codex-content.ts
Normal file
75
src/utils/codex-content.ts
Normal file
@@ -0,0 +1,75 @@
|
||||
export type CodexInvocationTargets = {
|
||||
promptTargets: Record<string, string>
|
||||
skillTargets: Record<string, string>
|
||||
}
|
||||
|
||||
/**
|
||||
* Transform Claude Code content to Codex-compatible content.
|
||||
*
|
||||
* Handles multiple syntax differences:
|
||||
* 1. Task agent calls: Task agent-name(args) -> Use the $agent-name skill to: args
|
||||
* 2. Slash command references:
|
||||
* - known prompt entrypoints -> /prompts:prompt-name
|
||||
* - known skills -> the exact skill name
|
||||
* - unknown slash refs -> /prompts:command-name
|
||||
* 3. Agent references: @agent-name -> $agent-name skill
|
||||
* 4. Claude config paths: .claude/ -> .codex/
|
||||
*/
|
||||
export function transformContentForCodex(
|
||||
body: string,
|
||||
targets?: CodexInvocationTargets,
|
||||
): string {
|
||||
let result = body
|
||||
const promptTargets = targets?.promptTargets ?? {}
|
||||
const skillTargets = targets?.skillTargets ?? {}
|
||||
|
||||
const taskPattern = /^(\s*-?\s*)Task\s+([a-z][a-z0-9:-]*)\(([^)]+)\)/gm
|
||||
result = result.replace(taskPattern, (_match, prefix: string, agentName: string, args: string) => {
|
||||
// For namespaced calls like "compound-engineering:research:repo-research-analyst",
|
||||
// use only the final segment as the skill name.
|
||||
const finalSegment = agentName.includes(":") ? agentName.split(":").pop()! : agentName
|
||||
const skillName = normalizeCodexName(finalSegment)
|
||||
const trimmedArgs = args.trim()
|
||||
return `${prefix}Use the $${skillName} skill to: ${trimmedArgs}`
|
||||
})
|
||||
|
||||
const slashCommandPattern = /(?<![:\w])\/([a-z][a-z0-9_:-]*?)(?=[\s,."')\]}`]|$)/gi
|
||||
result = result.replace(slashCommandPattern, (match, commandName: string) => {
|
||||
if (commandName.includes("/")) return match
|
||||
if (["dev", "tmp", "etc", "usr", "var", "bin", "home"].includes(commandName)) return match
|
||||
|
||||
const normalizedName = normalizeCodexName(commandName)
|
||||
if (promptTargets[normalizedName]) {
|
||||
return `/prompts:${promptTargets[normalizedName]}`
|
||||
}
|
||||
if (skillTargets[normalizedName]) {
|
||||
return `the ${skillTargets[normalizedName]} skill`
|
||||
}
|
||||
return `/prompts:${normalizedName}`
|
||||
})
|
||||
|
||||
result = result
|
||||
.replace(/~\/\.claude\//g, "~/.codex/")
|
||||
.replace(/\.claude\//g, ".codex/")
|
||||
|
||||
const agentRefPattern = /@([a-z][a-z0-9-]*-(?:agent|reviewer|researcher|analyst|specialist|oracle|sentinel|guardian|strategist))/gi
|
||||
result = result.replace(agentRefPattern, (_match, agentName: string) => {
|
||||
const skillName = normalizeCodexName(agentName)
|
||||
return `$${skillName} skill`
|
||||
})
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
export function normalizeCodexName(value: string): string {
|
||||
const trimmed = value.trim()
|
||||
if (!trimmed) return "item"
|
||||
const normalized = trimmed
|
||||
.toLowerCase()
|
||||
.replace(/[\\/]+/g, "-")
|
||||
.replace(/[:\s]+/g, "-")
|
||||
.replace(/[^a-z0-9_-]+/g, "-")
|
||||
.replace(/-+/g, "-")
|
||||
.replace(/^-+|-+$/g, "")
|
||||
return normalized || "item"
|
||||
}
|
||||
@@ -43,4 +43,22 @@ describe("loadClaudeHome", () => {
|
||||
expect(config.commands?.find((command) => command.name === "custom-command")?.allowedTools).toEqual(["Bash", "Read"])
|
||||
expect(config.mcpServers.context7?.url).toBe("https://mcp.context7.com/mcp")
|
||||
})
|
||||
|
||||
test("keeps personal skill directory names stable even when frontmatter name differs", async () => {
|
||||
const tempHome = await fs.mkdtemp(path.join(os.tmpdir(), "claude-home-skill-name-"))
|
||||
const skillDir = path.join(tempHome, "skills", "reviewer")
|
||||
|
||||
await fs.mkdir(skillDir, { recursive: true })
|
||||
await fs.writeFile(
|
||||
path.join(skillDir, "SKILL.md"),
|
||||
"---\nname: ce:plan\ndescription: Reviewer skill\nargument-hint: \"[topic]\"\n---\nReview things.\n",
|
||||
)
|
||||
|
||||
const config = await loadClaudeHome(tempHome)
|
||||
|
||||
expect(config.skills).toHaveLength(1)
|
||||
expect(config.skills[0]?.name).toBe("reviewer")
|
||||
expect(config.skills[0]?.description).toBe("Reviewer skill")
|
||||
expect(config.skills[0]?.argumentHint).toBe("[topic]")
|
||||
})
|
||||
})
|
||||
|
||||
@@ -31,6 +31,7 @@ const fixturePlugin: ClaudePlugin = {
|
||||
{
|
||||
name: "existing-skill",
|
||||
description: "Existing skill",
|
||||
argumentHint: "[ITEM]",
|
||||
sourceDir: "/tmp/plugin/skills/existing-skill",
|
||||
skillPath: "/tmp/plugin/skills/existing-skill/SKILL.md",
|
||||
},
|
||||
@@ -78,6 +79,81 @@ describe("convertClaudeToCodex", () => {
|
||||
expect(parsedSkill.body).toContain("Threat modeling")
|
||||
})
|
||||
|
||||
test("generates prompt wrappers for canonical ce workflow skills and omits workflows aliases", () => {
|
||||
const plugin: ClaudePlugin = {
|
||||
...fixturePlugin,
|
||||
manifest: { name: "compound-engineering", version: "1.0.0" },
|
||||
commands: [],
|
||||
agents: [],
|
||||
skills: [
|
||||
{
|
||||
name: "ce:plan",
|
||||
description: "Planning workflow",
|
||||
argumentHint: "[feature]",
|
||||
sourceDir: "/tmp/plugin/skills/ce-plan",
|
||||
skillPath: "/tmp/plugin/skills/ce-plan/SKILL.md",
|
||||
},
|
||||
{
|
||||
name: "workflows:plan",
|
||||
description: "Deprecated planning alias",
|
||||
argumentHint: "[feature]",
|
||||
sourceDir: "/tmp/plugin/skills/workflows-plan",
|
||||
skillPath: "/tmp/plugin/skills/workflows-plan/SKILL.md",
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
const bundle = convertClaudeToCodex(plugin, {
|
||||
agentMode: "subagent",
|
||||
inferTemperature: false,
|
||||
permissions: "none",
|
||||
})
|
||||
|
||||
expect(bundle.prompts).toHaveLength(1)
|
||||
expect(bundle.prompts[0]?.name).toBe("ce-plan")
|
||||
|
||||
const parsedPrompt = parseFrontmatter(bundle.prompts[0]!.content)
|
||||
expect(parsedPrompt.data.description).toBe("Planning workflow")
|
||||
expect(parsedPrompt.data["argument-hint"]).toBe("[feature]")
|
||||
expect(parsedPrompt.body).toContain("Use the ce:plan skill")
|
||||
|
||||
expect(bundle.skillDirs.map((skill) => skill.name)).toEqual(["ce:plan"])
|
||||
})
|
||||
|
||||
test("does not apply compound workflow canonicalization to other plugins", () => {
|
||||
const plugin: ClaudePlugin = {
|
||||
...fixturePlugin,
|
||||
manifest: { name: "other-plugin", version: "1.0.0" },
|
||||
commands: [],
|
||||
agents: [],
|
||||
skills: [
|
||||
{
|
||||
name: "ce:plan",
|
||||
description: "Custom CE-namespaced skill",
|
||||
argumentHint: "[feature]",
|
||||
sourceDir: "/tmp/plugin/skills/ce-plan",
|
||||
skillPath: "/tmp/plugin/skills/ce-plan/SKILL.md",
|
||||
},
|
||||
{
|
||||
name: "workflows:plan",
|
||||
description: "Custom workflows-namespaced skill",
|
||||
argumentHint: "[feature]",
|
||||
sourceDir: "/tmp/plugin/skills/workflows-plan",
|
||||
skillPath: "/tmp/plugin/skills/workflows-plan/SKILL.md",
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
const bundle = convertClaudeToCodex(plugin, {
|
||||
agentMode: "subagent",
|
||||
inferTemperature: false,
|
||||
permissions: "none",
|
||||
})
|
||||
|
||||
expect(bundle.prompts).toHaveLength(0)
|
||||
expect(bundle.skillDirs.map((skill) => skill.name)).toEqual(["ce:plan", "workflows:plan"])
|
||||
})
|
||||
|
||||
test("passes through MCP servers", () => {
|
||||
const bundle = convertClaudeToCodex(fixturePlugin, {
|
||||
agentMode: "subagent",
|
||||
@@ -131,6 +207,47 @@ Task best-practices-researcher(topic)`,
|
||||
expect(parsed.body).not.toContain("Task learnings-researcher")
|
||||
})
|
||||
|
||||
test("transforms namespaced Task agent calls to skill references using final segment", () => {
|
||||
const plugin: ClaudePlugin = {
|
||||
...fixturePlugin,
|
||||
commands: [
|
||||
{
|
||||
name: "plan",
|
||||
description: "Planning with namespaced agents",
|
||||
body: `Run these agents in parallel:
|
||||
|
||||
- Task compound-engineering:research:repo-research-analyst(feature_description)
|
||||
- Task compound-engineering:research:learnings-researcher(feature_description)
|
||||
|
||||
Then consolidate findings.
|
||||
|
||||
Task compound-engineering:review:security-reviewer(code_diff)`,
|
||||
sourcePath: "/tmp/plugin/commands/plan.md",
|
||||
},
|
||||
],
|
||||
agents: [],
|
||||
skills: [],
|
||||
}
|
||||
|
||||
const bundle = convertClaudeToCodex(plugin, {
|
||||
agentMode: "subagent",
|
||||
inferTemperature: false,
|
||||
permissions: "none",
|
||||
})
|
||||
|
||||
const commandSkill = bundle.generatedSkills.find((s) => s.name === "plan")
|
||||
expect(commandSkill).toBeDefined()
|
||||
const parsed = parseFrontmatter(commandSkill!.content)
|
||||
|
||||
// Namespaced Task calls should use only the final segment as the skill name
|
||||
expect(parsed.body).toContain("Use the $repo-research-analyst skill to: feature_description")
|
||||
expect(parsed.body).toContain("Use the $learnings-researcher skill to: feature_description")
|
||||
expect(parsed.body).toContain("Use the $security-reviewer skill to: code_diff")
|
||||
|
||||
// Original namespaced Task syntax should not remain
|
||||
expect(parsed.body).not.toContain("Task compound-engineering:")
|
||||
})
|
||||
|
||||
test("transforms slash commands to prompts syntax", () => {
|
||||
const plugin: ClaudePlugin = {
|
||||
...fixturePlugin,
|
||||
@@ -172,6 +289,61 @@ Don't confuse with file paths like /tmp/output.md or /dev/null.`,
|
||||
expect(parsed.body).toContain("/dev/null")
|
||||
})
|
||||
|
||||
test("transforms canonical workflow slash commands to Codex prompt references", () => {
|
||||
const plugin: ClaudePlugin = {
|
||||
...fixturePlugin,
|
||||
manifest: { name: "compound-engineering", version: "1.0.0" },
|
||||
commands: [
|
||||
{
|
||||
name: "review",
|
||||
description: "Review command",
|
||||
body: `After the brainstorm, run /ce:plan.
|
||||
|
||||
If planning is complete, continue with /ce:work.`,
|
||||
sourcePath: "/tmp/plugin/commands/review.md",
|
||||
},
|
||||
],
|
||||
agents: [],
|
||||
skills: [
|
||||
{
|
||||
name: "ce:plan",
|
||||
description: "Planning workflow",
|
||||
argumentHint: "[feature]",
|
||||
sourceDir: "/tmp/plugin/skills/ce-plan",
|
||||
skillPath: "/tmp/plugin/skills/ce-plan/SKILL.md",
|
||||
},
|
||||
{
|
||||
name: "ce:work",
|
||||
description: "Implementation workflow",
|
||||
argumentHint: "[feature]",
|
||||
sourceDir: "/tmp/plugin/skills/ce-work",
|
||||
skillPath: "/tmp/plugin/skills/ce-work/SKILL.md",
|
||||
},
|
||||
{
|
||||
name: "workflows:work",
|
||||
description: "Deprecated implementation alias",
|
||||
argumentHint: "[feature]",
|
||||
sourceDir: "/tmp/plugin/skills/workflows-work",
|
||||
skillPath: "/tmp/plugin/skills/workflows-work/SKILL.md",
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
const bundle = convertClaudeToCodex(plugin, {
|
||||
agentMode: "subagent",
|
||||
inferTemperature: false,
|
||||
permissions: "none",
|
||||
})
|
||||
|
||||
const commandSkill = bundle.generatedSkills.find((s) => s.name === "review")
|
||||
expect(commandSkill).toBeDefined()
|
||||
const parsed = parseFrontmatter(commandSkill!.content)
|
||||
|
||||
expect(parsed.body).toContain("/prompts:ce-plan")
|
||||
expect(parsed.body).toContain("/prompts:ce-work")
|
||||
expect(parsed.body).not.toContain("the ce:plan skill")
|
||||
})
|
||||
|
||||
test("excludes commands with disable-model-invocation from prompts and skills", () => {
|
||||
const plugin: ClaudePlugin = {
|
||||
...fixturePlugin,
|
||||
|
||||
@@ -105,4 +105,105 @@ describe("writeCodexBundle", () => {
|
||||
const backupContent = await fs.readFile(path.join(codexRoot, backupFileName!), "utf8")
|
||||
expect(backupContent).toBe(originalContent)
|
||||
})
|
||||
|
||||
test("transforms copied SKILL.md files using Codex invocation targets", async () => {
|
||||
const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), "codex-skill-transform-"))
|
||||
const sourceSkillDir = path.join(tempRoot, "source-skill")
|
||||
await fs.mkdir(sourceSkillDir, { recursive: true })
|
||||
await fs.writeFile(
|
||||
path.join(sourceSkillDir, "SKILL.md"),
|
||||
`---
|
||||
name: ce:brainstorm
|
||||
description: Brainstorm workflow
|
||||
---
|
||||
|
||||
Continue with /ce:plan when ready.
|
||||
Or use /workflows:plan if you're following an older doc.
|
||||
Use /deepen-plan for deeper research.
|
||||
`,
|
||||
)
|
||||
await fs.writeFile(
|
||||
path.join(sourceSkillDir, "notes.md"),
|
||||
"Reference docs still mention /ce:plan here.\n",
|
||||
)
|
||||
|
||||
const bundle: CodexBundle = {
|
||||
prompts: [],
|
||||
skillDirs: [{ name: "ce:brainstorm", sourceDir: sourceSkillDir }],
|
||||
generatedSkills: [],
|
||||
invocationTargets: {
|
||||
promptTargets: {
|
||||
"ce-plan": "ce-plan",
|
||||
"workflows-plan": "ce-plan",
|
||||
"deepen-plan": "deepen-plan",
|
||||
},
|
||||
skillTargets: {},
|
||||
},
|
||||
}
|
||||
|
||||
await writeCodexBundle(tempRoot, bundle)
|
||||
|
||||
const installedSkill = await fs.readFile(
|
||||
path.join(tempRoot, ".codex", "skills", "ce:brainstorm", "SKILL.md"),
|
||||
"utf8",
|
||||
)
|
||||
expect(installedSkill).toContain("/prompts:ce-plan")
|
||||
expect(installedSkill).not.toContain("/workflows:plan")
|
||||
expect(installedSkill).toContain("/prompts:deepen-plan")
|
||||
|
||||
const notes = await fs.readFile(
|
||||
path.join(tempRoot, ".codex", "skills", "ce:brainstorm", "notes.md"),
|
||||
"utf8",
|
||||
)
|
||||
expect(notes).toContain("/ce:plan")
|
||||
})
|
||||
|
||||
test("transforms namespaced Task calls in copied SKILL.md files", async () => {
|
||||
const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), "codex-ns-task-"))
|
||||
const sourceSkillDir = path.join(tempRoot, "source-skill")
|
||||
await fs.mkdir(sourceSkillDir, { recursive: true })
|
||||
await fs.writeFile(
|
||||
path.join(sourceSkillDir, "SKILL.md"),
|
||||
`---
|
||||
name: ce:plan
|
||||
description: Planning workflow
|
||||
---
|
||||
|
||||
Run these research agents:
|
||||
|
||||
- Task compound-engineering:research:repo-research-analyst(feature_description)
|
||||
- Task compound-engineering:research:learnings-researcher(feature_description)
|
||||
|
||||
Also run bare agents:
|
||||
|
||||
- Task best-practices-researcher(topic)
|
||||
`,
|
||||
)
|
||||
|
||||
const bundle: CodexBundle = {
|
||||
prompts: [],
|
||||
skillDirs: [{ name: "ce:plan", sourceDir: sourceSkillDir }],
|
||||
generatedSkills: [],
|
||||
invocationTargets: {
|
||||
promptTargets: {},
|
||||
skillTargets: {},
|
||||
},
|
||||
}
|
||||
|
||||
await writeCodexBundle(tempRoot, bundle)
|
||||
|
||||
const installedSkill = await fs.readFile(
|
||||
path.join(tempRoot, ".codex", "skills", "ce:plan", "SKILL.md"),
|
||||
"utf8",
|
||||
)
|
||||
|
||||
// Namespaced Task calls should be rewritten using the final segment
|
||||
expect(installedSkill).toContain("Use the $repo-research-analyst skill to: feature_description")
|
||||
expect(installedSkill).toContain("Use the $learnings-researcher skill to: feature_description")
|
||||
expect(installedSkill).not.toContain("Task compound-engineering:")
|
||||
|
||||
// Bare Task calls should still be rewritten
|
||||
expect(installedSkill).toContain("Use the $best-practices-researcher skill to: topic")
|
||||
expect(installedSkill).not.toContain("Task best-practices-researcher")
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user