fix: route /codex:rescue through the Agent tool to stop Skill recursion (#234) (#235)

* 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

* test: match quoted result and cancel command arguments

---------

Co-authored-by: Dominik Kundel <dkundel@openai.com>
This commit is contained in:
Friende 2026-04-18 16:38:45 -04:00 committed by GitHub
parent 6a5c2ba53b
commit bb38412a67
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 15 additions and 6 deletions

View File

@ -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 <model|spark>] [--effort <none|minimal|low|medium|high|xhigh>] [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:

View File

@ -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 <model\|spark>/);
@ -165,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);
});