diff --git a/README.md b/README.md index 458c39fb..1739cd03 100644 --- a/README.md +++ b/README.md @@ -137,7 +137,9 @@ Use it when you want Codex to: > [!NOTE] > Depending on the task and the model you choose these tasks might take a long time and it's generally recommended to force the task to be in the background or move the agent to the background. -It supports `--background`, `--wait`, `--resume`, and `--fresh`. If you omit `--resume` and `--fresh`, the plugin can offer to continue the latest rescue thread for this repo. +It supports `--background`, `--wait`, `--resume`, `--fresh`, and `--from-review `. If you omit `--resume` and `--fresh`, the plugin can offer to continue the latest rescue thread for this repo. + +Use `--from-review` when you want Codex to apply the smallest safe fixes for findings from a completed `/codex:review` or `/codex:adversarial-review` run. The id can be either a review job id from `/codex:status` or the Codex session id shown by `/codex:result`. Examples: @@ -145,6 +147,8 @@ Examples: /codex:rescue investigate why the tests started failing /codex:rescue fix the failing test with the smallest safe patch /codex:rescue --resume apply the top fix from the last run +/codex:rescue --from-review review-abc123 +/codex:rescue --from-review thr_review_session fix only the high severity finding /codex:rescue --model gpt-5.4-mini --effort medium investigate the flaky integration test /codex:rescue --model spark fix the issue quickly /codex:rescue --background investigate the regression @@ -161,6 +165,7 @@ Ask Codex to redesign the database connection to be more resilient. - if you do not pass `--model` or `--effort`, Codex chooses its own defaults. - if you say `spark`, the plugin maps that to `gpt-5.3-codex-spark` - follow-up rescue requests can continue the latest Codex task in the repo +- a single review-looking id such as `/codex:rescue review-abc123` is treated as `/codex:rescue --from-review review-abc123` ### `/codex:status` diff --git a/docs/codex-command-parameters.zh.html b/docs/codex-command-parameters.zh.html new file mode 100644 index 00000000..c727d0ba --- /dev/null +++ b/docs/codex-command-parameters.zh.html @@ -0,0 +1,974 @@ + + + + + + Codex Claude Code 插件命令参数速查 + + + +
+ + +
+
+
+ 7 + 公开 slash commands +
+
+ 2 + 只读 review 命令 +
+
+ 1 + Codex 任务委托命令 +
+
+ 3 + 后台任务管理命令 +
+
+ +
+

全部命令

+
+ + +
+
+ +
+
+ +
+
+

Usage

+
/codex:setup [--enable-review-gate|--disable-review-gate]
+
+
+

参数

+ + + + + + +
参数含义
--enable-review-gate启用 stop-time review gate。Claude 停止前会触发 Codex review,发现问题时阻止结束。
--disable-review-gate关闭 stop-time review gate。
+
+
    +
  • --enable-review-gate--disable-review-gate 不能同时使用。
  • +
  • 如果缺少 Codex 且 npm 可用,Claude Code 会询问是否安装 @openai/codex
  • +
+
+

示例

+
/codex:setup
+
/codex:setup --enable-review-gate
+
/codex:setup --disable-review-gate
+
+
+
+ +
+ +
+
+

Usage

+
/codex:review [--wait|--background] [--base <ref>] [--scope auto|working-tree|branch]
+
+
+

参数

+ + + + + + + + + + +
参数含义
--wait前台等待 review 完成。
--background后台运行 review,之后用 /codex:status/codex:result 查看。
--base <ref>review 当前分支相对某个 git ref 的差异,例如 main
--scope auto自动选择 review 目标。工作区有改动时 review working tree,否则 review 当前分支相对默认分支的差异。
--scope working-treereview 当前工作区改动,包括 staged、unstaged、untracked。
--scope branchreview 当前分支相对默认分支的差异。
+
+
    +
  • 不支持额外 focus text。
  • +
  • 不支持 staged-only 或 unstaged-only review。
  • +
  • 需要自定义关注点时使用 /codex:adversarial-review
  • +
+
+

示例

+
/codex:review
+
/codex:review --background
+
/codex:review --base main
+
/codex:review --scope working-tree --wait
+
+
+
+ +
+ +
+
+

Usage

+
/codex:adversarial-review [--wait|--background] [--base <ref>] [--scope auto|working-tree|branch] [focus ...]
+
+
+

参数

+ + + + + + + + + + + +
参数含义
--wait前台等待 review 完成。
--background后台运行 review。
--base <ref>review 当前分支相对某个 git ref 的差异。
--scope auto自动选择 review 目标。
--scope working-treereview 当前工作区改动。
--scope branchreview 当前分支相对默认分支的差异。
focus ...额外关注点,例如缓存设计、并发、回滚、权限边界等。
+
+
    +
  • 不支持 --scope staged--scope unstaged
  • +
  • /codex:review 使用相同的 review 目标选择逻辑。
  • +
+
+

示例

+
/codex:adversarial-review
+
/codex:adversarial-review --base main challenge the caching and retry design
+
/codex:adversarial-review --background look for race conditions and rollback risks
+
+
+
+ +
+ +
+
+

Usage

+
/codex:rescue [--background|--wait] [--resume|--fresh] [--from-review <review-job-id|session-id>] [--model <model|spark>] [--effort <none|minimal|low|medium|high|xhigh>] [task ...]
+
+
+

参数

+ + + + + + + + + + + + + +
参数含义
--background后台运行 Codex 任务。
--wait前台等待 Codex 任务完成。
--resume继续当前或最近的 Codex rescue 线程。
--fresh强制开启新的 Codex 线程。
--from-review <id>从已完成的 review job id 或 Codex session id 生成修复任务。
--model <model>指定 Codex 使用的模型,例如 gpt-5.4-mini
--model sparkspark 是简写,会映射到 gpt-5.3-codex-spark
--effort none|minimal|low|medium|high|xhigh指定推理强度。
task ...要 Codex 做的任务描述。
+
+
    +
  • 默认倾向于 write-capable run,也就是 Codex 可能修改文件。
  • +
  • --from-review 默认要求 Codex 对 review findings 做最小安全修复;如果 review 没有实质问题,Codex 应简短验证并避免无意义改动。
  • +
  • 单独传一个看起来像 review id 的参数,例如 /codex:rescue review-abc123,等价于 /codex:rescue --from-review review-abc123
  • +
  • 只想要 plan、分析或诊断时,明确写 read-onlyplan onlydo not edit files,或中文“只分析,不要修改文件”。
  • +
  • 不传 --model--effort 时,Codex 使用默认选择。
  • +
+
+

示例

+
/codex:rescue --fresh investigate why tests are failing
+
/codex:rescue --background --model spark fix the failing login test
+
/codex:rescue --resume implement the previous plan
+
/codex:rescue --from-review review-abc123
+
/codex:rescue --from-review thr_review_session fix only the high severity finding
+
/codex:rescue --fresh --effort high read-only plan only: design the refactor, do not edit files
+
+
+
+ +
+ +
+
+

Usage

+
/codex:status [job-id] [--wait] [--timeout-ms <ms>] [--all]
+
+
+

参数

+ + + + + + + + +
参数含义
job-id查看某个具体 job 的详细状态。
--wait等待指定 job 结束后再返回。
--timeout-ms <ms>配合 --wait 使用,设置最长等待时间,单位毫秒。
--all显示更多历史 jobs,而不只显示默认范围。
+
+
    +
  • --wait 需要配合 job-id 使用。
  • +
+
+

示例

+
/codex:status
+
/codex:status --all
+
/codex:status task-abc123
+
/codex:status task-abc123 --wait --timeout-ms 300000
+
+
+
+ +
+ +
+
+

Usage

+
/codex:result [job-id]
+
+
+

参数

+ + + + + +
参数含义
job-id指定要查看结果的 job。不传时通常查看最近的可用结果。
+
+
+

示例

+
/codex:result
+
/codex:result task-abc123
+
+
+
+ +
+ +
+
+

Usage

+
/codex:cancel [job-id]
+
+
+

参数

+ + + + + +
参数含义
job-id指定要取消的 job。不传时会尝试取消当前可取消的 job。
+
+
+

示例

+
/codex:cancel
+
/codex:cancel task-abc123
+
+
+
+
+ +

没有匹配的命令或参数。

+
+
+ + + + diff --git a/docs/codex-command-parameters.zh.md b/docs/codex-command-parameters.zh.md new file mode 100644 index 00000000..c3730dca --- /dev/null +++ b/docs/codex-command-parameters.zh.md @@ -0,0 +1,256 @@ +# Codex Claude Code 插件命令参数速查 + +本文总结 `codex-plugin-cc` 提供的 Claude Code slash commands 及其参数。 + +## 命令总览 + +| 命令 | 用途 | 是否会修改文件 | +| --- | --- | --- | +| `/codex:setup` | 检查 Codex CLI 是否可用、是否已登录,并可开关 stop-time review gate | 不会 | +| `/codex:review` | 对当前工作区或分支差异运行普通 Codex review | 不会 | +| `/codex:adversarial-review` | 对实现方案、设计取舍和风险做挑战式 review | 不会 | +| `/codex:rescue` | 把调查、计划、review 结果修复或实现任务委托给 Codex | 默认可能会 | +| `/codex:status` | 查看当前仓库里的 Codex 任务状态 | 不会 | +| `/codex:result` | 查看已完成任务的最终输出 | 不会 | +| `/codex:cancel` | 取消正在运行的后台 Codex 任务 | 不会修改代码 | + +## `/codex:setup` + +```text +/codex:setup [--enable-review-gate|--disable-review-gate] +``` + +检查本机 Codex CLI 是否已安装、是否已认证。如果缺少 Codex 且 npm 可用,Claude Code 会询问是否安装 `@openai/codex`。 + +| 参数 | 含义 | +| --- | --- | +| `--enable-review-gate` | 启用 stop-time review gate。Claude 停止前会触发 Codex review,发现问题时阻止结束 | +| `--disable-review-gate` | 关闭 stop-time review gate | + +限制: + +- `--enable-review-gate` 和 `--disable-review-gate` 不能同时使用。 + +示例: + +```text +/codex:setup +/codex:setup --enable-review-gate +/codex:setup --disable-review-gate +``` + +## `/codex:review` + +```text +/codex:review [--wait|--background] [--base ] [--scope auto|working-tree|branch] +``` + +运行普通 Codex code review。这个命令只做 review,不会修复问题、应用 patch 或修改文件。 + +| 参数 | 含义 | +| --- | --- | +| `--wait` | 前台等待 review 完成 | +| `--background` | 后台运行 review,之后用 `/codex:status` 和 `/codex:result` 查看 | +| `--base ` | review 当前分支相对某个 git ref 的差异,例如 `main` | +| `--scope auto` | 自动选择 review 目标。工作区有改动时 review working tree,否则 review 当前分支相对默认分支的差异 | +| `--scope working-tree` | review 当前工作区改动,包括 staged、unstaged、untracked | +| `--scope branch` | review 当前分支相对默认分支的差异 | + +限制: + +- 不支持额外 focus text。 +- 不支持 staged-only 或 unstaged-only review。 +- 如果需要自定义关注点,用 `/codex:adversarial-review`。 + +示例: + +```text +/codex:review +/codex:review --background +/codex:review --base main +/codex:review --scope working-tree --wait +``` + +## `/codex:adversarial-review` + +```text +/codex:adversarial-review [--wait|--background] [--base ] [--scope auto|working-tree|branch] [focus ...] +``` + +运行挑战式 review。它关注实现方向、设计取舍、隐藏假设和真实场景下的失败模式,不只是找代码缺陷。这个命令也是只读的,不会改文件。 + +| 参数 | 含义 | +| --- | --- | +| `--wait` | 前台等待 review 完成 | +| `--background` | 后台运行 review | +| `--base ` | review 当前分支相对某个 git ref 的差异 | +| `--scope auto` | 自动选择 review 目标 | +| `--scope working-tree` | review 当前工作区改动 | +| `--scope branch` | review 当前分支相对默认分支的差异 | +| `focus ...` | 额外关注点,例如缓存设计、并发、回滚、权限边界等 | + +限制: + +- 不支持 `--scope staged` 或 `--scope unstaged`。 +- 与 `/codex:review` 使用相同的 review 目标选择逻辑。 + +示例: + +```text +/codex:adversarial-review +/codex:adversarial-review --base main challenge the caching and retry design +/codex:adversarial-review --background look for race conditions and rollback risks +``` + +## `/codex:rescue` + +```text +/codex:rescue [--background|--wait] [--resume|--fresh] [--from-review ] [--model ] [--effort ] [task ...] +``` + +把一个调查、计划、review 结果修复或实现任务转交给 Codex。`rescue` 更像是“委托 Codex 接手这个问题”,不是把 Claude Code 的内部状态完整迁移给 Codex。 + +| 参数 | 含义 | +| --- | --- | +| `--background` | 后台运行 Codex 任务 | +| `--wait` | 前台等待 Codex 任务完成 | +| `--resume` | 继续当前或最近的 Codex rescue 线程 | +| `--fresh` | 强制开启新的 Codex 线程 | +| `--from-review ` | 从已完成的 review job id 或 Codex session id 生成修复任务 | +| `--model ` | 指定 Codex 使用的模型,例如 `gpt-5.4-mini` | +| `--model spark` | `spark` 是简写,会映射到 `gpt-5.3-codex-spark` | +| `--effort none` | 不使用额外推理强度 | +| `--effort minimal` | 最小推理强度 | +| `--effort low` | 低推理强度 | +| `--effort medium` | 中等推理强度 | +| `--effort high` | 高推理强度 | +| `--effort xhigh` | 最高推理强度 | +| `task ...` | 要 Codex 做的任务描述 | + +行为要点: + +- 默认会倾向于 write-capable Codex run,也就是 Codex 可能修改文件。 +- `--from-review` 默认要求 Codex 对 review findings 做最小安全修复;如果 review 没有实质问题,Codex 应简短验证并避免无意义改动。 +- 单独传一个看起来像 review id 的参数,例如 `/codex:rescue review-abc123`,等价于 `/codex:rescue --from-review review-abc123`。 +- 如果只想要 plan、分析或诊断,需要在任务描述里明确写 `read-only`、`plan only`、`do not edit files`,或中文“只分析,不要修改文件”。 +- `--resume` 和 `--fresh` 不能表达同一个意图;需要二选一。 +- 如果不传 `--model` 或 `--effort`,Codex 使用默认选择。 + +示例: + +```text +/codex:rescue --fresh investigate why tests are failing +/codex:rescue --background --model spark fix the failing login test +/codex:rescue --resume implement the previous plan +/codex:rescue --from-review review-abc123 +/codex:rescue --from-review thr_review_session fix only the high severity finding +/codex:rescue --fresh --effort high read-only plan only: design the refactor, do not edit files +``` + +## `/codex:status` + +```text +/codex:status [job-id] [--wait] [--timeout-ms ] [--all] +``` + +查看当前仓库里运行中和近期的 Codex jobs。 + +| 参数 | 含义 | +| --- | --- | +| `job-id` | 查看某个具体 job 的详细状态 | +| `--wait` | 等待指定 job 结束后再返回 | +| `--timeout-ms ` | 配合 `--wait` 使用,设置最长等待时间,单位毫秒 | +| `--all` | 显示更多历史 jobs,而不只显示默认范围 | + +限制: + +- `--wait` 需要配合 `job-id` 使用。 + +示例: + +```text +/codex:status +/codex:status --all +/codex:status task-abc123 +/codex:status task-abc123 --wait --timeout-ms 300000 +``` + +## `/codex:result` + +```text +/codex:result [job-id] +``` + +查看已完成 job 的最终输出。输出可能包含 review 结论、发现的问题、文件路径、行号、artifact、下一步命令等。 + +| 参数 | 含义 | +| --- | --- | +| `job-id` | 指定要查看结果的 job。不传时通常查看最近的可用结果 | + +示例: + +```text +/codex:result +/codex:result task-abc123 +``` + +## `/codex:cancel` + +```text +/codex:cancel [job-id] +``` + +取消一个正在运行的后台 Codex job。 + +| 参数 | 含义 | +| --- | --- | +| `job-id` | 指定要取消的 job。不传时会尝试取消当前可取消的 job | + +示例: + +```text +/codex:cancel +/codex:cancel task-abc123 +``` + +## 常见组合 + +只让 Codex 出 plan,不改文件: + +```text +/codex:rescue --fresh read-only plan only: analyze the repo and propose an implementation plan. Do not edit files. +``` + +让 Codex 后台实现任务: + +```text +/codex:rescue --background --fresh implement the requested feature and run relevant tests +``` + +查看后台任务: + +```text +/codex:status +``` + +等待某个任务结束: + +```text +/codex:status task-abc123 --wait --timeout-ms 300000 +``` + +查看最终结果: + +```text +/codex:result task-abc123 +``` + +## Runtime 内部参数 + +插件底层脚本还支持一些内部或命令包装层使用的参数,例如 `--json`、`--cwd`、`--prompt-file`、`--write`、`--resume-last`、`--poll-interval-ms`。日常在 Claude Code 里使用 slash commands 时通常不需要直接使用它们。 + +公开 slash commands 里最重要的是: + +- 用 `/codex:review` 和 `/codex:adversarial-review` 做只读 review。 +- 用 `/codex:rescue` 委托 Codex 做调查、计划或实现。 +- 用 `/codex:status`、`/codex:result`、`/codex:cancel` 管理后台任务。 diff --git a/plugins/codex/agents/codex-rescue.md b/plugins/codex/agents/codex-rescue.md index 7009ec86..9c9471d8 100644 --- a/plugins/codex/agents/codex-rescue.md +++ b/plugins/codex/agents/codex-rescue.md @@ -31,10 +31,13 @@ Forwarding rules: - If the user asks for `spark`, map that to `--model gpt-5.3-codex-spark`. - If the user asks for a concrete model name such as `gpt-5.4-mini`, pass it through with `--model`. - Treat `--effort ` and `--model ` as runtime controls and do not include them in the task text you pass through. +- Treat `--from-review ` as a review-result source control and pass it through to `task`. - Default to a write-capable Codex run by adding `--write` unless the user explicitly asks for read-only behavior or only wants review, diagnosis, or research without edits. - Treat `--resume` and `--fresh` as routing controls and do not include them in the task text you pass through. - `--resume` means add `--resume-last`. - `--fresh` means do not add `--resume-last`. +- If the user provides exactly one review-looking id such as `review-abc123` or `thr_abc123`, convert it to `--from-review `. +- If the request includes `--from-review` and does not include `--resume`, start a fresh task thread. - If the user is clearly asking to continue prior Codex work in this repository, such as "continue", "keep going", "resume", "apply the top fix", or "dig deeper", add `--resume-last` unless `--fresh` is present. - Otherwise forward the task as a fresh `task` run. - Preserve the user's task text as-is apart from stripping routing flags. diff --git a/plugins/codex/commands/rescue.md b/plugins/codex/commands/rescue.md index 56de9555..8a3e3099 100644 --- a/plugins/codex/commands/rescue.md +++ b/plugins/codex/commands/rescue.md @@ -1,6 +1,6 @@ --- -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]" +description: Delegate investigation, an explicit fix request, review-result rescue, or follow-up rescue work to the Codex rescue subagent +argument-hint: "[--background|--wait] [--resume|--fresh] [--from-review ] [--model ] [--effort ] [what Codex should investigate, solve, or continue]" allowed-tools: Bash(node:*), AskUserQuestion, Agent --- @@ -20,6 +20,8 @@ Execution mode: - `--model` and `--effort` are runtime-selection flags. Preserve them for the forwarded `task` call, but do not treat them as part of the natural-language task text. - If the request includes `--resume`, do not ask whether to continue. The user already chose. - If the request includes `--fresh`, do not ask whether to continue. The user already chose. +- If the request includes `--from-review `, do not ask whether to continue unless the request also includes `--resume`. Start a fresh rescue by preserving `--from-review` and adding `--fresh` before routing to the subagent. +- If the request is exactly one review-looking id such as `review-abc123` or `thr_abc123`, do not ask whether to continue. Treat it as review-result rescue and add `--fresh` before routing to the subagent. - Otherwise, before starting Codex, check for a resumable rescue thread from this Claude session by running: ```bash @@ -44,6 +46,7 @@ Operating rules: - Do not ask the subagent to inspect files, monitor progress, poll `/codex:status`, fetch `/codex:result`, call `/codex:cancel`, summarize output, or do follow-up work of its own. - Leave `--effort` unset unless the user explicitly asks for a specific reasoning effort. - Leave the model unset unless the user explicitly asks for one. If they ask for `spark`, map it to `gpt-5.3-codex-spark`. +- Leave `--from-review` in the forwarded request. It tells `task` to turn a completed review job or Codex review session into a fix prompt. - Leave `--resume` and `--fresh` in the forwarded request. The subagent handles that routing when it builds the `task` command. - If the helper reports that Codex is missing or unauthenticated, stop and tell the user to run `/codex:setup`. - If the user did not supply a request, ask what Codex should investigate or fix. diff --git a/plugins/codex/scripts/codex-companion.mjs b/plugins/codex/scripts/codex-companion.mjs index 35222fd5..ff153acc 100644 --- a/plugins/codex/scripts/codex-companion.mjs +++ b/plugins/codex/scripts/codex-companion.mjs @@ -77,7 +77,7 @@ function printUsage() { " node scripts/codex-companion.mjs setup [--enable-review-gate|--disable-review-gate] [--json]", " node scripts/codex-companion.mjs review [--wait|--background] [--base ] [--scope ]", " node scripts/codex-companion.mjs adversarial-review [--wait|--background] [--base ] [--scope ] [focus text]", - " node scripts/codex-companion.mjs task [--background] [--write] [--resume-last|--resume|--fresh] [--model ] [--effort ] [prompt]", + " node scripts/codex-companion.mjs task [--background] [--write] [--resume-last|--resume|--fresh] [--from-review ] [--model ] [--effort ] [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]" @@ -625,6 +625,108 @@ function requireTaskRequest(prompt, resumeLast) { } } +function looksLikeReviewReference(value) { + return /^review[-_][a-z0-9._-]+$/i.test(String(value ?? "")) || /^thr[_-][a-z0-9._-]+$/i.test(String(value ?? "")); +} + +function shouldUseReviewReferenceShorthand(options, positionals) { + return !options["from-review"] && !options["prompt-file"] && positionals.length === 1 && looksLikeReviewReference(positionals[0]); +} + +function selectStoredReviewOutput(storedJob) { + const candidates = [ + storedJob?.rendered, + storedJob?.result?.codex?.stdout, + storedJob?.result?.rawOutput, + storedJob?.result?.rawOutput?.stdout, + storedJob?.result?.codex?.stderr + ]; + + return candidates.find((value) => typeof value === "string" && value.trim())?.trim() ?? ""; +} + +function resolveReviewJobForTask(cwd, reference) { + const workspaceRoot = resolveWorkspaceRoot(cwd); + const jobs = sortJobsNewestFirst(listJobs(workspaceRoot)); + const ref = String(reference ?? "").trim(); + if (!ref) { + throw new Error("Provide a review job id or Codex session id after --from-review."); + } + + const exactJob = jobs.find((job) => job.id === ref); + if (exactJob) { + if (exactJob.jobClass !== "review") { + throw new Error(`Job "${ref}" is not a review job.`); + } + return { workspaceRoot, job: exactJob }; + } + + const prefixMatches = jobs.filter((job) => job.id?.startsWith(ref)); + if (prefixMatches.length > 1) { + throw new Error(`Review reference "${ref}" is ambiguous. Use a longer job id or the Codex session id.`); + } + if (prefixMatches.length === 1) { + const [job] = prefixMatches; + if (job.jobClass !== "review") { + throw new Error(`Job "${ref}" is not a review job.`); + } + return { workspaceRoot, job }; + } + + const threadMatches = jobs.filter((job) => job.jobClass === "review" && job.threadId === ref); + if (threadMatches.length > 1) { + throw new Error(`Review session id "${ref}" is ambiguous. Use the review job id instead.`); + } + if (threadMatches.length === 1) { + return { workspaceRoot, job: threadMatches[0] }; + } + + throw new Error(`No review job found for "${ref}". Run /codex:status --all to list known jobs.`); +} + +function buildPromptFromReviewResult(cwd, reference, extraInstructions) { + const { workspaceRoot, job } = resolveReviewJobForTask(cwd, reference); + const storedJob = readStoredJob(workspaceRoot, job.id); + if (!storedJob) { + throw new Error(`Review job "${job.id}" has no stored result payload.`); + } + if (job.status !== "completed" && storedJob.status !== "completed") { + throw new Error(`Review job "${job.id}" is ${job.status ?? storedJob.status ?? "not completed"}; only completed review jobs can be rescued.`); + } + + const reviewOutput = selectStoredReviewOutput(storedJob); + if (!reviewOutput) { + throw new Error(`Review job "${job.id}" has no captured review output to rescue.`); + } + + const instructions = String(extraInstructions ?? "").trim(); + const metadata = [ + `Review job id: ${job.id}`, + `Review kind: ${job.kindLabel ?? job.kind ?? "review"}`, + `Review target: ${job.summary ?? storedJob.targetLabel ?? "unknown"}`, + `Codex review session id: ${storedJob.threadId ?? job.threadId ?? "unknown"}` + ].join("\n"); + + return [ + "Apply the smallest safe fixes for the actionable findings from this Codex review.", + "", + "Rules:", + "- Preserve unrelated behavior and avoid broad refactors.", + "- If the review says there are no material findings, verify briefly and do not make unnecessary edits.", + "- Run focused relevant checks when available. If a check cannot be run, report why.", + "- In the final response, summarize the changes and verification.", + "", + "Review metadata:", + metadata, + "", + "Review output:", + "```text", + reviewOutput, + "```", + ...(instructions ? ["", "Additional user instructions:", instructions] : []) + ].join("\n"); +} + async function runForegroundCommand(job, runner, options = {}) { const { logFile, progress } = createTrackedProgress(job, { logFile: options.logFile, @@ -731,7 +833,7 @@ async function handleReview(argv) { async function handleTask(argv) { const { options, positionals } = parseCommandInput(argv, { - valueOptions: ["model", "effort", "cwd", "prompt-file"], + valueOptions: ["model", "effort", "cwd", "prompt-file", "from-review"], booleanOptions: ["json", "write", "resume-last", "resume", "fresh", "background"], aliasMap: { m: "model" @@ -742,7 +844,11 @@ async function handleTask(argv) { const workspaceRoot = resolveCommandWorkspace(options); const model = normalizeRequestedModel(options.model); const effort = normalizeReasoningEffort(options.effort); - const prompt = readTaskPrompt(cwd, options, positionals); + const useReviewShorthand = shouldUseReviewReferenceShorthand(options, positionals); + const fromReview = options["from-review"] ?? (useReviewShorthand ? positionals[0] : null); + const prompt = fromReview + ? buildPromptFromReviewResult(cwd, fromReview, useReviewShorthand ? "" : readTaskPrompt(cwd, options, positionals)) + : readTaskPrompt(cwd, options, positionals); const resumeLast = Boolean(options["resume-last"] || options.resume); const fresh = Boolean(options.fresh); diff --git a/plugins/codex/skills/codex-cli-runtime/SKILL.md b/plugins/codex/skills/codex-cli-runtime/SKILL.md index 0e91bfb5..aef01e41 100644 --- a/plugins/codex/skills/codex-cli-runtime/SKILL.md +++ b/plugins/codex/skills/codex-cli-runtime/SKILL.md @@ -28,6 +28,8 @@ Command selection: - If the forwarded request includes `--background` or `--wait`, treat that as Claude-side execution control only. Strip it before calling `task`, and do not treat it as part of the natural-language task text. - If the forwarded request includes `--model`, normalize `spark` to `gpt-5.3-codex-spark` and pass it through to `task`. - If the forwarded request includes `--effort`, pass it through to `task`. +- If the forwarded request includes `--from-review `, pass it through to `task` so Codex can fix findings from that completed review result. +- If the forwarded request is exactly one review-looking id such as `review-abc123` or `thr_abc123`, convert it to `--from-review `. - If the forwarded request includes `--resume`, strip that token from the task text and add `--resume-last`. - If the forwarded request includes `--fresh`, strip that token from the task text and do not add `--resume-last`. - `--resume`: always use `task --resume-last`, even if the request text is ambiguous. diff --git a/tests/commands.test.mjs b/tests/commands.test.mjs index 3724ffa4..522fb04d 100644 --- a/tests/commands.test.mjs +++ b/tests/commands.test.mjs @@ -102,6 +102,7 @@ test("rescue command absorbs continue semantics", () => { assert.doesNotMatch(rescue, /^context:\s*fork\b/m); assert.match(rescue, /--background\|--wait/); assert.match(rescue, /--resume\|--fresh/); + assert.match(rescue, /--from-review /); assert.match(rescue, /--model /); assert.match(rescue, /--effort /); assert.match(rescue, /task-resume-candidate --json/); @@ -116,15 +117,19 @@ test("rescue command absorbs continue semantics", () => { assert.match(rescue, /If they ask for `spark`, map it to `gpt-5\.3-codex-spark`/i); assert.match(rescue, /If the request includes `--resume`, do not ask whether to continue/i); assert.match(rescue, /If the request includes `--fresh`, do not ask whether to continue/i); + assert.match(rescue, /If the request includes `--from-review `, do not ask whether to continue/i); + assert.match(rescue, /If the request is exactly one review-looking id/i); assert.match(rescue, /If the user chooses continue, add `--resume`/i); assert.match(rescue, /If the user chooses a new thread, add `--fresh`/i); assert.match(rescue, /thin forwarder only/i); assert.match(rescue, /Return the Codex companion stdout verbatim to the user/i); assert.match(rescue, /Do not paraphrase, summarize, rewrite, or add commentary before or after it/i); assert.match(rescue, /return that command's stdout as-is/i); + assert.match(rescue, /Leave `--from-review` in the forwarded request/i); assert.match(rescue, /Leave `--resume` and `--fresh` in the forwarded request/i); assert.match(agent, /--resume/); assert.match(agent, /--fresh/); + assert.match(agent, /--from-review/); assert.match(agent, /thin forwarding wrapper/i); assert.match(agent, /prefer foreground for a small, clearly bounded rescue request/i); assert.match(agent, /If the user did not explicitly choose `--background` or `--wait` and the task looks complicated, open-ended, multi-step, or likely to keep Codex running for a long time, prefer background execution/i); @@ -135,6 +140,8 @@ test("rescue command absorbs continue semantics", () => { assert.match(agent, /Leave model unset by default/i); assert.match(agent, /If the user asks for `spark`, map that to `--model gpt-5\.3-codex-spark`/i); assert.match(agent, /If the user asks for a concrete model name such as `gpt-5\.4-mini`, pass it through with `--model`/i); + assert.match(agent, /Treat `--from-review ` as a review-result source control/i); + assert.match(agent, /convert it to `--from-review `/i); assert.match(agent, /Return the stdout of the `codex-companion` command exactly as-is/i); assert.match(agent, /If the Bash call fails or Codex cannot be invoked, return nothing/i); assert.match(agent, /gpt-5-4-prompting/); @@ -147,6 +154,7 @@ test("rescue command absorbs continue semantics", () => { assert.match(runtimeSkill, /Leave `--effort` unset unless the user explicitly requests a specific effort/i); assert.match(runtimeSkill, /Leave model unset by default/i); assert.match(runtimeSkill, /Map `spark` to `--model gpt-5\.3-codex-spark`/i); + assert.match(runtimeSkill, /--from-review/); assert.match(runtimeSkill, /If the forwarded request includes `--background` or `--wait`, treat that as Claude-side execution control only/i); assert.match(runtimeSkill, /Strip it before calling `task`/i); assert.match(runtimeSkill, /`--effort`: accepted values are `none`, `minimal`, `low`, `medium`, `high`, `xhigh`/i); @@ -155,6 +163,7 @@ test("rescue command absorbs continue semantics", () => { assert.match(readme, /`codex:codex-rescue` subagent/i); assert.match(readme, /if you do not pass `--model` or `--effort`, Codex chooses its own defaults/i); assert.match(readme, /--model gpt-5\.4-mini --effort medium/i); + assert.match(readme, /--from-review review-/i); assert.match(readme, /`spark`, the plugin maps that to `gpt-5\.3-codex-spark`/i); assert.match(readme, /continue a previous Codex task/i); assert.match(readme, /### `\/codex:setup`/); diff --git a/tests/runtime.test.mjs b/tests/runtime.test.mjs index 90408372..b301fd99 100644 --- a/tests/runtime.test.mjs +++ b/tests/runtime.test.mjs @@ -580,6 +580,232 @@ test("write task output focuses on the Codex result without generic follow-up hi assert.equal(result.stdout, "Handled the requested task.\nTask prompt accepted.\n"); }); +test("task --from-review turns a review job result into a write prompt", () => { + const repo = makeTempDir(); + const binDir = makeTempDir(); + const statePath = path.join(binDir, "fake-codex-state.json"); + installFakeCodex(binDir); + initGitRepo(repo); + fs.writeFileSync(path.join(repo, "README.md"), "hello\n"); + run("git", ["add", "README.md"], { cwd: repo }); + run("git", ["commit", "-m", "init"], { cwd: repo }); + fs.writeFileSync(path.join(repo, "README.md"), "hello again\n"); + + const review = run("node", [SCRIPT, "review"], { + cwd: repo, + env: buildEnv(binDir) + }); + assert.equal(review.status, 0, review.stderr); + + const stateDir = resolveStateDir(repo); + const companionState = JSON.parse(fs.readFileSync(path.join(stateDir, "state.json"), "utf8")); + const reviewJob = companionState.jobs.find((job) => job.jobClass === "review"); + assert.ok(reviewJob); + + const result = run("node", [SCRIPT, "task", "--write", "--from-review", reviewJob.id], { + cwd: repo, + env: buildEnv(binDir) + }); + + assert.equal(result.status, 0, result.stderr); + assert.equal(result.stdout, "Handled the requested task.\nTask prompt accepted.\n"); + const fakeState = JSON.parse(fs.readFileSync(statePath, "utf8")); + assert.match(fakeState.lastTurnStart.prompt, /Apply the smallest safe fixes for the actionable findings/); + assert.match(fakeState.lastTurnStart.prompt, new RegExp(`Review job id: ${reviewJob.id}`)); + assert.match(fakeState.lastTurnStart.prompt, /Reviewed uncommitted changes\./); +}); + +test("task --from-review accepts a review session id and extra instructions", () => { + const workspace = makeTempDir(); + const binDir = makeTempDir(); + const statePath = path.join(binDir, "fake-codex-state.json"); + installFakeCodex(binDir); + const stateDir = resolveStateDir(workspace); + const jobsDir = path.join(stateDir, "jobs"); + fs.mkdirSync(jobsDir, { recursive: true }); + + fs.writeFileSync( + path.join(jobsDir, "review-finished.json"), + JSON.stringify( + { + id: "review-finished", + status: "completed", + title: "Codex Adversarial Review", + rendered: "# Codex Adversarial Review\n\nFindings:\n- [high] Missing empty-state guard (src/app.js:4-6)\n", + threadId: "thr_review_finished" + }, + null, + 2 + ), + "utf8" + ); + fs.writeFileSync( + path.join(stateDir, "state.json"), + `${JSON.stringify( + { + version: 1, + config: { stopReviewGate: false }, + jobs: [ + { + id: "review-finished", + kind: "adversarial-review", + kindLabel: "adversarial-review", + status: "completed", + title: "Codex Adversarial Review", + jobClass: "review", + threadId: "thr_review_finished", + summary: "Adversarial review working tree diff", + updatedAt: "2026-03-24T20:00:00.000Z" + } + ] + }, + null, + 2 + )}\n`, + "utf8" + ); + + const result = run("node", [SCRIPT, "task", "--write", "--from-review", "thr_review_finished", "fix only the high severity finding"], { + cwd: workspace, + env: buildEnv(binDir) + }); + + assert.equal(result.status, 0, result.stderr); + const fakeState = JSON.parse(fs.readFileSync(statePath, "utf8")); + assert.match(fakeState.lastTurnStart.prompt, /Review job id: review-finished/); + assert.match(fakeState.lastTurnStart.prompt, /Review kind: adversarial-review/); + assert.match(fakeState.lastTurnStart.prompt, /Missing empty-state guard/); + assert.match(fakeState.lastTurnStart.prompt, /Additional user instructions:\nfix only the high severity finding/); +}); + +test("task treats a single review-looking id as --from-review shorthand", () => { + const workspace = makeTempDir(); + const binDir = makeTempDir(); + const statePath = path.join(binDir, "fake-codex-state.json"); + installFakeCodex(binDir); + const stateDir = resolveStateDir(workspace); + const jobsDir = path.join(stateDir, "jobs"); + fs.mkdirSync(jobsDir, { recursive: true }); + + fs.writeFileSync( + path.join(jobsDir, "review-shorthand.json"), + JSON.stringify( + { + id: "review-shorthand", + status: "completed", + title: "Codex Review", + rendered: "# Codex Review\n\nFinding: fix src/app.js\n", + threadId: "thr_review_shorthand" + }, + null, + 2 + ), + "utf8" + ); + fs.writeFileSync( + path.join(stateDir, "state.json"), + `${JSON.stringify( + { + version: 1, + config: { stopReviewGate: false }, + jobs: [ + { + id: "review-shorthand", + status: "completed", + title: "Codex Review", + jobClass: "review", + threadId: "thr_review_shorthand", + summary: "Review working tree diff", + updatedAt: "2026-03-24T20:00:00.000Z" + } + ] + }, + null, + 2 + )}\n`, + "utf8" + ); + + const result = run("node", [SCRIPT, "task", "--write", "review-shorthand"], { + cwd: workspace, + env: buildEnv(binDir) + }); + + assert.equal(result.status, 0, result.stderr); + const fakeState = JSON.parse(fs.readFileSync(statePath, "utf8")); + assert.match(fakeState.lastTurnStart.prompt, /Review job id: review-shorthand/); + assert.match(fakeState.lastTurnStart.prompt, /Finding: fix src\/app\.js/); +}); + +test("task --from-review rejects unresolved, running, non-review, and resultless refs", () => { + const workspace = makeTempDir(); + const binDir = makeTempDir(); + installFakeCodex(binDir); + const stateDir = resolveStateDir(workspace); + const jobsDir = path.join(stateDir, "jobs"); + fs.mkdirSync(jobsDir, { recursive: true }); + + fs.writeFileSync(path.join(jobsDir, "review-running.json"), JSON.stringify({ id: "review-running", status: "running" }, null, 2), "utf8"); + fs.writeFileSync(path.join(jobsDir, "task-finished.json"), JSON.stringify({ id: "task-finished", status: "completed" }, null, 2), "utf8"); + fs.writeFileSync(path.join(jobsDir, "review-empty.json"), JSON.stringify({ id: "review-empty", status: "completed" }, null, 2), "utf8"); + fs.writeFileSync(path.join(jobsDir, "review-ambig-a.json"), JSON.stringify({ id: "review-ambig-a", status: "completed", rendered: "A" }, null, 2), "utf8"); + fs.writeFileSync(path.join(jobsDir, "review-ambig-b.json"), JSON.stringify({ id: "review-ambig-b", status: "completed", rendered: "B" }, null, 2), "utf8"); + fs.writeFileSync( + path.join(stateDir, "state.json"), + `${JSON.stringify( + { + version: 1, + config: { stopReviewGate: false }, + jobs: [ + { id: "review-running", status: "running", title: "Codex Review", jobClass: "review", updatedAt: "2026-03-24T20:00:00.000Z" }, + { id: "task-finished", status: "completed", title: "Codex Task", jobClass: "task", updatedAt: "2026-03-24T19:00:00.000Z" }, + { id: "review-empty", status: "completed", title: "Codex Review", jobClass: "review", updatedAt: "2026-03-24T18:00:00.000Z" }, + { id: "review-ambig-a", status: "completed", title: "Codex Review", jobClass: "review", updatedAt: "2026-03-24T17:00:00.000Z" }, + { id: "review-ambig-b", status: "completed", title: "Codex Review", jobClass: "review", updatedAt: "2026-03-24T16:00:00.000Z" } + ] + }, + null, + 2 + )}\n`, + "utf8" + ); + + const missing = run("node", [SCRIPT, "task", "--write", "--from-review", "review-missing"], { + cwd: workspace, + env: buildEnv(binDir) + }); + assert.equal(missing.status, 1); + assert.match(missing.stderr, /No review job found for "review-missing"/); + + const running = run("node", [SCRIPT, "task", "--write", "--from-review", "review-running"], { + cwd: workspace, + env: buildEnv(binDir) + }); + assert.equal(running.status, 1); + assert.match(running.stderr, /only completed review jobs can be rescued/); + + const nonReview = run("node", [SCRIPT, "task", "--write", "--from-review", "task-finished"], { + cwd: workspace, + env: buildEnv(binDir) + }); + assert.equal(nonReview.status, 1); + assert.match(nonReview.stderr, /is not a review job/); + + const resultless = run("node", [SCRIPT, "task", "--write", "--from-review", "review-empty"], { + cwd: workspace, + env: buildEnv(binDir) + }); + assert.equal(resultless.status, 1); + assert.match(resultless.stderr, /has no captured review output/); + + const ambiguous = run("node", [SCRIPT, "task", "--write", "--from-review", "review-ambig"], { + cwd: workspace, + env: buildEnv(binDir) + }); + assert.equal(ambiguous.status, 1); + assert.match(ambiguous.stderr, /is ambiguous/); +}); + test("task --resume acts like --resume-last without leaking the flag into the prompt", () => { const repo = makeTempDir(); const binDir = makeTempDir();