mcpField
R
review-deep

review-deep

github.com/jimweller测试与质量19 次安装
代码仓库

关于 review-deep

review-deep 是面向团队和维护者的代码审查技能,可对仓库进行广泛的分组件审查。它让九种审查视角分别使用三种模型,并并行运行独立的覆盖扫描;审查过程与实际仓库隔离。

主要功能

  1. 01进行 27 组视角与模型审查
  2. 02将仓库划分为最多 25 个文件的组件
  3. 03运行独立的 ocr 覆盖扫描
  4. 04审查者使用隔离的提交副本
  5. 05记录发现项与文件覆盖情况

适用场景

  1. 01从安全、架构、测试等方面审查仓库
  2. 02按限定文件组件检查大型代码库
  3. 03比较不同模型提供方的审查结果
  4. 04避免模型访问实际工作树

MCP 相关

审查流程仅使用 Serena 作为 MCP 服务,并配置它导航导出的审查项目。该技能不会加载操作者的其他 MCP 服务。

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/jimweller/clanker-code-review-plugin --skill review-deep

安装后在 Claude Code 中输入/review-deep确认已加载。

包含的文件

除 SKILL.md 外,安装时一起下载的参考资料和脚本。

opencode/serena/
reviewer-context.yml
system-prompt.md
prompts/
collate.txt
merge.txt
verify.txt
scripts/
check_reviews.py
checkout.py
collate.py
common.py
evidence.py
export.py
init_run.py
ledger.py
merge.py
normalize.py
opencode_env.py
partition.py
pool.py
preflight.py
rank.py
report.py
verify.py
windows.py

详细介绍

STARTER_CHARACTER = 🕵️‍♂️

代码审查命令

从 9 个视角,使用 3 个模型审查代码库。仓库会拆分为最多包含 25 个文件的组件;27 个视角与模型的组合会分别审查每个组件,因此每个文件都会得到全部 27 项审查。每个审查分支都是独立的 opencode run 进程,在被审查提交的独立副本中工作,使用 Serena 浏览代码,并写入各自的发现。随后,每个组合都会进行一次全面检查,查找被组件边界掩盖的缺陷。

这里不会打包任务,也没有编排进程或 task 扇出。审查者会读取被审查提交中的文件,因此引用指向真实文件中的真实行。没有审查者会读取实时仓库。

同时还会运行一次 ocr scan。它会在独立对话中使用通用检查清单审查每个可审查文件,作为独立的第四个来源。

参数

如果用户在调用时提供了路径,则将其视为相对于仓库根目录的目标目录。否则审查整个仓库。这是有意设定的默认值,调用此技能的人已经接受,无需再次确认。

此技能以 context: fork 运行,并作为子 Agent 派发;没有用户在场回答提示词。第 2 步之前绝不要暂停,以确认范围、成本或模型费用。请从第 1 步直接进入派发。如果成本很重要,调用前就应通过传入更窄的路径来决定。

模型

标签模型 ID变体
openaiopenai/gpt-6-sol
geminigoogle/gemini-3.8-flash
claudeaz-anthropic/claude-opus-5-5xhigh

OCR 覆盖检查会使用 ~/.opencodereview/config.json 中的提供商和模型。

每个 ID 都必须存在于 ~/.config/opencode/opencode.json 中对应的提供商下,并列入该提供商的 whitelist。任一处缺少 ID,都会导致对应审查分支报错 Model not found,且此错误只会显示为 NDJSON 中的 error 事件。

jq -r '.provider | to_entries[] | .key as $p | .value.models | keys[] | "\($p)/\(.)"' ~/.config/opencode/opencode.json

gemini 审查分支有意使用 Flash 模型。Google 没有发布高于 gemini-3.1-pro-preview 的 Pro 模型,因此最新的 Google 模型是 gemini-3.8-flash,比最新的 Pro 模型高出七个次要版本。在一个包含 1070 个文件的仓库上进行的单次安全审查中,同一个 Agent 使用相同提示词时,Flash 给出了 11 项发现,而 Pro 给出了 6 项,使用的令牌数约为其两倍。这只是一次比较,并非基准测试。

学会这一规则花了两天

每次运行 opencode run 时,都要将标准输入重定向到 /dev/null。否则,进程会在创建会话前卡住,写入 0 字节,日志停留在 message=init,只有被终止时才会退出。opencode run 会先读取标准输入,直到 EOF,然后才执行其他操作,因此任何让标准输入保持打开状态的启动器都会让它挂起,后台作业、CI 步骤和 Agent 执行框架也都如此。这是 anomalyco/opencode issue #38723,针对 1.18.25 提出,并在此处使用 1.18.20 复现。

这种挂起看起来时有时无,但事实并非如此。启动器是否关闭标准输入决定了结果。三位报告者在 macOS arm64、Linux aarch64 和 Windows 上进行了确定性测试:不重定向时,5 次测试中 5 次挂起;重定向时,10 次测试中有 5 到 10 次成功。

本文件的早期版本禁止用 timeout 包装 opencode run,并将 44% 的失败率归咎于包装器。这是错误的。即使完全不使用 timeout,也能复现卡顿,而且上游自己的复现脚本使用了 timeout 120。移除所有超时限制,才让一个标准输入的小问题演变成持续数日的调查,因此现在 Step 2b 会检查每位审核者是否都有会话记录。

不要对审核者设置令牌上限或轮次上限。审核大型代码库需要多久就多久。

机械臂唯一可用的 MCP 服务是 Serena。Step 1 的 opencode_env.py 会使用 --project-from-cwd 启动它,而 --dir 会指向目标目录,以便 Serena 遍历并找到项目。Serena 从 SERENA_HOME 读取设置,该变量默认指向 ~/.serena-reviewer,其中存放着 serena_config.yml 和下载的语言服务器。

为什么禁用 researcher

机械臂不会加载操作员的 MCP 服务,因此 researcher 无法访问这些服务。了解它为何曾导致故障也很有价值:Google 会拒绝它的架构。

researcher 将可选数组声明为联合类型 ["null","array"],这符合 JSON Schema。Anthropic 和 OpenAI 都接受这种写法。Google 会将 JSON Schema 转换为自己的 OpenAPI 子集 proto,把联合类型转换为 any_of,从数组分支中移除 items,然后又因 any_of[0].items: missing field 拒绝自己的输出。gemini 机械臂会在 5 秒内失败,只产生一个 error 事件和 5712 字节的 NDJSON。

任何使用这种模式的 MCP 服务都会以同样的方式导致 Google 机械臂故障。如果 gemini 机械臂开始因 field predicate failed: $type == Type.ARRAY 而失败,请找到拥有相关命名属性的服务并禁用它。

操作流程

步骤 1:确定目标并做好准备
PROJECT_ROOT=$(git rev-parse --show-toplevel)
PROJECT_ROOT=$(cd -P "$PROJECT_ROOT" && pwd -P)
TARGET_PATH="<user-provided path or empty>"
[ -z "$TARGET_PATH" ] && TARGET_PATH="$PROJECT_ROOT"

TARGET_PATH=$(cd -P "$TARGET_PATH" 2>/dev/null && pwd -P) || { echo "TARGET_PATH does not exist"; exit 1; }
case "$TARGET_PATH" in
  "$PROJECT_ROOT"|"$PROJECT_ROOT"/*) ;;
  *) echo "TARGET_PATH escapes PROJECT_ROOT"; exit 1 ;;
esac

TARGET_NAME=$(basename "$TARGET_PATH")
[ "$TARGET_PATH" = "$PROJECT_ROOT" ] && TARGET_NAME="repo"

S="${CLAUDE_SKILL_DIR}/scripts"   # replace ${CLAUDE_SKILL_DIR} with this SKILL.md's own directory on a non-Claude harness
[ -f "$S/init_run.py" ] || { echo "scripts not found at $S"; exit 1; }
python3 "$S/preflight.py" --tier deep || exit 1

STATE_DIR="$PROJECT_ROOT/.llmtmp/review-deep"
mkdir -p "$STATE_DIR"
find "$STATE_DIR" -mindepth 1 -delete
mkdir -p "$STATE_DIR/components" "$STATE_DIR/coverage" "$STATE_DIR/parts"

INIT=$(python3 "$S/init_run.py" "$PROJECT_ROOT" "$TARGET_PATH" "$STATE_DIR") || exit 1
echo "$INIT"
RUN_DIR=$(printf '%s\n' "$INIT" | sed -n 's/^RUN_DIR=//p')
EXPORT=$(python3 "$S/export.py" "$RUN_DIR" | sed -n 's/^EXPORT=//p')
[ -d "$EXPORT" ] || { echo "export.py made no export"; exit 1; }

SCOPE=""
[ "$TARGET_PATH" != "$PROJECT_ROOT" ] && SCOPE="--path ${TARGET_PATH#"$PROJECT_ROOT"/}"
ocr scan --preview --format json --repo "$EXPORT" $SCOPE 2>/dev/null > "$STATE_DIR/ocr-preview.json"
jq -r '.files[] | select(.will_review) | .path' "$STATE_DIR/ocr-preview.json" > "$STATE_DIR/ledger.txt"
jq -r '.files[] | select(.will_review | not) | "\(.path)\t\(.exclude_reason)"' "$STATE_DIR/ocr-preview.json" > "$STATE_DIR/skipped.txt"

python3 "$S/partition.py" "$STATE_DIR"
python3 "$S/opencode_env.py" "$STATE_DIR" || exit 1

echo "PROJECT_ROOT=$PROJECT_ROOT"
echo "TARGET_PATH=$TARGET_PATH"
echo "TARGET_NAME=$TARGET_NAME"
echo "STATE_DIR=$STATE_DIR"
echo "EXPORT=$EXPORT"
echo "LEDGER=$(wc -l < "$STATE_DIR/ledger.txt" | tr -d ' ') reviewable, $(wc -l < "$STATE_DIR/skipped.txt" | tr -d ' ') skipped"

preflight.py --tier deep 会在任何内容被清除之前,检查此技能所需的每个工具、配置文件、模型白名单条目和提供商密钥;如果检查失败,会直接指出具体名称,而不会等到运行中途才报错。$S 守卫会以同样的方式捕获不完整或损坏的插件安装。

init_run.py 会记录已审核的提交、此刻的 HEAD,并在仓库外的 ${XDG_CACHE_HOME:-~/.cache}/review-deep/<repo>-<commit12> 创建运行目录。它会打印 RUN_DIR,步骤 5 会用到它。如果工作树中有未提交的更改,它会发出警告,因为每位审查者和裁决者读取的都是该提交,因此未提交的更改不会经过审查。如果警告列出了源文件,请先提交更改。运行目录中的 run.json 保存模型、并发数、裁决者不得查看的文件,以及工单中携带的提交说明。要更改其中任何内容,请在步骤 5 之前编辑该文件。

export.py 会使用 git archive 将已审核的提交导出到一个新的临时目录,在任意深度删除 run.json 的 judge_exclude 中列出的所有名称,例如 CLAUDE.md、AGENTS.md、.claude/、.opencode/ 和 .llmdocs/,然后将结果提交为独立的单提交仓库,以便 opencode、Serena 和 ocr 能在其中找到项目根目录。每个工作分支都会审查各自的 cp -R 副本,ocr 扫描则会审查另一份副本。模型绝不会读取或修改正在使用的仓库。在一个包含 1179 个文件的仓库中测得,导出耗时 0.6 秒,占用 12 MB,因此 27 个并发副本总共约占 324 MB。

ocr scan --preview 会在导出目录上运行,列出每个文件,但不会调用模型,因此清单与各工作分支可见的内容完全一致。ledger.txt 保存可审查的文件,skipped.txt 保存其余文件及 ocr 给出的原因,例如 binary 或 unsupported_ext;步骤 5 会将其报告为跳过清单。

partition.py 会沿目录边界将清单划分为若干部分,每部分最多 25 个文件;这是 Anthropic 的 Claude Security 扫描器采用的规模。获得整个仓库的审查者会自行决定在审查 40 到 50 个文件后停止。获得某个部分的审查者则会审查其中的每个文件。包含 41 个文件的仓库会得到 1 个部分,包含 414 个文件的仓库会得到 22 个部分。

opencode_env.py 会写入工作分支唯一读取的 opencode 配置 $STATE_DIR/opencode-config/opencode/opencode.json。它会从 ~/.config/opencode/opencode.json 复制 provider 和 enabled_providers,并将 Serena 添加为唯一的 MCP 服务,同时将此插件的审查者上下文和手册放在 opencode/serena/ 下。它还会添加 9 个主要 Agent,其提示词内容取自插件的 agents/reviewer-<area>.md 正文,与 review-full 和 review-diff 提供给 Claude 审查者的文本相同。每个 Agent 都可以读取、搜索、使用 Serena 的读取工具,并且只能在 $STATE_DIR/parts 和 $STATE_DIR/coverage 下写入。没有技能、子 Agent、shell 或 Web 工具。

使用 find -delete,不要使用 rm -f。如果 PATH 中有 safe-rm 替代程序,它会将路径移入废纸篓,并且即使使用 -f,遇到不存在的路径也会以非零状态退出,导致首次运行时的清理失败。

步骤 2:派发组件审查者并执行覆盖率检查

27 个领域与模型的组合会分别审查每个组件,因此每个组件的运行都会有 27 个工作分支。工作池会一次运行 CONCURRENCY 个任务,同时进行一次 ocr 扫描。请替换为步骤 1 打印的值。

请按原样将此代码块作为一次后台 Bash 调用运行。该调用会一直保持打开状态,直到工作池和 ocr 扫描退出;完成通知就是步骤 2 已结束的信号。绝不要使用 nohup、末尾带 & 的命令,或在后台启动的包装脚本来运行此代码块,否则调用会立即返回,之后也不会收到通知。绝不要通过日志中的 done, 行来判断是否完成。在一次 quartermaster 运行中,Agent 将代码块分离运行,并使用 tail -f | grep -m1 '^done,' 等待。grep 匹配后退出,但 tail 只有在下一次写入时才会退出,而 done, 是日志的最后一行。调用因此一直闲置,直到 60 分钟的 timeout 结束进程并杀死 tail。

export PROJECT_ROOT TARGET_PATH STATE_DIR EXPORT
AREAS="security architecture solid correctness testing ops performance quality data"
MODELS="openai:openai/gpt-6-sol gemini:google/gemini-3.8-flash claude:az-anthropic/claude-opus-5-5"
CONCURRENCY=27

cat > "$STATE_DIR/arm.sh" <<'ARM'
#!/bin/bash
label=$1 model=$2 area=$3 comp=$4
if [ ! -d "$STATE_DIR/parts" ] || [ ! -d "$TARGET_PATH" ] || [ ! -d "$PROJECT_ROOT" ] || [ ! -d "$EXPORT" ]; then
  echo "arm.sh: STATE_DIR='$STATE_DIR' TARGET_PATH='$TARGET_PATH' PROJECT_ROOT='$PROJECT_ROOT' EXPORT='$EXPORT'. Export all four with the values Step 1 printed." >&2
  exit 255
fi
VARIANT=""
[ "$label" = claude ] && VARIANT="--variant xhigh"
base="$label-$area-$comp"
arm_dir=$(mktemp -d "${TMPDIR:-/tmp}/review-deep-arm.XXXXXX")
trap 'rm -rf "$arm_dir"' EXIT
copy="$(cd -P "$arm_dir" && pwd -P)/repo"
cp -R "$EXPORT" "$copy"
work="$copy/.review-arm"
mkdir -p "$work" "$STATE_DIR/arms"
echo "$copy" > "$STATE_DIR/arms/$base.root"
target="$copy${TARGET_PATH#"$PROJECT_ROOT"}"
if [ "$comp" = sweep ]; then
  cp "$STATE_DIR/ledger.txt" "$work/ledger.txt"
  cp "$STATE_DIR/$label-$area.md" "$work/listed-findings.md"
  scope="LEDGER_PATH: $work/ledger.txt
FINDINGS_PATH: $work/listed-findings.md

LEDGER_PATH lists every reviewable source file. A component-by-component
review already covered them, and its findings are in FINDINGS_PATH. Look only
for defects that are not already listed there, especially defects that span
components. Do not repeat a listed finding."
else
  cp "$STATE_DIR/components/$comp.txt" "$work/ledger.txt"
  scope="LEDGER_PATH: $work/ledger.txt
COVERAGE_PATH: $work/coverage.txt

LEDGER_PATH lists the source files assigned to you, one path per line,
relative to $copy. Read it first and review every file it lists. The rest of
the repository is context: follow calls into it, and report defects in the
listed files.

For every path in LEDGER_PATH, write one line to COVERAGE_PATH: the path, then
reviewed or skipped, then a short reason. Every listed path must appear
exactly once before you finish."
fi
prompt="Review the source files under $target that your ledger assigns.

Navigate with serena symbolic tools: get_symbols_overview to map a file,
find_symbol to read a definition, find_referencing_symbols to find callers,
find_declaration and find_implementations to resolve a usage. Read a whole
file only when symbolic navigation cannot answer the question. Everything
you need is under $copy.

Cite path:line with the path relative to $copy, and name the enclosing
function, method, or type. A line you saw counts as verified, and find_symbol
returns a symbol's line range. When you cannot verify the exact line, cite the
nearest line you saw and end the finding with \`line unconfirmed\`. Never drop
a real defect because its line number is uncertain.

$scope

OUTPUT_PATH: $work/findings.md

Write your findings to OUTPUT_PATH. Writing that file is mandatory and is
how your work is delivered. Do not return findings as your response."
raw="$STATE_DIR/parts/raw-$base.ndjson"
wrote_findings() {
  [ -s "$work/findings.md" ] ||
    jq -r 'select(.type=="text") | .part.text' "$raw" 2>/dev/null | grep -q '^## '
}
mkdir -p "$STATE_DIR/opencode-db" "$STATE_DIR/parts/attempts"
attempt=1
max_attempts=3
while :; do
  XDG_CONFIG_HOME="$STATE_DIR/opencode-config" \
  OPENCODE_PURE=1 \
  OPENCODE_DISABLE_EXTERNAL_SKILLS=1 \
  OPENCODE_DISABLE_CLAUDE_CODE=1 \
  OPENCODE_DISABLE_PROJECT_CONFIG=1 \
  OPENCODE_DISABLE_AUTOUPDATE=1 \
  OPENCODE_DB="$STATE_DIR/opencode-db/$base.db" \
  opencode run \
    --agent "reviewer-$area" \
    -m "$model" $VARIANT \
    --format json \
    --dir "$target" \
    --title "Review $label $area $comp" \
    "$prompt" \
    < /dev/null \
    > "$raw" 2> "$STATE_DIR/parts/$base.log"

  if wrote_findings && ! grep -q '"Failed to execute statement"' "$raw" 2>/dev/null; then break; fi
  [ "$attempt" -ge "$max_attempts" ] && break
  mv "$raw" "$STATE_DIR/parts/attempts/raw-$base.attempt$attempt.ndjson"
  sleep $((5 + RANDOM % 15))
  attempt=$((attempt + 1))
done
[ -s "$work/findings.md" ] && sed "s|$copy/||g" "$work/findings.md" > "$STATE_DIR/parts/$base.md"
[ -s "$work/coverage.txt" ] && sed "s|$copy/||g" "$work/coverage.txt" > "$STATE_DIR/coverage/$base.txt"
wrote_findings || { echo "arm.sh: $base wrote no findings in $attempt attempts" >&2; exit 1; }
ARM

: > "$STATE_DIR/tasks.txt"
for comp in $(ls "$STATE_DIR/components" | sed 's/\.txt$//'); do
  for entry in $MODELS; do
    for area in $AREAS; do
      echo "${entry%%:*} ${entry#*:} $area $comp" >> "$STATE_DIR/tasks.txt"
    done
  done
done

SCOPE=""
[ "$TARGET_PATH" != "$PROJECT_ROOT" ] && SCOPE="--path ${TARGET_PATH#"$PROJECT_ROOT"/}"
OCR_COPY="$(mktemp -d "${TMPDIR:-/tmp}/review-deep-ocr.XXXXXX")/repo"
cp -R "$EXPORT" "$OCR_COPY"
ocr scan --audience agent --repo "$OCR_COPY" $SCOPE --max-tokens 1000000 \
  --format json --output "$STATE_DIR/ocr-scan.json" \
  < /dev/null > /dev/null 2> "$STATE_DIR/ocr-scan.log" &
echo $! > "$STATE_DIR/ocr.pid"

echo "dispatching $(wc -l < "$STATE_DIR/tasks.txt" | tr -d ' ') arms, $CONCURRENCY at a time, plus 1 ocr scan"
xargs -P "$CONCURRENCY" -L 1 bash "$STATE_DIR/arm.sh" < "$STATE_DIR/tasks.txt"
wait
rm -rf "$(dirname "$OCR_COPY")"
echo "done, $(ls "$STATE_DIR"/parts/*.md 2>/dev/null | wc -l | tr -d ' ') part files written"

xargs -P 会在 tasks.txt 的扁平任务列表上滚动调度任务,而不是按组件分批同步执行:27 个槽位中只要有一个空出,列表中的下一个任务就会启动,不论它属于哪个组件。c01 的任务仍在运行时,c03 的任务通常就会启动。不要报告批次数量或逐个组件的预计耗时;应根据实际吞吐量来衡量,即每分钟实际经过时间内写入的 parts/*.md 数量,并据此推算剩余时间。

CONCURRENCY=27 会同时运行 27 个 opencode 进程,一台 48 GB 的机器可以轻松承载。每个进程占用 370 到 800 MB。

审查器以独立设备模式运行。XDG_CONFIG_HOME 会让 opencode 使用 Step 1 生成的配置,因此不会加载操作人员的 opencode Agent、技能、规则、插件或 MCP 服务。凭据仍可正常解析,因为 opencode 会将 auth.json 保存在 XDG_DATA_HOME 下,而服务提供商会从环境变量读取密钥。OPENCODE_PURE=1 会跳过外部插件,OPENCODE_DISABLE_EXTERNAL_SKILLS=1 会跳过对 ~/.claude 和 ~/.agents 的技能扫描,OPENCODE_DISABLE_CLAUDE_CODE=1 会跳过 ~/.claude/CLAUDE.md 和 .claude/skills,而 OPENCODE_DISABLE_PROJECT_CONFIG=1 会跳过被审查仓库的 opencode.json、.opencode/ 和 AGENTS.md。我们使用 opencode 1.18.20,通过 opencode debug config、opencode debug skill 和一个要求模型找出预先埋入的指令,对每项设置进行了测试。

每个任务臂都会在导出内容的副本 $TMPDIR/review-deep-arm.* 中运行,并将 --dir 指向该副本内的目录。arm.sh 会将任务臂的账本,以及在一次扫描中该区域合并后的发现项,放入副本的 .review-arm/。任务臂会在此处使用 write 写入发现项和覆盖记录;如果使用的是 OpenAI 模型,则通过 apply_patch 写入。arm.sh 会将这些文件复制到 parts/ 和 coverage/,从引用中移除副本的路径,并在退出时删除副本。生成的 Agent 会先拒绝所有工具,然后允许使用 read、grep、glob、写入工具,以及 Serena 中除四类写入操作外的工具。一个共享的 external_directory 规则会拒绝访问任务臂自身副本之外的所有路径,而 edit 规则只允许在 .review-arm/ 下写入。我们在 opencode 1.18.20 上进行了测试:对副本外的绝对路径使用 grep、使用 glob 和 read 均会被拒绝,而在副本内使用 grep 则可正常工作。

Serena 作为独立进程运行,因此 opencode 的规则无法约束它。我们在 Serena 1.7.0 上进行了测试:它会拒绝离开副本的相对路径和绝对目录路径,但对副本外文件的绝对路径调用 find_symbol、get_symbols_overview 和 get_diagnostics_for_file 时,仍会返回结果。知道或猜到机器上其他位置的文件路径的任务臂,仍可通过 Serena 读取该文件中的符号。它能解析副本内的 go.work 模块,即使该模块跨越了模块边界。它们以 mode: primary 运行,因为通过 --agent 选择 Agent 时,opencode 会忽略子 Agent 的 tools 块。

审查器不得调用 task。在一个包含 1070 个文件的仓库中,27 个审查器里有 2 个启动了一个始终未返回的子 Agent;由于 TaskTool 会以前台方式运行子 Agent,并在 yield 处阻塞,父进程会卡住,Step 2 的 wait 也会因此卡住。这些 Agent 设置了 tools.task: false 和 permission.task: {"*": deny}。修复后,两名审查器均顺利完成,task 调用次数为 0。

目前没有任何机制可防止同时运行两个任务。Step 1 会清空 STATE_DIR,而每个任务臂都会写入固定路径,因此对同一仓库再次运行会破坏第一次运行的结果。调度任务前,请先检查是否已有任务正在运行。

服务提供商的限额并不是瓶颈。OpenAI 报告的每分钟令牌数为 40,000,000,Azure Foundry 则为 15,000,000。 opencode 自身的会话存储在 CONCURRENCY=27 时会因写入压力而崩溃。在一个包含 1217 个文件的仓库上测得:26 个 arm 在四秒内集中失败,错误为 error="Failed to execute statement",问题出在 opencode 共享的 SQLite 存储中(可在 ~/.local/share/opencode/log/opencode.log 中看到,而 arm 自身的 stderr 中看不到,后者始终为空)。其中 25 个完全没有写入输出。已在上游确认(anomalyco/opencode#48416、#47566、#38849、#33320、#21215):opencode 的 SQLite 连接使用 busy_timeout=0,因此争抢写锁失败的写入者会立即报错,而不会等待;SQL 适配器上的 Effect.orDie 会将其转成致命消息错误,而不是警告。失败的写入会在任意并发进程的工具调用返回后,立即从 child_process 退出处理程序中触发,因此它取决于当时有多少 opencode 进程处于运行且繁忙状态,而不是刚刚启动了哪些进程。

arm.sh 为每个 arm 使用独立数据库,不再争用共享数据库:OPENCODE_DB="$STATE_DIR/opencode-db/$base.db"(通过阅读 opencode 自身的源代码确认:它会直接检查 OPENCODE_DB;若给定绝对路径,就将其用作 SQLite 文件,不会再相对于共享数据目录解析)。这是真正的修复,而非变通方案;这种规模的运行不再访问 ~/.local/share/opencode/opencode.db,写入争用失败也就无从发生。auth.json、提供方凭据以及 bin/ 下下载的工具缓存都不会受到影响,因为只有数据库路径发生变化。

在两种情况下,重试循环会以相同的 opencode run 调用最多再运行两次。第一种情况是 arm 的 NDJSON 中匹配到 "Failed to execute statement",这是为每个 arm 独立数据库仍未覆盖的情况提供纵深防御。第二种情况是 arm 结束时没有发现结果,也就是说 OUTPUT_PATH 为空,且没有任何文本事件包含 ## 标题。在一个包含 718 个文件的仓库上测得:351 个 gemini arm 中有 20 个读取了文件,没有进行写入调用,并以 0 个输出 token 的 stop 步骤结束。写入了发现结果的 331 个 gemini arm 中,没有一个以这种方式结束。每次尝试之间会进行 5 至 19 秒的抖动休眠,避免重试再次集中到同一个争用时段。被后续尝试取代的尝试,其 NDJSON 会移动到 parts/attempts/raw-$base.attemptN.ndjson,且永不删除。该目录位于 Step 3 提取的 parts/raw-*.ndjson glob 匹配范围之外。若 arm 重试 3 次后仍没有发现结果,则会以 1 退出,在 stderr 中输出其名称,并在 Step 3 中显示为普通的 MISSING 条目。

如果 STATE_DIR、TARGET_PATH、PROJECT_ROOT 或 EXPORT 未设置或指定的目录不存在,arm.sh 会以 255 退出。xargs 在遇到 255 退出码后会停止启动 arm,因此,从未导出这些变量的 shell 中分派批次,会导致其在启动最初几个 arm 后失败,而不是报告成功。Shell 变量不会在 Bash 调用之间保留,因此每次运行 arm.sh 的调用都要导出全部四个变量。有一个重试批次在 STATE_DIR 为空的情况下运行了全部 20 个 arm。每个 arm 都因 mkdir: /opencode-db: Read-only file system 而失败,但批次仍以 0 退出。

步骤 2b:确认执行单元已启动

在步骤 2 仍在等待时,单独以前台调用的方式运行此命令。未能创建会话的执行单元不会写入任何内容,也永远不会写入,并且会占用一个池槽位。

find "$STATE_DIR/parts" -maxdepth 1 -name 'raw-*.ndjson' -size 0 -mmin +1 | sed 's|.*/raw-||; s|\.ndjson$||; s/^/NO SESSION /'

正常的执行单元会在启动后几秒内写入 NDJSON。终止此处列出的所有执行单元。步骤 3 会重新派发它们。

使用步骤 2 保存的 PID,以相同方式检查 ocr 扫描。它以 node 运行,而不是 ocr,因此 pgrep -x ocr 或匹配 ocr scan 名称都找不到它;有一次,进程检查没有结果时,误报为“ocr 扫描已退出”,但它实际上仍在运行。

OCR_PID=$(cat "$STATE_DIR/ocr.pid" 2>/dev/null)
if [ -n "$OCR_PID" ] && kill -0 "$OCR_PID" 2>/dev/null; then
  echo "ocr scan running, pid $OCR_PID, log last wrote $(stat -f '%Sm' "$STATE_DIR/ocr-scan.log" 2>/dev/null)"
else
  echo "ocr scan not running and $STATE_DIR/ocr-scan.json is $([ -s "$STATE_DIR/ocr-scan.json" ] && echo present || echo MISSING)"
fi

几分钟没有新增输出的旧日志并不能证明进程卡住了;该工具只会记录警告和失败的工具调用,因此成功工作期间没有日志是正常的。在一个包含 1217 个文件的仓库中测得:日志安静了约 40 分钟,之后没有再写入任何内容,并在启动约 80 分钟后干净退出,且存在 ocr-scan.json,状态为 status=success(已审查 719 个文件,发现 2586 个问题)。对于这种规模的仓库,数十分钟没有日志是正常运行的预期表现,并非异常症状。进程不在 ps 中(kill -0 失败)且缺少 ocr-scan.json,才能证明它已停止。仅在这种情况下,才使用步骤 2 中的相同命令重新启动,并使用相同的 $STATE_DIR/ocr-scan.json 路径。

步骤 3:收集并验证

有时,执行单元会将发现作为响应返回,而不是写入 OUTPUT_PATH。gpt-6-sol 就是如此。发现内容位于 NDJSON 中,因此请先将它们复制出来。没有可复制内容的执行单元不会留下部件文件,因此 MISSING 检查和文件计数会一致。然后将每个区域和模型的部件合并为一个文件。

合并操作会从各个组成部件重新构建每个合并文件,并且在步骤 4 运行后,还会加入该区域的扫描部件。无论何时再次运行,结果都相同,因此在扫描后重新派发不会丢失扫描结果。

for f in "$STATE_DIR"/parts/raw-*.ndjson; do
  b=$(basename "$f" .ndjson); out="$STATE_DIR/parts/${b#raw-}.md"
  [ -s "$out" ] && continue
  jq -r 'select(.type=="text") | .part.text' "$f" | awk '/^## /{p=1} p' | sed -E 's#[^ `]*/review-deep-arm\.[^/ ]+/repo/##g' > "$out"
  [ -s "$out" ] || rm -f "$out"
done

for label in openai gemini claude; do
  for area in security architecture solid correctness testing ops performance quality data; do
    out="$STATE_DIR/$label-$area.md"
    parts=$(ls "$STATE_DIR"/parts/"$label-$area"-c*.md "$STATE_DIR"/parts/"$label-$area"-sweep.md 2>/dev/null)
    [ -z "$parts" ] && continue
    { grep -h -m1 '^## ' $parts | head -1; echo; grep -h '^- \*\*' $parts; } > "$out"
    grep -q '^- \*\*' "$out" || echo "No findings." >> "$out"
    for c in "$STATE_DIR"/components/*.txt; do
      comp=$(basename "$c" .txt)
      [ -s "$STATE_DIR/parts/$label-$area-$comp.md" ] || echo "MISSING $label $area $comp"
    done
  done
done
for label in openai gemini claude; do
  echo "== $label =="
  for area in security architecture solid correctness testing ops performance quality data; do
    f="$STATE_DIR/$label-$area.md"
    if [ ! -s "$f" ]; then
      echo "  $area MISSING"
    else
      n=$(grep -c '^- \*\*' "$f")
      cited=$(grep -oE '^- \*\*(Critical|High|Medium|Low)\*\* `[^`]+`' "$f" | grep -c ':[0-9]')
      unconf=$(grep -c 'line unconfirmed' "$f")
      echo "  $area ok findings=$n cited=$cited unconfirmed=$unconf"
    fi
  done
done

然后检查覆盖情况。每个执行单元的覆盖记录会说明它审查了哪些文件。已打开文件数会根据执行单元实际读取的内容检查这些记录。

total=$(wc -l < "$STATE_DIR/ledger.txt" | tr -d ' ')
while read -r label model area comp; do
  f="$STATE_DIR/coverage/$label-$area-$comp.txt"
  want=$(wc -l < "$STATE_DIR/components/$comp.txt" | tr -d ' ')
  got=$(awk '$2 ~ /^reviewed/' "$f" 2>/dev/null | wc -l | tr -d ' ')
  [ "$got" -lt "$want" ] && echo "SHORT $label $area $comp reviewed=$got of $want"
done < "$STATE_DIR/tasks.txt"
cat "$STATE_DIR"/coverage/*.txt 2>/dev/null | awk '$2 ~ /^reviewed/ {print $1}' | sort -u > "$STATE_DIR/reviewed.txt"
echo "ledger files reviewed by at least one arm=$(comm -12 <(sort "$STATE_DIR/ledger.txt") "$STATE_DIR/reviewed.txt" | wc -l | tr -d ' ') of $total"
comm -23 <(sort "$STATE_DIR/ledger.txt") "$STATE_DIR/reviewed.txt" | sed 's/^/  never reviewed: /'
jq -r 'select(.type=="tool_use") | .part.state.input | (.filePath // .relative_path // .path // empty)' \
  "$STATE_DIR"/parts/raw-*.ndjson 2>/dev/null | sed -E 's#^.*/review-deep-arm\.[^/]+/repo/##' | sort -u > "$STATE_DIR/opened.txt"
echo "ledger files opened=$(comm -12 <(sort "$STATE_DIR/ledger.txt") "$STATE_DIR/opened.txt" | wc -l | tr -d ' ') of $total"
if [ -s "$STATE_DIR/ocr-scan.json" ]; then
  jq -r '"ocr status=\(.status) files=\(.summary.files_reviewed) findings=\(.comments|length)"' "$STATE_DIR/ocr-scan.json"
else
  echo "ocr-scan.json MISSING, run the Step 2b ocr liveness check before treating this as a dead scan"
fi

SHORT 行表示某个执行单元将其部分组成文件标记为 skipped,或未写入覆盖记录。请在覆盖记录中查看原因。因文件属于其他区域而跳过是可以接受的。因审查深度不足而跳过则是缺口。

从一个导出步骤 1 打印的值的 Shell 中,重新派发 MISSING 执行单元,然后再次运行上面的提取与合并代码块。arm.sh 已经重试过该执行单元两次,因此只需重新派发一次。

export PROJECT_ROOT=<PROJECT_ROOT> TARGET_PATH=<TARGET_PATH> STATE_DIR=<STATE_DIR> EXPORT=<EXPORT>
bash "$STATE_DIR/arm.sh" <label> <model> <area> <comp>

如果某个区域的合并文件缺失,或者既没有发现行,也没有且仅有 No findings.,则将该区域视为未审查。绝不要将这两种情况记录为 No findings.。一次重试后仍缺失的所有执行单元,都应报告给操作人员并标记为未审查,同时注明模型、区域和组成文件。

比较 cited 与 findings。如果某项发现的第一个反引号字段不含 :line,则该发现不可用,因为除非重新阅读整个文件,否则无人能够确认它。cited 会统计带有 path:line 的发现,无论其末尾是否标有 line unconfirmed;因此,审查者认可的数量应从中减去 unconfirmed。重新派发前,请先阅读未引用来源的发现;只有它们也没有符号时,才重新派发。

步骤 4:全面扫描

每个区域和模型各启动一个全面扫描分支。它会获取完整台账和该区域合并后的发现,只查找尚未列出的问题。这能发现组件边界掩盖的问题。

: > "$STATE_DIR/sweeps.txt"
for entry in $MODELS; do
  for area in $AREAS; do
    [ -s "$STATE_DIR/${entry%%:*}-$area.md" ] && echo "${entry%%:*} ${entry#*:} $area sweep" >> "$STATE_DIR/sweeps.txt"
  done
done
xargs -P "$CONCURRENCY" -L 1 bash "$STATE_DIR/arm.sh" < "$STATE_DIR/sweeps.txt"

像步骤 2 一样,将此代码块作为一次后台 Bash 调用运行。完成通知就是继续操作的信号。

然后重新运行步骤 3 的提取与合并代码块。它会提取每次全面扫描的发现,并据此重建该区域的合并文件。之后检查每次全面扫描。

while read -r label model area comp; do
  s="$STATE_DIR/parts/$label-$area-sweep.md"
  if [ -s "$s" ]; then echo "$label-$area sweep added=$(grep -c '^- \*\*' "$s")"; else echo "MISSING $label $area sweep"; fi
done < "$STATE_DIR/sweeps.txt"

按照步骤 3 中 MISSING 分支的相同方式,重新派发 MISSING 全面扫描。

如果某个代码块作为独立调用运行,而非与步骤 2 一起运行,请重新导出 STATE_DIR、TARGET_PATH、PROJECT_ROOT 和 EXPORT,并重置 AREAS、MODELS 和 CONCURRENCY。

最后一次全面扫描和最后一次重新派发结束后,删除导出内容。步骤 5 会为评审员单独检出代码。

rm -rf "$EXPORT"
步骤 5:汇总、验证并报告

运行脚本化阶段。将每个模型阶段作为后台 Bash 调用运行。所有调用完成后,调用池才会返回,完成通知就是继续操作的信号。切勿使用 sleep 轮询正在运行的阶段。直接将阶段命令作为后台调用运行。切勿使用 nohup 或在命令末尾加上 & 来分离进程,也切勿使用 tail -f | grep -m1 等待日志行,原因见步骤 2。

S="${CLAUDE_SKILL_DIR}/scripts"   # replace ${CLAUDE_SKILL_DIR} with this SKILL.md's own directory on a non-Claude harness
[ -f "$S/init_run.py" ] || { echo "scripts not found at $S"; exit 1; }
RUN=<RUN_DIR printed by Step 1>
python3 "$S/checkout.py" "$RUN"
python3 "$S/normalize.py" "$RUN"
python3 "$S/collate.py" "$RUN"         # one model call per component, background
uv run --with scikit-learn --with scipy --with numpy python "$S/windows.py" "$RUN"
python3 "$S/merge.py" "$RUN"           # one model call per similarity window, background
python3 "$S/rank.py" "$RUN"
python3 "$S/verify.py" "$RUN"          # one model call per Critical or High issue, background
python3 "$S/report.py" "$RUN"
阶段输出功能
checkout.pycheckout/使用 git archive 导出已评审的提交,并删除 judge_exclude 路径,例如 CLAUDE.md、.claude/ 和评审状态。每个模型阶段只读取此目录树。
normalize.pyfindings.jsonl解析每个合并后的发现文件,并将 OCR 注释转换为带有稳定 ID 的发现。遇到任何无法解析的项目符号时,以状态码 1 退出。
collate.pyissues-by-component.jsonl将每个组件的发现归类为问题,其中一个问题对应一项代码更改。每个发现 ID 必须且只能归入一个问题或丢弃列表。
windows.pywindows.json按文本相似度对所有问题排序,并划分重叠窗口,每个窗口包含 150 个问题,因此同一窗口可以看到在两个组件中报告的缺陷。
merge.pyglobal_merges.json合并描述同一缺陷的问题,即一次更改即可全部修复的问题。独立位置中的相同模式仍分别保留。
rank.pyissues.jsonl、issues.md应用合并结果,并按严重程度、来源数和发现数排序。
verify.pyverify/results.jsonl、verify/second.jsonl使用高档 Opus 尝试反驳每个严重和高优先级问题,并返回判定、引用的证据、无法成立的位置和成员,以及其自身评定的严重程度。高档 Fable 会对验证者确认的每个严重问题进行盲审复核。
report.pyreport.md汇总数量、按两次评级中较低评级列出每个已确认的问题、列出带有原因的反驳问题,以及覆盖缺口。

每次模型调用都会作为独立的 claude -p --bare 进程运行,使用 --tools Bash,Read 和 --permission-mode dontAsk,提示词缓存时间为 5 分钟,提示词通过标准输入传入。调用池会检查每个进程是否恰好以这些工具启动,检查每个回答的内容,并将拒绝回答的原因附加后最多重试三次。如果判定中的证据引用并非来自已评审提交中的代码,该回答会被拒绝。review-tickets 技能文档说明了这些设置背后的衡量依据。

合并规则比以前更严格。早期规则还会合并具有相同修复方式的同类缺陷。结果生成了一个包含 77 条发现、涉及 24 个文件的问题,而据此生成的工单引用的位置并不能支持该主张。

向操作人员报告 report.md 中的统计数量、已确认的严重问题以及覆盖缺口。运行目录是 review-tickets 技能的输入。

预期输出文件

.llmtmp/review-deep/ 保存整个运行过程,并会在下次运行开始时清空:

  • <label>-<area>.md,27 个合并后的发现文件,包含组件部分和全面检查
  • ocr-scan.json、ocr-scan.log 和 ocr.pid,覆盖率检查
  • ledger.txt 和 skipped.txt,分别记录可审查文件,以及附有 ocr 排除原因的文件
  • components/cNN.txt,分区
  • coverage/<label>-<area>-<comp>.txt,每个执行臂的已审查或已跳过记录
  • reviewed.txt 和 opened.txt,分别记录执行臂标记为已审查的文件,以及它们打开过的文件
  • parts/<label>-<area>-<comp>.md、parts/raw-*.ndjson、parts/*.log,分别记录每个执行臂的发现、事件流和标准错误
  • parts/attempts/raw-*.attemptN.ndjson,被取代的尝试的事件流;当 arm.sh 在 SQLite 写入争用失败后重试,或某个执行臂未写入任何发现时保留
  • opencode-db/<label>-<area>-<comp>.db,每个执行臂独立的 opencode 会话数据库
  • opencode-config/,生成的 opencode 配置,供每个执行臂运行,并包含按区域划分的提示词文件
  • arms/<label>-<area>-<comp>.root,每个执行臂运行所在副本的路径,也是其工具调用可能触及的唯一根目录
  • arm.sh、tasks.txt、sweeps.txt,运行器及其工作列表

运行目录 ${XDG_CACHE_HOME:-~/.cache}/review-deep/<repo>-<commit12>/ 保存第 5 步的输出,并会在审查不同提交时继续保留:

  • run.json,记录仓库、提交、模型、并发数、评审排除项和提交说明
  • checkout/,供每位评审者读取的导出内容,以及 checkout_excluded.json
  • findings.jsonl、issues-by-component.jsonl、dropped.jsonl、unassigned.jsonl、windows.json、global_merges.json
  • issues.jsonl 和 issues.md,包含每个问题及其内联的成员发现
  • verify/results.jsonl 和 verify/second.jsonl,评审结果
  • report.md
  • collate/、merge/、verify/first/、verify/second/,每个目录均包含 pool.log,以及每个模型进程位于 calls/ 下的事件流

NDJSON 参考

每行一个 JSON 对象。跳过解析失败的行。

类型含义关键字段
step_start大模型回合开始
text模型输出文本part.text
tool_use模型调用工具part.tool、part.state.status、part.state.input
step_finish回合完成part.reason、part.cost、part.tokens
error会话级错误error.data.message

step_finish 的原因是 stop 表示已完成,tool-calls 表示仍在继续。

jq -s '[.[] | select(.type=="step_finish") | .part.cost] | add' "$NDJSON"
tac "$NDJSON" | jq -s 'first(.[] | select(.type=="step_finish")) | .part.tokens.total'
jq -r 'select(.type=="tool_use") | .part.tool' "$NDJSON" | sort | uniq -c | sort -rn
jq -r 'select(.type=="error") | .error.data.message' "$NDJSON"

Serena 在一个 TypeScript 仓库中执行了 181 次工具调用中的 119 次;在一个包含 HTML、CSS 和 Dockerfile 的仓库中,则只执行了 28 次中的 2 次。对于没有语言服务器支持的文件,符号导航几乎没有价值。

测试

针对 review-deep 和 review-tickets 的测试与评估,以及覆盖全部四项技能的防护测试和预检测试,都位于此插件仓库根目录下的 evals/ 中,不属于已安装的技能。

检查项命令(在 evals/ 中运行)成本
单元测试:每项技能各一个目录,另加预检和防护测试uv run --with pytest pytest -q unit/review_deep、unit/review_tickets、unit/preflight 和 unit/guard无
使用 harbor fixture 对全部三种类型进行端到端重放uv run --with pytest pytest -q test_end_to_end.py无
针对 fixture/truth.json 的实时质量评估python3 live_eval.py WORK_DIR --run按标价约 $3

unit/review_deep 会导入 windows.py,而该文件会导入 scikit-learn、scipy 和 numpy,因此请使用 uv run --with pytest --with numpy --with scipy --with scikit-learn pytest -q unit/review_deep 运行。其他目录无需额外的包。

每次修改 scripts/ 后,都要运行单元测试和重放。提示词发生变化时,重放会因 "prompt changed" 而停止。使用 python3 pipeline.py WORK_DIR --mode record 重新录制,使用 live_eval.py 检查运行结果,再用 python3 golden.py save RUN_DIR 保存。修改模型、effort level 或提示词后,按需运行实时评估。

规则

  • 发起调用的 Agent 只负责启动,不执行任何评审分析。所有分析都由 Step 5 脚本及其模型调用完成。
  • 绝 NEVER 让 Step 5 模型调用读取实时工作树。它们会在运行目录的 checkout/ 中运行。
  • 绝 NEVER 使用 sleep、等待循环或 tail -f | grep -m1 之类的日志监视器轮询后台阶段。请等待其完成通知。
  • 将提示词保存在 prompts/ 中,格式为 .txt。Markdown 格式化工具曾重写 .md 提示词并改变其含义。
  • 始终在 opencode run 上将标准输入重定向自 /dev/null。省略此项会导致进程在创建会话前挂起,没有输出也没有错误;这是 anomalyco/opencode issue #38723。
  • 不要为评审者设置 token 上限或轮次上限。让它完成。
  • 派发后约一分钟运行 Step 2b,长时间运行时也要再次运行。NDJSON 为空的 arm 已失效,却会占用池槽位;仅凭耗时无法区分这种情况和缓慢的评审。
  • 始终将 Step 2 和 Step 4 的派发代码块作为后台 Bash 调用运行,绝不要使用 nohup 或 & 将其脱离会话运行。池会阻塞,直到每个 arm 都退出;放到后台运行可避免阻塞主会话。调用的完成通知是池已完成的唯一信号。
  • 每个运行 arm.sh 的 Bash 调用都必须始终导出 STATE_DIR、TARGET_PATH 和 PROJECT_ROOT,包括单次重新派发。Shell 变量不会跨调用保留;缺少这些变量时,arm.sh 会以 255 退出。
  • 在同一个 Bash 代码块中,通过池启动每个 arm,并运行 OCR 扫描。它们是彼此独立的进程。
  • 使用普通消息调用,不要使用 --command。--command 标志存在与 context7 MCP 服务配合使用时的已知问题。
  • 运行期间不要清理各区域文件、NDJSON 或日志。Step 1 会在下次运行开始时清理它们。
  • 如果某个 arm 失败,仍要等待其他 arm 完成并报告它们的结果。
  • opencode arms 只需要操作员在 ~/.config/opencode/opencode.json 中配置的提供方,以及位于 ~/.serena-reviewer 的 Serena 主目录。它们的 Agent、Serena 上下文和 Serena 手册都由此插件通过 opencode_env.py 提供。preflight.py --tier deep 会在 Step 1 清理任何内容之前,按名称检查提供方和 Serena 主目录。

评论

支持 Markdown