Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -148,3 +148,9 @@ vite.config.ts.timestamp-*

output/
plugins/codex/.generated/

# Development workflow
.dev-flow/

# IDE
.idea/
21 changes: 21 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ they already have.
- `/codex:review` for a normal read-only Codex review
- `/codex:adversarial-review` for a steerable challenge review
- `/codex:rescue`, `/codex:status`, `/codex:result`, and `/codex:cancel` to delegate work and manage background jobs
- `/codex:run-skill` to invoke a Codex skill through the shared runtime

## Requirements

Expand Down Expand Up @@ -202,6 +203,26 @@ Examples:
/codex:cancel task-abc123
```

### `/codex:run-skill`

Runs a Codex skill through the shared runtime. Use it to invoke any skill installed in your local Codex environment.

Use it when you want:

- to invoke a specific Codex skill from inside Claude Code
- to list the skills available in your local Codex installation

Examples:

```bash
/codex:run-skill --list
/codex:run-skill --skill ui-ux-pro-max design a landing page
/codex:run-skill --skill xlfoundry-plan --background 规划一个新功能
/codex:run-skill --skill my-write-skill --write create new files
```

This command reads the local skill definition from `~/.codex/skills/<name>/SKILL.md`, includes the skill directory path for access to bundled resources (scripts, references, etc.), and sends everything to Codex along with your prompt. Results are returned to Claude Code. Use `--write` to allow the skill to modify files (default is read-only).

### `/codex:setup`

Checks whether Codex is installed and authenticated.
Expand Down
70 changes: 70 additions & 0 deletions plugins/codex/commands/run-skill.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
---
description: Run a Codex skill through the shared runtime
argument-hint: '[--skill <name>] [--list] [--background|--wait] [--write] [prompt]'
allowed-tools: Read, Glob, Grep, Bash(node:*), AskUserQuestion
---

Run a Codex skill through the shared runtime.

Raw slash-command arguments:
`$ARGUMENTS`

Core constraint:
- This command is a thin forwarder to the Codex companion script.
- Do not fix issues, apply patches, or add independent analysis.
- Your only job is to run the skill and return Codex's output verbatim to the user.

Execution mode rules:
- If the raw arguments include `--list`, run the command immediately and return the output.
- If the raw arguments include `--wait`, do not ask. Run the skill in the foreground.
- If the raw arguments include `--background`, do not ask. Run the skill in a Claude background task.
- If the raw arguments do not include `--skill <name>` and do not include `--list`, use `AskUserQuestion` exactly once to ask:
Comment on lines +19 to +21
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Require skill selection before honoring run mode flags

The execution rules prioritize --wait/--background before the missing-skill branch, so /codex:run-skill --background (or --wait) without --skill skips AskUserQuestion and forwards incomplete args to the companion, which then fails with “Specify a skill…”. This breaks the documented guided flow for users who choose mode first; the command should still collect --skill <name> whenever --list is not present.

Useful? React with 👍 / 👎.

- Question: "Which Codex skill would you like to run, and what should it do?"
- Provide a free-text answer option.
- After receiving the answer, construct the full command with `--skill <name>` and the user's prompt. Do NOT use `$ARGUMENTS` for this execution path.
- Otherwise (raw arguments include `--skill`), use `AskUserQuestion` exactly once with two options, putting the recommended option first and suffixing its label with `(Recommended)`:
- `Run in background (Recommended)`
- `Wait for results`

Argument handling:
- When `--skill` is provided in `$ARGUMENTS`, preserve the user's arguments exactly.
- When `--skill` was obtained via AskUserQuestion, build the command from the user's answer instead of `$ARGUMENTS`.
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve non-skill flags in interactive skill selection

When --skill is omitted, this flow rebuilds the command solely from the AskUserQuestion response and explicitly avoids $ARGUMENTS, which drops previously supplied flags like --write. In practice, /codex:run-skill --write will prompt for a skill, then execute without --write, forcing read-only sandboxing and breaking write-capable skills. Carry forward non-skill flags from the original arguments when constructing the post-question command.

Useful? React with 👍 / 👎.

- Do not strip `--wait`, `--background`, or `--write` yourself.
- `--write` allows the skill to modify files (default is read-only sandbox).
- The companion script parses these flags, but Claude Code's `Bash(..., run_in_background: true)` is what actually detaches the run.

List mode:
- When `--list` is present, run the command and return the output verbatim.
- Do not paraphrase or add commentary.

Foreground flow:
- If `--skill` was in the raw arguments, run:
```bash
node "${CLAUDE_PLUGIN_ROOT}/scripts/codex-companion.mjs" run-skill "$ARGUMENTS"
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Build skill arguments before invoking companion

When the user omits --skill, this command asks a free-text question (lines 25–27) but still executes run-skill "$ARGUMENTS" here, and the same file also says to preserve raw arguments exactly. In that flow, $ARGUMENTS stays missing --skill, so codex-companion.mjs run-skill exits with "Specify a skill with --skill " instead of running the selected skill. This breaks the guided path for users who start with /codex:run-skill and rely on the prompt to supply the skill name.

Useful? React with 👍 / 👎.

```
- If `--skill` was obtained via AskUserQuestion, build and run the command from the user's answer instead of `$ARGUMENTS`:
```bash
node "${CLAUDE_PLUGIN_ROOT}/scripts/codex-companion.mjs" run-skill --skill "<name>" <prompt>
```
- Return the command stdout verbatim, exactly as-is.
- Do not paraphrase, summarize, or add commentary before or after it.

Background flow:
- If `--skill` was in the raw arguments, launch with:
```typescript
Bash({
command: `node "${CLAUDE_PLUGIN_ROOT}/scripts/codex-companion.mjs" run-skill "$ARGUMENTS"`,
description: "Codex skill run",
run_in_background: true
})
```
- If `--skill` was obtained via AskUserQuestion, build the command from the user's answer instead of `$ARGUMENTS`:
```typescript
Bash({
command: `node "${CLAUDE_PLUGIN_ROOT}/scripts/codex-companion.mjs" run-skill --skill "<name>" <prompt>`,
description: "Codex skill run",
run_in_background: true
})
```
- Do not call `BashOutput` or wait for completion in this turn.
- After launching the command, tell the user: "Codex skill run started in the background. Check `/codex:status` for progress."
114 changes: 111 additions & 3 deletions plugins/codex/scripts/codex-companion.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import { spawn } from "node:child_process";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import process from "node:process";
import { fileURLToPath } from "node:url";
Expand Down Expand Up @@ -51,6 +52,7 @@ import {
SESSION_ID_ENV
} from "./lib/tracked-jobs.mjs";
import { resolveWorkspaceRoot } from "./lib/workspace.mjs";
import { listAvailableSkills, validateSkill } from "./lib/skills.mjs";
import {
renderNativeReviewResult,
renderReviewResult,
Expand Down Expand Up @@ -80,7 +82,8 @@ function printUsage() {
" node scripts/codex-companion.mjs task [--background] [--write] [--resume-last|--resume|--fresh] [--model <model|spark>] [--effort <none|minimal|low|medium|high|xhigh>] [prompt]",
" node scripts/codex-companion.mjs status [job-id] [--all] [--json]",
" node scripts/codex-companion.mjs result [job-id] [--json]",
" node scripts/codex-companion.mjs cancel [job-id] [--json]"
" node scripts/codex-companion.mjs cancel [job-id] [--json]",
" node scripts/codex-companion.mjs run-skill [--skill <name>] [--list] [prompt]"
].join("\n")
);
}
Expand Down Expand Up @@ -555,8 +558,8 @@ function renderQueuedTaskLaunch(payload) {
}

function getJobKindLabel(kind, jobClass) {
if (kind === "adversarial-review") {
return "adversarial-review";
if (kind) {
return kind;
}
return jobClass === "review" ? "review" : "rescue";
}
Expand Down Expand Up @@ -792,6 +795,108 @@ async function handleTask(argv) {
);
}

async function executeSkillRun(request) {
const workspaceRoot = resolveWorkspaceRoot(request.cwd);
const result = await runAppServerTurn(workspaceRoot, {
prompt: request.prompt,
onProgress: request.onProgress,
persistThread: false,
sandbox: request.write ? "workspace-write" : "read-only"
});

const rawOutput = typeof result.finalMessage === "string" ? result.finalMessage : "";
const failureMessage = result.error?.message ?? result.stderr ?? "";
const rendered = rawOutput || failureMessage || "Codex did not return a final message.\n";
const payload = {
status: result.status,
threadId: result.threadId,
rawOutput
};

return {
exitStatus: result.status,
threadId: result.threadId,
turnId: result.turnId,
payload,
rendered,
summary: firstMeaningfulLine(rawOutput, firstMeaningfulLine(failureMessage, `Codex skill ${request.skillName} finished.`)),
jobTitle: `Run Codex skill: ${request.skillName}`,
jobClass: "skill"
};
}

async function handleRunSkill(argv) {
const { options, positionals } = parseCommandInput(argv, {
valueOptions: ["skill", "cwd"],
booleanOptions: ["list", "wait", "background", "json", "write"]
});

const cwd = resolveCommandCwd(options);
const workspaceRoot = resolveCommandWorkspace(options);
const codexHome = process.env.CODEX_HOME || path.join(os.homedir(), ".codex");

if (options.list) {
const skills = listAvailableSkills(codexHome);
if (skills.length === 0) {
outputCommandResult({ skills: [] }, "No Codex skills found in ~/.codex/skills/.", options.json);
return;
}
const lines = skills.map((s) => ` ${s.name}${s.description ? ` — ${s.description}` : ""}`);
outputCommandResult({ skills }, `Available Codex skills:\n${lines.join("\n")}`, options.json);
return;
}

const skillName = options.skill;
if (!skillName) {
throw new Error("Specify a skill with --skill <name> or use --list to see available skills.");
}

ensureCodexAvailable(cwd);
const { entry: skillEntry, filePath: skillFilePath } = validateSkill(codexHome, skillName);
const skillContent = skillFilePath ? fs.readFileSync(skillFilePath, "utf8") : "";
const skillDir = skillFilePath ? path.dirname(skillFilePath) : null;

const userPrompt = positionals.join(" ").trim();
const skillDirNote = skillDir
? `Additional skill resources (scripts, references, etc.) are located at: ${skillDir}\nYou may read files from this directory as needed.\n`
: "";
const parts = [
`Execute the "${skillName}" Codex skill using the definition below. Do not run shell commands to search for skill files elsewhere.`,
"",
skillDirNote,
"## Skill Definition",
"",
skillContent,
"",
"## User Request",
"",
userPrompt || "(no additional request — execute the skill with default behavior)"
];
const prompt = parts.join("\n");

const job = createCompanionJob({
prefix: "run-skill",
kind: "run-skill",
title: `Run Codex skill: ${skillName}`,
workspaceRoot,
jobClass: "skill",
summary: userPrompt || `Run skill ${skillName}`
});

await runForegroundCommand(
job,
(progress) =>
executeSkillRun({
cwd,
skillName,
prompt,
write: options.write,
onProgress: progress
}),
{ json: options.json }
);
}

async function handleTaskWorker(argv) {
const { options } = parseCommandInput(argv, {
valueOptions: ["cwd", "job-id"]
Expand Down Expand Up @@ -1000,6 +1105,9 @@ async function main() {
case "task":
await handleTask(argv);
break;
case "run-skill":
await handleRunSkill(argv);
break;
case "task-worker":
await handleTaskWorker(argv);
break;
Expand Down
Loading