rust-review について
Claude Code 向けに Rust アプリケーションやライブラリーを段階的にセキュリティレビューするスキルです。複数のサブエージェントにコード群を割り当て、重複や誤検出、重要度を確認して Markdown と SARIF のレポートを作成します。
主な機能
- 01Rust のメモリー安全性と unsafe 境界の監査
- 02並行処理、FFI、非同期処理のリスク確認
- 03コード群を並列サブエージェントに割り当て
- 04指摘の重複排除と誤検出の評価
- 05Markdown と SARIF のレポート生成
活用シーン
- 01ライブラリー内の unsafe Rust コードのレビュー
- 02Rust サーバーの panic によるサービス拒否リスクの評価
- 03コードベース全体の並行処理と FFI リスクの監査
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ trailofbits/ skills --skill rust-reviewインストール後、Claude Code で/rust-reviewを入力して読み込まれたか確認します。
詳細
Rust セキュリティレビュー
メインの会話で実行します(/rust-review:rust-review を使用して呼び出します)。オーケストレーターは、再試行の記録として Task* 台帳を管理します。ワーカーと判定者は Task ツールを持ちません。ワーカーと判定者は、名前付きのプラグインサブエージェントです(rust-review:rust-review-worker、rust-review:rust-review-dedup-judge、rust-review:rust-review-fp-judge)。ツールセットは plugins/rust-review/agents/*.md で宣言されています。指摘事項は、共有出力ディレクトリ内の YAML を含む Markdown ファイルを介してやり取りします。
使用する場面
Rust アプリケーション/ライブラリのセキュリティレビュー:safe/unsafe 境界の監査、unsafe ブロック内のメモリ安全性、並行処理の危険性、サーバーでの panic による DoS、FFI の安全性、非同期ランタイムの誤用。
使用しない場面
- Pure-C/pure-C++ のコードベース — 代わりに
c-reviewを使用してください。 - スマートコントラクト(Solana プログラム/NEAR コントラクト/Ink!)—
solana-vulnerability-scannerまたはコントラクト専用のスキルを使用してください。 - ユーザー空間アロケーターを持たないカーネルモードの Rust ドライバー — 対応範囲が不完全なため、参考情報としてのみ指摘してください。
- シークレット/鍵のメモリ衛生(ゼロ化、
Zeroize/ZeroizeOnDrop/secrecyの使用、スタック/ヒープ上に残るコピー)—zeroize-auditスキルを使用してください。rust-review はメモリのゼロ化を対象としていません。
サブエージェント
| サブエージェントの種類 | 目的 | ツールセット |
|---|---|---|
rust-review:rust-review-worker | 割り当てられたクラスターを調査し、指摘事項を記述 | Read, Write, Edit, Bash |
rust-review:rust-review-dedup-judge | 重複を統合(最初に実行) | Read, Write, Edit, Glob |
rust-review:rust-review-fp-judge | FP + 重大度 + 最終レポート(2 番目に実行) | Read, Write, Edit, Bash |
ツールは、各エージェントの起動時に frontmatter から取得されます。オーケストレーターの Task*/Agent/Bash などは、このスキルの allowed-tools から取得されます。検索ツールと Bash の併用について: 現在の Claude Code では、Bash を許可されたエージェントに専用の Glob または Grep ツールも許可されるわけではありません(呼び出しは No such tool available を返します。ハーネスでは、代わりに Bash 経由で find/grep/rg を使用します)。そのため、Bash を持たない唯一のエージェントである dedup-judge だけが Glob を使用します。worker、fp-judge、オーケストレーターは、代わりに Read/Bash の find/rg/grep/test -f でパスを特定して検索します。クラスター/ファインダーのプロンプトシードは ripgrep の正規表現構文(\s、\d、\b)で記述されているため、Bash を持つエージェントは rg で実行する必要があります。rg がインストールされていない場合、呼び出しはエラーを明確に返します(command not found)。その場合は、POSIX クラスを使う grep -E に切り替えてください(\s→[[:space:]]、\d→[[:digit:]]、\b は削除)。\s をそのまま使う grep は、空の結果を黙って返し、誤った cleared につながるため使用しないでください。Bash を持つエージェントのプロトコルに Glob/Grep を再導入しないでください。
アーキテクチャ
coordinator: write context.md → build_run_plan.py → TaskCreate × M
→ spawn primer (foreground) → spawn M workers (parallel)
→ classify Phase-7 outcomes + write findings-index.txt
→ dedup-judge → fp-judge → report safety net (SARIF + REPORT.md) → return REPORT.md
出力ディレクトリには、context.md、plan.json、worker-prompts/、findings/、findings-index.d/(ワーカーごとの分割ファイル)、findings-index.txt、coverage/(ワーカーごとのカバレッジゲートファイル)、run-summary.md、dedup-summary.md、fp-summary.md、REPORT.md、REPORT.sarif が含まれます。
パスの規約: 後続の各フェーズでは ${RUST_REVIEW_PLUGIN_ROOT}/scripts/*.py をシェルから実行するため、まずこの変数を prompts/clusters/unsafe-boundary.md(および scripts/build_run_plan.py)を含むプラグインディレクトリに解決してください。次の順に試し、最初に見つかったものを使用します。
- ネイティブ Claude Code —
${CLAUDE_PLUGIN_ROOT}を確認します。Bash: ls "${CLAUDE_PLUGIN_ROOT}/prompts/clusters/unsafe-boundary.md"で解決できる場合に有効です。 - Codex —
${CODEX_PLUGIN_ROOT}を確認します(この変数が設定されていて、同じ方法でマーカーを解決できる場合も同様に設定します)。 - フォールバック検索 —
~/.codex配下の Codex インストール、~/.claude配下の Claude インストール、ローカルのチェックアウト/リポジトリでの実行を対象とします。Bash: find ~/.claude ~/.codex . -path '*/plugins/rust-review/prompts/clusters/unsafe-boundary.md' -print -quit 2>/dev/nullを実行してください。一致したパスを取得し、末尾の/prompts/clusters/unsafe-boundary.mdを取り除いてルートを得ます(ホームディレクトリが.より先に検索されるため、監査対象のリポジトリに同梱されたコピーよりインストール済みのコピーが優先されます)。
解決したルートを RUST_REVIEW_PLUGIN_ROOT に設定します。3つすべてで失敗した場合は、検索したルートを明記したメッセージを出して中止してください。変数が空のままフェーズ4に進んではいけません(すべての uv run --no-project "${RUST_REVIEW_PLUGIN_ROOT}/scripts/..." 呼び出しが、分かりにくいパスエラーで失敗します)。
スコープの規約: 実行中は、次の2つのスコープを分けてください。
finding_scope_root— ユーザーが指定した監査対象のサブツリーです。ワーカーが指摘事項として報告できるのは、脆弱性の場所がこのサブツリー内にあるものだけです。context_roots— 到達可能性、呼び出し元、ラッパー、ビルドフラグ、緩和策、脅威モデルの詳細を確認するために、ワーカーと判定者が調査できる読み取り専用のリポジトリルート/ファイルです。ユーザーがより広いコンテキストを明示的に禁止していない限り、デフォルトは.です。finding_scope_rootの外側のコンテキストを読むことは許可されますが、そこで指摘事項を報告することはできません。
拒否すべき誤った判断
- 「
unsafeはまれだから、メモリー安全性クラスターを手動で飛ばそう。」 クラスターリストは編集しないでください。フェーズ1でhas_unsafeを正確に設定し、build_run_plan.pyに判断させます。メモリー安全性に関するすべてのバグクラス(UAF、二重解放、未初期化データの読み取り、Vec::set_len、unionのUB)にはunsafeが必要です。そのため、has_unsafe=trueならプランナーはmemory-safetyクラスター全体を実行し、falseなら正しく除外します。「とにかく実行する」という選択肢はありません。unsafe-boundaryクラスターは別です。requiresを持たず、常に実行されます(統合済みです。安全性に関するドキュメントとrepr(C)の衛生対策は、目に見えるunsafe { }ブロックがなくてもFFI宣言に適用されます)。 - 「コンパイラーが検出した。」 借用チェッカーが証明するのは、安全なコードにデータ競合がないことです。unsafeブロック、panicの到達可能性、ABBAデッドロック、アトミックなロード/ストアの順序付け、FFI ABIの不一致については何も証明しません。
- 「
// SAFETY: documented infallibleと書いてあれば、unwrap()は問題ない。」// SAFETY:が説明するのはunsafe操作であり、失敗しないという主張ではありません。文書化された失敗しない入力に対するunwrap()も、その説明が誤っていれば依然として危険です。重大度を低として報告し、FP判定エージェントに判断させてください。 - 「
has_unsafe=falseだから実行を省略しよう。」 純粋なsafe Rustのクレートにも、panicによるDoS、アトミックの競合、drop時のpanic、トレイト実装に関する危険があります。常時実行されるクラスターを実行してください。 - 「バックグラウンドで起動すれば、ワーカーを並列化できる。」 できません。1つのアシスタントメッセージ内の
Agent呼び出しは、すでに並行して実行されます。run_in_background=trueにするとフェーズ6aのプライマーキャッシュが無効になり、各ワーカーがキャッシュ作成の全コスト(cache_read_input_tokens=0)を負担し、約15 KトークンのプライマーがM回分無駄になります。デフォルトでは、ワーカーの起動時にrun_in_backgroundを省略してください。 - 「
build_run_plan.pyを実行せず、クラスターリスト/パス/パスの接頭辞をその場で導き出そう。」 選択と出力形式を決める唯一の基準はスクリプトです。内容を言い換えると、ワーカーの自己チェックに必要なフィールドが抜けて、worker-N abort: spawn prompt malformedが発生します。必ずスクリプトを実行し、plan.jsonを読み取ってください。 - 「実行は一部成功した。完了した分だけで
REPORT.mdを書こう。」 部分的な実行を成功レポートで隠すのは、正確性上の不具合です。フェーズ5のクラスタータスクが1つでもcompletedでない場合は、そのことをrun-summary.mdと最終回答で目立つように示してください。 - 「検出結果がゼロだから、フェーズ8を省略しよう。」 2つの判定エージェントとフェーズ8bは必ず実行してください。dedup-judgeは空のインデックスでも最小限の何もしない
dedup-summary.mdを書き、fp-judgeは空のREPORT.md/REPORT.sarifを書きます。また、フェーズ8bのSARIFジェネレーターは空の場合にresults: []を出力します。SARIFの利用側は、成果物一式が常に揃っていることを前提としています。 - 「事前チェックなら
Bash: ls README*で問題ない。」 zshでは、マッチしないグロブがあると2>/dev/nullが実行される前に複合コマンド全体が中断します。マッチなしでも失敗しないfindを使ってください。Bashも使用するエージェントでは利用できないため、Globは使わないでください。
オーケストレーションのワークフロー
これらのフェーズはメインの会話内で実行してください。
フェーズ 0:パラメーターの収集
開始条件: スキルが呼び出されます。終了条件: threat_model、worker_model、severity_filter が解決済みであること、scope_subpath が解決済みまたは "." に設定されていること、finding_scope_root=scope_subpath であること、context_roots が解決済みであること。
このスキルは直接呼び出されます(コマンドラッパーはありません)。ユーザーが /rust-review:rust-review の行に渡した自由形式の引数(例:flamenco only、high severity only、use haiku)を解析し、それらが示す回答をあらかじめ入力してから、未回答の必須パラメーターを 1 回の AskUserQuestion 呼び出しで質問します。必須パラメーターを黙ってデフォルト値に設定してはいけません。
必須パラメーター:
| パラメーター | 値 | 引数からの推測方法 |
|---|---|---|
threat_model | REMOTE / LOCAL_UNPRIVILEGED / BOTH | 「remote」、「network」、「attacker」など → REMOTE、「local」、「unprivileged」など → LOCAL_UNPRIVILEGED。それ以外は質問します。 |
worker_model | haiku / sonnet / opus | 引数にモデル名が明示されている場合はそれを使用します。それ以外は質問します(暗黙のデフォルト設定はありません)。 |
severity_filter | all / medium / high | 「all」、「every」、「noisy」など → all、「medium and above」 → medium、「high only」、「criticals only」 → `high」。それ以外は質問します — 暗黙のデフォルト設定は禁止です。 |
scope_subpath | リポジトリ相対のディレクトリ(任意) | 「X only」、「just audit X/」、「review subdirectory X」など → src/X/ または一致するサブディレクトリ。リポジトリのトップレベルのサブディレクトリとファジーマッチングします。指定がない場合は "." に設定し、曖昧な場合は質問します。 |
未解決の必須パラメーター(threat_model、worker_model、severity_filter)のみを指定して AskUserQuestion をちょうど 1 回呼び出します。ユーザーが範囲の絞り込みを明示的に要求していて、その指定が曖昧な場合に限り、scope_subpath も含めます。必須パラメーターがすべて事前入力済みで、スコープが指定されていないか解決済みの場合は、質問を省略します。
scope_subpath の解決後、finding_scope_root="${scope_subpath:-.}" を設定します。絞り込んだサブツリーの外にある呼び出し元やビルド設定もワーカーが確認でき、スコープ外の指摘を報告しないよう、デフォルトで context_roots="." を設定します。ユーザーが広いコンテキストの参照を明示的に禁止した場合は、context_roots="${finding_scope_root}" を設定し、到達可能性の確信度が低くなる可能性があることを伝えます。
フェーズ 1:前提条件
開始条件: フェーズ 0 が完了していること。終了条件: has_unsafe、has_ffi、has_concurrency、has_async、has_packed_repr、has_fs_io のフラグが決定していること。${finding_scope_root} 以下に *.rs ファイルがない場合は、明確なメッセージを表示して中止します。
まず uv が存在することを確認します(command -v uv)。フェーズ 4 以降のすべてのヘルパースクリプトは uv 経由で実行されます。存在しない場合は中止し、ユーザーにインストールするよう伝えます(brew install uv または公式インストーラー)。これがないと、以降の処理は実行できません。
以下の Bash コマンドで ${finding_scope_root:-.} 以下を調べます(出力が空でなければフラグは true)。このオーケストレーターは Bash を保持しているため、専用の Grep/Glob ツールは利用できません。Bash 経由で grep/rg/find を使ってください。(プローブの正規表現では \s/\b を使用しています。お使いの grep が GNU の \s に対応していない場合は、\s に対応し、無視対象のファイルも検索する rg -uu で実行してください。また、rg もインストールされていない場合は、\s を [[:space:]] に置き換え、\b を削除してください。ここでは範囲を広げても問題ありません。誤検知によるフラグ設定は無害な追加ワーカーが起動するだけですが、見逃しがあるとパス全体がスキップされます。)
# Rust source presence (precondition)
find "${finding_scope_root:-.}" -name '*.rs' -print -quit
# has_unsafe
grep -rlE '\bunsafe\s+(extern|fn|impl|trait)\b|\bunsafe\s*\{' --include='*.rs' "${finding_scope_root:-.}" | head -1
# has_ffi
grep -rlE 'extern\s+"(C|system|stdcall|cdecl|win64|sysv64|aapcs|fastcall|thiscall|vectorcall|efiapi)(-unwind)?"|\bextern\s+fn\b|extern\s+\{|#\[repr\((C|transparent)\b|\b(CString|CStr)\b|use\s+(libc|core::ffi|std::ffi|std::os::raw|cty)|\blibc::|\b(bindgen|cbindgen)\b|\bc_void\b' --include='*.rs' "${finding_scope_root:-.}" | head -1
# has_concurrency
grep -rlE '\b(std::(thread|sync)|parking_lot::|crossbeam|rayon::|tokio::sync|core::sync::atomic|std::sync::atomic|Atomic[A-Za-z0-9_]*|UnsafeCell|static\s+mut|unsafe\s+impl\s+(Send|Sync)|memmap2::|Mmap(Options|Mut)?|MAP_SHARED|shm_open|mmap\s*\(|memfd_create|shared_memory|raw_sync|CreateFileMapping|MapViewOfFile|once_cell|sigaction|signal_hook|nix::sys::signal|libc::signal|libc::sigaction)' --include='*.rs' "${finding_scope_root:-.}" | head -1
# has_async
grep -rlE '\basync\s+(fn|move|\{)|\.await\b|tokio::|async_std::|futures::' --include='*.rs' "${finding_scope_root:-.}" | head -1
# has_packed_repr (outer #[repr(...packed...)] and inner #![repr(...packed...)])
grep -rlE '#!?\[repr\([^]]*packed' --include='*.rs' "${finding_scope_root:-.}" | head -1
# has_fs_io (path types / construction)
grep -rlE '\bPathBuf\b|\bPath\b' --include='*.rs' "${finding_scope_root:-.}" | head -1
# has_fs_io (fs module and file APIs)
grep -rlE '\bfs::|\bFile::(open|create)\b|OpenOptions|\.exists\(\)|\.metadata\(|symlink_metadata|read_dir|read_to_string' --include='*.rs' "${finding_scope_root:-.}" | head -1
ほかのフラグと同様に、どちらかのプローブで出力が空でなければ、フラグは true です。これらの検出器は意図的に慎重に設計されています。判断に迷う場合はフラグを true にします。誤検知によるフラグ設定は無害な追加ワーカーが起動するだけですが、誤検出によって実際のパスがスキップされることは避けるためです。has_fs_io は、単独の .join(/.push( 呼び出しではなく、パスタイプ(PathBuf/Path。&Path パラメーターや単独の Path:: 呼び出しも対象)と、ファイルシステムを示す識別子(fs::/File::/OpenOptions/read_dir/…)を基準にします。パス構築はパスタイプの識別子を通じて検出されるため、判定から join/push を除外することで、無関係なイテレーターや JoinHandle の join、ほぼすべてのクレートで判定が発動してしまう Vec::push との誤マッチを避けられます。
Cargo.toml に関する注意:スコープのサブパスにまだ .await がなくても has_async=true を設定できるよう、[dependencies] tokio、async-std なども確認してください(ライブラリークレートでは再エクスポートしていることがよくあります)。
また、Cargo.toml の有無も確認してください(参考情報です。監査が Cargo ワークスペース、単一クレート、または単独の .rs ファイルを対象にしたかを run-summary.md に記載します)。
# context_roots may be comma-separated (build_run_plan.py treats it as a list),
# so probe each root rather than passing "a,b" as one (nonexistent) path.
echo "${context_roots:-.}" | tr ',' '\n' | while IFS= read -r root; do
find "${root:-.}" -name 'Cargo.toml' -print -quit
done | head -1
フェーズ 2:出力ディレクトリー
開始条件: フェーズ 1 のフラグが設定されていること。終了条件: 絶対パスの output_dir が解決され、${output_dir}/findings/ と ${output_dir}/coverage/ が存在すること。
output_dir の絶対パスを解決します(既定値:$(pwd)/.rust-review-results/$(date -u +%Y%m%dT%H%M%SZ)/)。
mkdir -p "${output_dir}/findings" "${output_dir}/coverage"
coverage/ サブディレクトリーには、ワーカーごとのカバレッジゲート監査ファイル(coverage/worker-{N}.md)が保存されます。ワーカーは表を返信に埋め込まず、ここに書き込みます。agents/rust-review-worker.md の手順 5 を参照してください。
フェーズ 3:コードベースのコンテキスト
開始条件: ${output_dir} が存在します。終了条件: ${output_dir}/context.md が書き込まれています。
README.{md,rst,txt} と、ビルド/マニフェストファイル(Cargo.toml、Cargo.lock、rust-toolchain.toml、build.rs)にざっと目を通します。Read を実行する前に、find(Bash 経由)で事前確認してください(存在しないファイルに対する Read はターンを中断します。Glob はこのオーケストレーターが Bash を保持しているため利用できません)。事前確認には Bash: ls README* を使用しないでください。zsh では、glob に一致するファイルがない場合、2>/dev/null が実行される前に複合コマンド全体が中断されます。一致するファイルがなくても失敗しない、次のコマンドを使用してください。find . -maxdepth 2 -name 'README*' -o -name 'Cargo.toml' -o -name 'rust-toolchain.toml' -o -name 'build.rs'
${output_dir}/context.md を作成し、次の内容を記述します。YAML フロントマター(threat_model、severity_filter、scope_subpath、finding_scope_root、context_roots、has_unsafe、has_ffi、has_concurrency、has_async、has_packed_repr、has_fs_io、output_dir、cargo_manifest。cargo_manifest は workspace/single-crate/absent のいずれかとし、存在する場合はパスも記載します)。続いて、次の 5 つのセクションからなる短い Markdown 本文を記述します。目的(1~3 文)、スコープ(finding_scope_root に含まれるもの、およびその範囲外の検出結果は対象外であること)、エントリーポイント(信頼できないデータの入力元:ネットワーク、ファイル、CLI、IPC、serde によるデシリアライズ、FFI 入力)、信頼境界(サンドボックス化された相手、信頼された相手、不特定のリモート相手の区別)、既存の強化策(ファジングハーネス、MIRI の実行、clippy::pedantic、cargo-deny、cargo-audit)。
フェーズ 4: 実行プランの構築 (決定論的)
開始条件: ケイパビリティフラグ + threat_model が判明している; ${output_dir}/findings/ が存在する。 終了条件: ${output_dir}/plan.json と ${output_dir}/worker-prompts/*.txt が書き込まれている; M = worker_count が判明している。
選択、フィルタリング、パス解決、およびスポーンプロンプトのレンダリングは、スポーンプロンプトを完全かつ一貫した状態に保つため、スクリプトに委譲されます:
uv run --no-project "${RUST_REVIEW_PLUGIN_ROOT}/scripts/build_run_plan.py" \
--plugin-root "${RUST_REVIEW_PLUGIN_ROOT}" --output-dir "${output_dir}" \
--threat-model "${threat_model}" --severity-filter "${severity_filter}" \
--scope-subpath "${finding_scope_root:-.}" --context-roots "${context_roots:-.}" \
--has-unsafe "${has_unsafe}" --has-ffi "${has_ffi}" \
--has-concurrency "${has_concurrency}" --has-async "${has_async}" \
--has-packed-repr "${has_packed_repr}" --has-fs-io "${has_fs_io}" \
--max-passes-per-worker 4
スクリプトは plan.json + worker-prompts/worker-N.txt + (--cache-primer=true の場合、これがデフォルト) worker-prompts/cache-primer.txt を書き込み、stdout に JSON サマリーを出力します。プロンプトが1つでも欠けている場合は非ゼロで終了します — メッセージを表示して停止してください。デフォルトの --max-passes-per-worker 4 では、プランナーは約8クラスターを選択します → 純粋な安全な Rust (FFI / 並行性 / 非同期なし; info-disclosure は常に有効) では M ≈ 13 ワーカー、並行安全な Rust では約10クラスター → M ≈ 15、フル Rust (unsafe + FFI + 並行性 + 非同期、さらに has_fs_io の場合は input-os-safety、has_packed_repr の場合は layout-safety) では約15クラスター → M ≈ 23 となります。M はチャンク化後のワーカー数 (plan.workers.length) であるため、クラスター数を上回ります — チャンク化は複数パスの非統合クラスター (例: panic-dos、memory-safety) を分割しますが、2つの統合クラスター (unsafe-boundary、concurrency-locking) は決してチャンク化されません。それぞれ1つのワーカーが共有インベントリを一度構築し、そのすべてのフェーズを実行します。recursion-dos はワーカーあたり1パスです。戻った後は、構造化された選択のために plan.json を読み取ってください — フィルタリングやパスを再導出しないでください。
--max-passes-per-worker N はワーカーあたりのパス数を上限設定します。プランナーは、N を超えるパスを持つ非統合クラスターを決定論的に ceil(K/N) 個の連続するチャンクに分割します。各チャンクは独自の rust-review-worker スポーンとなり、-{i} を接尾辞とする cluster_id (例: panic-dos-1、panic-dos-2) を持ちます。統合クラスター (unsafe-boundary、concurrency-locking) は対象外です — パス数やオーバーライドに関係なく決してチャンク化されません — そのため1つのワーカーが共有のフェーズ A インベントリを一度構築し、すべてのフェーズを実行します (統合クラスターをチャンク化すると、各チャンクがそのインベントリを再構築することを強いられますが、実際にはワーカーはそれをスキップします)。共有プロンプトキャッシュプレフィックスと Cluster prompt: パスはチャンク間でバイト単位で同一であるため、キャッシュプライマーは依然としてすべてのワーカーをウォームします。デフォルトの4は manifest.json のヘビーテールクラスターに対して調整されています。出力の多い一部の非統合クラスターは、より小さなマニフェストレベルの max_passes_per_worker オーバーライドを宣言しており、各高コストなパスが独自のワーカーを取得します (例: recursion-dos)。--max-passes-per-worker 0 を渡すと、マニフェストオーバーライドを含むすべてのチャンク化が無効になります (クラスターあたり1ワーカー)。
フェーズ 5: 記帳タスクの作成 (オーケストレーター内部)
開始条件: ${output_dir}/plan.json が存在する; M = plan.workers.length。 終了条件: cluster_task_ids[] が作成されている (plan.workers と1:1対応)、すべて pending。
タスク台帳はオーケストレーターの記帳専用です (TUI の可視性 + フェーズ7のリトライ追跡) — ワーカーは決してそれを読み書きしません。ワーカーごとに1つの TaskCreate を実行し、metadata に kind="cluster"、worker_n、cluster_id、spawn_prompt_path、pass_prefixes、attempt=1 を設定します — すべての値は plan.workers[i] からそのままコピーします。cluster_task_ids[] を plan.workers の順序で追跡します。
フェーズ 6: ワーカーのスポーン (オプションでキャッシュプライマーを先に、次に M を並列で)
開始条件: cluster_task_ids[] が設定されている; ワーカーごとのスポーンプロンプトファイルが ${output_dir}/worker-prompts/worker-N.txt に存在する。 終了条件: すべてのウェーブにわたる M 個すべての Agent 呼び出しが戻っている (並列スポーンブロックが完了している)。
フェーズ 6a:キャッシュプライマー(plan.run.cache_primer が有効な場合)
コールドスタートからの並列バッチではキャッシュを共有できません(すべての M リクエストが同時にディスパッチされ、書き込みを完了したものがないためです)。プレフィックスをウォームアップするには、まず小さなプライマーを起動します — フォアグラウンドで起動してください(バックグラウンドでの起動では、後続のフォアグラウンド起動とキャッシュを共有できません)。
plan.run.cache_primer == true の場合、build_run_plan.py は ${output_dir}/worker-prompts/cache-primer.txt を書き出しています。プライマーは独立したアシスタントメッセージで起動します。ファイルを Read し、その内容をそのまま Agent の prompt に渡し、subagent_type=rust-review:rust-review-worker、model=${worker_model}、description="Rust review cache primer" を指定します。run_in_background は指定しません。スクリプトは <context> ブロックまでのプレフィックスを worker-1.txt とバイト単位で同一になるように書き出しています。このバイト単位の同一性によって、並列ワーカーのキャッシュヒットが実現します。プライマーの末尾には Cache primer: true が含まれており、ワーカーのシステムプロンプトはこれを第一級のモードとして扱い、ツール呼び出しを一切行わず、テキスト応答 1 件で worker-PRIMER abort: cache primer (no analysis performed) をそのまま返します。この中断行は破棄してください — フェーズ 7 では無視されます(worker-N の ID がないため)。
フォアグラウンドでの起動はすでにシリアライズされるため、フェーズ 6b の前に sleep は不要です。plan.run.cache_primer == false の場合は、フェーズ 6a 全体を省略してください。
フェーズ 6b:実際の M 個のワーカーを並列で起動する(1 メッセージあたり最大 16 件のウェーブ)
停止 — 起動メッセージを作成する前に、これを読んでください。
ワーカーはフォアグラウンドで起動する必要があります(
run_in_backgroundフィールドを指定しないか、run_in_background=falseを指定します)。 ここでいう「並列」とは、そのウェーブのAgent呼び出しを含む 1 つのアシスタントメッセージを意味します — それだけで同時実行されます。(M が大きい場合は、連続するウェーブに分け、1 ウェーブあたり最大 16 件の呼び出しを 1 メッセージにまとめます — 下記の「必須の起動形式」を参照してください。)このスキルでは、バックグラウンド起動によって並列化してはいけません。バックグラウンド起動では、フェーズ 6a のプライマーキャッシュが無効になります。各ワーカーは最初のターンでキャッシュの作成コストをすべて負担し(
cache_read_input_tokens=0)、約 15 K トークンのプライマーが M 回分無駄になります。実際の 2 回の実行で、すべてのワーカーのfirst_cr=0というまさにこの症状が確認されています。起動メッセージを送信する前に、下書きを監査してください。すべての
Agent呼び出しにrun_in_backgroundキーがないことを確認します。run_in_background=trueと書いた場合は削除してください。
必須の起動形式: そのウェーブの Agent ツール呼び出しを含む単一のアシスタントメッセージを出力してください — その 1 つのメッセージによって同時実行されます。逐次起動(1 メッセージにつき Agent 呼び出しを 1 件ずつ)はレビューをシリアライズするため、これも誤りです。ただし、この失敗は目に見えます(所要時間)。一方、バックグラウンド起動の失敗は見えません(コスト)。
M が 1 メッセージあたりの上限を超える場合のウェーブ。 ハーネスは、1 つのアシスタントメッセージからディスパッチする Agent 呼び出し数に上限を設けています(観測例:Claude Code では約 20 件 — 実際の 25 ワーカーの実行では、最初の 20 件だけが黙って実行され、残りの 5 件を 2 つ目のメッセージで起動する必要がありました)。そのため、M > 16 の場合は事前にウェーブを計画し、ワーカーを連続するウェーブに分割します。各ウェーブは、Agent 呼び出し 16 件以下で構成し、それぞれ単一のアシスタントメッセージにまとめます。ルール:
- ウェーブ内: 1 つのメッセージ内のすべての
Agent呼び出しを、フォアグラウンドで(run_in_backgroundなしで)行います。単一ウェーブ実行と同一の形です。 - ウェーブ間: ウェーブ k+1 は別のメッセージであり、ウェーブ k の
Agent呼び出しがすべて戻った後にのみ送信できます(ツール使用メッセージはターンを終了します)。したがってウェーブ同士は直列化されます。これは正しく、そして明示的です。それを受け入れてください。それらを重ね合わせようとしないでください。 - 1 つのメッセージにより多くのワーカーを詰め込むために
run_in_background=trueに手を伸ばすことは決してしないでください。より多くのウェーブであり、決してバックグラウンドではありません。バックグラウンドはプライマーキャッシュを無効化し(STOP ボックスを参照)、このスキルが防ごうとしている重大なエラーです。 - ウェーブ間のキャッシュ: プライマープレフィックスには約 5 分のキャッシュ TTL があり、ヒットするたびに更新されるため、連続するウェーブはそれをヒットし続けます(25 ワーカーの実行では、2 番目のウェーブで
cache_read≈14 Kが確認されました)。後のウェーブが前のウェーブから約 5 分以上経過してから開始される場合(非常に大きなMまたは遅いワーカー)、そのウェーブの前にまず独自のメッセージでフェーズ 6a のプライマーを再スポーンして、プレフィックスを再ウォームアップしてください。 - ウェーブのバランスを取る(例:
M=25→ 13+12、20+5 ではない)ことで、どのウェーブも上限に張り付かず、最後のウェーブが小さな取り残しになりません。 - すべてのウェーブが戻った後、完全な M ワーカー結果のセットでフェーズ 7 に進んでください。
各ワーカー N ∈ [1..M] について(割り当てられたウェーブ内で):
Read: ${output_dir}/worker-prompts/worker-N.txt- ファイルの内容をそのまま
Agentツールのprompt引数として渡します:
| パラメーター | 値 |
|---|---|
subagent_type | rust-review:rust-review-worker |
model | ${worker_model}(haiku / sonnet / opus) |
description | Rust review worker N |
prompt | worker-N.txt の全文(編集なし) |
run_in_background | フィールドは省略するか、false に設定する必要があります。 決して true にしないでください。上記のフォアグラウンドスポーンの警告を参照してください。 |
スポーンプロンプトが唯一の権威です。それをそのまま渡してください。すべてのフィールドはワーカーの自己チェックで必須です。少しでも逸脱すると worker-N abort: spawn prompt malformed が発生します。
拒否すべきアンチパターン:
run_in_background=trueを渡す(上記の警告を参照)。Mが大きいときに1 つのメッセージに約 16 を超えるAgent呼び出しを詰め込む — ハーネスは最初の約 20 のみを黙って保持し、残りを破棄します。すべての M をカバーするには、≤16 のバランスの取れたウェーブを使用し、決してバックグラウンドスポーンを使用しないでください。worker-N.txtを読み取る代わりにスポーンプロンプトを手入力する。- Task 関連の指示(「最初に TaskList を呼び出す」、「割り当てられたタスク ID: 」)を挿入する。ワーカーには Task ツールがありません。
- 渡す前にレンダリングされたプロンプトを編集する(「冗長な」フィールドを削る、パスリストを折りたたむ)。
フェーズ 7:ワーカーの完了を待ち、結果を分類する
開始条件: フェーズ 6 の M 件の Agent 呼び出しがすべて戻っていること。終了条件: すべてのクラスターが成功するか、上限まで再試行されていること。${output_dir}/findings-index.txt が書き込まれていること。
フェーズ 6 の Agent 呼び出しは、各ワーカーが戻るまでブロックします。各ワーカーの戻りテキストを調べ、以下の分類を順に適用してください。最初に一致したものが適用されます。
| # | 一致条件(戻りテキスト内) | 結果 | アクション |
|---|---|---|---|
| 1 | worker-N complete: | 暫定成功 | wrote N finding files の件数を解析し、TaskUpdate で completed にする前に、以下のアーティファクト検証を実行します。 |
| 2 | abort: spawn prompt malformed、abort: pre-work budget exceeded、または abort: TaskList unavailable(レガシー) | 再試行不可のオーケストレーターのバグ | 実行を停止し、中断内容とスポーンプロンプトのパスを報告します。同じプロンプトを再実行しても失敗が繰り返されます。事前作業予算の超過は、常にワーカーがセルフチェックに合格できなかったことを意味し、再試行しても解決しません。 |
| 3 | その他の worker-N abort: | 再試行可能 | pending に設定し、metadata.abort_reason を設定し、needs_respawn=true にして、attempt をインクリメントします。 |
| 4 | Agent がエラーになった、または complete:/abort: トークンがない | 再試行可能 | #3 と同じです(一時的なワーカーのクラッシュ)。 |
再試行不可のものがあれば、そこで停止します。それ以外の場合は、再スポーンする前に、再試行可能な各ワーカーのプレフィックス領域をディスク上でクリアしてください。フェーズ 7 のインデックスはディスクから作成されるため、クラッシュした試行で生成された、より大きな ID の残存ファイル(置き換え後のワーカーが再生成しないファイル)がレポートに復活してしまいます。ワーカーの実際の pass_prefixes(タスクの metadata から取得)を順に処理し、それぞれの実際のプレフィックスを ${pfx} に代入してください。リテラルの PREFIX を使ってコマンドを実行しないでください。
# zsh-safe: `find … -delete` never aborts on no-match (an `rm PREFIX-*.md` glob would).
# Replace `PREFIX1 PREFIX2` with the worker's actual space-separated pass_prefixes.
for pfx in PREFIX1 PREFIX2; do
find "${output_dir}/findings" -maxdepth 1 -type f -name "${pfx}-*.md" -delete
done
その後、attempt <= 2 の pending 状態にある再試行可能な各ワーカーを、1 つの並列ブロックで再スポーンします(上限はクラスターあたり 2 回の試行です)。最初の失敗時に attempt はすでに 2 にインクリメントされているため、1 回の再試行を可能にするには、ガードで 2 を許可する必要があります。attempt < 2 では、すべての再試行がブロックされます。2 回目の失敗では 3 にインクリメントされ、<= 2 を満たさなくなるため、再試行は終了します。置き換え後のワーカーはプレフィックスごとに決定論的な検出 ID を再利用するため、プレフィックス領域をクリアしてから新たに書き込むことで、シャード、カバレッジ、ディスク上のデータセットの一貫性が保たれます。
整合性チェックとインデックスの書き込み
仮の complete: クラスターごとに、タスクを完了としてマークする前に、ワーカー所有のシャード、カバレッジファイル、カバレッジ行、登録済み ID、申告された検出件数を plan.json と照合して検証します。完了したワーカーごとに 1 つのコマンドを実行するか、複数のワーカーを 1 つのコマンドで検証します。以下の申告件数のどちらの形式も有効です。--claimed-count フラグの後にまとめて指定するか、フラグを繰り返すことなく、worker-N=N の値だけを渡してはいけません。
uv run --no-project "${RUST_REVIEW_PLUGIN_ROOT}/scripts/validate_artifacts.py" "${output_dir}/plan.json" \
--worker worker-N --claimed-count worker-N=<claimed_count_from_complete_line>
uv run --no-project "${RUST_REVIEW_PLUGIN_ROOT}/scripts/validate_artifacts.py" "${output_dir}/plan.json" \
--worker worker-1 --worker worker-2 \
--claimed-count worker-1=0 worker-2=3
uv run --no-project "${RUST_REVIEW_PLUGIN_ROOT}/scripts/validate_artifacts.py" "${output_dir}/plan.json" \
--worker worker-1 --worker worker-2 \
--claimed-count worker-1=0 --claimed-count worker-2=3
検証がゼロ以外の終了コードで終了した場合、その完了は不正な形式で再試行可能なものとして扱います(分類器の行 #4)。タスクを pending にし、バリデーターの出力を metadata.abort_reason に保存し、needs_respawn=true に設定して、attempt を増やします。findings-index.d/worker-N.txt の欠落、coverage/worker-N.md の欠落、カバレッジ行の欠落、無効な skipped: 行、シャードまたはディスクに存在しない登録済み ID、申告件数の不一致は、すべて不正な完了です。再試行上限に達した後は、クラスタータスクを未完了のままにし、バリデーターの出力を run-summary.md と最終応答に記載します。検証に合格した仮の完了だけを TaskUpdate で completed にできます。
次に、インデックスを構築します。正規のインデックスは、シャードの和集合ではなく、実際にディスク上に存在する検出ファイルの集合です。ディスクから構築することで、対応するシャードエントリなしに書き込まれた検出(Write とシャードへの追記の間にクラッシュしたワーカー、またはワーカープロンプトが警告する単一プレフィックスの空シャードの落とし穴)も、黙って消えることなく重複排除 → fp-judge → REPORT/SARIF に取り込まれ、すべてのインデックスエントリが実在するファイルを参照することが保証されます。
# Canonical index = every finding file on disk. `find` never fails on no-match
# (an empty findings/ yields an empty index — the unambiguous "zero findings"
# signal). `sort -u` collapses Phase-7 retry duplicates: replacement workers reuse
# deterministic ids, so the same path appears once.
find "${output_dir}/findings" -maxdepth 1 -type f -name '*.md' 2>/dev/null \
| sort -u > "${output_dir}/findings-index.txt"
# Reconcile against the per-worker shards: any path on disk but in NO shard is an
# orphan whose worker failed to record it. It is already in the index above (so it
# is NOT dropped) — print it so the bookkeeping gap can be surfaced. Non-fatal.
if [ -d "${output_dir}/findings-index.d" ]; then
# Reconcile by basename (finding ids are unique), so a path-format difference
# between the worker `find` and this one (trailing slash, /var↔/private/var)
# cannot manufacture false orphans. Any basename on disk but in no shard is an
# orphan whose worker failed to record it.
comm -13 \
<(find "${output_dir}/findings-index.d" -maxdepth 1 -type f -name 'worker-*.txt' -exec awk 1 {} + 2>/dev/null | sed 's#.*/##; /^[[:space:]]*$/d' | sort -u) \
<(find "${output_dir}/findings" -maxdepth 1 -type f -name '*.md' 2>/dev/null | sed 's#.*/##' | sort -u)
fi
シャードは引き続きワーカーごとの監査証跡(validate_artifacts.py が検査します)および dedup-judge のクラッシュ復旧用フォールバックとして使われますが、パイプラインに渡される対象を制限するものではなくなります。reconcile が出力する孤立した basename ごとに、plan.json を使ってその <PREFIX> の所有ワーカーを特定し、そのワーカーのシャードが不完全だったことを run-summary.md に記録します。検出はすでにインデックスに含まれているため失われませんが、記録上の不整合は明らかにしておく必要があります。それでも、インデックスの行数をワーカーが申告した wrote N の合計と照合し、不一致は記録しますが、中断はしません。
タスクの更新とインデックスの作成後、TaskList を実行し、${output_dir}/run-summary.md に次の内容を書き込みます。
- 解決済みのパラメーター(
threat_model、severity_filter、finding_scope_root、context_roots、機能フラグhas_unsafe/has_ffi/has_concurrency/has_async、Cargo マニフェストの状態) - ワーカー結果の表(
worker_n、cluster_id、申告された検出件数、シャードの行数、カバレッジファイルのパス(coverage/worker-{N}.md)、タスクの状態、再試行/中断の状態) findings-index.txtの行数、およびワーカーの申告件数との不一致- Phase 8 完了後の judge の状態、または judge がスキップ/失敗した理由
Phase 5 のクラスタータスクが completed でない場合、またはいずれかのワーカーが truncated at hard cap トークンを含む complete: 行を返した場合(すべてのパスを検索する前にツール呼び出し上限に達しており、カバレッジファイルには cleared (NOT SEARCHED — truncated at hard cap) の行が 1 行以上表示されます)、そのことを run-summary.md と最終応答で目立つように記載します。ハードキャップで打ち切られたワーカーは台帳上 completed としますが、結果は部分的です。成功レポートによって不完全なカバレッジが隠されないよう、completed という状態に頼ってはいけません。
検出件数がゼロの場合も、必ず Phase 8 を実行してください。 インデックスが空の場合、どちらの judge も短絡終了します。dedup-judge は最小限の no-op dedup-summary.md を書き込み、fp-judge は空の REPORT.md/REPORT.sarif を書き込むため、SARIF コンシューマーに対して安定した成果物一式を提供できます。
フェーズ 8:判定パイプライン(順次実行、重複排除 → FP+重大度)
開始条件: findings-index.txt が存在すること。 終了条件: dedup-judge と fp-judge が戻り、dedup-summary.md、fp-summary.md、REPORT.md、および可能であれば REPORT.sarif が書き出されていること。
各判定エージェントの完全なプロトコルは、そのシステムプロンプト(agents/rust-review-{dedup,fp}-judge.md)に記載されています。起動プロンプトでは、実行ごとの変数だけを渡します。prompts/internal/judges/ を参照しないでください。これらのファイルは存在しません。
停止 — この 2 つの判定エージェントは並列ではなく、順次実行します。 Phase 6b のワーカー(並行実行されるよう、1 つのメッセージで M 件の
Agent呼び出しとして起動するもの)とは異なり、判定エージェントには厳密なデータ依存関係があります。fp-judge は dedup-judge が書き込むmerged_into/also_known_as注釈を確認する必要があり、merged_intoがすでに付いているファイルのみをスキップします。両方のAgent呼び出しを 1 つのメッセージに記述すると、並行実行されます。すると fp-judge はマージ注釈が付く前に検出結果を読み込み、重複をすべて個別の主要検出結果として判定し、さらにdedup-summary.mdがまだ存在しないため、「dedup が実行されなかった」場合のフォールバックが作動して、重複した膨大なREPORT.md/SARIF が生成されます。dedup-judge は専用のアシスタントメッセージで起動し、その
dedup-judge complete:(またはabort:)の戻りを待ってから、別のメッセージで fp-judge を起動してください。fp-judge の起動メッセージを作成する前に、dedup が完了したことを確認してください。Bash: test -f ${output_dir}/dedup-summary.mdが成功すること(または dedup のcomplete:トークンを確認済みであること)が必要です。2 つの判定エージェントのAgent呼び出しを、決して同じメッセージに含めないでください。
- 最初のメッセージ —
Agent(subagent_type="rust-review:rust-review-dedup-judge", description="Dedup judge", prompt=f"output_dir: {output_dir}")。戻りを待ち、続行する前に(以下に従って)分類します。 - その後、別のメッセージで —
Agent(subagent_type="rust-review:rust-review-fp-judge", description="FP + severity judge", prompt=f"output_dir: {output_dir}\nsarif_generator_path: {sarif_generator_path}")。sarif_generator_pathは${RUST_REVIEW_PLUGIN_ROOT}/scripts/generate_sarif.pyに解決します。
判定エージェントの失敗への対応。 Phase 7 の分類と同じ形式を、判定エージェントの戻りテキストに適用します。
… complete:→ 成功です。… abort:→ その判定エージェントについて再試行できません。abort 行とls -l ${output_dir}/findings-index.txtの結果を提示し、それでも Phase 8b を実行します(Phase 8b は、判定エージェントが abort した場合でも成果物一式を保証する SARIF とREPORT.mdのセーフティネットです。Phase 8b の「fp-judge が戻った場合、または実行が途中で中断された場合」の開始条件を参照してください)。その後、追加の判定エージェントを起動せずに停止します。「停止」とは、判定パイプラインを続行しないという意味です。Phase 8b を省略するという意味ではありません。complete:がない場合(ヘルプメッセージ/エラー/質問)→ 1 回だけ再試行できます。 新たに起動するのではなく、SendMessage(to=<agentId>, …)を使います(エージェントはすでにプロトコルの解析にコストをかけているためです)。findings-index.txtに記載された検出結果のパスを明示的に含めます。2 回目も失敗した場合は、トランスクリプトを提示して Phase 8b に進みます。
フェーズ 8b:レポートのセーフティネット(SARIF + REPORT.md)
開始条件: fp-judge が戻った、または実行が途中で中断されました。終了条件: ${output_dir}/REPORT.sarif と ${output_dir}/REPORT.md の両方が存在します。
test -d "${output_dir}/findings" && uv run --no-project "${RUST_REVIEW_PLUGIN_ROOT}/scripts/generate_sarif.py" "${output_dir}"
findings/ が存在する場合は、SARIF ジェネレーターを必ず実行します。ジェネレーターは冪等(完全に上書き)で、該当する検出結果が 0 件の実行では results: [] を出力し、部分的な実行にも対応します(fp_verdict のない検出結果は LIKELY_TP として出力されます。重大度がジャッジによって検証されていないため、severity_filter の対象外となり、unjudged: true / severity_validated: false のマークと [UNVALIDATED SEVERITY — not judged] というメッセージ接頭辞が付与されます。そのため、推定された重大度が medium/high フィルターによって気づかれないまま除外されることはありません)。常に上書きすることで、fp-judge が書き込み中にクラッシュし、破損した REPORT.sarif がディスクに残る事態を防ぎます。
ジェネレーターが標準出力に WARNING: skipped N … という行を出力した場合(SARIF の invocations[].properties.skipped_findings にも記録され、除外されたファイルごとに warning 通知が出されます)、1 つ以上の検出結果ファイルが読み取れなかったか、解析可能なフロントマターがなく、レポートから除外されたことを示します。これは結果の欠落です。未完了のクラスタタスクと同様に、件数とパスを run-summary.md および最終回答で明確に示してください。ほかの点で問題のない SARIF によって、この欠落が隠れないようにしてください。
次に、REPORT.md が必ず存在するようにします。機械的に生成される SARIF とは異なり、REPORT.md は fp-judge が選別して作成する成果物であるため、ジャッジが作成したファイルを上書きしないでください。(fp-judge は Write ツールではなく Bash のヒアドキュメントで REPORT.md を書き込みます。ハーネスがサブエージェントのレポートファイルに対する Write ツールをブロックするためです。ジャッジに対して Write を改めて必須にするような「修正」はしないでください。オーケストレーターはメインエージェントであり、この制限の対象ではないため、以下の Write は機能します。)ファイルの有無を確認し、存在しない場合(ジャッジがクラッシュした、Bash のヒアドキュメントによる書き込みにも失敗した、またはファイルを書かずにレポートをチャットテキストとして返した場合)は、実行を失敗させるのではなく、オーケストレーターが REPORT.md を自ら書き込みます。
- fp-judge がレポート本文をトランスクリプトで返している場合は、そのテキストを一字一句そのまま
${output_dir}/REPORT.mdにWriteします。 - そうでない場合は、ディスク上の検出結果からレポートを作成します。
findings-index.txtに記載された、最終的に残ったプライマリ検出結果(fp_verdict ∈ {TRUE_POSITIVE, LIKELY_TP}であり、merged_intoがないもの。ジャッジが実行されていない場合は、fp_verdictのない検出結果を最終結果として扱います)を対象にします。判定済みの最終結果にのみcontext.mdのseverity_filterを適用します。未判定の検出結果(fp_verdictなし)はフィルターに関係なく含め、SARIF と同様に、厳格なフィルターで気づかれないまま除外されないよう、Unvalidated (severity not judged)セクションに[UNVALIDATED SEVERITY — not judged]ラベルを付けて表示します。fp-judge のテンプレートに沿ったREPORT.mdをWriteします。内容は、YAML フロントマター(stage: final-report、threat_model、severity_filter、total_primaries、reported_findings)、重大度分布テーブル、重大度ごとにまとめた報告対象の検出結果セクションで構成します。CRITICAL/HIGH については Description/Code/Data flow/Impact/Recommendation の本文を埋め込み、MEDIUM/LOW については検出結果ファイルを参照します。
いずれの場合も、REPORT.md がオーケストレーターによって合成された(ジャッジが作成したものではない)ことを ${output_dir}/run-summary.md に記録します。${output_dir}/findings/ が存在しない場合(フェーズ 2 が失敗した場合)に限り、SARIF ジェネレーターの実行とこの確認を省略してください。このフェーズの後、ジャッジ/SARIF/レポートのステータスを ${output_dir}/run-summary.md に更新します。
フェーズ 9:レポートの返却
開始条件: フェーズ 8b が完了していること。終了条件: 成功基準 のすべてが真であることを確認し、REPORT.md を呼び出し元に返却すること。
応答を作成する前に、以下の 成功基準 のチェックリストを確認し、ディスク上の成果物(クラスタータスクは TaskList、ファイルは ls/Read)に照らして各項目を検証してください。基準を満たさない項目がある場合は、その不備を応答内で明確に示してください。部分的な実行を成功レポートで隠してはいけません。
続いて Read ${output_dir}/REPORT.md を実行し、その内容を呼び出し元に返してください。findings/、findings-index.txt、run-summary.md、dedup-summary.md、fp-summary.md、REPORT.md、REPORT.sarif を指す成果物リストを追加してください。
検出ファイルのフロントマター — 3 段階
正式なスキーマ:agents/rust-review-worker.md(「Finding File Format」)。3 段階で書き込みます。
- ワーカー — 基本フィールド(
id、bug_class、title、location、function、confidence、worker)と 7 つの本文セクションを記述します。 - 重複判定担当 — 重複項目には
merged_intoを追加し、統合元となった主検出項目にはalso_known_asとlocationsを追加します。 - 誤検出+重大度判定担当 — すべての主検出項目に
fp_verdictとfp_rationaleを追加します。生き残った検出項目(TRUE_POSITIVE/LIKELY_TP)には、さらにseverity、attack_vector、exploitability、severity_rationaleを追加します。
バグクラス/クラスター
正式な定義:prompts/clusters/manifest.json。37 のバグクラスは always ゲート付きクラスターに属するため、クラスターは常に実行されます。そのうち 35 クラスは常に実行され、残る 2 クラス(logic-correctness の adversarial-trait(TRAITADV)と closure-panic(CLOSUREPANIC))には追加で requires: has_unsafe が指定されているため、has_unsafe=true の場合にのみ実行されます。すべての条件付きゲートが有効な場合、全クラスターで 69 のバグクラスが対象になります。memory-safety クラスターは has_unsafe を条件とし(すべてのバグクラスで unsafe が必要)、PATHJOIN と TOCTOU は input-os-safety 経由で has_fs_io を条件としており、PACKEDREF は条件付きの layout-safety クラスター(has_packed_repr)に属します。PTREXPOSE は info-disclosure クラスター経由で常時有効です。unsafe-boundary と concurrency-locking は完全に統合済みです(実行時にサブプロンプトを再読み込みしません)。
成功基準
各フェーズの終了条件で大半は確認済みです。オーケストレーターから確認できる最終状態は次のとおりです。
- フェーズ 5 のクラスタータスクがすべて
completedであること(TaskListで確認します)。 ${output_dir}/run-summary.mdが存在し、解決済みのスコープ/コンテキスト、Cargo マニフェストのプローブ結果、ワーカーの申告数とインデックスの件数、タスクの状態、判定/SARIF の状態が記録されていること。- すべての主検出項目(
merged_intoがないもの)にfp_verdictとfp_rationaleがあること。生き残った検出項目(TRUE_POSITIVE/LIKELY_TP)には、さらにseverity、attack_vector、exploitability、severity_rationaleがあること。 REPORT.mdが存在し、severity_filterに従って重大度でフィルタリングされていること(フェーズ 8b のセーフティネットにより、誤検出判定担当が書き込みに失敗した場合も保証されます)。REPORT.sarifが存在すること(フェーズ 8b のセーフティネットにより保証されます)。