setup-repo-guards 対象リポジトリへ組織標準の CI ガード一式を導入する。導入するのは次の 4 点で、 Step 1 → 4 の順に、各 Step を PR → CI 全 pass → レビュースレッド resolve → squash merge で確定させてから次へ進む。 1. ai-review(PR 自動レビュー workflow の wrapper) 2. AGENTS.md(AI レビューが読むレビュー観点集) 3. CI 必須チェックの集約ジョブ( 等) 4. branch protection ruleset・リポジトリ追加ガード(GitHub API のみ、コミット不要。適用前に退避・差分提示・明示承認を必須とする) この順序を守る理由: 「必須チェックに入れる集約ジョブが先に存在する」「AGENTS.md は PR の base コミット参照のため マージ後の次の PR から実効」という依存関係が、この順で自然に満たされる。 使い方 - 第 1 引数: 対象リポジトリ名( )。許容形式は のみ(Step 0 で検証する) - 第 2 引数: visibility( / の完全一致のみ。Step 0 で検証する)。runner 方針の分岐に使う(public は ubuntu ホステッド、private は self-hosted。ai-rev…

\n # CR は read -r が除去しないため、束縛後の値で検出できる(改行は束縛時の bind_ok で検査済み)\n case \"${repo}${visibility}\" in *

setup-repo-guards 対象リポジトリへ組織標準の CI ガード一式を導入する。導入するのは次の 4 点で、 Step 1 → 4 の順に、各 Step を PR → CI 全 pass → レビュースレッド resolve → squash merge で確定させてから次へ進む。 1. ai-review(PR 自動レビュー workflow の wrapper) 2. AGENTS.md(AI レビューが読むレビュー観点集) 3. CI 必須チェックの集約ジョブ( 等) 4. branch protection ruleset・リポジトリ追加ガード(GitHub API のみ、コミット不要。適用前に退避・差分提示・明示承認を必須とする) この順序を守る理由: 「必須チェックに入れる集約ジョブが先に存在する」「AGENTS.md は PR の base コミット参照のため マージ後の次の PR から実効」という依存関係が、この順で自然に満たされる。 使い方 - 第 1 引数: 対象リポジトリ名( )。許容形式は のみ(Step 0 で検証する) - 第 2 引数: visibility( / の完全一致のみ。Step 0 で検証する)。runner 方針の分岐に使う(public は ubuntu ホステッド、private は self-hosted。ai-rev…

\\r'*) echo \"不正な入力: CR を含む\"; return 1 ;; esac\n if ! [[ \"${repo}\" =~ ${repo_re} ]]; then echo \"不正な repo: 形式外\"; return 1; fi\n name=\"${repo#*/}\"\n case \"${repo}\" in -*) echo \"不正な repo: 先頭が -\"; return 1 ;; esac\n case \"${name}\" in .|..) echo \"不正な repo: . / .. は不可\"; return 1 ;; esac\n case \"${visibility}\" in public|private) ;; *) echo \"不正な visibility\"; return 1 ;; esac\n}\n# 一括適用の例: 組ごとに呼び、失敗した組だけ完了報告へ回して次へ進む\nif [ \"${bind_ok}\" -ne 1 ]; then echo \"除外: 複数行または空の入力\"\nelif ! validate_pair \"${repo}\" \"${visibility}\"; then echo \"除外: 検証失敗\"; fi\n```\n\n- 拒否する値: 余分な `/` を含む値、先頭が `-` の値(`gh` のオプションと取り違えられる)、\n リポジトリ名が `.` / `..` だけの値、空文字、空白・改行・`;` 等を含む値\n- 大文字小文字の違いや末尾の空白・改行を暗黙に補正しない(補正せず拒否する)\n- 実在確認: 形式検証を通った後に読み取りのみで確かめる\n\n ```bash\n gh api \"repos/${repo}\" --jq '{full_name, private, archived, admin: .permissions.admin}'\n ```\n\n - `full_name` が入力と一致すること\n - `private` が引数の visibility と整合すること(`public` なら false、`private` なら true)。\n 不一致なら中止して報告し、勝手に補正しない\n - `admin == true` かつ `archived == false` であること(ruleset の作成・更新には admin 権限が必要で、\n archived は読み取り専用で変更できないため。満たさなければ後続 Step が必ず失敗する)\n - 取得失敗時は `gh api` のエラーが stdout に出るため終了コードだけで判断せず、\n HTTP status(403 / 404 等)を確認して「判定不能」として中止する。通過に倒さない\n- 以降の Step で使う `repo` / `visibility` は、この検証を通った値だけにする。\n 検証に失敗した場合は `gh` を含むコマンドを一切実行せず中止し、理由を報告する\n\n### Step 1: ai-review の導入(未導入の場合のみ)\n\n`.github/workflows/ai-review.yml` を wrapper として追加し、Fandhe-AI/actions の reusable workflow\n`ai-review.yml` を **可変タグ `@latest`** で参照する(Fandhe-AI/actions の利用側規約。`latest` は main へ\n自動追従するため、参照の更新作業は不要。commit SHA 固定・`@main` は使わない)。\n旧 `codex-review.yml` は Fandhe-AI/actions から削除済みのため、新規導入では使わない。\n\n- 既に `.github/workflows/ai-review.yml` がある場合はこの Step をスキップする\n- 旧 `.github/workflows/codex-review.yml` が残っている場合は、`codex-review.yml@latest` が解決できず\n レビューが動かない状態になっている。Fandhe-AI/actions の `ai-review/README.md`「codex-review からの移行」の\n 手順(入力名の対応表・required check 名の差し替え順序)に従って置き換える\n\nwrapper は単一 provider テンプレートをコピーして作る(既定は `provider: codex`、ジョブ ID `codex`)。\n取得元を commit SHA に固定し、配置前に構造検査を通す。テンプレートを ref 指定なし・無検証で配置すると、\n取得元の改ざん・誤更新がそのまま対象リポジトリの workflow になるため。\n\n- `@latest` を wrapper に残すのは Fandhe-AI/actions の組織規約として受容した残留リスクである\n (実行時の reusable workflow の内容は wrapper 側で固定しない)。本手順の固定取得と検査が塞ぐのは\n 「wrapper ファイル自体の改ざん・誤配置」だけで、`latest` タグ自体の改ざんは検査と目視で補う限界がある\n- 検査スクリプト(`scripts/verify-ai-review-wrapper.sh`)は allow-list 方式で、`uses:` が期待値と完全一致で\n ちょうど 1 件・特権トリガー無し・`run:` / `steps:` / `env:` 無し・`secrets: inherit` 無し・権限が\n `contents: read`(ジョブ側のみ加えて `pull-requests: write`)に限られ、両 permissions が欠落していないこと、\n `on:` 配下が `pull_request` のみであることを確かめる(親キーを追跡し、任意のジョブ名は `jobs:` 直下だけ許可)。\n 構造は行の位置に依存しない状態機械で追い、ジョブの設定はジョブ ID の行より後ろ(現在のジョブ)でだけ受理して、そのジョブ自身が `uses`・権限・`with.provider` を揃えていることを確かめる(ジョブ ID を末尾へ移す等の並べ替えは不可)。\n 値はキーごとに許可する文法へ完全一致させる(`with` の値は 1 行の plain scalar か全体を引用符で囲んだ文字列で、`: `・`#`・anchor・tag・flow 記法は不可。\n `types` / `branches` は `[a, b]` を要素に分割し、各要素の前後の空白だけを除いて許可語と完全一致、`types` は `opened` / `synchronize` / `reopened` が要素として揃うこと。空要素・要素内部の空白・重複は不可)。\n 文字列が必要な位置(`name`・ジョブ ID・`concurrency.group`・`with` の文字列キー・`branches` の要素)では、引用符なしで書くと YAML が null・真偽値・数値・日付に解決する値(`null`・`~`・`true`・`yes`・`on`・`1`・`1_000`・`0x1F`・`1.5`・`.inf`・`2026-10-24` 等。YAML 1.1 と 1.2 の和集合)を拒否する。\n `with` の値は引用符で囲めば文字列として許可する(`runner: \"yes\"`)。`branches` の要素は引用符つきに未対応のため、数字だけ・日付形のブランチ名は許可リスト更新の対象にする。\n `concurrency` は任意だが、あるときは `group` を必須とする。ファイル内容は出力しない\n\n```bash\ntmp=\"$(mktemp -d)\" || exit 1\ntrap 'rm -rf \"${tmp}\"' EXIT\n# latest を commit SHA に解決(取得中にタグが動いても同じ commit を使う)\nsha=\"$(gh api repos/Fandhe-AI/actions/commits/latest --jq .sha)\" || exit 1\nprintf '%s' \"${sha}\" | grep -Eq '^[0-9a-f]{40}

setup-repo-guards 対象リポジトリへ組織標準の CI ガード一式を導入する。導入するのは次の 4 点で、 Step 1 → 4 の順に、各 Step を PR → CI 全 pass → レビュースレッド resolve → squash merge で確定させてから次へ進む。 1. ai-review(PR 自動レビュー workflow の wrapper) 2. AGENTS.md(AI レビューが読むレビュー観点集) 3. CI 必須チェックの集約ジョブ( 等) 4. branch protection ruleset・リポジトリ追加ガード(GitHub API のみ、コミット不要。適用前に退避・差分提示・明示承認を必須とする) この順序を守る理由: 「必須チェックに入れる集約ジョブが先に存在する」「AGENTS.md は PR の base コミット参照のため マージ後の次の PR から実効」という依存関係が、この順で自然に満たされる。 使い方 - 第 1 引数: 対象リポジトリ名( )。許容形式は のみ(Step 0 で検証する) - 第 2 引数: visibility( / の完全一致のみ。Step 0 で検証する)。runner 方針の分岐に使う(public は ubuntu ホステッド、private は self-hosted。ai-rev…

|| { echo \"SHA の形式が不正のため中止\"; exit 1; }\n# gh api はエラー本文も stdout へ出すため、成否は終了コードで判定する\ngh api --method GET -H 'Accept: application/vnd.github.raw' -f ref=\"${sha}\" \\\n repos/Fandhe-AI/actions/contents/ai-review/templates/ai-review.single.yml \\\n > \"${tmp}/ai-review.yml\" || { echo \"取得失敗のため中止\"; exit 1; }\n# 構造検査(不合格なら配置しない。\u003cskill-dir> はこのスキルのディレクトリ)\nbash \u003cskill-dir>/scripts/verify-ai-review-wrapper.sh \"${tmp}/ai-review.yml\" || exit 1\nmkdir -p .github/workflows\ncp \"${tmp}/ai-review.yml\" .github/workflows/ai-review.yml\necho \"取得元: Fandhe-AI/actions@${sha}\"\n```\n\n- 配置後は `cat .github/workflows/ai-review.yml` で全文を目視確認し、コミット本文に\n `取得元: Fandhe-AI/actions@\u003c上で出力した SHA>` を記録する\n- 検査が不合格のときは配置せず、取得内容を目視で確かめる。上流の正当な変更なら、先に\n `verify-ai-review-wrapper.sh` の許可リストを更新する PR を出す(検査を緩めて通さない)\n\n- 書き方・適用条件(fork PR 拒否・sudo なし runner 等)は Fandhe-AI/actions の `ai-review/README.md` と\n `ai-review/docs/runner-exception.md`、既存導入リポジトリ(fandhe-frontend 等)の wrapper を参照して揃える\n- runner 方針: CI ジョブは public なら ubuntu ホステッド、private なら self-hosted。\n ai-review の review ジョブ(`runner`。codex の既定は self-hosted の `codex` ラベル)のみ専用 runner の例外。\n `preflight` / `post_feedback`(`post-feedback-runner`)は資格情報に触れず、既定で可視性に応じて\n 自動切替する(public は `ubuntu-latest`、private は `self-hosted`)ため、明示指定は不要\n- 資格情報: `CODEX_HOME_DIR` は org 単位の Actions variable(visibility: all)として設定済みのため、リポジトリには設定しない。\n `gh api orgs/Fandhe-AI/actions/variables --jq '.variables[].name'` で存在を確認し、無い場合(他 org 等)は\n `ai-review/README.md`「セットアップ」手順 3 に従ってリポジトリへ設定する\n- `update-external` の日次同期 PR(`chore/skills-update-*` / `chore/submodule-update-*`)がある場合は\n `skip-branch-prefixes: chore/skills-update-,chore/submodule-update-` を指定する。push できる主体は\n ブランチ名で gate を回避できる受容済み残留リスクがある(`ai-review/README.md`\n 「`skip-branch-prefixes` の受容済み残留リスク」)ため、**指定する前に受容条件を確認する**:\n - 確認手順(読み取りのみ。`permission` で絞らず全件取得し、各主体の `role_name` / `permissions` で判定する):\n - 個人: `gh api \"repos/${repo}/collaborators?affiliation=all\" --paginate --jq '.[] | {login, role_name, permissions}'`\n で全件取得し、`role_name` が `write` / `maintain` / `admin`(または `permissions.push == true`)の主体を write 以上とみなす。\n `affiliation=all` は組織メンバー・チーム経由の権限も返すため、outside collaborator は\n `affiliation=outside` で別途列挙して第三者の有無を確認する\n - チーム: `gh api \"repos/${repo}/teams\" --paginate --jq '.[] | {slug, permission}'` で、\n `permission` が `push` / `maintain` / `admin` のチームを列挙し、メンバーが信頼済み主体だけか確認する\n - fork 経路: `gh api \"repos/${repo}\" --jq '{fork, allow_forking}'` と\n `gh api \"repos/${repo}/forks\" --paginate --jq '.[].full_name'` で外部 fork の有無を確認する\n - 受容してよい条件(すべて満たすこと): write 以上の権限者が org 内の信頼済み主体に限られる/\n skip 対象の prefix が `chore/skills-update-` と `chore/submodule-update-` に限られる/\n 該当ブランチの PR が update-external の自動生成に限られる\n - 受容しない条件(1 つでも当たれば指定しない): 第三者の write コラボレーターや outside collaborator がいる/\n fork からの外部 push 経路がある/確認結果を取得できない(判定不能は受容に倒さない)\n - 受容しない場合の代替: `skip-branch-prefixes` を指定せず、同期 PR にも通常どおり ai-review を走らせる。\n 同期 PR の負荷が問題なら別対策として Issue 化をユーザーに提案する(ruleset のブランチ名ルール制限は\n 本スキルの範囲外。提案にとどめる)\n - 確認結果と判断(受容した/しなかった、根拠)を完了報告に 1 行で記録する\n- `collaborators-only`(write 未満の actor のイベントでは `preflight` を失敗させる)は既定の `true` のまま使う\n- 資格情報が使えない間は review ジョブが `skipped` になり、workflow 全体では成功に見える。\n 導入 PR では必ずジョブ単位で `review` が実行されたことを確認する(下記「検証」)\n\n### Step 2: AGENTS.md(AI レビューの観点集)の追加\n\nai-review は **PR の base コミットの AGENTS.md** をリポジトリ固有のレビュー基準として読む。\nリポジトリルートに日本語で新規作成する(既存があれば基準を弱めず不足観点を追記する)。\n\n必須 3 観点を、必ず**対象リポジトリ固有の具体項目**に落とし込んで書く(別リポジトリ用の記述を流用しない):\n\n1. **セキュリティ観点**: 秘密情報混入・インジェクション・依存監査・権限最小化など、リポジトリの実態に即して\n2. **アーキテクチャ・設計整合の観点**: CLAUDE.md・設計文書の責務境界・規約整合\n3. **再利用・アセット化の観点**: 汎用実装の分離・ハードコード回避・転用容易性・ドキュメント整備\n\n加えて:\n\n- CLAUDE.md / `.claude/rules/` / 設計文書から抽出したリポジトリ固有観点を整理する\n- 重大度区分 P0(マージブロック)/ P1(強く推奨)/ P2(提案)を付け、**P1 の定義は CI ゲートの実挙動\n (ai-review の `block-priorities` 既定 `P0,P1` により review ジョブは P1 でも fail する)と矛盾させない**\n- カスタム prompt(`.github/ai-review/prompts/review.md`)が既にある場合は二重管理を避ける:\n 観点の正は AGENTS.md、enforcement(完了判定・機械格上げ)の正は review.md という役割分担にし、\n review.md へは AGENTS.md 読み取りの最小追記のみ行う。旧パス `.github/codex/prompts/review.md` に\n 置いたままの場合は `.github/ai-review/prompts/review.md` へ移す(または `prompt-path` で旧パスを明示する)\n- 参考実例: fandhe-backend / fandhe-frontend / Fandhe-AI/actions / agent-cli-skills の AGENTS.md\n- 注意: 新基準はマージ後の**次の PR から**実効(base 参照仕様)\n\n### Step 3: CI 必須チェックの集約ジョブ整備\n\n複数ジョブを持つ workflow には集約ジョブ(`ci-complete` 等)を追加する:\n全ジョブを `needs:` に列挙し、`if: always()` を付け、`toJSON(needs)` を jq で判定する。\n\n- **fail-closed を厳守**: `skipped` の許容は「条件付きジョブ(`if:` を持つジョブ)」の**明示リストに限定**し、\n 他ジョブは success のみ受理する(skipped 全許容は fail-open として AI レビューに P1 指摘される)。\n change detection ゲートがある場合はゲート出力に基づき「skip が正しい状況」でのみ skipped を受理する\n- 集約ジョブには `permissions: {}` を付け、「ジョブ追加時は needs への追加が必要」のコメントを書く\n- Fandhe-AI/actions の reusable workflow(lint-docs 等)を使っている場合、集約ジョブ\n (`lint-docs / lint-docs-complete` 等)が入った SHA まで参照をバンプすればチェック 1 件に集約できる\n- YAML の `name:` に ` #` や「: 」を含む場合は必ずクォートする(未クォートだと API 報告名が途中で切れ、\n 必須チェック名として不安定になる)\n\n### Step 4: branch protection / リポジトリ設定(GitHub API のみ、コミット不要)\n\nStep 4 の書き込み系呼び出し(PATCH / PUT / POST / DELETE)は、**4.1 退避 → 4.2 差分提示と明示承認 → 4.3 適用**\nの順を必ず踏む。承認が得られない場合は何も変更せず中止する(fail-closed)。誤適用したときは 4.4 で戻す。\n`PUT /rulesets/{id}` は `required_status_checks.parameters` を丸ごと置換し、`integration_id` 束縛などが\n黙って脱落し得るため、現在値の保存と差分の確認なしに送らない。\n\n#### 4.1 退避(変更前の現在値を作業ツリーの外へ保存)\n\n- 保存先は `mktemp -d` が返す絶対パスのディレクトリにする。対象リポジトリの作業ツリー内・`.claude/` 配下には\n 置かない(誤コミット防止)。`chmod 700` を付け、パスを利用者へ表示して復旧まで保持する旨を伝える。\n 複数リポジトリの一括適用ではリポジトリごとに別ディレクトリにする\n- リポジトリ名は `owner/repo` 形式を検証してから使う\n- 退避対象(取得は 1 件ごとに成否を確認し、存在判定は終了コードではなく HTTP status で行う。`gh api` のエラー本文は stdout に出る):\n - 全 branch ruleset: 一覧と、各 ruleset の詳細 JSON(`Repository` は `repos/{repo}/rulesets/{id}`、`Organization` は\n 読み取りのみ。org 継承 ruleset はリポジトリ側 API で変更しない)。tag ruleset を扱う場合はその一覧と詳細も\n - リポジトリ本体: `GET repos/{repo}` の `allow_squash_merge` / `allow_merge_commit` / `allow_rebase_merge` /\n `delete_branch_on_merge` / `allow_auto_merge` / `squash_merge_commit_title` / `squash_merge_commit_message` /\n `has_wiki` / `has_projects` / `allow_forking` / `security_and_analysis`\n - Actions 権限: `GET repos/{repo}/actions/permissions` と `GET repos/{repo}/actions/permissions/workflow`\n - Dependabot alerts(`vulnerability-alerts`): HTTP status で有効 / 無効を記録する(204 = 有効 / 404 = 無効)\n - automated security fixes・private vulnerability reporting: 有効でも無効でも 200 と `{\"enabled\": bool}` を返すため、\n status では判別できない。本文の `enabled` を JSON として退避する\n- 取得失敗(403 / 404 以外の異常を含む)は「未取得」と明示し、未取得の項目は変更しない。取得失敗を「現在値なし」に倒して\n 新規作成・上書きしない(判定不能を green に倒さない方針と同じ)\n- 退避の完了条件は「ruleset 一覧を全ページ取得(`--paginate --slurp` + `jq 'add'`)して単一配列に統合できた」かつ「取得に成功した全 `*.json` が `jq -e .` で JSON として妥当」かつ「ruleset 一覧にある Repository ruleset の全 id の詳細が退避済み」であること。未取得 id が 1 件でもあれば列挙して 4.2 へ進まない(ゲート。その ruleset は変更しない)。取得失敗時は壊れたファイル(空・エラー本文)を残さず、そのファイルは「未取得」として扱う(4.2 の差分・4.4 の復旧の入力に使わない)\n- Bash ツールは呼び出しをまたいでシェル変数を保持しない。4.1 から 4.4 を複数回の呼び出しに分けるときは、各呼び出しの先頭で `repo` と `backup`(4.1 で表示した絶対パス)を再束縛し、`[ -d \"${backup}\" ]` を確認してから使う(空の変数のまま実行しない)\n- 退避した JSON の件数と sha256 を表示し、保存を確認してから 4.2 へ進む。保存先のパスと内容は PR 本文・コミット・ログへ貼らない(要約のみ)\n\n```bash\nrepo=\"Fandhe-AI/\u003cREPO>\"\ncase \"${repo}\" in */*) ;; *) echo \"repo は owner/repo 形式\" >&2; exit 1 ;; esac\nbackup=\"$(mktemp -d)\" && chmod 700 \"${backup}\" && echo \"退避先: ${backup}\"\n# gh api は HTTP エラー本文を stdout に出すため、直接リダイレクトせず一時ファイルへ書く。\n# 成功(終了コード 0)かつ jq -e . で JSON として妥当なときだけ mv し、失敗時は一時ファイルを消して退避ファイルを残さない\nfetch_json() { # fetch_json \u003c出力ファイル名> \u003cgh api の引数...>\n out=\"${backup}/$1\"; shift\n if gh api \"$@\" > \"${out}.tmp\" 2>/dev/null && jq -e . \"${out}.tmp\" > /dev/null 2>&1; then\n mv \"${out}.tmp\" \"${out}\"\n else\n rm -f \"${out}.tmp\"; echo \"未取得: ${out##*/}(変更しない)\" >&2; return 1\n fi\n}\n# ruleset 一覧は既定では最初のページしか返らない。--paginate --slurp で全ページを配列の配列として取り、jq 'add' で単一配列へ統合する。\n# gh の終了コードは jq と切り離して独立に確認する(パイプだと pipefail 無しでは末尾の jq の終了コードしか見えず、gh が失敗して無出力でも空の一覧が保存される)。\n# 1) gh の出力を一時ファイルへ取得し、gh が 0 以外・空出力なら未取得として止める\n# 2) 取得した JSON を統合し、配列であることを検証できたときだけ rulesets.json として確定する\n# 後段の詳細取得と完了判定は、この統合済みの rulesets.json だけを使う\nraw=\"${backup}/rulesets.raw.tmp\"\nif gh api --paginate --slurp \"repos/${repo}/rulesets\" > \"${raw}\" 2>/dev/null \\\n && [ -s \"${raw}\" ] \\\n && jq -e 'add // [] | if type == \"array\" then . else error(\"not array\") end' \"${raw}\" > \"${backup}/rulesets.json.tmp\" 2>/dev/null; then\n mv \"${backup}/rulesets.json.tmp\" \"${backup}/rulesets.json\"\n rm -f \"${raw}\"\nelse\n rm -f \"${raw}\" \"${backup}/rulesets.json.tmp\"; echo \"退避失敗: ruleset 一覧(全ページ取得不可)\" >&2; exit 1\nfi\necho \"ruleset 一覧: $(jq 'length' \"${backup}/rulesets.json\") 件(0 件なら新規作成のみ。既存を変更する前提にしない)\"\njq -r '.[] | select(.source_type == \"Repository\") | .id' \"${backup}/rulesets.json\" |\nwhile read -r id; do\n fetch_json \"ruleset-${id}.json\" \"repos/${repo}/rulesets/${id}\"\ndone\n# ゲート: 一覧にある Repository ruleset の詳細が全件退避されたこと(未取得 id があれば 4.2 へ進まない)\nmissing=0\nfor id in $(jq -r '.[] | select(.source_type == \"Repository\") | .id' \"${backup}/rulesets.json\"); do\n [ -s \"${backup}/ruleset-${id}.json\" ] || { echo \"未退避の ruleset id: ${id}(変更しない)\" >&2; missing=1; }\ndone\n[ \"${missing}\" -eq 0 ] || { echo \"退避未完了: 4.2 へ進まない\" >&2; exit 1; }\nfetch_json repo.json \"repos/${repo}\" --jq '{allow_squash_merge, allow_merge_commit, allow_rebase_merge, delete_branch_on_merge, allow_auto_merge, squash_merge_commit_title, squash_merge_commit_message, has_wiki, has_projects, allow_forking, security_and_analysis}'\nfetch_json actions-workflow.json \"repos/${repo}/actions/permissions/workflow\"\nfetch_json actions.json \"repos/${repo}/actions/permissions\"\ncode=\"$(gh api -i \"repos/${repo}/vulnerability-alerts\" 2>/dev/null | awk 'NR==1{print $2}')\"\nprintf '%s\\n' \"${code:-unknown}\" > \"${backup}/vulnerability-alerts.status\" # 204 = 有効 / 404 = 無効 / 他 = 判定不能(変更しない)\n# この 2 つは status ではなく本文の {\"enabled\": bool} で判別する\nfetch_json automated-security-fixes.json \"repos/${repo}/automated-security-fixes\"\nfetch_json private-vulnerability-reporting.json \"repos/${repo}/private-vulnerability-reporting\"\n# 完了条件: 退避された *.json がすべて JSON として妥当であること\nfor f in \"${backup}\"/*.json; do jq -e . \"${f}\" > /dev/null || echo \"不正な退避: ${f##*/}(変更しない)\" >&2; done\n(cd \"${backup}\" && ls -1 && shasum -a 256 -- * )\n```\n\n#### 4.2 差分提示と明示承認\n\n- 適用予定の本文(PUT / PATCH / POST の body)を退避先と同じディレクトリの一時ファイルに作り、現在値との差分を\n `jq -S` で正規化した JSON 同士の `diff` で提示する(キー順の差を無視する)\n- 最低限、次の増減を明示する: `required_status_checks` の `context` 一覧と各エントリの `integration_id` の有無(脱落)/\n `strict_required_status_checks_policy` / `bypass_actors` の件数 / `enforcement` / `required_review_thread_resolution` /\n `allowed_merge_methods`\n- 新規作成(POST)で現在値が無い場合は「新規作成」と明記し、復旧は DELETE であることを併記する\n\n```bash\ndiff \u003c(jq -S . \"${backup}/ruleset-\u003cID>.json\") \u003c(jq -S . \"${backup}/ruleset-\u003cID>.new.json\")\n# integration_id の脱落を直接確認(現在値で束縛済みの context のうち、新 body で束縛が無くなったものを列挙)\njq -rn --slurpfile a \"${backup}/ruleset-\u003cID>.json\" --slurpfile b \"${backup}/ruleset-\u003cID>.new.json\" '\n def bound($x): [$x.rules[]? | select(.type==\"required_status_checks\") | .parameters.required_status_checks[]? | select(.integration_id != null) | .context];\n (bound($a[0]) - bound($b[0]))[]'\n```\n\n- 利用者へ「適用してよいか」を尋ね、肯定の明示返答を得てから 4.3 へ進む。無返答・曖昧な返答・質問できない自動運転環境では\n 適用せず、差分と退避先を報告して停止する\n- 承認はそのリポジトリの、その差分 1 回分に限る。差分が変わったら再提示して再承認を得る。一括適用でもリポジトリごとに承認を取る\n- 承認を事実以上に記述しない。得ていない承認を完了報告・PR 本文に「承認済み」と書かない\n\n#### 4.3 適用(4.2 で承認された差分の範囲でのみ実行)\n\nruleset を更新する PUT は、4.1 で退避した JSON の `required_status_checks` 等の必要箇所だけを差し替えた body を\n`--input \u003cfile>` で送る(丸ごと新規構築しない・本文を文字列連結しない)。適用後は直ちに下記「検証」の\nStep 4-a / 4-b スイープを実行し、崩れていたら 4.4 で戻す。新規作成した ruleset の id・変更項目一覧・退避先パスを\n完了報告に記録する。\n\n- マージ設定(`gh api -X PATCH repos/...`): squash のみ(merge commit / rebase 禁止)、\n マージ後ブランチ自動削除、auto-merge 許可、squash タイトル = PR_TITLE / 本文 = PR_BODY\n- ruleset `main-protection`(target: branch、`~DEFAULT_BRANCH`、enforcement: active、bypass_actors 空):\n - deletion 禁止 / non_fast_forward(force push 禁止)\n - pull_request: required_approving_review_count 0(人間 approver 不在の AI 運用。レビューゲートは\n ai-review の必須チェックで担保)・required_review_thread_resolution true・allowed_merge_methods [\"squash\"]\n - **0 承認では ai-review の必須チェックが唯一のレビューゲートになる**。これは設計上の受容事項であり、\n 次の条件をすべて満たすリポジトリでのみ適用する(strict false の規定とは独立の論点):\n 人間 approver 不在の運用を意図している/必須チェックに `preflight`・`review`・`post_feedback`\n (または `ai-review-gate`)が入っている/`collaborators-only` が既定の `true`/bypass_actors が空\n - 適用後(下記「検証」)に `review` ジョブが実際に実行されたことを確認する(適用前には確認できないため、\n 条件ではなく適用後の事後確認として扱う)。確認できない、または skipped のままなら、受容条件を満たさない\n ものとして `required_approving_review_count` を 1 以上へ是正する。**この是正も ruleset の書き込みであり、\n 4.4(復旧)とは別物**(4.4 は退避した元の値へ戻す操作で、既存の 0 承認 ruleset では 0 に戻るだけで是正にならない)。\n したがって 4.1(是正前の現在値を再度退避)→ 4.2(`jq -S` 差分の提示と明示承認)→ 4.3(GET した ruleset JSON の\n `pull_request` ルールの `required_approving_review_count` だけを差し替えて `--input \u003cfile>` で PUT する。\n `required_status_checks` の `integration_id` 束縛を含む他フィールドを保持し、素朴な PUT をしない)の順に通し、\n 適用後に `.claude/rules/ruleset-policy.md` の 3 軸 + classic BP 検証(下記 Step 4-a / 4-b)を実行する。\n 承認が得られない場合は書き込まず、受容不可として完了報告に記録して停止する\n 適用から確認までの間は `review` ジョブがまだ動いておらず、レビューゲートなしでマージできる窓が残る。\n この間は PR をマージしない(確認を済ませるまで自動マージも走らせない)\n - 条件を満たさない場合の代替: `required_approving_review_count` を 1 以上にする(可能なら CODEOWNERS と併用)。\n approver がいない単独運用ではマージが止まり、人間 approve を前提としない `implement-issue-tree` の\n 自動マージも止まり得るため、運用者に確認してから設定する\n - required_status_checks(**strict false は必須**。true にすると 1 件マージするたびに他の open PR の\n base が陳腐化し、implement-issue-tree の並列ランが収束しなくなる。strict は鮮度制御であって\n bypass 不能性の制御ではないため、false でもクライアント側自動マージの G0 は通過する。\n 実行環境に `.claude/rules/ruleset-policy.md` が存在する場合はそちらの詳細に従う): 必須集合は\n `autoMerge` を使うかどうかで変わる(下記「必須チェック選定の注意」参照)。使わない場合は\n **[\u003c集約ジョブ>, (集約できない別 workflow の常時チェック), ai-review の 3 チェック]** の最小集合、\n 使う場合は PR HEAD へ報告される全 context。ai-review の 3 チェックはジョブ ID が先頭になる\n `\u003cjob> / preflight`・`\u003cjob> / review`・`\u003cjob> / post_feedback`(テンプレート既定なら `codex / preflight` 等。\n 複数モデル構成ではテンプレート同梱の集約 gate `ai-review-gate` 1 件でもよい)。個別ジョブを登録する場合は\n **`preflight` を必ず含める**: actor 判定等で `preflight` が失敗すると後続が skipped になり、skipped は\n required check を満たしてしまう(`ai-review-gate` は reviewer の `preflight` 失敗で自身も失敗するため、\n gate 1 件で登録する場合は個別の `preflight` を追加しなくてよい)\n - 自動マージ(implement-issue-tree の `autoMerge: true`)を使う場合、required_status_checks の\n 各エントリに発行元 App の `integration_id` を束縛する(未束縛だと G0 が `issuer-unbound` で辞退する)。\n **ruleset を PUT で更新した後は必ず下記「検証」の 3 軸スイープを実行する**(`PUT` は\n `required_status_checks.parameters` を丸ごと置換するため束縛が落ち得る。複数リポへ一括適用する\n 場合は全リポで実行する)\n- 必須チェック選定の注意(重要な落とし穴):\n - 直近 PR の check runs で「**常に報告される**」チェックのみ選ぶ。workflow レベル paths フィルタで\n 実行されないことがあるチェックは入れない(マージが永久ブロックされる)。ジョブレベル条件の skipped は可\n - **autoMerge を使わない場合**: Cursor Bugbot 等の外部アプリは必須にしない。\n - **`autoMerge: true`(implement-issue-tree)を使う場合**: 上記の除外は適用しない。\n `implement-issue-tree` の autoMerge G0 は「PR HEAD 上の check-run / commit status のうち\n required_status_checks に含まれない context が 0 件であること」を合格条件にする\n (`skills/implement-issue-tree/references/automerge-design.md` の G0 (d)/(f)/(g))。\n 外部アプリを required から外すと、その context が G0 (f) に「required でない context」として\n 検出され `server-enforcement-missing` で辞退し、自動マージが構造的に成立しない。\n `args.externalChecks` で宣言した外部 App の context と ai-review workflow の全ジョブ\n (`preflight` / `review` / `post_feedback`)を含め、**PR HEAD へ報告される全 context**\n を required に列挙し、外部 App の required エントリは発行元 App の ID(ruleset は\n `integration_id`)で束縛する。commit status は App 束縛(`integration_id`)を持てないため、\n 束縛不能な commit-status 専用チェックしか発行しない外部プロバイダは autoMerge と併用できない\n (G0 (g) の帰結)。paths フィルタで実行されないことがあるチェックを必須化できない制約と\n 両立させるには、条件は workflow の `on.pull_request.paths` ではなくジョブレベル `if:` に寄せ、\n check-run 自体は常に報告させる(skipped も可)実装パターンを使う(実例:\n `Fandhe-AI/fandhe-frontend` の `musl-smoke.yml` / `image-size.yml` の「Detect relevant changes」\n ステップ。同リポの `.claude/rules/ci.md`「`ci-complete` 集約ジョブと ruleset 必須チェック」節\n 〔イシュー #2325〕も、外部チェックを required から外さない運用を採用している)。\n - **チェック名が変わる PR をマージするときは、マージ前に ruleset を新チェック名へ PUT で置換する**\n (旧名のままだと CI 全 pass でも「Expected」のまま BLOCKED になる)。PUT は GET した JSON の\n required_status_checks のみ差し替えて送る\n- 追加ガード: secret scanning + push protection / Dependabot alerts + automated security fixes /\n (public なら)private vulnerability reporting / タグ運用があれば tag ruleset(deletion・non_fast_forward)/\n GITHUB_TOKEN 既定権限 read 化(**全 workflow の permissions: 明示を確認してから**)/\n Actions の PR 作成・承認許可は自動 PR 運用(update-external 等)が無ければ false /\n (private なら)forking 禁止 / 未使用の wiki・projects 無効化\n- プラン制限(403/422)に当たった項目は「ユーザー操作が必要」として記録し、他を続行する\n\n#### 4.4 復旧(退避した JSON で元に戻す)\n\n復旧自体も書き込みなので、実行前に復旧内容を利用者へ提示して承認を得る。復旧に失敗した項目は握りつぶさず報告する。\n\n- 既存 ruleset の復旧: 退避した詳細 JSON から PUT 可能なフィールド(`name` / `target` / `enforcement` / `bypass_actors` /\n `conditions` / `rules`)だけを抽出し、`PUT repos/{repo}/rulesets/{id}` へ `--input` で送る。読み取り専用フィールド\n (`id` / `source` / `source_type` / `node_id` / `_links` / `created_at` / `updated_at` / `current_user_can_bypass`)は送らない。\n 復旧後は 3 軸スイープで退避時点と同じ値(strict / bypass / `integration_id` / total)に戻ったことを実測する\n- 新規作成した ruleset の復旧: 記録した id のみ `DELETE repos/{repo}/rulesets/{id}`(他の ruleset は触らない)\n- リポジトリ本体: 退避した値のうち変更した項目だけを `PATCH repos/{repo}` で戻す\n- 追加ガード: 退避時の状態へ対応する API で戻す(Dependabot alerts / automated security fixes / private vulnerability\n reporting は `PUT` / `DELETE`、Actions 権限は `PUT actions/permissions/workflow`、secret scanning / push protection は\n `security_and_analysis` の PATCH)\n- 退避ディレクトリは復旧不要と確認できるまで削除しない。削除は自分が `mktemp -d` で作ったものだけに限る\n\n```bash\njq '{name, target, enforcement, bypass_actors, conditions, rules}' \"${backup}/ruleset-\u003cID>.json\" > \"${backup}/ruleset-\u003cID>.restore.json\" \\\n && gh api -X PUT \"repos/${repo}/rulesets/\u003cID>\" --input \"${backup}/ruleset-\u003cID>.restore.json\"\n```\n\n## 検証\n\n各 Step の完了は以下で確認する:\n\n```bash\nrepo=\"Fandhe-AI/\u003cREPO>\" # Step 0 の検証を通過した値のみ\n# Step 1: wrapper の存在と @latest 参照(@main・旧 codex-review.yml が残っていないこと)\ngrep -n \"uses: Fandhe-AI/actions\" .github/workflows/ai-review.yml\n# Step 1: 配置済み wrapper が構造検査(uses 1 件・特権トリガー無し・権限最小)に合格すること\nbash \u003cskill-dir>/scripts/verify-ai-review-wrapper.sh .github/workflows/ai-review.yml\ntest ! -e .github/workflows/codex-review.yml && echo \"旧 wrapper なし\"\n# Step 1: 直近 run でジョブ単位に review が実行されたこと(skipped は「workflow 成功」に見える)\n# head_branch は検証対象の PR(導入 PR 等)の head ブランチ名\nrun_id=$(gh run list --repo \"${repo}\" --workflow \"AI PR review\" --event pull_request --branch \"${head_branch}\" --limit 1 --json databaseId --jq '.[0].databaseId')\ngh run view \"${run_id}\" --repo \"${repo}\" --json jobs --jq '.jobs[] | {name, status, conclusion}'\n# Step 2: AGENTS.md の存在と P0/P1/P2 定義\ngrep -n \"P0\\|P1\\|P2\" AGENTS.md | head\n# Step 3: 直近 PR の check runs で集約ジョブが報告されること\ngh pr checks \"$(gh pr list --repo \"${repo}\" --state merged --limit 1 --json number --jq '.[0].number')\" --repo \"${repo}\"\n# Step 4: 退避ファイルの存在と件数(4.1)\nls -1 \"${backup}\" | wc -l\n# Step 4: 適用後の ruleset が承認済みの差分と一致すること(4.2 の新 body と現在値を jq -S で比較し、差分が空であること)\n# Step 4: ruleset とマージ設定\ngh api \"repos/${repo}/rulesets\" --jq '.[] | {id, name, enforcement}'\ngh api \"repos/${repo}\" --jq '{allow_squash_merge, allow_merge_commit, allow_rebase_merge, delete_branch_on_merge, allow_auto_merge}'\n\n# Step 4-a: 一括更新後の 3 軸スイープ(strict / bypass_actors / integration_id 残存)\n# PUT した ruleset 単体ではなく branch target の全 ruleset を掃く(org 継承は source_type でルーティング)。\n# コマンド全文・判定表は実行環境に .claude/rules/ruleset-policy.md があれば「一括更新後の検証」節を参照。\n# 無ければ下記 Step 4-a/4-b のコマンド・判定表をそのまま用いる\norg=\"${repo%%/*}\"\ngh api --paginate --slurp \"repos/${repo}/rulesets\" |\n jq -r 'add // [] | .[] | select(.target == \"branch\") | [(.id|tostring), .name, (.source_type // \"unknown\")] | @tsv' |\nwhile IFS=

setup-repo-guards 対象リポジトリへ組織標準の CI ガード一式を導入する。導入するのは次の 4 点で、 Step 1 → 4 の順に、各 Step を PR → CI 全 pass → レビュースレッド resolve → squash merge で確定させてから次へ進む。 1. ai-review(PR 自動レビュー workflow の wrapper) 2. AGENTS.md(AI レビューが読むレビュー観点集) 3. CI 必須チェックの集約ジョブ( 等) 4. branch protection ruleset・リポジトリ追加ガード(GitHub API のみ、コミット不要。適用前に退避・差分提示・明示承認を必須とする) この順序を守る理由: 「必須チェックに入れる集約ジョブが先に存在する」「AGENTS.md は PR の base コミット参照のため マージ後の次の PR から実効」という依存関係が、この順で自然に満たされる。 使い方 - 第 1 引数: 対象リポジトリ名( )。許容形式は のみ(Step 0 で検証する) - 第 2 引数: visibility( / の完全一致のみ。Step 0 で検証する)。runner 方針の分岐に使う(public は ubuntu ホステッド、private は self-hosted。ai-rev…

\\t' read -r id name src; do\n case \"${src}\" in\n Repository) path=\"repos/${repo}/rulesets/${id}\" ;;\n Organization) path=\"orgs/${org}/rulesets/${id}\" ;;\n *) echo \"UNKNOWN source_type: ${name} (${src}) — 手動確認\"; continue ;;\n esac\n gh api \"${path}\" --jq '{\n name: .name, enforcement: .enforcement, bypass: (.bypass_actors | length),\n strict: ([.rules[]? | select(.type==\"required_status_checks\") | .parameters.strict_required_status_checks_policy] | first),\n total: ([.rules[]? | select(.type==\"required_status_checks\") | .parameters.required_status_checks[]?] | length),\n unbound: [.rules[]? | select(.type==\"required_status_checks\") | .parameters.required_status_checks[]? | select(.integration_id == null) | .context]\n }'\ndone\n\n# Step 4-b: classic branch protection を別枠で掃く(defaultBranchRef 解決・@uri エンコード・HTTP status 分岐)\ndb=$(gh repo view \"${repo}\" --json defaultBranchRef --jq '.defaultBranchRef.name')\ndb_enc=$(printf '%s' \"${db}\" | jq -sRr '@uri')\ncode=$(gh api -i \"repos/${repo}/branches/${db_enc}/protection\" 2>/dev/null | awk 'NR==1{print $2}')\ncase \"${code}\" in\n 200) gh api \"repos/${repo}/branches/${db_enc}/protection\" --jq \\\n '{strict: (.required_status_checks.strict // \"none\"), unbound: [.required_status_checks.checks[]? | select(.app_id == null) | .context]}' ;;\n 404) echo \"classic BP なし(${db})\" ;;\n *) echo \"判定不能 (HTTP ${code:-?}) — Administration: read 権限を確認。green と扱わない\" ;;\nesac\n```\n\n最終確認として、導入後に小さな PR を 1 件流し、ai-review の実行・必須チェックの報告・\nsquash merge のみ許可・スレッド resolve 必須が実際に効いていることを観察する。\n\n## 完了報告\n\nStep ごとの PR 番号、AGENTS.md の観点構成、ruleset の最終必須チェック一覧、適用できなかった項目\n(理由付き)、退避先パス(内容は貼らない)、承認の有無(得た場合のみ事実どおりに記載)を表でまとめて報告する。\n受容判断の記録欄として、`skip-branch-prefixes`(指定した/しなかった、write 権限者の確認結果)と\n`required_approving_review_count 0`(受容した/1 以上にした、根拠)を各 1 行で含める。\nStep 0 で拒否した引数があれば理由付きで載せる。\n\n## よくある失敗\n\n| 問題 | 回避策 |\n|------|--------|\n| 集約ジョブの skipped 全許容が fail-open として AI レビューに P1 指摘される | Step 3: skipped 許容を条件付きジョブの明示リストに限定する |\n| チェック名変更 PR が CI 全 pass でも BLOCKED(ruleset が旧名のまま「Expected」待ち) | Step 4: マージ前に ruleset を新チェック名へ PUT で置換する |\n| 未クォート `name:` の ` #` 以降が切れて API 報告名が不安定になる | Step 3: name に ` #` や「: 」を含む場合はクォート必須 |\n| paths フィルタ付き workflow のチェックを必須にするとマージが永久ブロックされる | Step 4: 常に報告されるチェックのみ必須化する |\n| 参考ファイルの取り違え(別リポジトリ用 AGENTS.md の混入)を AI レビューが検出 | Step 2: 対象リポジトリ固有の具体項目に落とし込む |\n| AGENTS.md の P1 定義と CI ゲート実挙動(P1 でも fail)の矛盾を AI レビューが指摘 | Step 2: P1 定義をゲート実挙動と矛盾させない |\n| GITHUB_TOKEN read 化で暗黙 write 依存の workflow が壊れる | Step 4: 全 workflow の permissions 明示を確認してから適用する |\n| ruleset を PUT したら `integration_id` 束縛が落ち、自動マージが静かに止まる(strict と bypass だけ見ると全 green に見える) | Step 4-a: PUT 後に 3 軸スイープ(`select(.integration_id==null)`)を実行する |\n| 旧 ruleset / classic BP の掃き漏らしで未束縛・strict=true が残る | Step 4-a/4-b: 全 branch ruleset を列挙して掃き、classic BP は `defaultBranchRef` 解決 + status 分岐で別枠確認する |\n| AGENTS.md に自動マージの G0 契約を書く際 strict を要件として列挙し、実装より強い契約が AI レビューの P0 の根拠になる | Step 2: G0 契約を書くなら strict は「意図的な非要件」と明記する(実行環境に `.claude/rules/ruleset-policy.md` があれば参照) |\n| autoMerge 運用で外部アプリ・post_feedback 等を required から外すと G0 (f) が server-enforcement-missing で辞退し自動マージが静かに恒久停止する | Step 4: autoMerge を使う場合は PR HEAD の全 context を required に列挙する(外部アプリ・ai-review の全ジョブを含む) |\n| ai-review テンプレートを ref 指定なし・無検証で配置し、取得元の改ざん・誤更新がそのまま workflow になる | Step 1: `latest` を commit SHA に解決して固定取得し、`verify-ai-review-wrapper.sh` と `cat` の目視を通してから配置する |\n| 旧 `codex-review.yml` wrapper を新規導入する・残したままにする(Fandhe-AI/actions から削除済みで workflow を解決できない) | Step 1: `ai-review/templates/ai-review.single.yml` から導入し、旧 wrapper は「codex-review からの移行」で置き換える |\n| ai-review の `preflight` を required から外し、actor 判定の失敗で review が skipped のまま必須チェックを満たす | Step 4: `\u003cjob> / preflight` を必ず required に含める |\n| 資格情報未設定で review が skipped なのに workflow 全体の成功を見て導入完了とする | 検証: `gh run view --json jobs` でジョブ単位に `review` の実行を確認する |\n| 引数を検証せず `gh api` のパスや分岐に渡す | Step 0: 許可リスト検証と実在確認を `gh` 実行前に行う |\n| 第三者 write ありのリポジトリで `skip-branch-prefixes` を指定し、ブランチ名でレビューを回避される | Step 1: write 権限者を列挙して受容条件を確認し、満たさなければ指定しない |\n| 0 承認の受容条件を確認せずに適用し、ai-review が唯一のゲートなのに `review` が動いていない | Step 4.3: 受容条件を確認し、満たさなければ 1 以上にする。適用後に `review` が動かなければ 4.1 → 4.2 → 4.3 を通して是正する(4.4 の復旧では是正にならない) |\n| 現在値を保存せずに PUT し、`integration_id` の脱落・設定項目の消失に気づけず復旧できない | Step 4.1: 変更前に作業ツリー外へ退避する |\n| 差分を示さず承認なしで適用する | Step 4.2: 差分提示と明示承認を取り、得られなければ停止する |\n| 退避ファイルを作業ツリー内に置いてコミットしてしまう | Step 4.1: 作業ツリー外の `mktemp -d` に保存する |\n| 取得失敗を「現在値なし」として扱い、誤って新規作成・上書きする | Step 4.1: 取得失敗は未取得とし、その項目は変更しない |\n\n## 注意事項\n\n- コミットは日本語 Conventional Commits 形式で作成し、対象リポジトリのコミット規約・フッター運用に従う。`--no-verify` は使用しない\n- シェル変数は `\"${var}\"` 形式でクォートする\n- Step 0 の検証を通っていない値を `gh` に渡さない\n- 承認を得るまで Step 4 の書き込み系 API を実行しない。自動運転などで承認を取れない場合は適用せず停止する\n- AI レビューの指摘が正当なら修正で応える(テスト・ゲートの弱体化やスキップでごまかさない)。\n スレッドへ対応内容を返信して resolve してからマージする\n- スコープ外の発見は Issue 化をユーザーに提案する(勝手に起票しない)\n- このスキルはネットワーク越しの GitHub 操作(ruleset の PUT・workflow の配布)を必須とする。該当コマンドはコマンド単位で sandbox 無効にして実行する。ネットワーク遮断を解除できない環境では実行できない","attachment_filenames":["scripts/verify-ai-review-wrapper.sh","tests/fixtures/[email protected]","tests/verify-ai-review-wrapper.test.mjs"],"attachments":[{"filename":"scripts/verify-ai-review-wrapper.sh","content":"#!/usr/bin/env bash\n# verify-ai-review-wrapper.sh\n#\n# 役割: setup-repo-guards の Step 1 が Fandhe-AI/actions から取得した ai-review wrapper\n# (ai-review.single.yml)を、対象リポジトリの .github/workflows/ へ配置する前に検査する。\n# 取得元の改ざん・誤更新が、そのまま対象リポジトリの workflow になるのを防ぐ関門。\n# 呼び出し元: skills/setup-repo-guards/SKILL.md Step 1(配置前に必ず実行。不合格なら配置しない)。\n# 方式: allow-list(fail-closed)。コメント行と空行を除いた有効行を 1 行ずつ許可パターンと\n# 突き合わせ、未知のキー・想定外の値は不合格にする。YAML パーサは使わない\n# (bash 3.2 と GNU bash の両方で動かすため、外部依存を持たない)。\n# 値は「キーごとに許可する文法」へ完全一致させる(共通の文字集合で緩く受けない):\n# with の値は 1 行の plain scalar(または全体を 1 組の引用符で囲んだ文字列)に限り、\n# `: `・`#`・anchor・tag・flow 記法は拒否する。types / branches / skip-branch-prefixes 等の\n# 列挙は要素に分割し、各要素の前後の空白だけを除いて許可語・要素の文法へ完全一致させる\n# (要素内部の空白・空要素・重複は拒否)。判断に迷う入力は拒否する。\n# 文字列でなければならない位置(name・ジョブ ID・concurrency.group・with の文字列キー・\n# on.pull_request.branches の要素)では、引用符なしで書かれると YAML が null・真偽値・数値・\n# 日付に解決する値(null・~・true・yes・on・1・1_000・0x1F・1.5・.inf・2026-10-24 等。\n# YAML 1.1 と 1.2 の和集合)を拒否する(is_typed_plain_scalar)。引用符で囲んだ値は文字列なので許可する。\n# concurrency は任意だが、あるときは group が必須(group の無い concurrency は GitHub Actions が\n# workflow を無効にする)。\n# 構造は位置に依存しない状態機械で追う: インデント 0 の行ごとにセクションを切り替え、\n# jobs の内側ではジョブ ID の行の後ろ(現在のジョブ)でだけ設定を受理し、uses・permissions・\n# with.provider は現在のジョブに帰属させて、ジョブを終える時点(次のジョブ ID・jobs を抜ける\n# トップレベルキー・ファイル末尾)に揃っていることを確かめる。\n# 出力: 結果行(OK / NG と違反項目名・行番号)のみ。ファイル内容は出力しない\n# (検出器が内容の漏洩経路にならないようにする)。\n# 終了コード: 0 = 合格 / 1 = 検査不合格 / 2 = 使い方の誤り・検査自体の失敗。\nset -u\n# 文字クラス・範囲の解釈をロケールに依存させない\nexport LC_ALL=C\n\nEXPECTED_USES='Fandhe-AI/actions/.github/workflows/ai-review.yml@latest'\n\nif [ \"$#\" -ne 1 ]; then\n echo \"使い方: verify-ai-review-wrapper.sh \u003c検査対象の workflow ファイル>\" >&2\n exit 2\nfi\ntarget=\"$1\"\nif [ ! -f \"${target}\" ] || [ ! -r \"${target}\" ]; then\n echo \"検査失敗: 読み取れる通常ファイルではない\" >&2\n exit 2\nfi\n\nviolations=0\njobs_seen=0 # トップレベル jobs があった\nperm_scope=\"\" # \"\" / top / job\nperm_indent=0\nsection=\"\" # 直近のトップレベルキー(on / concurrency / jobs / permissions)\ntop_perm_ok=0 # トップレベル permissions に contents: read があった\n# 以下 job_* は「現在のジョブ」(cur_job)に帰属する。ジョブ ID の行で初期化し、\n# 次のジョブ ID・jobs を抜けるトップレベルキー・ファイル末尾で finish_job が必須項目を確認する\njob_uses=0 # 現在のジョブに期待値と一致する uses があった\njob_contents=0 # 現在のジョブの permissions に contents: read があった\njob_prw=0 # 現在のジョブの permissions に pull-requests: write があった\njob_provider=0 # 現在のジョブの with に provider があった\njob_sub=\"\" # jobs 配下のインデント 4 の直近キー(with / secrets / permissions / その他)\non_seen=0 # トップレベル on があった\nconc_seen=0 # トップレベル concurrency があった\ngroup_seen=0 # concurrency 直下に group の行があった(値の良否は別途検査する)\npr_seen=0 # on 直下(インデント 2)の pull_request があった\non_sub=\"\" # on 直下(インデント 2)の直近キー(types / branches を置けるのは pull_request の直下だけ)\nseen=\" \" # 出現済みキーの集合(スコープ/キー を空白区切りで保持。重複キー検出用)\ncur_job=\"\" # 現在のジョブ ID。空 = ジョブの内側にいない(jobs の外、または jobs 直下でジョブ ID より前)\njob_count=0 # jobs 直下のジョブ数(uses と権限を同じジョブに束縛するため 1 件に限定する)\nlist_els=\" \" # parse_list が集めた要素(空白区切り。重複と必須要素の判定に使う)\nn=0\n\n# 現在のジョブを終える。必須の uses・権限・provider が、そのジョブ自身に揃っていなければ不合格にする。\n# 呼び出し: 次のジョブ ID の行・jobs を抜けるトップレベルキー・ファイル末尾。\n# 終えたら cur_job を空にし、以後の字下げ付き行がこのジョブへ誤って帰属しないようにする\nfinish_job() {\n [ -n \"${cur_job}\" ] || return 0\n if [ \"${job_uses}\" -ne 1 ]; then\n echo \"NG: ジョブ ${cur_job} に期待値と一致する uses がない\"\n violations=$((violations + 1))\n fi\n if [ \"${job_contents}\" -ne 1 ] || [ \"${job_prw}\" -ne 1 ]; then\n echo \"NG: ジョブ ${cur_job} の permissions に contents: read と pull-requests: write が必要\"\n violations=$((violations + 1))\n fi\n if [ \"${job_provider}\" -ne 1 ]; then\n echo \"NG: ジョブ ${cur_job} の with に provider が必要\"\n violations=$((violations + 1))\n fi\n cur_job=\"\"\n}\n\n# 同一 mapping 内のキー重複を検出する。YAML では後勝ち・解釈系で差が出るため fail-closed で拒否する\nonce() {\n case \"${seen}\" in\n *\" $1 \"*) ng \"重複キー\"; return 1 ;;\n esac\n seen=\"${seen}$1 \"\n return 0\n}\n\nng() {\n # 違反項目名と行番号のみを出す(行の内容は出さない)\n echo \"NG: $1 (行 ${n})\"\n violations=$((violations + 1))\n}\n\n# 値の文法。いずれも bash 3.2 で動く(連想配列・mapfile・${x,,} は使わない)。\nRE_TOKEN='^[A-Za-z0-9_.][A-Za-z0-9_./-]*

setup-repo-guards 対象リポジトリへ組織標準の CI ガード一式を導入する。導入するのは次の 4 点で、 Step 1 → 4 の順に、各 Step を PR → CI 全 pass → レビュースレッド resolve → squash merge で確定させてから次へ進む。 1. ai-review(PR 自動レビュー workflow の wrapper) 2. AGENTS.md(AI レビューが読むレビュー観点集) 3. CI 必須チェックの集約ジョブ( 等) 4. branch protection ruleset・リポジトリ追加ガード(GitHub API のみ、コミット不要。適用前に退避・差分提示・明示承認を必須とする) この順序を守る理由: 「必須チェックに入れる集約ジョブが先に存在する」「AGENTS.md は PR の base コミット参照のため マージ後の次の PR から実効」という依存関係が、この順で自然に満たされる。 使い方 - 第 1 引数: 対象リポジトリ名( )。許容形式は のみ(Step 0 で検証する) - 第 2 引数: visibility( / の完全一致のみ。Step 0 で検証する)。runner 方針の分岐に使う(public は ubuntu ホステッド、private は self-hosted。ai-rev…

# 空白・コロン・記号を含まない 1 語\nRE_WORDS='^[A-Za-z0-9_.-]+( [A-Za-z0-9_.-]+)*

setup-repo-guards 対象リポジトリへ組織標準の CI ガード一式を導入する。導入するのは次の 4 点で、 Step 1 → 4 の順に、各 Step を PR → CI 全 pass → レビュースレッド resolve → squash merge で確定させてから次へ進む。 1. ai-review(PR 自動レビュー workflow の wrapper) 2. AGENTS.md(AI レビューが読むレビュー観点集) 3. CI 必須チェックの集約ジョブ( 等) 4. branch protection ruleset・リポジトリ追加ガード(GitHub API のみ、コミット不要。適用前に退避・差分提示・明示承認を必須とする) この順序を守る理由: 「必須チェックに入れる集約ジョブが先に存在する」「AGENTS.md は PR の base コミット参照のため マージ後の次の PR から実効」という依存関係が、この順で自然に満たされる。 使い方 - 第 1 引数: 対象リポジトリ名( )。許容形式は のみ(Step 0 で検証する) - 第 2 引数: visibility( / の完全一致のみ。Step 0 で検証する)。runner 方針の分岐に使う(public は ubuntu ホステッド、private は self-hosted。ai-rev…

# 空白 1 個区切りの語\nRE_EXPR_VARS='^\\$\\{\\{ vars\\.[A-Za-z0-9_]+ \\}\\}

setup-repo-guards 対象リポジトリへ組織標準の CI ガード一式を導入する。導入するのは次の 4 点で、 Step 1 → 4 の順に、各 Step を PR → CI 全 pass → レビュースレッド resolve → squash merge で確定させてから次へ進む。 1. ai-review(PR 自動レビュー workflow の wrapper) 2. AGENTS.md(AI レビューが読むレビュー観点集) 3. CI 必須チェックの集約ジョブ( 等) 4. branch protection ruleset・リポジトリ追加ガード(GitHub API のみ、コミット不要。適用前に退避・差分提示・明示承認を必須とする) この順序を守る理由: 「必須チェックに入れる集約ジョブが先に存在する」「AGENTS.md は PR の base コミット参照のため マージ後の次の PR から実効」という依存関係が、この順で自然に満たされる。 使い方 - 第 1 引数: 対象リポジトリ名( )。許容形式は のみ(Step 0 で検証する) - 第 2 引数: visibility( / の完全一致のみ。Step 0 で検証する)。runner 方針の分岐に使う(public は ubuntu ホステッド、private は self-hosted。ai-rev…

# 組織・リポジトリ変数の参照だけを式として許可\nRE_URL='^https?://[A-Za-z0-9.-]+(:[0-9]+)?(/[A-Za-z0-9_./-]*)?

setup-repo-guards 対象リポジトリへ組織標準の CI ガード一式を導入する。導入するのは次の 4 点で、 Step 1 → 4 の順に、各 Step を PR → CI 全 pass → レビュースレッド resolve → squash merge で確定させてから次へ進む。 1. ai-review(PR 自動レビュー workflow の wrapper) 2. AGENTS.md(AI レビューが読むレビュー観点集) 3. CI 必須チェックの集約ジョブ( 等) 4. branch protection ruleset・リポジトリ追加ガード(GitHub API のみ、コミット不要。適用前に退避・差分提示・明示承認を必須とする) この順序を守る理由: 「必須チェックに入れる集約ジョブが先に存在する」「AGENTS.md は PR の base コミット参照のため マージ後の次の PR から実効」という依存関係が、この順で自然に満たされる。 使い方 - 第 1 引数: 対象リポジトリ名( )。許容形式は のみ(Step 0 で検証する) - 第 2 引数: visibility( / の完全一致のみ。Step 0 で検証する)。runner 方針の分岐に使う(public は ubuntu ホステッド、private は self-hosted。ai-rev…

\nRE_BOT='^[A-Za-z0-9_-]+(\\[bot\\])?

setup-repo-guards 対象リポジトリへ組織標準の CI ガード一式を導入する。導入するのは次の 4 点で、 Step 1 → 4 の順に、各 Step を PR → CI 全 pass → レビュースレッド resolve → squash merge で確定させてから次へ進む。 1. ai-review(PR 自動レビュー workflow の wrapper) 2. AGENTS.md(AI レビューが読むレビュー観点集) 3. CI 必須チェックの集約ジョブ( 等) 4. branch protection ruleset・リポジトリ追加ガード(GitHub API のみ、コミット不要。適用前に退避・差分提示・明示承認を必須とする) この順序を守る理由: 「必須チェックに入れる集約ジョブが先に存在する」「AGENTS.md は PR の base コミット参照のため マージ後の次の PR から実効」という依存関係が、この順で自然に満たされる。 使い方 - 第 1 引数: 対象リポジトリ名( )。許容形式は のみ(Step 0 で検証する) - 第 2 引数: visibility( / の完全一致のみ。Step 0 で検証する)。runner 方針の分岐に使う(public は ubuntu ホステッド、private は self-hosted。ai-rev…

\nRE_EVENT='^(opened|synchronize|reopened|ready_for_review)

setup-repo-guards 対象リポジトリへ組織標準の CI ガード一式を導入する。導入するのは次の 4 点で、 Step 1 → 4 の順に、各 Step を PR → CI 全 pass → レビュースレッド resolve → squash merge で確定させてから次へ進む。 1. ai-review(PR 自動レビュー workflow の wrapper) 2. AGENTS.md(AI レビューが読むレビュー観点集) 3. CI 必須チェックの集約ジョブ( 等) 4. branch protection ruleset・リポジトリ追加ガード(GitHub API のみ、コミット不要。適用前に退避・差分提示・明示承認を必須とする) この順序を守る理由: 「必須チェックに入れる集約ジョブが先に存在する」「AGENTS.md は PR の base コミット参照のため マージ後の次の PR から実効」という依存関係が、この順で自然に満たされる。 使い方 - 第 1 引数: 対象リポジトリ名( )。許容形式は のみ(Step 0 で検証する) - 第 2 引数: visibility( / の完全一致のみ。Step 0 で検証する)。runner 方針の分岐に使う(public は ubuntu ホステッド、private は self-hosted。ai-rev…

\nRE_BRANCH='^[A-Za-z0-9_.][A-Za-z0-9_./*-]*

setup-repo-guards 対象リポジトリへ組織標準の CI ガード一式を導入する。導入するのは次の 4 点で、 Step 1 → 4 の順に、各 Step を PR → CI 全 pass → レビュースレッド resolve → squash merge で確定させてから次へ進む。 1. ai-review(PR 自動レビュー workflow の wrapper) 2. AGENTS.md(AI レビューが読むレビュー観点集) 3. CI 必須チェックの集約ジョブ( 等) 4. branch protection ruleset・リポジトリ追加ガード(GitHub API のみ、コミット不要。適用前に退避・差分提示・明示承認を必須とする) この順序を守る理由: 「必須チェックに入れる集約ジョブが先に存在する」「AGENTS.md は PR の base コミット参照のため マージ後の次の PR から実効」という依存関係が、この順で自然に満たされる。 使い方 - 第 1 引数: 対象リポジトリ名( )。許容形式は のみ(Step 0 で検証する) - 第 2 引数: visibility( / の完全一致のみ。Step 0 で検証する)。runner 方針の分岐に使う(public は ubuntu ホステッド、private は self-hosted。ai-rev…

\nRE_PREFIX='^[A-Za-z0-9_.][A-Za-z0-9_./-]*

setup-repo-guards 対象リポジトリへ組織標準の CI ガード一式を導入する。導入するのは次の 4 点で、 Step 1 → 4 の順に、各 Step を PR → CI 全 pass → レビュースレッド resolve → squash merge で確定させてから次へ進む。 1. ai-review(PR 自動レビュー workflow の wrapper) 2. AGENTS.md(AI レビューが読むレビュー観点集) 3. CI 必須チェックの集約ジョブ( 等) 4. branch protection ruleset・リポジトリ追加ガード(GitHub API のみ、コミット不要。適用前に退避・差分提示・明示承認を必須とする) この順序を守る理由: 「必須チェックに入れる集約ジョブが先に存在する」「AGENTS.md は PR の base コミット参照のため マージ後の次の PR から実効」という依存関係が、この順で自然に満たされる。 使い方 - 第 1 引数: 対象リポジトリ名( )。許容形式は のみ(Step 0 で検証する) - 第 2 引数: visibility( / の完全一致のみ。Step 0 で検証する)。runner 方針の分岐に使う(public は ubuntu ホステッド、private は self-hosted。ai-rev…

\nRE_WITH_LINE='^([a-z][a-z-]*):[ ]+(.+)

setup-repo-guards 対象リポジトリへ組織標準の CI ガード一式を導入する。導入するのは次の 4 点で、 Step 1 → 4 の順に、各 Step を PR → CI 全 pass → レビュースレッド resolve → squash merge で確定させてから次へ進む。 1. ai-review(PR 自動レビュー workflow の wrapper) 2. AGENTS.md(AI レビューが読むレビュー観点集) 3. CI 必須チェックの集約ジョブ( 等) 4. branch protection ruleset・リポジトリ追加ガード(GitHub API のみ、コミット不要。適用前に退避・差分提示・明示承認を必須とする) この順序を守る理由: 「必須チェックに入れる集約ジョブが先に存在する」「AGENTS.md は PR の base コミット参照のため マージ後の次の PR から実効」という依存関係が、この順で自然に満たされる。 使い方 - 第 1 引数: 対象リポジトリ名( )。許容形式は のみ(Step 0 で検証する) - 第 2 引数: visibility( / の完全一致のみ。Step 0 で検証する)。runner 方針の分岐に使う(public は ubuntu ホステッド、private は self-hosted。ai-rev…

\nRE_NAME_LINE='^name:[ ]+(.+)

setup-repo-guards 対象リポジトリへ組織標準の CI ガード一式を導入する。導入するのは次の 4 点で、 Step 1 → 4 の順に、各 Step を PR → CI 全 pass → レビュースレッド resolve → squash merge で確定させてから次へ進む。 1. ai-review(PR 自動レビュー workflow の wrapper) 2. AGENTS.md(AI レビューが読むレビュー観点集) 3. CI 必須チェックの集約ジョブ( 等) 4. branch protection ruleset・リポジトリ追加ガード(GitHub API のみ、コミット不要。適用前に退避・差分提示・明示承認を必須とする) この順序を守る理由: 「必須チェックに入れる集約ジョブが先に存在する」「AGENTS.md は PR の base コミット参照のため マージ後の次の PR から実効」という依存関係が、この順で自然に満たされる。 使い方 - 第 1 引数: 対象リポジトリ名( )。許容形式は のみ(Step 0 で検証する) - 第 2 引数: visibility( / の完全一致のみ。Step 0 で検証する)。runner 方針の分岐に使う(public は ubuntu ホステッド、private は self-hosted。ai-rev…

\nRE_GROUP_LINE='^group:[ ]+(.+)

setup-repo-guards 対象リポジトリへ組織標準の CI ガード一式を導入する。導入するのは次の 4 点で、 Step 1 → 4 の順に、各 Step を PR → CI 全 pass → レビュースレッド resolve → squash merge で確定させてから次へ進む。 1. ai-review(PR 自動レビュー workflow の wrapper) 2. AGENTS.md(AI レビューが読むレビュー観点集) 3. CI 必須チェックの集約ジョブ( 等) 4. branch protection ruleset・リポジトリ追加ガード(GitHub API のみ、コミット不要。適用前に退避・差分提示・明示承認を必須とする) この順序を守る理由: 「必須チェックに入れる集約ジョブが先に存在する」「AGENTS.md は PR の base コミット参照のため マージ後の次の PR から実効」という依存関係が、この順で自然に満たされる。 使い方 - 第 1 引数: 対象リポジトリ名( )。許容形式は のみ(Step 0 で検証する) - 第 2 引数: visibility( / の完全一致のみ。Step 0 で検証する)。runner 方針の分岐に使う(public は ubuntu ホステッド、private は self-hosted。ai-rev…

\nRE_TYPES_LINE='^types:[ ]+(.+)

setup-repo-guards 対象リポジトリへ組織標準の CI ガード一式を導入する。導入するのは次の 4 点で、 Step 1 → 4 の順に、各 Step を PR → CI 全 pass → レビュースレッド resolve → squash merge で確定させてから次へ進む。 1. ai-review(PR 自動レビュー workflow の wrapper) 2. AGENTS.md(AI レビューが読むレビュー観点集) 3. CI 必須チェックの集約ジョブ( 等) 4. branch protection ruleset・リポジトリ追加ガード(GitHub API のみ、コミット不要。適用前に退避・差分提示・明示承認を必須とする) この順序を守る理由: 「必須チェックに入れる集約ジョブが先に存在する」「AGENTS.md は PR の base コミット参照のため マージ後の次の PR から実効」という依存関係が、この順で自然に満たされる。 使い方 - 第 1 引数: 対象リポジトリ名( )。許容形式は のみ(Step 0 で検証する) - 第 2 引数: visibility( / の完全一致のみ。Step 0 で検証する)。runner 方針の分岐に使う(public は ubuntu ホステッド、private は self-hosted。ai-rev…

\nRE_BRANCHES_LINE='^branches:[ ]+(.+)

setup-repo-guards 対象リポジトリへ組織標準の CI ガード一式を導入する。導入するのは次の 4 点で、 Step 1 → 4 の順に、各 Step を PR → CI 全 pass → レビュースレッド resolve → squash merge で確定させてから次へ進む。 1. ai-review(PR 自動レビュー workflow の wrapper) 2. AGENTS.md(AI レビューが読むレビュー観点集) 3. CI 必須チェックの集約ジョブ( 等) 4. branch protection ruleset・リポジトリ追加ガード(GitHub API のみ、コミット不要。適用前に退避・差分提示・明示承認を必須とする) この順序を守る理由: 「必須チェックに入れる集約ジョブが先に存在する」「AGENTS.md は PR の base コミット参照のため マージ後の次の PR から実効」という依存関係が、この順で自然に満たされる。 使い方 - 第 1 引数: 対象リポジトリ名( )。許容形式は のみ(Step 0 で検証する) - 第 2 引数: visibility( / の完全一致のみ。Step 0 で検証する)。runner 方針の分岐に使う(public は ubuntu ホステッド、private は self-hosted。ai-rev…

\n\n# YAML が文字列以外に解決する plain scalar の形(YAML 1.1 と 1.2 の和集合。どちらかの方言で\n# 文字列にならなければ拒否する)。GitHub Actions のパーサがどちらの方言に従うかに判定を依存させない\nRE_TY_INT='^[-+]?[0-9][0-9_]*

setup-repo-guards 対象リポジトリへ組織標準の CI ガード一式を導入する。導入するのは次の 4 点で、 Step 1 → 4 の順に、各 Step を PR → CI 全 pass → レビュースレッド resolve → squash merge で確定させてから次へ進む。 1. ai-review(PR 自動レビュー workflow の wrapper) 2. AGENTS.md(AI レビューが読むレビュー観点集) 3. CI 必須チェックの集約ジョブ( 等) 4. branch protection ruleset・リポジトリ追加ガード(GitHub API のみ、コミット不要。適用前に退避・差分提示・明示承認を必須とする) この順序を守る理由: 「必須チェックに入れる集約ジョブが先に存在する」「AGENTS.md は PR の base コミット参照のため マージ後の次の PR から実効」という依存関係が、この順で自然に満たされる。 使い方 - 第 1 引数: 対象リポジトリ名( )。許容形式は のみ(Step 0 で検証する) - 第 2 引数: visibility( / の完全一致のみ。Step 0 で検証する)。runner 方針の分岐に使う(public は ubuntu ホステッド、private は self-hosted。ai-rev…

# 10 進・先頭 0 の 8 進(1.1)・_ 区切り\nRE_TY_HEX='^[-+]?0[xX][0-9a-fA-F_]+

setup-repo-guards 対象リポジトリへ組織標準の CI ガード一式を導入する。導入するのは次の 4 点で、 Step 1 → 4 の順に、各 Step を PR → CI 全 pass → レビュースレッド resolve → squash merge で確定させてから次へ進む。 1. ai-review(PR 自動レビュー workflow の wrapper) 2. AGENTS.md(AI レビューが読むレビュー観点集) 3. CI 必須チェックの集約ジョブ( 等) 4. branch protection ruleset・リポジトリ追加ガード(GitHub API のみ、コミット不要。適用前に退避・差分提示・明示承認を必須とする) この順序を守る理由: 「必須チェックに入れる集約ジョブが先に存在する」「AGENTS.md は PR の base コミット参照のため マージ後の次の PR から実効」という依存関係が、この順で自然に満たされる。 使い方 - 第 1 引数: 対象リポジトリ名( )。許容形式は のみ(Step 0 で検証する) - 第 2 引数: visibility( / の完全一致のみ。Step 0 で検証する)。runner 方針の分岐に使う(public は ubuntu ホステッド、private は self-hosted。ai-rev…

\nRE_TY_OCT='^0o[0-7_]+

setup-repo-guards 対象リポジトリへ組織標準の CI ガード一式を導入する。導入するのは次の 4 点で、 Step 1 → 4 の順に、各 Step を PR → CI 全 pass → レビュースレッド resolve → squash merge で確定させてから次へ進む。 1. ai-review(PR 自動レビュー workflow の wrapper) 2. AGENTS.md(AI レビューが読むレビュー観点集) 3. CI 必須チェックの集約ジョブ( 等) 4. branch protection ruleset・リポジトリ追加ガード(GitHub API のみ、コミット不要。適用前に退避・差分提示・明示承認を必須とする) この順序を守る理由: 「必須チェックに入れる集約ジョブが先に存在する」「AGENTS.md は PR の base コミット参照のため マージ後の次の PR から実効」という依存関係が、この順で自然に満たされる。 使い方 - 第 1 引数: 対象リポジトリ名( )。許容形式は のみ(Step 0 で検証する) - 第 2 引数: visibility( / の完全一致のみ。Step 0 で検証する)。runner 方針の分岐に使う(public は ubuntu ホステッド、private は self-hosted。ai-rev…

# 1.2 の 8 進\nRE_TY_BIN='^[-+]?0[bB][01_]+

setup-repo-guards 対象リポジトリへ組織標準の CI ガード一式を導入する。導入するのは次の 4 点で、 Step 1 → 4 の順に、各 Step を PR → CI 全 pass → レビュースレッド resolve → squash merge で確定させてから次へ進む。 1. ai-review(PR 自動レビュー workflow の wrapper) 2. AGENTS.md(AI レビューが読むレビュー観点集) 3. CI 必須チェックの集約ジョブ( 等) 4. branch protection ruleset・リポジトリ追加ガード(GitHub API のみ、コミット不要。適用前に退避・差分提示・明示承認を必須とする) この順序を守る理由: 「必須チェックに入れる集約ジョブが先に存在する」「AGENTS.md は PR の base コミット参照のため マージ後の次の PR から実効」という依存関係が、この順で自然に満たされる。 使い方 - 第 1 引数: 対象リポジトリ名( )。許容形式は のみ(Step 0 で検証する) - 第 2 引数: visibility( / の完全一致のみ。Step 0 で検証する)。runner 方針の分岐に使う(public は ubuntu ホステッド、private は self-hosted。ai-rev…

# 1.1 の 2 進\nRE_TY_FLOAT='^[-+]?([0-9][0-9_]*)?\\.[0-9_]*([eE][-+]?[0-9]+)?

setup-repo-guards 対象リポジトリへ組織標準の CI ガード一式を導入する。導入するのは次の 4 点で、 Step 1 → 4 の順に、各 Step を PR → CI 全 pass → レビュースレッド resolve → squash merge で確定させてから次へ進む。 1. ai-review(PR 自動レビュー workflow の wrapper) 2. AGENTS.md(AI レビューが読むレビュー観点集) 3. CI 必須チェックの集約ジョブ( 等) 4. branch protection ruleset・リポジトリ追加ガード(GitHub API のみ、コミット不要。適用前に退避・差分提示・明示承認を必須とする) この順序を守る理由: 「必須チェックに入れる集約ジョブが先に存在する」「AGENTS.md は PR の base コミット参照のため マージ後の次の PR から実効」という依存関係が、この順で自然に満たされる。 使い方 - 第 1 引数: 対象リポジトリ名( )。許容形式は のみ(Step 0 で検証する) - 第 2 引数: visibility( / の完全一致のみ。Step 0 で検証する)。runner 方針の分岐に使う(public は ubuntu ホステッド、private は self-hosted。ai-rev…

# 1.5・.5・1.・1.5e3\nRE_TY_EXP='^[-+]?[0-9][0-9_]*[eE][-+]?[0-9]+

setup-repo-guards 対象リポジトリへ組織標準の CI ガード一式を導入する。導入するのは次の 4 点で、 Step 1 → 4 の順に、各 Step を PR → CI 全 pass → レビュースレッド resolve → squash merge で確定させてから次へ進む。 1. ai-review(PR 自動レビュー workflow の wrapper) 2. AGENTS.md(AI レビューが読むレビュー観点集) 3. CI 必須チェックの集約ジョブ( 等) 4. branch protection ruleset・リポジトリ追加ガード(GitHub API のみ、コミット不要。適用前に退避・差分提示・明示承認を必須とする) この順序を守る理由: 「必須チェックに入れる集約ジョブが先に存在する」「AGENTS.md は PR の base コミット参照のため マージ後の次の PR から実効」という依存関係が、この順で自然に満たされる。 使い方 - 第 1 引数: 対象リポジトリ名( )。許容形式は のみ(Step 0 で検証する) - 第 2 引数: visibility( / の完全一致のみ。Step 0 で検証する)。runner 方針の分岐に使う(public は ubuntu ホステッド、private は self-hosted。ai-rev…

# 1.2 の 1e3(小数点なし)\nRE_TY_INF='^[-+]?\\.(inf|Inf|INF)

setup-repo-guards 対象リポジトリへ組織標準の CI ガード一式を導入する。導入するのは次の 4 点で、 Step 1 → 4 の順に、各 Step を PR → CI 全 pass → レビュースレッド resolve → squash merge で確定させてから次へ進む。 1. ai-review(PR 自動レビュー workflow の wrapper) 2. AGENTS.md(AI レビューが読むレビュー観点集) 3. CI 必須チェックの集約ジョブ( 等) 4. branch protection ruleset・リポジトリ追加ガード(GitHub API のみ、コミット不要。適用前に退避・差分提示・明示承認を必須とする) この順序を守る理由: 「必須チェックに入れる集約ジョブが先に存在する」「AGENTS.md は PR の base コミット参照のため マージ後の次の PR から実効」という依存関係が、この順で自然に満たされる。 使い方 - 第 1 引数: 対象リポジトリ名( )。許容形式は のみ(Step 0 で検証する) - 第 2 引数: visibility( / の完全一致のみ。Step 0 で検証する)。runner 方針の分岐に使う(public は ubuntu ホステッド、private は self-hosted。ai-rev…

\nRE_TY_NAN='^\\.(nan|NaN|NAN)

setup-repo-guards 対象リポジトリへ組織標準の CI ガード一式を導入する。導入するのは次の 4 点で、 Step 1 → 4 の順に、各 Step を PR → CI 全 pass → レビュースレッド resolve → squash merge で確定させてから次へ進む。 1. ai-review(PR 自動レビュー workflow の wrapper) 2. AGENTS.md(AI レビューが読むレビュー観点集) 3. CI 必須チェックの集約ジョブ( 等) 4. branch protection ruleset・リポジトリ追加ガード(GitHub API のみ、コミット不要。適用前に退避・差分提示・明示承認を必須とする) この順序を守る理由: 「必須チェックに入れる集約ジョブが先に存在する」「AGENTS.md は PR の base コミット参照のため マージ後の次の PR から実効」という依存関係が、この順で自然に満たされる。 使い方 - 第 1 引数: 対象リポジトリ名( )。許容形式は のみ(Step 0 で検証する) - 第 2 引数: visibility( / の完全一致のみ。Step 0 で検証する)。runner 方針の分岐に使う(public は ubuntu ホステッド、private は self-hosted。ai-rev…

\n# 日付。時刻つきの形はコロンを含み、どの文法もコロンを拒否するためここでは日付のみを見る\nRE_TY_DATE='^[0-9]{4}-[0-9]{1,2}-[0-9]{1,2}

setup-repo-guards 対象リポジトリへ組織標準の CI ガード一式を導入する。導入するのは次の 4 点で、 Step 1 → 4 の順に、各 Step を PR → CI 全 pass → レビュースレッド resolve → squash merge で確定させてから次へ進む。 1. ai-review(PR 自動レビュー workflow の wrapper) 2. AGENTS.md(AI レビューが読むレビュー観点集) 3. CI 必須チェックの集約ジョブ( 等) 4. branch protection ruleset・リポジトリ追加ガード(GitHub API のみ、コミット不要。適用前に退避・差分提示・明示承認を必須とする) この順序を守る理由: 「必須チェックに入れる集約ジョブが先に存在する」「AGENTS.md は PR の base コミット参照のため マージ後の次の PR から実効」という依存関係が、この順で自然に満たされる。 使い方 - 第 1 引数: 対象リポジトリ名( )。許容形式は のみ(Step 0 で検証する) - 第 2 引数: visibility( / の完全一致のみ。Step 0 で検証する)。runner 方針の分岐に使う(public は ubuntu ホステッド、private は self-hosted。ai-rev…

\n\n# 引用符なしで書かれたとき、YAML が文字列以外(null・真偽値・数値・日付など)に解決する値なら return 0。\n# 拒否する形:\n# null : null Null NULL ~\n# 真偽値 : true/false・yes/no・on/off・y/n(大文字始まり・全大文字を含む)\n# 整数 : 10 進(+-・_ 区切り・先頭 0 の 8 進)・0x… ・0o… ・0b…\n# 浮動小数 : 1.5 .5 1. 1.5e3 1e3 ±.inf .nan\n# 日付 : YYYY-M-D\n# その他 : = と \u003c\u003c(YAML 1.1 の value / merge)\n# 引用符で囲まれた値は文字列なので、呼び出し側は unq_quoted=0 のときだけ呼ぶ\nis_typed_plain_scalar() {\n case \"$1\" in\n null|Null|NULL|'~') return 0 ;;\n true|True|TRUE|false|False|FALSE) return 0 ;;\n yes|Yes|YES|no|No|NO|on|On|ON|off|Off|OFF) return 0 ;;\n y|Y|n|N|'='|'\u003c\u003c') return 0 ;;\n esac\n [[ \"$1\" =~ ${RE_TY_INT} ]] && return 0\n [[ \"$1\" =~ ${RE_TY_HEX} ]] && return 0\n [[ \"$1\" =~ ${RE_TY_OCT} ]] && return 0\n [[ \"$1\" =~ ${RE_TY_BIN} ]] && return 0\n [[ \"$1\" =~ ${RE_TY_FLOAT} ]] && return 0\n [[ \"$1\" =~ ${RE_TY_EXP} ]] && return 0\n [[ \"$1\" =~ ${RE_TY_INF} ]] && return 0\n [[ \"$1\" =~ ${RE_TY_NAN} ]] && return 0\n [[ \"$1\" =~ ${RE_TY_DATE} ]] && return 0\n return 1\n}\n\n# 前後の空白(半角スペースのみ。タブは先に拒否済み)を除く。結果は trimmed に入れる\ntrim() {\n trimmed=\"$1\"\n trimmed=\"${trimmed#\"${trimmed%%[! ]*}\"}\"\n trimmed=\"${trimmed%\"${trimmed##*[! ]}\"}\"\n}\n\n# 全体を 1 組の同じ引用符で囲んだ値は内側を unq に入れる。引用符なしはそのまま。\n# 引用符の有無は unq_quoted(0 / 1)に残す(引用符つきは文字列なので is_typed_plain_scalar の対象外)。\n# 開き引用符だけ・開閉が食い違う値は return 1(内側の引用符は各キーの文法が拒否する)\nunquote() {\n unq=\"$1\"\n unq_quoted=0\n case \"$1\" in\n \\\"*) case \"$1\" in \\\"?*\\\") unq=\"${1#\\\"}\"; unq=\"${unq%\\\"}\"; unq_quoted=1 ;; *) return 1 ;; esac ;;\n \\'*) case \"$1\" in \\'?*\\') unq=\"${1#\\'}\"; unq=\"${unq%\\'}\"; unq_quoted=1 ;; *) return 1 ;; esac ;;\n esac\n return 0\n}\n\n# 区切りが , の要素列を分割し、各要素の前後の空白だけを除いて ERE ($2) へ完全一致させる。\n# 空要素・不一致・重複は return 1。$3 が 1 の要素列(フロー配列。要素ごとに独立した YAML scalar)では、\n# 引用符なしで文字列以外に解決される要素を return 3 にする。通った要素は list_els に \" a b c \" の形で残す\nparse_list() {\n local rest=\"$1,\" el\n list_els=\" \"\n while [ -n \"${rest}\" ]; do\n el=\"${rest%%,*}\"\n rest=\"${rest#*,}\"\n trim \"${el}\"\n el=\"${trimmed}\"\n [[ \"${el}\" =~ $2 ]] || return 1\n if [ \"${3:-0}\" = \"1\" ] && is_typed_plain_scalar \"${el}\"; then return 3; fi\n case \"${list_els}\" in *\" ${el} \"*) return 1 ;; esac\n list_els=\"${list_els}${el} \"\n done\n return 0\n}\n\n# on.pull_request 配下の [a, b] 形式。角括弧で囲まれた 1 行のフロー配列だけを受け、要素は parse_list で検査する。\n# $3 が 1 のときは要素の型付き scalar を return 3 で拒否する(parse_list 参照)。成功時は list_els に要素を残す\nparse_flow_list() {\n local v=\"$1\"\n [[ \"${v}\" =~ ^\\[(.*)\\]$ ]] || return 1\n parse_list \"${BASH_REMATCH[1]}\" \"$2\" \"${3:-0}\"\n}\n\n# concurrency.group: 語と、許可した github 式の並び。許可外の式・閉じていない式は残るので文字集合で落ちる\ncheck_group() {\n local g=\"$1\" e\n # 置換パターンは変数経由で渡す(単引用符の解釈が bash のバージョンで異なるため)\n for e in '${{ github.ref }}' '${{ github.head_ref }}' '${{ github.workflow }}' '${{ github.event.pull_request.number }}'; do\n g=\"${g//\"${e}\"/X}\"\n done\n [[ \"${g}\" =~ ^[A-Za-z0-9_.-]+$ ]]\n}\n\n# 引用符なしの列挙(a,b)は空白を含められない。空白で区切りたい場合は全体を引用符で囲む\n# (plain scalar の中の「, 」を要素の区切りと読むかは受け側で異なるため、曖昧な形は拒否する)\nunquoted_list_ok() {\n case \"$1\" in\n \\\"*|\\'*) return 0 ;;\n esac\n case \"$1\" in\n *\" \"*) return 1 ;;\n esac\n return 0\n}\n\n# with の値を、キーごとの文法へ完全一致させる。通れば return 0。収集は呼び出し側が wunq から行う\ncheck_with_value() {\n local key=\"$1\" val=\"$2\"\n trim \"${val}\"\n val=\"${trimmed}\"\n unquote \"${val}\" || return 1\n wunq=\"${unq}\"\n # 文字列でなければならないキーは、引用符なしで文字列以外に解決される値を拒否する(return 3)。\n # max-diff-bytes(数値)・collaborators-only(真偽値)は意図した型なので対象外。provider は固定語\n case \"${key}\" in\n runner|model|api-base-url|reasoning-effort|post-feedback-runner|reviewer-id|reviewer-name|prompt-path|skip-branch-prefixes|allowed-bot-actors|block-priorities)\n if [ \"${unq_quoted}\" -eq 0 ] && is_typed_plain_scalar \"${unq}\"; then return 3; fi ;;\n esac\n case \"${key}\" in\n provider) [[ \"${unq}\" =~ ^(codex|claude|gemini|grok|openai-compatible)$ ]] ;;\n runner|model) [[ \"${unq}\" =~ $RE_TOKEN ]] || [[ \"${unq}\" =~ $RE_EXPR_VARS ]] ;;\n api-base-url) [[ \"${unq}\" =~ $RE_URL ]] || [[ \"${unq}\" =~ $RE_EXPR_VARS ]] ;;\n reasoning-effort|post-feedback-runner|reviewer-id)\n [[ \"${unq}\" =~ $RE_TOKEN ]] ;;\n reviewer-name) [[ \"${unq}\" =~ $RE_WORDS ]] ;;\n max-diff-bytes) [[ \"${unq}\" =~ ^[0-9]+$ ]] ;;\n prompt-path) [[ \"${unq}\" =~ $RE_TOKEN ]] && [[ \"${unq}\" != *..* ]] ;;\n collaborators-only) [ \"${unq}\" = \"true\" ] ;;\n skip-branch-prefixes) unquoted_list_ok \"${val}\" && parse_list \"${unq}\" \"${RE_PREFIX}\" ;;\n allowed-bot-actors) unquoted_list_ok \"${val}\" && parse_list \"${unq}\" \"${RE_BOT}\" ;;\n block-priorities) return 0 ;; # 要素の検査は呼び出し側(P0 と P1 の必須判定が要る)\n *) return 2 ;; # 許可リストにないキー\n esac\n}\n\nwhile IFS= read -r line || [ -n \"${line}\" ]; do\n n=$((n + 1))\n # CR・タブは構造の誤読につながるため拒否する\n case \"${line}\" in\n *

setup-repo-guards 対象リポジトリへ組織標準の CI ガード一式を導入する。導入するのは次の 4 点で、 Step 1 → 4 の順に、各 Step を PR → CI 全 pass → レビュースレッド resolve → squash merge で確定させてから次へ進む。 1. ai-review(PR 自動レビュー workflow の wrapper) 2. AGENTS.md(AI レビューが読むレビュー観点集) 3. CI 必須チェックの集約ジョブ( 等) 4. branch protection ruleset・リポジトリ追加ガード(GitHub API のみ、コミット不要。適用前に退避・差分提示・明示承認を必須とする) この順序を守る理由: 「必須チェックに入れる集約ジョブが先に存在する」「AGENTS.md は PR の base コミット参照のため マージ後の次の PR から実効」という依存関係が、この順で自然に満たされる。 使い方 - 第 1 引数: 対象リポジトリ名( )。許容形式は のみ(Step 0 で検証する) - 第 2 引数: visibility( / の完全一致のみ。Step 0 で検証する)。runner 方針の分岐に使う(public は ubuntu ホステッド、private は self-hosted。ai-rev…

\\r'*) ng \"改行コード CR を含む\"; continue ;;\n *

setup-repo-guards 対象リポジトリへ組織標準の CI ガード一式を導入する。導入するのは次の 4 点で、 Step 1 → 4 の順に、各 Step を PR → CI 全 pass → レビュースレッド resolve → squash merge で確定させてから次へ進む。 1. ai-review(PR 自動レビュー workflow の wrapper) 2. AGENTS.md(AI レビューが読むレビュー観点集) 3. CI 必須チェックの集約ジョブ( 等) 4. branch protection ruleset・リポジトリ追加ガード(GitHub API のみ、コミット不要。適用前に退避・差分提示・明示承認を必須とする) この順序を守る理由: 「必須チェックに入れる集約ジョブが先に存在する」「AGENTS.md は PR の base コミット参照のため マージ後の次の PR から実効」という依存関係が、この順で自然に満たされる。 使い方 - 第 1 引数: 対象リポジトリ名( )。許容形式は のみ(Step 0 で検証する) - 第 2 引数: visibility( / の完全一致のみ。Step 0 で検証する)。runner 方針の分岐に使う(public は ubuntu ホステッド、private は self-hosted。ai-rev…

\\t'*) ng \"タブ文字を含む\"; continue ;;\n esac\n # コメント行・空行は検査対象外\n if [[ \"${line}\" =~ ^[[:space:]]*(#.*)?$ ]]; then\n continue\n fi\n body=\"${line#\"${line%%[![:space:]]*}\"}\"\n indent=$(( ${#line} - ${#body} ))\n\n # permissions ブロックの内側(親より深いインデント)は権限キーだけを許可する\n if [ -n \"${perm_scope}\" ] && [ \"${indent}\" -gt \"${perm_indent}\" ]; then\n # 子キーは親のちょうど +2 のインデントに限る(不正な入れ子・過剰インデントを拒否)\n if [ \"${indent}\" -ne $((perm_indent + 2)) ]; then\n ng \"permissions の子キーのインデントが不正\"\n continue\n fi\n once \"perm:${perm_scope}:${cur_job}/${body%%:*}\" || continue # top のとき cur_job は空\n if [[ \"${body}\" =~ ^contents:[[:space:]]+read[[:space:]]*$ ]]; then\n if [ \"${perm_scope}\" = \"top\" ]; then top_perm_ok=1; else job_contents=1; fi\n continue\n fi\n if [ \"${perm_scope}\" = \"job\" ] && [[ \"${body}\" =~ ^pull-requests:[[:space:]]+write[[:space:]]*$ ]]; then\n job_prw=1\n continue\n fi\n ng \"許可外の権限\"\n continue\n fi\n perm_scope=\"\"\n\n # インデント 0 の行はトップレベルの新しいセクションの開始。jobs を抜けるときは現在のジョブを終える。\n # 許可リストにないキーでもセクションを切り替える(直前のセクションの子として誤読しない)\n if [ \"${indent}\" -eq 0 ]; then\n finish_job\n section=\"other\"\n job_sub=\"\"\n on_sub=\"\"\n if [[ \"${body}\" =~ ^(on|concurrency|jobs|permissions|name):([[:space:]]|$) ]]; then\n once \"top/${body%%:*}\" || continue\n section=\"${body%%:*}\"\n if [ \"${section}\" = \"on\" ]; then on_seen=1; fi\n if [ \"${section}\" = \"jobs\" ]; then jobs_seen=1; fi\n if [ \"${section}\" = \"concurrency\" ]; then conc_seen=1; fi\n fi\n fi\n if [[ \"${body}\" =~ ^permissions:[[:space:]]*$ ]]; then\n perm_indent=\"${indent}\"\n if [ \"${indent}\" -eq 0 ]; then\n perm_scope=\"top\"\n elif [ \"${section}\" = \"jobs\" ] && [ \"${indent}\" -eq 4 ] && [ -n \"${cur_job}\" ]; then\n once \"job:${cur_job}/permissions\" || continue\n perm_scope=\"job\"\n job_sub=\"permissions\"\n else\n ng \"ジョブ ID の後ろ・各ジョブ直下以外での permissions\"\n fi\n continue\n fi\n if [[ \"${body}\" =~ ^permissions: ]]; then\n ng \"permissions のインライン指定(write-all 等)\"\n continue\n fi\n\n # 名前付きの違反(原因が分かるように先に判定する)\n if [[ \"${body}\" =~ ^(pull_request_target|workflow_run|issue_comment|issues|push|workflow_dispatch|schedule|repository_dispatch):? ]]; then\n ng \"特権または許可外のトリガー\"; continue\n fi\n if [[ \"${body}\" =~ ^(run|steps|container|services|env|runs-on):?([[:space:]]|$) ]]; then\n ng \"run/steps/container/services/env/runs-on は thin wrapper に不要\"; continue\n fi\n if [[ \"${body}\" =~ ^secrets:[[:space:]]*inherit ]]; then\n ng \"secrets: inherit\"; continue\n fi\n\n # 親キーごとの allow-list(親を追跡し、on 配下は pull_request のみ・任意名は jobs 直下のみ)\n if [ \"${indent}\" -eq 0 ]; then\n if [[ \"${body}\" =~ ${RE_NAME_LINE} ]]; then\n trim \"${BASH_REMATCH[1]}\"\n if ! { unquote \"${trimmed}\" && [[ \"${unq}\" =~ ${RE_WORDS} ]]; }; then\n ng \"name の値が許可する文法に一致しない\"\n elif [ \"${unq_quoted}\" -eq 0 ] && is_typed_plain_scalar \"${unq}\"; then\n ng \"name の値が YAML で文字列以外に解決される(null・真偽値・数値・日付)\"\n fi\n continue\n fi\n if [[ \"${body}\" =~ ^(on|concurrency|jobs):[[:space:]]*$ ]]; then continue; fi\n ng \"許可リストにないトップレベルキー\"; continue\n fi\n case \"${section}\" in\n on)\n if [ \"${indent}\" -eq 2 ]; then\n on_sub=\"\"\n if [[ \"${body}\" =~ ^pull_request:[[:space:]]*$ ]]; then once \"on/pull_request\" || true; pr_seen=1; on_sub=\"pull_request\"; continue; fi\n ng \"on 配下は pull_request(types・branches)のみ許可\"; continue\n fi\n if [ \"${indent}\" -eq 4 ] && [ \"${on_sub}\" = \"pull_request\" ]; then\n if [[ \"${body}\" =~ ${RE_TYPES_LINE} ]]; then\n once \"on/types\" || true\n trim \"${BASH_REMATCH[1]}\"\n # 要素ごとに許可語へ完全一致させ、PR の再レビューに必要な 3 イベントが要素として揃うことを確かめる\n if ! parse_flow_list \"${trimmed}\" \"${RE_EVENT}\"; then\n ng \"types は [a, b] 形式で、各要素が opened・synchronize・reopened・ready_for_review のいずれかであること(空要素・重複・内部の空白は不可)\"\n else\n for ev in opened synchronize reopened; do\n case \"${list_els}\" in\n *\" ${ev} \"*) ;;\n *) ng \"types に ${ev} が必要\"; ;;\n esac\n done\n fi\n continue\n fi\n if [[ \"${body}\" =~ ${RE_BRANCHES_LINE} ]]; then\n once \"on/branches\" || true\n trim \"${BASH_REMATCH[1]}\"\n brc=0\n parse_flow_list \"${trimmed}\" \"${RE_BRANCH}\" 1 || brc=$?\n if [ \"${brc}\" -eq 3 ]; then\n ng \"branches の要素が YAML で文字列以外に解決される(null・真偽値・数値・日付)\"\n elif [ \"${brc}\" -ne 0 ]; then\n ng \"branches は [a, b] 形式で、各要素が 1 語のブランチ名であること(空要素・重複・内部の空白は不可)\"\n fi\n continue\n fi\n fi\n ng \"on 配下は pull_request(types・branches)のみ許可\"; continue ;;\n concurrency)\n if [ \"${indent}\" -eq 2 ] && [[ \"${body}\" =~ ${RE_GROUP_LINE} ]]; then\n trim \"${BASH_REMATCH[1]}\"\n group_seen=1\n if ! { unquote \"${trimmed}\" && check_group \"${unq}\"; }; then\n ng \"concurrency.group の値が許可する文法に一致しない\"\n elif [ \"${unq_quoted}\" -eq 0 ] && is_typed_plain_scalar \"${unq}\"; then\n ng \"concurrency.group の値が YAML で文字列以外に解決される(null・真偽値・数値・日付)\"\n else\n once \"concurrency/group\" || true\n fi\n continue\n fi\n if [ \"${indent}\" -eq 2 ] && [[ \"${body}\" =~ ^cancel-in-progress:[[:space:]]+(true|false)[[:space:]]*$ ]]; then once \"concurrency/cancel\" || true; continue; fi\n ng \"concurrency 配下の許可外の行\"; continue ;;\n jobs) ;;\n *) ng \"許可外のセクション配下の行(トップレベルの親キーの内側でのみ設定を受け付ける)\"; continue ;;\n esac\n\n # ここから jobs の内側。インデント 2 = ジョブ ID、4 以上 = ジョブの設定。\n # ジョブの設定はジョブ ID の行より後ろ(cur_job が設定済み)でだけ受理する。\n # ジョブ ID より前の設定行・jobs: より前のジョブキーを、後続のジョブ ID で後から成立させない\n if [ \"${indent}\" -ne 2 ] && [ -z \"${cur_job}\" ]; then\n ng \"ジョブ ID より前、またはジョブの外にある設定行\"; continue\n fi\n\n # jobs 配下: インデント 2 = ジョブ ID、4 = ジョブのキー、6 = with / secrets の子\n if [ \"${indent}\" -eq 2 ]; then\n job_sub=\"\"\n finish_job\n if [[ \"${body}\" =~ ^[A-Za-z_][A-Za-z0-9_-]*:[[:space:]]*$ ]]; then\n if is_typed_plain_scalar \"${body%%:*}\"; then\n # キーが null・真偽値などに解決されるジョブ ID(null:・true:・yes:・on: 等)。後続の行が誤帰属しないよう cur_job は空のまま\n ng \"ジョブ ID が YAML で文字列以外に解決される(null・真偽値)\"; continue\n fi\n cur_job=\"${body%%:*}\"\n job_uses=0; job_contents=0; job_prw=0; job_provider=0\n once \"jobs/${cur_job}\" || true\n job_count=$((job_count + 1))\n continue\n fi\n ng \"jobs 直下はジョブ ID のみ許可\"; continue\n fi\n if [ \"${indent}\" -eq 4 ]; then\n job_sub=\"other\"\n once \"job:${cur_job}/${body%%:*}\" || continue\n if [[ \"${body}\" =~ ^uses: ]]; then\n if [ \"${body}\" != \"uses: ${EXPECTED_USES}\" ]; then\n ng \"uses が期待値と一致しない\"\n else\n job_uses=1\n fi\n continue\n fi\n if [[ \"${body}\" =~ ${RE_NAME_LINE} ]]; then\n trim \"${BASH_REMATCH[1]}\"\n if unquote \"${trimmed}\" && [[ \"${unq}\" =~ ${RE_WORDS} ]]; then\n if [ \"${unq_quoted}\" -eq 0 ] && is_typed_plain_scalar \"${unq}\"; then\n ng \"ジョブの name の値が YAML で文字列以外に解決される(null・真偽値・数値・日付)\"\n fi\n continue\n fi\n ng \"ジョブの name の値が許可する文法に一致しない\"; continue\n fi\n if [[ \"${body}\" =~ ^(with|secrets):[[:space:]]*$ ]]; then job_sub=\"${body%%:*}\"; continue; fi\n ng \"ジョブ直下の許可リストにないキー(uses は直下に必要)\"; continue\n fi\n if [ \"${indent}\" -eq 6 ]; then\n if [ \"${job_sub}\" = \"with\" ] && [[ \"${body}\" =~ ${RE_WITH_LINE} ]]; then\n wkey=\"${BASH_REMATCH[1]}\"\n wraw=\"${BASH_REMATCH[2]}\"\n once \"job:${cur_job}/with/${wkey}\" || continue\n # 値はキーごとの文法へ完全一致させる。許可外のキーもここで不合格にする\n wrc=0\n check_with_value \"${wkey}\" \"${wraw}\" || wrc=$?\n if [ \"${wrc}\" -eq 2 ]; then\n ng \"with に許可リストにないキー\"\n continue\n elif [ \"${wrc}\" -eq 3 ]; then\n ng \"with の値が YAML で文字列以外に解決される(null・真偽値・数値・日付。文字列にするなら引用符で囲む)\"\n continue\n elif [ \"${wrc}\" -ne 0 ]; then\n ng \"with の値が、そのキーに許可する文法に一致しない\"\n continue\n fi\n case \"${wkey}\" in\n provider) job_provider=1 ;;\n block-priorities)\n # 外側の引用符と各要素の前後の空白だけを正規化し、要素ごとに P0〜P3 へ完全一致させる。\n # 内部の空白・引用符・空要素・重複('P0,P 1' 等)は配置後のファイルに残り P1 の指定にならないため拒否する\n if ! parse_list \"${wunq}\" '^P[0-3]

setup-repo-guards 対象リポジトリへ組織標準の CI ガード一式を導入する。導入するのは次の 4 点で、 Step 1 → 4 の順に、各 Step を PR → CI 全 pass → レビュースレッド resolve → squash merge で確定させてから次へ進む。 1. ai-review(PR 自動レビュー workflow の wrapper) 2. AGENTS.md(AI レビューが読むレビュー観点集) 3. CI 必須チェックの集約ジョブ( 等) 4. branch protection ruleset・リポジトリ追加ガード(GitHub API のみ、コミット不要。適用前に退避・差分提示・明示承認を必須とする) この順序を守る理由: 「必須チェックに入れる集約ジョブが先に存在する」「AGENTS.md は PR の base コミット参照のため マージ後の次の PR から実効」という依存関係が、この順で自然に満たされる。 使い方 - 第 1 引数: 対象リポジトリ名( )。許容形式は のみ(Step 0 で検証する) - 第 2 引数: visibility( / の完全一致のみ。Step 0 で検証する)。runner 方針の分岐に使う(public は ubuntu ホステッド、private は self-hosted。ai-rev…

; then\n ng \"block-priorities の要素は P0〜P3 のみ許可(内部の空白・引用符・空要素・重複は不可)\"\n else\n # P0 と P1 の両方を含まない値は既存のブロックゲートを外すため拒否する\n case \"${list_els}\" in *\" P0 \"*) ;; *) ng \"block-priorities に P0 が必要\" ;; esac\n case \"${list_els}\" in *\" P1 \"*) ;; *) ng \"block-priorities に P1 が必要\" ;; esac\n fi ;;\n esac\n continue\n fi\n if [ \"${job_sub}\" = \"secrets\" ] && once \"job:${cur_job}/secrets/API_KEY\" && [[ \"${body}\" =~ ^API_KEY:[[:space:]]+\\$\\{\\{[[:space:]]*secrets\\.[A-Za-z0-9_]+[[:space:]]*\\}\\}[[:space:]]*$ ]]; then continue; fi\n fi\n\n ng \"許可リストにない行\"\ndone \u003c \"${target}\"\n\n# 最後のジョブを終える(ファイル末尾)。各ジョブの uses・権限・provider は finish_job が確認する\nfinish_job\n\nif [ \"${jobs_seen}\" -ne 1 ] || [ \"${job_count}\" -ne 1 ]; then\n echo \"NG: トップレベル jobs の下にジョブがちょうど 1 件必要(現在 ${job_count} 件。uses と権限を同じジョブに束縛するため。空ファイル・エラー応答を含む)\"\n violations=$((violations + 1))\nfi\n\n# concurrency は任意だが、あるなら group が必要(group の無い mapping は workflow を無効にする)\nif [ \"${conc_seen}\" -eq 1 ] && [ \"${group_seen}\" -ne 1 ]; then\n echo \"NG: concurrency には group が必要\"\n violations=$((violations + 1))\nfi\n\nif [ \"${on_seen}\" -ne 1 ] || [ \"${pr_seen}\" -ne 1 ]; then\n echo \"NG: トップレベル on とその直下の pull_request トリガーが必要\"\n violations=$((violations + 1))\nfi\n\n# トップレベル permissions の欠落は GITHUB_TOKEN をリポジトリ・組織の既定値に委ねるため不合格にする\n# (ジョブ側の permissions は finish_job が、そのジョブ自身に対して確認する)\nif [ \"${top_perm_ok}\" -ne 1 ]; then\n echo \"NG: トップレベル permissions の contents: read がない\"\n violations=$((violations + 1))\nfi\n\nif [ \"${violations}\" -gt 0 ]; then\n echo \"検査不合格: 違反 ${violations} 件。配置せず、差分を目視確認すること\"\n exit 1\nfi\necho \"OK: ai-review wrapper の構造検査に合格\"\nexit 0\n","content_type":"application/x-sh; charset=utf-8","language":"bash","size":27296,"content_sha256":"a07fda06c1f7f6795c96f3cd59f41301a5ad8d45d57467da2789cc2d1c22fb05"},{"filename":"tests/fixtures/[email protected]","content":"# ai-review wrapper テンプレート(単一 provider)。\n# 呼び出し側リポジトリの .github/workflows/ai-review.yml へコピーして使う。\n# 導入手順は Fandhe-AI/actions の ai-review/README.md「セットアップ」を参照。\n#\n# provider を切り替えるときは `provider:` と(必要なら)`runner:` / `secrets:` を書き換える。\n# runner の既定値:\n# - codex / claude / gemini(agentic): provider と同名の用途ラベル(codex / claude / gemini)\n# - grok / openai-compatible(API): private リポジトリは self-hosted、public は ubuntu-latest\n# - preflight / post_feedback(資格情報に触れない): private は self-hosted、public は ubuntu-latest\n# 別のラベルを使う場合は runner: に単一ラベルか JSON 配列を渡す(例: '[\"self-hosted\",\"no-sudo\"]')。\nname: AI PR review\n\non:\n pull_request:\n types: [opened, synchronize, reopened]\n branches: [main]\n\n# reusable workflow のトップレベル concurrency は機能しないため呼び出し側で設定する\nconcurrency:\n group: ai-review-${{ github.ref }}\n cancel-in-progress: true\n\npermissions:\n contents: read\n\njobs:\n # ジョブ ID が check 名の先頭になる(例: \"codex / review\")。required status check に\n # するときはこの名前で登録する\n codex:\n uses: Fandhe-AI/actions/.github/workflows/ai-review.yml@latest\n permissions:\n contents: read\n pull-requests: write\n with:\n provider: codex\n # 有効化スイッチ: Actions variable CODEX_HOME_DIR(runner 上のログイン済み CODEX_HOME)。\n # 未設定の間はレビュージョブが skip される。API キーで動かす場合は secrets.API_KEY を渡す\n #\n # --- よく使う調整 ---\n # runner: codex # review ジョブの runner ラベル(JSON 配列も可)\n # model: gpt-6-astra # 深いレビューが要る場合(quota 消費が大きい)\n # reasoning-effort: medium\n # block-priorities: \"P0,P1,P2\" # P2 もブロック対象に追加(P0/P1 は除外不可)\n # collaborators-only: false # write 未満の actor もレビュー対象にする(既定 true)\n # allowed-bot-actors: \"dependabot[bot],renovate[bot]\" # 権限によらず許可する bot(既定 dependabot[bot])\n # skip-branch-prefixes: chore/skills-update-,chore/submodule-update-\n # ↑ 自動生成 PR(日次同期 PR 等)を対象外にする。push できる主体はブランチ名で gate を\n # 回避できる(受容済み残留リスク。第三者の write コラボレーターがいる場合は指定しない)\n # secrets:\n # API_KEY: ${{ secrets.OPENAI_API_KEY }}\n\n # --- 他の provider の例(上の codex ジョブと置き換える) ---\n #\n # claude:\n # uses: Fandhe-AI/actions/.github/workflows/ai-review.yml@latest\n # permissions:\n # contents: read\n # pull-requests: write\n # with:\n # provider: claude\n # # 有効化: Actions variable CLAUDE_HOME_DIR(ログイン済み CLAUDE_CONFIG_DIR)または secret\n # secrets:\n # # Anthropic API キー、または `claude setup-token` の OAuth トークン(sk-ant-oat…)\n # API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}\n #\n # gemini:\n # uses: Fandhe-AI/actions/.github/workflows/ai-review.yml@latest\n # permissions:\n # contents: read\n # pull-requests: write\n # with:\n # provider: gemini\n # # 有効化: Actions variable GEMINI_HOME_DIR(ログイン済み GEMINI_CLI_HOME)または secret\n # secrets:\n # API_KEY: ${{ secrets.GEMINI_API_KEY }}\n #\n # grok:\n # uses: Fandhe-AI/actions/.github/workflows/ai-review.yml@latest\n # permissions:\n # contents: read\n # pull-requests: write\n # with:\n # provider: grok\n # model: ${{ vars.GROK_MODEL }} # xAI のモデル名(必須)\n # secrets:\n # API_KEY: ${{ secrets.XAI_API_KEY }} # 未設定の間は skip\n #\n # local-llm:\n # uses: Fandhe-AI/actions/.github/workflows/ai-review.yml@latest\n # permissions:\n # contents: read\n # pull-requests: write\n # with:\n # provider: openai-compatible\n # reviewer-id: local-llm\n # reviewer-name: Local LLM\n # # 例 http://llm-node:8000/v1。Actions variable AI_REVIEW_API_BASE_URLS(送信先の\n # # 許可リスト。同じ値を登録)と完全一致が必要。どちらかが未設定の間は skip\n # api-base-url: ${{ vars.LOCAL_LLM_BASE_URL }}\n # model: ${{ vars.LOCAL_LLM_MODEL }}\n # runner: self-hosted # LLM サーバーへ到達できる LAN 内の runner\n # max-diff-bytes: 600000 # モデルのコンテキスト長に合わせる\n","content_type":"application/yaml; charset=utf-8","language":"yaml","size":4823,"content_sha256":"65a8b303026175f7a998bab241546a528a19f80bdab2935d8e5582ba6f53edbe"},{"filename":"tests/verify-ai-review-wrapper.test.mjs","content":"// verify-ai-review-wrapper.sh の回帰テスト。\n// 取得した ai-review wrapper を配置前に検査する関門(allow-list・fail-closed)の契約を固定する:\n// 合格は実テンプレートの有効行と同形のみ、1 項目ずつの変異は不合格、使い方の誤りは exit 2、\n// 出力にファイル内容を含めない。YAML が文字列以外に解決する引用符なしの値(null・真偽値・数値・日付)は\n// 文字列の位置ごとに拒否し、引用符で囲んだ値は文字列として許可する(Issue #710)。\n// すべての検査を、実行できる bash(macOS の /bin/bash 3.2 と PATH の bash 5 等)で走らせ、結果が一致することを確かめる。\nimport { test } from 'node:test';\nimport assert from 'node:assert/strict';\nimport { mkdtempSync, writeFileSync, rmSync, existsSync, realpathSync, readFileSync } from 'node:fs';\nimport { tmpdir } from 'node:os';\nimport { join, resolve, dirname } from 'node:path';\nimport { spawnSync } from 'node:child_process';\nimport { fileURLToPath } from 'node:url';\n\nconst SCRIPT = resolve(dirname(fileURLToPath(import.meta.url)), '../scripts/verify-ai-review-wrapper.sh');\n\n// Fandhe-AI/actions の ai-review.single.yml の有効行と同形\nconst GOOD = `# コメント\nname: AI PR review\n\non:\n pull_request:\n types: [opened, synchronize, reopened]\n branches: [main]\n\nconcurrency:\n group: ai-review-\\${{ github.ref }}\n cancel-in-progress: true\n\npermissions:\n contents: read\n\njobs:\n codex:\n uses: Fandhe-AI/actions/.github/workflows/ai-review.yml@latest\n permissions:\n contents: read\n pull-requests: write\n with:\n provider: codex\n # runner: codex\n skip-branch-prefixes: chore/skills-update-,chore/submodule-update-\n post-feedback-runner: ubuntu-latest\n`;\n\n// 実行できる bash の一覧(実体パスで重複除去)。macOS では /bin/bash(3.2)と Homebrew の bash が両方あり得る。\n// PATH の bash(既定 bash)も含める。どれも見つからなければ PATH の bash を 1 つだけ返す。\n// このスキルは単独で vendoring されるため、他スキルのテストヘルパーは import せず複製する\nfunction availableBashes() {\n const seen = new Set();\n const out = [];\n const onPath = spawnSync('sh', ['-c', 'command -v bash'], { encoding: 'utf8' }).stdout.trim();\n for (const p of ['/bin/bash', '/usr/bin/bash', '/opt/homebrew/bin/bash', '/usr/local/bin/bash', onPath]) {\n if (!p || !existsSync(p)) continue;\n const real = realpathSync(p);\n if (seen.has(real)) continue;\n seen.add(real);\n out.push(p);\n }\n if (out.length === 0) out.push('bash');\n return out;\n}\nconst BASHES = availableBashes();\n\nfunction runBash(bash, file, args) {\n const r = spawnSync(bash, [SCRIPT, ...(args ?? [file])], { encoding: 'utf8' });\n return { code: r.status, out: r.stdout + r.stderr };\n}\n\n// 全 bash で実行し、終了コードと出力が一致することを確かめて返す(bash のバージョン差による判定のずれを検出する)\nfunction verify(content, args, bashes = BASHES) {\n const dir = mkdtempSync(join(tmpdir(), 'verify-wrapper-'));\n try {\n const f = join(dir, 'ai-review.yml');\n if (content !== undefined) writeFileSync(f, content);\n const results = bashes.map((b) => ({ bash: b, ...runBash(b, f, args) }));\n const [first, ...rest] = results;\n for (const r of rest) {\n assert.ok(r.code === first.code && r.out === first.out,\n `bash 間で結果が食い違う: ${first.bash} は exit ${first.code}、${r.bash} は exit ${r.code}\\n--- ${first.bash}\\n${first.out}\\n--- ${r.bash}\\n${r.out}`);\n }\n return { code: first.code, out: first.out };\n } finally {\n rmSync(dir, { recursive: true, force: true });\n }\n}\n\ntest('検査を走らせる bash の一覧(macOS では /bin/bash 3.2 と PATH の bash を含む)', (t) => {\n t.diagnostic(BASHES.map((b) => `${b}: ${spawnSync(b, ['--version'], { encoding: 'utf8' }).stdout.split('\\n')[0]}`).join(' / '));\n assert.ok(BASHES.length >= 1);\n});\n\ntest('実テンプレート同形・コメント追加・空行追加は合格する', () => {\n assert.equal(verify(GOOD).code, 0);\n assert.equal(verify('# 追加コメント\\n\\n' + GOOD + '\\n\\n# 末尾\\n').code, 0);\n});\n\ntest('旧パスのカスタム prompt を保持する prompt-path 指定は合格する', () => {\n const s = GOOD.replace(' provider: codex\\n', ' provider: codex\\n prompt-path: .github/ai-review/prompt.md\\n');\n assert.notEqual(s, GOOD);\n assert.equal(verify(s).code, 0);\n});\n\ntest('安全値の collaborators-only・block-priorities は合格する', () => {\n const s = GOOD.replace(' provider: codex\\n', ' provider: codex\\n collaborators-only: true\\n block-priorities: P0,P1,P2\\n');\n assert.equal(verify(s).code, 0);\n const q = GOOD.replace(' provider: codex\\n', ' provider: codex\\n block-priorities: \"P0, P1\"\\n');\n assert.equal(verify(q).code, 0);\n});\n\n// with 配下に追加した 1 行を GOOD の provider 行の直後へ差し込む\nconst withLine = (line) => GOOD.replace(' provider: codex\\n', ` provider: codex\\n ${line}\\n`);\nconst replaceTypes = (v) => GOOD.replace('[opened, synchronize, reopened]', v);\nconst replaceBranches = (v) => GOOD.replace('[main]', v);\n\nconst accepted = {\n 'types の空白なし [opened,synchronize,reopened]': replaceTypes('[opened,synchronize,reopened]'),\n 'types の括弧内外の余分な空白': replaceTypes('[ opened , synchronize , reopened ]'),\n 'types に ready_for_review を追加': replaceTypes('[opened, synchronize, reopened, ready_for_review]'),\n 'types の順序違い': replaceTypes('[reopened, opened, synchronize]'),\n 'branches が複数': replaceBranches('[main, release/1.0]'),\n 'branches の空白なし': replaceBranches('[main,develop]'),\n 'provider が引用符付き': withLine('model: x').replace('provider: codex', 'provider: \"codex\"'),\n 'provider が claude': GOOD.replace('provider: codex', 'provider: claude'),\n 'runner': withLine('runner: self-hosted'),\n 'model': withLine('model: gpt-6-astra'),\n 'model が vars 式': withLine('model: ${{ vars.GROK_MODEL }}'),\n 'api-base-url': withLine('api-base-url: http://llm-node:8000/v1'),\n 'api-base-url が vars 式': withLine('api-base-url: ${{ vars.LOCAL_LLM_BASE_URL }}'),\n 'reasoning-effort': withLine('reasoning-effort: medium'),\n 'reviewer-id': withLine('reviewer-id: local-llm'),\n 'reviewer-name が空白区切り': withLine('reviewer-name: Local LLM'),\n 'max-diff-bytes': withLine('max-diff-bytes: 600000'),\n 'allowed-bot-actors が引用符付き': withLine('allowed-bot-actors: \"dependabot[bot],renovate[bot]\"'),\n 'allowed-bot-actors が空白付き': withLine('allowed-bot-actors: \"dependabot[bot], renovate[bot]\"'),\n 'collaborators-only が引用符付き true': withLine('collaborators-only: \"true\"'),\n 'block-priorities が引用符なしで空白なし': withLine('block-priorities: P0,P1'),\n 'skip-branch-prefixes が引用符付きでカンマ後に空白': GOOD.replace('chore/skills-update-,chore/submodule-update-', '\"chore/a-, chore/b-\"'),\n 'skip-branch-prefixes が 1 件': GOOD.replace('chore/skills-update-,chore/submodule-update-', 'chore/skills-update-'),\n 'concurrency group が別の式': GOOD.replace('ai-review-${{ github.ref }}', 'ai-review-${{ github.workflow }}-${{ github.ref }}'),\n 'secrets の API_KEY': GOOD + ' secrets:\\n API_KEY: ${{ secrets.OPENAI_API_KEY }}\\n',\n // --- 型付き scalar に見えるが文字列になる値(#710)。引用符で囲んだ値は文字列、予約語・数値の一部だけを含む語も文字列 ---\n 'runner が引用符付き \"yes\"': withLine('runner: \"yes\"'),\n 'runner が引用符付き \\'null\\'': withLine(\"runner: 'null'\"),\n 'model が引用符付き \"1.5\"': withLine('model: \"1.5\"'),\n 'reviewer-id が引用符付き \"123\"': withLine('reviewer-id: \"123\"'),\n 'reviewer-name が引用符付き \"true\"': withLine('reviewer-name: \"true\"'),\n 'skip-branch-prefixes が引用符付き \"2024\"': GOOD.replace('chore/skills-update-,chore/submodule-update-', '\"2024\"'),\n 'skip-branch-prefixes が数字を含む列挙': GOOD.replace('chore/skills-update-,chore/submodule-update-', '2024,2025'),\n 'concurrency group が引用符付き \"1.5\"': GOOD.replace('ai-review-${{ github.ref }}', '\"1.5\"'),\n 'トップの name が引用符付き \"null\"': GOOD.replace('name: AI PR review', 'name: \"null\"'),\n 'ジョブの name が引用符付き \"123\"': GOOD.replace(' uses:', ' name: \"123\"\\n uses:'),\n 'ジョブの name が数字を含む語': GOOD.replace(' uses:', ' name: Review 2\\n uses:'),\n 'トップの name が予約語で始まる語': GOOD.replace('name: AI PR review', 'name: Yes we review'),\n 'branches が数字・予約語を含む語': replaceBranches('[1.0.0, v1, 2026-10-24-hotfix, nullable, on-call, yesterday, 0x1G, 1e3x]'),\n 'ジョブ ID が予約語で始まる語': GOOD.replace(' codex:\\n', ' nullable:\\n'),\n 'max-diff-bytes と collaborators-only は意図した数値・真偽値': withLine('max-diff-bytes: 600000').replace(' provider: codex\\n', ' provider: codex\\n collaborators-only: true\\n'),\n};\nfor (const [name, content] of Object.entries(accepted)) {\n test(`合格: ${name}`, () => {\n const r = verify(content);\n assert.equal(r.code, 0, r.out);\n });\n}\n\ntest('合格: リポジトリ自身の .github/workflows/ai-review.yml(全 bash)', (t) => {\n const f = resolve(dirname(SCRIPT), '../../../.github/workflows/ai-review.yml');\n if (!existsSync(f)) return t.skip('このリポジトリ外(vendoring 先)では存在しない');\n const r = verify(undefined, [f]);\n assert.equal(r.code, 0, r.out);\n});\n\n// Fandhe-AI/actions@8df2fb1bfff527731dcf74fa5cedc4bd6ee040af の ai-review/templates/ai-review.single.yml をそのまま保存した fixture。\n// 最小化した GOOD ではなく、上流の実物(コメント・他 provider の例を含む)が合格することを固定する\n// (上流テンプレートが更新されたら、固定取得の SHA とともにこの fixture も差し替える)\ntest('合格: 上流テンプレート ai-review.single.yml(Fandhe-AI/actions@8df2fb1b の実物・全 bash)', () => {\n const f = resolve(dirname(fileURLToPath(import.meta.url)), 'fixtures/[email protected]');\n assert.ok(readFileSync(f, 'utf8').includes('uses: Fandhe-AI/actions/.github/workflows/ai-review.yml@latest'));\n const r = verify(undefined, [f]);\n assert.equal(r.code, 0, r.out);\n});\n\nconst mutations = {\n 'uses が @main': (s) => s.replace('@latest', '@main'),\n 'uses が SHA': (s) => s.replace('@latest', '@8df2fb1bfff527731dcf74fa5cedc4bd6ee040af'),\n 'uses が別 org': (s) => s.replace('Fandhe-AI/actions', 'evil/actions'),\n 'uses がローカル参照': (s) => s.replace(/uses: .*/, 'uses: ./.github/workflows/x.yml'),\n 'uses を with 配下へ移動': (s) => s.replace(' uses: Fandhe-AI/actions/.github/workflows/ai-review.yml@latest\\n', '').replace(' provider: codex\\n', ' uses: Fandhe-AI/actions/.github/workflows/ai-review.yml@latest\\n provider: codex\\n'),\n 'with キーがジョブ直下': (s) => s.replace(' provider: codex\\n', '').replace(' with:\\n', ' provider: codex\\n with:\\n'),\n 'permissions がジョブ直下より深い': (s) => s.replace(' permissions:\\n contents: read\\n pull-requests: write\\n', ' with:\\n permissions:\\n contents: read\\n pull-requests: write\\n'),\n 'on ブロック削除': (s) => s.replace(/on:\\n pull_request:\\n types: .*\\n branches: .*\\n\\n/, ''),\n 'on はあるが pull_request なし': (s) => s.replace(/ pull_request:\\n types: .*\\n branches: .*\\n/, ''),\n 'uses が 2 件': (s) => s + ' second:\\n uses: Fandhe-AI/actions/.github/workflows/ai-review.yml@latest\\n',\n 'pull_request_target': (s) => s.replace('pull_request:', 'pull_request_target:'),\n 'on 配下に pull_request_review': (s) => s.replace('on:\\n', 'on:\\n pull_request_review:\\n'),\n 'on 配下に delete': (s) => s.replace('on:\\n', 'on:\\n delete:\\n'),\n 'トップレベル permissions 欠落': (s) => s.replace('permissions:\\n contents: read\\n\\njobs:', 'jobs:'),\n 'ジョブ側 permissions 欠落': (s) => s.replace(' permissions:\\n contents: read\\n pull-requests: write\\n', ''),\n 'ジョブ側 pull-requests: write 欠落': (s) => s.replace(' pull-requests: write\\n', ''),\n 'concurrency 配下の任意キー': (s) => s.replace(' cancel-in-progress: true\\n', ' cancel-in-progress: true\\n extra: x\\n'),\n 'workflow_run 追加': (s) => s.replace('on:\\n', 'on:\\n workflow_run:\\n'),\n 'run 追加': (s) => s + ' run: echo hi\\n',\n 'steps 追加': (s) => s + ' steps:\\n',\n 'env 追加': (s) => s + ' env:\\n',\n 'secrets: inherit': (s) => s + ' secrets: inherit\\n',\n 'contents: write': (s) => s.replace('permissions:\\n contents: read', 'permissions:\\n contents: write'),\n 'ジョブ側 id-token: write': (s) => s.replace(' pull-requests: write\\n', ' pull-requests: write\\n id-token: write\\n'),\n 'permissions: write-all': (s) => s.replace('permissions:\\n contents: read', 'permissions: write-all'),\n '未知キー': (s) => s + ' unknown-key: x\\n',\n 'block scalar': (s) => s.replace('provider: codex', 'provider: |'),\n 'CR を含む': (s) => s.replace('provider: codex', 'provider: codex\\r'),\n 'jobs: の再出現': (s) => s + '\\njobs:\\n',\n 'トップレベル on の重複': (s) => s + '\\non:\\n pull_request:\\n',\n 'トップレベル name の重複': (s) => s + '\\nname: x\\n',\n 'トップレベル permissions の重複': (s) => s + '\\npermissions:\\n contents: read\\n',\n 'ジョブ ID の重複': (s) => s + ' codex:\\n',\n 'ジョブ直下 with の重複': (s) => s + ' with:\\n provider: codex\\n',\n 'with キーの重複': (s) => s + ' provider: claude\\n',\n 'ジョブ側 permissions の重複': (s) => s + ' permissions:\\n contents: read\\n pull-requests: write\\n',\n 'permissions 内キーの重複': (s) => s.replace(' pull-requests: write\\n', ' pull-requests: write\\n pull-requests: write\\n'),\n 'concurrency の重複': (s) => s + '\\nconcurrency:\\n group: x\\n',\n // --- Issue #710: concurrency は任意だが、あるなら group が必須(PR #707 thread PRRT_kwDORuXFg86vr1N1) ---\n 'concurrency の group 欠落(cancel-in-progress のみ)': (s) => s.replace(' group: ai-review-${{ github.ref }}\\n', ''),\n 'concurrency が空(子なし)': (s) => s.replace(' group: ai-review-${{ github.ref }}\\n cancel-in-progress: true\\n', ''),\n 'concurrency の group の値が空': (s) => s.replace('group: ai-review-${{ github.ref }}', 'group:'),\n // --- Issue #710: branches の要素が YAML で文字列以外になる値(PR #707 thread PRRT_kwDORuXFg86vr1N_) ---\n 'branches が [null]': (s) => s.replace('[main]', '[null]'),\n 'branches が [true]': (s) => s.replace('[main]', '[true]'),\n 'branches の 2 要素目が [main, null]': (s) => s.replace('[main]', '[main, null]'),\n 'branches の要素が引用符付き([\"1.0\"]。引用符つき要素は未対応で拒否のまま)': (s) => s.replace('[main]', '[\"1.0\"]'),\n 'ジョブ ID が null:': (s) => s.replace(' codex:\\n', ' null:\\n'),\n 'ジョブ ID が true:': (s) => s.replace(' codex:\\n', ' true:\\n'),\n 'ジョブ ID が yes:': (s) => s.replace(' codex:\\n', ' yes:\\n'),\n 'collaborators-only: false': (s) => s.replace(' provider: codex\\n', ' provider: codex\\n collaborators-only: false\\n'),\n 'collaborators-only が任意値': (s) => s.replace(' provider: codex\\n', ' provider: codex\\n collaborators-only: maybe\\n'),\n 'block-priorities: P3': (s) => s.replace(' provider: codex\\n', ' provider: codex\\n block-priorities: P3\\n'),\n 'block-priorities に P1 欠落': (s) => s.replace(' provider: codex\\n', ' provider: codex\\n block-priorities: P0\\n'),\n 'block-priorities の内部空白 P 1': (s) => s.replace(' provider: codex\\n', \" provider: codex\\n block-priorities: 'P0,P 1'\\n\"),\n 'block-priorities の空要素': (s) => s.replace(' provider: codex\\n', ' provider: codex\\n block-priorities: P0,,P1\\n'),\n 'block-priorities に P0 欠落': (s) => s.replace(' provider: codex\\n', ' provider: codex\\n block-priorities: P1,P2\\n'),\n 'permissions の子キーが親+4 のインデント': (s) => s.replace(' contents: read\\n pull-requests: write\\n', ' contents: read\\n pull-requests: write\\n'),\n 'permissions の子キーが親+1 のインデント': (s) => s.replace(' contents: read\\n pull-requests: write\\n', ' contents: read\\n pull-requests: write\\n'),\n 'uses と permissions が別ジョブ': (s) => s.replace(' permissions:\\n contents: read\\n pull-requests: write\\n', '') + ' other:\\n permissions:\\n contents: read\\n pull-requests: write\\n',\n 'types が closed のみ': (s) => s.replace('[opened, synchronize, reopened]', '[closed]'),\n 'types に reopened 欠落': (s) => s.replace('[opened, synchronize, reopened]', '[opened, synchronize]'),\n // --- 値の文法(PR #707 の指摘 vpkrK・vpt-U と、同類型の退行防止) ---\n 'provider の値に \": \"(codex: broken)': (s) => s.replace('provider: codex', 'provider: codex: broken'),\n 'provider の値の末尾が \":\"': (s) => s.replace('provider: codex', 'provider: codex:'),\n 'provider が許可外の値': (s) => s.replace('provider: codex', 'provider: evil'),\n 'provider の引用符が閉じていない': (s) => s.replace('provider: codex', 'provider: \"codex'),\n 'provider の引用符が食い違う': (s) => s.replace('provider: codex', 'provider: \"codex\\''),\n 'provider の値に行末コメント': (s) => s.replace('provider: codex', 'provider: codex # x'),\n 'provider がフロー mapping': (s) => s.replace('provider: codex', 'provider: {a: b}'),\n 'provider がフロー配列': (s) => s.replace('provider: codex', 'provider: [codex]'),\n 'provider が anchor': (s) => s.replace('provider: codex', 'provider: &a codex'),\n 'provider が alias': (s) => s.replace('provider: codex', 'provider: *a'),\n 'provider が tag': (s) => s.replace('provider: codex', 'provider: !!str codex'),\n 'provider の値が空': (s) => s.replace('provider: codex', 'provider:'),\n 'provider の値が引用符の中に \": \"': (s) => s.replace('provider: codex', 'provider: \"codex: broken\"'),\n 'provider の値が内部空白': (s) => s.replace('provider: codex', 'provider: co dex'),\n 'with のキーの後にコロンの直後が空白でない': (s) => s.replace('provider: codex', 'provider:codex'),\n 'with のキーとコロンの間に空白': (s) => s.replace('provider: codex', 'provider : codex'),\n 'api-base-url に \": \"': (s) => s.replace(' provider: codex\\n', ' provider: codex\\n api-base-url: http://x: y\\n'),\n 'api-base-url がスキーム違い': (s) => s.replace(' provider: codex\\n', ' provider: codex\\n api-base-url: file:///etc/passwd\\n'),\n 'api-base-url が式(secrets)': (s) => s.replace(' provider: codex\\n', ' provider: codex\\n api-base-url: ${{ secrets.X }}\\n'),\n 'model の値に \": \"': (s) => s.replace(' provider: codex\\n', ' provider: codex\\n model: a: b\\n'),\n 'model が式を含む混在値': (s) => s.replace(' provider: codex\\n', ' provider: codex\\n model: x-${{ vars.M }}\\n'),\n 'runner が JSON 配列(許可リスト外)': (s) => s.replace(' provider: codex\\n', \" provider: codex\\n runner: '[\\\"self-hosted\\\",\\\"x\\\"]'\\n\"),\n 'reviewer-name に \": \"': (s) => s.replace(' provider: codex\\n', ' provider: codex\\n reviewer-name: a: b\\n'),\n 'max-diff-bytes が数字以外': (s) => s.replace(' provider: codex\\n', ' provider: codex\\n max-diff-bytes: 12a\\n'),\n 'prompt-path が親ディレクトリを辿る': (s) => s.replace(' provider: codex\\n', ' provider: codex\\n prompt-path: ../../etc/passwd\\n'),\n 'prompt-path が絶対パス': (s) => s.replace(' provider: codex\\n', ' provider: codex\\n prompt-path: /etc/passwd\\n'),\n 'skip-branch-prefixes の引用符なしでカンマ後に空白': (s) => s.replace('chore/skills-update-,chore/submodule-update-', 'a, b'),\n 'skip-branch-prefixes の要素内部の空白': (s) => s.replace('chore/skills-update-,chore/submodule-update-', '\"a b,c\"'),\n 'skip-branch-prefixes の空要素': (s) => s.replace('chore/skills-update-,chore/submodule-update-', 'a,,b'),\n 'skip-branch-prefixes に \": \"': (s) => s.replace('chore/skills-update-,chore/submodule-update-', 'a: b'),\n 'allowed-bot-actors に許可外の記号': (s) => s.replace(' provider: codex\\n', ' provider: codex\\n allowed-bot-actors: \"a;b\"\\n'),\n 'allowed-bot-actors の要素内部の空白': (s) => s.replace(' provider: codex\\n', ' provider: codex\\n allowed-bot-actors: \"dependabot[bot],ren ovate[bot]\"\\n'),\n 'collaborators-only が引用符付き false': (s) => s.replace(' provider: codex\\n', ' provider: codex\\n collaborators-only: \"false\"\\n'),\n 'types の要素内部の空白(synchron ize)': (s) => s.replace('[opened, synchronize, reopened]', '[opened, synchron ize, reopened]'),\n 'types の要素内部の空白(op ened)': (s) => s.replace('[opened, synchronize, reopened]', '[op ened, synchronize, reopened]'),\n 'types が空配列': (s) => s.replace('[opened, synchronize, reopened]', '[]'),\n 'types の末尾カンマ': (s) => s.replace('[opened, synchronize, reopened]', '[opened, synchronize, reopened,]'),\n 'types の空要素': (s) => s.replace('[opened, synchronize, reopened]', '[opened,, synchronize, reopened]'),\n 'types の重複要素': (s) => s.replace('[opened, synchronize, reopened]', '[opened, opened, synchronize, reopened]'),\n 'types に許可外のイベント': (s) => s.replace('[opened, synchronize, reopened]', '[opened, synchronize, reopened, closed]'),\n 'types の要素が引用符付き': (s) => s.replace('[opened, synchronize, reopened]', '[\"opened\", synchronize, reopened]'),\n 'types の要素が部分一致(openedx)': (s) => s.replace('[opened, synchronize, reopened]', '[openedx, synchronize, reopened]'),\n 'types が配列でない': (s) => s.replace('[opened, synchronize, reopened]', 'opened'),\n 'types の閉じ括弧なし': (s) => s.replace('[opened, synchronize, reopened]', '[opened, synchronize, reopened'),\n 'types の括弧が入れ子': (s) => s.replace('[opened, synchronize, reopened]', '[[opened], synchronize, reopened]'),\n 'types の後ろに余計な文字': (s) => s.replace('[opened, synchronize, reopened]', '[opened, synchronize, reopened] x'),\n 'types が pull_request の外': (s) => s.replace(' pull_request:\\n types: [opened, synchronize, reopened]\\n', ' types: [opened, synchronize, reopened]\\n pull_request:\\n'),\n 'branches の要素内部の空白': (s) => s.replace('[main]', '[ma in]'),\n 'branches の空要素': (s) => s.replace('[main]', '[main,,dev]'),\n 'branches の末尾カンマ': (s) => s.replace('[main]', '[main,]'),\n 'branches が空配列': (s) => s.replace('[main]', '[]'),\n 'branches に \": \"': (s) => s.replace('[main]', '[a: b]'),\n 'branches の要素が alias 始まり': (s) => s.replace('[main]', '[*main]'),\n 'branches の重複': (s) => s.replace('[main]', '[main, main]'),\n 'name の値に \": \"': (s) => s.replace('name: AI PR review', 'name: AI: PR review'),\n 'name の値に行末コメント': (s) => s.replace('name: AI PR review', 'name: AI PR review # x'),\n 'name が anchor': (s) => s.replace('name: AI PR review', 'name: &a AI'),\n 'name が flow mapping': (s) => s.replace('name: AI PR review', 'name: {a: b}'),\n 'concurrency group の値に \": \"': (s) => s.replace('group: ai-review-${{ github.ref }}', 'group: a: b'),\n 'concurrency group が許可外の式': (s) => s.replace('github.ref', 'secrets.X'),\n 'concurrency group の式が閉じていない': (s) => s.replace('${{ github.ref }}', '${{ github.ref'),\n 'ジョブ ID が数字始まり': (s) => s.replace(' codex:\\n', ' 1codex:\\n'),\n 'ジョブの name に \": \"': (s) => s.replace(' uses:', ' name: a: b\\n uses:'),\n // --- ジョブ構造(PR #707 の指摘 vrYoY: 設定行はジョブ ID より後・ジョブ内でのみ受理する) ---\n 'ジョブ ID の行を末尾へ移動': (s) => s.replace(' codex:\\n', '') + ' codex:\\n',\n 'ジョブ ID の行をジョブ設定の途中へ移動': (s) => s.replace(' codex:\\n', '').replace(' with:\\n', ' codex:\\n with:\\n'),\n 'ジョブ ID より前の permissions': (s) => s.replace(' codex:\\n', ' permissions:\\n contents: read\\n pull-requests: write\\n codex:\\n').replace(/( codex:\\n uses: .*\\n) permissions:\\n contents: read\\n pull-requests: write\\n/, '$1'),\n 'ジョブ ID より前の uses': (s) => s.replace(' codex:\\n uses: Fandhe-AI/actions/.github/workflows/ai-review.yml@latest\\n', ' uses: Fandhe-AI/actions/.github/workflows/ai-review.yml@latest\\n codex:\\n'),\n 'ジョブ ID より前の with': (s) => s.replace(' codex:\\n', ' with:\\n provider: codex\\n codex:\\n'),\n 'ジョブが空': (s) => s.replace(/ codex:\\n[\\s\\S]*$/, ' codex:\\n'),\n 'jobs が空': (s) => s.replace(/jobs:\\n[\\s\\S]*$/, 'jobs:\\n'),\n 'uses が 2 ジョブに分散(uses のないジョブ)': (s) => s.replace(' uses: Fandhe-AI/actions/.github/workflows/ai-review.yml@latest\\n', '') + ' second:\\n uses: Fandhe-AI/actions/.github/workflows/ai-review.yml@latest\\n',\n 'ジョブが uses を持たない(権限だけ)': (s) => s.replace(' uses: Fandhe-AI/actions/.github/workflows/ai-review.yml@latest\\n', ''),\n 'jobs より前のジョブキー': (s) => s.replace('jobs:\\n', ' uses: Fandhe-AI/actions/.github/workflows/ai-review.yml@latest\\njobs:\\n').replace(' codex:\\n uses: Fandhe-AI/actions/.github/workflows/ai-review.yml@latest\\n', ' codex:\\n'),\n 'トップレベル name の後ろにジョブのキー': (s) => s.replace('name: AI PR review\\n', '') + 'name: tail\\n permissions:\\n contents: read\\n',\n 'トップレベル name の後ろに on の子': (s) => s.replace('name: AI PR review\\n', 'name: AI PR review\\n pull_request:\\n'),\n 'jobs の後ろのトップレベル name の下にジョブ ID': (s) => s.replace('name: AI PR review\\n', '') + 'name: tail\\n other:\\n',\n 'on より前に pull_request': (s) => s.replace('on:\\n', ' pull_request:\\non:\\n'),\n 'concurrency の外の group': (s) => s.replace('concurrency:\\n', '').replace(' group:', ' group:'),\n 'with 値にコマンド置換風の文字': (s) => s.replace('provider: codex', 'provider: codex; rm -rf /'),\n};\n\nfor (const [name, mutate] of Object.entries(mutations)) {\n test(`不合格: ${name}`, () => {\n const r = verify(mutate(GOOD));\n assert.equal(r.code, 1, r.out);\n assert.match(r.out, /NG/);\n });\n}\n\n// --- Issue #710: 型付き scalar × 文字列の位置 の直積 ---\n// 引用符なしで書くと YAML が文字列以外(null・真偽値・数値・日付)に解決する値を、文字列でなければならない\n// すべての位置で拒否する。位置ごとに個別の指摘が連鎖していた(branches → group → name → with → ジョブ ID)ため、\n// 1 つの表で位置と値の全組を固定する。YAML 1.1 と 1.2 の和集合を拒否し、どちらかの方言で文字列にならない値は残さない。\n// 値の一覧(拒否する形の表)は scripts/verify-ai-review-wrapper.sh の is_typed_plain_scalar のコメントと対応させる\nconst TYPED_SCALARS = [\n 'null', 'Null', 'NULL', '~',\n 'true', 'True', 'TRUE', 'false', 'False', 'FALSE',\n 'yes', 'Yes', 'YES', 'no', 'No', 'NO', 'on', 'On', 'ON', 'off', 'Off', 'OFF', 'y', 'Y', 'n', 'N',\n '1', '0', '+1', '-1', '1_000', '0777', '0x1F', '0o17', '0b101',\n '1.5', '.5', '1.', '-1.5', '1.5e3', '1e3', '1E+3', '.inf', '.Inf', '.INF', '-.inf', '.nan', '.NaN', '.NAN',\n '2026-10-24', '2026-1-5',\n '=', '\u003c\u003c',\n];\nconst TYPED_SLOTS = {\n 'branches の要素': (v) => replaceBranches(`[${v}]`),\n 'branches の 2 要素目': (v) => replaceBranches(`[main, ${v}]`),\n 'concurrency.group': (v) => GOOD.replace('ai-review-${{ github.ref }}', v),\n 'トップの name': (v) => GOOD.replace('name: AI PR review', `name: ${v}`),\n 'ジョブの name': (v) => GOOD.replace(' uses:', ` name: ${v}\\n uses:`),\n 'ジョブ ID': (v) => GOOD.replace(' codex:\\n', ` ${v}:\\n`),\n 'with.runner': (v) => withLine(`runner: ${v}`),\n 'with.model': (v) => withLine(`model: ${v}`),\n 'with.api-base-url': (v) => withLine(`api-base-url: ${v}`),\n 'with.reasoning-effort': (v) => withLine(`reasoning-effort: ${v}`),\n 'with.post-feedback-runner': (v) => GOOD.replace('post-feedback-runner: ubuntu-latest', `post-feedback-runner: ${v}`),\n 'with.reviewer-id': (v) => withLine(`reviewer-id: ${v}`),\n 'with.reviewer-name': (v) => withLine(`reviewer-name: ${v}`),\n 'with.prompt-path': (v) => withLine(`prompt-path: ${v}`),\n 'with.skip-branch-prefixes': (v) => GOOD.replace('chore/skills-update-,chore/submodule-update-', v),\n 'with.allowed-bot-actors': (v) => withLine(`allowed-bot-actors: ${v}`),\n 'with.block-priorities': (v) => withLine(`block-priorities: ${v}`),\n};\n// 各位置の文法だけなら受理される値(型の拒否が効いていることを理由メッセージで確かめる対象)。\n// 先頭が英数字・_ ・. で、英数字・_ ・. ・- だけの値。ジョブ ID は先頭が英字・_ の値のみ\nconst GRAMMAR_SAFE = /^[A-Za-z0-9_.][A-Za-z0-9_.-]*$/;\nconst NO_DOT_SLOTS = new Set(['with.allowed-bot-actors', 'with.block-priorities']);\nfor (const [slot, build] of Object.entries(TYPED_SLOTS)) {\n test(`不合格: ${slot} に型付き scalar(${TYPED_SCALARS.length} 値)`, () => {\n const failures = [];\n for (const v of TYPED_SCALARS) {\n const r = verify(build(v));\n if (r.code !== 1 || !/NG/.test(r.out)) failures.push(`${v} → exit ${r.code}`);\n // 位置の文法だけなら通る値は、型の拒否の理由で落ちていること(文法の偶然に頼らない)\n const reasonable = GRAMMAR_SAFE.test(v) && !(slot === 'ジョブ ID' && !/^[A-Za-z_]/.test(v)) && !(NO_DOT_SLOTS.has(slot) && v.includes('.'));\n if (reasonable && slot !== 'with.block-priorities' && !/文字列以外/.test(r.out)) failures.push(`${v} → 型の拒否ではない理由で不合格: ${r.out.trim().split('\\n')[0]}`);\n }\n assert.deepEqual(failures, []);\n });\n}\n\n// 引用符で囲めば文字列なので、同じ値でも合格する対照(引用符を受け付けない branches の要素は含めない)\nconst QUOTABLE_SLOTS = ['concurrency.group', 'トップの name', 'ジョブの name', 'with.runner', 'with.model', 'with.reasoning-effort', 'with.reviewer-id', 'with.reviewer-name', 'with.prompt-path', 'with.skip-branch-prefixes'];\nfor (const slot of QUOTABLE_SLOTS) {\n test(`合格(対照): ${slot} は同じ値でも引用符で囲めば文字列`, () => {\n const safe = TYPED_SCALARS.filter((v) => GRAMMAR_SAFE.test(v) && !(slot === 'with.prompt-path' && v.includes('..')));\n const failures = [];\n for (const v of safe) {\n for (const q of ['\"', \"'\"]) {\n const r = verify(TYPED_SLOTS[slot](`${q}${v}${q}`));\n if (r.code !== 0) failures.push(`${q}${v}${q} → exit ${r.code}: ${r.out.trim().split('\\n')[0]}`);\n }\n }\n assert.deepEqual(failures, []);\n });\n}\n\ntest('不合格: concurrency に group が無い形は子の構成によらず拒否し、concurrency 自体の省略は合格する', () => {\n const noConc = GOOD.replace(/concurrency:\\n group: .*\\n cancel-in-progress: true\\n\\n/, '');\n assert.notEqual(noConc, GOOD);\n assert.equal(verify(noConc).code, 0); // concurrency は任意\n for (const body of ['', ' cancel-in-progress: true\\n', ' cancel-in-progress: false\\n']) {\n const r = verify(GOOD.replace(/concurrency:\\n group: .*\\n cancel-in-progress: true\\n/, `concurrency:\\n${body}`));\n assert.equal(r.code, 1, r.out);\n assert.match(r.out, /concurrency/);\n }\n const grouped = verify(GOOD.replace(' cancel-in-progress: true\\n', ''));\n assert.equal(grouped.code, 0, grouped.out); // group だけなら合格\n});\n\n// 行ベースの検査器と YAML の意味のずれを機械的に確かめる安全網。GOOD の構造行を移動・削除・複製・\n// インデント変更した変異のうち、検査器が合格にしたものを実際の YAML パーサで読み、\n// jobs.\u003cid>.uses と permissions などが期待の構造であることを確認する。\n// パーサ(python3 + PyYAML)が無い環境では skip する(新規依存は追加しない)。\nconst PYTHON_CHECK = `\nimport json, sys\ntry:\n import yaml\nexcept Exception:\n print(json.dumps({\"skip\": \"PyYAML なし\"})); sys.exit(0)\nEXPECTED = \"Fandhe-AI/actions/.github/workflows/ai-review.yml@latest\"\nWITH_KEYS = {\"provider\",\"runner\",\"model\",\"reasoning-effort\",\"block-priorities\",\"collaborators-only\",\"allowed-bot-actors\",\"skip-branch-prefixes\",\"post-feedback-runner\",\"max-diff-bytes\",\"reviewer-id\",\"reviewer-name\",\"api-base-url\",\"prompt-path\"}\ndef check(text):\n try:\n doc = yaml.safe_load(text)\n except Exception as e:\n return \"YAML として読めない: \" + str(e)[:80]\n if not isinstance(doc, dict): return \"トップがマップでない\"\n on = doc.get(\"on\", doc.get(True))\n extra = set(map(str, doc.keys())) - {\"name\",\"on\",\"True\",\"concurrency\",\"permissions\",\"jobs\"}\n if extra: return \"トップレベルに余計なキー: %s\" % sorted(extra)\n if doc.get(\"permissions\") != {\"contents\": \"read\"}: return \"トップレベル permissions が期待と違う\"\n if not isinstance(on, dict) or set(on.keys()) != {\"pull_request\"}: return \"on が pull_request のみでない\"\n pr = on[\"pull_request\"] if on[\"pull_request\"] is not None else {} # 子なし = 既定のイベント\n if not isinstance(pr, dict): return \"pull_request がマップでない\"\n if set(pr.keys()) - {\"types\",\"branches\"}: return \"pull_request の子が余計\"\n if \"types\" in pr:\n if not isinstance(pr[\"types\"], list) or not {\"opened\",\"synchronize\",\"reopened\"} \u003c= set(pr[\"types\"]) or len(set(pr[\"types\"])) != len(pr[\"types\"]): return \"types が期待と違う\"\n if \"branches\" in pr:\n if not isinstance(pr[\"branches\"], list) or not all(isinstance(b, str) and b and \" \" not in b for b in pr[\"branches\"]): return \"branches が期待と違う\"\n if \"name\" in doc and not isinstance(doc[\"name\"], str): return \"name が文字列でない\"\n if \"concurrency\" in doc:\n c = doc[\"concurrency\"]\n if not isinstance(c, dict) or not isinstance(c.get(\"group\"), str) or not c[\"group\"]: return \"concurrency に文字列の group がない\"\n if set(c.keys()) - {\"group\",\"cancel-in-progress\"}: return \"concurrency に余計なキー\"\n if \"cancel-in-progress\" in c and not isinstance(c[\"cancel-in-progress\"], bool): return \"cancel-in-progress が真偽値でない\"\n jobs = doc.get(\"jobs\")\n if not isinstance(jobs, dict) or len(jobs) != 1: return \"jobs がちょうど 1 件でない\"\n (jid, job), = jobs.items()\n if not isinstance(jid, str): return \"ジョブ ID が文字列でない\"\n if not isinstance(job, dict): return \"ジョブがマップでない\"\n if \"name\" in job and not isinstance(job[\"name\"], str): return \"ジョブの name が文字列でない\"\n if job.get(\"uses\") != EXPECTED: return \"jobs.\u003cid>.uses が期待と違う\"\n if job.get(\"permissions\") != {\"contents\": \"read\", \"pull-requests\": \"write\"}: return \"ジョブ permissions が期待と違う\"\n if set(job.keys()) - {\"name\",\"uses\",\"permissions\",\"with\",\"secrets\"}: return \"ジョブに余計なキー\"\n w = job.get(\"with\", {})\n if not isinstance(w, dict) or set(w.keys()) - WITH_KEYS: return \"with が期待と違う\"\n for k, v in w.items():\n if k == \"max-diff-bytes\":\n if not (isinstance(v, int) and not isinstance(v, bool)) and not (isinstance(v, str) and v.isdigit()): return \"max-diff-bytes が数値でない\"\n elif k == \"collaborators-only\":\n if v is not True and v != \"true\": return \"collaborators-only が true でない\"\n elif not isinstance(v, str): return \"with.%s が文字列でない\" % k\n if w.get(\"provider\") not in (\"codex\",\"claude\",\"gemini\",\"grok\",\"openai-compatible\"): return \"provider が期待と違う\"\n if \"collaborators-only\" in w and w[\"collaborators-only\"] is not True and w[\"collaborators-only\"] != \"true\": return \"collaborators-only が true でない\"\n return None\ncases = json.load(sys.stdin)\nprint(json.dumps([check(c) for c in cases]))\n`;\n\nfunction mulberry32(seed) {\n let a = seed >>> 0;\n return () => {\n a = (a + 0x6d2b79f5) >>> 0;\n let t = a;\n t = Math.imul(t ^ (t >>> 15), t | 1);\n t ^= t + Math.imul(t ^ (t >>> 7), t | 61);\n return ((t ^ (t >>> 14)) >>> 0) / 4294967296;\n };\n}\n\nfunction structuralMutants(base, count, seed = 705) {\n const rnd = mulberry32(seed);\n const lines = base.split('\\n');\n if (lines[lines.length - 1] === '') lines.pop();\n const out = [];\n const pick = () => Math.floor(rnd() * lines.length);\n for (let i = 0; i \u003c count; i++) {\n const ls = lines.slice();\n const steps = 1 + Math.floor(rnd() * 2);\n for (let k = 0; k \u003c steps; k++) {\n const a = Math.min(pick(), ls.length - 1);\n const kind = Math.floor(rnd() * 4);\n if (kind === 0) { // 移動\n const [l] = ls.splice(a, 1);\n ls.splice(Math.floor(rnd() * (ls.length + 1)), 0, l);\n } else if (kind === 1) { // 削除\n ls.splice(a, 1);\n } else if (kind === 2) { // 複製\n ls.splice(Math.floor(rnd() * (ls.length + 1)), 0, ls[a]);\n } else { // インデント変更\n ls[a] = rnd() \u003c 0.5 ? ' ' + ls[a] : ls[a].replace(/^ {1,2}/, '');\n }\n if (ls.length === 0) ls.push('');\n }\n out.push(ls.join('\\n') + '\\n');\n }\n return out;\n}\n\ntest('検査器が合格にした変異は YAML パーサで読んでも期待の構造になる(行ベースと YAML の意味のずれの検出)', (t) => {\n const probe = spawnSync('python3', ['-I', '-c', PYTHON_CHECK], { input: '[]', encoding: 'utf8' });\n if (probe.status !== 0) return t.skip('python3 を実行できない');\n const probed = JSON.parse(probe.stdout);\n if (!Array.isArray(probed)) return t.skip(probed.skip);\n\n // 値の変異(型付き scalar を各位置へ・引用符つきの対照)は、ここで検査器が合格にしたものを oracle が\n // str として読み直す。構造だけを動かす変異では [null] のような値の型の穴を一度も見られなかった(#710)\n const ORACLE_VALUES = ['null', '~', 'true', 'yes', 'on', '1', '1_000', '0x1F', '0o17', '1.5', '.inf', '2026-10-24']; // 全値は上の直積テストで検査済み。ここは oracle の代表値\n const valueMutants = [];\n for (const build of Object.values(TYPED_SLOTS)) {\n for (const v of ORACLE_VALUES) valueMutants.push(build(v), build(`\"${v}\"`));\n }\n const candidates = [GOOD, ...Object.values(accepted), ...valueMutants, ...structuralMutants(GOOD, Number(process.env.FUZZ_COUNT ?? 400), Number(process.env.FUZZ_SEED ?? 705))];\n // 候補ごとに bash を順繰りに 1 つだけ使う(全 bash での一致は他のテストが固定済み。ここは件数が多く時間がかかるため)\n const passed = candidates.filter((c, i) => verify(c, undefined, [BASHES[i % BASHES.length]]).code === 0);\n // 値の変異のうち合格したものは引用符つきの文字列だけで、型付き scalar の素の値は 1 件も残らない\n assert.ok(passed.some((c) => /: \"(yes|null|1\\.5)\"/.test(c)), '引用符つきの対照が合格に含まれない');\n assert.ok(passed.length >= Object.keys(accepted).length + 1, '合格ケースが少なすぎる');\n const r = spawnSync('python3', ['-I', '-c', PYTHON_CHECK], { input: JSON.stringify(passed), encoding: 'utf8' });\n assert.equal(r.status, 0, r.stderr);\n const results = JSON.parse(r.stdout);\n const bad = results.map((m, i) => (m === null ? null : `${m}\\n---\\n${passed[i]}`)).filter(Boolean);\n assert.equal(bad.length, 0, `検査器が合格にしたが YAML の意味が期待と違う ${bad.length} 件:\\n${bad.slice(0, 3).join('\\n=====\\n')}`);\n});\n\ntest('oracle 自体の自己テスト: 型付き scalar と group 欠落を YAML パーサの側でも検出できる(oracle が素通しになっていない)', (t) => {\n const probe = spawnSync('python3', ['-I', '-c', PYTHON_CHECK], { input: '[]', encoding: 'utf8' });\n if (probe.status !== 0) return t.skip('python3 を実行できない');\n if (!Array.isArray(JSON.parse(probe.stdout))) return t.skip('PyYAML なし');\n const bad = [\n replaceBranches('[null]'), replaceBranches('[true]'), replaceBranches('[2026-10-24]'),\n GOOD.replace(' group: ai-review-${{ github.ref }}\\n', ''),\n GOOD.replace('name: AI PR review', 'name: on'),\n GOOD.replace('ai-review-${{ github.ref }}', '1.5'),\n GOOD.replace(' codex:\\n', ' null:\\n'),\n withLine('runner: yes'),\n withLine('model: null'),\n ];\n const r = spawnSync('python3', ['-I', '-c', PYTHON_CHECK], { input: JSON.stringify([GOOD, ...bad]), encoding: 'utf8' });\n const res = JSON.parse(r.stdout);\n assert.equal(res[0], null, `GOOD は oracle も合格: ${res[0]}`);\n res.slice(1).forEach((m, i) => assert.notEqual(m, null, `oracle が見逃した: ${bad[i]}`));\n});\n\ntest('不合格: 空ファイルと HTTP エラー JSON(uses 0 件)', () => {\n assert.equal(verify('').code, 1);\n assert.equal(verify('{\"message\":\"Not Found\",\"status\":\"404\"}\\n').code, 1);\n});\n\ntest('使い方の誤りは exit 2', () => {\n assert.equal(verify(undefined, []).code, 2);\n assert.equal(verify(undefined, ['/nonexistent/path']).code, 2);\n assert.equal(verify(undefined, [tmpdir()]).code, 2);\n});\n\ntest('出力にファイル内容(注入文字列)を含めない', () => {\n const secret = 'SECRET_MARKER_12345';\n const r = verify(GOOD + ` ${secret}: x\\n`);\n assert.equal(r.code, 1);\n assert.ok(!r.out.includes(secret));\n});\n","content_type":"text/javascript; charset=utf-8","language":"javascript","size":42400,"content_sha256":"8d51ef9c52afd77dacd55693390e2ec461cd0288c155e79ba9c3804d418fd62d"}],"content_json":{"type":"doc","content":[{"type":"heading","attrs":{"level":1},"content":[{"text":"setup-repo-guards","type":"text"}]},{"type":"paragraph","content":[{"text":"対象リポジトリへ組織標準の CI ガード一式を導入する。導入するのは次の 4 点で、 Step 1 → 4 の順に、各 Step を PR → CI 全 pass → レビュースレッド resolve → squash merge で確定させてから次へ進む。","type":"text"}]},{"type":"ordered_list","attrs":{"order":1,"listStyle":"number"},"content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"ai-review(PR 自動レビュー workflow の wrapper)","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"AGENTS.md(AI レビューが読むレビュー観点集)","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"CI 必須チェックの集約ジョブ(","type":"text"},{"text":"ci-complete","type":"text","marks":[{"type":"code_inline"}]},{"text":" 等)","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"branch protection ruleset・リポジトリ追加ガード(GitHub API のみ、コミット不要。適用前に退避・差分提示・明示承認を必須とする)","type":"text"}]}]}]},{"type":"paragraph","content":[{"text":"この順序を守る理由: 「必須チェックに入れる集約ジョブが先に存在する」「AGENTS.md は PR の base コミット参照のため マージ後の次の PR から実効」という依存関係が、この順で自然に満たされる。","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"使い方","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":""},"content":[{"text":"setup-repo-guards Fandhe-AI/my-repo public\nsetup-repo-guards Fandhe-AI/repo-a public Fandhe-AI/repo-b private","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"第 1 引数: 対象リポジトリ名(","type":"text"},{"text":"owner/repo","type":"text","marks":[{"type":"code_inline"}]},{"text":")。許容形式は ","type":"text"},{"text":"^[A-Za-z0-9-]+/[A-Za-z0-9._-]+$","type":"text","marks":[{"type":"code_inline"}]},{"text":" のみ(Step 0 で検証する)","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"第 2 引数: visibility(","type":"text"},{"text":"public","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"private","type":"text","marks":[{"type":"code_inline"}]},{"text":" の完全一致のみ。Step 0 で検証する)。runner 方針の分岐に使う(public は ubuntu ホステッド、private は self-hosted。ai-review の review ジョブのみ self-hosted の専用 runner の例外)","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"複数リポジトリへ一括適用する場合はリポジトリと visibility の組を列挙する。各リポジトリのセッション(または並列 agent)で順次消化する","type":"text"}]}]}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"前提条件","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"gh","type":"text","marks":[{"type":"code_inline"}]},{"text":" CLI がインストールされ、対象リポジトリの admin 権限で認証済みであること(","type":"text"},{"text":"command -v gh && gh auth status","type":"text","marks":[{"type":"code_inline"}]},{"text":" で確認)","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"jq","type":"text","marks":[{"type":"code_inline"}]},{"text":" がインストールされていること(","type":"text"},{"text":"command -v jq","type":"text","marks":[{"type":"code_inline"}]},{"text":" で確認)","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Fandhe-AI/actions(reusable workflow の提供元)への参照権限があること","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"対象リポジトリを clone 済みで、CLAUDE.md・既存 workflow を読める状態であること","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Step 4 の適用可否に利用者が応答できること(対話可能であること)。応答できない自動運転環境では Step 4 の書き込みを実行せず、差分と退避先を報告して停止する","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"退避先(","type":"text"},{"text":"mktemp -d","type":"text","marks":[{"type":"code_inline"}]},{"text":" が作る作業ツリー外のディレクトリ)に書き込めること","type":"text"}]}]}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"フロー","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Step 0: 引数の検証(最初に実行)","type":"text"}]},{"type":"paragraph","content":[{"text":"引数の値は ","type":"text"},{"text":"gh api","type":"text","marks":[{"type":"code_inline"}]},{"text":" のパスや runner 方針の分岐にそのまま入るため、","type":"text"},{"text":"gh","type":"text","marks":[{"type":"code_inline"}]},{"text":" を含むコマンドを実行する前に 許可リストで検証する(A03 インジェクション・パス注入の防止)。複数リポジトリの一括適用では組ごとに検証し、 不正な組は処理せず完了報告に載せて、他の組は続行する。","type":"text"}]},{"type":"paragraph","content":[{"text":"束縛の安全性(検証より前に生の入力をシェルへ入れない)","type":"text","marks":[{"type":"strong"}]},{"text":": 引数値を ","type":"text"},{"text":"repo=\"\u003c入力>\"","type":"text","marks":[{"type":"code_inline"}]},{"text":" のように ダブルクォートへ直接展開してコマンド文字列に埋め込まない(","type":"text"},{"text":"\"; cmd; \"","type":"text","marks":[{"type":"code_inline"}]},{"text":" や ","type":"text"},{"text":"$(...)","type":"text","marks":[{"type":"code_inline"}]},{"text":" を含む入力が検証前に 実行される)。クォート付き HEREDOC も、入力が区切り語の単独行とそれに続くコマンドを含むと HEREDOC を抜けて 後続行が実行されるため、それだけでは安全ではない。次の手順で束縛する。","type":"text"}]},{"type":"ordered_list","attrs":{"order":1,"listStyle":"number"},"content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"先に文字列として照合する(シェルへ渡す前): 入力が単一行で、改行・CR・区切り語(下記 ","type":"text"},{"text":"ARG_EOF_","type":"text","marks":[{"type":"code_inline"}]},{"text":" 接頭辞の行)を 含まず、","type":"text"},{"text":"^[A-Za-z0-9-]+/[A-Za-z0-9._-]+$","type":"text","marks":[{"type":"code_inline"}]},{"text":"(visibility は ","type":"text"},{"text":"public","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"private","type":"text","marks":[{"type":"code_inline"}]},{"text":")に一致することを確かめる。 一致しない、または改行を含むと判断した時点で、シェルコマンドを組み立てずに中止する","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"一致した値だけを、推測されにくい区切り語のクォート付き HEREDOC(展開が起きない)で束縛する。 区切り語は実行ごとにランダムな値にし、入力に含まれないことを手順 1 で確認済みの前提で使う","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"束縛と同時に、入力が 1 行だけであることを検査する。","type":"text"},{"text":"read -r","type":"text","marks":[{"type":"code_inline"}]},{"text":" は先頭行しか取らず残りを黙って捨てるため、 束縛後の値に改行は残らず、値に対する改行の検査は常に素通りする(検査として機能しない)。 そこで 1 行目を読んだ後に 2 行目を読もうとし、読めた(= 2 行以上ある、空の 2 行目を含む)場合は ","type":"text"},{"text":"bind_ok=0","type":"text","marks":[{"type":"code_inline"}]},{"text":" にして検証へ進まない。CR は ","type":"text"},{"text":"read -r","type":"text","marks":[{"type":"code_inline"}]},{"text":" が除去しないため、束縛後の値に対する検査が有効に働く","type":"text"}]}]}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"# bash 3.2(macOS)と zsh の両方で動く。正規表現は変数に入れて unquoted で使う\n# 束縛: クォート付き HEREDOC は展開を行わない(値に $ や ` や \" があっても実行されない)\n# 区切り語 ARG_EOF_7f3a9c は例。実行ごとに別のランダム値にし、入力に現れないことを事前に確かめる\n# 2 行目が読めたら複数行入力として bind_ok=0 にする(1 行目しか束縛されず残りが捨てられるのを防ぐ)\nbind_ok=1\n{ IFS= read -r repo && ! IFS= read -r _extra; } \u003c\u003c'ARG_EOF_7f3a9c' || bind_ok=0\nFandhe-AI/my-repo\nARG_EOF_7f3a9c\n{ IFS= read -r visibility && ! IFS= read -r _extra; } \u003c\u003c'ARG_EOF_7f3a9c' || bind_ok=0\npublic\nARG_EOF_7f3a9c\n\n# 単一の組の検証。一括適用では組ごとに関数として呼ぶ。不正なら return 1 で当該組だけ除外して\n# 次の組へ進む(exit 1 はシェルごと終了するため使わない)\nvalidate_pair() {\n local repo=\"$1\" visibility=\"$2\" name\n local repo_re='^[A-Za-z0-9-]+/[A-Za-z0-9._-]+

setup-repo-guards 対象リポジトリへ組織標準の CI ガード一式を導入する。導入するのは次の 4 点で、 Step 1 → 4 の順に、各 Step を PR → CI 全 pass → レビュースレッド resolve → squash merge で確定させてから次へ進む。 1. ai-review(PR 自動レビュー workflow の wrapper) 2. AGENTS.md(AI レビューが読むレビュー観点集) 3. CI 必須チェックの集約ジョブ( 等) 4. branch protection ruleset・リポジトリ追加ガード(GitHub API のみ、コミット不要。適用前に退避・差分提示・明示承認を必須とする) この順序を守る理由: 「必須チェックに入れる集約ジョブが先に存在する」「AGENTS.md は PR の base コミット参照のため マージ後の次の PR から実効」という依存関係が、この順で自然に満たされる。 使い方 - 第 1 引数: 対象リポジトリ名( )。許容形式は のみ(Step 0 で検証する) - 第 2 引数: visibility( / の完全一致のみ。Step 0 で検証する)。runner 方針の分岐に使う(public は ubuntu ホステッド、private は self-hosted。ai-rev…

\n # CR は read -r が除去しないため、束縛後の値で検出できる(改行は束縛時の bind_ok で検査済み)\n case \"${repo}${visibility}\" in *

setup-repo-guards 対象リポジトリへ組織標準の CI ガード一式を導入する。導入するのは次の 4 点で、 Step 1 → 4 の順に、各 Step を PR → CI 全 pass → レビュースレッド resolve → squash merge で確定させてから次へ進む。 1. ai-review(PR 自動レビュー workflow の wrapper) 2. AGENTS.md(AI レビューが読むレビュー観点集) 3. CI 必須チェックの集約ジョブ( 等) 4. branch protection ruleset・リポジトリ追加ガード(GitHub API のみ、コミット不要。適用前に退避・差分提示・明示承認を必須とする) この順序を守る理由: 「必須チェックに入れる集約ジョブが先に存在する」「AGENTS.md は PR の base コミット参照のため マージ後の次の PR から実効」という依存関係が、この順で自然に満たされる。 使い方 - 第 1 引数: 対象リポジトリ名( )。許容形式は のみ(Step 0 で検証する) - 第 2 引数: visibility( / の完全一致のみ。Step 0 で検証する)。runner 方針の分岐に使う(public は ubuntu ホステッド、private は self-hosted。ai-rev…

\\r'*) echo \"不正な入力: CR を含む\"; return 1 ;; esac\n if ! [[ \"${repo}\" =~ ${repo_re} ]]; then echo \"不正な repo: 形式外\"; return 1; fi\n name=\"${repo#*/}\"\n case \"${repo}\" in -*) echo \"不正な repo: 先頭が -\"; return 1 ;; esac\n case \"${name}\" in .|..) echo \"不正な repo: . / .. は不可\"; return 1 ;; esac\n case \"${visibility}\" in public|private) ;; *) echo \"不正な visibility\"; return 1 ;; esac\n}\n# 一括適用の例: 組ごとに呼び、失敗した組だけ完了報告へ回して次へ進む\nif [ \"${bind_ok}\" -ne 1 ]; then echo \"除外: 複数行または空の入力\"\nelif ! validate_pair \"${repo}\" \"${visibility}\"; then echo \"除外: 検証失敗\"; fi","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"拒否する値: 余分な ","type":"text"},{"text":"/","type":"text","marks":[{"type":"code_inline"}]},{"text":" を含む値、先頭が ","type":"text"},{"text":"-","type":"text","marks":[{"type":"code_inline"}]},{"text":" の値(","type":"text"},{"text":"gh","type":"text","marks":[{"type":"code_inline"}]},{"text":" のオプションと取り違えられる)、 リポジトリ名が ","type":"text"},{"text":".","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"..","type":"text","marks":[{"type":"code_inline"}]},{"text":" だけの値、空文字、空白・改行・","type":"text"},{"text":";","type":"text","marks":[{"type":"code_inline"}]},{"text":" 等を含む値","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"大文字小文字の違いや末尾の空白・改行を暗黙に補正しない(補正せず拒否する)","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"実在確認: 形式検証を通った後に読み取りのみで確かめる","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"gh api \"repos/${repo}\" --jq '{full_name, private, archived, admin: .permissions.admin}'","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"full_name","type":"text","marks":[{"type":"code_inline"}]},{"text":" が入力と一致すること","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"private","type":"text","marks":[{"type":"code_inline"}]},{"text":" が引数の visibility と整合すること(","type":"text"},{"text":"public","type":"text","marks":[{"type":"code_inline"}]},{"text":" なら false、","type":"text"},{"text":"private","type":"text","marks":[{"type":"code_inline"}]},{"text":" なら true)。 不一致なら中止して報告し、勝手に補正しない","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"admin == true","type":"text","marks":[{"type":"code_inline"}]},{"text":" かつ ","type":"text"},{"text":"archived == false","type":"text","marks":[{"type":"code_inline"}]},{"text":" であること(ruleset の作成・更新には admin 権限が必要で、 archived は読み取り専用で変更できないため。満たさなければ後続 Step が必ず失敗する)","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"取得失敗時は ","type":"text"},{"text":"gh api","type":"text","marks":[{"type":"code_inline"}]},{"text":" のエラーが stdout に出るため終了コードだけで判断せず、 HTTP status(403 / 404 等)を確認して「判定不能」として中止する。通過に倒さない","type":"text"}]}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"以降の Step で使う ","type":"text"},{"text":"repo","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"visibility","type":"text","marks":[{"type":"code_inline"}]},{"text":" は、この検証を通った値だけにする。 検証に失敗した場合は ","type":"text"},{"text":"gh","type":"text","marks":[{"type":"code_inline"}]},{"text":" を含むコマンドを一切実行せず中止し、理由を報告する","type":"text"}]}]}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Step 1: ai-review の導入(未導入の場合のみ)","type":"text"}]},{"type":"paragraph","content":[{"text":".github/workflows/ai-review.yml","type":"text","marks":[{"type":"code_inline"}]},{"text":" を wrapper として追加し、Fandhe-AI/actions の reusable workflow ","type":"text"},{"text":"ai-review.yml","type":"text","marks":[{"type":"code_inline"}]},{"text":" を ","type":"text"},{"text":"可変タグ ","type":"text","marks":[{"type":"strong"}]},{"text":"@latest","type":"text","marks":[{"type":"code_inline"},{"type":"strong"}]},{"text":" で参照する(Fandhe-AI/actions の利用側規約。","type":"text"},{"text":"latest","type":"text","marks":[{"type":"code_inline"}]},{"text":" は main へ 自動追従するため、参照の更新作業は不要。commit SHA 固定・","type":"text"},{"text":"@main","type":"text","marks":[{"type":"code_inline"}]},{"text":" は使わない)。 旧 ","type":"text"},{"text":"codex-review.yml","type":"text","marks":[{"type":"code_inline"}]},{"text":" は Fandhe-AI/actions から削除済みのため、新規導入では使わない。","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"既に ","type":"text"},{"text":".github/workflows/ai-review.yml","type":"text","marks":[{"type":"code_inline"}]},{"text":" がある場合はこの Step をスキップする","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"旧 ","type":"text"},{"text":".github/workflows/codex-review.yml","type":"text","marks":[{"type":"code_inline"}]},{"text":" が残っている場合は、","type":"text"},{"text":"codex-review.yml@latest","type":"text","marks":[{"type":"code_inline"}]},{"text":" が解決できず レビューが動かない状態になっている。Fandhe-AI/actions の ","type":"text"},{"text":"ai-review/README.md","type":"text","marks":[{"type":"code_inline"}]},{"text":"「codex-review からの移行」の 手順(入力名の対応表・required check 名の差し替え順序)に従って置き換える","type":"text"}]}]}]},{"type":"paragraph","content":[{"text":"wrapper は単一 provider テンプレートをコピーして作る(既定は ","type":"text"},{"text":"provider: codex","type":"text","marks":[{"type":"code_inline"}]},{"text":"、ジョブ ID ","type":"text"},{"text":"codex","type":"text","marks":[{"type":"code_inline"}]},{"text":")。 取得元を commit SHA に固定し、配置前に構造検査を通す。テンプレートを ref 指定なし・無検証で配置すると、 取得元の改ざん・誤更新がそのまま対象リポジトリの workflow になるため。","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"@latest","type":"text","marks":[{"type":"code_inline"}]},{"text":" を wrapper に残すのは Fandhe-AI/actions の組織規約として受容した残留リスクである (実行時の reusable workflow の内容は wrapper 側で固定しない)。本手順の固定取得と検査が塞ぐのは 「wrapper ファイル自体の改ざん・誤配置」だけで、","type":"text"},{"text":"latest","type":"text","marks":[{"type":"code_inline"}]},{"text":" タグ自体の改ざんは検査と目視で補う限界がある","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"検査スクリプト(","type":"text"},{"text":"scripts/verify-ai-review-wrapper.sh","type":"text","marks":[{"type":"code_inline"}]},{"text":")は allow-list 方式で、","type":"text"},{"text":"uses:","type":"text","marks":[{"type":"code_inline"}]},{"text":" が期待値と完全一致で ちょうど 1 件・特権トリガー無し・","type":"text"},{"text":"run:","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"steps:","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"env:","type":"text","marks":[{"type":"code_inline"}]},{"text":" 無し・","type":"text"},{"text":"secrets: inherit","type":"text","marks":[{"type":"code_inline"}]},{"text":" 無し・権限が ","type":"text"},{"text":"contents: read","type":"text","marks":[{"type":"code_inline"}]},{"text":"(ジョブ側のみ加えて ","type":"text"},{"text":"pull-requests: write","type":"text","marks":[{"type":"code_inline"}]},{"text":")に限られ、両 permissions が欠落していないこと、 ","type":"text"},{"text":"on:","type":"text","marks":[{"type":"code_inline"}]},{"text":" 配下が ","type":"text"},{"text":"pull_request","type":"text","marks":[{"type":"code_inline"}]},{"text":" のみであることを確かめる(親キーを追跡し、任意のジョブ名は ","type":"text"},{"text":"jobs:","type":"text","marks":[{"type":"code_inline"}]},{"text":" 直下だけ許可)。 構造は行の位置に依存しない状態機械で追い、ジョブの設定はジョブ ID の行より後ろ(現在のジョブ)でだけ受理して、そのジョブ自身が ","type":"text"},{"text":"uses","type":"text","marks":[{"type":"code_inline"}]},{"text":"・権限・","type":"text"},{"text":"with.provider","type":"text","marks":[{"type":"code_inline"}]},{"text":" を揃えていることを確かめる(ジョブ ID を末尾へ移す等の並べ替えは不可)。 値はキーごとに許可する文法へ完全一致させる(","type":"text"},{"text":"with","type":"text","marks":[{"type":"code_inline"}]},{"text":" の値は 1 行の plain scalar か全体を引用符で囲んだ文字列で、","type":"text"},{"text":": ","type":"text","marks":[{"type":"code_inline"}]},{"text":"・","type":"text"},{"text":"#","type":"text","marks":[{"type":"code_inline"}]},{"text":"・anchor・tag・flow 記法は不可。 ","type":"text"},{"text":"types","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"branches","type":"text","marks":[{"type":"code_inline"}]},{"text":" は ","type":"text"},{"text":"[a, b]","type":"text","marks":[{"type":"code_inline"}]},{"text":" を要素に分割し、各要素の前後の空白だけを除いて許可語と完全一致、","type":"text"},{"text":"types","type":"text","marks":[{"type":"code_inline"}]},{"text":" は ","type":"text"},{"text":"opened","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"synchronize","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"reopened","type":"text","marks":[{"type":"code_inline"}]},{"text":" が要素として揃うこと。空要素・要素内部の空白・重複は不可)。 文字列が必要な位置(","type":"text"},{"text":"name","type":"text","marks":[{"type":"code_inline"}]},{"text":"・ジョブ ID・","type":"text"},{"text":"concurrency.group","type":"text","marks":[{"type":"code_inline"}]},{"text":"・","type":"text"},{"text":"with","type":"text","marks":[{"type":"code_inline"}]},{"text":" の文字列キー・","type":"text"},{"text":"branches","type":"text","marks":[{"type":"code_inline"}]},{"text":" の要素)では、引用符なしで書くと YAML が null・真偽値・数値・日付に解決する値(","type":"text"},{"text":"null","type":"text","marks":[{"type":"code_inline"}]},{"text":"・","type":"text"},{"text":"~","type":"text","marks":[{"type":"code_inline"}]},{"text":"・","type":"text"},{"text":"true","type":"text","marks":[{"type":"code_inline"}]},{"text":"・","type":"text"},{"text":"yes","type":"text","marks":[{"type":"code_inline"}]},{"text":"・","type":"text"},{"text":"on","type":"text","marks":[{"type":"code_inline"}]},{"text":"・","type":"text"},{"text":"1","type":"text","marks":[{"type":"code_inline"}]},{"text":"・","type":"text"},{"text":"1_000","type":"text","marks":[{"type":"code_inline"}]},{"text":"・","type":"text"},{"text":"0x1F","type":"text","marks":[{"type":"code_inline"}]},{"text":"・","type":"text"},{"text":"1.5","type":"text","marks":[{"type":"code_inline"}]},{"text":"・","type":"text"},{"text":".inf","type":"text","marks":[{"type":"code_inline"}]},{"text":"・","type":"text"},{"text":"2026-10-24","type":"text","marks":[{"type":"code_inline"}]},{"text":" 等。YAML 1.1 と 1.2 の和集合)を拒否する。 ","type":"text"},{"text":"with","type":"text","marks":[{"type":"code_inline"}]},{"text":" の値は引用符で囲めば文字列として許可する(","type":"text"},{"text":"runner: \"yes\"","type":"text","marks":[{"type":"code_inline"}]},{"text":")。","type":"text"},{"text":"branches","type":"text","marks":[{"type":"code_inline"}]},{"text":" の要素は引用符つきに未対応のため、数字だけ・日付形のブランチ名は許可リスト更新の対象にする。 ","type":"text"},{"text":"concurrency","type":"text","marks":[{"type":"code_inline"}]},{"text":" は任意だが、あるときは ","type":"text"},{"text":"group","type":"text","marks":[{"type":"code_inline"}]},{"text":" を必須とする。ファイル内容は出力しない","type":"text"}]}]}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"tmp=\"$(mktemp -d)\" || exit 1\ntrap 'rm -rf \"${tmp}\"' EXIT\n# latest を commit SHA に解決(取得中にタグが動いても同じ commit を使う)\nsha=\"$(gh api repos/Fandhe-AI/actions/commits/latest --jq .sha)\" || exit 1\nprintf '%s' \"${sha}\" | grep -Eq '^[0-9a-f]{40}

setup-repo-guards 対象リポジトリへ組織標準の CI ガード一式を導入する。導入するのは次の 4 点で、 Step 1 → 4 の順に、各 Step を PR → CI 全 pass → レビュースレッド resolve → squash merge で確定させてから次へ進む。 1. ai-review(PR 自動レビュー workflow の wrapper) 2. AGENTS.md(AI レビューが読むレビュー観点集) 3. CI 必須チェックの集約ジョブ( 等) 4. branch protection ruleset・リポジトリ追加ガード(GitHub API のみ、コミット不要。適用前に退避・差分提示・明示承認を必須とする) この順序を守る理由: 「必須チェックに入れる集約ジョブが先に存在する」「AGENTS.md は PR の base コミット参照のため マージ後の次の PR から実効」という依存関係が、この順で自然に満たされる。 使い方 - 第 1 引数: 対象リポジトリ名( )。許容形式は のみ(Step 0 で検証する) - 第 2 引数: visibility( / の完全一致のみ。Step 0 で検証する)。runner 方針の分岐に使う(public は ubuntu ホステッド、private は self-hosted。ai-rev…

|| { echo \"SHA の形式が不正のため中止\"; exit 1; }\n# gh api はエラー本文も stdout へ出すため、成否は終了コードで判定する\ngh api --method GET -H 'Accept: application/vnd.github.raw' -f ref=\"${sha}\" \\\n repos/Fandhe-AI/actions/contents/ai-review/templates/ai-review.single.yml \\\n > \"${tmp}/ai-review.yml\" || { echo \"取得失敗のため中止\"; exit 1; }\n# 構造検査(不合格なら配置しない。\u003cskill-dir> はこのスキルのディレクトリ)\nbash \u003cskill-dir>/scripts/verify-ai-review-wrapper.sh \"${tmp}/ai-review.yml\" || exit 1\nmkdir -p .github/workflows\ncp \"${tmp}/ai-review.yml\" .github/workflows/ai-review.yml\necho \"取得元: Fandhe-AI/actions@${sha}\"","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"配置後は ","type":"text"},{"text":"cat .github/workflows/ai-review.yml","type":"text","marks":[{"type":"code_inline"}]},{"text":" で全文を目視確認し、コミット本文に ","type":"text"},{"text":"取得元: Fandhe-AI/actions@\u003c上で出力した SHA>","type":"text","marks":[{"type":"code_inline"}]},{"text":" を記録する","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"検査が不合格のときは配置せず、取得内容を目視で確かめる。上流の正当な変更なら、先に ","type":"text"},{"text":"verify-ai-review-wrapper.sh","type":"text","marks":[{"type":"code_inline"}]},{"text":" の許可リストを更新する PR を出す(検査を緩めて通さない)","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"書き方・適用条件(fork PR 拒否・sudo なし runner 等)は Fandhe-AI/actions の ","type":"text"},{"text":"ai-review/README.md","type":"text","marks":[{"type":"code_inline"}]},{"text":" と ","type":"text"},{"text":"ai-review/docs/runner-exception.md","type":"text","marks":[{"type":"code_inline"}]},{"text":"、既存導入リポジトリ(fandhe-frontend 等)の wrapper を参照して揃える","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"runner 方針: CI ジョブは public なら ubuntu ホステッド、private なら self-hosted。 ai-review の review ジョブ(","type":"text"},{"text":"runner","type":"text","marks":[{"type":"code_inline"}]},{"text":"。codex の既定は self-hosted の ","type":"text"},{"text":"codex","type":"text","marks":[{"type":"code_inline"}]},{"text":" ラベル)のみ専用 runner の例外。 ","type":"text"},{"text":"preflight","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"post_feedback","type":"text","marks":[{"type":"code_inline"}]},{"text":"(","type":"text"},{"text":"post-feedback-runner","type":"text","marks":[{"type":"code_inline"}]},{"text":")は資格情報に触れず、既定で可視性に応じて 自動切替する(public は ","type":"text"},{"text":"ubuntu-latest","type":"text","marks":[{"type":"code_inline"}]},{"text":"、private は ","type":"text"},{"text":"self-hosted","type":"text","marks":[{"type":"code_inline"}]},{"text":")ため、明示指定は不要","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"資格情報: ","type":"text"},{"text":"CODEX_HOME_DIR","type":"text","marks":[{"type":"code_inline"}]},{"text":" は org 単位の Actions variable(visibility: all)として設定済みのため、リポジトリには設定しない。 ","type":"text"},{"text":"gh api orgs/Fandhe-AI/actions/variables --jq '.variables[].name'","type":"text","marks":[{"type":"code_inline"}]},{"text":" で存在を確認し、無い場合(他 org 等)は ","type":"text"},{"text":"ai-review/README.md","type":"text","marks":[{"type":"code_inline"}]},{"text":"「セットアップ」手順 3 に従ってリポジトリへ設定する","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"update-external","type":"text","marks":[{"type":"code_inline"}]},{"text":" の日次同期 PR(","type":"text"},{"text":"chore/skills-update-*","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"chore/submodule-update-*","type":"text","marks":[{"type":"code_inline"}]},{"text":")がある場合は ","type":"text"},{"text":"skip-branch-prefixes: chore/skills-update-,chore/submodule-update-","type":"text","marks":[{"type":"code_inline"}]},{"text":" を指定する。push できる主体は ブランチ名で gate を回避できる受容済み残留リスクがある(","type":"text"},{"text":"ai-review/README.md","type":"text","marks":[{"type":"code_inline"}]},{"text":" 「","type":"text"},{"text":"skip-branch-prefixes","type":"text","marks":[{"type":"code_inline"}]},{"text":" の受容済み残留リスク」)ため、","type":"text"},{"text":"指定する前に受容条件を確認する","type":"text","marks":[{"type":"strong"}]},{"text":":","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"確認手順(読み取りのみ。","type":"text"},{"text":"permission","type":"text","marks":[{"type":"code_inline"}]},{"text":" で絞らず全件取得し、各主体の ","type":"text"},{"text":"role_name","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"permissions","type":"text","marks":[{"type":"code_inline"}]},{"text":" で判定する):","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"個人: ","type":"text"},{"text":"gh api \"repos/${repo}/collaborators?affiliation=all\" --paginate --jq '.[] | {login, role_name, permissions}'","type":"text","marks":[{"type":"code_inline"}]},{"text":" で全件取得し、","type":"text"},{"text":"role_name","type":"text","marks":[{"type":"code_inline"}]},{"text":" が ","type":"text"},{"text":"write","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"maintain","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"admin","type":"text","marks":[{"type":"code_inline"}]},{"text":"(または ","type":"text"},{"text":"permissions.push == true","type":"text","marks":[{"type":"code_inline"}]},{"text":")の主体を write 以上とみなす。 ","type":"text"},{"text":"affiliation=all","type":"text","marks":[{"type":"code_inline"}]},{"text":" は組織メンバー・チーム経由の権限も返すため、outside collaborator は ","type":"text"},{"text":"affiliation=outside","type":"text","marks":[{"type":"code_inline"}]},{"text":" で別途列挙して第三者の有無を確認する","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"チーム: ","type":"text"},{"text":"gh api \"repos/${repo}/teams\" --paginate --jq '.[] | {slug, permission}'","type":"text","marks":[{"type":"code_inline"}]},{"text":" で、 ","type":"text"},{"text":"permission","type":"text","marks":[{"type":"code_inline"}]},{"text":" が ","type":"text"},{"text":"push","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"maintain","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"admin","type":"text","marks":[{"type":"code_inline"}]},{"text":" のチームを列挙し、メンバーが信頼済み主体だけか確認する","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"fork 経路: ","type":"text"},{"text":"gh api \"repos/${repo}\" --jq '{fork, allow_forking}'","type":"text","marks":[{"type":"code_inline"}]},{"text":" と ","type":"text"},{"text":"gh api \"repos/${repo}/forks\" --paginate --jq '.[].full_name'","type":"text","marks":[{"type":"code_inline"}]},{"text":" で外部 fork の有無を確認する","type":"text"}]}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"受容してよい条件(すべて満たすこと): write 以上の権限者が org 内の信頼済み主体に限られる/ skip 対象の prefix が ","type":"text"},{"text":"chore/skills-update-","type":"text","marks":[{"type":"code_inline"}]},{"text":" と ","type":"text"},{"text":"chore/submodule-update-","type":"text","marks":[{"type":"code_inline"}]},{"text":" に限られる/ 該当ブランチの PR が update-external の自動生成に限られる","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"受容しない条件(1 つでも当たれば指定しない): 第三者の write コラボレーターや outside collaborator がいる/ fork からの外部 push 経路がある/確認結果を取得できない(判定不能は受容に倒さない)","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"受容しない場合の代替: ","type":"text"},{"text":"skip-branch-prefixes","type":"text","marks":[{"type":"code_inline"}]},{"text":" を指定せず、同期 PR にも通常どおり ai-review を走らせる。 同期 PR の負荷が問題なら別対策として Issue 化をユーザーに提案する(ruleset のブランチ名ルール制限は 本スキルの範囲外。提案にとどめる)","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"確認結果と判断(受容した/しなかった、根拠)を完了報告に 1 行で記録する","type":"text"}]}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"collaborators-only","type":"text","marks":[{"type":"code_inline"}]},{"text":"(write 未満の actor のイベントでは ","type":"text"},{"text":"preflight","type":"text","marks":[{"type":"code_inline"}]},{"text":" を失敗させる)は既定の ","type":"text"},{"text":"true","type":"text","marks":[{"type":"code_inline"}]},{"text":" のまま使う","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"資格情報が使えない間は review ジョブが ","type":"text"},{"text":"skipped","type":"text","marks":[{"type":"code_inline"}]},{"text":" になり、workflow 全体では成功に見える。 導入 PR では必ずジョブ単位で ","type":"text"},{"text":"review","type":"text","marks":[{"type":"code_inline"}]},{"text":" が実行されたことを確認する(下記「検証」)","type":"text"}]}]}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Step 2: AGENTS.md(AI レビューの観点集)の追加","type":"text"}]},{"type":"paragraph","content":[{"text":"ai-review は ","type":"text"},{"text":"PR の base コミットの AGENTS.md","type":"text","marks":[{"type":"strong"}]},{"text":" をリポジトリ固有のレビュー基準として読む。 リポジトリルートに日本語で新規作成する(既存があれば基準を弱めず不足観点を追記する)。","type":"text"}]},{"type":"paragraph","content":[{"text":"必須 3 観点を、必ず","type":"text"},{"text":"対象リポジトリ固有の具体項目","type":"text","marks":[{"type":"strong"}]},{"text":"に落とし込んで書く(別リポジトリ用の記述を流用しない):","type":"text"}]},{"type":"ordered_list","attrs":{"order":1,"listStyle":"number"},"content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"セキュリティ観点","type":"text","marks":[{"type":"strong"}]},{"text":": 秘密情報混入・インジェクション・依存監査・権限最小化など、リポジトリの実態に即して","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"アーキテクチャ・設計整合の観点","type":"text","marks":[{"type":"strong"}]},{"text":": CLAUDE.md・設計文書の責務境界・規約整合","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"再利用・アセット化の観点","type":"text","marks":[{"type":"strong"}]},{"text":": 汎用実装の分離・ハードコード回避・転用容易性・ドキュメント整備","type":"text"}]}]}]},{"type":"paragraph","content":[{"text":"加えて:","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"CLAUDE.md / ","type":"text"},{"text":".claude/rules/","type":"text","marks":[{"type":"code_inline"}]},{"text":" / 設計文書から抽出したリポジトリ固有観点を整理する","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"重大度区分 P0(マージブロック)/ P1(強く推奨)/ P2(提案)を付け、","type":"text"},{"text":"P1 の定義は CI ゲートの実挙動 (ai-review の ","type":"text","marks":[{"type":"strong"}]},{"text":"block-priorities","type":"text","marks":[{"type":"code_inline"},{"type":"strong"}]},{"text":" 既定 ","type":"text","marks":[{"type":"strong"}]},{"text":"P0,P1","type":"text","marks":[{"type":"code_inline"},{"type":"strong"}]},{"text":" により review ジョブは P1 でも fail する)と矛盾させない","type":"text","marks":[{"type":"strong"}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"カスタム prompt(","type":"text"},{"text":".github/ai-review/prompts/review.md","type":"text","marks":[{"type":"code_inline"}]},{"text":")が既にある場合は二重管理を避ける: 観点の正は AGENTS.md、enforcement(完了判定・機械格上げ)の正は review.md という役割分担にし、 review.md へは AGENTS.md 読み取りの最小追記のみ行う。旧パス ","type":"text"},{"text":".github/codex/prompts/review.md","type":"text","marks":[{"type":"code_inline"}]},{"text":" に 置いたままの場合は ","type":"text"},{"text":".github/ai-review/prompts/review.md","type":"text","marks":[{"type":"code_inline"}]},{"text":" へ移す(または ","type":"text"},{"text":"prompt-path","type":"text","marks":[{"type":"code_inline"}]},{"text":" で旧パスを明示する)","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"参考実例: fandhe-backend / fandhe-frontend / Fandhe-AI/actions / agent-cli-skills の AGENTS.md","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"注意: 新基準はマージ後の","type":"text"},{"text":"次の PR から","type":"text","marks":[{"type":"strong"}]},{"text":"実効(base 参照仕様)","type":"text"}]}]}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Step 3: CI 必須チェックの集約ジョブ整備","type":"text"}]},{"type":"paragraph","content":[{"text":"複数ジョブを持つ workflow には集約ジョブ(","type":"text"},{"text":"ci-complete","type":"text","marks":[{"type":"code_inline"}]},{"text":" 等)を追加する: 全ジョブを ","type":"text"},{"text":"needs:","type":"text","marks":[{"type":"code_inline"}]},{"text":" に列挙し、","type":"text"},{"text":"if: always()","type":"text","marks":[{"type":"code_inline"}]},{"text":" を付け、","type":"text"},{"text":"toJSON(needs)","type":"text","marks":[{"type":"code_inline"}]},{"text":" を jq で判定する。","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"fail-closed を厳守","type":"text","marks":[{"type":"strong"}]},{"text":": ","type":"text"},{"text":"skipped","type":"text","marks":[{"type":"code_inline"}]},{"text":" の許容は「条件付きジョブ(","type":"text"},{"text":"if:","type":"text","marks":[{"type":"code_inline"}]},{"text":" を持つジョブ)」の","type":"text"},{"text":"明示リストに限定","type":"text","marks":[{"type":"strong"}]},{"text":"し、 他ジョブは success のみ受理する(skipped 全許容は fail-open として AI レビューに P1 指摘される)。 change detection ゲートがある場合はゲート出力に基づき「skip が正しい状況」でのみ skipped を受理する","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"集約ジョブには ","type":"text"},{"text":"permissions: {}","type":"text","marks":[{"type":"code_inline"}]},{"text":" を付け、「ジョブ追加時は needs への追加が必要」のコメントを書く","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Fandhe-AI/actions の reusable workflow(lint-docs 等)を使っている場合、集約ジョブ (","type":"text"},{"text":"lint-docs / lint-docs-complete","type":"text","marks":[{"type":"code_inline"}]},{"text":" 等)が入った SHA まで参照をバンプすればチェック 1 件に集約できる","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"YAML の ","type":"text"},{"text":"name:","type":"text","marks":[{"type":"code_inline"}]},{"text":" に ","type":"text"},{"text":" #","type":"text","marks":[{"type":"code_inline"}]},{"text":" や「: 」を含む場合は必ずクォートする(未クォートだと API 報告名が途中で切れ、 必須チェック名として不安定になる)","type":"text"}]}]}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Step 4: branch protection / リポジトリ設定(GitHub API のみ、コミット不要)","type":"text"}]},{"type":"paragraph","content":[{"text":"Step 4 の書き込み系呼び出し(PATCH / PUT / POST / DELETE)は、","type":"text"},{"text":"4.1 退避 → 4.2 差分提示と明示承認 → 4.3 適用","type":"text","marks":[{"type":"strong"}]},{"text":" の順を必ず踏む。承認が得られない場合は何も変更せず中止する(fail-closed)。誤適用したときは 4.4 で戻す。 ","type":"text"},{"text":"PUT /rulesets/{id}","type":"text","marks":[{"type":"code_inline"}]},{"text":" は ","type":"text"},{"text":"required_status_checks.parameters","type":"text","marks":[{"type":"code_inline"}]},{"text":" を丸ごと置換し、","type":"text"},{"text":"integration_id","type":"text","marks":[{"type":"code_inline"}]},{"text":" 束縛などが 黙って脱落し得るため、現在値の保存と差分の確認なしに送らない。","type":"text"}]},{"type":"heading","attrs":{"level":4},"content":[{"text":"4.1 退避(変更前の現在値を作業ツリーの外へ保存)","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"保存先は ","type":"text"},{"text":"mktemp -d","type":"text","marks":[{"type":"code_inline"}]},{"text":" が返す絶対パスのディレクトリにする。対象リポジトリの作業ツリー内・","type":"text"},{"text":".claude/","type":"text","marks":[{"type":"code_inline"}]},{"text":" 配下には 置かない(誤コミット防止)。","type":"text"},{"text":"chmod 700","type":"text","marks":[{"type":"code_inline"}]},{"text":" を付け、パスを利用者へ表示して復旧まで保持する旨を伝える。 複数リポジトリの一括適用ではリポジトリごとに別ディレクトリにする","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"リポジトリ名は ","type":"text"},{"text":"owner/repo","type":"text","marks":[{"type":"code_inline"}]},{"text":" 形式を検証してから使う","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"退避対象(取得は 1 件ごとに成否を確認し、存在判定は終了コードではなく HTTP status で行う。","type":"text"},{"text":"gh api","type":"text","marks":[{"type":"code_inline"}]},{"text":" のエラー本文は stdout に出る):","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"全 branch ruleset: 一覧と、各 ruleset の詳細 JSON(","type":"text"},{"text":"Repository","type":"text","marks":[{"type":"code_inline"}]},{"text":" は ","type":"text"},{"text":"repos/{repo}/rulesets/{id}","type":"text","marks":[{"type":"code_inline"}]},{"text":"、","type":"text"},{"text":"Organization","type":"text","marks":[{"type":"code_inline"}]},{"text":" は 読み取りのみ。org 継承 ruleset はリポジトリ側 API で変更しない)。tag ruleset を扱う場合はその一覧と詳細も","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"リポジトリ本体: ","type":"text"},{"text":"GET repos/{repo}","type":"text","marks":[{"type":"code_inline"}]},{"text":" の ","type":"text"},{"text":"allow_squash_merge","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"allow_merge_commit","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"allow_rebase_merge","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"delete_branch_on_merge","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"allow_auto_merge","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"squash_merge_commit_title","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"squash_merge_commit_message","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"has_wiki","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"has_projects","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"allow_forking","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"security_and_analysis","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Actions 権限: ","type":"text"},{"text":"GET repos/{repo}/actions/permissions","type":"text","marks":[{"type":"code_inline"}]},{"text":" と ","type":"text"},{"text":"GET repos/{repo}/actions/permissions/workflow","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Dependabot alerts(","type":"text"},{"text":"vulnerability-alerts","type":"text","marks":[{"type":"code_inline"}]},{"text":"): HTTP status で有効 / 無効を記録する(204 = 有効 / 404 = 無効)","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"automated security fixes・private vulnerability reporting: 有効でも無効でも 200 と ","type":"text"},{"text":"{\"enabled\": bool}","type":"text","marks":[{"type":"code_inline"}]},{"text":" を返すため、 status では判別できない。本文の ","type":"text"},{"text":"enabled","type":"text","marks":[{"type":"code_inline"}]},{"text":" を JSON として退避する","type":"text"}]}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"取得失敗(403 / 404 以外の異常を含む)は「未取得」と明示し、未取得の項目は変更しない。取得失敗を「現在値なし」に倒して 新規作成・上書きしない(判定不能を green に倒さない方針と同じ)","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"退避の完了条件は「ruleset 一覧を全ページ取得(","type":"text"},{"text":"--paginate --slurp","type":"text","marks":[{"type":"code_inline"}]},{"text":" + ","type":"text"},{"text":"jq 'add'","type":"text","marks":[{"type":"code_inline"}]},{"text":")して単一配列に統合できた」かつ「取得に成功した全 ","type":"text"},{"text":"*.json","type":"text","marks":[{"type":"code_inline"}]},{"text":" が ","type":"text"},{"text":"jq -e .","type":"text","marks":[{"type":"code_inline"}]},{"text":" で JSON として妥当」かつ「ruleset 一覧にある Repository ruleset の全 id の詳細が退避済み」であること。未取得 id が 1 件でもあれば列挙して 4.2 へ進まない(ゲート。その ruleset は変更しない)。取得失敗時は壊れたファイル(空・エラー本文)を残さず、そのファイルは「未取得」として扱う(4.2 の差分・4.4 の復旧の入力に使わない)","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Bash ツールは呼び出しをまたいでシェル変数を保持しない。4.1 から 4.4 を複数回の呼び出しに分けるときは、各呼び出しの先頭で ","type":"text"},{"text":"repo","type":"text","marks":[{"type":"code_inline"}]},{"text":" と ","type":"text"},{"text":"backup","type":"text","marks":[{"type":"code_inline"}]},{"text":"(4.1 で表示した絶対パス)を再束縛し、","type":"text"},{"text":"[ -d \"${backup}\" ]","type":"text","marks":[{"type":"code_inline"}]},{"text":" を確認してから使う(空の変数のまま実行しない)","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"退避した JSON の件数と sha256 を表示し、保存を確認してから 4.2 へ進む。保存先のパスと内容は PR 本文・コミット・ログへ貼らない(要約のみ)","type":"text"}]}]}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"repo=\"Fandhe-AI/\u003cREPO>\"\ncase \"${repo}\" in */*) ;; *) echo \"repo は owner/repo 形式\" >&2; exit 1 ;; esac\nbackup=\"$(mktemp -d)\" && chmod 700 \"${backup}\" && echo \"退避先: ${backup}\"\n# gh api は HTTP エラー本文を stdout に出すため、直接リダイレクトせず一時ファイルへ書く。\n# 成功(終了コード 0)かつ jq -e . で JSON として妥当なときだけ mv し、失敗時は一時ファイルを消して退避ファイルを残さない\nfetch_json() { # fetch_json \u003c出力ファイル名> \u003cgh api の引数...>\n out=\"${backup}/$1\"; shift\n if gh api \"$@\" > \"${out}.tmp\" 2>/dev/null && jq -e . \"${out}.tmp\" > /dev/null 2>&1; then\n mv \"${out}.tmp\" \"${out}\"\n else\n rm -f \"${out}.tmp\"; echo \"未取得: ${out##*/}(変更しない)\" >&2; return 1\n fi\n}\n# ruleset 一覧は既定では最初のページしか返らない。--paginate --slurp で全ページを配列の配列として取り、jq 'add' で単一配列へ統合する。\n# gh の終了コードは jq と切り離して独立に確認する(パイプだと pipefail 無しでは末尾の jq の終了コードしか見えず、gh が失敗して無出力でも空の一覧が保存される)。\n# 1) gh の出力を一時ファイルへ取得し、gh が 0 以外・空出力なら未取得として止める\n# 2) 取得した JSON を統合し、配列であることを検証できたときだけ rulesets.json として確定する\n# 後段の詳細取得と完了判定は、この統合済みの rulesets.json だけを使う\nraw=\"${backup}/rulesets.raw.tmp\"\nif gh api --paginate --slurp \"repos/${repo}/rulesets\" > \"${raw}\" 2>/dev/null \\\n && [ -s \"${raw}\" ] \\\n && jq -e 'add // [] | if type == \"array\" then . else error(\"not array\") end' \"${raw}\" > \"${backup}/rulesets.json.tmp\" 2>/dev/null; then\n mv \"${backup}/rulesets.json.tmp\" \"${backup}/rulesets.json\"\n rm -f \"${raw}\"\nelse\n rm -f \"${raw}\" \"${backup}/rulesets.json.tmp\"; echo \"退避失敗: ruleset 一覧(全ページ取得不可)\" >&2; exit 1\nfi\necho \"ruleset 一覧: $(jq 'length' \"${backup}/rulesets.json\") 件(0 件なら新規作成のみ。既存を変更する前提にしない)\"\njq -r '.[] | select(.source_type == \"Repository\") | .id' \"${backup}/rulesets.json\" |\nwhile read -r id; do\n fetch_json \"ruleset-${id}.json\" \"repos/${repo}/rulesets/${id}\"\ndone\n# ゲート: 一覧にある Repository ruleset の詳細が全件退避されたこと(未取得 id があれば 4.2 へ進まない)\nmissing=0\nfor id in $(jq -r '.[] | select(.source_type == \"Repository\") | .id' \"${backup}/rulesets.json\"); do\n [ -s \"${backup}/ruleset-${id}.json\" ] || { echo \"未退避の ruleset id: ${id}(変更しない)\" >&2; missing=1; }\ndone\n[ \"${missing}\" -eq 0 ] || { echo \"退避未完了: 4.2 へ進まない\" >&2; exit 1; }\nfetch_json repo.json \"repos/${repo}\" --jq '{allow_squash_merge, allow_merge_commit, allow_rebase_merge, delete_branch_on_merge, allow_auto_merge, squash_merge_commit_title, squash_merge_commit_message, has_wiki, has_projects, allow_forking, security_and_analysis}'\nfetch_json actions-workflow.json \"repos/${repo}/actions/permissions/workflow\"\nfetch_json actions.json \"repos/${repo}/actions/permissions\"\ncode=\"$(gh api -i \"repos/${repo}/vulnerability-alerts\" 2>/dev/null | awk 'NR==1{print $2}')\"\nprintf '%s\\n' \"${code:-unknown}\" > \"${backup}/vulnerability-alerts.status\" # 204 = 有効 / 404 = 無効 / 他 = 判定不能(変更しない)\n# この 2 つは status ではなく本文の {\"enabled\": bool} で判別する\nfetch_json automated-security-fixes.json \"repos/${repo}/automated-security-fixes\"\nfetch_json private-vulnerability-reporting.json \"repos/${repo}/private-vulnerability-reporting\"\n# 完了条件: 退避された *.json がすべて JSON として妥当であること\nfor f in \"${backup}\"/*.json; do jq -e . \"${f}\" > /dev/null || echo \"不正な退避: ${f##*/}(変更しない)\" >&2; done\n(cd \"${backup}\" && ls -1 && shasum -a 256 -- * )","type":"text"}]},{"type":"heading","attrs":{"level":4},"content":[{"text":"4.2 差分提示と明示承認","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"適用予定の本文(PUT / PATCH / POST の body)を退避先と同じディレクトリの一時ファイルに作り、現在値との差分を ","type":"text"},{"text":"jq -S","type":"text","marks":[{"type":"code_inline"}]},{"text":" で正規化した JSON 同士の ","type":"text"},{"text":"diff","type":"text","marks":[{"type":"code_inline"}]},{"text":" で提示する(キー順の差を無視する)","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"最低限、次の増減を明示する: ","type":"text"},{"text":"required_status_checks","type":"text","marks":[{"type":"code_inline"}]},{"text":" の ","type":"text"},{"text":"context","type":"text","marks":[{"type":"code_inline"}]},{"text":" 一覧と各エントリの ","type":"text"},{"text":"integration_id","type":"text","marks":[{"type":"code_inline"}]},{"text":" の有無(脱落)/ ","type":"text"},{"text":"strict_required_status_checks_policy","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"bypass_actors","type":"text","marks":[{"type":"code_inline"}]},{"text":" の件数 / ","type":"text"},{"text":"enforcement","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"required_review_thread_resolution","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"allowed_merge_methods","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"新規作成(POST)で現在値が無い場合は「新規作成」と明記し、復旧は DELETE であることを併記する","type":"text"}]}]}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"diff \u003c(jq -S . \"${backup}/ruleset-\u003cID>.json\") \u003c(jq -S . \"${backup}/ruleset-\u003cID>.new.json\")\n# integration_id の脱落を直接確認(現在値で束縛済みの context のうち、新 body で束縛が無くなったものを列挙)\njq -rn --slurpfile a \"${backup}/ruleset-\u003cID>.json\" --slurpfile b \"${backup}/ruleset-\u003cID>.new.json\" '\n def bound($x): [$x.rules[]? | select(.type==\"required_status_checks\") | .parameters.required_status_checks[]? | select(.integration_id != null) | .context];\n (bound($a[0]) - bound($b[0]))[]'","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"利用者へ「適用してよいか」を尋ね、肯定の明示返答を得てから 4.3 へ進む。無返答・曖昧な返答・質問できない自動運転環境では 適用せず、差分と退避先を報告して停止する","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"承認はそのリポジトリの、その差分 1 回分に限る。差分が変わったら再提示して再承認を得る。一括適用でもリポジトリごとに承認を取る","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"承認を事実以上に記述しない。得ていない承認を完了報告・PR 本文に「承認済み」と書かない","type":"text"}]}]}]},{"type":"heading","attrs":{"level":4},"content":[{"text":"4.3 適用(4.2 で承認された差分の範囲でのみ実行)","type":"text"}]},{"type":"paragraph","content":[{"text":"ruleset を更新する PUT は、4.1 で退避した JSON の ","type":"text"},{"text":"required_status_checks","type":"text","marks":[{"type":"code_inline"}]},{"text":" 等の必要箇所だけを差し替えた body を ","type":"text"},{"text":"--input \u003cfile>","type":"text","marks":[{"type":"code_inline"}]},{"text":" で送る(丸ごと新規構築しない・本文を文字列連結しない)。適用後は直ちに下記「検証」の Step 4-a / 4-b スイープを実行し、崩れていたら 4.4 で戻す。新規作成した ruleset の id・変更項目一覧・退避先パスを 完了報告に記録する。","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"マージ設定(","type":"text"},{"text":"gh api -X PATCH repos/...","type":"text","marks":[{"type":"code_inline"}]},{"text":"): squash のみ(merge commit / rebase 禁止)、 マージ後ブランチ自動削除、auto-merge 許可、squash タイトル = PR_TITLE / 本文 = PR_BODY","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"ruleset ","type":"text"},{"text":"main-protection","type":"text","marks":[{"type":"code_inline"}]},{"text":"(target: branch、","type":"text"},{"text":"~DEFAULT_BRANCH","type":"text","marks":[{"type":"code_inline"}]},{"text":"、enforcement: active、bypass_actors 空):","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"deletion 禁止 / non_fast_forward(force push 禁止)","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"pull_request: required_approving_review_count 0(人間 approver 不在の AI 運用。レビューゲートは ai-review の必須チェックで担保)・required_review_thread_resolution true・allowed_merge_methods [\"squash\"]","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"0 承認では ai-review の必須チェックが唯一のレビューゲートになる","type":"text","marks":[{"type":"strong"}]},{"text":"。これは設計上の受容事項であり、 次の条件をすべて満たすリポジトリでのみ適用する(strict false の規定とは独立の論点): 人間 approver 不在の運用を意図している/必須チェックに ","type":"text"},{"text":"preflight","type":"text","marks":[{"type":"code_inline"}]},{"text":"・","type":"text"},{"text":"review","type":"text","marks":[{"type":"code_inline"}]},{"text":"・","type":"text"},{"text":"post_feedback","type":"text","marks":[{"type":"code_inline"}]},{"text":" (または ","type":"text"},{"text":"ai-review-gate","type":"text","marks":[{"type":"code_inline"}]},{"text":")が入っている/","type":"text"},{"text":"collaborators-only","type":"text","marks":[{"type":"code_inline"}]},{"text":" が既定の ","type":"text"},{"text":"true","type":"text","marks":[{"type":"code_inline"}]},{"text":"/bypass_actors が空","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"適用後(下記「検証」)に ","type":"text"},{"text":"review","type":"text","marks":[{"type":"code_inline"}]},{"text":" ジョブが実際に実行されたことを確認する(適用前には確認できないため、 条件ではなく適用後の事後確認として扱う)。確認できない、または skipped のままなら、受容条件を満たさない ものとして ","type":"text"},{"text":"required_approving_review_count","type":"text","marks":[{"type":"code_inline"}]},{"text":" を 1 以上へ是正する。","type":"text"},{"text":"この是正も ruleset の書き込みであり、 4.4(復旧)とは別物","type":"text","marks":[{"type":"strong"}]},{"text":"(4.4 は退避した元の値へ戻す操作で、既存の 0 承認 ruleset では 0 に戻るだけで是正にならない)。 したがって 4.1(是正前の現在値を再度退避)→ 4.2(","type":"text"},{"text":"jq -S","type":"text","marks":[{"type":"code_inline"}]},{"text":" 差分の提示と明示承認)→ 4.3(GET した ruleset JSON の ","type":"text"},{"text":"pull_request","type":"text","marks":[{"type":"code_inline"}]},{"text":" ルールの ","type":"text"},{"text":"required_approving_review_count","type":"text","marks":[{"type":"code_inline"}]},{"text":" だけを差し替えて ","type":"text"},{"text":"--input \u003cfile>","type":"text","marks":[{"type":"code_inline"}]},{"text":" で PUT する。 ","type":"text"},{"text":"required_status_checks","type":"text","marks":[{"type":"code_inline"}]},{"text":" の ","type":"text"},{"text":"integration_id","type":"text","marks":[{"type":"code_inline"}]},{"text":" 束縛を含む他フィールドを保持し、素朴な PUT をしない)の順に通し、 適用後に ","type":"text"},{"text":".claude/rules/ruleset-policy.md","type":"text","marks":[{"type":"code_inline"}]},{"text":" の 3 軸 + classic BP 検証(下記 Step 4-a / 4-b)を実行する。 承認が得られない場合は書き込まず、受容不可として完了報告に記録して停止する 適用から確認までの間は ","type":"text"},{"text":"review","type":"text","marks":[{"type":"code_inline"}]},{"text":" ジョブがまだ動いておらず、レビューゲートなしでマージできる窓が残る。 この間は PR をマージしない(確認を済ませるまで自動マージも走らせない)","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"条件を満たさない場合の代替: ","type":"text"},{"text":"required_approving_review_count","type":"text","marks":[{"type":"code_inline"}]},{"text":" を 1 以上にする(可能なら CODEOWNERS と併用)。 approver がいない単独運用ではマージが止まり、人間 approve を前提としない ","type":"text"},{"text":"implement-issue-tree","type":"text","marks":[{"type":"code_inline"}]},{"text":" の 自動マージも止まり得るため、運用者に確認してから設定する","type":"text"}]}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"required_status_checks(","type":"text"},{"text":"strict false は必須","type":"text","marks":[{"type":"strong"}]},{"text":"。true にすると 1 件マージするたびに他の open PR の base が陳腐化し、implement-issue-tree の並列ランが収束しなくなる。strict は鮮度制御であって bypass 不能性の制御ではないため、false でもクライアント側自動マージの G0 は通過する。 実行環境に ","type":"text"},{"text":".claude/rules/ruleset-policy.md","type":"text","marks":[{"type":"code_inline"}]},{"text":" が存在する場合はそちらの詳細に従う): 必須集合は ","type":"text"},{"text":"autoMerge","type":"text","marks":[{"type":"code_inline"}]},{"text":" を使うかどうかで変わる(下記「必須チェック選定の注意」参照)。使わない場合は ","type":"text"},{"text":"[\u003c集約ジョブ>, (集約できない別 workflow の常時チェック), ai-review の 3 チェック]","type":"text","marks":[{"type":"strong"}]},{"text":" の最小集合、 使う場合は PR HEAD へ報告される全 context。ai-review の 3 チェックはジョブ ID が先頭になる ","type":"text"},{"text":"\u003cjob> / preflight","type":"text","marks":[{"type":"code_inline"}]},{"text":"・","type":"text"},{"text":"\u003cjob> / review","type":"text","marks":[{"type":"code_inline"}]},{"text":"・","type":"text"},{"text":"\u003cjob> / post_feedback","type":"text","marks":[{"type":"code_inline"}]},{"text":"(テンプレート既定なら ","type":"text"},{"text":"codex / preflight","type":"text","marks":[{"type":"code_inline"}]},{"text":" 等。 複数モデル構成ではテンプレート同梱の集約 gate ","type":"text"},{"text":"ai-review-gate","type":"text","marks":[{"type":"code_inline"}]},{"text":" 1 件でもよい)。個別ジョブを登録する場合は ","type":"text"},{"text":"preflight","type":"text","marks":[{"type":"code_inline"},{"type":"strong"}]},{"text":" を必ず含める","type":"text","marks":[{"type":"strong"}]},{"text":": actor 判定等で ","type":"text"},{"text":"preflight","type":"text","marks":[{"type":"code_inline"}]},{"text":" が失敗すると後続が skipped になり、skipped は required check を満たしてしまう(","type":"text"},{"text":"ai-review-gate","type":"text","marks":[{"type":"code_inline"}]},{"text":" は reviewer の ","type":"text"},{"text":"preflight","type":"text","marks":[{"type":"code_inline"}]},{"text":" 失敗で自身も失敗するため、 gate 1 件で登録する場合は個別の ","type":"text"},{"text":"preflight","type":"text","marks":[{"type":"code_inline"}]},{"text":" を追加しなくてよい)","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"自動マージ(implement-issue-tree の ","type":"text"},{"text":"autoMerge: true","type":"text","marks":[{"type":"code_inline"}]},{"text":")を使う場合、required_status_checks の 各エントリに発行元 App の ","type":"text"},{"text":"integration_id","type":"text","marks":[{"type":"code_inline"}]},{"text":" を束縛する(未束縛だと G0 が ","type":"text"},{"text":"issuer-unbound","type":"text","marks":[{"type":"code_inline"}]},{"text":" で辞退する)。 ","type":"text"},{"text":"ruleset を PUT で更新した後は必ず下記「検証」の 3 軸スイープを実行する","type":"text","marks":[{"type":"strong"}]},{"text":"(","type":"text"},{"text":"PUT","type":"text","marks":[{"type":"code_inline"}]},{"text":" は ","type":"text"},{"text":"required_status_checks.parameters","type":"text","marks":[{"type":"code_inline"}]},{"text":" を丸ごと置換するため束縛が落ち得る。複数リポへ一括適用する 場合は全リポで実行する)","type":"text"}]}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"必須チェック選定の注意(重要な落とし穴):","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"直近 PR の check runs で「","type":"text"},{"text":"常に報告される","type":"text","marks":[{"type":"strong"}]},{"text":"」チェックのみ選ぶ。workflow レベル paths フィルタで 実行されないことがあるチェックは入れない(マージが永久ブロックされる)。ジョブレベル条件の skipped は可","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"autoMerge を使わない場合","type":"text","marks":[{"type":"strong"}]},{"text":": Cursor Bugbot 等の外部アプリは必須にしない。","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"autoMerge: true","type":"text","marks":[{"type":"code_inline"},{"type":"strong"}]},{"text":"(implement-issue-tree)を使う場合","type":"text","marks":[{"type":"strong"}]},{"text":": 上記の除外は適用しない。 ","type":"text"},{"text":"implement-issue-tree","type":"text","marks":[{"type":"code_inline"}]},{"text":" の autoMerge G0 は「PR HEAD 上の check-run / commit status のうち required_status_checks に含まれない context が 0 件であること」を合格条件にする (","type":"text"},{"text":"skills/implement-issue-tree/references/automerge-design.md","type":"text","marks":[{"type":"code_inline"}]},{"text":" の G0 (d)/(f)/(g))。 外部アプリを required から外すと、その context が G0 (f) に「required でない context」として 検出され ","type":"text"},{"text":"server-enforcement-missing","type":"text","marks":[{"type":"code_inline"}]},{"text":" で辞退し、自動マージが構造的に成立しない。 ","type":"text"},{"text":"args.externalChecks","type":"text","marks":[{"type":"code_inline"}]},{"text":" で宣言した外部 App の context と ai-review workflow の全ジョブ (","type":"text"},{"text":"preflight","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"review","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"post_feedback","type":"text","marks":[{"type":"code_inline"}]},{"text":")を含め、","type":"text"},{"text":"PR HEAD へ報告される全 context","type":"text","marks":[{"type":"strong"}]},{"text":" を required に列挙し、外部 App の required エントリは発行元 App の ID(ruleset は ","type":"text"},{"text":"integration_id","type":"text","marks":[{"type":"code_inline"}]},{"text":")で束縛する。commit status は App 束縛(","type":"text"},{"text":"integration_id","type":"text","marks":[{"type":"code_inline"}]},{"text":")を持てないため、 束縛不能な commit-status 専用チェックしか発行しない外部プロバイダは autoMerge と併用できない (G0 (g) の帰結)。paths フィルタで実行されないことがあるチェックを必須化できない制約と 両立させるには、条件は workflow の ","type":"text"},{"text":"on.pull_request.paths","type":"text","marks":[{"type":"code_inline"}]},{"text":" ではなくジョブレベル ","type":"text"},{"text":"if:","type":"text","marks":[{"type":"code_inline"}]},{"text":" に寄せ、 check-run 自体は常に報告させる(skipped も可)実装パターンを使う(実例: ","type":"text"},{"text":"Fandhe-AI/fandhe-frontend","type":"text","marks":[{"type":"code_inline"}]},{"text":" の ","type":"text"},{"text":"musl-smoke.yml","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"image-size.yml","type":"text","marks":[{"type":"code_inline"}]},{"text":" の「Detect relevant changes」 ステップ。同リポの ","type":"text"},{"text":".claude/rules/ci.md","type":"text","marks":[{"type":"code_inline"}]},{"text":"「","type":"text"},{"text":"ci-complete","type":"text","marks":[{"type":"code_inline"}]},{"text":" 集約ジョブと ruleset 必須チェック」節 〔イシュー #2325〕も、外部チェックを required から外さない運用を採用している)。","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"チェック名が変わる PR をマージするときは、マージ前に ruleset を新チェック名へ PUT で置換する","type":"text","marks":[{"type":"strong"}]},{"text":" (旧名のままだと CI 全 pass でも「Expected」のまま BLOCKED になる)。PUT は GET した JSON の required_status_checks のみ差し替えて送る","type":"text"}]}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"追加ガード: secret scanning + push protection / Dependabot alerts + automated security fixes / (public なら)private vulnerability reporting / タグ運用があれば tag ruleset(deletion・non_fast_forward)/ GITHUB_TOKEN 既定権限 read 化(","type":"text"},{"text":"全 workflow の permissions: 明示を確認してから","type":"text","marks":[{"type":"strong"}]},{"text":")/ Actions の PR 作成・承認許可は自動 PR 運用(update-external 等)が無ければ false / (private なら)forking 禁止 / 未使用の wiki・projects 無効化","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"プラン制限(403/422)に当たった項目は「ユーザー操作が必要」として記録し、他を続行する","type":"text"}]}]}]},{"type":"heading","attrs":{"level":4},"content":[{"text":"4.4 復旧(退避した JSON で元に戻す)","type":"text"}]},{"type":"paragraph","content":[{"text":"復旧自体も書き込みなので、実行前に復旧内容を利用者へ提示して承認を得る。復旧に失敗した項目は握りつぶさず報告する。","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"既存 ruleset の復旧: 退避した詳細 JSON から PUT 可能なフィールド(","type":"text"},{"text":"name","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"target","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"enforcement","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"bypass_actors","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"conditions","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"rules","type":"text","marks":[{"type":"code_inline"}]},{"text":")だけを抽出し、","type":"text"},{"text":"PUT repos/{repo}/rulesets/{id}","type":"text","marks":[{"type":"code_inline"}]},{"text":" へ ","type":"text"},{"text":"--input","type":"text","marks":[{"type":"code_inline"}]},{"text":" で送る。読み取り専用フィールド (","type":"text"},{"text":"id","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"source","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"source_type","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"node_id","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"_links","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"created_at","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"updated_at","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"current_user_can_bypass","type":"text","marks":[{"type":"code_inline"}]},{"text":")は送らない。 復旧後は 3 軸スイープで退避時点と同じ値(strict / bypass / ","type":"text"},{"text":"integration_id","type":"text","marks":[{"type":"code_inline"}]},{"text":" / total)に戻ったことを実測する","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"新規作成した ruleset の復旧: 記録した id のみ ","type":"text"},{"text":"DELETE repos/{repo}/rulesets/{id}","type":"text","marks":[{"type":"code_inline"}]},{"text":"(他の ruleset は触らない)","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"リポジトリ本体: 退避した値のうち変更した項目だけを ","type":"text"},{"text":"PATCH repos/{repo}","type":"text","marks":[{"type":"code_inline"}]},{"text":" で戻す","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"追加ガード: 退避時の状態へ対応する API で戻す(Dependabot alerts / automated security fixes / private vulnerability reporting は ","type":"text"},{"text":"PUT","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"DELETE","type":"text","marks":[{"type":"code_inline"}]},{"text":"、Actions 権限は ","type":"text"},{"text":"PUT actions/permissions/workflow","type":"text","marks":[{"type":"code_inline"}]},{"text":"、secret scanning / push protection は ","type":"text"},{"text":"security_and_analysis","type":"text","marks":[{"type":"code_inline"}]},{"text":" の PATCH)","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"退避ディレクトリは復旧不要と確認できるまで削除しない。削除は自分が ","type":"text"},{"text":"mktemp -d","type":"text","marks":[{"type":"code_inline"}]},{"text":" で作ったものだけに限る","type":"text"}]}]}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"jq '{name, target, enforcement, bypass_actors, conditions, rules}' \"${backup}/ruleset-\u003cID>.json\" > \"${backup}/ruleset-\u003cID>.restore.json\" \\\n && gh api -X PUT \"repos/${repo}/rulesets/\u003cID>\" --input \"${backup}/ruleset-\u003cID>.restore.json\"","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"検証","type":"text"}]},{"type":"paragraph","content":[{"text":"各 Step の完了は以下で確認する:","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"repo=\"Fandhe-AI/\u003cREPO>\" # Step 0 の検証を通過した値のみ\n# Step 1: wrapper の存在と @latest 参照(@main・旧 codex-review.yml が残っていないこと)\ngrep -n \"uses: Fandhe-AI/actions\" .github/workflows/ai-review.yml\n# Step 1: 配置済み wrapper が構造検査(uses 1 件・特権トリガー無し・権限最小)に合格すること\nbash \u003cskill-dir>/scripts/verify-ai-review-wrapper.sh .github/workflows/ai-review.yml\ntest ! -e .github/workflows/codex-review.yml && echo \"旧 wrapper なし\"\n# Step 1: 直近 run でジョブ単位に review が実行されたこと(skipped は「workflow 成功」に見える)\n# head_branch は検証対象の PR(導入 PR 等)の head ブランチ名\nrun_id=$(gh run list --repo \"${repo}\" --workflow \"AI PR review\" --event pull_request --branch \"${head_branch}\" --limit 1 --json databaseId --jq '.[0].databaseId')\ngh run view \"${run_id}\" --repo \"${repo}\" --json jobs --jq '.jobs[] | {name, status, conclusion}'\n# Step 2: AGENTS.md の存在と P0/P1/P2 定義\ngrep -n \"P0\\|P1\\|P2\" AGENTS.md | head\n# Step 3: 直近 PR の check runs で集約ジョブが報告されること\ngh pr checks \"$(gh pr list --repo \"${repo}\" --state merged --limit 1 --json number --jq '.[0].number')\" --repo \"${repo}\"\n# Step 4: 退避ファイルの存在と件数(4.1)\nls -1 \"${backup}\" | wc -l\n# Step 4: 適用後の ruleset が承認済みの差分と一致すること(4.2 の新 body と現在値を jq -S で比較し、差分が空であること)\n# Step 4: ruleset とマージ設定\ngh api \"repos/${repo}/rulesets\" --jq '.[] | {id, name, enforcement}'\ngh api \"repos/${repo}\" --jq '{allow_squash_merge, allow_merge_commit, allow_rebase_merge, delete_branch_on_merge, allow_auto_merge}'\n\n# Step 4-a: 一括更新後の 3 軸スイープ(strict / bypass_actors / integration_id 残存)\n# PUT した ruleset 単体ではなく branch target の全 ruleset を掃く(org 継承は source_type でルーティング)。\n# コマンド全文・判定表は実行環境に .claude/rules/ruleset-policy.md があれば「一括更新後の検証」節を参照。\n# 無ければ下記 Step 4-a/4-b のコマンド・判定表をそのまま用いる\norg=\"${repo%%/*}\"\ngh api --paginate --slurp \"repos/${repo}/rulesets\" |\n jq -r 'add // [] | .[] | select(.target == \"branch\") | [(.id|tostring), .name, (.source_type // \"unknown\")] | @tsv' |\nwhile IFS=

setup-repo-guards 対象リポジトリへ組織標準の CI ガード一式を導入する。導入するのは次の 4 点で、 Step 1 → 4 の順に、各 Step を PR → CI 全 pass → レビュースレッド resolve → squash merge で確定させてから次へ進む。 1. ai-review(PR 自動レビュー workflow の wrapper) 2. AGENTS.md(AI レビューが読むレビュー観点集) 3. CI 必須チェックの集約ジョブ( 等) 4. branch protection ruleset・リポジトリ追加ガード(GitHub API のみ、コミット不要。適用前に退避・差分提示・明示承認を必須とする) この順序を守る理由: 「必須チェックに入れる集約ジョブが先に存在する」「AGENTS.md は PR の base コミット参照のため マージ後の次の PR から実効」という依存関係が、この順で自然に満たされる。 使い方 - 第 1 引数: 対象リポジトリ名( )。許容形式は のみ(Step 0 で検証する) - 第 2 引数: visibility( / の完全一致のみ。Step 0 で検証する)。runner 方針の分岐に使う(public は ubuntu ホステッド、private は self-hosted。ai-rev…

\\t' read -r id name src; do\n case \"${src}\" in\n Repository) path=\"repos/${repo}/rulesets/${id}\" ;;\n Organization) path=\"orgs/${org}/rulesets/${id}\" ;;\n *) echo \"UNKNOWN source_type: ${name} (${src}) — 手動確認\"; continue ;;\n esac\n gh api \"${path}\" --jq '{\n name: .name, enforcement: .enforcement, bypass: (.bypass_actors | length),\n strict: ([.rules[]? | select(.type==\"required_status_checks\") | .parameters.strict_required_status_checks_policy] | first),\n total: ([.rules[]? | select(.type==\"required_status_checks\") | .parameters.required_status_checks[]?] | length),\n unbound: [.rules[]? | select(.type==\"required_status_checks\") | .parameters.required_status_checks[]? | select(.integration_id == null) | .context]\n }'\ndone\n\n# Step 4-b: classic branch protection を別枠で掃く(defaultBranchRef 解決・@uri エンコード・HTTP status 分岐)\ndb=$(gh repo view \"${repo}\" --json defaultBranchRef --jq '.defaultBranchRef.name')\ndb_enc=$(printf '%s' \"${db}\" | jq -sRr '@uri')\ncode=$(gh api -i \"repos/${repo}/branches/${db_enc}/protection\" 2>/dev/null | awk 'NR==1{print $2}')\ncase \"${code}\" in\n 200) gh api \"repos/${repo}/branches/${db_enc}/protection\" --jq \\\n '{strict: (.required_status_checks.strict // \"none\"), unbound: [.required_status_checks.checks[]? | select(.app_id == null) | .context]}' ;;\n 404) echo \"classic BP なし(${db})\" ;;\n *) echo \"判定不能 (HTTP ${code:-?}) — Administration: read 権限を確認。green と扱わない\" ;;\nesac","type":"text"}]},{"type":"paragraph","content":[{"text":"最終確認として、導入後に小さな PR を 1 件流し、ai-review の実行・必須チェックの報告・ squash merge のみ許可・スレッド resolve 必須が実際に効いていることを観察する。","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"完了報告","type":"text"}]},{"type":"paragraph","content":[{"text":"Step ごとの PR 番号、AGENTS.md の観点構成、ruleset の最終必須チェック一覧、適用できなかった項目 (理由付き)、退避先パス(内容は貼らない)、承認の有無(得た場合のみ事実どおりに記載)を表でまとめて報告する。 受容判断の記録欄として、","type":"text"},{"text":"skip-branch-prefixes","type":"text","marks":[{"type":"code_inline"}]},{"text":"(指定した/しなかった、write 権限者の確認結果)と ","type":"text"},{"text":"required_approving_review_count 0","type":"text","marks":[{"type":"code_inline"}]},{"text":"(受容した/1 以上にした、根拠)を各 1 行で含める。 Step 0 で拒否した引数があれば理由付きで載せる。","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"よくある失敗","type":"text"}]},{"type":"table","attrs":{"layout":null},"content":[{"type":"tr","content":[{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"問題","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"回避策","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"集約ジョブの skipped 全許容が fail-open として AI レビューに P1 指摘される","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Step 3: skipped 許容を条件付きジョブの明示リストに限定する","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"チェック名変更 PR が CI 全 pass でも BLOCKED(ruleset が旧名のまま「Expected」待ち)","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Step 4: マージ前に ruleset を新チェック名へ PUT で置換する","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"未クォート ","type":"text"},{"text":"name:","type":"text","marks":[{"type":"code_inline"}]},{"text":" の ","type":"text"},{"text":" #","type":"text","marks":[{"type":"code_inline"}]},{"text":" 以降が切れて API 報告名が不安定になる","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Step 3: name に ","type":"text"},{"text":" #","type":"text","marks":[{"type":"code_inline"}]},{"text":" や「: 」を含む場合はクォート必須","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"paths フィルタ付き workflow のチェックを必須にするとマージが永久ブロックされる","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Step 4: 常に報告されるチェックのみ必須化する","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"参考ファイルの取り違え(別リポジトリ用 AGENTS.md の混入)を AI レビューが検出","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Step 2: 対象リポジトリ固有の具体項目に落とし込む","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"AGENTS.md の P1 定義と CI ゲート実挙動(P1 でも fail)の矛盾を AI レビューが指摘","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Step 2: P1 定義をゲート実挙動と矛盾させない","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"GITHUB_TOKEN read 化で暗黙 write 依存の workflow が壊れる","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Step 4: 全 workflow の permissions 明示を確認してから適用する","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"ruleset を PUT したら ","type":"text"},{"text":"integration_id","type":"text","marks":[{"type":"code_inline"}]},{"text":" 束縛が落ち、自動マージが静かに止まる(strict と bypass だけ見ると全 green に見える)","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Step 4-a: PUT 後に 3 軸スイープ(","type":"text"},{"text":"select(.integration_id==null)","type":"text","marks":[{"type":"code_inline"}]},{"text":")を実行する","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"旧 ruleset / classic BP の掃き漏らしで未束縛・strict=true が残る","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Step 4-a/4-b: 全 branch ruleset を列挙して掃き、classic BP は ","type":"text"},{"text":"defaultBranchRef","type":"text","marks":[{"type":"code_inline"}]},{"text":" 解決 + status 分岐で別枠確認する","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"AGENTS.md に自動マージの G0 契約を書く際 strict を要件として列挙し、実装より強い契約が AI レビューの P0 の根拠になる","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Step 2: G0 契約を書くなら strict は「意図的な非要件」と明記する(実行環境に ","type":"text"},{"text":".claude/rules/ruleset-policy.md","type":"text","marks":[{"type":"code_inline"}]},{"text":" があれば参照)","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"autoMerge 運用で外部アプリ・post_feedback 等を required から外すと G0 (f) が server-enforcement-missing で辞退し自動マージが静かに恒久停止する","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Step 4: autoMerge を使う場合は PR HEAD の全 context を required に列挙する(外部アプリ・ai-review の全ジョブを含む)","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"ai-review テンプレートを ref 指定なし・無検証で配置し、取得元の改ざん・誤更新がそのまま workflow になる","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Step 1: ","type":"text"},{"text":"latest","type":"text","marks":[{"type":"code_inline"}]},{"text":" を commit SHA に解決して固定取得し、","type":"text"},{"text":"verify-ai-review-wrapper.sh","type":"text","marks":[{"type":"code_inline"}]},{"text":" と ","type":"text"},{"text":"cat","type":"text","marks":[{"type":"code_inline"}]},{"text":" の目視を通してから配置する","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"旧 ","type":"text"},{"text":"codex-review.yml","type":"text","marks":[{"type":"code_inline"}]},{"text":" wrapper を新規導入する・残したままにする(Fandhe-AI/actions から削除済みで workflow を解決できない)","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Step 1: ","type":"text"},{"text":"ai-review/templates/ai-review.single.yml","type":"text","marks":[{"type":"code_inline"}]},{"text":" から導入し、旧 wrapper は「codex-review からの移行」で置き換える","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"ai-review の ","type":"text"},{"text":"preflight","type":"text","marks":[{"type":"code_inline"}]},{"text":" を required から外し、actor 判定の失敗で review が skipped のまま必須チェックを満たす","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Step 4: ","type":"text"},{"text":"\u003cjob> / preflight","type":"text","marks":[{"type":"code_inline"}]},{"text":" を必ず required に含める","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"資格情報未設定で review が skipped なのに workflow 全体の成功を見て導入完了とする","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"検証: ","type":"text"},{"text":"gh run view --json jobs","type":"text","marks":[{"type":"code_inline"}]},{"text":" でジョブ単位に ","type":"text"},{"text":"review","type":"text","marks":[{"type":"code_inline"}]},{"text":" の実行を確認する","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"引数を検証せず ","type":"text"},{"text":"gh api","type":"text","marks":[{"type":"code_inline"}]},{"text":" のパスや分岐に渡す","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Step 0: 許可リスト検証と実在確認を ","type":"text"},{"text":"gh","type":"text","marks":[{"type":"code_inline"}]},{"text":" 実行前に行う","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"第三者 write ありのリポジトリで ","type":"text"},{"text":"skip-branch-prefixes","type":"text","marks":[{"type":"code_inline"}]},{"text":" を指定し、ブランチ名でレビューを回避される","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Step 1: write 権限者を列挙して受容条件を確認し、満たさなければ指定しない","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"0 承認の受容条件を確認せずに適用し、ai-review が唯一のゲートなのに ","type":"text"},{"text":"review","type":"text","marks":[{"type":"code_inline"}]},{"text":" が動いていない","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Step 4.3: 受容条件を確認し、満たさなければ 1 以上にする。適用後に ","type":"text"},{"text":"review","type":"text","marks":[{"type":"code_inline"}]},{"text":" が動かなければ 4.1 → 4.2 → 4.3 を通して是正する(4.4 の復旧では是正にならない)","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"現在値を保存せずに PUT し、","type":"text"},{"text":"integration_id","type":"text","marks":[{"type":"code_inline"}]},{"text":" の脱落・設定項目の消失に気づけず復旧できない","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Step 4.1: 変更前に作業ツリー外へ退避する","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"差分を示さず承認なしで適用する","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Step 4.2: 差分提示と明示承認を取り、得られなければ停止する","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"退避ファイルを作業ツリー内に置いてコミットしてしまう","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Step 4.1: 作業ツリー外の ","type":"text"},{"text":"mktemp -d","type":"text","marks":[{"type":"code_inline"}]},{"text":" に保存する","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"取得失敗を「現在値なし」として扱い、誤って新規作成・上書きする","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Step 4.1: 取得失敗は未取得とし、その項目は変更しない","type":"text"}]}]}]}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"注意事項","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"コミットは日本語 Conventional Commits 形式で作成し、対象リポジトリのコミット規約・フッター運用に従う。","type":"text"},{"text":"--no-verify","type":"text","marks":[{"type":"code_inline"}]},{"text":" は使用しない","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"シェル変数は ","type":"text"},{"text":"\"${var}\"","type":"text","marks":[{"type":"code_inline"}]},{"text":" 形式でクォートする","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Step 0 の検証を通っていない値を ","type":"text"},{"text":"gh","type":"text","marks":[{"type":"code_inline"}]},{"text":" に渡さない","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"承認を得るまで Step 4 の書き込み系 API を実行しない。自動運転などで承認を取れない場合は適用せず停止する","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"AI レビューの指摘が正当なら修正で応える(テスト・ゲートの弱体化やスキップでごまかさない)。 スレッドへ対応内容を返信して resolve してからマージする","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"スコープ外の発見は Issue 化をユーザーに提案する(勝手に起票しない)","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"このスキルはネットワーク越しの GitHub 操作(ruleset の PUT・workflow の配布)を必須とする。該当コマンドはコマンド単位で sandbox 無効にして実行する。ネットワーク遮断を解除できない環境では実行できない","type":"text"}]}]}]}]},"metadata":{"date":"2026-11-02","name":"setup-repo-guards","model":"opus","author":"@skillopedia","source":{"stars":2,"repo_name":"agent-cli-skills","origin_url":"https://github.com/Fandhe-AI/agent-cli-skills/blob/HEAD/skills/setup-repo-guards/SKILL.md","repo_owner":"Fandhe-AI","body_sha256":"6a51a50543a43cc486f8b14359b96ddcc72561321c2fb43363c536061103ab35","cluster_key":"26a0abe5ecdde57a6c2828b61c99e5108cc2f3eb7f3d943f20c47f1bc62c97a3","clean_bundle":{"format":"clean-skill-bundle-v1","source":"Fandhe-AI/agent-cli-skills/skills/setup-repo-guards/SKILL.md","text_bytes":74519,"source_commit":"b1c76148d77c8f8897e0f7206af34b126739fd83","skills_sh_rank":15611,"attachment_count":3,"text_attachments":3,"attachment_storage":"skill_attachments","binary_attachments":0},"cluster_size":1,"skill_md_path":"skills/setup-repo-guards/SKILL.md","import_metadata":{"date":"2026-11-02","author":"@skillopedia","version":"v1","category":"devops-infrastructure","category_label":"DevOps"},"exact_dupes_collapsed_into_this":0},"version":"v1","category":"devops-infrastructure","import_tag":"skills-sh-top20k-2026-11","description":"Fandhe-AI 組織標準の CI ガード一式(ai-review 導入 / AGENTS.md レビュー観点集 / 必須チェック集約ジョブ / branch protection ruleset・追加ガード)を対象リポジトリへ導入する。「リポジトリガード入れて」「CI ガード導入して」 「branch protection セットアップ」「ai-review 導入して」「AGENTS.md 整備して」などで使用。 複数リポジトリへの一括適用にも対応。.claude/ 体系の初期セットアップは init-claude、既存体系の充実は update-claude を参照。\n","argument-hint":"\u003c対象リポジトリ名 (owner/repo)> \u003cpublic|private> [追加のリポジトリ...]","user-invocable":true}},"renderedAt":1793666254711}

setup-repo-guards 対象リポジトリへ組織標準の CI ガード一式を導入する。導入するのは次の 4 点で、 Step 1 → 4 の順に、各 Step を PR → CI 全 pass → レビュースレッド resolve → squash merge で確定させてから次へ進む。 1. ai-review(PR 自動レビュー workflow の wrapper) 2. AGENTS.md(AI レビューが読むレビュー観点集) 3. CI 必須チェックの集約ジョブ( 等) 4. branch protection ruleset・リポジトリ追加ガード(GitHub API のみ、コミット不要。適用前に退避・差分提示・明示承認を必須とする) この順序を守る理由: 「必須チェックに入れる集約ジョブが先に存在する」「AGENTS.md は PR の base コミット参照のため マージ後の次の PR から実効」という依存関係が、この順で自然に満たされる。 使い方 - 第 1 引数: 対象リポジトリ名( )。許容形式は のみ(Step 0 で検証する) - 第 2 引数: visibility( / の完全一致のみ。Step 0 で検証する)。runner 方針の分岐に使う(public は ubuntu ホステッド、private は self-hosted。ai-rev…