Skip to content
Merged
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
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,9 @@ __pycache__/
# Plugin-generated context files
# (none currently)

# cross-review worktree tmp
.cross_review/

# Serena MCP runtime data
${SERENA_HOME}/
.serena/logs/
Expand Down
196 changes: 196 additions & 0 deletions issues/PLAN22_cross-review-gemini-result-workspace-constraint.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,196 @@
# PLAN22: cross-review TMP_DIR を worktree 内 `.cross_review/` に統一

- 起票日: 2026-05-24
- 対象 plugin: `ndf` v4.7.4
- 対象 skill: `ndf:cross-review`
- 関連 issue: [GitHub Issue #7](https://github.com/devbasex/ai-plugins/issues/7)
- 関連 plan (先行): PLAN20 (v4.7.3, PR #4 で merge 済み)
- 報告者: takemi-ohama (`devbasex/devbase#25` の `/ndf:cross-review 25` 実行中に検出)

## 背景・課題

### PLAN20 で行ったこと

PLAN20 で `_tmpdir.sh` / `state.py _tmp_dir()` / `monitor.py _tmp_dir()` の
TMP_DIR 解決先を `/tmp/` → `~/.cross_review/<workspace-basename>/` に統一した。
これは launcher / monitor / state 側のパス統一としては正しく機能している。

### PLAN20 で解決できなかったこと

gemini CLI の `write_file` ツールは **workspace ディレクトリ (= worktree) 内**にしか
書き込みできない。`~/.cross_review/` は gemini の設定ディレクトリであって workspace ではないため、
gemini からの書き込みは依然としてブロックされる:

```
Error executing tool write_file: File path must be within one of the workspace directories: /work/worktrees/pr25
```

検証済み:
- `--include-directories "$TMP_DIR"` → 効果なし
- `GEMINI_CLI_TRUST_WORKSPACE=true` → 効果なし (既に設定済み)

gemini はフォールバックとして worktree 直下に `gemini-review-pr<PR>-result.json` を書くが、
`monitor.py` は `~/.cross_review/<basename>/` のみを参照するため `NO_RESULT` (exit 3) を返し、
cross-review ループが止まる。

### 影響

- 各 round でメインセッション側の手動補完が必要
- cross-review の「AI 直接投稿 + メイン context 節約」設計の前提が崩れる
- context 消費増・中断リスク増

## 修正方針

TMP_DIR を `~/.cross_review/<basename>/` (worktree 外) から **`<worktree>/.cross_review/`** (worktree 内) に変更する。

gemini の workspace 制約を根本的に解消でき、monitor.py の fallback コピー等の追加ロジックも不要。

### なぜこれで全体が連動するか

SKILL.md (メインループ) の L171:
```bash
export CROSS_REVIEW_TMP_DIR="$TMP_DIR"
```

`state.py init` が出力する `TMP_DIR` を env 伝播しているため、`state.py _tmp_dir()` の
解決先を変えるだけで全後続スクリプト (launcher / monitor.py) が連動する。
`CROSS_REVIEW_TMP_DIR` env は `_tmpdir.sh` / `monitor.py _tmp_dir()` の最優先パスなので、
それぞれの fallback ロジックは通過しない。

### 変更 1: `state.py _tmp_dir()` — 解決先を worktree 内に変更

**ファイル**: `plugins/ndf/skills/cross-review/scripts/state.py` L60-82

```python
# Before
base_name = pathlib.Path(workspace or os.getcwd()).name
gemini_root = pathlib.Path.home() / ".gemini" / "tmp"
if gemini_root.is_dir() and base_name:
d = gemini_root / base_name

# After
ws = pathlib.Path(workspace or os.getcwd()).resolve()
d = ws / ".cross_review"
```

`workspace` が指定されていれば `<workspace>/.cross_review/`、なければ `<cwd>/.cross_review/` を返す。

### 変更 2: `_tmpdir.sh` — fallback を整合

**ファイル**: `plugins/ndf/skills/cross-review/scripts/_tmpdir.sh`

通常フローでは `CROSS_REVIEW_TMP_DIR` env で上書きされるため到達しないが、
直接実行時の fallback も揃える:

```bash
# Before
gemini_root="$HOME/.gemini/tmp"
if [ -d "$gemini_root" ] && [ -n "$base" ]; then
mkdir -p "$gemini_root/$base"
echo "$gemini_root/$base"

# After
_root="$(git rev-parse --show-toplevel 2>/dev/null || pwd)"
mkdir -p "$_root/.cross_review"
echo "$_root/.cross_review"
```

### 変更 3: `monitor.py _tmp_dir()` — fallback を整合

**ファイル**: `plugins/ndf/skills/cross-review/scripts/monitor.py` L203-220

同様に fallback を `git rev-parse --show-toplevel` ベースの `<worktree-root>/.cross_review/` に変更。

### 変更 4: `.gitignore` — `.cross_review/` 除外

**ファイル**: `.gitignore`

```
# cross-review gemini workspace tmp
.cross_review/
```

cross-review の一時ファイルディレクトリを除外。

### 変更 5: SKILL.md — ドキュメント整合

**ファイル**: `plugins/ndf/skills/cross-review/SKILL.md` L86

「tmp dir は `~/.cross_review/<workspace>/` を採用」の記述を更新。

## 変更対象ファイル一覧

| ファイル | 変更内容 |
|---|---|
| `plugins/ndf/skills/cross-review/scripts/state.py` | `_tmp_dir()` の解決先を `<workspace>/.cross_review/` に変更 |
| `plugins/ndf/skills/cross-review/scripts/_tmpdir.sh` | fallback を `$PWD/.cross_review/` に変更 |
| `plugins/ndf/skills/cross-review/scripts/monitor.py` | fallback を `cwd/.cross_review/` に変更 |
| `.gitignore` | `.cross_review/` 追加 |
| `plugins/ndf/skills/cross-review/SKILL.md` | ドキュメント整合 |

## launch-gemini.sh は変更不要

`launch-gemini.sh` のプロンプト内のパスは `$TMP_DIR/gemini-review-pr$STATE_PR-result.json` と
変数展開している。`$TMP_DIR` が `<worktree>/.cross_review/` に変わることで、
プロンプト上のパスも自動的に worktree 内を指す。gemini はこのパスに書き込み可能。

## 変更 6 (試行): `launch-codex.sh` — sandbox モード緩和

**ファイル**: `plugins/ndf/skills/cross-review/scripts/launch-codex.sh` L101

TMP_DIR が worktree 内に移ることで、codex が workspace 外に書き込む必要がなくなる。
`--dangerously-bypass-approvals-and-sandbox` を `-s workspace-write` に切り替え可能か試行する。

```bash
# Before
codex exec --dangerously-bypass-approvals-and-sandbox \
--config reasoning.effort=medium -C "$WORKTREE" \

# After (試行)
codex exec -s workspace-write \
--config reasoning.effort=medium -C "$WORKTREE" \
```

`codex exec` は非対話モードのため approval bypass は不要と想定。
`-s workspace-write` で `gh api` (ネットワーク + シェル実行) が通るかが判定基準:

- **通る場合**: `-s workspace-write` に切り替え。より安全な sandbox 設定になる
- **通らない場合**: 現行の `--dangerously-bypass-approvals-and-sandbox` を維持

## 変更 7: `/ndf:merged` — worktree クリーンアップ追加

**ファイル**: `plugins/ndf/skills/merged/SKILL.md`

PR マージ後のクリーンアップに、cross-review で作成された worktree の削除を追加する。

現状の手順:
1. PR がマージ済みか確認
2. stash → main checkout → pull
3. feature ブランチ削除 → stash 復元

追加する手順 (3 の前に挿入):
- `git worktree list` で当該 PR 番号に対応する worktree を探す (例: `pr<PR番号>`)
- 見つかれば `git worktree remove <path>` で削除
- worktree 内の `.cross_review/` も worktree ごと消えるため個別削除は不要

## 単一 PR 判定

- 変更ファイル 6〜7 個、差分 ~40〜50 行
- 全変更は TMP_DIR パスの統一 + それに伴う sandbox 緩和 + クリーンアップという 1 つの関心事
- release ブランチ不要。通常の feature branch + PR フローで進行

## ブランチ

```
feature/PLAN22-gemini-result-workspace
```

## テスト計画

- [ ] worktree 上で `/ndf:cross-review` を実行し、gemini が `<worktree>/.cross_review/` に result.json を書くことを確認
- [ ] `monitor.py` が `<worktree>/.cross_review/` の result.json を検出して OK 判定することを確認
- [ ] `state.py read-result` が正常に result.json を読めることを確認
- [ ] `.cross_review/` が `git status` に表示されないことを確認
- [ ] codex 側のフローに影響がないことを確認 (codex は workspace 制約がないが、パスが変わっても動作すること)
- [ ] codex で `-s workspace-write` に切り替え、`gh api` によるレビュー投稿が通ることを確認。通らなければ `--dangerously-bypass-approvals-and-sandbox` を維持
- [ ] `/ndf:merged <PR>` 実行後、対応する worktree (`pr<PR>`) が削除されていることを確認
4 changes: 2 additions & 2 deletions plugins/ndf/skills/cross-review/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ state.json の読み書きや AI launcher 起動・完了待ちは全て委譲
| レビュー投稿 | **AI 自身が `gh api` で PR に直接投稿**。メインはペイロードを保持しない |
| 修正 | **必ずサブエージェント (`general-purpose`) で実行**。メイン context に diff は載せない |
| ユーザ問い合わせ | 自動判断を最大化(`critical`/`major`/`minor` は自動修正、`nit` は最後にまとめて 1 回だけ問い合わせ) |
| 状態の永続化 | `/tmp/cross-review-pr<番号>-state.json` に集約。中断・再開可能 |
| 状態の永続化 | `<worktree>/.cross_review/cross-review-pr<番号>-state.json` に集約。中断・再開可能 |
| 長尺PR対策 | **`--rotate-after` ラウンドで PR をローテーション**(default=light: 同ブランチで PR 巻き直し / squash: 新ブランチ + squash 統合) |
| 振動検知 | 同じ指摘が 2 round で 50%以上重複したら中断 |

Expand Down Expand Up @@ -83,7 +83,7 @@ state.json の読み書きや AI launcher 起動・完了待ちは全て委譲
|---|---|---|
| 1 | 自分の PR 判定(422 回避) | `gh api user` と `gh pr view --json author` を比較し `is_own_pr` / `event_downgrade` を state.json に書く |
| 2 | worktree 分離 | `git worktree add <worktree-base>/pr<PR> <head>` を冪等実行(`<worktree-base>` は `NDF_WORKTREE_BASE` env > `/work/worktrees` > `$HOME/work/worktrees` の優先順で解決) |
| 3 | gemini trusted directory | `launch-gemini.sh` が `GEMINI_CLI_TRUST_WORKSPACE=true` + `--skip-trust` を必ず併用。さらに **tmp dir は `~/.gemini/tmp/<workspace>/`** を採用し、gemini の workspace 制約 (workspace 外の `read_file` / `write_file` がブロックされる) を回避 |
| 3 | gemini trusted directory | `launch-gemini.sh` が `GEMINI_CLI_TRUST_WORKSPACE=true` + `--skip-trust` を必ず併用。**tmp dir は `<worktree>/.cross_review/`** を採用し、gemini の workspace 制約 (workspace 外の `write_file` がブロックされる) を根本回避 |
| 4 | 既存コメント差分 | `gh api .../comments --paginate` を `$TMP_DIR/cross-review-pr<PR>-existing-comments.txt` に保存し、gemini プロンプトには **内容をインライン埋め込み**、codex プロンプトには path を渡す |

### `<worktree-base>` の解決順
Expand Down
4 changes: 2 additions & 2 deletions plugins/ndf/skills/cross-review/docs/02-fix-and-rotation.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,8 @@
4. nit / 判断が割れる minor は **修正せず deferred 記録**(reply は `[deferred / nit]` ラベル付き、Resolve しない)
5. **PR レベルの Summary コメントを `gh pr comment` で投稿**(対応件数 / 重要度別 / deferred 件数 / rejected 件数 / commit SHA を含む)
6. 戻り値ファイル `$TMP_DIR/fix-pr<PR>-result.json` を必ず書き出す
(`$TMP_DIR` は env `CROSS_REVIEW_TMP_DIR` > `~/.gemini/tmp/<workspace>/` > `/tmp/` の順で解決。
詳細は `scripts/state.py _tmp_dir()` 参照。`/tmp/` 直書きでも `state.py merge-fix` は fallback で拾う)
(`$TMP_DIR` は env `CROSS_REVIEW_TMP_DIR` > `<worktree>/.cross_review/` の順で解決。
詳細は `scripts/state.py _tmp_dir()` 参照。`/tmp/` 直書きでも `state.py merge-fix` は legacy fallback で拾う)

> ⚠ inline thread への reply + Resolve **だけでは不十分**。PR ページの
> conversation タブに表示される **PR レベルコメント** がレビュアーへの
Expand Down
17 changes: 6 additions & 11 deletions plugins/ndf/skills/cross-review/scripts/_tmpdir.sh
Original file line number Diff line number Diff line change
Expand Up @@ -7,22 +7,17 @@
#
# 優先順位:
# 1. 環境変数 CROSS_REVIEW_TMP_DIR (明示)
# 2. ~/.gemini/tmp/<cwd-basename>/ (gemini の workspace 制約を回避する公式 path)
# 3. /tmp/ (フォールバック)
# 2. <worktree-root>/.cross_review/ (worktree 内。gemini の workspace 制約を根本回避)

tmpdir() {
if [ -n "${CROSS_REVIEW_TMP_DIR:-}" ]; then
mkdir -p "$CROSS_REVIEW_TMP_DIR"
echo "$CROSS_REVIEW_TMP_DIR"
return
fi
local base
base=$(basename "$PWD")
local gemini_root="$HOME/.gemini/tmp"
if [ -d "$gemini_root" ] && [ -n "$base" ]; then
mkdir -p "$gemini_root/$base"
echo "$gemini_root/$base"
return
fi
echo "/tmp"
# サブディレクトリから呼ばれた場合でも worktree root を正しく特定する
local root
root="$(git rev-parse --show-toplevel 2>/dev/null)" || root="$PWD"
mkdir -p "$root/.cross_review"
echo "$root/.cross_review"
}
6 changes: 4 additions & 2 deletions plugins/ndf/skills/cross-review/scripts/launch-codex.sh
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,10 @@
# gh コマンドに使う「現在のレビュー対象 PR」は state.json の `current_pr` を読む。
#
# tmp ディレクトリは `_tmpdir.sh` の `tmpdir()` 関数で決定:
# CROSS_REVIEW_TMP_DIR env → ~/.gemini/tmp/<workspace>/ → /tmp/
# gemini の workspace 制約を回避するため、`~/.gemini/tmp/...` を優先採用する。
# CROSS_REVIEW_TMP_DIR env → $PWD/.cross_review/
# worktree 内に配置することで gemini の workspace 制約を根本回避。
# codex は --dangerously-bypass-approvals-and-sandbox を維持
# (-s workspace-write は bwrap 非対応環境で失敗するため)。
#
# 状態ファイル: $TMP_DIR/codex-review-pr<STATE_PR>-{result,err,stdout,pid}.json
# (パスは STATE_PR ベースで固定 — monitor.py / state.py と一致させる。)
Expand Down
2 changes: 1 addition & 1 deletion plugins/ndf/skills/cross-review/scripts/launch-gemini.sh
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ SHA=$(gh pr view "$PR" --json headRefOid -q .headRefOid)

PROMPT=$TMP_DIR/gemini-review-pr$STATE_PR-prompt.md
# 既存コメントは **プロンプトにインライン埋め込み** する。
# tmp dir は `~/.gemini/tmp/<workspace>/` を使うようになったが、念のため
# tmp dir は `<worktree>/.cross_review/` を使うが、念のため
# プロンプト埋め込み方式も維持 (gemini が read_file を呼ばずに済むので確実)。
EXISTING_FILE=$TMP_DIR/cross-review-pr$STATE_PR-existing-comments.txt
if [ -s "$EXISTING_FILE" ]; then
Expand Down
30 changes: 19 additions & 11 deletions plugins/ndf/skills/cross-review/scripts/monitor.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
- **FATAL** (auth/quota/sandbox 等の明確な致命): 検知時に kill
- **WARN** (生の `Error:` / `Traceback` 等の曖昧パターン): 警告ログのみ、kill せず通常判定を継続
- `--no-early-error` / `MONITOR_NO_EARLY_ERROR=1` で検知自体を無効化可
4. **result.json**: プロセス終了後に `/tmp/<agent>-review-pr<PR>-result.json` が
4. **result.json**: プロセス終了後に `<worktree>/.cross_review/<agent>-review-pr<PR>-result.json` が
生成されていなければ失敗扱い
5. **hard timeout**: 既定 7 分。`--timeout` または `MONITOR_TIMEOUT` で上書き可
6. **stall timeout**: err.log + stdout.log の合計サイズが一定時間変化しなければ
Expand Down Expand Up @@ -205,21 +205,29 @@ def _tmp_dir() -> pathlib.Path:

state.py の `_tmp_dir()` と同じロジック。優先:
1. `CROSS_REVIEW_TMP_DIR` env
2. `~/.gemini/tmp/<cwd-basename>/` (`~/.gemini/tmp/` が存在するとき)
3. `/tmp/` (フォールバック)
2. `<worktree-root>/.cross_review/` (worktree 内。gemini の workspace 制約を根本回避)

worktree root は `git rev-parse --show-toplevel` で取得する。
サブディレクトリから起動した場合でも一貫したパスを返す。
"""
env = os.environ.get("CROSS_REVIEW_TMP_DIR")
if env:
d = pathlib.Path(env)
d.mkdir(parents=True, exist_ok=True)
return d
base_name = pathlib.Path(os.getcwd()).name
gemini_root = pathlib.Path.home() / ".gemini" / "tmp"
if gemini_root.is_dir() and base_name:
d = gemini_root / base_name
d = pathlib.Path(env).resolve()
d.mkdir(parents=True, exist_ok=True)
return d
return pathlib.Path("/tmp")
# git worktree root を取得。サブディレクトリから起動しても一貫したパスにする。
import subprocess as _sp
r = _sp.run(
["git", "rev-parse", "--show-toplevel"],
capture_output=True, text=True,
)
if r.returncode == 0 and r.stdout.strip():
root = pathlib.Path(r.stdout.strip()).resolve()
else:
root = pathlib.Path.cwd().resolve()
d = root / ".cross_review"
d.mkdir(parents=True, exist_ok=True)
return d


# ---------- データ型 ----------
Expand Down
Loading