rust-review

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Rust Security Review

Rust安全审查

Runs in the main conversation (invoke via
/rust-review:rust-review
). Orchestrator owns the
Task*
ledger as bookkeeping for retries; workers and judges have no Task tools. Workers and judges are named plugin subagents (
rust-review:rust-review-worker
,
rust-review:rust-review-dedup-judge
,
rust-review:rust-review-fp-judge
); tool sets are declared in
plugins/rust-review/agents/*.md
. Findings are exchanged via markdown-with-YAML files in a shared output directory.
在主对话中运行(通过
/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文件交换。

When to Use

使用场景

Rust application/library security review: safe/unsafe boundary auditing, memory safety in
unsafe
blocks, concurrency hazards, panic-induced DoS on servers, FFI safety, async-runtime mistakes.
Rust应用/库安全审查:safe/unsafe边界审计、
unsafe
块中的内存安全、并发风险、服务器上panic导致的DoS、FFI安全、异步运行时错误。

When NOT to Use

不适用场景

  • Pure-C / pure-C++ codebases — use
    c-review
    instead.
  • Smart contracts (Solana programs / NEAR contracts / Ink!) — use
    solana-vulnerability-scanner
    or the contract-specific skill.
  • Kernel-mode Rust drivers without userspace allocator — coverage is incomplete; flag as advisory only.
  • Secrets/key memory hygiene (zeroization,
    Zeroize
    /
    ZeroizeOnDrop
    /
    secrecy
    usage, lingering stack/heap copies) — use the
    zeroize-audit
    skill; rust-review does not cover memory zeroization.
  • 纯C/纯C++代码库——改用
    c-review
  • 智能合约(Solana程序/NEAR合约/Ink!)——使用
    solana-vulnerability-scanner
    或合约专用技能。
  • 无用户空间分配器的内核模式Rust驱动——覆盖范围不完整;仅作为建议标记。
  • 密钥/机密内存卫生(零化、
    Zeroize
    /
    ZeroizeOnDrop
    /
    secrecy
    使用、栈/堆残留副本)——使用
    zeroize-audit
    技能;rust-review不涵盖内存零化。

Subagents

子代理

Subagent typePurposeTool set
rust-review:rust-review-worker
Run assigned cluster, write findingsRead, Write, Edit, Bash
rust-review:rust-review-dedup-judge
Merge duplicates (runs first)Read, Write, Edit, Glob
rust-review:rust-review-fp-judge
FP + severity + final reports (runs second)Read, Write, Edit, Bash
Tools come from each agent's frontmatter at spawn time. The orchestrator's
Task*
/
Agent
/
Bash
/etc. come from this skill's
allowed-tools
. Search-tool /
Bash
interaction:
in current Claude Code, an agent granted
Bash
is not also granted the dedicated
Glob
or
Grep
tools (the calls return
No such tool available
; the harness expects
find
/
grep
/
rg
via
Bash
instead). So only the dedup-judge — the one agent that holds no
Bash
— uses
Glob
; the worker, fp-judge, and the orchestrator resolve and search paths with
Read
/
Bash
find
/
rg
/
grep
/
test -f
instead. Because the cluster/finder prompt seeds are written in ripgrep regex syntax (
\s
,
\d
,
\b
),
Bash
-holding agents must run them with
rg
. If
rg
is not installed its call fails loudly (
command not found
) — fall back to
grep -E
with POSIX classes (
\s
[[:space:]]
,
\d
[[:digit:]]
, drop
\b
), never a raw-
\s
grep
whose silent empty becomes a bad
cleared
. Do not reintroduce
Glob
/
Grep
into a
Bash
-holding agent's protocol.

子代理类型用途工具集
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
误报处理 + 严重性评估 + 最终报告(其次运行)Read, Write, Edit, Bash
工具来自每个代理启动时的前置内容。编排器的
Task*
/
Agent
/
Bash
等工具来自本技能的
allowed-tools
搜索工具 /
Bash
交互
:在当前Claude Code中,被授予
Bash
的代理不会同时获得专用的
Glob
Grep
工具(调用会返回
No such tool available
;工具框架期望通过
Bash
使用
find
/
grep
/
rg
)。因此只有去重评审器——唯一不持有
Bash
的代理——使用
Glob
;工作器、误报评审器和编排器通过
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
状态。不要
Glob
/
Grep
重新引入持有
Bash
的代理协议中。

Architecture

架构

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
Output directory contains:
context.md
,
plan.json
,
worker-prompts/
,
findings/
,
findings-index.d/
(per-worker shards),
findings-index.txt
,
coverage/
(per-worker coverage-gate files),
run-summary.md
,
dedup-summary.md
,
fp-summary.md
,
REPORT.md
,
REPORT.sarif
.
Path convention: every later phase shells out to
${RUST_REVIEW_PLUGIN_ROOT}/scripts/*.py
, so resolve that variable first to the plugin directory that contains
prompts/clusters/unsafe-boundary.md
(and
scripts/build_run_plan.py
). Try in order, first hit wins:
  1. Native Claude Code
    ${CLAUDE_PLUGIN_ROOT}
    , accepted if
    Bash: ls "${CLAUDE_PLUGIN_ROOT}/prompts/clusters/unsafe-boundary.md"
    resolves.
  2. Codex
    ${CODEX_PLUGIN_ROOT}
    (set it the same way if that var is present and resolves the marker).
  3. Fallback search — covers Codex installs under
    ~/.codex
    , Claude installs under
    ~/.claude
    , and a local checkout / repo run:
    Bash: find ~/.claude ~/.codex . -path '*/plugins/rust-review/prompts/clusters/unsafe-boundary.md' -print -quit 2>/dev/null
    . Take the match and strip the trailing
    /prompts/clusters/unsafe-boundary.md
    to get the root (the home dirs are searched before
    .
    so an installed copy wins over any vendored copy in the audited repo).
Set
RUST_REVIEW_PLUGIN_ROOT
to the resolved root. If all three fail, abort with a message naming the roots searched — do not enter Phase 4 with an empty variable (every
python3 "${RUST_REVIEW_PLUGIN_ROOT}/scripts/..."
call would fail with a confusing path error).
Scope convention: keep two scopes separate throughout the run:
  • finding_scope_root
    — the user-requested audit subtree. Workers may only file findings whose vulnerable location is inside this subtree.
  • context_roots
    — read-only repo roots/files workers and judges may inspect to verify reachability, callers, wrappers, build flags, mitigations, and threat-model details. Default to
    .
    unless the user explicitly forbids broader context. Reading context outside
    finding_scope_root
    is allowed; filing findings there is not.

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
)的插件目录。按以下顺序尝试,第一个匹配项生效:
  1. 原生Claude Code
    ${CLAUDE_PLUGIN_ROOT}
    ,如果
    Bash: ls "${CLAUDE_PLUGIN_ROOT}/prompts/clusters/unsafe-boundary.md"
    能解析则接受。
  2. Codex
    ${CODEX_PLUGIN_ROOT}
    (如果该变量存在且能解析标记文件,则按相同方式设置)。
  3. 回退搜索 — 覆盖
    ~/.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
    以得到根目录(先搜索主目录再搜索
    .
    ,因此已安装的副本优先于审计仓库中的任何 vendored 副本)。
RUST_REVIEW_PLUGIN_ROOT
设置为解析后的根目录。如果三者都失败,终止并返回包含已搜索根目录的消息——不要在变量为空的情况下进入第4阶段(所有
python3 "${RUST_REVIEW_PLUGIN_ROOT}/scripts/..."
调用都会因路径错误而失败)。
范围约定:在整个运行过程中保持两个范围分离:
  • finding_scope_root
    — 用户请求的审计子树。工作器只能提交漏洞位置在此子树内的检查结果。
  • context_roots
    — 工作器和评审器可检查的只读仓库根/文件,用于验证可达性、调用者、包装器、构建标志、缓解措施和威胁模型细节。默认值为
    .
    ,除非用户明确禁止更广泛的上下文。允许读取
    finding_scope_root
    之外的上下文,但禁止在此范围内提交检查结果。

Rationalizations to Reject

需拒绝的合理化理由

  • "
    unsafe
    is rare, so hand-skip the memory-safety cluster."
    Don't edit the cluster list — set
    has_unsafe
    accurately in Phase 1 and let
    build_run_plan.py
    decide. Every memory-safety bug class (UAF, double-free, uninitialized reads,
    Vec::set_len
    , union UB) requires
    unsafe
    , so the planner runs the whole
    memory-safety
    cluster when
    has_unsafe=true
    and correctly omits it when
    false
    — there is no "run it anyway." The unsafe-boundary cluster is different: it has no
    requires
    and always runs (consolidated; its safety-doc and
    repr(C)
    hygiene apply to FFI declarations even without visible
    unsafe { }
    blocks).
  • "The compiler caught it." The borrow checker proves absence of safe-code data races; it proves nothing about unsafe blocks, panic reachability, ABBA deadlocks, atomic-load/store sequencing, or FFI ABI mismatch.
  • "
    unwrap()
    is fine if it's
    // SAFETY: documented infallible
    ."
    // SAFETY:
    documents
    unsafe
    operations, not infallibility claims. An
    unwrap()
    on documented-infallible input is still risky if the documentation is wrong — file as low severity and let the FP judge decide.
  • "
    has_unsafe=false
    so skip the run."
    Pure safe-Rust crates still have panic-DoS, atomic races, drop-panics, and trait-implementation hazards. Run the always-on clusters.
  • "Background spawns parallelize the workers." They do not —
    Agent
    calls in a single assistant message already run concurrently.
    run_in_background=true
    defeats the Phase 6a primer cache, so every worker pays full cache-creation (
    cache_read_input_tokens=0
    ) and the ~15 K-token primer is wasted M times. Default: omit
    run_in_background
    from worker spawns.
  • "I'll re-derive the cluster list / paths / pass prefixes inline instead of running
    build_run_plan.py
    ."
    The script is the only authority for selection and rendering. Paraphrasing it drops fields that the worker self-check requires, producing
    worker-N abort: spawn prompt malformed
    . Always run the script and
    Read plan.json
    .
  • "The run partially succeeded — I'll just write
    REPORT.md
    from what completed."
    Hiding partial runs behind a successful report is a correctness bug. If any Phase-5 cluster task is not
    completed
    , surface it prominently in
    run-summary.md
    and the final response.
  • "Zero findings — skip Phase 8." Always run both judges and Phase 8b: dedup-judge writes a minimal no-op
    dedup-summary.md
    on an empty index, fp-judge writes empty
    REPORT.md
    /
    REPORT.sarif
    , and Phase 8b's SARIF generator emits
    results: []
    for the empty case. SARIF consumers depend on a stable artifact set.
  • "
    Bash: ls README*
    is fine for the preflight."
    Under zsh, an unmatched glob aborts the whole compound command before
    2>/dev/null
    runs. Use
    find
    (never fails on no-match) — and not
    Glob
    , which is unavailable to an agent that also holds
    Bash
    .

  • "
    unsafe
    很少见,所以手动跳过内存安全集群。"
    不要编辑集群列表——在第1阶段准确设置
    has_unsafe
    ,让
    build_run_plan.py
    决定。每个内存安全漏洞类别(UAF、双重释放、未初始化读取、
    Vec::set_len
    、联合UB)都需要
    unsafe
    ,因此当
    has_unsafe=true
    时,规划器会运行整个
    memory-safety
    集群,当
    false
    时会正确省略——没有“无论如何都运行”的情况。unsafe-boundary集群不同:它没有
    requires
    ,始终运行(合并后的集群;其安全文档和
    repr(C)
    卫生规则适用于FFI声明,即使没有可见的
    unsafe { }
    块)。
  • "编译器已经发现了问题。" 借用检查器证明安全代码中不存在数据竞争;但它无法证明unsafe块、panic可达性、ABBA死锁、原子加载/存储顺序或FFI ABI不匹配的安全性。
  • "如果有
    // SAFETY: documented infallible
    unwrap()
    是没问题的。"
    // SAFETY:
    用于记录
    unsafe
    操作,而不是无错误声明。即使输入被记录为无错误,
    unwrap()
    仍然存在风险——如果文档错误的话。将其标记为低严重性,让误报评审器决定。
  • "
    has_unsafe=false
    所以跳过运行。"
    纯安全Rust crate仍然存在panic-DoS、原子竞争、drop-panics和 trait实现风险。运行始终启用的集群。
  • "后台启动可并行化工作器。" 并非如此——单个助手消息中的
    Agent
    调用已经是并发运行的。
    run_in_background=true
    会破坏第6a阶段的 primer缓存,因此每个工作器都要完全创建缓存(
    cache_read_input_tokens=0
    ),约15K-token的primer会被浪费M次。默认:在工作器启动中省略
    run_in_background
  • "我将在线重新推导集群列表/路径/前缀,而不是运行
    build_run_plan.py
    。"
    该脚本是选择和渲染的唯一权威。改写它会丢失工作器自检查所需的字段,导致
    worker-N abort: spawn prompt malformed
    。始终运行脚本并
    Read plan.json
  • "运行部分成功——我将根据已完成的内容编写
    REPORT.md
    。"
    在成功报告背后隐藏部分运行是正确性错误。如果任何第5阶段集群任务未
    completed
    ,在
    run-summary.md
    和最终响应中突出显示。
  • "没有检查结果——跳过第8阶段。" 始终运行两个评审器和第8b阶段:去重评审器在空索引上编写最小的无操作
    dedup-summary.md
    ,误报评审器编写空的
    REPORT.md
    /
    REPORT.sarif
    ,第8b阶段的SARIF生成器为空情况输出
    results: []
    。SARIF消费者依赖稳定的工件集。
  • "
    Bash: ls README*
    用于预检查没问题。"
    在zsh下,不匹配的glob会在
    2>/dev/null
    运行前终止整个复合命令。使用
    find
    (无匹配时绝不会失败)——不要使用
    Glob
    ,因为持有
    Bash
    的代理无法使用它。

Orchestration Workflow

编排工作流

Run these phases in the main conversation.
主对话中运行这些阶段。

Phase 0: Parameter Collection

阶段0:参数收集

Entry: skill invoked. Exit:
threat_model
,
worker_model
,
severity_filter
resolved;
scope_subpath
resolved or set to
"."
;
finding_scope_root=scope_subpath
;
context_roots
resolved.
The skill is invoked directly (no command wrapper). Parse any free-text arguments the user passed on the
/rust-review:rust-review
line (e.g.
flamenco only
,
high severity only
,
use haiku
) and pre-fill the answers they imply — then ask for any missing required parameters with one
AskUserQuestion
call. Never silently default the required parameters.
Required parameters:
ParameterValuesHow to infer from args
threat_model
REMOTE
/
LOCAL_UNPRIVILEGED
/
BOTH
Words like "remote", "network", "attacker" →
REMOTE
; "local", "unprivileged" →
LOCAL_UNPRIVILEGED
; otherwise ask.
worker_model
haiku
/
sonnet
/
opus
Explicit model name in args. Otherwise ask (no silent default).
severity_filter
all
/
medium
/
high
"all", "every", "noisy" →
all
; "medium and above" →
medium
; "high only", "criticals only" →
high
. Otherwise ask — no silent default.
scope_subpath
repo-relative directory (optional)Phrases like "X only", "just audit X/", "review subdirectory X" →
src/X/
or the matching subdir. Apply fuzzy matching against top-level subdirectories of the repo. If absent, set
"."
; if ambiguous, ask.
Call
AskUserQuestion
exactly once with only unresolved required parameters (
threat_model
,
worker_model
,
severity_filter
) plus
scope_subpath
only when the user explicitly requested a narrowed scope but it is ambiguous. If the required parameters were all pre-filled and scope is absent or resolved, skip the question.
After resolving
scope_subpath
, set
finding_scope_root="${scope_subpath:-.}"
. Set
context_roots="."
by default so workers can verify callers/build settings outside a narrowed subtree without filing out-of-scope findings. If the user explicitly asks to forbid broader context, set
context_roots="${finding_scope_root}"
and note that reachability confidence may be lower.
入口:技能被调用。出口
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
)并预填充它们暗示的答案——然后用一次
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
)加上
scope_subpath
(仅当用户明确请求缩小范围但范围有歧义时)调用一次
AskUserQuestion
。如果必填参数都已预填充且范围不存在或已解析,则跳过询问。
解析
scope_subpath
后,设置
finding_scope_root="${scope_subpath:-.}"
。默认设置
context_roots="."
,以便工作器可以验证缩小子树之外的调用者/构建设置,而不会提交超出范围的检查结果。如果用户明确要求禁止更广泛的上下文,设置
context_roots="${finding_scope_root}"
并注意可达性置信度可能较低。

Phase 1: Prerequisites

阶段1:先决条件

Entry: Phase 0 complete. Exit:
has_unsafe
,
has_ffi
,
has_concurrency
,
has_async
,
has_packed_repr
,
has_fs_io
flags determined. Abort with a clear message if no
*.rs
files exist under
${finding_scope_root}
.
Probe within
${finding_scope_root:-.}
with the
Bash
commands below (non-empty output ⇒ flag true). The dedicated
Grep
/
Glob
tools are unavailable to this orchestrator because it holds
Bash
— use
grep
/
rg
/
find
via
Bash
. (The probe regexes use
\s
/
\b
; if your
grep
lacks GNU
\s
support, run them with
rg -uu
— which honors
\s
and still searches ignored files — or, if
rg
is not installed either, replace
\s
[[:space:]]
and drop
\b
. Widening is safe here: a false-positive capability flag only adds a harmless extra worker, whereas a missed match would skip a whole pass.)
bash
undefined
入口:阶段0完成。出口:已确定
has_unsafe
has_ffi
has_concurrency
has_async
has_packed_repr
has_fs_io
标志。如果
${finding_scope_root}
下不存在
*.rs
文件,终止并返回清晰消息。
使用以下
Bash
命令在
${finding_scope_root:-.}
内探测(非空输出 ⇒ 标志为true)。由于编排器持有
Bash
,专用的
Grep
/
Glob
工具不可用——通过
Bash
使用
grep
/
rg
/
find
。(探测正则使用
\s
/
\b
;如果
grep
不支持GNU
\s
,使用
rg -uu
运行——它支持
\s
且仍会搜索被忽略的文件——或者如果未安装
rg
,将
\s
替换为
[[:space:]]
并移除
\b
。此处放宽是安全的:误报的能力标志只会添加一个无害的额外工作器,而漏匹配会跳过整个检查。)
bash
undefined

Rust source presence (precondition)

Rust源文件存在性(前置条件)

find "${finding_scope_root:-.}" -name '*.rs' -print -quit
find "${finding_scope_root:-.}" -name '*.rs' -print -quit

has_unsafe

has_unsafe

grep -rlE '\bunsafe\s+(extern|fn|impl|trait)\b|\bunsafe\s*{' --include='*.rs' "${finding_scope_root:-.}" | head -1
grep -rlE '\bunsafe\s+(extern|fn|impl|trait)\b|\bunsafe\s*{' --include='*.rs' "${finding_scope_root:-.}" | head -1

has_ffi

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
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

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
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

has_async

grep -rlE '\basync\s+(fn|move|{)|.await\b|tokio::|async_std::|futures::' --include='*.rs' "${finding_scope_root:-.}" | head -1
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...)])

has_packed_repr (外部#[repr(...packed...)]和内部#![repr(...packed...)])

grep -rlE '#!?[repr([^]]packed' --include='.rs' "${finding_scope_root:-.}" | head -1
grep -rlE '#!?[repr([^]]packed' --include='.rs' "${finding_scope_root:-.}" | head -1

has_fs_io (path types / construction)

has_fs_io (路径类型/构造)

grep -rlE '\bPathBuf\b|\bPath\b' --include='*.rs' "${finding_scope_root:-.}" | head -1
grep -rlE '\bPathBuf\b|\bPath\b' --include='*.rs' "${finding_scope_root:-.}" | head -1

has_fs_io (fs module and file APIs)

has_fs_io (fs模块和文件API)

grep -rlE '\bfs::|\bFile::(open|create)\b|OpenOptions|.exists()|.metadata(|symlink_metadata|read_dir|read_to_string' --include='*.rs' "${finding_scope_root:-.}" | head -1

As with the other flags, non-empty output (from either probe) means the flag is true. These detectors are intentionally conservative: when in doubt they set the flag true, because a false-positive flag only costs a harmless extra worker while a false-negative would skip a real pass. `has_fs_io` keys on path types (`PathBuf`/`Path`, which also covers `&Path` parameters and bare `Path::` calls) and filesystem anchors (`fs::`/`File::`/`OpenOptions`/`read_dir`/…) rather than the bare `.join(`/`.push(` calls — path construction is reached via the path-type anchors, so leaving join/push out of the gate avoids matching unrelated iterator/`JoinHandle` joins and `Vec::push` that would make the gate fire on nearly every crate.

Note for `Cargo.toml`: also probe for `[dependencies] tokio`, `async-std`, etc., to set `has_async=true` even if the scope subpath has no `.await` yet (library crates often re-export).

Also probe `Cargo.toml` presence (informational — note in `run-summary.md` whether the audit was over a Cargo workspace, single crate, or loose `.rs` files):

```bash
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`基于路径类型(`PathBuf`/`Path`,也涵盖`&Path`参数和裸`Path::`调用)和文件系统锚点(`fs::`/`File::`/`OpenOptions`/`read_dir`/…),而不是裸`.join(`/`.push(`调用——路径构造通过路径类型锚点检测,因此将join/push排除在检测之外可避免匹配无关的迭代器/`JoinHandle` joins和`Vec::push`,这些会导致几乎每个crate都触发检测。

关于`Cargo.toml`的注意事项:还要探测`[dependencies] tokio`、`async-std`等,即使范围子目录中还没有`.await`,也要设置`has_async=true`(库crate通常会重新导出)。

还要探测`Cargo.toml`的存在性(信息性——在`run-summary.md`中记录审计是针对Cargo工作区、单个crate还是松散的`.rs`文件):

```bash

context_roots may be comma-separated (build_run_plan.py treats it as a list),

context_roots可能是逗号分隔的(build_run_plan.py将其视为列表),

so probe each root rather than passing "a,b" as one (nonexistent) path.

因此要探测每个根目录,而不是将"a,b"作为一个(不存在的)路径传递。

echo "${context_roots:-.}" | tr ',' '\n' | while IFS= read -r root; do find "${root:-.}" -name 'Cargo.toml' -print -quit done | head -1
undefined
echo "${context_roots:-.}" | tr ',' '\n' | while IFS= read -r root; do find "${root:-.}" -name 'Cargo.toml' -print -quit done | head -1
undefined

Phase 2: Output Directory

阶段2:输出目录

Entry: Phase 1 flags set. Exit: absolute
output_dir
resolved;
${output_dir}/findings/
and
${output_dir}/coverage/
exist.
Resolve an absolute path for
output_dir
(default:
$(pwd)/.rust-review-results/$(date -u +%Y%m%dT%H%M%SZ)/
):
bash
mkdir -p "${output_dir}/findings" "${output_dir}/coverage"
The
coverage/
subdirectory holds per-worker coverage-gate audit files (
coverage/worker-{N}.md
). Workers write to it instead of embedding the table in their reply — see
agents/rust-review-worker.md
step 5.
入口:阶段1标志已设置。出口:已解析绝对路径
output_dir
${output_dir}/findings/
${output_dir}/coverage/
已存在。
解析
output_dir
的绝对路径(默认:
$(pwd)/.rust-review-results/$(date -u +%Y%m%dT%H%M%SZ)/
):
bash
mkdir -p "${output_dir}/findings" "${output_dir}/coverage"
coverage/
子目录保存每个工作器的覆盖检查文件(
coverage/worker-{N}.md
)。工作器写入此目录,而不是将表格嵌入回复中——参见
agents/rust-review-worker.md
步骤5。

Phase 3: Codebase Context

阶段3:代码库上下文

Entry:
${output_dir}
exists. Exit:
${output_dir}/context.md
written.
Skim
README.{md,rst,txt}
and any build/manifest file (
Cargo.toml
,
Cargo.lock
,
rust-toolchain.toml
,
build.rs
) — preflight with
find
(via
Bash
) before any
Read
(a
Read
on a missing file aborts the turn;
Glob
is unavailable to this orchestrator because it holds
Bash
). Do not use
Bash: ls README*
for the preflight: under zsh, an unmatched glob aborts the whole compound command before
2>/dev/null
runs. Use
find . -maxdepth 2 -name 'README*' -o -name 'Cargo.toml' -o -name 'rust-toolchain.toml' -o -name 'build.rs'
, which never fails on no-match.
Write
${output_dir}/context.md
with: YAML frontmatter (
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
as
workspace
/
single-crate
/
absent
plus path when present), then a short markdown body with five sections — Purpose (1-3 sentences), Scope (what's in
finding_scope_root
, and that findings outside it are out of scope), Entry points (where untrusted data enters: network, files, CLI, IPC,
serde
deserialization, FFI inputs), Trust boundaries (sandboxed vs trusted peers vs arbitrary remote), Existing hardening (fuzzing harnesses, MIRI runs,
clippy::pedantic
,
cargo-deny
,
cargo-audit
).
入口
${output_dir}
已存在。出口:已写入
${output_dir}/context.md
浏览
README.{md,rst,txt}
和任何构建/清单文件(
Cargo.toml
Cargo.lock
rust-toolchain.toml
build.rs
)——在任何
Read
之前用
find
(通过
Bash
)进行预检查(对不存在的文件进行
Read
会终止当前轮次;由于编排器持有
Bash
Glob
不可用)。不要使用
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
workspace
/
single-crate
/
absent
加上存在时的路径),然后是简短的Markdown正文,包含五个部分——用途(1-3句话)、范围
finding_scope_root
包含的内容,以及超出此范围的检查结果不纳入范围)、入口点(不受信任数据进入的位置:网络、文件、CLI、IPC、
serde
反序列化、FFI输入)、信任边界(沙盒化vs可信对等方vs任意远程)、现有强化措施(模糊测试 harness、MIRI运行、
clippy::pedantic
cargo-deny
cargo-audit
)。

Phase 4: Build Run Plan (deterministic)

阶段4:构建运行计划(确定性)

Entry: capability flags +
threat_model
known;
${output_dir}/findings/
exists. Exit:
${output_dir}/plan.json
and
${output_dir}/worker-prompts/*.txt
written;
M = worker_count
known.
Selection, filtering, path resolution, and spawn-prompt rendering are delegated to the script to keep spawn prompts complete and consistent:
bash
python3 "${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
The script writes
plan.json
+
worker-prompts/worker-N.txt
+ (if
--cache-primer=true
, the default)
worker-prompts/cache-primer.txt
, and prints a JSON summary on stdout. Exits non-zero on any missing prompt — surface the message and stop. With the default
--max-passes-per-worker 4
the planner selects ~8 clusters → M ≈ 13 workers for pure safe Rust (no FFI / concurrency / async;
info-disclosure
is always on), ~10 clusters → M ≈ 15 for concurrent safe Rust, and ~15 clusters → M ≈ 23 for full Rust (unsafe + FFI + concurrency + async, plus
input-os-safety
when
has_fs_io
and
layout-safety
when
has_packed_repr
). M is the post-chunk worker count (
plan.workers.length
), so it runs above the cluster count — chunking splits multi-pass non-consolidated clusters (e.g.
panic-dos
,
memory-safety
), while the two consolidated clusters (
unsafe-boundary
,
concurrency-locking
) are never chunked: one worker each builds the shared inventory once and runs all its phases.
recursion-dos
is one pass per worker. After it returns,
Read plan.json
for the structured selection — never re-derive filtering or paths.
--max-passes-per-worker N
caps the per-worker pass count. The planner deterministically splits any non-consolidated cluster with more than
N
passes into
ceil(K/N)
contiguous chunks; each chunk becomes its own
rust-review-worker
spawn with a
-{i}
-suffixed
cluster_id
(e.g.
panic-dos-1
,
panic-dos-2
). Consolidated clusters (
unsafe-boundary
,
concurrency-locking
) are exempt — never chunked, regardless of pass count or override — so one worker builds their shared Phase-A inventory once and runs every phase
(chunking a consolidated cluster would force each chunk to rebuild that inventory, which workers skip in practice). The shared prompt-cache prefix and
Cluster prompt:
path are byte-identical across chunks, so the cache primer still warms every worker. Default 4 is calibrated against the heavy-tail clusters in
manifest.json
. Some output-heavy non-consolidated clusters declare a smaller manifest-level
max_passes_per_worker
override so each expensive pass gets its own worker (e.g.
recursion-dos
). Pass
--max-passes-per-worker 0
to disable all chunking, including manifest overrides (one worker per cluster).
入口:能力标志 +
threat_model
已知;
${output_dir}/findings/
已存在。出口:已写入
${output_dir}/plan.json
${output_dir}/worker-prompts/*.txt
;已知
M = worker_count
选择、过滤、路径解析和启动提示渲染委托给脚本,以保持启动提示完整一致:
bash
python3 "${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摘要。如果缺少任何提示,非零退出——显示消息并停止。使用默认的
--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
)拆分,而两个合并集群(
unsafe-boundary
concurrency-locking
)永远不会被分块:每个集群由一个工作器构建一次共享清单,然后运行所有阶段。
recursion-dos
每个工作器一轮。返回后,
Read plan.json
获取结构化选择——绝不重新推导过滤或路径。
--max-passes-per-worker N
限制每个工作器的轮次数量。规划器会确定性地将任何轮次超过
N
非合并集群拆分为
ceil(K/N)
个连续块;每个块成为一个带有
-{i}
后缀
cluster_id
rust-review-worker
启动(例如
panic-dos-1
panic-dos-2
)。合并集群(
unsafe-boundary
concurrency-locking
)豁免——无论轮次数量或覆盖如何,永远不会被分块——因此一个工作器构建一次它们的共享Phase-A清单并运行所有阶段
(分块合并集群会迫使每个块重新构建该清单,而工作器实际上会跳过)。共享提示缓存前缀和
Cluster prompt:
路径在块之间是字节相同的,因此缓存primer仍然可以预热每个工作器。默认值4是针对
manifest.json
中长尾集群校准的。一些输出密集的非合并集群声明了更小的清单级
max_passes_per_worker
覆盖,以便每个昂贵的轮次都有自己的工作器(例如
recursion-dos
)。传递
--max-passes-per-worker 0
以禁用所有分块,包括清单覆盖(每个集群一个工作器)。

Phase 5: Create Bookkeeping Tasks (orchestrator-internal)

阶段5:创建记账任务(编排器内部)

Entry:
${output_dir}/plan.json
exists;
M = plan.workers.length
. Exit:
cluster_task_ids[]
created (1:1 with
plan.workers
), all
pending
.
The task ledger is orchestrator bookkeeping only (TUI visibility + Phase-7 retry tracking) — workers never read or write it. One
TaskCreate
per worker, populating
metadata
with
kind="cluster"
,
worker_n
,
cluster_id
,
spawn_prompt_path
,
pass_prefixes
,
attempt=1
— all values copied verbatim from
plan.workers[i]
. Track
cluster_task_ids[]
in
plan.workers
order.
入口
${output_dir}/plan.json
已存在;
M = plan.workers.length
出口:已创建
cluster_task_ids[]
(与
plan.workers
一一对应),所有状态为
pending
任务分类账仅用于编排器记账(TUI可见性 + 阶段7重试跟踪)——工作器从不读取或写入它。每个工作器对应一个
TaskCreate
,在
metadata
中填充
kind="cluster"
worker_n
cluster_id
spawn_prompt_path
pass_prefixes
attempt=1
——所有值均直接复制自
plan.workers[i]
。按
plan.workers
顺序跟踪
cluster_task_ids[]

Phase 6: Spawn workers (optional cache-primer first, then M in parallel)

阶段6:启动工作器(可选先启动缓存primer,然后并行启动M个)

Entry:
cluster_task_ids[]
populated; per-worker spawn prompt files exist at
${output_dir}/worker-prompts/worker-N.txt
. Exit: all M
Agent
calls — across every wave — have returned (the parallel spawn block(s) completed).
入口
cluster_task_ids[]
已填充;每个工作器的启动提示文件位于
${output_dir}/worker-prompts/worker-N.txt
出口:所有M个
Agent
调用——跨所有波次——已返回(并行启动块已完成)。

Phase 6a: Cache primer (gated on
plan.run.cache_primer
)

阶段6a:缓存primer(受
plan.run.cache_primer
控制)

A parallel batch from cold start cannot share cache (all M requests dispatch simultaneously, none has finished writing). To warm the prefix, spawn a tiny primer first — foreground (background spawns don't share cache with subsequent foreground spawns).
If
plan.run.cache_primer == true
,
build_run_plan.py
has written
${output_dir}/worker-prompts/cache-primer.txt
. Spawn it in its own assistant message:
Read
the file, pass verbatim as
Agent
prompt
with
subagent_type=rust-review:rust-review-worker
,
model=${worker_model}
,
description="Rust review cache primer"
, no
run_in_background
. The script wrote the prefix byte-identical to
worker-1.txt
through the
<context>
block — that byte-identity is what gives the parallel workers their cache hit. The primer trailer contains
Cache primer: true
, which the worker system prompt treats as a first-class mode and returns exactly
worker-PRIMER abort: cache primer (no analysis performed)
in one text response with zero tool calls. Discard the abort line — Phase 7 ignores it (no
worker-N
id).
Foreground spawn already serializes — no
sleep
needed before Phase 6b. Skip Phase 6a entirely if
plan.run.cache_primer == false
.
冷启动的并行批处理无法共享缓存(所有M个请求同时调度,没有一个完成写入)。为了预热前缀,先启动一个小型primer——前台(后台启动不会与后续前台启动共享缓存)。
如果
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
。脚本写入的前缀与
worker-1.txt
<context>
块字节相同——这种字节相同性使并行工作器能够命中缓存。primer尾部包含
Cache primer: true
,工作器系统提示将其视为一等模式,并在一个文本响应中返回完全相同的
worker-PRIMER abort: cache primer (no analysis performed)
,没有工具调用。丢弃终止行——阶段7会忽略它(没有
worker-N
id)。
前台启动已经是串行的——阶段6b之前不需要
sleep
。如果
plan.run.cache_primer == false
,完全跳过阶段6a。

Phase 6b: Spawn M real workers in parallel (one message per wave of ≤16)

阶段6b:并行启动M个真实工作器(每个波次≤16个,一个消息)

STOP — read this before composing the spawn message.
Workers MUST be spawned foreground (no
run_in_background
field, or
run_in_background=false
). "Parallel" here means one assistant message containing the wave's
Agent
calls
— that already runs them concurrently. (For large
M
, split into consecutive waves of ≤16 calls, one message per wave — see "Required spawn shape" below.) Background spawns are NOT how you parallelize this skill.
Background spawns defeat Phase 6a's primer cache: every worker pays full cache-creation on its first turn (
cache_read_input_tokens=0
), and the primer's ~15 K tokens are wasted M times over. Two real runs had exactly this symptom — every worker started with
first_cr=0
.
Before sending the spawn message, audit your draft: every
Agent
call must have no
run_in_background
key. If you wrote
run_in_background=true
, delete it.
Required spawn shape: emit a single assistant message containing the wave's
Agent
tool invocations — that one message is what runs them concurrently. Sequential spawning (one
Agent
call per message) serializes the review and is also wrong, but that failure is loud (timing); the background-spawn failure is silent (cost).
Waves when
M
exceeds the per-message cap.
The harness caps the number of
Agent
calls it will dispatch from a single assistant message (observed: ~20 in Claude Code — a real 25-worker run silently kept only the first 20 and had to spawn the remaining 5 in a second message). So when
M > 16
, plan the waves up front: split the workers into consecutive waves of ≤16
Agent
calls
, each wave its own single assistant message. Rules:
  • Within a wave: all
    Agent
    calls in one message, foreground (no
    run_in_background
    ) — identical shape to a single-wave run.
  • Across waves: wave k+1 is a separate message that can only be sent after wave k's
    Agent
    calls all return (a tool-use message ends the turn). Waves are therefore serialized with respect to each other — that is correct and loud; accept it. Do not try to overlap them.
  • Never reach for
    run_in_background=true
    to fit more workers in one message. More waves, never background — background defeats the primer cache (see the STOP box) and is the cardinal error this skill guards against.
  • Cache across waves: the primer prefix has a ~5-minute cache TTL that refreshes on every hit, so back-to-back waves keep hitting it (the 25-worker run confirmed
    cache_read≈14 K
    on its second wave). If a later wave will start more than ~5 minutes after the previous one (very large
    M
    or slow workers), re-spawn the Phase-6a primer in its own message first to re-warm the prefix before that wave.
  • Balance the waves (e.g.
    M=25
    → 13+12, not 20+5) so no wave hugs the cap and the last wave isn't a tiny straggler.
  • After every wave has returned, proceed to Phase 7 with the full set of M worker results.
For each worker
N ∈ [1..M]
(in its assigned wave):
  1. Read: ${output_dir}/worker-prompts/worker-N.txt
  2. Pass the file contents verbatim as the
    Agent
    tool's
    prompt
    argument:
ParameterValue
subagent_type
rust-review:rust-review-worker
model
${worker_model}
(haiku / sonnet / opus)
description
Rust review worker N
prompt
the full text of
worker-N.txt
(no edits)
run_in_background
field MUST be omitted, OR set to
false
.
Never
true
. See the foreground-spawn warning above.
The spawn prompt is the single authority. Pass it verbatim — every field is required by the worker's self-check; any deviation triggers
worker-N abort: spawn prompt malformed
.
Anti-patterns to reject:
  • Passing
    run_in_background=true
    (see warning above).
  • Cramming more than ~16
    Agent
    calls into one message
    when
    M
    is large — the harness silently keeps only the first ~20 and drops the rest. Use balanced waves of ≤16, never background spawns, to cover all M.
  • Hand-typing the spawn prompt instead of reading
    worker-N.txt
    .
  • Inserting Task-related instructions ("first call TaskList", "Assigned task id: <N>"). Workers have no Task tools.
  • Editing the rendered prompt before passing it (trimming "redundant" fields, collapsing pass lists).
停止——在编写启动消息前阅读此内容。
工作器必须前台启动(不设置
run_in_background
字段,或设置为
run_in_background=false
)。 这里的“并行”指一个助手消息包含波次的
Agent
调用
——这已经是并发运行的。(对于大M,拆分为连续的≤16个调用的波次,每个波次一个消息——参见下面的“必填启动格式”。)后台启动不是并行此技能的方式。
后台启动会破坏阶段6a的primer缓存:每个工作器在第一轮都要完全创建缓存(
cache_read_input_tokens=0
),primer的约15K-token会被浪费M次。两次真实运行都出现了完全相同的症状——每个工作器都从
first_cr=0
开始。
在发送启动消息前,检查你的草稿:每个
Agent
调用必须没有
run_in_background
键。如果你写了
run_in_background=true
,删除它。
必填启动格式:生成一个包含波次
Agent
工具调用的助手消息——这个消息就是并发运行它们的方式。顺序启动(每个消息一个
Agent
调用)会序列化审查,这也是错误的,但这种失败很明显(计时问题);后台启动的失败是静默的(成本问题)。
当M超过每条消息限制时的波次处理。工具框架限制单个助手消息可以调度的
Agent
调用数量(观察到:Claude Code中约20个——一次25个工作器的真实运行静默保留了前20个,不得不在第二个消息中启动剩下的5个)。因此当
M > 16
时,提前规划波次:将工作器拆分为连续的**≤16个
Agent
调用**的波次,每个波次一个单独的助手消息。规则:
  • 波次内:所有
    Agent
    调用在一个消息中,前台(无
    run_in_background
    )——与单波次运行格式相同。
  • 波次间:波次_k+1_是一个单独的消息,只能在波次_k_的
    Agent
    调用全部返回后发送(工具使用消息结束当前轮次)。因此波次之间是串行的——这是正确且明显的;接受它。不要尝试重叠它们。
  • 绝不使用
    run_in_background=true
    在一个消息中容纳更多工作器。使用更多波次,绝不使用后台——后台会破坏primer缓存(参见停止框),是此技能防范的主要错误。
  • 波次间缓存:primer前缀有约5分钟的缓存TTL,每次命中都会刷新,因此连续波次会持续命中缓存(25个工作器的运行确认第二波次
    cache_read≈14 K
    )。如果后续波次在前一个波次之后约5分钟以上才启动(M非常大或工作器缓慢),先在单独的消息中重新启动阶段6a的primer,以在该波次前重新预热前缀。
  • 平衡波次(例如
    M=25
    → 13+12,而不是20+5),这样没有波次接近限制,最后一个波次也不是很小的零散工作器。
  • 所有波次返回后,使用完整的M个工作器结果进入阶段7。
对于每个工作器
N ∈ [1..M]
(在其分配的波次中):
  1. Read: ${output_dir}/worker-prompts/worker-N.txt
  2. 将文件内容直接作为
    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很大时,在一个消息中塞入超过约16个
    Agent
    调用
    ——工具框架会静默保留前约20个并丢弃其余的。使用≤16个的平衡波次,绝不使用后台启动,以覆盖所有M个工作器。
  • 手动输入启动提示而不是读取
    worker-N.txt
  • 插入与Task相关的指令("首先调用TaskList"、"分配的任务id: <N>")。工作器没有Task工具。
  • 在传递前编辑渲染后的提示(修剪“冗余”字段,合并轮次列表)。

Phase 7: Wait for Workers and Classify Outcomes

阶段7:等待工作器并分类结果

Entry: all M Phase-6
Agent
calls have returned. Exit: every cluster has either succeeded or been retried up to the cap;
${output_dir}/findings-index.txt
written.
The Phase-6
Agent
invocations block until each worker returns. Inspect each worker's return text and apply this classifier in order — first match wins:
#Match (in return text)OutcomeAction
1
worker-N complete:
provisional successParse the
wrote N finding files
count, then run the artifact validator below before
TaskUpdate
to
completed
.
2
abort: spawn prompt malformed
,
abort: pre-work budget exceeded
, or
abort: TaskList unavailable
(legacy)
non-retryable orchestrator bugStop the run, surface the abort + spawn-prompt path. Re-running the same prompt repeats the failure — pre-work-budget exhaustion always means the worker couldn't pass its self-check, which a retry won't fix.
3other
worker-N abort:
retryableMark
pending
, set
metadata.abort_reason
,
needs_respawn=true
, increment
attempt
.
4
Agent
errored or no
complete:
/
abort:
token
retryableSame as #3 (transient worker crash).
If any non-retryable, stop. Otherwise, before re-spawning, clear each retryable worker's prefix-space on disk — the Phase-7 index is built from disk, so a crashed attempt's higher-id stragglers (files the replacement never re-emits) would otherwise be resurrected into the report. Loop over the worker's actual
pass_prefixes
(from its task
metadata
), substituting each real prefix for
${pfx}
— do not run the command with a literal
PREFIX
:
bash
undefined
入口:所有阶段6的
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
运行命令:
bash
undefined

zsh-safe:
find … -delete
never aborts on no-match (an
rm PREFIX-*.md
glob would).

对zsh安全:
find … -delete
无匹配时绝不会终止(
rm PREFIX-*.md
glob会终止)。

Replace
PREFIX1 PREFIX2
with the worker's actual space-separated pass_prefixes.

PREFIX1 PREFIX2
替换为工作器实际的空格分隔pass_prefixes。

for pfx in PREFIX1 PREFIX2; do find "${output_dir}/findings" -maxdepth 1 -type f -name "${pfx}-*.md" -delete done

Then re-spawn each `pending` retryable with `attempt <= 2` in one parallel block (cap = 2 attempts per cluster). `attempt` was just incremented to `2` on the first failure, so the guard must admit `2` to allow the single retry — `attempt < 2` would block every retry. A second failure increments to `3`, which fails `<= 2` and ends retries. Replacement workers reuse deterministic finding IDs per prefix, so a cleared prefix-space plus a fresh write yields a consistent shard / coverage / disk set.
for pfx in PREFIX1 PREFIX2; do find "${output_dir}/findings" -maxdepth 1 -type f -name "${pfx}-*.md" -delete done

然后在一个并行块中重新启动每个`pending`且`attempt <= 2`的可重试工作器(每个集群上限=2次尝试)。第一次失败时`attempt`刚递增到`2`,因此守卫必须允许`2`以允许单次重试——`attempt < 2`会阻止所有重试。第二次失败会递增到`3`,不满足`<= 2`,结束重试。替换工作器会为每个前缀重用确定性的检查结果ID,因此清除前缀空间加上重新写入会产生一致的分片/覆盖/磁盘集。

Sanity-check + write index

健全性检查 + 写入索引

For every provisional
complete:
cluster, validate the worker-owned shard, coverage file, coverage rows, filed IDs, and claimed finding count against
plan.json
before marking the task completed. Run one command per completed worker, or validate multiple workers in one command. Both claimed-count forms below are valid; do not pass bare
worker-N=N
values without either grouping them after a
--claimed-count
flag or repeating the flag.
bash
python3 "${RUST_REVIEW_PLUGIN_ROOT}/scripts/validate_artifacts.py" "${output_dir}/plan.json" \
  --worker worker-N --claimed-count worker-N=<claimed_count_from_complete_line>
bash
python3 "${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
bash
python3 "${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
If validation exits non-zero, treat the completion as malformed and retryable (classifier row #4): mark the task
pending
, store the validator output in
metadata.abort_reason
, set
needs_respawn=true
, and increment
attempt
. Missing
findings-index.d/worker-N.txt
, missing
coverage/worker-N.md
, missing coverage rows, invalid
skipped:
rows, filed IDs absent from the shard or disk, and claimed-count mismatches are all malformed completions. After the retry cap, leave the cluster task incomplete and surface the validator output in
run-summary.md
and the final response. Only validation-clean provisional completions may be
TaskUpdate
d to
completed
.
Then build the index. The canonical index is the set of finding files actually on disk, not the shard union — building from disk guarantees that a finding written without a matching shard entry (a worker that crashed between its
Write
and its shard-append, or the single-prefix empty-shard trap the worker prompt warns about) is still picked up by dedup → fp-judge → REPORT/SARIF instead of silently vanishing, and that every index entry resolves to a real file:
bash
undefined
对于每个临时
complete:
集群,在标记任务为完成之前,根据
plan.json
验证工作器拥有的分片、覆盖文件、覆盖行、提交的ID和声称的检查结果计数。为每个完成的工作器运行一个命令,或在一个命令中验证多个工作器。下面两种声称计数格式都有效;不要在没有
--claimed-count
标志分组或重复标志的情况下传递裸
worker-N=N
值。
bash
python3 "${RUST_REVIEW_PLUGIN_ROOT}/scripts/validate_artifacts.py" "${output_dir}/plan.json" \
  --worker worker-N --claimed-count worker-N=<claimed_count_from_complete_line>
bash
python3 "${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
bash
python3 "${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
和分片追加之间崩溃,或工作器提示警告的单前缀空分片陷阱)仍然会被去重→误报评审器→REPORT/SARIF拾取,而不是静默消失,并且每个索引条目都指向真实文件:
bash
undefined

Canonical index = every finding file on disk.
find
never fails on no-match

规范索引 = 磁盘上的每个检查结果文件。
find
无匹配时绝不会失败

(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"
#(空findings/会产生空索引——明确的“无检查结果”信号)。
sort -u
合并阶段7的重试重复项:替换工作器重用

Reconcile against the per-worker shards: any path on disk but in NO shard is an

确定性id,因此相同路径只出现一次。

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

The shards stay the per-worker audit trail (`validate_artifacts.py` checks them) and the dedup-judge's crash-recovery fallback, but they no longer gate what reaches the pipeline. For each orphan basename the reconcile prints, map its `<PREFIX>` to the owning worker via `plan.json` and note in `run-summary.md` that that worker's shard was incomplete — the finding is already in the index (so it is not lost), but the bookkeeping gap should be visible. Still cross-check the index line count against the sum of `wrote N` worker claims; log mismatches but don't abort.

After task updates and index creation, run `TaskList` and write `${output_dir}/run-summary.md` with:

- resolved parameters (`threat_model`, `severity_filter`, `finding_scope_root`, `context_roots`, capability flags `has_unsafe`/`has_ffi`/`has_concurrency`/`has_async`, Cargo manifest status)
- worker outcome table (`worker_n`, `cluster_id`, claimed finding count, shard line count, coverage-file path (`coverage/worker-{N}.md`), task status, retry/abort state)
- `findings-index.txt` line count and any mismatch against worker claims
- judge status once Phase 8 finishes, or the reason a judge was skipped/failed

If any Phase-5 cluster task is not `completed` — **or** any worker returned a `complete:` line carrying the `truncated at hard cap` token (it hit the tool-call cap before searching every pass; its coverage file will show one or more `cleared (NOT SEARCHED — truncated at hard cap)` rows) — include it prominently in `run-summary.md` and the final response. A hard-cap-truncated worker is marked `completed` for ledger purposes but is a **partial** result: do not let that `completed` status hide the incomplete coverage behind a successful report.

**Always run Phase 8 even on zero findings** — both judges short-circuit on an empty index: dedup-judge writes a minimal no-op `dedup-summary.md`, and fp-judge writes empty `REPORT.md`/`REPORT.sarif` so SARIF consumers get a stable artifact set.
find "${output_dir}/findings" -maxdepth 1 -type f -name '*.md' 2>/dev/null
| sort -u > "${output_dir}/findings-index.txt"

Phase 8: Judge Pipeline (sequential, dedup → fp+severity)

与每个工作器的分片协调:磁盘上存在但不在任何分片中的路径是工作器未能记录的孤立文件。它已在上面的索引中(因此

不会被丢弃)——打印它以便显示记账缺口。非致命。

Entry:
findings-index.txt
exists. Exit: dedup-judge and fp-judge have returned;
dedup-summary.md
,
fp-summary.md
,
REPORT.md
, and ideally
REPORT.sarif
are written.
Each judge's full protocol is its system prompt (
agents/rust-review-{dedup,fp}-judge.md
); spawn prompts pass only per-run variables. Do not reference
prompts/internal/judges/
— those files don't exist.
STOP — these two judges run in SEQUENCE, not in parallel. Unlike the Phase-6b workers (which you spawn as M
Agent
calls in one message precisely because that runs them concurrently), the judges have a hard data dependency: fp-judge must see the
merged_into
/
also_known_as
annotations dedup-judge writes, and it only skips files already carrying
merged_into
. If you emit both
Agent
calls in one message they run concurrently — fp-judge reads findings before any merge annotations exist, judges every duplicate as a separate primary, and (because
dedup-summary.md
doesn't exist yet) trips its "dedup did not run" fallback, producing an inflated, duplicated
REPORT.md
/SARIF.
Spawn dedup-judge in its own assistant message, wait for its
dedup-judge complete:
(or
abort:
) return, then spawn fp-judge in a separate message. Before composing the fp-judge spawn, confirm dedup finished —
Bash: test -f ${output_dir}/dedup-summary.md
must succeed (or you saw the dedup
complete:
token). Never put both judge
Agent
calls in the same message.
  1. First message
    Agent(subagent_type="rust-review:rust-review-dedup-judge", description="Dedup judge", prompt=f"output_dir: {output_dir}")
    . Wait for its return and classify it (below) before continuing.
  2. Then, in a separate message
    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}")
    — resolve
    sarif_generator_path
    to
    ${RUST_REVIEW_PLUGIN_ROOT}/scripts/generate_sarif.py
    .
Judge failure handling. Same shape as Phase 7's classifier, applied to judge return text:
  • … complete:
    success.
  • … abort:
    non-retryable for that judge. Surface the abort line plus
    ls -l ${output_dir}/findings-index.txt
    , then still run Phase 8b (its SARIF +
    REPORT.md
    safety net guarantees the artifact set even when a judge aborts — see Phase 8b's "fp-judge returned, or the run aborted early" entry), and stop without spawning further judges. "Stop" means do not continue the judge pipeline — it does not mean skip Phase 8b.
  • No
    complete:
    (help message / error / question) → retryable once.
    SendMessage(to=<agentId>, …)
    rather than a fresh spawn (the agent already paid the protocol-parse cost). Include the explicit finding paths from
    findings-index.txt
    . If the second try still fails, surface the transcript and continue to Phase 8b.
if [ -d "${output_dir}/findings-index.d" ]; then

按基名协调(检查结果id是唯一的),因此工作器
find
和此命令之间的路径格式差异

#(尾部斜杠、/var↔/private/var)不会产生假孤立文件。磁盘上存在但不在任何分片中的基名是

工作器未能记录的孤立文件。

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`会检查它们)和去重评审器的崩溃恢复回退,但它们不再控制哪些内容进入管道。对于协调打印的每个孤立基名,通过`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`行数以及与工作器声称的任何不匹配
- 阶段8完成后的评审器状态,或评审器被跳过/失败的原因

如果任何阶段5的集群任务未`completed`——**或**任何工作器返回带有`truncated at hard cap`标记的`complete:`行(在搜索所有轮次前达到工具调用上限;其覆盖文件会显示一个或多个`cleared (NOT SEARCHED — truncated at hard cap)`行)——在`run-summary.md`和最终响应中突出显示。硬上限截断的工作器出于分类账目的标记为`completed`,但它是**部分**结果:不要让`completed`状态在成功报告背后隐藏不完整的覆盖范围。

**即使没有检查结果也要始终运行阶段8**——两个评审器在空索引上会短路:去重评审器编写最小的无操作`dedup-summary.md`,误报评审器编写空的`REPORT.md`/`REPORT.sarif`,以便SARIF消费者获得稳定的工件集。

Phase 8b: Report safety net (SARIF + REPORT.md)

阶段8:评审器流水线(串行,去重 → 误报+严重性)

Entry: fp-judge returned, or the run aborted early. Exit:
${output_dir}/REPORT.sarif
and
${output_dir}/REPORT.md
both exist.
bash
test -d "${output_dir}/findings" && python3 "${RUST_REVIEW_PLUGIN_ROOT}/scripts/generate_sarif.py" "${output_dir}"
Run the SARIF generator unconditionally whenever
findings/
exists — it is idempotent (full overwrite), emits
results: []
for zero-survivor runs, and handles partial runs (findings without
fp_verdict
are emitted as
LIKELY_TP
, exempt from the
severity_filter
since their severity was never judge-validated, and marked
unjudged: true
/
severity_validated: false
with an
[UNVALIDATED SEVERITY — not judged]
message prefix — so an inferred severity guess can never silently drop them under a
medium
/
high
filter). Always overwriting protects against an fp-judge that crashed mid-write and left a corrupt
REPORT.sarif
on disk.
If the generator prints a
WARNING: skipped N …
line on stdout (it also records
invocations[].properties.skipped_findings
in the SARIF and a
warning
notification per dropped file), one or more finding files were unreadable or had no parseable frontmatter and were excluded from the report. This is a dropped result — surface it prominently in
run-summary.md
and the final response with the count and paths, the same way a non-
completed
cluster task is surfaced. Do not let the otherwise-clean SARIF hide the loss.
Then guarantee
REPORT.md
exists. Unlike SARIF (mechanical),
REPORT.md
is the fp-judge's curated artifact, so do not overwrite a judge-written one. (The fp-judge writes
REPORT.md
with a
Bash
heredoc, not the
Write
tool, because the harness blocks the
Write
tool for subagent report files — do not "fix" the judge by re-mandating
Write
. The orchestrator is the main agent and is not subject to that block, so its own
Write
below works.) Check for it, and if it is missing (the judge crashed, even its
Bash
-heredoc write failed, or it returned the report as chat text instead of writing the file), the orchestrator writes
REPORT.md
itself
rather than failing the run:
  • If the fp-judge returned the report body in its transcript,
    Write
    that text verbatim to
    ${output_dir}/REPORT.md
    .
  • Otherwise synthesize it from the on-disk findings: take the survivor primaries (
    fp_verdict ∈ {TRUE_POSITIVE, LIKELY_TP}
    , no
    merged_into
    ; if the judge never ran, treat a finding with no
    fp_verdict
    as a survivor) listed in
    findings-index.txt
    , apply
    severity_filter
    from
    context.md
    to judged survivors only — unjudged findings (no
    fp_verdict
    ) are included regardless of filter and rendered under an
    Unvalidated (severity not judged)
    section with a
    [UNVALIDATED SEVERITY — not judged]
    label, mirroring the SARIF behavior so a strict filter never silently drops them — and
    Write
    a
    REPORT.md
    mirroring the fp-judge template — YAML frontmatter (
    stage: final-report
    ,
    threat_model
    ,
    severity_filter
    ,
    total_primaries
    ,
    reported_findings
    ), a severity-distribution table, then one section per reported finding grouped by severity (embed the Description / Code / Data flow / Impact / Recommendation body for CRITICAL/HIGH; reference the finding file for MEDIUM/LOW).
Either way, note in
${output_dir}/run-summary.md
that
REPORT.md
was orchestrator-synthesized (not judge-authored). Skip the SARIF generator and this check only if
${output_dir}/findings/
doesn't exist (Phase 2 failed). After this phase, update
${output_dir}/run-summary.md
with judge / SARIF / report status.
入口
findings-index.txt
已存在。出口:去重评审器和误报评审器已返回;已写入
dedup-summary.md
fp-summary.md
REPORT.md
,理想情况下还有
REPORT.sarif
每个评审器的完整协议是其系统提示(
agents/rust-review-{dedup,fp}-judge.md
);启动提示仅传递每个运行的变量。不要引用
prompts/internal/judges/
——这些文件不存在。
停止——这两个评审器必须串行运行,不能并行。 与阶段6b的工作器(你作为一个消息中的M个
Agent
调用启动,正是因为这样可以并发运行)不同,评审器有严格的数据依赖:误报评审器必须看到去重评审器写入的
merged_into
/
also_known_as
注释,并且它只会跳过已带有
merged_into
的文件。如果你在一个消息中发出两个
Agent
调用,它们会并发运行——误报评审器在合并注释存在前读取检查结果,将每个重复项视为单独的主项,并且(因为
dedup-summary.md
还不存在)触发其“去重未运行”回退,产生膨胀、重复的
REPORT.md
/SARIF。
单独的助手消息中启动去重评审器,等待其
dedup-judge complete:
(或
abort:
)返回,然后单独的消息中启动误报评审器。编写误报评审器启动前,确认去重已完成——
Bash: test -f ${output_dir}/dedup-summary.md
必须成功(或你看到了去重的
complete:
标记)。绝不要将两个评审器的
Agent
调用放在同一个消息中。
  1. 第一个消息
    Agent(subagent_type="rust-review:rust-review-dedup-judge", description="Dedup judge", prompt=f"output_dir: {output_dir}")
    。等待其返回并分类(如下)后再继续。
  2. 然后,在单独的消息中
    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
评审器失败处理。与阶段7的分类器格式相同,应用于评审器返回文本:
  • … complete:
    成功。
  • … abort:
    该评审器不可重试。 显示终止行加上
    ls -l ${output_dir}/findings-index.txt
    ,然后仍然运行阶段8b(其SARIF +
    REPORT.md
    安全网即使在评审器终止时也能保证工件集——参见阶段8b的“fp-judge返回,或运行提前终止”入口),并停止而不启动更多评审器。“停止”意味着不要继续评审器流水线——不是跳过阶段8b。
  • complete:
    (帮助消息/错误/问题)→ 可重试一次。 使用
    SendMessage(to=<agentId>, …)
    而不是重新启动(代理已经支付了协议解析成本)。包含
    findings-index.txt
    中的明确检查结果路径。如果第二次尝试仍然失败,显示记录并继续到阶段8b。

Phase 9: Return Report

阶段8b:报告安全网(SARIF + REPORT.md)

Entry: Phase 8b complete. Exit: every item in Success Criteria verified true;
REPORT.md
returned to the caller.
Before composing the response, walk the Success Criteria checklist below and confirm each bullet against on-disk artifacts (
TaskList
for cluster tasks,
ls
/
Read
for the files). If any criterion fails, surface the failure prominently in the response — do not hide a partial run behind a successful report.
Then
Read ${output_dir}/REPORT.md
and return its content to the caller. Append an Artifacts list pointing at
findings/
,
findings-index.txt
,
run-summary.md
,
dedup-summary.md
,
fp-summary.md
,
REPORT.md
,
REPORT.sarif
.

入口:误报评审器返回,或运行提前终止。出口
${output_dir}/REPORT.sarif
${output_dir}/REPORT.md
都已存在。
bash
test -d "${output_dir}/findings" && python3 "${RUST_REVIEW_PLUGIN_ROOT}/scripts/generate_sarif.py" "${output_dir}"
只要
findings/
存在,就无条件运行SARIF生成器——它是幂等的(完全覆盖),无存活检查结果时输出
results: []
,并处理部分运行(无
fp_verdict
的检查结果会以
LIKELY_TP
输出,不受
severity_filter
限制
,因为其严重性从未经过评审器验证,并标记为
unjudged: true
/
severity_validated: false
,前缀为
[UNVALIDATED SEVERITY — not judged]
消息——因此推断的严重性猜测永远不会被
medium
/
high
过滤器静默丢弃)。始终覆盖可防止误报评审器崩溃时在磁盘上留下损坏的
REPORT.sarif
如果生成器在stdout上打印
WARNING: skipped N …
行(它还会在SARIF的
invocations[].properties.skipped_findings
中记录,并为每个丢弃的文件添加
warning
通知),则一个或多个检查结果文件无法读取或没有可解析的前置内容,被排除在报告之外。这是丢失的结果——在
run-summary.md
和最终响应中突出显示计数和路径,就像处理未
completed
的集群任务一样。不要让看似干净的SARIF隐藏丢失的内容。
然后保证
REPORT.md
存在。与SARIF(机械生成)不同,
REPORT.md
是误报评审器的精心策划的工件,因此不要覆盖评审器编写的版本。(误报评审器使用
Bash
heredoc写入
REPORT.md
,而不是
Write
工具,因为工具框架禁止子代理使用
Write
工具写入报告文件——不要通过重新强制使用
Write
来“修复”评审器。编排器是主代理,不受此限制,因此其自身的
Write
可以正常工作。)检查它是否存在,如果缺失(评审器崩溃,甚至其
Bash
heredoc写入失败,或它返回报告作为聊天文本而不是写入文件),编排器自行写入
REPORT.md
,而不是让运行失败:
  • 如果误报评审器在记录中返回了报告正文,
    Write
    该文本到
    ${output_dir}/REPORT.md
  • 否则从磁盘上的检查结果合成:获取
    findings-index.txt
    中列出的存活主项(
    fp_verdict ∈ {TRUE_POSITIVE, LIKELY_TP}
    ,无
    merged_into
    ;如果评审器从未运行,将无
    fp_verdict
    的检查结果视为存活项),对已评审的存活项应用
    context.md
    中的
    severity_filter
    ——未评审的检查结果(无
    fp_verdict
    )无论过滤器如何都包含在内,并在
    Unvalidated (severity not judged)
    部分下渲染,带有
    [UNVALIDATED SEVERITY — not judged]
    标签,与SARIF行为一致,因此严格的过滤器永远不会静默丢弃它们——然后
    Write
    一个镜像误报评审器模板的
    REPORT.md
    ——YAML前置内容(
    stage: final-report
    threat_model
    severity_filter
    total_primaries
    reported_findings
    )、严重性分布表,然后按严重性分组的每个报告检查结果部分(CRITICAL/HIGH级嵌入Description/Code/Data flow/Impact/Recommendation正文;MEDIUM/LOW级引用检查结果文件)。
无论哪种情况,在
${output_dir}/run-summary.md
中记录
REPORT.md
是编排器合成的(不是评审器编写的)。仅当
${output_dir}/findings/
不存在(阶段2失败)时,才跳过SARIF生成器和此检查。此阶段后,更新
${output_dir}/run-summary.md
中的评审器/SARIF/报告状态。

Finding file frontmatter — three stages

阶段9:返回报告

Authoritative schema:
agents/rust-review-worker.md
("Finding File Format"). Three-stage write:
  1. Worker — base fields (
    id
    ,
    bug_class
    ,
    title
    ,
    location
    ,
    function
    ,
    confidence
    ,
    worker
    ) + seven body sections.
  2. Dedup-judge — adds
    merged_into
    on duplicates, or
    also_known_as
    +
    locations
    on primaries that absorbed.
  3. FP+Severity judge — adds
    fp_verdict
    +
    fp_rationale
    on every primary; on survivors (
    TRUE_POSITIVE
    /
    LIKELY_TP
    ) also adds
    severity
    ,
    attack_vector
    ,
    exploitability
    ,
    severity_rationale
    .
入口:阶段8b完成。出口成功标准中的每个项目都已验证为true;
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

Bug classes / clusters

检查结果文件前置内容——三个阶段

Authoritative:
prompts/clusters/manifest.json
. 37 bug classes live in
always
-gated clusters (so the cluster always runs); of those, 35 always fire and 2 —
adversarial-trait
(TRAITADV) and
closure-panic
(CLOSUREPANIC) in
logic-correctness
— additionally carry
requires: has_unsafe
, so they only fire when
has_unsafe=true
. 69 bug classes across all clusters when every conditional gate is enabled. The
memory-safety
cluster is gated on
has_unsafe
(all its bug classes require
unsafe
); PATHJOIN and TOCTOU are gated behind
has_fs_io
via
input-os-safety
, and PACKEDREF lives in the conditional
layout-safety
cluster (
has_packed_repr
); PTREXPOSE stays always-on via the
info-disclosure
cluster.
unsafe-boundary
and
concurrency-locking
are fully consolidated (their sub-prompts are not re-read at runtime).

权威模式:
agents/rust-review-worker.md
(“Finding File Format”)。三阶段写入:
  1. 工作器 — 基础字段(
    id
    bug_class
    title
    location
    function
    confidence
    worker
    )+ 七个正文部分。
  2. 去重评审器 — 在重复项上添加
    merged_into
    ,或在吸收了重复项的主项上添加
    also_known_as
    +
    locations
  3. 误报+严重性评审器 — 在每个主项上添加
    fp_verdict
    +
    fp_rationale
    ;在存活项(
    TRUE_POSITIVE
    /
    LIKELY_TP
    )上还添加
    severity
    attack_vector
    exploitability
    severity_rationale

Success Criteria

漏洞类别/集群

The phase exits already cover most of this; the orchestrator-visible end-state is:
  • Every Phase-5 cluster task is
    completed
    (verify via
    TaskList
    ).
  • ${output_dir}/run-summary.md
    exists and records resolved scope/context, Cargo manifest probe result, worker claims vs index count, task status, and judge/SARIF status.
  • Every primary finding (no
    merged_into
    ) has
    fp_verdict
    +
    fp_rationale
    ; every survivor (
    TRUE_POSITIVE
    /
    LIKELY_TP
    ) also has
    severity
    ,
    attack_vector
    ,
    exploitability
    ,
    severity_rationale
    .
  • REPORT.md
    exists, severity-filtered per
    severity_filter
    (Phase 8b safety net guarantees this even when the fp-judge fails to write it).
  • REPORT.sarif
    exists (Phase 8b safety net guarantees this).
权威来源:
prompts/clusters/manifest.json
。37个漏洞类别位于
always
gated集群中(因此集群始终运行);其中35个始终触发,2个——
logic-correctness
中的
adversarial-trait
(TRAITADV)和
closure-panic
(CLOSUREPANIC)——还带有
requires: has_unsafe
,因此仅当
has_unsafe=true
时触发。启用所有条件门时,所有集群共有69个漏洞类别。
memory-safety
集群受
has_unsafe
gated(其所有漏洞类别都需要
unsafe
);PATHJOIN和TOCTOU通过
input-os-safety
has_fs_io
gated,PACKEDREF位于条件
layout-safety
集群中(
has_packed_repr
);PTREXPOSE通过
info-disclosure
集群始终启用。
unsafe-boundary
concurrency-locking
完全合并(其子提示不会在运行时重新读取)。

成功标准

阶段退出已经涵盖大部分内容;编排器可见的最终状态是:
  • 所有阶段5的集群任务都
    completed
    (通过
    TaskList
    验证)。
  • ${output_dir}/run-summary.md
    存在,并记录了已解析的范围/上下文、Cargo manifest探测结果、工作器声称与索引计数、任务状态以及评审器/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安全网保证)。