サイト内検索

hermes-talaria に組織横断の Issue 展開スキル ht-github-issue-rollout を追加した — 可視範囲と管理カバレッジを分け、ネイティブ階層を Markdown リンクで代替しない

重岡 正 · Thu, October 22, 2026

組織全体で脆弱性対応をする場面が増えてきました。CVE が公開されてから数日以内に、該当する依存を使っている全リポジトリへ調査と修正依頼を出す、という作業です。手元の hermes-talaria に、GitHub organization や enterprise 単位で一括展開するスキルを追加しました。PR は codenote-net/hermes-talaria#62 です。

スキル名は ht-github-issue-rollout、呼び出しは /ht-github-issue-rollout です。対象は https://github.com/orgs/ORG/repositories 形式の organization URL と、https://github.com/enterprises/ENTERPRISE/organizations 形式の enterprise URL です。本記事ではこのスキルの設計で判断した点をまとめます。

追加した機能の概要

  • Organization の全リポジトリ、または enterprise の全 organization と全リポジトリをスキャンする Hermes Agent 用スキル
  • 共有された依頼(脆弱性修正、依存マイグレーション、ポリシー変更など)を各リポジトリに照らして評価し、承認されたネイティブ epic/sub-issue 階層を作る
  • organization mode は「organization epic → repository issues」、enterprise mode は「enterprise epic → organization epics → repository tracking issues」の 2 階層
  • 設計判断は以降の節で述べる(可視範囲と管理カバレッジの区別、評価状態の分離、owner 同一制約、hidden marker による復旧)
  • Python/gh 製の read-only インベントリヘルパー を同梱。評価・承認・作成・復旧は AI エージェント手続き側に残す

GitHub Security Campaigns ではなくエージェント手続きにした理由

GitHub Security Campaigns は、Code Scanning と secret scanning のアラートをまとめて修正する機能です。今回扱いたいのは、これらのアラートを起点としない依存更新・ポリシー変更・SaaS 設定変更・ランタイム移行の依頼も含めた「組織横断の一括依頼」です。

CVE そのものは GitHub Advisory や npm audit 側で検出しても、「どのリポジトリに影響があるか」の判断は、manifest・lockfile・ランタイム設定・ワークスペース構成を見ないと決まりません。その評価と、承認済みのネイティブ階層の作成、復旧までを AI エージェントに任せる形にしました。

スキル側では、以下は実行しません。

  • リポジトリのコードの実行
  • 修正パッチの適用
  • セキュリティ設定の変更
  • 発見した脆弱性情報の公開リポジトリへの流出

可視範囲(API visibility)と管理カバレッジ(administrative coverage)を分ける

最初に明文化したのは、「GraphQL で totalCount と pageInfo.hasNextPage を使い、全ページを取得し終えた」ことと、「スコープ内の全リポジトリに到達している」ことを別の概念として扱う点です。

GitHub の GraphQL API は、認証プリンシパルの可視範囲にスコープされます。SSO やアプリインストール、個別のリポジトリ権限によって、ページング上は「次ページなし」になっても、見えていないリポジトリが残っている可能性があります。全組織に対して脆弱性対応の調査依頼を出すとき、ここを混同すると、「全リポジトリに依頼を出したつもりで、実は見えていないリポジトリを対象にしていなかった」事態になります。

SKILL.md では次を要求しています。

  • インベントリヘルパーの pagination_complete は管理カバレッジの証拠ではない
  • 認可された管理者のインベントリ、または「このプリンシパルがスコープ内の全リポジトリを見えている」ことの明示的確認と ID・件数を突き合わせる
  • 完全カバレッジが取れない場合は、可視サブセットの結果だけを報告して bulk 作成前に停止する
  • 書き込みプランを凍結する直前に再列挙し、追加・移管・改名・削除・件数のずれを ID で突き合わせる

Enterprise mode では、read:enterprise スコープを持つクラシックトークンが必要なケースがあり、Fine-grained token や GitHub App token では該当クエリに対応していないことがあります。アクセスが足りないときは、トークンを吐き出したり黙って自分のメンバーシップに置換したりせず、停止して認可されたセットアップを求めます。

applicable・not_applicable・unknown を分けて扱う

脆弱性対応の bulk ロールアウトで起きやすい誤りは、調査が完了しなかったリポジトリを対象外(not_applicable)に分類してしまうことです。全リポジトリに依頼が届いていると報告した後で、調査失敗を無視したリポジトリに本番影響があることが分かる、という流れです。

SKILL.md では評価状態を 3 つに分けました。

状態意味既定の issue アクション
applicable証拠が依頼の条件を満たすリポジトリに対応依頼 issue を 1 件作成
not_applicable条件が当てはまらないことを証拠が示すスキップし、理由をプライベート領域に残す
unknownアクセス不足・証拠不足・条件が曖昧調査する。not_applicable に再分類しない

評価は manifest、lockfile、設定ファイル、ネストしたワークスペース、承認がある場合はセキュリティアラートを対象に行い、特定コミットに固定します。build や install スクリプトを調査のために実行しません。空の検索結果だけで not_applicable にしない、archive・issues 無効・権限ブロックは applicability と別軸で記録する、unknown の解消前に本番書き込みを始めない、という点も明記しました。

「not_applicable のリポジトリにも管理用の issue を立てたい」という明示的な依頼があったときだけ、タイトルに [Not applicable] を付け、本文冒頭に「Not applicable to this request — no remediation required」を入れる非対応通知 issue を作ります。この issue には対応チェックリスト・期限・脆弱性ラベルは付けず、applicable のカウントとは別集計で扱います。

owner 同一制約の中でネイティブ階層を保つ

GitHub の sub-issue REST API は、2026-10-22 時点のドキュメントで、子 issue が親 issue と同じリポジトリ owner に属することを要求します。Enterprise 単位で複数 organization にまたがる階層を REST API で作ろうとすると、この制約に該当します。

よくある回避策として、親 issue の本文に「対応リポジトリの issue 一覧」を Markdown リンクで並べる、という方法があります。これは見た目は似ていますが、GitHub の native sub-issue ではないため、進捗の自動集計・親からの子一覧のページング・親の変更検知・階層の循環防止といった platform 側の機能が効きません。

SKILL.md では次の形で整理しました。

  • Enterprise mode では、承認済みの中央 tracking repository を 1 つ用意し、enterprise epic・各 organization epic・各リポジトリ tracking issue をすべてそのリポジトリに置く
  • 各 issue 本文に、どの organization・どのリポジトリを代表する issue かを明示する
  • ユーザーが「元のリポジトリに置きたい」と要求した場合は、ドキュメント上の API で単一のネイティブツリーを保証できない旨を説明して停止する
  • Markdown リンクをネイティブ sub-issue と呼ばない。重複プロキシ issue を自動で複製しない
  • Enterprise メンバーシップを根拠に、ドキュメントに書かれていない cross-owner edge を作ったつもりにしない

ネイティブ関係にはもう 1 つ制約があり、sub-issue の追加に関するドキュメントによると、2026-10-22 時点では親 1 件あたりの sub-issue は 100、ネスト 8 階層までです。SKILL.md では、作成前に既存の子・祖先の深さを含めて現状を測り、超えるときは正確な overflow を提示して承認済みのバッチ階層か複数ルートを求める、スキャンの切り詰めや階層の黙示的変更はしない、と要求しました。

作成前に凍結したプランを承認する

ネイティブ階層・tracking destination・issue タイトル/本文・ラベル・担当者・順序は、どれか 1 つでも後から変わると承認の意味が崩れます。SKILL.md では、書き込み前に次を含む凍結プランを作って承認を求めます。

  • 安定した rollout key(例: ht-advisory-example)とノードキー
  • 各ノードが代表するリポジトリ ID
  • hosting repository
  • タイトル・本文
  • applicability
  • 親ノードキー
  • 想定順序
  • ラベル・担当者
  • issue アクション

承認後にスコープ・階層・destination・件数・issue 文面のどれかが変わる場合は、再承認が必要です。audience も必ず見直します。プライベート・インターナルリポジトリ名や脆弱性の証拠を、より広いアクセスを持つ tracking repository に流し込まないためです。

必要以上の mass mention は含めません。承認済み担当者と、既存の verified ラベルだけを使い、label や assignee を黙って作成しません。

Hidden marker で部分失敗から復旧する

数十〜数百リポジトリ規模のロールアウトでは、途中で rate limit・ネットワーク断・曖昧な HTTP 応答が入ります。GitHub の Issue 作成に Idempotency-Key のようなヘッダは無いため、「POST を送った直後にタイムアウトしたが、issue が作られているかどうかは応答が無いまま分からない」状況が起きます。

SKILL.md では、各新規 issue の本文に以下の形の hidden marker を埋めました。

<!-- ht-issue-rollout: ROLLOUT_KEY NODE_KEY -->

ROLLOUT_KEY / NODE_KEY は英数字・ハイフン・アンダースコア・ドットに制限し、untrusted な生テキストを marker に入れません。復旧時は、hosting repository の open と closed の issue を REST API のページング で全て舐め、pull request を除外した上で、exact marker と本文のセマンティック重複を比較します。

  • 単一マッチ: 部分完了として扱い、以降の作成とアタッチを再開する
  • 複数マッチ・close 済み候補・互換性のない本文・親が既に存在・marker 無しの意味的に等価な issue: すべて明示的な reuse/skip 判断を求める
  • 曖昧な作成応答: /issues?state=all を舐めて marker 突き合わせしてからでなければリトライしない
  • 曖昧なアタッチ応答: 親と子の両方を先に読み、両方向の parent・sub_issues ページングを確認する
  • 復旧できないときは、復旧 ledger を残して停止する。自動 close や detach によるロールバックはしない

書き込みは 1 ロールアウトキーにつき 1 writer に限定します。GitHub 側に create の原子的な冪等キーが無いため、並列書き込みを許すと重複がすり抜けます。

読み戻し検証を Issue 単位でも親子関係単位でも行う

HTTP の POST が 201 を返しただけでは、「書いたものと保存されたものが一致している」ことの証明になりません。SKILL.md では、作成した issue ごとに次を要求しています。

  • 作成直後に GET /repos/{owner}/{repo}/issues/{number} を叩き、タイトル・本文・state・ラベル・担当者を凍結ペイロードと一致させる
  • アタッチには issue 番号や GraphQL node ID ではなく、REST の integer id を使う
  • アタッチ後に、子側の /parent と、親側の /sub_issues を全ページ分 slurp して、どちらの方向からも想定通りに見えるか確認する
  • 承認済みプランで表示順を指定している場合は、PATCH /repos/{owner}/{repo}/issues/{number}/sub_issues/priority で after_id / before_id を使い、各手順の前後で全ページを読み直して想定の ID 集合と一致することを確認する

gh api --paginate --slurp の結果はページごとの配列になるため、コード側で平坦化してから ID や件数を比較します。マージコミットなどの pull_request フィールドを持つオブジェクトは除外します。親エンドポイントの 404 だけでは「アタッチ失敗」の証拠にならず、issue・リポジトリ・エンドポイント・権限が到達可能であることを確認し、親の全子ページで突き合わせます。

このスキルが実行しない操作

SKILL.md では、ロールアウトを早く終わらせるためであっても実行しない操作を明記しました。

  • リポジトリの owner を黙って変更する、または Markdown リンクをネイティブ sub-issue と呼ぶ
  • 権限が無いリポジトリをアーカイブ解除する、または Issues を自動で有効化する
  • 既存の親子関係を replace_parent: true で上書きする
  • 曖昧な作成応答に対して exact marker 突き合わせなしに作成をリトライする
  • 自動 close・自動 detach・自動 reparent による巻き戻し
  • Enterprise メンバーシップ・Code Search・現在のユーザーのメンバーシップを「全リポジトリ」のインベントリ代わりにする
  • セキュリティ証拠を audience の広い tracking repository に貼り付ける
  • トークン・資格情報が入った URL をサニタイズして続行する
  • 読み戻し検証なしに「全組織に展開完了」と報告する
  • Code Scanning で検出されるアラートの対応を、このスキルの手続きに丸めて GitHub Security Campaigns の代替と称する

以上、hermes-talaria に組織横断の Issue 展開スキル ht-github-issue-rollout を追加し、可視範囲と管理カバレッジ・applicable と unknown・ネイティブ階層と Markdown リンクをそれぞれ混ぜないための設計をまとめた、現場からお送りしました。

参考情報