From 976f8d2c073f7fb504031df52bd3f403d7a91dca Mon Sep 17 00:00:00 2001 From: "takemi.ohama" Date: Mon, 25 May 2026 02:36:38 +0000 Subject: [PATCH 1/8] =?UTF-8?q?fix:=20PR=E3=82=B3=E3=83=A1=E3=83=B3?= =?UTF-8?q?=E3=83=88=E5=8F=96=E5=BE=97=E3=82=923=E3=82=BD=E3=83=BC?= =?UTF-8?q?=E3=82=B9=E5=AF=BE=E5=BF=9C=E3=81=AB=E6=8B=A1=E5=BC=B5=20(revie?= =?UTF-8?q?w=20body=20/=20PR=20=E3=83=AC=E3=83=99=E3=83=AB=E3=82=B3?= =?UTF-8?q?=E3=83=A1=E3=83=B3=E3=83=88=E8=BF=BD=E5=8A=A0)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit GitHub PR の review body と PR レベルコメントが取得されず、 人間レビュアーの指摘が cross-review ループで検出されない問題を修正。 共有スクリプト fetch-pr-comments.sh を fix skill に配置し、 cross-review / fix / review-pr-comments の 3 skill で共用する。 Closes #13 Co-Authored-By: Claude Opus 4.7 (1M context) --- ...PLAN13_cross-review-review-body-missing.md | 150 ++++++++++++++++++ plugins/ndf/skills/cross-review/SKILL.md | 2 +- .../cross-review/docs/01-state-and-review.md | 2 +- .../cross-review/docs/02-fix-and-rotation.md | 2 +- .../ndf/skills/cross-review/scripts/state.py | 12 +- plugins/ndf/skills/fix/SKILL.md | 10 +- .../skills/fix/scripts/fetch-pr-comments.sh | 23 +++ .../ndf/skills/review-pr-comments/SKILL.md | 14 +- 8 files changed, 200 insertions(+), 15 deletions(-) create mode 100644 issues/PLAN13_cross-review-review-body-missing.md create mode 100755 plugins/ndf/skills/fix/scripts/fetch-pr-comments.sh diff --git a/issues/PLAN13_cross-review-review-body-missing.md b/issues/PLAN13_cross-review-review-body-missing.md new file mode 100644 index 00000000..c62d52a1 --- /dev/null +++ b/issues/PLAN13_cross-review-review-body-missing.md @@ -0,0 +1,150 @@ +# PLAN13: cross-review — PR review body の指摘見落とし修正 + +- 起票日: 2026-05-25 +- 対象 plugin: `ndf` v4.7.5 +- 対象 skill: `ndf:cross-review`, `ndf:fix`, `ndf:review-pr-comments` +- 関連 issue: [GitHub Issue #13](https://github.com/devbasex/ai-plugins/issues/13) +- 報告者: takemi-ohama +- 実際のケース: [carmo-system-console PR #14137](https://github.com/volareinc/carmo-system-console/pull/14137) で人間レビュアーの `CHANGES_REQUESTED` review body 指摘が cross-review 6 ラウンド通じて検出されなかった + +## 背景・課題 + +### 現状 + +PR コメント取得時に **インラインコメント (`pulls/{pr}/comments`)** のみを取得している。GitHub の PR コメントは 3 つのソースに分かれるが、うち 2 つが見落とされている: + +| ソース | API | 取得状況 | 内容 | +|---|---|---|---| +| インラインコメント | `pulls/{pr}/comments` | ✅ 取得済み | diff の特定行に紐づくコメント | +| レビュー body | `pulls/{pr}/reviews` の `body` フィールド | ❌ **未取得** | レビュー投稿時の総評テキスト | +| PR レベルコメント | `issues/{pr}/comments` | ❌ **未取得** | Conversation タブの通常コメント | + +### 影響 + +- 人間レビュアーが review body にのみ指摘を書いた場合、cross-review ループ全体で検出されない +- `/ndf:fix` が review body の指摘を修正対象として認識しない +- `/ndf:review-pr-comments` が review body/PR レベルコメントを分類対象に含めない + +### 影響箇所 + +| ファイル | 修正内容 | +|---|---| +| `plugins/ndf/skills/fix/scripts/fetch-pr-comments.sh` | **新規作成** — 3 ソース一括取得の共有スクリプト | +| `plugins/ndf/skills/cross-review/scripts/state.py` (L306-324) | 既存の `gh api` 直接呼び出しを `fetch-pr-comments.sh` 呼び出しに差し替え | +| `plugins/ndf/skills/fix/SKILL.md` (L184) | スクリプト参照と使い方を追記 | +| `plugins/ndf/skills/review-pr-comments/SKILL.md` (L49) | コメント取得を共有スクリプト参照に変更 | +| `plugins/ndf/skills/cross-review/SKILL.md` (L87) | Step 4 の説明を共有スクリプト経由に更新 | +| `plugins/ndf/skills/cross-review/docs/01-state-and-review.md` (L98) | 既存コメント差分の説明を共有スクリプト参照に更新 | +| `plugins/ndf/skills/cross-review/docs/02-fix-and-rotation.md` (L76) | fix prompt 内のコメント取得手順を共有スクリプト参照に更新 | + +## 修正方針 + +### 設計方針: コメント取得の共有スクリプト化 + +3 つのスキル (cross-review, fix, review-pr-comments) が同じ 3 ソースの `gh api` 呼び出しを必要とするため、共有スクリプトに切り出す。 + +**配置場所**: `plugins/ndf/skills/fix/scripts/fetch-pr-comments.sh` + +cross-review は既に fix をサブエージェント経由で呼ぶ依存関係にあるため、fix 側にスクリプトを置けば依存方向が一致する。review-pr-comments も fix の前段(分類→修正)の関係。 + +### 1. `fix/scripts/fetch-pr-comments.sh` — 共有スクリプト新規作成 (コア) + +3 ソースを一括取得し、タグ付き行単位で stdout に出力するシェルスクリプト。 + +```bash +#!/usr/bin/env bash +# Usage: fetch-pr-comments.sh +set -uo pipefail # -e は意図的に外す (0件ソースで後続が止まるのを防止) + +REPO="$1" +PR="$2" + +# 1. インラインコメント (diff の特定行に紐づく) +gh api "repos/${REPO}/pulls/${PR}/comments" --paginate --jq \ + '.[] | "\(.path // "?"):\(.line // .original_line // "?") [\(.user.login)] \(.body // "" | split("\n")[0])"' \ + || true + +# 2. レビュー body (CHANGES_REQUESTED / COMMENTED 等の総評) +gh api "repos/${REPO}/pulls/${PR}/reviews" --paginate --jq \ + '.[] | select(.body != null and .body != "") | "[REVIEW-BODY] [\(.user.login)] state=\(.state) \(.body | split("\n")[0])"' \ + || true + +# 3. PR レベルコメント (Conversation タブの通常コメント) +gh api "repos/${REPO}/issues/${PR}/comments" --paginate --jq \ + '.[] | "[PR-COMMENT] [\(.user.login)] \(.body // "" | split("\n")[0])"' \ + || true +``` + +出力フォーマット: +- インラインコメント: `path:line [user] body` +- review body: `[REVIEW-BODY] [user] state=CHANGES_REQUESTED body` +- PR コメント: `[PR-COMMENT] [user] body` + +### 2. `state.py` — 既存コメント収集を共有スクリプト呼び出しに差し替え + +`init()` 内の L306-324 (インラインコメント取得 + ファイル書き出し) を `fetch-pr-comments.sh` の呼び出しに置き換え: + +```python +fetch_script = Path(__file__).resolve().parent.parent.parent / "fix" / "scripts" / "fetch-pr-comments.sh" +r = subprocess.run( + [str(fetch_script), repo, str(pr)], + capture_output=True, text=True, +) +existing_path = tmp_dir / f"cross-review-pr{pr}-existing-comments.txt" +if r.returncode == 0: + existing_path.write_text(r.stdout) +else: + info(f"⚠ 既存コメント取得失敗: {r.stderr.strip()[:200]}") + existing_path.write_text("") +``` + +既存の `jq_filter` 変数と `subprocess.run(["gh", "api", ...])` ブロックは削除。 + +### 3. `fix/SKILL.md` — スクリプト参照とコマンド例の更新 + +gh コマンド例セクションに `fetch-pr-comments.sh` の使い方を追記: + +```markdown +### PR コメント一括取得 (3 ソース) + +```bash +# 共有スクリプトで全ソース一括取得 +"$(dirname "$0")/scripts/fetch-pr-comments.sh" +``` + +### 4. `review-pr-comments/SKILL.md` — コメント取得セクション更新 + +Step 2 のコメント取得で `fix/scripts/fetch-pr-comments.sh` を参照: + +```markdown +### 2. PRコメント取得 + +fix skill の共有スクリプトで 3 ソース (インラインコメント / レビュー body / PR レベルコメント) を一括取得: + +```bash +PLUGIN_DIR="$(cd "$(dirname "$0")/../.." && pwd)" +"$PLUGIN_DIR/skills/fix/scripts/fetch-pr-comments.sh" "$REPO" "$PR_NUMBER" +``` + +### 5. ドキュメント整合 + +| ファイル | 更新内容 | +|---|---| +| `cross-review/SKILL.md` (L87) | Step 4 の説明を「3 ソース (fetch-pr-comments.sh 経由)」に更新 | +| `cross-review/docs/01-state-and-review.md` (L98) | 既存コメント差分の説明を共有スクリプト参照に更新 | +| `cross-review/docs/02-fix-and-rotation.md` (L76) | fix prompt 内のコメント取得手順を共有スクリプト参照に更新 | + +## 単一 PR 判定 + +- 変更ファイル: 7 ファイル (新規 1 + 既存 6) +- 差分: 推定 80-120 行 (共有スクリプト化で各ファイルの変更量は減少) +- すべて同一目的 (コメント取得ソースの拡張 + 共有スクリプト化) で結合度が高い +- 依存関係のある複数タスクなし + +→ **単一 PR で対応**。release ブランチ不要。 + +## テスト計画 + +- [ ] `state.py` の変更後、実 PR に対して `state.py init` を実行し、`existing-comments.txt` に 3 ソースの内容が含まれることを確認 +- [ ] review body にのみ指摘がある PR で `/ndf:cross-review` を実行し、指摘が検出されることを確認 +- [ ] `claude plugin validate` が通ることを確認 diff --git a/plugins/ndf/skills/cross-review/SKILL.md b/plugins/ndf/skills/cross-review/SKILL.md index fe0d7860..c9b4271f 100644 --- a/plugins/ndf/skills/cross-review/SKILL.md +++ b/plugins/ndf/skills/cross-review/SKILL.md @@ -84,7 +84,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 は `/.cross_review/`** を採用し、gemini の workspace 制約 (workspace 外の `write_file` がブロックされる) を根本回避 | -| 4 | 既存コメント差分 | `gh api .../comments --paginate` を `$TMP_DIR/cross-review-pr-existing-comments.txt` に保存し、gemini プロンプトには **内容をインライン埋め込み**、codex プロンプトには path を渡す | +| 4 | 既存コメント差分 | `fix/scripts/fetch-pr-comments.sh` で 3 ソース (インラインコメント / レビュー body / PR レベルコメント) を一括取得し `$TMP_DIR/cross-review-pr-existing-comments.txt` に保存。gemini プロンプトには **内容をインライン埋め込み**、codex プロンプトには path を渡す | ### `` の解決順 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 0f724c77..0d70272a 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 @@ -95,7 +95,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` を参照) -4. 既存コメントスナップショット → `$TMP_DIR/cross-review-pr-existing-comments.txt` +4. 既存コメントスナップショット (`fix/scripts/fetch-pr-comments.sh` で 3 ソース一括取得) → `$TMP_DIR/cross-review-pr-existing-comments.txt` 5. state.json 書き出し **重要**: 以降の全ステップで `cd $WORKTREE` を強制。 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 f1984762..fcf95a99 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 @@ -73,7 +73,7 @@ worktree 外を触ると競合します。 ## 必須実行手順(順序厳守) -1. PR コメント取得: `gh api "repos/{OWNER_REPO}/pulls/{PR}/comments" --paginate` +1. PR コメント取得 (3 ソース): `fix/scripts/fetch-pr-comments.sh {OWNER_REPO} {PR}` でインラインコメント / レビュー body / PR レベルコメントを一括取得 2. 重要度を独自再判定(AI agent のラベルは参考値) 3. CI 状態スナップショット: `gh pr checks {PR} --json name,state` (**完了待ちはしない**、PENDING は無視して FAILURE のみ修正対象に取り込む) 4. critical/major + 該当 minor/nit の修正コミット(worktree 内のみ) diff --git a/plugins/ndf/skills/cross-review/scripts/state.py b/plugins/ndf/skills/cross-review/scripts/state.py index f1c63017..4611b60a 100755 --- a/plugins/ndf/skills/cross-review/scripts/state.py +++ b/plugins/ndf/skills/cross-review/scripts/state.py @@ -304,16 +304,12 @@ def cmd_init(args: argparse.Namespace) -> None: state_file = tmp_dir / f"cross-review-pr{pr}-state.json" # 既存コメントスナップショット(重複指摘防止)。 - # NOTE: `gh api --paginate` は REST のページごとに **JSON 配列が連続して** stdout に出る - # ため、`json.loads(r.stdout)` は複数ページで JSONDecodeError になり、コメントが空に - # 落ちる。`--jq '.[] | ...'` で gh CLI 側に整形させ、行単位で素直に書き出す。 + # 3 ソース (インラインコメント / レビュー body / PR レベルコメント) を + # fix skill の共有スクリプトで一括取得する。 repo = _sh(["gh", "repo", "view", "--json", "nameWithOwner", "-q", ".nameWithOwner"]) - jq_filter = ( - r'.[] | "\(.path // "?"):\(.line // .original_line // "?") ' - r'[\(.user.login)] \(.body // "" | split("\n")[0])"' - ) + fetch_script = Path(__file__).resolve().parent.parent.parent / "fix" / "scripts" / "fetch-pr-comments.sh" r = subprocess.run( - ["gh", "api", f"repos/{repo}/pulls/{pr}/comments", "--paginate", "--jq", jq_filter], + [str(fetch_script), repo, str(pr)], capture_output=True, text=True, ) existing_path = tmp_dir / f"cross-review-pr{pr}-existing-comments.txt" diff --git a/plugins/ndf/skills/fix/SKILL.md b/plugins/ndf/skills/fix/SKILL.md index c767d5ff..ff779f80 100644 --- a/plugins/ndf/skills/fix/SKILL.md +++ b/plugins/ndf/skills/fix/SKILL.md @@ -177,10 +177,18 @@ review指摘とCIエラーは**同じPRで一緒に修正**する: ## ghコマンド例 +### PR コメント一括取得 (3 ソース) + +```bash +# インラインコメント / レビュー body / PR レベルコメントを一括取得 +PLUGIN_DIR="$(cd "$(dirname "$0")/../.." && pwd)" +"$PLUGIN_DIR/skills/fix/scripts/fetch-pr-comments.sh" +``` + ### コメントへの返信 ```bash -# PRのレビューコメント一覧を取得 +# PRのレビューコメント一覧を取得 (インラインコメントのみ) gh api repos/{owner}/{repo}/pulls/{pr_number}/comments # 特定のコメントに返信(in_reply_to にコメントIDを指定) diff --git a/plugins/ndf/skills/fix/scripts/fetch-pr-comments.sh b/plugins/ndf/skills/fix/scripts/fetch-pr-comments.sh new file mode 100755 index 00000000..eaaf11a1 --- /dev/null +++ b/plugins/ndf/skills/fix/scripts/fetch-pr-comments.sh @@ -0,0 +1,23 @@ +#!/usr/bin/env bash +# Usage: fetch-pr-comments.sh +# 3 ソース (インラインコメント / レビュー body / PR レベルコメント) を一括取得し、 +# タグ付き行単位で stdout に出力する。 +set -uo pipefail + +REPO="$1" +PR="$2" + +# 1. インラインコメント (diff の特定行に紐づく) +gh api "repos/${REPO}/pulls/${PR}/comments" --paginate --jq \ + '.[] | "\(.path // "?"):\(.line // .original_line // "?") [\(.user.login)] \(.body // "" | split("\n")[0])"' \ + || true + +# 2. レビュー body (CHANGES_REQUESTED / COMMENTED 等の総評) +gh api "repos/${REPO}/pulls/${PR}/reviews" --paginate --jq \ + '.[] | select(.body != null and .body != "") | "[REVIEW-BODY] [\(.user.login)] state=\(.state) \(.body | split("\n")[0])"' \ + || true + +# 3. PR レベルコメント (Conversation タブの通常コメント) +gh api "repos/${REPO}/issues/${PR}/comments" --paginate --jq \ + '.[] | "[PR-COMMENT] [\(.user.login)] \(.body // "" | split("\n")[0])"' \ + || true diff --git a/plugins/ndf/skills/review-pr-comments/SKILL.md b/plugins/ndf/skills/review-pr-comments/SKILL.md index e424e725..9ff0582d 100644 --- a/plugins/ndf/skills/review-pr-comments/SKILL.md +++ b/plugins/ndf/skills/review-pr-comments/SKILL.md @@ -42,11 +42,19 @@ CURRENT_BRANCH=$(git branch --show-current) PR_NUMBER="${ARGUMENTS:-$(gh pr view --json number --jq .number)}" ``` -### 2. PRコメント取得 +### 2. PRコメント取得 (3 ソース) + +fix skill の共有スクリプトで インラインコメント / レビュー body / PR レベルコメントを一括取得: + +```bash +PLUGIN_DIR="$(cd "$(dirname "$0")/../.." && pwd)" +"$PLUGIN_DIR/skills/fix/scripts/fetch-pr-comments.sh" "$(gh repo view --json nameWithOwner -q .nameWithOwner)" "$PR_NUMBER" +``` + +補助情報 (reviewDecision 等): ```bash -gh pr view "$PR_NUMBER" --json comments,reviewDecision -gh api "repos/:owner/:repo/pulls/$PR_NUMBER/comments" +gh pr view "$PR_NUMBER" --json reviewDecision ``` GitHub MCP を使う場合は `mcp__github__get_pull_request_comments` を利用。 From 90025cd58321b67b105724695a5ee2c54009e36c Mon Sep 17 00:00:00 2001 From: "takemi.ohama" Date: Mon, 25 May 2026 03:37:00 +0000 Subject: [PATCH 2/8] =?UTF-8?q?Fix:=20Path=E6=9C=AAimport=E3=81=AENameErro?= =?UTF-8?q?r=E4=BF=AE=E6=AD=A3=20+=20fetch-pr-comments.sh=E3=81=AE?= =?UTF-8?q?=E3=82=A8=E3=83=A9=E3=83=BC=E3=83=8F=E3=83=B3=E3=83=89=E3=83=AA?= =?UTF-8?q?=E3=83=B3=E3=82=B0=E3=83=BB=E6=9C=AC=E6=96=87=E4=BF=9D=E6=8C=81?= =?UTF-8?q?=E6=94=B9=E5=96=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - state.py: Path → pathlib.Path に修正 (NameError 解消) - fetch-pr-comments.sh: review body の改行を \n エスケープして全文保持 - fetch-pr-comments.sh: 全ソース取得失敗時のみ非0終了 (0件取得と取得失敗の区別) Co-Authored-By: Claude Opus 4.7 (1M context) --- .../ndf/skills/cross-review/scripts/state.py | 2 +- .../skills/fix/scripts/fetch-pr-comments.sh | 31 +++++++++++++------ 2 files changed, 23 insertions(+), 10 deletions(-) diff --git a/plugins/ndf/skills/cross-review/scripts/state.py b/plugins/ndf/skills/cross-review/scripts/state.py index 4611b60a..6ad5ee66 100755 --- a/plugins/ndf/skills/cross-review/scripts/state.py +++ b/plugins/ndf/skills/cross-review/scripts/state.py @@ -307,7 +307,7 @@ def cmd_init(args: argparse.Namespace) -> None: # 3 ソース (インラインコメント / レビュー body / PR レベルコメント) を # fix skill の共有スクリプトで一括取得する。 repo = _sh(["gh", "repo", "view", "--json", "nameWithOwner", "-q", ".nameWithOwner"]) - fetch_script = Path(__file__).resolve().parent.parent.parent / "fix" / "scripts" / "fetch-pr-comments.sh" + fetch_script = pathlib.Path(__file__).resolve().parent.parent.parent / "fix" / "scripts" / "fetch-pr-comments.sh" r = subprocess.run( [str(fetch_script), repo, str(pr)], capture_output=True, text=True, diff --git a/plugins/ndf/skills/fix/scripts/fetch-pr-comments.sh b/plugins/ndf/skills/fix/scripts/fetch-pr-comments.sh index eaaf11a1..e4d05955 100755 --- a/plugins/ndf/skills/fix/scripts/fetch-pr-comments.sh +++ b/plugins/ndf/skills/fix/scripts/fetch-pr-comments.sh @@ -2,22 +2,35 @@ # Usage: fetch-pr-comments.sh # 3 ソース (インラインコメント / レビュー body / PR レベルコメント) を一括取得し、 # タグ付き行単位で stdout に出力する。 +# 全ソース取得失敗時は非 0 で終了する(0件取得と取得失敗を区別)。 set -uo pipefail REPO="$1" PR="$2" +FAIL_COUNT=0 + # 1. インラインコメント (diff の特定行に紐づく) -gh api "repos/${REPO}/pulls/${PR}/comments" --paginate --jq \ - '.[] | "\(.path // "?"):\(.line // .original_line // "?") [\(.user.login)] \(.body // "" | split("\n")[0])"' \ - || true +if ! gh api "repos/${REPO}/pulls/${PR}/comments" --paginate --jq \ + '.[] | "\(.path // "?"):\(.line // .original_line // "?") [\(.user.login)] \(.body // "" | split("\n")[0])"'; then + (( FAIL_COUNT += 1 )) || true +fi # 2. レビュー body (CHANGES_REQUESTED / COMMENTED 等の総評) -gh api "repos/${REPO}/pulls/${PR}/reviews" --paginate --jq \ - '.[] | select(.body != null and .body != "") | "[REVIEW-BODY] [\(.user.login)] state=\(.state) \(.body | split("\n")[0])"' \ - || true +# 本文全体を保持する。改行は \n エスケープして 1 行に収める。 +if ! gh api "repos/${REPO}/pulls/${PR}/reviews" --paginate --jq \ + '.[] | select(.body != null and .body != "") | "[REVIEW-BODY] [\(.user.login)] state=\(.state) \(.body | gsub("\n"; "\\n"))"'; then + (( FAIL_COUNT += 1 )) || true +fi # 3. PR レベルコメント (Conversation タブの通常コメント) -gh api "repos/${REPO}/issues/${PR}/comments" --paginate --jq \ - '.[] | "[PR-COMMENT] [\(.user.login)] \(.body // "" | split("\n")[0])"' \ - || true +if ! gh api "repos/${REPO}/issues/${PR}/comments" --paginate --jq \ + '.[] | "[PR-COMMENT] [\(.user.login)] \(.body // "" | split("\n")[0])"'; then + (( FAIL_COUNT += 1 )) || true +fi + +# 全ソース失敗時のみ非 0 で終了(認証切れ等の検出) +if (( FAIL_COUNT >= 3 )); then + echo "ERROR: 全 3 ソースの取得に失敗しました" >&2 + exit 1 +fi From 52a46852a2ac2c0f8fc719899c48afcb47dcf280 Mon Sep 17 00:00:00 2001 From: "takemi.ohama" Date: Mon, 25 May 2026 03:43:28 +0000 Subject: [PATCH 3/8] =?UTF-8?q?Fix:=20=E5=85=A83=E3=82=BD=E3=83=BC?= =?UTF-8?q?=E3=82=B9=E3=81=AE=E3=82=B3=E3=83=A1=E3=83=B3=E3=83=88=E6=9C=AC?= =?UTF-8?q?=E6=96=87=E3=82=92=E5=85=A8=E6=96=87=E4=BF=9D=E6=8C=81=E3=81=AB?= =?UTF-8?q?=E7=B5=B1=E4=B8=80=20(split=E2=86=92gsub)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PRレベルコメント・インラインコメントも review body と同様に gsub("\n"; "\\n") で全文を1行に保持するよう統一。 split("\n")[0] による先頭行切り詰めを排除し、 fix/review-pr-comments が複数行の指摘を見落とさないようにする。 --- plugins/ndf/skills/fix/scripts/fetch-pr-comments.sh | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/plugins/ndf/skills/fix/scripts/fetch-pr-comments.sh b/plugins/ndf/skills/fix/scripts/fetch-pr-comments.sh index e4d05955..92a4f01d 100755 --- a/plugins/ndf/skills/fix/scripts/fetch-pr-comments.sh +++ b/plugins/ndf/skills/fix/scripts/fetch-pr-comments.sh @@ -11,8 +11,9 @@ PR="$2" FAIL_COUNT=0 # 1. インラインコメント (diff の特定行に紐づく) +# 本文全体を保持する。改行は \n エスケープして 1 行に収める。 if ! gh api "repos/${REPO}/pulls/${PR}/comments" --paginate --jq \ - '.[] | "\(.path // "?"):\(.line // .original_line // "?") [\(.user.login)] \(.body // "" | split("\n")[0])"'; then + '.[] | "\(.path // "?"):\(.line // .original_line // "?") [\(.user.login)] \(.body // "" | gsub("\n"; "\\n"))"'; then (( FAIL_COUNT += 1 )) || true fi @@ -24,8 +25,9 @@ if ! gh api "repos/${REPO}/pulls/${PR}/reviews" --paginate --jq \ fi # 3. PR レベルコメント (Conversation タブの通常コメント) +# 本文全体を保持する。改行は \n エスケープして 1 行に収める。 if ! gh api "repos/${REPO}/issues/${PR}/comments" --paginate --jq \ - '.[] | "[PR-COMMENT] [\(.user.login)] \(.body // "" | split("\n")[0])"'; then + '.[] | "[PR-COMMENT] [\(.user.login)] \(.body // "" | gsub("\n"; "\\n"))"'; then (( FAIL_COUNT += 1 )) || true fi From 129fc2e86861f26931356cbcf90035cd61a7a54d Mon Sep 17 00:00:00 2001 From: "takemi.ohama" Date: Mon, 25 May 2026 04:03:49 +0000 Subject: [PATCH 4/8] =?UTF-8?q?Fix:=20=E6=97=A2=E5=AD=98=E3=82=B3=E3=83=A1?= =?UTF-8?q?=E3=83=B3=E3=83=88=E5=8F=96=E5=BE=97=E5=A4=B1=E6=95=97=E6=99=82?= =?UTF-8?q?=E3=81=ABdie()=E3=81=A7=E4=B8=AD=E6=96=AD=E3=81=99=E3=82=8B?= =?UTF-8?q?=E3=82=88=E3=81=86=E4=BF=AE=E6=AD=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit fetch-pr-comments.shが全ソース失敗で非0を返した場合、 空スナップショットで黙って継続せずdie()でinitを失敗させる。 重複検出が無効な状態でレビューを進めないようにする。 --- plugins/ndf/skills/cross-review/scripts/state.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/plugins/ndf/skills/cross-review/scripts/state.py b/plugins/ndf/skills/cross-review/scripts/state.py index 6ad5ee66..d61e2b6f 100755 --- a/plugins/ndf/skills/cross-review/scripts/state.py +++ b/plugins/ndf/skills/cross-review/scripts/state.py @@ -316,8 +316,7 @@ def cmd_init(args: argparse.Namespace) -> None: if r.returncode == 0: existing_path.write_text(r.stdout) else: - info(f"⚠ 既存コメント取得失敗: {r.stderr.strip()[:200]}") - existing_path.write_text("") + die(f"既存コメント取得失敗 (重複検出無効のため中断): {r.stderr.strip()[:200]}") state = { "started_at": _now(), From 097ec3bfc869420e8c65fd4c88d10a581ee7bda3 Mon Sep 17 00:00:00 2001 From: "takemi.ohama" Date: Mon, 25 May 2026 04:15:39 +0000 Subject: [PATCH 5/8] =?UTF-8?q?Fix:=20=E3=82=AF=E3=83=AD=E3=82=B9=E3=83=AC?= =?UTF-8?q?=E3=83=93=E3=83=A5=E3=83=BC=20Round=202=20=E6=8C=87=E6=91=98?= =?UTF-8?q?=E5=AF=BE=E5=BF=9C=20(codex=201=E4=BB=B6=20+=20gemini=205?= =?UTF-8?q?=E4=BB=B6)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - PLAN13 サンプルコードを実装済みの gsub + 失敗カウント方式に更新 (codex minor) - args.worktree を .resolve() で絶対パスに変換 (gemini major) - フォーク PR の fetch 失敗時に gh pr checkout --detach でフォールバック (gemini major) - KEY=VALUE 出力のパス・ブランチ名をダブルクォートで囲む (gemini major) - worktree ベースディレクトリの書き込み権限チェック追加 (gemini minor) - read_text() / write_text() 全箇所に encoding="utf-8" を明示指定 (gemini minor) --- ...PLAN13_cross-review-review-body-missing.md | 30 ++++++--- .../ndf/skills/cross-review/scripts/state.py | 65 ++++++++++++------- 2 files changed, 61 insertions(+), 34 deletions(-) diff --git a/issues/PLAN13_cross-review-review-body-missing.md b/issues/PLAN13_cross-review-review-body-missing.md index c62d52a1..e6668405 100644 --- a/issues/PLAN13_cross-review-review-body-missing.md +++ b/issues/PLAN13_cross-review-review-body-missing.md @@ -60,19 +60,31 @@ REPO="$1" PR="$2" # 1. インラインコメント (diff の特定行に紐づく) -gh api "repos/${REPO}/pulls/${PR}/comments" --paginate --jq \ - '.[] | "\(.path // "?"):\(.line // .original_line // "?") [\(.user.login)] \(.body // "" | split("\n")[0])"' \ - || true +# 本文全体を保持。改行は \n エスケープして 1 行に収める。 +if ! gh api "repos/${REPO}/pulls/${PR}/comments" --paginate --jq \ + '.[] | "\(.path // "?"):\(.line // .original_line // "?") [\(.user.login)] \(.body // "" | gsub("\n"; "\\n"))"'; then + (( FAIL_COUNT += 1 )) || true +fi # 2. レビュー body (CHANGES_REQUESTED / COMMENTED 等の総評) -gh api "repos/${REPO}/pulls/${PR}/reviews" --paginate --jq \ - '.[] | select(.body != null and .body != "") | "[REVIEW-BODY] [\(.user.login)] state=\(.state) \(.body | split("\n")[0])"' \ - || true +# 本文全体を保持。改行は \n エスケープして 1 行に収める。 +if ! gh api "repos/${REPO}/pulls/${PR}/reviews" --paginate --jq \ + '.[] | select(.body != null and .body != "") | "[REVIEW-BODY] [\(.user.login)] state=\(.state) \(.body | gsub("\n"; "\\n"))"'; then + (( FAIL_COUNT += 1 )) || true +fi # 3. PR レベルコメント (Conversation タブの通常コメント) -gh api "repos/${REPO}/issues/${PR}/comments" --paginate --jq \ - '.[] | "[PR-COMMENT] [\(.user.login)] \(.body // "" | split("\n")[0])"' \ - || true +# 本文全体を保持。改行は \n エスケープして 1 行に収める。 +if ! gh api "repos/${REPO}/issues/${PR}/comments" --paginate --jq \ + '.[] | "[PR-COMMENT] [\(.user.login)] \(.body // "" | gsub("\n"; "\\n"))"'; then + (( FAIL_COUNT += 1 )) || true +fi + +# 全ソース失敗時のみ非 0 で終了(認証切れ等の検出) +if (( FAIL_COUNT >= 3 )); then + echo "ERROR: 全 3 ソースの取得に失敗しました" >&2 + exit 1 +fi ``` 出力フォーマット: diff --git a/plugins/ndf/skills/cross-review/scripts/state.py b/plugins/ndf/skills/cross-review/scripts/state.py index d61e2b6f..9883a583 100755 --- a/plugins/ndf/skills/cross-review/scripts/state.py +++ b/plugins/ndf/skills/cross-review/scripts/state.py @@ -50,7 +50,9 @@ def _default_worktree_base() -> pathlib.Path: legacy = pathlib.Path("/work/worktrees") try: legacy.mkdir(parents=True, exist_ok=True) - # mkdir 成功 = 書き込み可能 → 既存環境互換でこちらを使う + if not os.access(legacy, os.W_OK): + die(f"worktree ベースディレクトリに書き込み権限がありません: {legacy}") + # mkdir 成功 + 書き込み可能 → 既存環境互換でこちらを使う return legacy except OSError: pass @@ -223,13 +225,13 @@ def _load(pr: int) -> dict[str, Any]: p = _state_path(pr) if not p.exists(): die(f"state.json not found: {p}") - return json.loads(p.read_text()) + return json.loads(p.read_text(encoding="utf-8")) def _save(pr: int, state: dict[str, Any]) -> None: p = _state_path(pr) tmp = p.with_suffix(".json.tmp") - tmp.write_text(json.dumps(state, indent=2, ensure_ascii=False)) + tmp.write_text(json.dumps(state, indent=2, ensure_ascii=False), encoding="utf-8") tmp.replace(p) @@ -256,7 +258,7 @@ def cmd_init(args: argparse.Namespace) -> None: pr = args.pr # worktree path を先に解決してから tmp_dir を決定する。 # tmp_dir は /.cross_review/ に配置し、gemini の workspace 制約を根本回避。 - worktree = args.worktree or str(_default_worktree_base() / f"pr{pr}") + worktree = str(pathlib.Path(args.worktree).resolve()) if args.worktree else str(_default_worktree_base() / f"pr{pr}") # worktree 存在チェック用: _tmp_dir() は mkdir するため、先に呼ぶと # worktree ディレクトリが副作用で作成され exists() が常に true になる。 @@ -265,14 +267,14 @@ def cmd_init(args: argparse.Namespace) -> None: # 再開チェック: 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()) + st = json.loads(resume_state_file.read_text(encoding="utf-8")) 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']}") - print(f"WORKTREE={wt}") - print(f"TMP_DIR={tmp_dir}") + print(f'PR={st["current_pr"]}') + print(f'WORKTREE="{wt}"') + print(f'TMP_DIR="{tmp_dir}"') print(f"RESUMED=1") return @@ -288,14 +290,27 @@ 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(): - _sh(["git", "fetch", "origin", head_branch]) - # head branch が既に別の worktree (例: 現在の作業ディレクトリ) で checkout されている - # 場合、`git worktree add ` は - # `fatal: '' is already used by worktree at ''` - # で落ちる。これを避けるため、`origin/` を **detached** で展開する。 - # cross-review はファイル参照しかしないので detached HEAD で全く問題ない。 - _sh(["git", "worktree", "add", "--detach", worktree, f"origin/{head_branch}"]) - info(f"✅ worktree 作成 (detached @ origin/{head_branch}): {worktree}") + # フォーク 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 を実行して正しいコミットに切り替え + subprocess.run( + ["gh", "pr", "checkout", str(pr), "--detach"], + capture_output=True, text=True, + cwd=worktree, + ) + info(f"✅ worktree 作成 (gh pr checkout --detach #{pr}): {worktree}") else: info(f"↻ 既存 worktree 流用: {worktree}") @@ -314,7 +329,7 @@ def cmd_init(args: argparse.Namespace) -> None: ) existing_path = tmp_dir / f"cross-review-pr{pr}-existing-comments.txt" if r.returncode == 0: - existing_path.write_text(r.stdout) + existing_path.write_text(r.stdout, encoding="utf-8") else: die(f"既存コメント取得失敗 (重複検出無効のため中断): {r.stderr.strip()[:200]}") @@ -337,14 +352,14 @@ def cmd_init(args: argparse.Namespace) -> None: "deferred_nits": [], "final": None, } - state_file.write_text(json.dumps(state, indent=2, ensure_ascii=False)) + state_file.write_text(json.dumps(state, indent=2, ensure_ascii=False), encoding="utf-8") info(f"✅ state 初期化: {state_file}") print(f"PR={pr}") - print(f"WORKTREE={worktree}") - print(f"TMP_DIR={tmp_dir}") - print(f"REPO={repo}") - print(f"HEAD_BRANCH={head_branch}") - print(f"BASE_BRANCH={base_branch}") + print(f'WORKTREE="{worktree}"') + print(f'TMP_DIR="{tmp_dir}"') + print(f'REPO="{repo}"') + print(f'HEAD_BRANCH="{head_branch}"') + print(f'BASE_BRANCH="{base_branch}"') print(f"IS_OWN_PR={'1' if is_own else '0'}") print(f"EVENT_DOWNGRADE={'1' if event_downgrade else '0'}") print("RESUMED=0") @@ -390,7 +405,7 @@ def cmd_read_result(args: argparse.Namespace) -> None: die(f"{agent}: result 未生成 ({rfile})") try: - r = json.loads(rfile.read_text()) + r = json.loads(rfile.read_text(encoding="utf-8")) except json.JSONDecodeError as exc: die(f"{agent}: result.json の parse に失敗 ({rfile}): {exc}", code=3) @@ -499,7 +514,7 @@ def collect_keys(round_no: int) -> set[str]: if not p.exists(): continue try: - payload = json.loads(p.read_text()) + payload = json.loads(p.read_text(encoding="utf-8")) except json.JSONDecodeError: continue # gemini round 4 指摘: payload は本来 dict (comments: [...]) だが、 From 8e54adec06135898e84cb6bfb836b68cee2be6b4 Mon Sep 17 00:00:00 2001 From: "takemi.ohama" Date: Mon, 25 May 2026 04:26:01 +0000 Subject: [PATCH 6/8] =?UTF-8?q?Fix:=20=E3=82=AF=E3=83=AD=E3=82=B9=E3=83=AC?= =?UTF-8?q?=E3=83=93=E3=83=A5=E3=83=BC=20Round=203=20=E6=8C=87=E6=91=98?= =?UTF-8?q?=E5=AF=BE=E5=BF=9C=20(codex=20major=201=E4=BB=B6=20+=20gemini?= =?UTF-8?q?=20major=201=E4=BB=B6=20+=20gemini=20minor=202=E4=BB=B6)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - state.py: gh pr checkout --detach の戻り値チェック追加、失敗時 die() で中断 - state.py: _default_worktree_base の os.access 失敗時に die() せずフォールバック継続 - fetch-pr-comments.sh: 引数 (REPO, PR) の存在チェック追加 - fetch-pr-comments.sh: 各 gh api 失敗時に stderr へ個別警告メッセージ出力 --- plugins/ndf/skills/cross-review/scripts/state.py | 11 +++++++---- plugins/ndf/skills/fix/scripts/fetch-pr-comments.sh | 8 ++++++++ 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/plugins/ndf/skills/cross-review/scripts/state.py b/plugins/ndf/skills/cross-review/scripts/state.py index 9883a583..937c2aee 100755 --- a/plugins/ndf/skills/cross-review/scripts/state.py +++ b/plugins/ndf/skills/cross-review/scripts/state.py @@ -51,9 +51,10 @@ def _default_worktree_base() -> pathlib.Path: try: legacy.mkdir(parents=True, exist_ok=True) if not os.access(legacy, os.W_OK): - die(f"worktree ベースディレクトリに書き込み権限がありません: {legacy}") - # mkdir 成功 + 書き込み可能 → 既存環境互換でこちらを使う - return legacy + info(f"⚠ worktree ベースディレクトリに書き込み権限がありません: {legacy} — フォールバック") + else: + # mkdir 成功 + 書き込み可能 → 既存環境互換でこちらを使う + return legacy except OSError: pass return pathlib.Path.home() / "work" / "worktrees" @@ -305,11 +306,13 @@ def cmd_init(args: argparse.Namespace) -> None: info(f"⚠ git fetch origin {head_branch} 失敗 (フォーク PR の可能性) — gh pr checkout でフォールバック") _sh(["git", "worktree", "add", "--detach", worktree, "HEAD"]) # worktree 内で gh pr checkout を実行して正しいコミットに切り替え - subprocess.run( + 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: info(f"↻ 既存 worktree 流用: {worktree}") diff --git a/plugins/ndf/skills/fix/scripts/fetch-pr-comments.sh b/plugins/ndf/skills/fix/scripts/fetch-pr-comments.sh index 92a4f01d..c173a041 100755 --- a/plugins/ndf/skills/fix/scripts/fetch-pr-comments.sh +++ b/plugins/ndf/skills/fix/scripts/fetch-pr-comments.sh @@ -5,6 +5,11 @@ # 全ソース取得失敗時は非 0 で終了する(0件取得と取得失敗を区別)。 set -uo pipefail +if [[ $# -lt 2 ]] || [[ -z "${1:-}" ]] || [[ -z "${2:-}" ]]; then + echo "ERROR: 引数が不足しています。Usage: $0 " >&2 + exit 1 +fi + REPO="$1" PR="$2" @@ -14,6 +19,7 @@ FAIL_COUNT=0 # 本文全体を保持する。改行は \n エスケープして 1 行に収める。 if ! gh api "repos/${REPO}/pulls/${PR}/comments" --paginate --jq \ '.[] | "\(.path // "?"):\(.line // .original_line // "?") [\(.user.login)] \(.body // "" | gsub("\n"; "\\n"))"'; then + echo "WARNING: インラインコメントの取得に失敗しました (repos/${REPO}/pulls/${PR}/comments)" >&2 (( FAIL_COUNT += 1 )) || true fi @@ -21,6 +27,7 @@ fi # 本文全体を保持する。改行は \n エスケープして 1 行に収める。 if ! gh api "repos/${REPO}/pulls/${PR}/reviews" --paginate --jq \ '.[] | select(.body != null and .body != "") | "[REVIEW-BODY] [\(.user.login)] state=\(.state) \(.body | gsub("\n"; "\\n"))"'; then + echo "WARNING: レビュー body の取得に失敗しました (repos/${REPO}/pulls/${PR}/reviews)" >&2 (( FAIL_COUNT += 1 )) || true fi @@ -28,6 +35,7 @@ fi # 本文全体を保持する。改行は \n エスケープして 1 行に収める。 if ! gh api "repos/${REPO}/issues/${PR}/comments" --paginate --jq \ '.[] | "[PR-COMMENT] [\(.user.login)] \(.body // "" | gsub("\n"; "\\n"))"'; then + echo "WARNING: PR レベルコメントの取得に失敗しました (repos/${REPO}/issues/${PR}/comments)" >&2 (( FAIL_COUNT += 1 )) || true fi From 871558a889a982381be84a31254e08af29cabf5b Mon Sep 17 00:00:00 2001 From: "takemi.ohama" Date: Mon, 25 May 2026 04:35:45 +0000 Subject: [PATCH 7/8] =?UTF-8?q?Fix:=20SKILL.md=20=E3=81=AE=E3=83=91?= =?UTF-8?q?=E3=82=B9=E8=A7=A3=E6=B1=BA=E3=82=92=20$0=20=E3=81=8B=E3=82=89?= =?UTF-8?q?=20$CLAUDE=5FPLUGIN=5FROOT=20=E3=81=AB=E4=BF=AE=E6=AD=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bash tool 実行時に $0 はシェル自体を指すため、SKILL.md からの 相対パス解決に使えない。Claude Code プラグインが提供する $CLAUDE_PLUGIN_ROOT 環境変数を使用するよう修正。 対象: - plugins/ndf/skills/fix/SKILL.md - plugins/ndf/skills/review-pr-comments/SKILL.md Co-Authored-By: Claude Opus 4.7 (1M context) --- plugins/ndf/skills/fix/SKILL.md | 4 ++-- plugins/ndf/skills/review-pr-comments/SKILL.md | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/plugins/ndf/skills/fix/SKILL.md b/plugins/ndf/skills/fix/SKILL.md index ff779f80..652cfa2e 100644 --- a/plugins/ndf/skills/fix/SKILL.md +++ b/plugins/ndf/skills/fix/SKILL.md @@ -181,8 +181,8 @@ review指摘とCIエラーは**同じPRで一緒に修正**する: ```bash # インラインコメント / レビュー body / PR レベルコメントを一括取得 -PLUGIN_DIR="$(cd "$(dirname "$0")/../.." && pwd)" -"$PLUGIN_DIR/skills/fix/scripts/fetch-pr-comments.sh" +FETCH_SCRIPT="$CLAUDE_PLUGIN_ROOT/skills/fix/scripts/fetch-pr-comments.sh" +"$FETCH_SCRIPT" ``` ### コメントへの返信 diff --git a/plugins/ndf/skills/review-pr-comments/SKILL.md b/plugins/ndf/skills/review-pr-comments/SKILL.md index 9ff0582d..52b03186 100644 --- a/plugins/ndf/skills/review-pr-comments/SKILL.md +++ b/plugins/ndf/skills/review-pr-comments/SKILL.md @@ -47,8 +47,8 @@ PR_NUMBER="${ARGUMENTS:-$(gh pr view --json number --jq .number)}" fix skill の共有スクリプトで インラインコメント / レビュー body / PR レベルコメントを一括取得: ```bash -PLUGIN_DIR="$(cd "$(dirname "$0")/../.." && pwd)" -"$PLUGIN_DIR/skills/fix/scripts/fetch-pr-comments.sh" "$(gh repo view --json nameWithOwner -q .nameWithOwner)" "$PR_NUMBER" +FETCH_SCRIPT="$CLAUDE_PLUGIN_ROOT/skills/fix/scripts/fetch-pr-comments.sh" +"$FETCH_SCRIPT" "$(gh repo view --json nameWithOwner -q .nameWithOwner)" "$PR_NUMBER" ``` 補助情報 (reviewDecision 等): From 645df8318bf3da22e35ecfd7305bc5ee4153b9b1 Mon Sep 17 00:00:00 2001 From: "takemi.ohama" Date: Mon, 25 May 2026 04:46:00 +0000 Subject: [PATCH 8/8] =?UTF-8?q?Fix:=20shlex.quote=E3=81=AB=E3=82=88?= =?UTF-8?q?=E3=82=8Beval=E5=87=BA=E5=8A=9B=E3=81=AE=E5=AE=89=E5=85=A8?= =?UTF-8?q?=E5=8C=96=20+=20fetch-pr-comments=E3=81=AE=E3=83=90=E3=83=83?= =?UTF-8?q?=E3=82=AF=E3=82=AF=E3=82=A9=E3=83=BC=E3=83=88=E3=83=95=E3=82=A7?= =?UTF-8?q?=E3=83=B3=E3=82=B9=E7=84=A1=E5=AE=B3=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - state.py: eval "$(state.py init ...)" で取り込む KEY=VALUE 出力のパス・文字列値を shlex.quote() で囲み、パスに特殊文字 ($, ", 改行等) が含まれる場合のコマンド注入・ 構文破壊を防止 (WORKTREE, TMP_DIR, REPO, HEAD_BRANCH, BASE_BRANCH) - fetch-pr-comments.sh: コメント本文中の ``` (3連バッククォート) を ` ` ` に置換し、 launch-gemini.sh の Markdown フェンスが本文中のコードブロックで閉じられる問題を防止 Co-Authored-By: Claude Opus 4.7 (1M context) --- plugins/ndf/skills/cross-review/scripts/state.py | 15 ++++++++------- .../ndf/skills/fix/scripts/fetch-pr-comments.sh | 6 +++--- 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/plugins/ndf/skills/cross-review/scripts/state.py b/plugins/ndf/skills/cross-review/scripts/state.py index 937c2aee..19556370 100755 --- a/plugins/ndf/skills/cross-review/scripts/state.py +++ b/plugins/ndf/skills/cross-review/scripts/state.py @@ -29,6 +29,7 @@ import json import os import pathlib +import shlex import subprocess import sys from typing import Any @@ -274,8 +275,8 @@ def cmd_init(args: argparse.Namespace) -> None: wt = st.get("worktree_path") or "" info(f"↻ 前回中断 state から再開(round={len(st.get('rounds', []))})") print(f'PR={st["current_pr"]}') - print(f'WORKTREE="{wt}"') - print(f'TMP_DIR="{tmp_dir}"') + print(f'WORKTREE={shlex.quote(str(wt))}') + print(f'TMP_DIR={shlex.quote(str(tmp_dir))}') print(f"RESUMED=1") return @@ -358,11 +359,11 @@ def cmd_init(args: argparse.Namespace) -> None: state_file.write_text(json.dumps(state, indent=2, ensure_ascii=False), encoding="utf-8") info(f"✅ state 初期化: {state_file}") print(f"PR={pr}") - print(f'WORKTREE="{worktree}"') - print(f'TMP_DIR="{tmp_dir}"') - print(f'REPO="{repo}"') - print(f'HEAD_BRANCH="{head_branch}"') - print(f'BASE_BRANCH="{base_branch}"') + print(f'WORKTREE={shlex.quote(str(worktree))}') + print(f'TMP_DIR={shlex.quote(str(tmp_dir))}') + print(f'REPO={shlex.quote(str(repo))}') + print(f'HEAD_BRANCH={shlex.quote(str(head_branch))}') + print(f'BASE_BRANCH={shlex.quote(str(base_branch))}') print(f"IS_OWN_PR={'1' if is_own else '0'}") print(f"EVENT_DOWNGRADE={'1' if event_downgrade else '0'}") print("RESUMED=0") diff --git a/plugins/ndf/skills/fix/scripts/fetch-pr-comments.sh b/plugins/ndf/skills/fix/scripts/fetch-pr-comments.sh index c173a041..d98ec70b 100755 --- a/plugins/ndf/skills/fix/scripts/fetch-pr-comments.sh +++ b/plugins/ndf/skills/fix/scripts/fetch-pr-comments.sh @@ -18,7 +18,7 @@ FAIL_COUNT=0 # 1. インラインコメント (diff の特定行に紐づく) # 本文全体を保持する。改行は \n エスケープして 1 行に収める。 if ! gh api "repos/${REPO}/pulls/${PR}/comments" --paginate --jq \ - '.[] | "\(.path // "?"):\(.line // .original_line // "?") [\(.user.login)] \(.body // "" | gsub("\n"; "\\n"))"'; then + '.[] | "\(.path // "?"):\(.line // .original_line // "?") [\(.user.login)] \(.body // "" | gsub("\n"; "\\n") | gsub("```"; "` ` `"))"'; then echo "WARNING: インラインコメントの取得に失敗しました (repos/${REPO}/pulls/${PR}/comments)" >&2 (( FAIL_COUNT += 1 )) || true fi @@ -26,7 +26,7 @@ fi # 2. レビュー body (CHANGES_REQUESTED / COMMENTED 等の総評) # 本文全体を保持する。改行は \n エスケープして 1 行に収める。 if ! gh api "repos/${REPO}/pulls/${PR}/reviews" --paginate --jq \ - '.[] | select(.body != null and .body != "") | "[REVIEW-BODY] [\(.user.login)] state=\(.state) \(.body | gsub("\n"; "\\n"))"'; then + '.[] | select(.body != null and .body != "") | "[REVIEW-BODY] [\(.user.login)] state=\(.state) \(.body | gsub("\n"; "\\n") | gsub("```"; "` ` `"))"'; then echo "WARNING: レビュー body の取得に失敗しました (repos/${REPO}/pulls/${PR}/reviews)" >&2 (( FAIL_COUNT += 1 )) || true fi @@ -34,7 +34,7 @@ fi # 3. PR レベルコメント (Conversation タブの通常コメント) # 本文全体を保持する。改行は \n エスケープして 1 行に収める。 if ! gh api "repos/${REPO}/issues/${PR}/comments" --paginate --jq \ - '.[] | "[PR-COMMENT] [\(.user.login)] \(.body // "" | gsub("\n"; "\\n"))"'; then + '.[] | "[PR-COMMENT] [\(.user.login)] \(.body // "" | gsub("\n"; "\\n") | gsub("```"; "` ` `"))"'; then echo "WARNING: PR レベルコメントの取得に失敗しました (repos/${REPO}/issues/${PR}/comments)" >&2 (( FAIL_COUNT += 1 )) || true fi