From a51272dd6f37088806e303b99a83efaf0be37538 Mon Sep 17 00:00:00 2001 From: YuXiang Zhu Date: Wed, 27 May 2026 22:58:16 +0800 Subject: [PATCH] fix(codex): rewrite unsupported tool references --- src/core/command-generation/adapters/codex.ts | 5 +- src/core/init.ts | 14 ++++- src/core/update.ts | 23 +++++-- src/core/workspace/skills.ts | 17 +++++- src/utils/command-references.ts | 60 +++++++++++++++++++ test/core/command-generation/adapters.test.ts | 13 ++++ test/utils/command-references.test.ts | 23 ++++++- 7 files changed, 142 insertions(+), 13 deletions(-) diff --git a/src/core/command-generation/adapters/codex.ts b/src/core/command-generation/adapters/codex.ts index 64e73550b..449a4c5bd 100644 --- a/src/core/command-generation/adapters/codex.ts +++ b/src/core/command-generation/adapters/codex.ts @@ -10,6 +10,7 @@ import os from 'os'; import path from 'path'; import type { CommandContent, ToolCommandAdapter } from '../types.js'; +import { transformCodexRuntimeToolReferences } from '../../../utils/command-references.js'; /** * Returns the Codex home directory. @@ -33,12 +34,14 @@ export const codexAdapter: ToolCommandAdapter = { }, formatFile(content: CommandContent): string { + const transformedBody = transformCodexRuntimeToolReferences(content.body); + return `--- description: ${content.description} argument-hint: command arguments --- -${content.body} +${transformedBody} `; }, }; diff --git a/src/core/init.ts b/src/core/init.ts index aa38408f2..de7feb875 100644 --- a/src/core/init.ts +++ b/src/core/init.ts @@ -11,7 +11,10 @@ import ora from 'ora'; import * as fs from 'fs'; import { createRequire } from 'module'; import { FileSystemUtils } from '../utils/file-system.js'; -import { transformToHyphenCommands } from '../utils/command-references.js'; +import { + transformCodexRuntimeToolReferences, + transformToHyphenCommands, +} from '../utils/command-references.js'; import { AI_TOOLS, OPENSPEC_DIR_NAME, @@ -537,8 +540,13 @@ export class InitCommand { const skillFile = path.join(skillDir, 'SKILL.md'); // Generate SKILL.md content with YAML frontmatter including generatedBy - // Use hyphen-based command references for tools where filename = command name - const transformer = (tool.value === 'opencode' || tool.value === 'pi') ? transformToHyphenCommands : undefined; + // Apply tool-specific rewrites for generated skill content. + const transformer = + tool.value === 'codex' + ? transformCodexRuntimeToolReferences + : tool.value === 'opencode' || tool.value === 'pi' + ? transformToHyphenCommands + : undefined; const skillContent = generateSkillContent(template, OPENSPEC_VERSION, transformer); // Write the skill file diff --git a/src/core/update.ts b/src/core/update.ts index e1582cd5b..69eeff831 100644 --- a/src/core/update.ts +++ b/src/core/update.ts @@ -11,7 +11,10 @@ import ora from 'ora'; import * as fs from 'fs'; import { createRequire } from 'module'; import { FileSystemUtils } from '../utils/file-system.js'; -import { transformToHyphenCommands } from '../utils/command-references.js'; +import { + transformCodexRuntimeToolReferences, + transformToHyphenCommands, +} from '../utils/command-references.js'; import { AI_TOOLS, OPENSPEC_DIR_NAME } from './config.js'; import { generateCommands, @@ -196,8 +199,13 @@ export class UpdateCommand { const skillDir = path.join(skillsDir, dirName); const skillFile = path.join(skillDir, 'SKILL.md'); - // Use hyphen-based command references for OpenCode - const transformer = (tool.value === 'opencode' || tool.value === 'pi') ? transformToHyphenCommands : undefined; + // Apply tool-specific rewrites for generated skill content. + const transformer = + tool.value === 'codex' + ? transformCodexRuntimeToolReferences + : tool.value === 'opencode' || tool.value === 'pi' + ? transformToHyphenCommands + : undefined; const skillContent = generateSkillContent(template, OPENSPEC_VERSION, transformer); await FileSystemUtils.writeFile(skillFile, skillContent); } @@ -690,8 +698,13 @@ export class UpdateCommand { const skillDir = path.join(skillsDir, dirName); const skillFile = path.join(skillDir, 'SKILL.md'); - // Use hyphen-based command references for OpenCode - const transformer = (tool.value === 'opencode' || tool.value === 'pi') ? transformToHyphenCommands : undefined; + // Apply tool-specific rewrites for generated skill content. + const transformer = + tool.value === 'codex' + ? transformCodexRuntimeToolReferences + : tool.value === 'opencode' || tool.value === 'pi' + ? transformToHyphenCommands + : undefined; const skillContent = generateSkillContent(template, OPENSPEC_VERSION, transformer); await FileSystemUtils.writeFile(skillFile, skillContent); } diff --git a/src/core/workspace/skills.ts b/src/core/workspace/skills.ts index 9caea04a9..d85d11dc8 100644 --- a/src/core/workspace/skills.ts +++ b/src/core/workspace/skills.ts @@ -2,7 +2,10 @@ import * as nodeFs from 'node:fs'; import { createRequire } from 'node:module'; import { FileSystemUtils } from '../../utils/file-system.js'; -import { transformToHyphenCommands } from '../../utils/command-references.js'; +import { + transformCodexRuntimeToolReferences, + transformToHyphenCommands, +} from '../../utils/command-references.js'; import { AI_TOOLS, type AIToolOption } from '../config.js'; import { getGlobalConfig, type Delivery, type Profile } from '../global-config.js'; import { getProfileWorkflows } from '../profiles.js'; @@ -354,7 +357,11 @@ export async function generateWorkspaceAgentSkills( try { const skillsDir = getWorkspaceSkillDirectoryForTool(workspaceRoot, tool); const transformer = - tool.value === 'opencode' || tool.value === 'pi' ? transformToHyphenCommands : undefined; + tool.value === 'codex' + ? transformCodexRuntimeToolReferences + : tool.value === 'opencode' || tool.value === 'pi' + ? transformToHyphenCommands + : undefined; for (const { template, dirName } of skillTemplates) { const skillFile = FileSystemUtils.joinPath(skillsDir, dirName, 'SKILL.md'); @@ -466,7 +473,11 @@ export async function updateWorkspaceAgentSkills( try { const skillsDir = getWorkspaceSkillDirectoryForTool(workspaceRoot, tool); const transformer = - tool.value === 'opencode' || tool.value === 'pi' ? transformToHyphenCommands : undefined; + tool.value === 'codex' + ? transformCodexRuntimeToolReferences + : tool.value === 'opencode' || tool.value === 'pi' + ? transformToHyphenCommands + : undefined; for (const { template, dirName } of skillTemplates) { const skillFile = FileSystemUtils.joinPath(skillsDir, dirName, 'SKILL.md'); diff --git a/src/utils/command-references.ts b/src/utils/command-references.ts index bfa49b9ff..35e6151e2 100644 --- a/src/utils/command-references.ts +++ b/src/utils/command-references.ts @@ -18,3 +18,63 @@ export function transformToHyphenCommands(text: string): string { return text.replace(/\/opsx:/g, '/opsx-'); } + +/** + * Rewrites generated workflow instructions that name Claude-style tools into + * runtime-neutral instructions Codex can follow without attempting missing tools. + */ +export function transformCodexRuntimeToolReferences(text: string): string { + return text + .replace( + /Use the \*\*AskUserQuestion tool\*\* \(open-ended, no preset options\) to ask:/g, + 'Ask the user in chat and wait for their reply:' + ) + .replace( + /Use \*\*AskUserQuestion tool\*\* with multi-select to let user choose changes:/g, + 'Ask the user in chat to choose one or more changes from a concise numbered list:' + ) + .replace( + /Use \*\*AskUserQuestion tool\*\* with a single confirmation:/g, + 'Ask the user in chat for a single confirmation:' + ) + .replace( + /Use the \*\*TodoWrite tool\*\* to track progress through the artifacts\./g, + "Track progress using Codex's native plan/checklist mechanism if available; otherwise keep a concise visible checklist." + ) + .replace( + /[Uu]se \*\*AskUserQuestion tool\*\*/g, + (match) => + match.startsWith('Use') + ? 'Ask the user in chat and wait for their reply' + : 'ask the user in chat and wait for their reply' + ) + .replace( + /[Uu]se the \*\*AskUserQuestion tool\*\*/g, + (match) => + match.startsWith('Use') + ? 'Ask the user in chat and wait for their reply' + : 'ask the user in chat and wait for their reply' + ) + .replace( + /[Uu]se \*\*TodoWrite tool\*\*/g, + (match) => + match.startsWith('Use') + ? "Use Codex's native plan/checklist mechanism if available" + : "use Codex's native plan/checklist mechanism if available" + ) + .replace( + /[Uu]se the \*\*TodoWrite tool\*\*/g, + (match) => + match.startsWith('Use') + ? "Use Codex's native plan/checklist mechanism if available" + : "use Codex's native plan/checklist mechanism if available" + ) + .replace( + /use Task tool \(subagent_type: "general-purpose", prompt: "Use Skill tool to invoke openspec-sync-specs for change ''\. Delta spec analysis: "\)/g, + "ask the agent to invoke openspec-sync-specs for change '' with the analyzed delta spec summary" + ) + .replace( + /Use Task tool \(subagent_type: "general-purpose", prompt: "([^"]+)"\)/g, + 'Use the agent native delegation or skill mechanism if available; otherwise perform this step directly: $1' + ); +} diff --git a/test/core/command-generation/adapters.test.ts b/test/core/command-generation/adapters.test.ts index b91dc024f..c9d3cc623 100644 --- a/test/core/command-generation/adapters.test.ts +++ b/test/core/command-generation/adapters.test.ts @@ -320,6 +320,19 @@ describe('command-generation/adapters', () => { expect(output).toContain('---\n\n'); expect(output).toContain('This is the command body.'); }); + + it('should replace unsupported runtime tool references', () => { + const output = codexAdapter.formatFile({ + ...sampleContent, + body: 'Use the **AskUserQuestion tool** to clarify.\nUse the **TodoWrite tool** to track progress.\nUse Task tool (subagent_type: "general-purpose", prompt: "sync").', + }); + + expect(output).not.toContain('AskUserQuestion'); + expect(output).not.toContain('TodoWrite'); + expect(output).not.toContain('Task tool'); + expect(output).not.toContain('subagent_type'); + expect(output).toContain('Ask the user in chat and wait for their reply'); + }); }); describe('codebuddyAdapter', () => { diff --git a/test/utils/command-references.test.ts b/test/utils/command-references.test.ts index c7ff2ed85..9b2af292f 100644 --- a/test/utils/command-references.test.ts +++ b/test/utils/command-references.test.ts @@ -1,5 +1,8 @@ import { describe, it, expect } from 'vitest'; -import { transformToHyphenCommands } from '../../src/utils/command-references.js'; +import { + transformCodexRuntimeToolReferences, + transformToHyphenCommands, +} from '../../src/utils/command-references.js'; describe('transformToHyphenCommands', () => { describe('basic transformations', () => { @@ -81,3 +84,21 @@ Finally /opsx-apply to implement`; } }); }); + +describe('transformCodexRuntimeToolReferences', () => { + it('should replace Claude-style tool references with Codex-safe instructions', () => { + const input = `Use the **AskUserQuestion tool** (open-ended, no preset options) to ask: +Use the **TodoWrite tool** to track progress through the artifacts. +If user chooses sync, use Task tool (subagent_type: "general-purpose", prompt: "Use Skill tool to invoke openspec-sync-specs for change ''. Delta spec analysis: "). Proceed to archive regardless of choice.`; + + const output = transformCodexRuntimeToolReferences(input); + + expect(output).toContain('Ask the user in chat and wait for their reply:'); + expect(output).toContain("Track progress using Codex's native plan/checklist mechanism"); + expect(output).toContain('invoke openspec-sync-specs'); + expect(output).not.toContain('AskUserQuestion'); + expect(output).not.toContain('TodoWrite'); + expect(output).not.toContain('Task tool'); + expect(output).not.toContain('subagent_type'); + }); +});