diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index a921715..63ce633 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -8,7 +8,7 @@ { "name": "ndf", "source": "./plugins/ndf", - "description": "All-in-one plugin (v4.14.0): 8 specialized agents (model-tiered), 48 skills including pytest-playwright E2E testing (7 focused skills: test-planning, script-creation, execution, report, kit-ops, browser-connect, evidence-drive), CDP remote browser support, Google Drive evidence archival, ML model structure standard, PR/review workflows, principles, data analysis, Codex/Gemini CLI integration, skill usage stats, statusline switcher (/ndf:statusline). SessionStart hook (transcript retention >= 90 days, default statusline when none is configured), Stop hook (AI-summarized Slack notifications)." + "description": "All-in-one plugin (v4.15.0): 8 specialized agents (model-tiered), 48 skills including pytest-playwright E2E testing (7 focused skills: test-planning, script-creation, execution, report, kit-ops, browser-connect, evidence-drive), CDP remote browser support, Google Drive evidence archival, ML model structure standard, PR/review workflows, principles, data analysis, Codex/Gemini CLI integration, skill usage stats, statusline switcher (/ndf:statusline). SessionStart hook (transcript retention >= 90 days, default statusline when none is configured), Stop hook (AI-summarized Slack notifications)." }, { "name": "affaan-m", diff --git a/AGENTS.md b/AGENTS.md index fa10bba..d0cb2aa 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -62,7 +62,7 @@ ai-plugins/ ## NDFプラグインについて -**NDFプラグイン**は、このマーケットプレイスの主要プラグインです(v4.14.0): +**NDFプラグイン**は、このマーケットプレイスの主要プラグインです(v4.15.0): - 8個の専門サブエージェント(director、data-analyst、corder、researcher、qa、debugger、devops-engineer、code-reviewer) - 48個のSkills(PR/コードレビューワークフロー、AIクロスレビュー (codex/gemini)、原則・ガイドライン、MLモデル構造標準 (ml-model-structure)、issue→multi-PR ワークフロー戦略、SQL最適化、データエクスポート、skill利用統計、statusline切替 (/ndf:statusline)、Codex CLI連携、Playwrightシナリオテスト (CDPリモート接続・Google Driveエビデンス保管含む)、Google Drive/Chat連携 等) - SessionStartフック(transcript保持期間自動管理 `cleanupPeriodDays >= 90`、statusLine未設定時のNDF標準statusline自動設定) diff --git a/docs/ndf-plugin-reference.md b/docs/ndf-plugin-reference.md index abfa7a9..4fe532f 100644 --- a/docs/ndf-plugin-reference.md +++ b/docs/ndf-plugin-reference.md @@ -4,7 +4,7 @@ NDF プラグインは、Claude Code / Kiro CLI 向けのオールインワン開発支援プラグイン。エージェント、Skills、フックを統合して提供する。 -**現行バージョン**: **v4.14.0** — `statusline` skill とデフォルト statusline 設定 hook を追加(47→48個)。statusLine 未設定時のみ NDF 標準 statusline(コンテナ名/ホスト名 + project_dir + コンテキスト使用率)を自動設定し、`/ndf:statusline set|restore|status` で切り替え・復元できる。**v4.13.0** で `issue-plan-strategy` の release PR body を self-contained 必須化。**v4.12.0** で Playwright E2E に `/ndf:playwright-browser-connect`(CDP リモートブラウザ接続)と `/ndf:playwright-evidence-drive`(Google Drive エビデンスアーカイブ)の 2 skill を追加(45→47個)。直前の **v4.11.0** で `/ndf:cross-review` の堅牢性改善(monitor.py の EARLY_ERROR 誤検知を解消: テスト用文字列リテラル / grep 形式ソース引用行を benign 自動判定し、ループ終了時の最終スイープで残 open review thread を全 Resolve)を実施。**v4.10.0** で `ml-model-structure` skill(MLモデル構築・推論API開発の標準ディレクトリ構造: 版内feature SSoT / train↔serve契約)を追加。`/ndf:fix` の修正ポリシー刷新(minor/nit のうち performance/readability/duplication は積極修正、+30 行超は要問い合わせ)、CI 完了待ち廃止、PR範囲外 flaky テストも修正対象。`/ndf:cross-review` 内のサブエージェントプロンプトも同期。重要度ラベルは AI agent の付与を鵜呑みにせず独自再判定。完了報告には PR URL 必須。詳細は [CHANGELOG.md](../plugins/ndf/CHANGELOG.md)。`/ndf:codex` skill + `corder` エージェント経由の Codex CLI 直接実行に一本化、Serena MCP は別プラグイン `mcp-serena` に分離済み、Playwright シナリオ E2E、Google Drive / Chat 連携 skill を提供。 +**現行バージョン**: **v4.15.0** — cross-review の worktree 生成先を非永続領域 `<システム tmpdir>/ndf-worktrees/--/pr` に変更(永続 volume 消費・他リポジトリの PR 番号衝突・残骸流用事故を解消。`NDF_WORKTREE_BASE` env で明示オーバーライド可)。**v4.14.0** で `statusline` skill とデフォルト statusline 設定 hook を追加(47→48個)。statusLine 未設定時のみ NDF 標準 statusline(コンテナ名/ホスト名 + project_dir + コンテキスト使用率)を自動設定し、`/ndf:statusline set|restore|status` で切り替え・復元できる。**v4.13.0** で `issue-plan-strategy` の release PR body を self-contained 必須化。**v4.12.0** で Playwright E2E に `/ndf:playwright-browser-connect`(CDP リモートブラウザ接続)と `/ndf:playwright-evidence-drive`(Google Drive エビデンスアーカイブ)の 2 skill を追加(45→47個)。直前の **v4.11.0** で `/ndf:cross-review` の堅牢性改善(monitor.py の EARLY_ERROR 誤検知を解消: テスト用文字列リテラル / grep 形式ソース引用行を benign 自動判定し、ループ終了時の最終スイープで残 open review thread を全 Resolve)を実施。**v4.10.0** で `ml-model-structure` skill(MLモデル構築・推論API開発の標準ディレクトリ構造: 版内feature SSoT / train↔serve契約)を追加。`/ndf:fix` の修正ポリシー刷新(minor/nit のうち performance/readability/duplication は積極修正、+30 行超は要問い合わせ)、CI 完了待ち廃止、PR範囲外 flaky テストも修正対象。`/ndf:cross-review` 内のサブエージェントプロンプトも同期。重要度ラベルは AI agent の付与を鵜呑みにせず独自再判定。完了報告には PR URL 必須。詳細は [CHANGELOG.md](../plugins/ndf/CHANGELOG.md)。`/ndf:codex` skill + `corder` エージェント経由の Codex CLI 直接実行に一本化、Serena MCP は別プラグイン `mcp-serena` に分離済み、Playwright シナリオ E2E、Google Drive / Chat 連携 skill を提供。 ## ディレクトリ構造 @@ -206,3 +206,4 @@ claude -p --settings '{"disableAllHooks": true, "disableAllPlugins": true}' --ou | **v4.12.1** | `/ndf:cross-review` デフォルト調整: `--max-rounds` 6→12 / `--rotate-after` 5→8、`--rotate-mode squash` 説明から「既存挙動」表記を削除 | | **v4.13.0** | `issue-plan-strategy`: release PR body の self-contained 必須化 (レビュアー視点の原則明文化、子 PR チェックリストを `
` に格下げ、Ready 前の body 最終化ステップ追加) | | **v4.14.0** | `statusline` skill + デフォルト statusline 設定 hook 追加 (47→48個)。statusLine 未設定時のみ NDF 標準 statusline を自動設定、`/ndf:statusline set\|restore\|status` で切替・復元 | +| **v4.15.0** | cross-review: worktree 生成先を `<システム tmpdir>/ndf-worktrees/--/pr` に変更 (非永続化 + リポジトリ別分離)。未登録パスの残骸は `.stale-` に退避して作り直すガード追加 | diff --git a/plugins/ndf/.claude-plugin/plugin.json b/plugins/ndf/.claude-plugin/plugin.json index c8f90a8..4dc8aea 100644 --- a/plugins/ndf/.claude-plugin/plugin.json +++ b/plugins/ndf/.claude-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "ndf", - "version": "4.14.0", + "version": "4.15.0", "description": "Integrated plugin with 8 specialized agents (model-tiered: opus/sonnet/haiku), 48 skills including official mcp-builder, on-demand loader for Anthropic official skills, generic workflow/principle skills, ML model structure standard (ml-model-structure), skill usage statistics, pytest-playwright E2E testing split into 7 focused skills (test-planning, script-creation, execution, report, kit-ops, browser-connect, evidence-drive) + orchestrator with video-by-default evidence, CDP remote browser support, and Google Drive evidence archival, Google Drive/Chat integration, and Codex CLI integration via /ndf:codex skill. Transcript retention is automatically kept at >= 90 days. Default statusline (container/host name + project dir + context usage) is set when none is configured, switchable via /ndf:statusline. Serena MCP is a separate plugin (mcp-serena).", "author": { "name": "takemi-ohama", diff --git a/plugins/ndf/CHANGELOG.md b/plugins/ndf/CHANGELOG.md index bdea5a0..b43b784 100644 --- a/plugins/ndf/CHANGELOG.md +++ b/plugins/ndf/CHANGELOG.md @@ -1,5 +1,26 @@ # NDF Plugin CHANGELOG +### v4.15.0 (cross-review: worktree を非永続領域 + リポジトリ別パスに変更) + +cross-review の worktree 生成先 `/work/worktrees/pr` が共有の永続 volume 上にあり、 +(1) 別リポジトリの同一 PR 番号と衝突する (2) 明示削除しないと残り続ける +(3) volume スペースを消費する、という問題に対応。実際に別プロジェクトの残骸 +worktree を流用して git 操作が壊れる事故が発生した。 + +- **デフォルト生成先を `<システム tmpdir>/ndf-worktrees` に変更** (`state.py`): + - 解決順: `NDF_WORKTREE_BASE` env > `tempfile.gettempdir()/ndf-worktrees` + - 非永続領域のためコンテナ再作成で自動消滅し、明示削除も不要 + - 旧 `/work/worktrees` / `$HOME/work/worktrees` フォールバックは廃止 +- **パスにリポジトリ slug を含める**: `/--/pr` 形式とし、 + 他リポジトリの同一 PR 番号と衝突しないようにした +- **残骸 worktree の流用ガード追加**: パスが存在しても `git worktree list` に + 登録されていなければ `.stale-` に退避して作り直す + (`_is_registered_worktree()` / `_create_worktree()` に分離) +- **テスト更新**: `test_default_worktree_base.py` を新解決順 + repo slug + + 残骸ガードのテストに刷新 (102 passed) +- **plugin.json: version 4.14.0 → 4.15.0**。marketplace.json / AGENTS.md / + ndf-plugin-reference の version 表記を整合。 + ### v4.14.0 (statusline skill + デフォルト statusline 設定 hook 追加) コンテナ名 (非コンテナ環境ではホスト名) + project_dir + コンテキスト使用率を表示する diff --git a/plugins/ndf/skills/cross-review/SKILL.md b/plugins/ndf/skills/cross-review/SKILL.md index 572fa27..8ba321a 100644 --- a/plugins/ndf/skills/cross-review/SKILL.md +++ b/plugins/ndf/skills/cross-review/SKILL.md @@ -88,7 +88,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` の優先順で解決) | +| 2 | worktree 分離 | `git worktree add /--/pr ` を冪等実行(`` は `NDF_WORKTREE_BASE` env > `<システム tmpdir>/ndf-worktrees` の優先順で解決)。パスが存在しても現リポジトリの登録済み worktree でなければ `.stale-` に退避して作り直す | | 3 | gemini trusted directory | `launch-gemini.sh` が `GEMINI_CLI_TRUST_WORKSPACE=true` + `--skip-trust` を必ず併用。**tmp dir は `/.cross_review/`** を採用し、gemini の workspace 制約 (workspace 外の `write_file` がブロックされる) を根本回避 | | 4 | 既存コメント差分 | `fix/scripts/fetch-pr-comments.sh` で 3 ソース (インラインコメント / レビュー body / PR レベルコメント) を一括取得し `$TMP_DIR/cross-review-pr-existing-comments.txt` に保存。gemini プロンプトには **内容をインライン埋め込み**、codex プロンプトには path を渡す | @@ -97,8 +97,14 @@ state.json の読み書きや AI launcher 起動・完了待ちは全て委譲 `state.py init` は worktree の親ディレクトリを以下の優先順で解決する: 1. `NDF_WORKTREE_BASE` 環境変数(明示オーバーライド) -2. `/work/worktrees`(Linux コンテナ環境互換。書き込み可能ならこちらを使用) -3. `$HOME/work/worktrees`(macOS / WSL 等のフォールバック) +2. `<システム tmpdir>/ndf-worktrees`(Python `tempfile.gettempdir()`。非永続領域のため + コンテナ再作成で自動消滅し、共有 volume を消費しない) + +worktree の実パスは `/--/pr` 形式で、リポジトリ slug を含める +ことで**他リポジトリの同一 PR 番号と衝突しない**。永続 volume(旧 `/work/worktrees`)を +使っていた頃は別プロジェクトの残骸 worktree を誤って流用する事故があったため、 +パスが存在しても `git worktree list` に登録されていなければ `.stale-` に +退避して作り直すガードも入っている。 解決した実パスは `state.json` の `worktree_path` に書かれるため、後続スクリプトや サブエージェント prompt は state.json から読めば追従できる。 diff --git a/plugins/ndf/skills/cross-review/docs/01-state-and-review.md b/plugins/ndf/skills/cross-review/docs/01-state-and-review.md index 4c6dbb9..3088617 100644 --- a/plugins/ndf/skills/cross-review/docs/01-state-and-review.md +++ b/plugins/ndf/skills/cross-review/docs/01-state-and-review.md @@ -30,7 +30,7 @@ "rotate_after": 8, "only": null, "current_pr": 123, - "worktree_path": "/work/worktrees/pr123", + "worktree_path": "/tmp/ndf-worktrees/owner--name/pr123", "repo": "owner/name", "head_branch": "feature/foo", "base_branch": "main", @@ -98,7 +98,7 @@ cd "$WORKTREE" 1. 既存 state.json があり `final == null` なら再開 2. 自分の PR 判定(`gh api user` と `gh pr view --json author` を比較) -3. worktree 作成(`/pr`。`` は `NDF_WORKTREE_BASE` env > `/work/worktrees` > `$HOME/work/worktrees` の優先順で解決。実 path は state.json の `worktree_path` を参照) +3. worktree 作成(`/--/pr`。`` は `NDF_WORKTREE_BASE` env > `<システム tmpdir>/ndf-worktrees` の優先順で解決。既存パスが現リポジトリの登録済み worktree でなければ `.stale-` に退避して作り直す。実 path は state.json の `worktree_path` を参照) 4. 既存コメントスナップショット (`fix/scripts/fetch-pr-comments.sh` で 3 ソース一括取得) → `$TMP_DIR/cross-review-pr-existing-comments.txt` 5. state.json 書き出し @@ -173,7 +173,7 @@ fi launcher が生成するプロンプトに以下を強制している: - **headRefOid (commit_id) を明示**: AI が自前で取得すると baseRefOid を誤って入れる事故が多発 -- **作業 worktree の絶対パス**: 「ファイル読み取りは必ず worktree 配下の絶対パスを使う」(実 path は state.json の `worktree_path` を参照。`` は `NDF_WORKTREE_BASE` env > `/work/worktrees` > `$HOME/work/worktrees` の優先順で解決) +- **作業 worktree の絶対パス**: 「ファイル読み取りは必ず worktree 配下の絶対パスを使う」(実 path は state.json の `worktree_path` を参照。`` は `NDF_WORKTREE_BASE` env > `<システム tmpdir>/ndf-worktrees` の優先順で解決) - **event ダウングレード警告**: `event_downgrade=true` のときは payload の `event` を `COMMENT` に - **既存コメント差分**: `$TMP_DIR/cross-review-pr-existing-comments.txt` を読んで重複指摘禁止 - **review body 先頭 prefix**: 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 52d749d..aaaecc3 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 @@ -209,7 +209,7 @@ state.json から旧 PR / worktree を解決し、以下の素材を 1 つの JS "state_pr": 217, "old_pr": 217, "old_pr_url": "https://github.com/.../pull/217", - "worktree_path": "/work/worktrees/pr217", + "worktree_path": "/tmp/ndf-worktrees/owner--name/pr217", "head_branch": "feature/...", "base_branch": "release/...", "is_draft": true, diff --git a/plugins/ndf/skills/cross-review/scripts/state.py b/plugins/ndf/skills/cross-review/scripts/state.py index bf5524e..d20b1b2 100755 --- a/plugins/ndf/skills/cross-review/scripts/state.py +++ b/plugins/ndf/skills/cross-review/scripts/state.py @@ -32,33 +32,81 @@ import shlex import subprocess import sys +import tempfile +import time from typing import Any # ---------------- helpers ---------------- def _default_worktree_base() -> pathlib.Path: - """worktree の親ディレクトリを環境に応じて解決する。 + """worktree の親ディレクトリを解決する。 優先順位: 1. 環境変数 NDF_WORKTREE_BASE (明示オーバーライド) - 2. /work/worktrees (Linux コンテナ環境互換、書き込み可能ならそれを使う) - 3. $HOME/work/worktrees (macOS / WSL 等のフォールバック) + 2. <システム tmpdir>/ndf-worktrees (非永続領域。コンテナ再作成で自動消滅) + + かつての /work/worktrees ($HOME/work/worktrees) は共有の永続 volume 上にあり、 + 別リポジトリの pr と衝突する・明示削除が必要・volume を消費する問題が + あったため廃止した。 """ env = os.environ.get("NDF_WORKTREE_BASE") if env: - return pathlib.Path(env) - legacy = pathlib.Path("/work/worktrees") - try: - legacy.mkdir(parents=True, exist_ok=True) - if not os.access(legacy, os.W_OK): - info(f"⚠ worktree ベースディレクトリに書き込み権限がありません: {legacy} — フォールバック") - else: - # mkdir 成功 + 書き込み可能 → 既存環境互換でこちらを使う - return legacy - except OSError: - pass - return pathlib.Path.home() / "work" / "worktrees" + # 相対パスのまま state.json に保存されると後続のパス比較が壊れるため、 + # 常に絶対パスへ解決して返す。 + return pathlib.Path(env).resolve() + return pathlib.Path(tempfile.gettempdir()) / "ndf-worktrees" + + +def _repo_slug(repo: str) -> str: + """`owner/name` を path-safe なディレクトリ名 `owner--name` に変換する。""" + return repo.replace("/", "--") + + +def _is_registered_worktree(path: str) -> bool: + """path が現リポジトリに登録済みの worktree かどうか。 + + パスが存在しても別リポジトリの残骸や git 管理外ディレクトリの可能性があり、 + 流用すると git 操作が壊れるため、流用前に必ずこれで検証する。 + """ + out = _sh(["git", "worktree", "list", "--porcelain"], check=False) + target = str(pathlib.Path(path).resolve()) + return any(line == f"worktree {target}" for line in out.splitlines()) + + +def _create_worktree(worktree: str, pr: int, head_branch: str) -> None: + """origin/ から detached worktree を作成する (フォーク PR はフォールバック)。""" + pathlib.Path(worktree).parent.mkdir(parents=True, exist_ok=True) + # フォーク PR の場合 origin に head_branch がないことがある。 + # fetch 失敗時は gh pr checkout --detach でフォールバックする。 + fetch_result = subprocess.run( + ["git", "fetch", "origin", head_branch], + capture_output=True, text=True, + ) + if fetch_result.returncode == 0: + # head branch が既に別の worktree で checkout されている場合を避けるため + # detached で展開する。cross-review はファイル参照しかしないので問題ない。 + _sh(["git", "worktree", "add", "--detach", worktree, f"origin/{head_branch}"]) + info(f"✅ worktree 作成 (detached @ origin/{head_branch}): {worktree}") + else: + info(f"⚠ git fetch origin {head_branch} 失敗 (フォーク PR の可能性) — gh pr checkout でフォールバック") + _sh(["git", "worktree", "add", "--detach", worktree, "HEAD"]) + # worktree 内で gh pr checkout を実行して正しいコミットに切り替え + checkout_result = subprocess.run( + ["gh", "pr", "checkout", str(pr), "--detach"], + capture_output=True, text=True, + cwd=worktree, + ) + if checkout_result.returncode != 0: + # HEAD (親コミット) 指向のまま残すと、次回実行時に + # _is_registered_worktree() を通過して不正流用されるため、 + # die() の前に作成済み worktree をロールバックする。 + subprocess.run( + ["git", "worktree", "remove", "--force", worktree], + capture_output=True, text=True, + ) + die(f"gh pr checkout --detach #{pr} 失敗: {checkout_result.stderr.strip()}") + info(f"✅ worktree 作成 (gh pr checkout --detach #{pr}): {worktree}") def _git_toplevel() -> str | None: @@ -287,7 +335,10 @@ def cmd_init(args: argparse.Namespace) -> None: pr = args.pr # worktree path を先に解決してから tmp_dir を決定する。 # tmp_dir は /.cross_review/ に配置し、gemini の workspace 制約を根本回避。 - worktree = str(pathlib.Path(args.worktree).resolve()) if args.worktree else str(_default_worktree_base() / f"pr{pr}") + # path には repo slug を含め、他リポジトリの同一 PR 番号と衝突しないようにする。 + repo = _sh(["gh", "repo", "view", "--json", "nameWithOwner", "-q", ".nameWithOwner"]) + worktree = str(pathlib.Path(args.worktree).resolve()) if args.worktree else str( + _default_worktree_base() / _repo_slug(repo) / f"pr{pr}") # worktree 存在チェック用: _tmp_dir() は mkdir するため、先に呼ぶと # worktree ディレクトリが副作用で作成され exists() が常に true になる。 @@ -331,31 +382,16 @@ def cmd_init(args: argparse.Namespace) -> None: 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(): - # フォーク PR の場合 origin に head_branch がないことがある。 - # fetch 失敗時は gh pr checkout --detach でフォールバックする。 - fetch_result = subprocess.run( - ["git", "fetch", "origin", head_branch], - capture_output=True, text=True, - ) - if fetch_result.returncode == 0: - # head branch が既に別の worktree で checkout されている場合を避けるため - # detached で展開する。cross-review はファイル参照しかしないので問題ない。 - _sh(["git", "worktree", "add", "--detach", worktree, f"origin/{head_branch}"]) - info(f"✅ worktree 作成 (detached @ origin/{head_branch}): {worktree}") - else: - info(f"⚠ git fetch origin {head_branch} 失敗 (フォーク PR の可能性) — gh pr checkout でフォールバック") - _sh(["git", "worktree", "add", "--detach", worktree, "HEAD"]) - # worktree 内で gh pr checkout を実行して正しいコミットに切り替え - checkout_result = subprocess.run( - ["gh", "pr", "checkout", str(pr), "--detach"], - capture_output=True, text=True, - cwd=worktree, - ) - if checkout_result.returncode != 0: - die(f"gh pr checkout --detach #{pr} 失敗: {checkout_result.stderr.strip()}") - info(f"✅ worktree 作成 (gh pr checkout --detach #{pr}): {worktree}") - else: + _create_worktree(worktree, pr, head_branch) + elif _is_registered_worktree(worktree): info(f"↻ 既存 worktree 流用: {worktree}") + else: + # パスは存在するが現リポジトリの worktree ではない (別リポジトリの残骸等)。 + # 流用すると git 操作が壊れるため退避して作り直す。 + stale = f"{worktree}.stale-{time.strftime('%Y%m%d%H%M%S')}" + pathlib.Path(worktree).rename(stale) + info(f"⚠ 現リポジトリの worktree でないため退避: {stale}") + _create_worktree(worktree, pr, head_branch) # worktree 作成/確認後に _tmp_dir() を呼ぶ (ここで .cross_review/ が作られる) tmp_dir = _tmp_dir(worktree) @@ -364,7 +400,6 @@ def cmd_init(args: argparse.Namespace) -> None: # 既存コメントスナップショット(重複指摘防止)。 # 3 ソース (インラインコメント / レビュー body / PR レベルコメント) を # fix skill の共有スクリプトで一括取得する。 - repo = _sh(["gh", "repo", "view", "--json", "nameWithOwner", "-q", ".nameWithOwner"]) fetch_script = pathlib.Path(__file__).resolve().parent.parent.parent / "fix" / "scripts" / "fetch-pr-comments.sh" r = subprocess.run( [str(fetch_script), repo, str(pr)], diff --git a/plugins/ndf/skills/cross-review/tests/test_default_worktree_base.py b/plugins/ndf/skills/cross-review/tests/test_default_worktree_base.py index 2515f27..b4d38da 100644 --- a/plugins/ndf/skills/cross-review/tests/test_default_worktree_base.py +++ b/plugins/ndf/skills/cross-review/tests/test_default_worktree_base.py @@ -1,9 +1,11 @@ -"""state.py `_default_worktree_base()` の解決順テスト。 +"""state.py `_default_worktree_base()` / `_repo_slug()` の解決テスト。 優先順位: 1. 環境変数 NDF_WORKTREE_BASE - 2. /work/worktrees (書込可能) - 3. $HOME/work/worktrees (フォールバック) + 2. <システム tmpdir>/ndf-worktrees (非永続領域) + +worktree path は `/--/pr` 形式で、他リポジトリの +同一 PR 番号と衝突しない。 """ from __future__ import annotations @@ -16,29 +18,24 @@ def test_env_override_takes_precedence(monkeypatch, tmp_path, state_mod): assert state_mod._default_worktree_base() == explicit -def test_fallback_to_home_when_legacy_unwritable(monkeypatch, tmp_path, state_mod): - """`/work/worktrees` への mkdir が失敗する環境では $HOME/work/worktrees を返す。 - - `Path.mkdir` を patch して `/work/worktrees` を書込不可な状態をエミュレートする。 - """ +def test_default_is_tmpdir_ndf_worktrees(monkeypatch, tmp_path, state_mod): + """既定では永続 volume ではなくシステム tmpdir 配下を使う。""" monkeypatch.delenv("NDF_WORKTREE_BASE", raising=False) - fake_home = tmp_path / "home" - fake_home.mkdir() - monkeypatch.setenv("HOME", str(fake_home)) - # Path.home() は HOME env を再評価しないため明示的に差し替える - monkeypatch.setattr( - state_mod.pathlib.Path, "home", - classmethod(lambda cls: cls(str(fake_home))), - ) + monkeypatch.setattr(state_mod.tempfile, "gettempdir", lambda: str(tmp_path)) + assert state_mod._default_worktree_base() == tmp_path / "ndf-worktrees" - orig_mkdir = state_mod.pathlib.Path.mkdir - def fake_mkdir(self, *args, **kwargs): - if str(self) == "/work/worktrees": - raise OSError("read-only file system") - return orig_mkdir(self, *args, **kwargs) +def test_repo_slug_is_path_safe(state_mod): + assert state_mod._repo_slug("devbasex/ai-plugins") == "devbasex--ai-plugins" - monkeypatch.setattr(state_mod.pathlib.Path, "mkdir", fake_mkdir) - result = state_mod._default_worktree_base() - assert result == pathlib.Path(str(fake_home)) / "work" / "worktrees" +def test_is_registered_worktree_rejects_foreign_dir(monkeypatch, tmp_path, state_mod): + """`git worktree list` に載っていないパスは流用しない (別リポジトリの残骸対策)。""" + registered = tmp_path / "registered" + foreign = tmp_path / "foreign" + monkeypatch.setattr( + state_mod, "_sh", + lambda cmd, check=True: f"worktree {registered}\nHEAD abc\n", + ) + assert state_mod._is_registered_worktree(str(registered)) is True + assert state_mod._is_registered_worktree(str(foreign)) is False