diff --git a/.gitignore b/.gitignore index d22db56..ae076d0 100644 --- a/.gitignore +++ b/.gitignore @@ -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/ diff --git a/issues/PLAN22_cross-review-gemini-result-workspace-constraint.md b/issues/PLAN22_cross-review-gemini-result-workspace-constraint.md new file mode 100644 index 0000000..f296657 --- /dev/null +++ b/issues/PLAN22_cross-review-gemini-result-workspace-constraint.md @@ -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//` に統一した。 +これは 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-result.json` を書くが、 +`monitor.py` は `~/.cross_review//` のみを参照するため `NO_RESULT` (exit 3) を返し、 +cross-review ループが止まる。 + +### 影響 + +- 各 round でメインセッション側の手動補完が必要 +- cross-review の「AI 直接投稿 + メイン context 節約」設計の前提が崩れる +- context 消費増・中断リスク増 + +## 修正方針 + +TMP_DIR を `~/.cross_review//` (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` が指定されていれば `/.cross_review/`、なければ `/.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` ベースの `/.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//` を採用」の記述を更新。 + +## 変更対象ファイル一覧 + +| ファイル | 変更内容 | +|---|---| +| `plugins/ndf/skills/cross-review/scripts/state.py` | `_tmp_dir()` の解決先を `/.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` が `/.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`) +- 見つかれば `git worktree remove ` で削除 +- 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 が `/.cross_review/` に result.json を書くことを確認 +- [ ] `monitor.py` が `/.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 ` 実行後、対応する worktree (`pr`) が削除されていることを確認 diff --git a/plugins/ndf/skills/cross-review/SKILL.md b/plugins/ndf/skills/cross-review/SKILL.md index 90dfd0f..fe0d786 100644 --- a/plugins/ndf/skills/cross-review/SKILL.md +++ b/plugins/ndf/skills/cross-review/SKILL.md @@ -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` に集約。中断・再開可能 | +| 状態の永続化 | `/.cross_review/cross-review-pr<番号>-state.json` に集約。中断・再開可能 | | 長尺PR対策 | **`--rotate-after` ラウンドで PR をローテーション**(default=light: 同ブランチで PR 巻き直し / squash: 新ブランチ + squash 統合) | | 振動検知 | 同じ指摘が 2 round で 50%以上重複したら中断 | @@ -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 /pr ` を冪等実行(`` は `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//`** を採用し、gemini の workspace 制約 (workspace 外の `read_file` / `write_file` がブロックされる) を回避 | +| 3 | gemini trusted directory | `launch-gemini.sh` が `GEMINI_CLI_TRUST_WORKSPACE=true` + `--skip-trust` を必ず併用。**tmp dir は `/.cross_review/`** を採用し、gemini の workspace 制約 (workspace 外の `write_file` がブロックされる) を根本回避 | | 4 | 既存コメント差分 | `gh api .../comments --paginate` を `$TMP_DIR/cross-review-pr-existing-comments.txt` に保存し、gemini プロンプトには **内容をインライン埋め込み**、codex プロンプトには path を渡す | ### `` の解決順 diff --git a/plugins/ndf/skills/cross-review/docs/02-fix-and-rotation.md b/plugins/ndf/skills/cross-review/docs/02-fix-and-rotation.md index 40e9239..f198476 100644 --- a/plugins/ndf/skills/cross-review/docs/02-fix-and-rotation.md +++ b/plugins/ndf/skills/cross-review/docs/02-fix-and-rotation.md @@ -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-result.json` を必ず書き出す - (`$TMP_DIR` は env `CROSS_REVIEW_TMP_DIR` > `~/.gemini/tmp//` > `/tmp/` の順で解決。 - 詳細は `scripts/state.py _tmp_dir()` 参照。`/tmp/` 直書きでも `state.py merge-fix` は fallback で拾う) + (`$TMP_DIR` は env `CROSS_REVIEW_TMP_DIR` > `/.cross_review/` の順で解決。 + 詳細は `scripts/state.py _tmp_dir()` 参照。`/tmp/` 直書きでも `state.py merge-fix` は legacy fallback で拾う) > ⚠ inline thread への reply + Resolve **だけでは不十分**。PR ページの > conversation タブに表示される **PR レベルコメント** がレビュアーへの diff --git a/plugins/ndf/skills/cross-review/scripts/_tmpdir.sh b/plugins/ndf/skills/cross-review/scripts/_tmpdir.sh index 8601615..8df7371 100755 --- a/plugins/ndf/skills/cross-review/scripts/_tmpdir.sh +++ b/plugins/ndf/skills/cross-review/scripts/_tmpdir.sh @@ -7,8 +7,7 @@ # # 優先順位: # 1. 環境変数 CROSS_REVIEW_TMP_DIR (明示) -# 2. ~/.gemini/tmp// (gemini の workspace 制約を回避する公式 path) -# 3. /tmp/ (フォールバック) +# 2. /.cross_review/ (worktree 内。gemini の workspace 制約を根本回避) tmpdir() { if [ -n "${CROSS_REVIEW_TMP_DIR:-}" ]; then @@ -16,13 +15,9 @@ tmpdir() { 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" } diff --git a/plugins/ndf/skills/cross-review/scripts/launch-codex.sh b/plugins/ndf/skills/cross-review/scripts/launch-codex.sh index 810ef28..22b4261 100755 --- a/plugins/ndf/skills/cross-review/scripts/launch-codex.sh +++ b/plugins/ndf/skills/cross-review/scripts/launch-codex.sh @@ -8,8 +8,10 @@ # gh コマンドに使う「現在のレビュー対象 PR」は state.json の `current_pr` を読む。 # # tmp ディレクトリは `_tmpdir.sh` の `tmpdir()` 関数で決定: -# CROSS_REVIEW_TMP_DIR env → ~/.gemini/tmp// → /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-{result,err,stdout,pid}.json # (パスは STATE_PR ベースで固定 — monitor.py / state.py と一致させる。) diff --git a/plugins/ndf/skills/cross-review/scripts/launch-gemini.sh b/plugins/ndf/skills/cross-review/scripts/launch-gemini.sh index 9e24c9e..b1ccc72 100755 --- a/plugins/ndf/skills/cross-review/scripts/launch-gemini.sh +++ b/plugins/ndf/skills/cross-review/scripts/launch-gemini.sh @@ -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//` を使うようになったが、念のため +# tmp dir は `/.cross_review/` を使うが、念のため # プロンプト埋め込み方式も維持 (gemini が read_file を呼ばずに済むので確実)。 EXISTING_FILE=$TMP_DIR/cross-review-pr$STATE_PR-existing-comments.txt if [ -s "$EXISTING_FILE" ]; then diff --git a/plugins/ndf/skills/cross-review/scripts/monitor.py b/plugins/ndf/skills/cross-review/scripts/monitor.py index ae57bb3..8a1be70 100755 --- a/plugins/ndf/skills/cross-review/scripts/monitor.py +++ b/plugins/ndf/skills/cross-review/scripts/monitor.py @@ -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/-review-pr-result.json` が + 4. **result.json**: プロセス終了後に `/.cross_review/-review-pr-result.json` が 生成されていなければ失敗扱い 5. **hard timeout**: 既定 7 分。`--timeout` または `MONITOR_TIMEOUT` で上書き可 6. **stall timeout**: err.log + stdout.log の合計サイズが一定時間変化しなければ @@ -205,21 +205,29 @@ def _tmp_dir() -> pathlib.Path: state.py の `_tmp_dir()` と同じロジック。優先: 1. `CROSS_REVIEW_TMP_DIR` env - 2. `~/.gemini/tmp//` (`~/.gemini/tmp/` が存在するとき) - 3. `/tmp/` (フォールバック) + 2. `/.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 # ---------- データ型 ---------- diff --git a/plugins/ndf/skills/cross-review/scripts/state.py b/plugins/ndf/skills/cross-review/scripts/state.py index 3a3df80..f1c6301 100755 --- a/plugins/ndf/skills/cross-review/scripts/state.py +++ b/plugins/ndf/skills/cross-review/scripts/state.py @@ -5,7 +5,7 @@ # /// """cross-review state.json 操作 CLI。 -`/tmp/cross-review-pr-state.json` の初期化 / 読み書きと、 +`/.cross_review/cross-review-pr-state.json` の初期化 / 読み書きと、 ループ判定(round 開始 / 収束 / 振動 / PR ローテーション要否 / fix 結果マージ / deferred nit レポート)を 1 つの CLI に集約する。 @@ -57,29 +57,40 @@ def _default_worktree_base() -> pathlib.Path: return pathlib.Path.home() / "work" / "worktrees" +def _git_toplevel() -> str | None: + """git worktree root を取得する。失敗時は None を返す。""" + try: + r = subprocess.run( + ["git", "rev-parse", "--show-toplevel"], + capture_output=True, text=True, + ) + if r.returncode == 0 and r.stdout.strip(): + return r.stdout.strip() + except OSError: + pass + return None + + def _tmp_dir(workspace: str | None = None) -> pathlib.Path: """cross-review 用 tmp ディレクトリを決定する。 優先順位: 1. 環境変数 `CROSS_REVIEW_TMP_DIR` (明示) - 2. `~/.gemini/tmp//` (gemini workspace 制約を回避するため、 - `~/.gemini/tmp/` が存在するなら自動使用) - 3. `/tmp/` (フォールバック) + 2. `/.cross_review/` (worktree 内。gemini の workspace 制約を根本回避) - `workspace` 未指定なら `os.getcwd()` の basename を使う。 + `workspace` 未指定なら `git rev-parse --show-toplevel` で worktree root を + 取得する。サブディレクトリから実行してもパス不一致が発生しない。 + git コマンドが失敗した場合のみ `os.getcwd()` にフォールバックする。 """ 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(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 + d = pathlib.Path(env).resolve() d.mkdir(parents=True, exist_ok=True) return d - return pathlib.Path("/tmp") + ws = pathlib.Path(workspace or _git_toplevel() or os.getcwd()).resolve() + d = ws / ".cross_review" + d.mkdir(parents=True, exist_ok=True) + return d def _state_path(pr: int) -> pathlib.Path: @@ -244,20 +255,19 @@ def cmd_init(args: argparse.Namespace) -> None: """Step 0 — state 初期化 or 既存 state 引き継ぎ + プリチェック。""" pr = args.pr # worktree path を先に解決してから tmp_dir を決定する。 - # gemini の workspace 制約 (~/.gemini/tmp/) と - # 一致させるため、worktree basename ベースで tmp_dir を計算する必要がある。 - # 旧実装は _tmp_dir(args.worktree) を args.worktree=None のまま呼び、 - # os.getcwd() の basename (= 親リポジトリ名) を採用していたため、 - # launch-gemini.sh で `cd $WORKTREE` した後の gemini が - # `~/.gemini/tmp/` への write をブロックして hard timeout していた。 + # tmp_dir は /.cross_review/ に配置し、gemini の workspace 制約を根本回避。 worktree = args.worktree or str(_default_worktree_base() / f"pr{pr}") - tmp_dir = _tmp_dir(worktree) - state_file = tmp_dir / f"cross-review-pr{pr}-state.json" - # 再開 - if state_file.exists(): - st = json.loads(state_file.read_text()) + # worktree 存在チェック用: _tmp_dir() は mkdir するため、先に呼ぶと + # worktree ディレクトリが副作用で作成され exists() が常に true になる。 + # そのため _tmp_dir() 呼び出しは worktree 作成/確認の後に行う。 + + # 再開チェック: state ファイルの存在確認は _tmp_dir() を使わず直接パスを組む + resume_state_file = pathlib.Path(worktree) / ".cross_review" / f"cross-review-pr{pr}-state.json" + if resume_state_file.exists(): + st = json.loads(resume_state_file.read_text()) if st.get("final") is None: + tmp_dir = _tmp_dir(worktree) wt = st.get("worktree_path") or "" info(f"↻ 前回中断 state から再開(round={len(st.get('rounds', []))})") print(f"PR={st['current_pr']}") @@ -274,7 +284,7 @@ def cmd_init(args: argparse.Namespace) -> None: if is_own: info(f"⚠ 自分の PR (author={me}) — REQUEST_CHANGES → COMMENT 強制ダウングレード") - # worktree 分離 + # worktree 分離 — _tmp_dir() より先に worktree を作成/確認する head_branch = _sh(["gh", "pr", "view", str(pr), "--json", "headRefName", "--jq", ".headRefName"]) base_branch = _sh(["gh", "pr", "view", str(pr), "--json", "baseRefName", "--jq", ".baseRefName"]) if not pathlib.Path(worktree).exists(): @@ -289,6 +299,10 @@ def cmd_init(args: argparse.Namespace) -> None: else: info(f"↻ 既存 worktree 流用: {worktree}") + # worktree 作成/確認後に _tmp_dir() を呼ぶ (ここで .cross_review/ が作られる) + tmp_dir = _tmp_dir(worktree) + state_file = tmp_dir / f"cross-review-pr{pr}-state.json" + # 既存コメントスナップショット(重複指摘防止)。 # NOTE: `gh api --paginate` は REST のページごとに **JSON 配列が連続して** stdout に出る # ため、`json.loads(r.stdout)` は複数ページで JSONDecodeError になり、コメントが空に diff --git a/plugins/ndf/skills/merged/SKILL.md b/plugins/ndf/skills/merged/SKILL.md index 5ceebb7..202450c 100644 --- a/plugins/ndf/skills/merged/SKILL.md +++ b/plugins/ndf/skills/merged/SKILL.md @@ -17,7 +17,8 @@ PRマージ後のクリーンアップを実行。 0. **事前確認**: github mcpで引数の(引数が無ければ自身が作成した最新の)PRがmainにmergeされていることを確認。mergeされていなければ終了 1. **事前確認**: `git status`→変更あればstash 2. **main更新**: `git checkout main`→`git pull` -3. **ブランチ削除**: `git branch -d ` → stash復元 +3. **worktreeクリーンアップ**: `git worktree list` で当該PR番号に対応する worktree (`pr`) を探し、あれば `git worktree remove ` で削除(worktree 内の `.cross_review/` も一緒に消える) +4. **ブランチ削除**: `git branch -d ` → stash復元 **注意**: 冪等性保証・エラー時中断・削除済み無視