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:
Kieran Klaassen
2026-03-16 21:19:03 -07:00
committed by GitHub
parent 8c9f905859
commit 82c1fe86df
21 changed files with 670 additions and 149 deletions

View File

@@ -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 {

View File

@@ -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,
})

View File

@@ -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,

View 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")
}

View File

@@ -47,6 +47,7 @@ export type ClaudeCommand = {
export type ClaudeSkill = {
name: string
description?: string
argumentHint?: string
disableModelInvocation?: boolean
sourceDir: string
skillPath: string

View File

@@ -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>
}

View 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"
}