From a053f528d80619dff982a710091a9eaec7309e5a Mon Sep 17 00:00:00 2001
From: TwoMonkeys
Date: Tue, 11 Aug 2026 13:32:15 +0800
Subject: [PATCH] fix: capture Codex plan review transcripts
---
docs/demo/demo.md | 38 +-
docs/demo/overview-and-quickstart.md | 25 +
docs/demo/overview-and-quickstart.zh-CN.md | 25 +
docs/install-guide.md | 25 +-
docs/install-guide.zh-CN.md | 25 +-
docs/integrations.md | 33 +-
docs/integrations.zh-CN.md | 33 +-
docs/regression-cases.md | 12 +
.../.openspec.yaml | 2 +
.../design.md | 44 ++
.../proposal.md | 28 ++
.../specs/agent-reply-capture/spec.md | 56 +++
.../tasks.md | 20 +
openspec/config.yaml | 4 +-
openspec/specs/agent-reply-capture/spec.md | 62 +++
src-tauri/src/cache.rs | 450 ++++++++++++++++--
src-tauri/src/cli.rs | 45 +-
src-tauri/src/extract/codex.rs | 2 +-
src-tauri/src/main.rs | 2 +-
src-tauri/tests/anti_crosstalk.rs | 129 +++++
.../components/PersonalizationPanelTabs.tsx | 3 +-
.../__tests__/PersonalizationPanel.test.tsx | 11 +-
src/lib/locales.ts | 4 +
23 files changed, 1015 insertions(+), 63 deletions(-)
create mode 100644 openspec/changes/archive/2026-08-11-capture-codex-plan-review/.openspec.yaml
create mode 100644 openspec/changes/archive/2026-08-11-capture-codex-plan-review/design.md
create mode 100644 openspec/changes/archive/2026-08-11-capture-codex-plan-review/proposal.md
create mode 100644 openspec/changes/archive/2026-08-11-capture-codex-plan-review/specs/agent-reply-capture/spec.md
create mode 100644 openspec/changes/archive/2026-08-11-capture-codex-plan-review/tasks.md
create mode 100644 openspec/specs/agent-reply-capture/spec.md
diff --git a/docs/demo/demo.md b/docs/demo/demo.md
index 567bf42..3dbce12 100644
--- a/docs/demo/demo.md
+++ b/docs/demo/demo.md
@@ -93,7 +93,7 @@ graph TB
CLIPBOARD["System Clipboard"]
end
- CODEX -->|"notify hook"| CLI
+ CODEX -->|"notify arg / Stop stdin"| CLI
CLAUDE -->|"Stop hook (stdin)"| CLI
GEMINI -->|"AfterAgent hook (stdin)"| CLI
@@ -171,7 +171,7 @@ Each supported AI agent has a unique hook mechanism. cliV normalizes these into
| Agent | Hook Type | Trigger | Data Source | Cache Key |
|:---|:---|:---|:---|:---|
-| Codex | `notify` | `agent-turn-complete` | CLI argument (JSON) | PID cache key + sidecar thread metadata |
+| Codex | `notify` + `Stop` | completed turn / lifecycle stop | CLI argument + stdin (JSON) | PID cache key + sidecar thread metadata |
| Claude Code | `Stop` | `Stop` event | stdin (JSON) | `session_id` + PID |
| Gemini CLI | `AfterAgent` | After agent response | stdin (JSON) | `GEMINI_SESSION_ID` + PID |
@@ -183,6 +183,29 @@ Each supported AI agent has a unique hook mechanism. cliV normalizes these into
notify = ["cliv", "cache-codex"]
```
+Add a user-level `Stop` hook in `~/.codex/hooks.json` for Plan Review, then review it through `/hooks`:
+
+```json
+{
+ "hooks": {
+ "Stop": [
+ {
+ "hooks": [
+ {
+ "type": "command",
+ "command": "cliv cache-codex",
+ "timeout": 5,
+ "statusMessage": "Caching reply for cliV"
+ }
+ ]
+ }
+ ]
+ }
+}
+```
+
+Codex's Plan decision dialog currently owns keyboard input. Choose No to return to the normal composer before pressing `Ctrl+G`; cliV then opens the plan captured by the Stop hook.
+
**Claude Code** — Add to `~/.claude/settings.json`:
```json
@@ -233,7 +256,7 @@ cliV uses a hand-rolled CLI parser (no external crate dependencies) to keep the
flowchart TD
START["cliV invoked"] --> CHECK_SUB{"argv[1] is subcommand?"}
- CHECK_SUB -->|"cache-codex"| CODEX_CACHE["CacheCodex Mode Parse JSON arg"]
+ CHECK_SUB -->|"cache-codex"| CODEX_CACHE["CacheCodex Mode Parse argv or stdin JSON"]
CHECK_SUB -->|"cache-claude"| CLAUDE_CACHE["CacheClaude Mode Read stdin"]
CHECK_SUB -->|"cache-gemini"| GEMINI_CACHE["CacheGemini Mode Read stdin"]
CHECK_SUB -->|"No"| GUI_MODE["GUI Mode"]
@@ -261,8 +284,8 @@ flowchart TD
pub enum CliMode {
/// Launch the Tauri GUI (default).
Gui,
- /// Cache a Codex reply from notify hook: `cliv cache-codex ''`
- CacheCodex(String),
+ /// Cache a Codex reply from notify argv or Stop-hook stdin.
+ CacheCodex(Option),
/// Cache a Claude reply from Stop hook (stdin): `cliv cache-claude`
CacheClaude,
/// Cache a Gemini reply from AfterAgent hook (stdin): `cliv cache-gemini`
@@ -1262,7 +1285,8 @@ graph TB
cliv document.md
# Cache an agent reply (called by hooks, not users)
-cliv cache-codex ''
+cliv cache-codex '' # notify payload from argv
+cliv cache-codex # Stop-hook payload from stdin
cliv cache-claude # reads from stdin
cliv cache-gemini # reads from stdin
@@ -1292,7 +1316,7 @@ mindmap
serde_json
dirs crate
Integration
- Codex notify hook
+ Codex notify + Stop hooks
Claude Stop hook
Gemini AfterAgent hook
Storage
diff --git a/docs/demo/overview-and-quickstart.md b/docs/demo/overview-and-quickstart.md
index e092576..3b90b21 100644
--- a/docs/demo/overview-and-quickstart.md
+++ b/docs/demo/overview-and-quickstart.md
@@ -107,6 +107,31 @@ On macOS, if `cliv` is not in PATH, use:
notify = ["/Applications/cliV.app/Contents/MacOS/cliv", "cache-codex"]
```
+Keep notify for compatibility, and create `~/.codex/hooks.json` to capture Plan Review content:
+
+```json
+{
+ "hooks": {
+ "Stop": [
+ {
+ "hooks": [
+ {
+ "type": "command",
+ "command": "cliv cache-codex",
+ "timeout": 5,
+ "statusMessage": "Caching reply for cliV"
+ }
+ ]
+ }
+ ]
+ }
+}
+```
+
+Use `/hooks` in Codex to review and trust the command. If inline hooks already exist in `config.toml`, merge the Stop handler there instead of maintaining both hook formats. On macOS without a symlink, use the full cliV executable path in `command` too.
+
+Codex's Plan decision dialog currently captures keyboard input, so `Ctrl+G` does not launch `$EDITOR` inside that dialog. Choose No to return to the normal composer, then press `Ctrl+G`; cliV will load the plan captured by the Stop hook.
+
#### Claude Code
Edit `~/.claude/settings.json` and merge this into your existing config:
diff --git a/docs/demo/overview-and-quickstart.zh-CN.md b/docs/demo/overview-and-quickstart.zh-CN.md
index 850d265..660f4a8 100644
--- a/docs/demo/overview-and-quickstart.zh-CN.md
+++ b/docs/demo/overview-and-quickstart.zh-CN.md
@@ -107,6 +107,31 @@ notify = ["cliv", "cache-codex"]
notify = ["/Applications/cliV.app/Contents/MacOS/cliv", "cache-codex"]
```
+保留 notify 用于兼容,并创建 `~/.codex/hooks.json` 来捕获 Plan Review 内容:
+
+```json
+{
+ "hooks": {
+ "Stop": [
+ {
+ "hooks": [
+ {
+ "type": "command",
+ "command": "cliv cache-codex",
+ "timeout": 5,
+ "statusMessage": "Caching reply for cliV"
+ }
+ ]
+ }
+ ]
+ }
+}
+```
+
+在 Codex 中使用 `/hooks` 审核并信任该命令。如果 `config.toml` 已经有 inline Hooks,请把 Stop handler 合并进去,不要同时维护两种 Hook 格式。macOS 未创建软链接时,`command` 也要使用 cliV 的完整可执行路径。
+
+Codex 的 Plan 决策弹窗当前会接管键盘输入,因此在弹窗内按 `Ctrl+G` 不会启动 `$EDITOR`。选择 No 回到普通输入框后再按 `Ctrl+G`,cliV 会加载 Stop Hook 已捕获的计划。
+
#### Claude Code
编辑 `~/.claude/settings.json`,把下面内容合并进现有配置:
diff --git a/docs/install-guide.md b/docs/install-guide.md
index b107f21..085f571 100644
--- a/docs/install-guide.md
+++ b/docs/install-guide.md
@@ -96,7 +96,7 @@ setx EDITOR cliv
Then open a new terminal.
-When an agent triggers `Ctrl+G`, it launches `$EDITOR`. That is how cliV opens from the CLI workflow.
+When an agent triggers `Ctrl+G` from its normal composer, it launches `$EDITOR`. Codex's Plan decision dialog currently owns keyboard input, so return to the composer before using this shortcut.
## 3. Configure Agent Hooks
@@ -111,6 +111,29 @@ Edit `~/.codex/config.toml`:
notify = ["cliv", "cache-codex"]
```
+`notify` keeps compatibility with ordinary completed turns. To capture plans when Codex enters Plan Review, also create `~/.codex/hooks.json`:
+
+```json
+{
+ "hooks": {
+ "Stop": [
+ {
+ "hooks": [
+ {
+ "type": "command",
+ "command": "cliv cache-codex",
+ "timeout": 5,
+ "statusMessage": "Caching reply for cliV"
+ }
+ ]
+ }
+ ]
+ }
+}
+```
+
+Open `/hooks` in Codex and trust this hook before testing it. Codex skips new or changed command hooks until they are reviewed. If you already define inline Codex hooks in `config.toml`, merge this `Stop` handler there instead of configuring both representations. On macOS without a symlink, replace the command with `/Applications/cliV.app/Contents/MacOS/cliv cache-codex`.
+
### Claude Code
Edit `~/.claude/settings.json`:
diff --git a/docs/install-guide.zh-CN.md b/docs/install-guide.zh-CN.md
index 6b1c4be..f961713 100644
--- a/docs/install-guide.zh-CN.md
+++ b/docs/install-guide.zh-CN.md
@@ -96,7 +96,7 @@ setx EDITOR cliv
然后重新打开一个新的终端。
-当 Agent 触发 `Ctrl+G` 时,它会启动 `$EDITOR`,这就是 cliV 被唤起的入口。
+在 Agent 的普通输入框中触发 `Ctrl+G` 时,它会启动 `$EDITOR`。Codex 的 Plan 决策弹窗当前会接管键盘输入,因此需要先回到输入框再使用该快捷键。
## 3. 配置 Agent Hook
@@ -111,6 +111,29 @@ setx EDITOR cliv
notify = ["cliv", "cache-codex"]
```
+`notify` 继续兼容普通已完成回合。为了在 Codex 进入 Plan Review 时捕获计划,还要创建 `~/.codex/hooks.json`:
+
+```json
+{
+ "hooks": {
+ "Stop": [
+ {
+ "hooks": [
+ {
+ "type": "command",
+ "command": "cliv cache-codex",
+ "timeout": 5,
+ "statusMessage": "Caching reply for cliV"
+ }
+ ]
+ }
+ ]
+ }
+}
+```
+
+测试前请在 Codex 中打开 `/hooks` 并信任该 Hook;新的或发生变化的 command Hook 在完成审核前会被跳过。如果你已经在 `config.toml` 中使用 inline Codex Hooks,请把这个 `Stop` handler 合并到现有配置,不要同时维护两种表示。macOS 未创建软链接时,把 command 改为 `/Applications/cliV.app/Contents/MacOS/cliv cache-codex`。
+
### Claude Code
编辑 `~/.claude/settings.json`:
diff --git a/docs/integrations.md b/docs/integrations.md
index 6412125..0f5eba8 100644
--- a/docs/integrations.md
+++ b/docs/integrations.md
@@ -87,6 +87,31 @@ notify = ["/Applications/cliV.app/Contents/MacOS/cliv", "cache-codex"]
Codex passes JSON as a command-line argument to `cliv cache-codex`.
+Keep that notify entry for compatibility, then add `~/.codex/hooks.json` so Plan Review replies are delivered through stdin:
+
+```json
+{
+ "hooks": {
+ "Stop": [
+ {
+ "hooks": [
+ {
+ "type": "command",
+ "command": "cliv cache-codex",
+ "timeout": 5,
+ "statusMessage": "Caching reply for cliV"
+ }
+ ]
+ }
+ ]
+ }
+}
+```
+
+Current Codex Plan Review builds may leave both assistant-message fields null. In that case cliV reads only the same-session, same-turn `item_completed/Plan` entry from the `transcript_path` supplied by the Stop hook; no additional user command is required.
+
+Review and trust the command through `/hooks` in Codex. If the same config layer already uses inline hooks, merge this handler there rather than keeping both inline hooks and `hooks.json`. On macOS without a symlink, use `/Applications/cliV.app/Contents/MacOS/cliv cache-codex` as the command.
+
### Claude Code
`~/.claude/settings.json`:
@@ -210,6 +235,9 @@ Codex:
```bash
CODEX_THREAD_ID=424242 cliv cache-codex '{"type":"agent-turn-complete","thread-id":"test-123","last-assistant-message":"# Hello\nTest reply."}'
cat ~/.codex/reply_cache/424242.md
+
+printf '%s' '{"hook_event_name":"Stop","session_id":"test-123","turn_id":"plan-1","permission_mode":"plan","last_assistant_message":"\n# Plan\n\n- Review this\n"}' | CODEX_THREAD_ID=424242 cliv cache-codex
+cat ~/.codex/reply_cache/424242.md
```
Claude Code:
@@ -230,6 +258,8 @@ If these commands write the expected `.md` files, cliV itself is working and the
- the wrong hook file path
- malformed hook JSON or TOML
+- a Codex Stop hook that has not been trusted through `/hooks`
+- Codex hooks disabled through the `features.hooks` setting
- a shell quoting issue
- a stale terminal session that has not reloaded `PATH` or `EDITOR`
@@ -241,4 +271,5 @@ Check these in order:
2. Did the hook actually write a cache file under `~/.codex`, `~/.claude`, or `~/.gemini`?
3. Is the lookup key the one cliV is expecting for that launch context?
4. Did a non-default path or shell quoting issue stop the hook from running?
-5. On Windows, did you reopen the terminal after install or after changing `EDITOR`?
+5. For Codex Plan Review, is the Stop hook listed as trusted under `/hooks`?
+6. On Windows, did you reopen the terminal after install or after changing `EDITOR`?
diff --git a/docs/integrations.zh-CN.md b/docs/integrations.zh-CN.md
index eff3105..b3fdbf4 100644
--- a/docs/integrations.zh-CN.md
+++ b/docs/integrations.zh-CN.md
@@ -87,6 +87,31 @@ notify = ["/Applications/cliV.app/Contents/MacOS/cliv", "cache-codex"]
Codex 会把 JSON 作为命令行参数传给 `cliv cache-codex`。
+保留该 notify 配置用于兼容,再添加 `~/.codex/hooks.json`,让 Plan Review 回复通过 stdin 传入:
+
+```json
+{
+ "hooks": {
+ "Stop": [
+ {
+ "hooks": [
+ {
+ "type": "command",
+ "command": "cliv cache-codex",
+ "timeout": 5,
+ "statusMessage": "Caching reply for cliV"
+ }
+ ]
+ }
+ ]
+ }
+}
+```
+
+当前 Codex 的 Plan Review 可能让两个 assistant-message 字段都为 `null`。此时 cliV 只读取 Stop Hook 提供的 `transcript_path` 中同 session、同 turn 的 `item_completed/Plan` 条目;用户不需要执行额外命令。
+
+请在 Codex 中通过 `/hooks` 审核并信任该命令。如果同一个配置层已经使用 inline Hooks,请把这个 handler 合并进去,不要同时保留 inline Hooks 和 `hooks.json`。macOS 未创建软链接时,command 使用 `/Applications/cliV.app/Contents/MacOS/cliv cache-codex`。
+
### Claude Code
`~/.claude/settings.json`:
@@ -210,6 +235,9 @@ Codex:
```bash
CODEX_THREAD_ID=424242 cliv cache-codex '{"type":"agent-turn-complete","thread-id":"test-123","last-assistant-message":"# Hello\nTest reply."}'
cat ~/.codex/reply_cache/424242.md
+
+printf '%s' '{"hook_event_name":"Stop","session_id":"test-123","turn_id":"plan-1","permission_mode":"plan","last_assistant_message":"\n# Plan\n\n- Review this\n"}' | CODEX_THREAD_ID=424242 cliv cache-codex
+cat ~/.codex/reply_cache/424242.md
```
Claude Code:
@@ -230,6 +258,8 @@ cat ~/.gemini/reply_cache/test-gemini.md
- hook 文件路径写错了
- JSON / TOML 语法有误
+- Codex Stop Hook 尚未通过 `/hooks` 信任
+- Codex 的 `features.hooks` 设置禁用了 Hooks
- shell 引号转义不对
- 终端仍然是旧会话,没有重新加载 `PATH` 或 `EDITOR`
@@ -241,4 +271,5 @@ cat ~/.gemini/reply_cache/test-gemini.md
2. 对应 `~/.codex`、`~/.claude`、`~/.gemini` 下是否真的写出了 cache 文件?
3. 当前启动上下文里,cliV 查找的 lookup key 是否和你预期一致?
4. 是否因为非默认路径或 shell 引号问题,导致 hook 根本没执行?
-5. Windows 上是否在安装或修改 `EDITOR` 之后重新打开了终端?
+5. Codex Plan Review 场景下,`/hooks` 是否显示 Stop Hook 已受信任?
+6. Windows 上是否在安装或修改 `EDITOR` 之后重新打开了终端?
diff --git a/docs/regression-cases.md b/docs/regression-cases.md
index d8ec98b..e01e8ca 100644
--- a/docs/regression-cases.md
+++ b/docs/regression-cases.md
@@ -115,6 +115,18 @@
---
+## Agent Reply Capture
+
+### ARC-001 — Codex Plan Review 应显示当前计划而不是空白或上一轮回复
+- **Area:** agent-integration
+- **Scenario:** Codex 同一会话先完成普通回复,随后在 Plan 模式产生 `last_assistant_message: null`,计划仅存在于同 session、同 turn 的 transcript `item_completed/Plan` 事件中;在 Plan 决策弹窗选择 No 回到普通输入框后,再通过 `$EDITOR` 调起 cliV
+- **Expected:** cliV 显示当前计划的 Markdown 内容,外层 transport tags 不可见;提交批注后内容写回 Codex 提供的临时 target
+- **Coverage:** manual
+- **Manual verification:** 在 `~/.codex/config.toml` 保留 notify,并在 `~/.codex/hooks.json` 配置、通过 `/hooks` 信任 `Stop -> cliv cache-codex`;重开 Codex,先完成一个普通回合,再进入 Plan 模式生成计划;在 Plan 决策弹窗选择 No 回到普通输入框,按 `Ctrl+G` 调起 cliV;确认显示的是当前计划,完成一条批注并验证 Codex 收到写回内容
+- **Reason:** null-message transcript fallback、双 transport、PID 覆盖和 extractor 回读已有 Rust 自动化;真实 Codex TUI 的 Plan Review、Hook trust 与外部 `$EDITOR` 交接需要安装态 Codex 进行跨进程人工验证
+
+---
+
## Worktree Tooling
### WT-001 — shared-cache helper 不得让默认 Rust 安装失效
diff --git a/openspec/changes/archive/2026-08-11-capture-codex-plan-review/.openspec.yaml b/openspec/changes/archive/2026-08-11-capture-codex-plan-review/.openspec.yaml
new file mode 100644
index 0000000..a8821c7
--- /dev/null
+++ b/openspec/changes/archive/2026-08-11-capture-codex-plan-review/.openspec.yaml
@@ -0,0 +1,2 @@
+schema: spec-driven
+created: 2026-08-11
diff --git a/openspec/changes/archive/2026-08-11-capture-codex-plan-review/design.md b/openspec/changes/archive/2026-08-11-capture-codex-plan-review/design.md
new file mode 100644
index 0000000..b690851
--- /dev/null
+++ b/openspec/changes/archive/2026-08-11-capture-codex-plan-review/design.md
@@ -0,0 +1,44 @@
+## Context
+
+cliV currently captures Codex replies only from the `notify` command configured in `~/.codex/config.toml`. Codex Plan Review can produce both notify and Stop events with null assistant-message fields. The completed plan is persisted in the session transcript as an `item_completed` event whose item type is `Plan`, keyed by the same session and turn ids. cliV already uses PID-keyed cache files plus metadata to keep concurrent agent sessions isolated.
+
+## Goals / Non-Goals
+
+**Goals:**
+
+- Capture the current Codex plan before cliV is opened as the Plan Review editor.
+- Preserve the existing notify command and cache lookup contract.
+- Keep cache writes atomic and scoped to the originating Codex process.
+- Prevent hook payload content from being copied into cliV logs.
+- Give users accurate setup and trust instructions without taking ownership of Codex configuration.
+
+**Non-Goals:**
+
+- Running or attaching to Codex App Server.
+- Automatically editing `~/.codex/config.toml` or `~/.codex/hooks.json`.
+- Changing annotation or write-back behavior after content reaches the document store.
+
+## Decisions
+
+1. **Use one backward-compatible command with two transports.** `cliv cache-codex ` continues to parse notify input. `cliv cache-codex` reads a `Stop` payload from stdin. A new subcommand would duplicate cache behavior and make installation guidance harder to maintain.
+2. **Normalize event schemas before writing.** Accepted notify and Stop payloads become one internal record containing session id, message, turn id, permission mode, and source. When a Stop message is empty, inspect only its declared transcript path and accept only an exact same-session, same-turn `item_completed/Plan` item. Unknown events or missing required content are logged without writing cache data.
+3. **Accept every Stop response.** Plan mode is identified for diagnostics by `permission_mode: "plan"`, but Stop capture is not filtered to that mode. This makes the lifecycle hook a resilient primary source while notify remains compatible; duplicate successful events are harmless because they contain the same current assistant reply and use atomic replacement.
+4. **Remove only an exact outer plan envelope.** When the complete trimmed response is enclosed by `` and ``, cache the enclosed Markdown. Embedded tags, code examples, and ordinary replies remain unchanged.
+5. **Retain PID/session isolation with token-aware process matching.** Both sources use the current Codex ancestor identity and the existing metadata resolution. Agent discovery matches exact or `agent-*` path components within process names and command-line tokens, not arbitrary substrings, so Node package paths remain detectable while a workspace containing `codex` cannot steal cache ownership. A later accepted response from the same Codex process replaces the prior reply, so Plan Review cannot intentionally fall back to an earlier turn.
+6. **Redact before logging argv.** Cache subcommand diagnostics record command shape and payload length, never the JSON argument itself. stdin continues to be represented only by length and parsed field metadata.
+7. **Document a user-level Stop hook.** The canonical configuration keeps notify in `config.toml` and adds `Stop -> cliv cache-codex` in `hooks.json`, followed by review through `/hooks`. Users with inline hooks merge the handler there instead of configuring both hook representations.
+
+## Risks / Trade-offs
+
+- **Hook unavailable, disabled, or untrusted** → notify remains supported; documentation and the Integrations UI explain hook trust and troubleshooting.
+- **Notify and Stop race** → both normalize to atomic writes for the same PID and normally carry identical content; null notify payloads do not replace valid Stop content.
+- **Codex changes hook or transcript fields** → reject unknown or incomplete payloads without scanning unrelated sessions; strict session/turn/item matching and regression fixtures prevent cross-talk.
+- **Plan envelope rules change** → exact-envelope stripping fails safe by preserving non-matching content.
+
+## Migration Plan
+
+Ship the dual-input command without changing existing installations. Users who need Plan Review capture add and trust the Stop hook; rollback consists of removing that hook while notify continues to work. No cache migration or dependency change is required.
+
+## Open Questions
+
+None.
diff --git a/openspec/changes/archive/2026-08-11-capture-codex-plan-review/proposal.md b/openspec/changes/archive/2026-08-11-capture-codex-plan-review/proposal.md
new file mode 100644
index 0000000..b0494bb
--- /dev/null
+++ b/openspec/changes/archive/2026-08-11-capture-codex-plan-review/proposal.md
@@ -0,0 +1,28 @@
+## Why
+
+Codex Plan Review can emit an `agent-turn-complete` notification whose `last-assistant-message` is null, so cliV's notify-only capture path has no plan content to display when Codex opens cliV as `$EDITOR`. Plan review is a high-value review point and must reliably show the current plan rather than an empty or previous reply.
+
+## What Changes
+
+- Extend `cliv cache-codex` to accept both the existing notify JSON argument and Codex `Stop` hook JSON on stdin.
+- Normalize both event shapes into the existing PID-isolated reply cache and render an outer `` envelope as normal Markdown content.
+- Keep the existing notify integration for compatibility while documenting the `Stop` hook as the Plan Review capture path.
+- Redact cache payloads from cliV command-line logging.
+- Update the Integrations UI, bilingual integration/install/demo documentation, and regression coverage.
+
+## Capabilities
+
+### New Capabilities
+
+- `agent-reply-capture`: Stable capture behavior for agent replies, including Codex notify and lifecycle-hook payloads used by Plan Review.
+
+### Modified Capabilities
+
+None.
+
+## Impact
+
+- Tauri/Rust CLI parsing, Codex cache ingestion, PID/session cache metadata flow, and diagnostic logging.
+- Frontend Integrations guidance and its localized tests.
+- Codex user configuration under `~/.codex/config.toml` and `~/.codex/hooks.json` remains user-owned and is documented rather than rewritten by cliV.
+- Public documentation and manual regression guidance for Linux, macOS, and Windows.
diff --git a/openspec/changes/archive/2026-08-11-capture-codex-plan-review/specs/agent-reply-capture/spec.md b/openspec/changes/archive/2026-08-11-capture-codex-plan-review/specs/agent-reply-capture/spec.md
new file mode 100644
index 0000000..931d318
--- /dev/null
+++ b/openspec/changes/archive/2026-08-11-capture-codex-plan-review/specs/agent-reply-capture/spec.md
@@ -0,0 +1,56 @@
+## ADDED Requirements
+
+### Requirement: Codex reply capture accepts notify and Stop hook input
+cliV SHALL preserve the `cliv cache-codex ` notify interface and SHALL also accept a Codex `Stop` lifecycle-hook payload from stdin when `cliv cache-codex` is invoked without a JSON argument.
+
+#### Scenario: Existing notify integration captures a reply
+- **WHEN** `cliv cache-codex` receives an `agent-turn-complete` JSON argument with a non-empty thread id and assistant message
+- **THEN** cliV stores that reply under the originating Codex process identity using the existing cache metadata contract
+
+#### Scenario: Plan mode Stop hook captures a plan
+- **WHEN** `cliv cache-codex` receives a Plan-mode `Stop` payload whose assistant-message field is empty but whose transcript contains an `item_completed` Plan for the same session id and turn id
+- **THEN** cliV stores that exact Plan item as the current reply for the originating Codex process
+
+#### Scenario: Stop hook captures a directly supplied plan
+- **WHEN** a Plan-mode `Stop` payload contains a non-empty last assistant message
+- **THEN** cliV stores that message without reading the transcript
+
+#### Scenario: Ordinary Stop hook captures a reply
+- **WHEN** a valid `Stop` hook payload has a permission mode other than `plan`
+- **THEN** cliV stores its last assistant message through the same cache path
+
+#### Scenario: Invalid or empty input does not replace content
+- **WHEN** the cache command receives malformed JSON, an unsupported event, or neither a non-empty assistant message nor an exact same-session same-turn Plan transcript item
+- **THEN** cliV does not replace the current reply cache with that input
+
+### Requirement: Current Codex reply replaces an earlier turn
+For accepted Codex events, cliV SHALL atomically replace the PID-keyed reply for the originating Codex process while retaining session metadata used for alias resolution.
+
+#### Scenario: Plan follows an ordinary reply in one Codex process
+- **WHEN** cliV captures an ordinary reply and then a valid Plan-mode Stop reply from the same Codex process
+- **THEN** subsequent reply extraction returns the plan rather than the earlier reply
+
+### Requirement: Plan transport envelope is reviewable Markdown
+cliV SHALL remove an exact outer `...` transport envelope before displaying the enclosed plan, without rewriting ordinary Markdown or embedded examples.
+
+#### Scenario: Wrapped plan is captured
+- **WHEN** a Stop message consists of a proposed-plan opening tag, Markdown plan content, and the corresponding closing tag
+- **THEN** cliV stores the enclosed Markdown without the outer transport tags
+
+#### Scenario: Non-envelope content is preserved
+- **WHEN** a reply does not consist entirely of the proposed-plan envelope
+- **THEN** cliV preserves the reply content unchanged
+
+### Requirement: Cache diagnostics do not expose reply payloads
+cliV MUST NOT write complete Codex notify or Stop payloads, user prompts, or assistant messages to its diagnostic log while handling cache commands.
+
+#### Scenario: Notify JSON is supplied on the command line
+- **WHEN** cliV logs invocation details for `cliv cache-codex `
+- **THEN** the log contains only redacted command information and payload length, not the JSON content
+
+### Requirement: Codex hook configuration remains user-owned
+cliV SHALL document the notify and Stop-hook configuration required for complete Codex reply capture and SHALL NOT automatically rewrite Codex configuration files.
+
+#### Scenario: User configures Plan Review capture
+- **WHEN** a user follows cliV's Codex integration instructions
+- **THEN** the instructions retain notify compatibility, add a user-level Stop hook, and require the hook to be reviewed through Codex's hook trust flow
diff --git a/openspec/changes/archive/2026-08-11-capture-codex-plan-review/tasks.md b/openspec/changes/archive/2026-08-11-capture-codex-plan-review/tasks.md
new file mode 100644
index 0000000..f1274f2
--- /dev/null
+++ b/openspec/changes/archive/2026-08-11-capture-codex-plan-review/tasks.md
@@ -0,0 +1,20 @@
+## 1. Codex capture implementation
+
+- [x] 1.1 Extend `cliv cache-codex` to preserve notify arguments, read Stop-hook JSON from stdin, recover a null-message Plan from the exact same session/turn transcript item, and strip only an exact outer proposed-plan envelope.
+- [x] 1.2 Reuse PID/session cache isolation for both sources and redact cache payloads from CLI invocation logs.
+
+## 2. Automated regression protection
+
+- [x] 2.1 Add Rust unit coverage for notify, Stop, real null-message Plan transcript fallback, invalid/empty payloads, exact turn isolation, envelope normalization, and argv redaction.
+- [x] 2.2 Add a binary integration regression that simulates a Codex ancestor, writes an ordinary reply followed by the real null-message Plan event shape, and proves current-plan extraction from the same PID.
+
+## 3. User-facing integration guidance
+
+- [x] 3.1 Update localized Integrations settings copy and frontend tests to show the Codex `config.toml` plus `hooks.json` boundary and hook-trust requirement.
+- [x] 3.2 Synchronize English and Chinese install, integration, and demo documentation for dual notify/Stop configuration, platform paths, verification, and troubleshooting.
+- [x] 3.3 Add a named Codex Plan Review manual regression case with `Manual verification:` and the reason the real Codex UI boundary is not fully automated.
+
+## 4. Validation and closeout
+
+- [x] 4.1 Run `cargo test --manifest-path src-tauri/Cargo.toml`.
+- [x] 4.2 Run the targeted Integrations frontend test, `pnpm typecheck`, and `pnpm test:docs`; record any environment-limited validation.
diff --git a/openspec/config.yaml b/openspec/config.yaml
index 729bd97..818e823 100644
--- a/openspec/config.yaml
+++ b/openspec/config.yaml
@@ -38,6 +38,6 @@ rules:
- 变更涉及用户可见流程、agent integration 或持久化行为时,必须包含验证工作。
- 文档或公开使用方式变化时,明确是否同步对应中英文文档。
- bug fix 在可行时补 automated regression;若暂时无法自动化,更新 `docs/regression-cases.md`。
- - `Coverage: manual` 的回归 case 需要 `Manual verification:`;若当前还不适合自动化,再补 `Reason:`。
- - `Coverage: pending` 的回归 case 需要 `Reason:`。
+ - "`Coverage: manual` 的回归 case 需要 `Manual verification:`;若当前还不适合自动化,再补 `Reason:`。"
+ - "`Coverage: pending` 的回归 case 需要 `Reason:`。"
- 变更影响测试标准、回归 case 或 CI 覆盖声明时,纳入 `pnpm test:docs` 验证。
diff --git a/openspec/specs/agent-reply-capture/spec.md b/openspec/specs/agent-reply-capture/spec.md
new file mode 100644
index 0000000..f6acf7b
--- /dev/null
+++ b/openspec/specs/agent-reply-capture/spec.md
@@ -0,0 +1,62 @@
+# agent-reply-capture Specification
+
+## Purpose
+
+Define how cliV captures current agent replies across supported integration transports while preserving session isolation, reviewable content, safe diagnostics, and external configuration ownership.
+
+## Requirements
+
+### Requirement: Codex reply capture accepts notify and Stop hook input
+cliV SHALL preserve the `cliv cache-codex ` notify interface and SHALL also accept a Codex `Stop` lifecycle-hook payload from stdin when `cliv cache-codex` is invoked without a JSON argument.
+
+#### Scenario: Existing notify integration captures a reply
+- **WHEN** `cliv cache-codex` receives an `agent-turn-complete` JSON argument with a non-empty thread id and assistant message
+- **THEN** cliV stores that reply under the originating Codex process identity using the existing cache metadata contract
+
+#### Scenario: Plan mode Stop hook captures a plan
+- **WHEN** `cliv cache-codex` receives a Plan-mode `Stop` payload whose assistant-message field is empty but whose transcript contains an `item_completed` Plan for the same session id and turn id
+- **THEN** cliV stores that exact Plan item as the current reply for the originating Codex process
+
+#### Scenario: Stop hook captures a directly supplied plan
+- **WHEN** a Plan-mode `Stop` payload contains a non-empty last assistant message
+- **THEN** cliV stores that message without reading the transcript
+
+#### Scenario: Ordinary Stop hook captures a reply
+- **WHEN** a valid `Stop` hook payload has a permission mode other than `plan`
+- **THEN** cliV stores its last assistant message through the same cache path
+
+#### Scenario: Invalid or empty input does not replace content
+- **WHEN** the cache command receives malformed JSON, an unsupported event, or neither a non-empty assistant message nor an exact same-session same-turn Plan transcript item
+- **THEN** cliV does not replace the current reply cache with that input
+
+### Requirement: Current Codex reply replaces an earlier turn
+For accepted Codex events, cliV SHALL atomically replace the PID-keyed reply for the originating Codex process while retaining session metadata used for alias resolution.
+
+#### Scenario: Plan follows an ordinary reply in one Codex process
+- **WHEN** cliV captures an ordinary reply and then a valid Plan-mode Stop reply from the same Codex process
+- **THEN** subsequent reply extraction returns the plan rather than the earlier reply
+
+### Requirement: Plan transport envelope is reviewable Markdown
+cliV SHALL remove an exact outer `...` transport envelope before displaying the enclosed plan, without rewriting ordinary Markdown or embedded examples.
+
+#### Scenario: Wrapped plan is captured
+- **WHEN** a Stop message consists of a proposed-plan opening tag, Markdown plan content, and the corresponding closing tag
+- **THEN** cliV stores the enclosed Markdown without the outer transport tags
+
+#### Scenario: Non-envelope content is preserved
+- **WHEN** a reply does not consist entirely of the proposed-plan envelope
+- **THEN** cliV preserves the reply content unchanged
+
+### Requirement: Cache diagnostics do not expose reply payloads
+cliV MUST NOT write complete Codex notify or Stop payloads, user prompts, or assistant messages to its diagnostic log while handling cache commands.
+
+#### Scenario: Notify JSON is supplied on the command line
+- **WHEN** cliV logs invocation details for `cliv cache-codex `
+- **THEN** the log contains only redacted command information and payload length, not the JSON content
+
+### Requirement: Codex hook configuration remains user-owned
+cliV SHALL document the notify and Stop-hook configuration required for complete Codex reply capture and SHALL NOT automatically rewrite Codex configuration files.
+
+#### Scenario: User configures Plan Review capture
+- **WHEN** a user follows cliV's Codex integration instructions
+- **THEN** the instructions retain notify compatibility, add a user-level Stop hook, and require the hook to be reviewed through Codex's hook trust flow
diff --git a/src-tauri/src/cache.rs b/src-tauri/src/cache.rs
index 5cf4b83..9335fb8 100644
--- a/src-tauri/src/cache.rs
+++ b/src-tauri/src/cache.rs
@@ -4,8 +4,8 @@ use crate::process::{collect_parent_processes, resolve_owner_identity, OwnerIden
use serde::Serialize;
use serde_json::Value;
use std::fs;
-use std::io::Read;
-use std::path::PathBuf;
+use std::io::{BufRead, BufReader, Read};
+use std::path::{Path, PathBuf};
fn home_dir() -> PathBuf {
dirs::home_dir().unwrap_or_else(|| PathBuf::from("."))
@@ -104,6 +104,14 @@ fn read_stdin() -> Option {
}
}
+fn agent_process_name_matches(candidate: &str, agent_name: &str) -> bool {
+ Path::new(candidate).components().any(|component| {
+ let value = component.as_os_str().to_string_lossy().to_lowercase();
+ let value = value.strip_suffix(".exe").unwrap_or(&value);
+ value == agent_name || value.starts_with(&format!("{agent_name}-"))
+ })
+}
+
/// Walk PPID chain to find the ancestor agent process PID.
#[cfg(target_os = "linux")]
fn find_ancestor_agent_pid(agent_name: &str) -> Option {
@@ -125,7 +133,7 @@ fn find_ancestor_agent_pid(agent_name: &str) -> Option {
level, pid, comm
));
- if comm.contains(agent_name) {
+ if agent_process_name_matches(&comm, agent_name) {
logging::debug(&format!(
" ancestor: candidate {} match at pid={}",
agent_name, pid
@@ -135,15 +143,13 @@ fn find_ancestor_agent_pid(agent_name: &str) -> Option {
// Fallback: when comm is a generic interpreter (e.g. "node"),
// read /proc/PID/cmdline for the full invocation path.
- if !comm.contains(agent_name) {
+ if !agent_process_name_matches(&comm, agent_name) {
if let Ok(raw) = std::fs::read(format!("/proc/{}/cmdline", pid)) {
- let cmdline = raw
+ let cmdline_matches = raw
.split(|&b| b == 0)
- .map(|seg| String::from_utf8_lossy(seg))
- .collect::>()
- .join(" ")
- .to_lowercase();
- if cmdline.contains(agent_name) {
+ .map(String::from_utf8_lossy)
+ .any(|token| agent_process_name_matches(&token, agent_name));
+ if cmdline_matches {
logging::debug(&format!(
" ancestor: candidate {} cmdline match at pid={}",
agent_name, pid
@@ -213,7 +219,7 @@ fn find_ancestor_agent_pid(agent_name: &str) -> Option {
level, pid, comm
));
- if comm.contains(agent_name) {
+ if agent_process_name_matches(&comm, agent_name) {
logging::debug(&format!(
" ancestor: candidate {} match at pid={}",
agent_name, pid
@@ -313,44 +319,209 @@ fn resolve_agent_pid(
// ─── Codex ────────────────────────────────────────────────
-pub fn cache_codex(json_arg: &str, launch: &LaunchConfig) {
- logging::log(&format!("cache-codex: json_len={}", json_arg.len()));
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+enum CodexPayloadSource {
+ Notify,
+ Stop,
+}
- let data: Value = match serde_json::from_str(json_arg) {
- Ok(v) => v,
- Err(e) => {
- logging::log(&format!("cache-codex: JSON parse error: {}", e));
- return;
+impl CodexPayloadSource {
+ fn as_str(self) -> &'static str {
+ match self {
+ Self::Notify => "notify",
+ Self::Stop => "stop",
}
- };
+ }
+}
- let event_type = data.get("type").and_then(|v| v.as_str()).unwrap_or("");
- logging::debug(&format!("cache-codex: type={}", event_type));
- if event_type != "agent-turn-complete" {
- logging::debug("cache-codex: not agent-turn-complete, skip");
- return;
+#[derive(Debug, PartialEq, Eq)]
+struct CodexCachePayload {
+ source: CodexPayloadSource,
+ session_id: String,
+ turn_id: Option,
+ permission_mode: Option,
+ message: String,
+}
+
+fn non_empty_string(data: &Value, key: &str) -> Option {
+ data.get(key)
+ .and_then(Value::as_str)
+ .filter(|value| !value.trim().is_empty())
+ .map(ToOwned::to_owned)
+}
+
+fn normalize_codex_message(message: &str) -> Option {
+ if message.trim().is_empty() {
+ return None;
}
- let thread_id = match data.get("thread-id").and_then(|v| v.as_str()) {
- Some(id) if !id.is_empty() => id,
- _ => {
- logging::log("cache-codex: no thread-id");
- return;
+ let trimmed = message.trim();
+ if let Some(inner) = trimmed
+ .strip_prefix("")
+ .and_then(|value| value.strip_suffix(""))
+ {
+ let plan = inner.trim_matches(['\r', '\n']);
+ return (!plan.trim().is_empty()).then(|| plan.to_string());
+ }
+
+ Some(message.to_string())
+}
+
+fn read_codex_plan_from_transcript(
+ transcript_path: &Path,
+ session_id: &str,
+ turn_id: &str,
+) -> Result
-
Codex: `~/.codex/config.toml`
+
{t("settings.integrations.codexFiles")}
+
{t("settings.integrations.codexHookTrust")}
Claude: `~/.claude/settings.json`
Gemini: `~/.gemini/settings.json`
diff --git a/src/app/components/__tests__/PersonalizationPanel.test.tsx b/src/app/components/__tests__/PersonalizationPanel.test.tsx
index 72344c4..3b6876a 100644
--- a/src/app/components/__tests__/PersonalizationPanel.test.tsx
+++ b/src/app/components/__tests__/PersonalizationPanel.test.tsx
@@ -171,7 +171,16 @@ describe("PersonalizationPanel", () => {
),
).toBeInTheDocument();
expect(screen.getByText("External agent hook boundary")).toBeInTheDocument();
- expect(screen.getByText("Codex: `~/.codex/config.toml`")).toBeInTheDocument();
+ expect(
+ screen.getByText(
+ "Codex: `~/.codex/config.toml` + `~/.codex/hooks.json`",
+ ),
+ ).toBeInTheDocument();
+ expect(
+ screen.getByText(
+ "Plan Review capture requires a trusted Stop hook; review it with `/hooks` in Codex.",
+ ),
+ ).toBeInTheDocument();
expect(
screen.getByText("Claude: `~/.claude/settings.json`"),
).toBeInTheDocument();
diff --git a/src/lib/locales.ts b/src/lib/locales.ts
index 04f012d..eda59c7 100644
--- a/src/lib/locales.ts
+++ b/src/lib/locales.ts
@@ -157,6 +157,8 @@ const zh = {
"settings.integrations.uiFromLegacy": "当前仍兼容旧 localStorage 偏好;一旦保存设置,就会迁移到统一 config。",
"settings.integrations.agentBoundaryTitle": "外部 Agent Hook 边界",
"settings.integrations.agentBoundaryDesc": "下面这些文件仍由各自的 agent CLI 管理。cliV 只展示边界,不会在这里直接改写它们。",
+ "settings.integrations.codexFiles": "Codex:`~/.codex/config.toml` + `~/.codex/hooks.json`",
+ "settings.integrations.codexHookTrust": "Plan Review 内容捕获需要已信任的 Stop Hook;请在 Codex 中使用 `/hooks` 完成审核。",
// ── Annotation: Card ──
"ann.edit": "编辑",
@@ -388,6 +390,8 @@ const en: Record = {
"settings.integrations.uiFromLegacy": "Legacy localStorage preferences are still honored for compatibility until the first save migrates them into the unified config.",
"settings.integrations.agentBoundaryTitle": "External agent hook boundary",
"settings.integrations.agentBoundaryDesc": "These files are still owned by their respective agent CLIs. cliV shows the boundary here but does not rewrite them directly.",
+ "settings.integrations.codexFiles": "Codex: `~/.codex/config.toml` + `~/.codex/hooks.json`",
+ "settings.integrations.codexHookTrust": "Plan Review capture requires a trusted Stop hook; review it with `/hooks` in Codex.",
// ── Annotation: Card ──
"ann.edit": "Edit",