From cd4cb6031416fe208691bef9597a105f8f7e272a Mon Sep 17 00:00:00 2001 From: Friende <35026241+pengyou200902@users.noreply.github.com> Date: Sat, 18 Apr 2026 03:39:13 -0400 Subject: [PATCH 1/2] fix: route /codex:rescue through the Agent tool to stop Skill recursion (#234) `/codex:rescue` previously combined two things that together caused a hang: - `context: fork` in the frontmatter, which spawns a `general-purpose` subagent for the command body. - Body prose "Route this request to the `codex:codex-rescue` subagent." without naming the transport. When the main agent called `Skill(codex:rescue)` programmatically, the fork resolved the ambiguous prose by trying `Skill(codex:codex-rescue)` (unknown skill) and then falling back to `Skill(codex:rescue)`, which re-entered this command and hung the session until the user cancelled. No Codex job was ever created. Naming the transport as `Agent(codex:codex-rescue)` alone is not enough: forked general-purpose subagents do not expose the `Agent` tool, so the forked runner cannot reach the subagent that way either. The minimal fix is therefore two coordinated changes: - Drop `context: fork` so the command body runs inline in the calling agent's context, where `Agent` is in scope. - Say explicitly "use the `Agent` tool with `subagent_type: "codex:codex-rescue"`", and call out that `Skill(codex:codex-rescue)` and `Skill(codex:rescue)` are not valid routing paths. Add `Agent` to `allowed-tools` so the call does not prompt for permission. Everything else in rescue.md (resume-candidate check, flag handling, background/foreground semantics, operating rules) is unchanged. The `codex:codex-rescue` subagent itself is unchanged. Tests pin the new allow-list, the explicit `subagent_type`, the ban on `Skill(codex:codex-rescue)`, and the absence of `context: fork`. The existing "run the `codex:codex-rescue` subagent in the background" assertion continues to hold since that sentence still reads correctly with the Agent-tool transport. Fixes openai/codex-plugin-cc#234 --- plugins/codex/commands/rescue.md | 6 +++--- tests/commands.test.mjs | 11 ++++++++++- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/plugins/codex/commands/rescue.md b/plugins/codex/commands/rescue.md index c92a2896..56de9555 100644 --- a/plugins/codex/commands/rescue.md +++ b/plugins/codex/commands/rescue.md @@ -1,11 +1,11 @@ --- description: Delegate investigation, an explicit fix request, or follow-up rescue work to the Codex rescue subagent argument-hint: "[--background|--wait] [--resume|--fresh] [--model ] [--effort ] [what Codex should investigate, solve, or continue]" -context: fork -allowed-tools: Bash(node:*), AskUserQuestion +allowed-tools: Bash(node:*), AskUserQuestion, Agent --- -Route this request to the `codex:codex-rescue` subagent. +Invoke the `codex:codex-rescue` subagent via the `Agent` tool (`subagent_type: "codex:codex-rescue"`), forwarding the raw user request as the prompt. +`codex:codex-rescue` is a subagent, not a skill — do not call `Skill(codex:codex-rescue)` (no such skill) or `Skill(codex:rescue)` (that re-enters this command and hangs the session). The command runs inline so the `Agent` tool stays in scope; forked general-purpose subagents do not expose it. The final user-visible response must be Codex's output verbatim. Raw user request: diff --git a/tests/commands.test.mjs b/tests/commands.test.mjs index ef5adb09..034d4d88 100644 --- a/tests/commands.test.mjs +++ b/tests/commands.test.mjs @@ -90,7 +90,16 @@ test("rescue command absorbs continue semantics", () => { const runtimeSkill = read("skills/codex-cli-runtime/SKILL.md"); assert.match(rescue, /The final user-visible response must be Codex's output verbatim/i); - assert.match(rescue, /allowed-tools:\s*Bash\(node:\*\),\s*AskUserQuestion/); + assert.match(rescue, /allowed-tools:\s*Bash\(node:\*\),\s*AskUserQuestion,\s*Agent/); + // Regression for #234: `Skill(codex:rescue)` from the main agent recursed + // because rescue.md named the routing with ambiguous prose ("Route this + // request to the `codex:codex-rescue` subagent") while running under + // `context: fork` — forked general-purpose subagents do not expose the + // `Agent` tool, so the fork fell back to `Skill` and re-entered this + // command. Pin the explicit transport and the inline (no-fork) execution. + assert.match(rescue, /subagent_type: "codex:codex-rescue"/); + assert.match(rescue, /do not call `Skill\(codex:codex-rescue\)`/i); + assert.doesNotMatch(rescue, /^context:\s*fork\b/m); assert.match(rescue, /--background\|--wait/); assert.match(rescue, /--resume\|--fresh/); assert.match(rescue, /--model /); From f503a8dab863cbd0a28f26ed9b2e204f7cd7a3a8 Mon Sep 17 00:00:00 2001 From: Dominik Kundel Date: Sat, 18 Apr 2026 13:36:11 -0700 Subject: [PATCH 2/2] test: match quoted result and cancel command arguments --- tests/commands.test.mjs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/commands.test.mjs b/tests/commands.test.mjs index 034d4d88..3724ffa4 100644 --- a/tests/commands.test.mjs +++ b/tests/commands.test.mjs @@ -174,9 +174,9 @@ test("result and cancel commands are exposed as deterministic runtime entrypoint const resultHandling = read("skills/codex-result-handling/SKILL.md"); assert.match(result, /disable-model-invocation:\s*true/); - assert.match(result, /codex-companion\.mjs" result \$ARGUMENTS/); + assert.match(result, /codex-companion\.mjs" result "\$ARGUMENTS"/); assert.match(cancel, /disable-model-invocation:\s*true/); - assert.match(cancel, /codex-companion\.mjs" cancel \$ARGUMENTS/); + assert.match(cancel, /codex-companion\.mjs" cancel "\$ARGUMENTS"/); assert.match(resultHandling, /do not turn a failed or incomplete Codex run into a Claude-side implementation attempt/i); assert.match(resultHandling, /if Codex was never successfully invoked, do not generate a substitute answer at all/i); });