review-deep

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese
<!-- markdownlint-disable-file MD041 -->
STARTER_CHARACTER = 🕵️‍♂️
<!-- markdownlint-disable-file MD041 -->
STARTER_CHARACTER = 🕵️‍♂️

Code Review Command

代码审查命令

Review a codebase from 9 perspectives against 3 models. The repo is cut into components of at most 25 files, and each of the 27 perspective and model pairs reviews every component, so every file gets all 27 reviews. Each arm is its own
opencode run
process that works in its own copy of the reviewed commit, navigates it with Serena, and writes its own findings. A sweep per pair then looks for defects the component boundaries hid.
Nothing is packed. There is no orchestrator process and no
task
fan-out. A reviewer reads the files of the reviewed commit, so a citation points at a real line in a real file. No reviewer reads the live repository.
One
ocr scan
runs alongside them. It reviews every reviewable file in its own conversation with a generic checklist, which makes it an independent fourth source.
使用 3 个模型从 9 个视角审查代码库。仓库按最多 25 个文件划分为组件,每个组件都会接受 27 个“视角与模型”组合的审查,因此每个文件都会得到全部 27 次审查。每个 arm 都是独立的
opencode run
进程,在被审查 commit 自己的副本中工作,通过 Serena 导航,并写出自己的发现项。随后,每个组合都会执行一次 sweep,查找因组件边界而隐藏的缺陷。
不采用打包方式。没有 orchestrator 进程,也没有
task
fan-out。审查器读取的是被审查 commit 的文件,因此引用会指向真实文件中的真实行。没有任何审查器读取实时仓库。
一个
ocr scan
与它们并行运行。它在独立对话中依据通用检查清单审查每个可审查文件,因此构成独立的第四个来源。

Arguments

参数

If the user provided a path with the invocation, treat it as the target directory relative to the repo root. Otherwise review the whole repo. That is the deliberate default, already accepted by whoever invoked this skill, not a decision to re-confirm.
This skill runs
context: fork
, dispatched as a subagent with no user present to answer a prompt. Never pause before Step 2 to confirm scope, cost, or model spend. Proceed straight through Step 1 into dispatch. If cost matters, that call was made before invocation, by passing a narrower path.
如果用户在调用时提供了路径,则将其视为相对于仓库根目录的目标目录。否则审查整个仓库。这是有意设定的默认行为,调用此 skill 的人已经接受该行为,因此无需再次确认范围。
此 skill 通过
context: fork
运行,作为没有用户在场回答 prompt 的子代理进行调度。绝不要在第 2 步之前暂停以确认范围、成本或模型支出。直接从第 1 步进入调度。如果成本很重要,应在调用之前通过传入更窄的路径作出决定。

Models

模型

LabelModel IDVariant
openaiopenai/gpt-6-sol
geminigoogle/gemini-3.8-flash
claudeaz-anthropic/claude-opus-5-5xhigh
The ocr coverage pass uses the provider and model in
~/.opencodereview/config.json
.
Every ID must exist in
~/.config/opencode/opencode.json
under the matching provider and in that provider's
whitelist
. An ID missing from either fails that arm with
Model not found
, which surfaces only as an
error
event in the NDJSON.
bash
jq -r '.provider | to_entries[] | .key as $p | .value.models | keys[] | "\($p)/\(.)"' ~/.config/opencode/opencode.json
The gemini arm runs a Flash model on purpose. Google publishes no pro above
gemini-3.1-pro-preview
, so the newest Google model available is
gemini-3.8-flash
, seven minor versions ahead of the newest pro. Measured on one security review of a 1070-file repo, flash returned 11 findings against pro's 6 on the same agent and prompt, at roughly twice the tokens. That is a single comparison, not a benchmark.
标签模型 ID变体
openaiopenai/gpt-6-sol
geminigoogle/gemini-3.8-flash
claudeaz-anthropic/claude-opus-5-5xhigh
ocr 覆盖率检查使用
~/.opencodereview/config.json
中配置的 provider 和 model。
每个 ID 都必须存在于
~/.config/opencode/opencode.json
中对应 provider 下,并且必须位于该 provider 的
whitelist
中。如果 ID 未在其中任一处出现,该 arm 会以
Model not found
失败;这只会作为 NDJSON 中的
error
事件呈现。
bash
jq -r '.provider | to_entries[] | .key as $p | .value.models | keys[] | "\($p)/\(.)"' ~/.config/opencode/opencode.json
gemini arm 有意使用 Flash 模型。Google 没有发布高于
gemini-3.1-pro-preview
的 pro,因此可用的最新 Google 模型是
gemini-3.8-flash
,它比最新的 pro 高七个次版本。在一个包含 1070 个文件的仓库的一次安全审查中实测,flash 使用相同 agent 和 prompt 返回了 11 个发现项,而 pro 返回 6 个,token 消耗约为两倍。这只是一次对比,并非 benchmark。

The rule that cost two days to learn

花了两天才弄明白的规则

Redirect stdin from
/dev/null
on every
opencode run
. Without it the process blocks before creating a session, writes zero bytes, logs nothing past
message=init
, and exits only when something kills it.
opencode run
reads stdin to EOF before it does anything else, so any launcher that leaves stdin open hangs it, and a background job, a CI step, and an agent harness all do. This is anomalyco/opencode issue #38723, open against 1.18.25 and reproduced here on 1.18.20.
The hang looks intermittent and is not. Whether the launcher closes stdin decides it. Three reporters measured it deterministically across macOS arm64, Linux aarch64, and Windows, 5 of 5 hangs without the redirect against 5 to 10 of 10 successes with it.
An earlier version of this file banned wrapping
opencode run
in
timeout
and blamed a 44 percent failure rate on the wrapper. That was wrong. The stall reproduces with no
timeout
anywhere, and upstream's own reproduction script uses
timeout 120
. Removing every bound is what turned a one-line stdin bug into a multi-day investigation, so Step 2b now checks each reviewer for a session row.
Impose no token cap and no turn cap on a reviewer. A review of a large repo takes as long as it takes.
Serena is the only MCP server the arms get. Step 1's
opencode_env.py
starts it with
--project-from-cwd
, and
--dir
points at the target so Serena's walk finds the project. Serena reads its settings from
SERENA_HOME
,
~/.serena-reviewer
by default, which holds its
serena_config.yml
and downloaded language servers.
每次运行
opencode run
时,都必须将 stdin 从
/dev/null
重定向。否则,进程会在创建 session 前阻塞,不写入任何字节,不记录
message=init
之后的任何日志,并且只有在某个进程将其终止时才会退出。
opencode run
在执行其他操作前会一直读取 stdin 直到 EOF,因此,任何保持 stdin 处于打开状态的启动器都会使它挂起,而后台作业、CI 步骤和 agent harness 都会出现这种情况。这是 anomalyco/opencode issue #38723;该 issue 针对 1.18.25 仍处于开放状态,并已在此处的 1.18.20 上复现。
这种挂起看起来像是间歇性故障,但实际上并非如此。是否挂起完全取决于启动器是否关闭 stdin。三名报告者在 macOS arm64、Linux aarch64 和 Windows 上进行了确定性测试:不重定向时 5 次全部挂起,进行重定向后 5 到 10 次全部成功。
此文件的早期版本禁止将
opencode run
包装在
timeout
中,并将 44% 的失败率归咎于该包装器。这一判断是错误的。即使任何地方都没有
timeout
,阻塞问题仍会复现,而且上游自己的复现脚本也使用了
timeout 120
。移除所有时间限制后,一个单行 stdin 缺陷变成了持续数天的调查,因此第 2b 步现在会检查每个审查器是否拥有 session 记录。
不要对审查器施加 token 上限或轮次上限。审查大型仓库需要多长时间就应让它运行多长时间。
Serena 是 arm 唯一可用的 MCP server。第 1 步的
opencode_env.py
使用
--project-from-cwd
启动它,而
--dir
指向目标目录,使 Serena 能够找到该项目。Serena 从
SERENA_HOME
读取设置,默认位置为
~/.serena-reviewer
,其中保存
serena_config.yml
和已下载的语言服务器。

Why researcher is disabled

禁用 researcher 的原因

The arms never load the operator's MCP servers, so
researcher
cannot reach them. It is worth knowing why it once broke them: Google rejects its schemas.
researcher
declares optional arrays as the union type
["null","array"]
, which is valid JSON Schema. Anthropic and OpenAI accept it. Google converts JSON Schema into its own OpenAPI-subset proto, turns the union into
any_of
, drops
items
from the array branch, then rejects its own output with
any_of[0].items: missing field
. The gemini arm fails in 5 seconds with one
error
event and 5712 bytes of NDJSON.
Any MCP server using that pattern breaks a Google arm the same way. If a gemini arm starts failing with
field predicate failed: $type == Type.ARRAY
, find the server owning the named properties and disable it.
arm 绝不会加载操作者的 MCP server,因此
researcher
无法访问它们。值得说明的是它过去为何会导致 arm 失败:Google 会拒绝其 schema。
researcher
将可选数组声明为联合类型
["null","array"]
,这是有效的 JSON Schema。Anthropic 和 OpenAI 可以接受它。Google 会将 JSON Schema 转换为其 OpenAPI 子集 proto,把联合类型转换为
any_of
,并从数组分支中删除
items
,随后又因
any_of[0].items: missing field
拒绝自己的输出。gemini arm 会在 5 秒后失败,产生一个
error
事件和 5712 字节 NDJSON。
任何使用这种模式的 MCP server 都会以相同方式破坏 Google arm。如果 gemini arm 开始因
field predicate failed: $type == Type.ARRAY
而失败,请找到拥有相关命名属性的 server 并将其禁用。

Procedure

流程

Step 1: Resolve Target and Prepare

第 1 步:解析目标并进行准备

bash
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
checks every tool, config file, model whitelist entry, and provider key this skill needs before anything is wiped, and fails loudly by name rather than partway through a run. The
$S
guard catches a partial or corrupted plugin install the same way.
init_run.py
records the reviewed commit, HEAD at this moment, and creates the run directory at
${XDG_CACHE_HOME:-~/.cache}/review-deep/<repo>-<commit12>
, outside the repository. It prints
RUN_DIR
, and Step 5 uses it. It warns when the working tree has uncommitted changes, because every reviewer and judge reads the commit, so uncommitted changes go unreviewed. Commit first when the warning lists source files.
run.json
in the run directory holds the models, the concurrency, the files a judge must not see, and the commit sentence tickets carry. Edit it before Step 5 to change any of them.
export.py
exports the reviewed commit with
git archive
into a new temporary directory, deletes every name in
run.json
judge_exclude
at any depth, such as
CLAUDE.md
,
AGENTS.md
,
.claude/
,
.opencode/
, and
.llmdocs/
, and commits the result as a one-commit repository of its own, so opencode, Serena, and ocr find their project root there. Every arm reviews its own
cp -R
copy of it, and the ocr scan reviews another. The live repository is never read by a model and never modified. Measured on a 1179-file repository, the export took 0.6 s and 12 MB, so 27 concurrent copies hold about 324 MB.
ocr scan --preview
runs on the export and lists every file without calling a model, so the ledger matches exactly what the arms can see.
ledger.txt
holds the reviewable ones.
skipped.txt
holds the rest with ocr's reason, such as
binary
or
unsupported_ext
, and Step 5 reports it as the skipped ledger.
partition.py
cuts the ledger into components of at most 25 files along directory lines, the size Anthropic's Claude Security scanner uses. A reviewer given the whole repo stops after 40 to 50 files by its own choice. Given a component, it reviews every file. A 41-file repo yields 1 component and a 414-file repo yields 22.
opencode_env.py
writes the only opencode config the arms read,
$STATE_DIR/opencode-config/opencode/opencode.json
. It copies
provider
and
enabled_providers
from
~/.config/opencode/opencode.json
and adds Serena as the one MCP server, with this plugin's reviewer context and manual under
opencode/serena/
. It also adds nine primary agents whose prompts are the bodies of the plugin's
agents/reviewer-<area>.md
, the same text review-full and review-diff give their Claude reviewers. Each agent may read, search, use Serena's read tools, and write only under
$STATE_DIR/parts
and
$STATE_DIR/coverage
. No skill, subagent, shell, or web tool.
find -delete
rather than
rm -f
. A
safe-rm
shim on
PATH
moves paths to Trash and exits non-zero on a missing path even under
-f
, which breaks the wipe on a first run.
bash
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
会在清除任何内容之前检查此 skill 所需的每个工具、配置文件、model whitelist 条目和 provider key,并以名称明确报错,而不是在运行中途才失败。
$S
守卫也会以相同方式捕获插件的部分安装或损坏安装。
init_run.py
记录被审查的 commit、此时的 HEAD,并在仓库外创建运行目录
${XDG_CACHE_HOME:-~/.cache}/review-deep/<repo>-<commit12>
。它会打印
RUN_DIR
,第 5 步将使用该值。当工作树存在未提交更改时,它会发出警告,因为每个审查器和 judge 都会读取 commit,所以未提交更改不会被审查。如果警告列出了源文件,请先提交。第 5 步会使用的运行目录中的
run.json
保存 model、并发度、judge 不得看到的文件,以及 commit 附带的句子。可在第 5 步之前编辑它以修改这些设置。
export.py
使用
git archive
将被审查的 commit 导出到新的临时目录,删除
run.json
中
judge_exclude
下列出的所有名称及其任意深度下的内容,例如
CLAUDE.md
、
AGENTS.md
、
.claude/
、
.opencode/
和
.llmdocs/
,然后将结果提交为独立的一 commit repository,使 opencode、Serena 和 ocr 能找到其项目根目录。每个 arm 都会审查该导出的一份
cp -R
副本,ocr scan 则审查另一份副本。实时仓库绝不会由 model 读取,也绝不会被修改。在一个包含 1179 个文件的仓库中实测,导出耗时 0.6 秒、占用 12 MB,因此 27 份并发副本合计约占用 324 MB。
ocr scan --preview
在导出上运行并列出每个文件,但不会调用 model,因此 ledger 会与 arm 可见的文件完全一致。
ledger.txt
保存可审查文件,
skipped.txt
保存其余文件及 ocr 给出的原因,例如
binary
或
unsupported_ext
;第 5 步会将其作为跳过的 ledger 报告。
partition.py
沿目录边界将 ledger 划分为每组最多 25 个文件的组件,这与 Anthropic Claude Security scanner 使用的规模相同。如果给审查器整个仓库,它会自行选择在处理 40 到 50 个文件后停止;如果给定一个组件,它会审查其中每个文件。包含 41 个文件的仓库会生成 1 个组件,包含 414 个文件的仓库会生成 22 个组件。
opencode_env.py
会写入 arm 唯一读取的 opencode 配置
$STATE_DIR/opencode-config/opencode/opencode.json
。它从
~/.config/opencode/opencode.json
复制
provider
和
enabled_providers
,并将 Serena 添加为唯一的 MCP server,同时在此插件的
opencode/serena/
下配置审查器上下文和手册。它还会添加 9 个主 agent,其 prompt 是插件
agents/reviewer-<area>.md
的正文,与 review-full 和 review-diff 交给 Claude 审查器的文本相同。每个 agent 可以读取、搜索、使用 Serena 的读取工具,并且只能在
$STATE_DIR/parts
和
$STATE_DIR/coverage
下写入。不得使用 skill、subagent、shell 或 web 工具。
使用
find -delete
而不是
rm -f
。
PATH
上的
safe-rm
shim 会将路径移至 Trash,并且在路径不存在时即使使用
-f
也会返回非零状态,从而导致首次运行时无法完成清理。

Step 2: Dispatch Component Reviewers and the Coverage Pass

第 2 步:调度组件审查器并执行覆盖率检查

Each of the 27 area and model pairs reviews every component, so a run has 27 arms per component. A worker pool runs them
CONCURRENCY
at a time, beside one ocr scan. Substitute the values Step 1 printed.
Run this block as one background Bash call, as written. The call stays open until the pool and the ocr scan exit, and its completion notification is the signal that Step 2 has finished. Never launch the block with
nohup
, a trailing
&
, or a wrapper script started in the background, because the call then returns at once and no notification ever comes. Never watch a log for the
done,
line. On a quartermaster run the agent detached the block and waited with
tail -f | grep -m1 '^done,'
.
grep
exited on the match, but
tail
exits only when it next writes, and
done,
is the last line the log gets. The call sat idle until its 60-minute
timeout
killed
tail
.
bash
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
is a rolling pool over the flat task list in
tasks.txt
, not a lockstep batch per component: as soon as any of the 27 slots frees, the next task in the list starts, whatever component it belongs to. Arms from c03 commonly start while c01 arms are still running. Do not report a wave count or a component-by-component ETA; measure actual throughput instead, from
parts/*.md
written per minute of elapsed time, and project the remainder from that rate.
CONCURRENCY=27
holds 27
opencode
processes at once, which a 48 GB machine carried without trouble. Each held 370 to 800 MB.
Reviewers run as an appliance.
XDG_CONFIG_HOME
points opencode at the config Step 1 generated, so none of the operator's opencode agents, skills, rules, plugins, or MCP servers load. Credentials still resolve, because opencode keeps
auth.json
under
XDG_DATA_HOME
and providers read keys from environment variables.
OPENCODE_PURE=1
skips external plugins,
OPENCODE_DISABLE_EXTERNAL_SKILLS=1
skips the skill scans under
~/.claude
and
~/.agents
,
OPENCODE_DISABLE_CLAUDE_CODE=1
skips
~/.claude/CLAUDE.md
and
.claude/skills
, and
OPENCODE_DISABLE_PROJECT_CONFIG=1
skips the reviewed repo's
opencode.json
,
.opencode/
, and
AGENTS.md
. Each was measured against opencode 1.18.20 with
opencode debug config
,
opencode debug skill
, and a planted instruction the model was asked to find.
Each arm runs in a copy of the export under
$TMPDIR/review-deep-arm.*
, with
--dir
inside it.
arm.sh
puts the arm's ledger, and for a sweep that area's merged findings, in the copy's
.review-arm/
. The arm writes its findings and coverage record there with
write
, or
apply_patch
on an OpenAI model.
arm.sh
copies them to
parts/
and
coverage/
, strips the copy's path from citations, and deletes the copy on exit. The generated agents deny every tool and then allow
read
,
grep
,
glob
, the write tools, and Serena minus its four write families. One shared
external_directory
rule denies every path outside the arm's own copy, and an
edit
rule allows writes only under
.review-arm/
. Measured on opencode 1.18.20:
grep
given an absolute path outside the copy,
glob
, and
read
were refused, and
grep
inside the copy worked.
Serena runs as its own process, so opencode's rules do not reach it. Measured on Serena 1.7.0: it refused a relative path that leaves the copy and an absolute directory path, but
find_symbol
,
get_symbols_overview
, and
get_diagnostics_for_file
answered for an absolute file path outside the copy. An arm that knows or guesses a file path elsewhere on the machine can still read that file's symbols through Serena. It resolved a
go.work
module inside the copy, across the module boundary. They run as
mode: primary
, because opencode ignores a subagent's
tools
block when the agent is selected with
--agent
.
A reviewer must not call
task
. Two of 27 reviewers on a 1070-file repo spawned a subagent that never returned, and
TaskTool
runs a foreground subagent with a blocking
yield
, so the parent hung and Step 2's
wait
hung with it. The agents set
tools.task: false
and
permission.task: {"*": deny}
. After the fix both reviewers completed with zero
task
calls.
Nothing guards against two runs at once. Step 1 wipes
STATE_DIR
and every arm writes to fixed paths, so a second invocation against the same repo destroys the first. Check for an in-flight run before dispatching.
Provider limits are not the constraint. OpenAI reports 40,000,000 tokens per minute and Azure Foundry 15,000,000.
opencode's own session store can die under write pressure at
CONCURRENCY=27
. Measured on a 1217-file repo: 26 arms failed in one four-second burst with
error="Failed to execute statement"
against opencode's shared SQLite store (visible in
~/.local/share/opencode/log/opencode.log
, not in the arm's own stderr, which stays empty). 25 of the 26 wrote no output at all. Confirmed upstream (anomalyco/opencode#48416, #47566, #38849, #33320, #21215): opencode's SQLite connection runs
busy_timeout=0
, so a writer that loses the race for the write lock fails immediately instead of waiting, and
Effect.orDie
on the SQL adapter turns that into a fatal message failure instead of a warning. The failing write fires from a
child_process
exit handler right after any concurrent process's tool call returns, so it tracks how many opencode processes are alive and busy at that moment, not which ones just launched.
arm.sh
gives each arm its own database instead of racing on the shared one:
OPENCODE_DB="$STATE_DIR/opencode-db/$base.db"
(confirmed by reading opencode's own source:
OPENCODE_DB
is checked directly and, given an absolute path, used as the SQLite file with no further resolution against the shared data dir). This is the actual fix, not a workaround; a run this size no longer touches
~/.local/share/opencode/opencode.db
at all, and the write-contention failure has nothing left to contend over.
auth.json
, the provider credentials, and the downloaded tool cache under
bin/
are untouched, since only the DB path changes.
The retry loop re-runs the same
opencode run
invocation up to twice more in two cases. The first is a
grep
match for
"Failed to execute statement"
in the arm's NDJSON, kept as defense in depth for whatever the per-arm database leaves uncovered. The second is an arm that ends with no findings, meaning
OUTPUT_PATH
is empty and no text event carries a
## 
heading. Measured on a 718-file repo, 20 of 351 gemini arms read files, made no write call, and ended on a
stop
step with 0 output tokens. None of the 331 gemini arms that wrote findings ended that way. Attempts are separated by 5 to 19 seconds of jittered sleep so retries don't restack into the same contention window. A superseded attempt's NDJSON moves to
parts/attempts/raw-$base.attemptN.ndjson
and is never deleted. That directory sits outside the
parts/raw-*.ndjson
glob Step 3 extracts from. An arm with no findings after 3 attempts exits 1 with its name on stderr and surfaces as an ordinary
MISSING
entry in Step 3.
arm.sh
exits 255 when
STATE_DIR
,
TARGET_PATH
,
PROJECT_ROOT
, or
EXPORT
is unset or names a missing directory.
xargs
stops launching arms after an exit of 255, so a batch dispatched from a shell that never exported them fails on its first arms instead of reporting success. Shell variables do not carry between Bash calls, so every call that runs
arm.sh
exports all four. One retry batch ran all 20 of its arms with an empty
STATE_DIR
. Each arm failed on
mkdir: /opencode-db: Read-only file system
, and the batch still exited 0.
27 个 area 与 model 组合都会审查每个组件,因此一次运行中,每个组件都有 27 个 arm。worker pool 每次运行
CONCURRENCY
个 arm,旁边还有一个 ocr scan。请替换为第 1 步打印的值。
请将下面的代码块按原样作为一个后台 Bash 调用运行。该调用会保持打开,直到 pool 和 ocr scan 退出;完成通知就是第 2 步已完成的信号。绝不要使用
nohup
、末尾的
&
,或以后台方式启动包装脚本,否则调用会立即返回,永远不会产生通知。不要监视日志中的
done,
行。在一次 quartermaster 运行中,agent 分离了该代码块并通过
tail -f | grep -m1 '^done,'
等待。
grep
在匹配后退出,但
tail
只有在下一次写入时才会退出,而
done,
是日志最后写入的行。结果调用一直处于空闲状态,直到 60 分钟的
timeout
终止了
tail
。
bash
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
中扁平任务列表的滚动 pool,而不是针对每个组件同步执行的批次:任何一个 27 槽位空闲后,下一个任务就会立即启动,无论其属于哪个组件。c03 的 arm 经常会在 c01 的 arm 仍在运行时启动。不要报告 wave 数量或按组件提供 ETA;应测量实际吞吐量,即根据经过时间计算每分钟写入的
parts/*.md
数量,再据此预测剩余时间。
CONCURRENCY=27
会同时保持 27 个
opencode
进程运行;一台 48 GB 内存的机器可以轻松承载。每个进程占用 370 到 800 MB 内存。
审查器作为隔离的 appliance 运行。
XDG_CONFIG_HOME
指向第 1 步生成的配置,因此不会加载操作者的 opencode agent、skill、rule、plugin 或 MCP server。凭据仍可解析,因为 opencode 将
auth.json
保存在
XDG_DATA_HOME
下,而 provider 会从环境变量读取 key。
OPENCODE_PURE=1
跳过外部 plugin,
OPENCODE_DISABLE_EXTERNAL_SKILLS=1
跳过
~/.claude
和
~/.agents
下的 skill 扫描,
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
以及要求 model 查找的植入指令进行验证。
每个 arm 都在
$TMPDIR/review-deep-arm.*
下运行导出的一份副本,并让
--dir
指向副本内部。
arm.sh
会将 arm 的 ledger 放入副本的
.review-arm/
;对于 sweep,还会放入该 area 合并后的发现项。arm 使用
write
将发现项和覆盖率记录写入该目录,使用 OpenAI model 时则使用
apply_patch
。
arm.sh
将它们复制到
parts/
和
coverage/
,从引用中去除副本路径,并在退出时删除副本。生成的 agent 拒绝所有工具,随后仅允许
read
、
grep
、
glob
、写入工具,以及除四类写入功能外的 Serena 工具。一条共享的
external_directory
rule 拒绝副本以外的所有路径,一条
edit
rule 仅允许在
.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
可以响应副本外绝对文件路径的查询。如果 arm 知道或猜到了机器上其他位置的文件路径,仍可能通过 Serena 读取该文件的 symbol。它能够解析副本内的
go.work
module,并跨越 module 边界。它们以
mode: primary
运行,因为 opencode 在通过
--agent
选择 agent 时会忽略 subagent 的
tools
块。
审查器不得调用
task
。在一个包含 1070 个文件的仓库中,27 个审查器中有两个生成了永远不会返回的 subagent,而
TaskTool
会以前台方式运行 subagent 并执行阻塞式
yield
,导致父进程挂起,第 2 步的
wait
也随之挂起。agent 设置了
tools.task: false
和
permission.task: {"*": deny}
。修复后,两个审查器都正常完成,且
task
调用数为零。
当前没有机制防止同时运行两次。第 1 步会清除
STATE_DIR
,且每个 arm 都写入固定路径,因此针对同一仓库的第二次调用会破坏第一次运行。调度前请检查是否存在正在进行的运行。
provider 限额不是约束。OpenAI 报告每分钟 40,000,000 token,Azure Foundry 为 15,000,000。
在
CONCURRENCY=27
的写入压力下,opencode 自身的 session store 可能会失效。在一个包含 1217 个文件的仓库中实测:26 个 arm 在四秒内集中失败,错误为
error="Failed to execute statement"
,目标是 opencode 的共享 SQLite store;该错误可见于
~/.local/share/opencode/log/opencode.log
,而 arm 自身的 stderr 为空。26 个失败 arm 中有 25 个完全没有写出任何内容。上游已确认此问题(anomalyco/opencode#48416、#47566、#38849、#33320、#21215):opencode 的 SQLite 连接使用
busy_timeout=0
,因此竞争写入锁失败的 writer 会立即报错,而不是等待;SQL adapter 上的
Effect.orDie
又会将此错误转换为致命的 message failure,而不是 warning。失败写入发生在任一并发进程的 tool call 返回后,由
child_process
exit handler 触发,因此它取决于当时存活且繁忙的 opencode 进程数量,而不取决于刚刚启动的是哪些进程。
arm.sh
为每个 arm 分配独立数据库,从而避免竞争共享数据库:
OPENCODE_DB="$STATE_DIR/opencode-db/$base.db"
。通过阅读 opencode 自身的源码已确认:
OPENCODE_DB
会被直接检查;给定绝对路径时,它将作为 SQLite 文件使用,不会再相对于共享 data dir 解析。这是实际修复,而非临时变通;这种规模的运行不再接触
~/.local/share/opencode/opencode.db
,因此不存在可继续竞争的写入对象。由于只更改 DB 路径,
auth.json
、provider 凭据以及
bin/
下已下载的 tool cache 均不受影响。
重试循环会在两种情况下以相同方式重新执行
opencode run
,最多额外重试两次。第一种情况是 arm 的 NDJSON 中匹配到
"Failed to execute statement"
,这作为纵深防御保留,用于覆盖独立 arm 数据库未能解决的情况。第二种情况是 arm 结束时没有发现项,即
OUTPUT_PATH
为空,并且没有 text 事件包含
## 
标题。在一个包含 718 个文件的仓库中实测,351 个 gemini arm 里有 20 个读取了文件,却未发起 write call,最终以
stop
step 结束,输出 token 数为 0。331 个成功写出发现项的 gemini arm 中没有一个出现这种情况。每次重试前会等待 5 到 19 秒的随机抖动间隔,避免重试集中在同一个竞争窗口。被取代的 attempt NDJSON 会移动到
parts/attempts/raw-$base.attemptN.ndjson
,且永不删除。该目录位于第 3 步用于提取内容的
parts/raw-*.ndjson
glob 之外。arm 在 3 次 attempt 后仍没有发现项,将以状态码 1 退出,并在 stderr 中输出其名称,随后在第 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 退出。

Step 2b: Confirm Arms Start

第 2b 步:确认 arm 已启动

Run this as a separate foreground call while Step 2 is still waiting. An arm that never created a session wrote nothing and never will, and it holds a pool slot.
bash
find "$STATE_DIR/parts" -maxdepth 1 -name 'raw-*.ndjson' -size 0 -mmin +1 | sed 's|.*/raw-||; s|\.ndjson$||; s/^/NO SESSION /'
A healthy arm writes NDJSON within seconds of launch. Kill any arm named here. Step 3 re-dispatches it.
Check the ocr scan the same way, using the PID Step 2 saved. It runs as
node
, not
ocr
, so
pgrep -x ocr
or an
ocr scan
name match never finds it; a process check with no result was misreported once as "the ocr scan has exited" when it was still running.
bash
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
A stale log with no growing output for several minutes is not proof of a hang; the tool only logs warnings and failed tool calls, so silence during successful work is normal. Measured on a 1217-file repo: the log went quiet for roughly 40 minutes, then wrote nothing further and exited clean about 80 minutes after launch, with
ocr-scan.json
present and
status=success
(719 files reviewed, 2586 findings). Tens of minutes of silence on a repo that size is the expected shape of a healthy run, not a symptom. Absence from
ps
(
kill -0
fails) together with a missing
ocr-scan.json
is proof it is dead. Re-launch only in that case, with the same command from Step 2, against the same
$STATE_DIR/ocr-scan.json
path.
在第 2 步仍在等待时,将下面命令作为独立的前台调用运行。从未创建 session 的 arm 没有写出任何内容,并且以后也不会写出,同时还会占用一个 pool 槽位。
bash
find "$STATE_DIR/parts" -maxdepth 1 -name 'raw-*.ndjson' -size 0 -mmin +1 | sed 's|.*/raw-||; s|\.ndjson$||; s/^/NO SESSION /'
健康的 arm 会在启动后数秒内写出 NDJSON。终止这里列出的任何 arm,第 3 步会重新调度它们。
使用第 2 步保存的 PID,以相同方式检查 ocr scan。它以
node
而不是
ocr
的形式运行,因此
pgrep -x ocr
或匹配
ocr scan
名称永远找不到它;曾有一次将没有结果的进程检查误报为“ocr scan 已退出”,而它实际上仍在运行。
bash
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
日志陈旧且数分钟内没有增长并不能证明发生挂起;该工具只记录 warning 和失败的 tool call,因此成功工作期间保持安静是正常现象。在一个包含 1217 个文件的仓库中实测:日志安静了大约 40 分钟,随后没有再写入内容,并在启动约 80 分钟后干净退出;此时
ocr-scan.json
存在且
status=success
(审查了 719 个文件,发现 2586 个问题)。对于这种规模的仓库,长时间无输出正是健康运行的预期表现,并不是症状。只有
ps
中不存在该进程(
kill -0
失败)且缺少
ocr-scan.json
才能证明它已经死亡。仅在这种情况下,使用第 2 步中的相同命令、针对相同的
$STATE_DIR/ocr-scan.json
路径重新启动。

Step 3: Collect and Verify

第 3 步:收集并验证

An arm sometimes returns its findings as its response instead of writing OUTPUT_PATH.
gpt-6-sol
does this. The findings are in the NDJSON, so copy them out first. An arm with nothing to copy is left with no part file, so a
MISSING
check and a file count agree. Then merge each area and model's parts into one file.
The merge rebuilds each merged file from scratch out of the component parts and, once Step 4 has run, that area's sweep part. Running it again at any point gives the same result, so a re-dispatch after the sweep keeps the sweep's findings.
bash
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
bash
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
Then check coverage. Each arm's coverage record says which of its files it reviewed. The opened-files count checks the records against what arms actually read.
bash
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
A
SHORT
line is an arm that marked some of its component
skipped
or wrote no coverage record. Read its reasons in the coverage record. A skip because the file belongs to another area is fine. A skip for lack of depth is a gap.
Re-dispatch a
MISSING
arm from a shell that exports the values Step 1 printed, then rerun the extract-and-merge block above.
arm.sh
already retried the arm twice, so re-dispatch it once.
bash
export PROJECT_ROOT=<PROJECT_ROOT> TARGET_PATH=<TARGET_PATH> STATE_DIR=<STATE_DIR> EXPORT=<EXPORT>
bash "$STATE_DIR/arm.sh" <label> <model> <area> <comp>
Treat an area as unreviewed when its merged file is missing, or has neither a finding line nor exactly
No findings.
Never record either as
No findings.
Report every arm still missing after one retry to the operator as not reviewed, naming the model, area, and component.
Compare
cited
against
findings
. A finding whose first backticked field carries no
:line
is unusable, because nobody can confirm it without re-reading the whole file.
cited
counts a finding carrying a
path:line
whether or not it ends with
line unconfirmed
, so subtract
unconfirmed
for the count the reviewer stands behind. Read uncited findings before re-dispatching, and re-dispatch only when they carry no symbol either.
arm 有时会把发现项作为响应返回,而不是写入
OUTPUT_PATH
。
gpt-6-sol
会这样处理。发现项位于 NDJSON 中,因此应先将其提取出来。没有可复制内容的 arm 不会留下 part 文件,因此
MISSING
检查与文件计数会保持一致。然后,将每个 area 和 model 的 part 合并为一个文件。
合并过程会根据组件 part 重新从头构建每个合并文件;第 4 步运行后,还会包含该 area 的 sweep part。任何时候重新运行都会得到相同结果,因此在 sweep 之后重新调度仍会保留 sweep 的发现项。
bash
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
bash
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
随后检查覆盖率。每个 arm 的覆盖率记录会说明它审查了哪些文件。已打开文件计数会将这些记录与 arm 实际读取的内容进行核对。
bash
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
行表示某个 arm 将其组件中的一些文件标记为
skipped
,或者没有写入覆盖率记录。请阅读覆盖率记录中的原因。如果文件属于其他 area,因此跳过是正常的;如果因为审查深度不足而跳过,则属于缺口。
从已导出第 1 步所打印值的 shell 中重新调度
MISSING
arm,然后重新运行上述提取和合并代码块。
arm.sh
已经重试过该 arm 两次,因此这里再重新调度一次。
bash
export PROJECT_ROOT=<PROJECT_ROOT> TARGET_PATH=<TARGET_PATH> STATE_DIR=<STATE_DIR> EXPORT=<EXPORT>
bash "$STATE_DIR/arm.sh" <label> <model> <area> <comp>
如果某个 area 的合并文件缺失,或者既没有发现项行,也没有内容恰好为
No findings.
的行,则应将该 area 视为未审查。绝不能将这两种情况都记录为
No findings.
。在一次重试后,仍缺失的每个 arm 都应作为“未审查”报告给操作者,并注明 model、area 和 component。
比较
cited
与
findings
。如果某个发现项的第一个反引号字段不包含
:line
,则该发现项不可用,因为不重新读取整个文件就无法确认。只计算带有
path:line
的发现项,无论其是否以
line unconfirmed
结尾,因此审查器认可的引用数应减去
unconfirmed
。重新调度前应阅读未引用发现项;仅当它们也不包含任何 symbol 时才重新调度。

Step 4: Sweep

第 4 步:Sweep

One sweep arm per area and model gets the whole ledger and that area's merged findings, and looks only for defects not already listed. It catches what a component boundary hid.
bash
: > "$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"
Run this block as one background Bash call, the same way as Step 2. Its completion notification is the signal to continue.
Then rerun the Step 3 extract-and-merge block. It extracts each sweep's findings and rebuilds that area's merged file with them. Check each sweep afterward.
bash
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"
Re-dispatch a
MISSING
sweep the same way as a
MISSING
arm in Step 3.
Re-export
STATE_DIR
,
TARGET_PATH
,
PROJECT_ROOT
, and
EXPORT
, and reset
AREAS
,
MODELS
, and
CONCURRENCY
, when a block runs as a separate call from Step 2.
After the last sweep and the last re-dispatch, delete the export. Step 5 makes its own checkout for the judges.
bash
rm -rf "$EXPORT"
每个 area 和 model 都有一个 sweep arm,它会获得完整 ledger 和该 area 合并后的发现项,并且只查找尚未列出的缺陷。它用于发现被组件边界隐藏的问题。
bash
: > "$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 步的提取和合并代码块。它会提取每个 sweep 的发现项,并使用这些发现项重新构建该 area 的合并文件。之后检查每个 sweep。
bash
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
arm 的相同方式重新调度
MISSING
sweep。
如果某个代码块作为独立于第 2 步的调用运行,请重新导出
STATE_DIR
、
TARGET_PATH
、
PROJECT_ROOT
和
EXPORT
,并重置
AREAS
、
MODELS
和
CONCURRENCY
。
最后一次 sweep 和最后一次重新调度完成后,删除导出内容。第 5 步会为 judge 创建自己的 checkout。
bash
rm -rf "$EXPORT"

Step 5: Collate, Verify, and Report

第 5 步:整理、验证并报告

Run the scripted stages. Run each model stage as a background Bash call. Its pool returns when every call has finished, and the completion notification is the signal to continue. Never poll a running stage with
sleep
. Run the stage command itself as the background call. Never detach it with
nohup
or a trailing
&
, and never wait on a log line with
tail -f | grep -m1
, for the reasons Step 2 gives.
bash
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"
StageOutputWhat it does
checkout.py
checkout/
Exports the reviewed commit with
git archive
and deletes
judge_exclude
paths, such as
CLAUDE.md
,
.claude/
, and review state. Every model stage reads only this tree.
normalize.py
findings.jsonl
Parses every merged findings file and ocr's comments into findings with stable IDs. Exits 1 on any bullet it cannot parse.
collate.py
issues-by-component.jsonl
Groups each component's findings into issues, where one issue is one code change. Every finding ID must land in exactly one issue or in the dropped list.
windows.py
windows.json
Orders all issues by text similarity and cuts overlapping windows of 150, so one window can see a defect reported in two components.
merge.py
global_merges.json
Merges issues that describe the same defect, where one change fixes all. The same pattern in independent places stays apart.
rank.py
issues.jsonl
,
issues.md
Applies the merges and ranks by severity, source count, and finding count.
verify.py
verify/results.jsonl
,
verify/second.jsonl
Opus at high tries to refute each Critical and High issue and returns a verdict, quoted evidence, the locations and members that do not hold, and its own severity. Fable at high rechecks, blind, every issue the verifier confirms as Critical.
report.py
report.md
Counts, every confirmed issue at the lower of its two ratings, the refuted ones with the reason, and coverage gaps.
Every model call runs as its own
claude -p --bare
process with
--tools Bash,Read
under
--permission-mode dontAsk
, a 5-minute prompt cache, and the prompt on stdin. The pool checks that each process started with exactly those tools, checks every answer's content, and retries a rejected answer up to three times with the reasons appended. A verdict whose evidence quote is not the code at the reviewed commit is rejected. The review-tickets skill documents the measurements behind each of these settings.
The merge rule is narrower than it was. An earlier rule also merged the same kind of defect with the same kind of fix. It built one issue of 77 findings across 24 files, and tickets built from it cited locations that did not support the claim.
Report the counts from
report.md
, the confirmed Critical issues, and the coverage gaps to the operator. The run directory is the input to the review-tickets skill.
运行脚本化阶段。每个 model 阶段都应作为一个后台 Bash 调用运行。其 pool 会在所有调用完成后返回,而完成通知就是可以继续的信号。不要使用
sleep
轮询正在运行的阶段。直接运行阶段命令本身作为后台调用。不要使用
nohup
或末尾的
&
将其分离,也不要使用
tail -f | grep -m1
等待日志行,原因与第 2 步相同。
bash
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.py
checkout/
使用
git archive
导出被审查的 commit,并删除
judge_exclude
路径,例如
CLAUDE.md
、
.claude/
和 review state。每个 model 阶段只读取该目录树。
normalize.py
findings.jsonl
将每个合并发现项文件和 ocr comment 解析为具有稳定 ID 的发现项。遇到任何无法解析的 bullet 时以状态码 1 退出。
collate.py
issues-by-component.jsonl
将每个组件的发现项整理为 issue,其中一个 issue 对应一项代码更改。每个发现项 ID 必须且只能归入一个 issue,或归入 dropped 列表。
windows.py
windows.json
按文本相似度对所有 issue 排序,并切分 150 个重叠项的窗口,使一个窗口能够发现两个组件中分别报告的同一缺陷。
merge.py
global_merges.json
合并描述同一缺陷的 issue,即一次更改可以修复所有这些 issue。独立位置上的相同模式仍会保持分离。
rank.py
issues.jsonl
、
issues.md
应用合并结果,并根据严重程度、来源数量和发现项数量进行排名。
verify.py
verify/results.jsonl
、
verify/second.jsonl
Opus at high 会尝试反驳每个 Critical 和 High issue,并返回 verdict、引用的证据、不成立的位置和成员,以及自身给出的严重程度。Fable at high 会以盲审方式重新检查 verifier 确认为 Critical 的每个 issue。
report.py
report.md
统计数量,以两者评级中较低者列出每个已确认 issue,列出被反驳的 issue 及其原因,并报告覆盖率缺口。
每个 model 调用都作为独立的
claude -p --bare
进程运行,使用
--tools Bash,Read
、
--permission-mode dontAsk
、5 分钟 prompt cache,并将 prompt 放在 stdin 上。pool 会检查每个进程是否恰好以指定工具启动,检查每个答案的内容,并在答案被拒绝时附加原因后最多重试三次。证据引用若不是被审查 commit 中的代码,verdict 会被拒绝。review-tickets skill 记录了这些设置背后的测量结果。
合并规则比之前更严格。早期规则还会合并具有相同缺陷类型和相同修复类型的项;它曾将 24 个文件中的 77 个发现项合并为一个 issue,而据此创建的 ticket 所引用的位置并不支持相应结论。
向操作者报告
report.md
中的计数、已确认的 Critical issue 和覆盖率缺口。运行目录是 review-tickets skill 的输入。

Expected Output Files

预期输出文件

.llmtmp/review-deep/
holds the whole run and is wiped at the start of the next one:
  • <label>-<area>.md
    , 27 merged findings files, component parts plus sweep
  • ocr-scan.json
    ,
    ocr-scan.log
    , and
    ocr.pid
    , the coverage pass
  • ledger.txt
    and
    skipped.txt
    , the reviewable files and the excluded ones with ocr's reason
  • components/cNN.txt
    , the partition
  • coverage/<label>-<area>-<comp>.txt
    , each arm's reviewed or skipped record
  • reviewed.txt
    and
    opened.txt
    , the files arms marked reviewed and the files they opened
  • parts/<label>-<area>-<comp>.md
    ,
    parts/raw-*.ndjson
    ,
    parts/*.log
    , each arm's findings, event stream, and stderr
  • parts/attempts/raw-*.attemptN.ndjson
    , a superseded attempt's event stream, kept when arm.sh retries past a SQLite write-contention failure or an arm that wrote no findings
  • opencode-db/<label>-<area>-<comp>.db
    , each arm's isolated opencode session database
  • opencode-config/
    , the generated opencode config every arm runs under, and its per-area prompt files
  • arms/<label>-<area>-<comp>.root
    , the path of the copy each arm ran in, the one root its tool calls may touch
  • arm.sh
    ,
    tasks.txt
    ,
    sweeps.txt
    , the runner and its work lists
The run directory,
${XDG_CACHE_HOME:-~/.cache}/review-deep/<repo>-<commit12>/
, holds Step 5's output and survives the next review of a different commit:
  • run.json
    , the repository, commit, models, concurrency, judge exclusions, and commit sentence
  • checkout/
    , the export every judge reads, and
    checkout_excluded.json
  • findings.jsonl
    ,
    issues-by-component.jsonl
    ,
    dropped.jsonl
    ,
    unassigned.jsonl
    ,
    windows.json
    ,
    global_merges.json
  • issues.jsonl
    and
    issues.md
    , every issue with its member findings inlined
  • verify/results.jsonl
    and
    verify/second.jsonl
    , the verdicts
  • report.md
  • collate/
    ,
    merge/
    ,
    verify/first/
    ,
    verify/second/
    , each with
    pool.log
    and every model process's event stream under
    calls/
.llmtmp/review-deep/
保存整个运行内容,并会在下一次运行开始时被清除:
  • <label>-<area>.md
    :27 个合并发现项文件,包含组件 part 和 sweep
  • ocr-scan.json
    、
    ocr-scan.log
    和
    ocr.pid
    :覆盖率检查
  • ledger.txt
    和
    skipped.txt
    :可审查文件,以及被排除文件和 ocr 给出的原因
  • components/cNN.txt
    :分区结果
  • coverage/<label>-<area>-<comp>.txt
    :每个 arm 的已审查或已跳过记录
  • reviewed.txt
    和
    opened.txt
    :arm 标记为已审查的文件及其打开过的文件
  • parts/<label>-<area>-<comp>.md
    、
    parts/raw-*.ndjson
    、
    parts/*.log
    :每个 arm 的发现项、事件流和 stderr
  • parts/attempts/raw-*.attemptN.ndjson
    :被取代 attempt 的事件流;当 arm.sh 在 SQLite 写入竞争失败或 arm 没有写出发现项后进行重试时予以保留
  • opencode-db/<label>-<area>-<comp>.db
    :每个 arm 独立的 opencode session database
  • opencode-config/
    :每个 arm 运行时所使用的 opencode 配置及其按 area 划分的 prompt 文件
  • arms/<label>-<area>-<comp>.root
    :每个 arm 运行所在的副本路径,也是其 tool call 唯一允许访问的根目录
  • arm.sh
    、
    tasks.txt
    、
    sweeps.txt
    :运行器及其工作列表
运行目录
${XDG_CACHE_HOME:-~/.cache}/review-deep/<repo>-<commit12>/
保存第 5 步的输出,并在下一次审查不同 commit 后继续存在:
  • run.json
    :仓库、commit、model、并发度、judge exclusion 和 commit 句子
  • checkout/
    :每个 judge 读取的导出,以及
    checkout_excluded.json
  • findings.jsonl
    、
    issues-by-component.jsonl
    、
    dropped.jsonl
    、
    unassigned.jsonl
    、
    windows.json
    、
    global_merges.json
  • issues.jsonl
    和
    issues.md
    :每个 issue 及其内联的成员发现项
  • verify/results.jsonl
    和
    verify/second.jsonl
    :verdict
  • report.md
  • collate/
    、
    merge/
    、
    verify/first/
    、
    verify/second/
    :各包含
    pool.log
    ,并在
    calls/
    下保存每个 model 进程的事件流

NDJSON Reference

NDJSON 参考

One JSON object per line. Skip lines that fail to parse.
TypeMeaningKey fields
step_start
An LLM turn begins
text
Model emitted text
part.text
tool_use
Model called a tool
part.tool
,
part.state.status
,
part.state.input
step_finish
A turn completed
part.reason
,
part.cost
,
part.tokens
error
Session-level error
error.data.message
A
step_finish
reason of
stop
means done and
tool-calls
means continuing.
bash
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 carried 119 of 181 tool calls on a TypeScript repo and 2 of 28 on a repo of HTML, CSS, and a Dockerfile. Symbolic navigation is worth little where no language server backs the files.
每行一个 JSON object。跳过无法解析的行。
类型含义关键字段
step_start
LLM turn 开始
text
model 输出文本
part.text
tool_use
model 调用 tool
part.tool
、
part.state.status
、
part.state.input
step_finish
turn 完成
part.reason
、
part.cost
、
part.tokens
error
session 级错误
error.data.message
step_finish
的 reason 为
stop
表示已完成,为
tool-calls
表示将继续。
bash
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"
在一个 TypeScript 仓库中,Serena 承担了 181 次 tool call 中的 119 次;在一个包含 HTML、CSS 和 Dockerfile 的仓库中,则只承担了 28 次中的 2 次。如果没有语言服务器支持相关文件,symbolic navigation 的价值就很有限。

Tests

测试

Tests and evals for review-deep and review-tickets, plus the guard tests and preflight tests that cover all four skills, live in
evals/
at the root of this plugin's repository, outside the installed skill.
CheckCommand, from
evals/
Cost
Unit tests, one directory per skill, plus preflight and guard
uv run --with pytest pytest -q unit/review_deep
,
unit/review_tickets
,
unit/preflight
, and
unit/guard
none
End-to-end replay on the harbor fixture, all three kinds
uv run --with pytest pytest -q test_end_to_end.py
none
Live quality eval against
fixture/truth.json
python3 live_eval.py WORK_DIR --run
about $3 at list price
unit/review_deep
imports
windows.py
, which imports scikit-learn, scipy, and numpy, so run it as
uv run --with pytest --with numpy --with scipy --with scikit-learn pytest -q unit/review_deep
. The other directories need no extra packages.
Run the unit tests and the replay after any change to
scripts/
. The replay stops with "prompt changed" when a prompt changes. Record again with
python3 pipeline.py WORK_DIR --mode record
, check the run with
live_eval.py
, and save it with
python3 golden.py save RUN_DIR
. Run the live eval on demand after changing a model, an effort level, or a prompt.
review-deep 和 review-tickets 的测试与 eval,以及覆盖全部四个 skill 的 guard test 和 preflight test,位于此插件仓库根目录下的
evals/
,不在已安装的 skill 中。
检查项命令,从
evals/
执行
成本
单元测试,每个 skill 一个目录,外加 preflight 和 guard
uv run --with pytest pytest -q unit/review_deep
、
unit/review_tickets
、
unit/preflight
和
unit/guard
无
对 harbor fixture 进行端到端 replay,覆盖全部三种类型
uv run --with pytest pytest -q test_end_to_end.py
无
基于
fixture/truth.json
的实时质量 eval
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
运行。其他目录不需要额外 package。
对
scripts/
做任何更改后,都应运行单元测试和 replay。Replay 会在 prompt 发生变化时以“prompt changed”停止。重新录制:
python3 pipeline.py WORK_DIR --mode record
;使用
live_eval.py
检查运行;然后保存:
python3 golden.py save RUN_DIR
。更改 model、effort level 或 prompt 后,可按需运行实时 eval。

Rules

规则

  • The invoking agent is a launcher. It performs no review analysis of its own. The Step 5 scripts and their model calls do all of it.
  • NEVER let a Step 5 model call read the live working tree. They run in the run directory's
    checkout/
    .
  • NEVER poll a background stage with
    sleep
    , a wait loop, or a log watcher such as
    tail -f | grep -m1
    . Wait for its completion notification.
  • Keep prompts in
    prompts/
    as
    .txt
    . A markdown formatter rewrote a
    .md
    prompt and changed its meaning.
  • ALWAYS redirect stdin from
    /dev/null
    on
    opencode run
    . Omitting it hangs the process before session creation with no output and no error, which is anomalyco/opencode issue #38723.
  • Impose no token cap and no turn cap on a reviewer. Let it finish.
  • Run Step 2b a minute or so after dispatch, and again during long runs. An arm with an empty NDJSON is dead and holds a pool slot, and wall time cannot tell that apart from a slow review.
  • Run the Step 2 and Step 4 dispatch blocks as background Bash calls, never detached with
    nohup
    or
    &
    . The pool blocks until every arm exits, and backgrounding keeps that out of the main session. The call's completion notification is the only signal that the pool has finished.
  • ALWAYS export
    STATE_DIR
    ,
    TARGET_PATH
    , and
    PROJECT_ROOT
    in every Bash call that runs
    arm.sh
    , including a single re-dispatch. Shell variables do not carry between calls, and
    arm.sh
    exits 255 without them.
  • Launch every arm through the pool and the ocr scan in one bash block. They are independent processes.
  • Use plain message invocation, not
    --command
    . The
    --command
    flag has a known issue with the context7 MCP server.
  • Do NOT clean up per-area files, NDJSON, or logs during a run. Step 1 wipes them at the start of the next one.
  • If an arm fails, still wait for and report the others.
  • The opencode arms need only the operator's providers in
    ~/.config/opencode/opencode.json
    and a Serena home at
    ~/.serena-reviewer
    . Their agents, Serena context, and Serena manual come from this plugin through
    opencode_env.py
    .
    preflight.py --tier deep
    checks the providers and the Serena home by name before Step 1 wipes anything.
  • 调用此 skill 的 agent 是 launcher。它自身不执行任何审查分析。第 5 步脚本及其 model 调用完成全部分析。
  • 绝不能让第 5 步的 model 调用读取实时工作树。它们必须读取运行目录中的
    checkout/
    。
  • 绝不要使用
    sleep
    、等待循环或
    tail -f | grep -m1
    等日志监视器轮询后台阶段。等待其完成通知。
  • 将 prompt 保留在
    prompts/
    中并使用
    .txt
    扩展名。Markdown formatter 曾重写
    .md
    prompt 并改变其含义。
  • 在
    opencode run
    上始终将 stdin 从
    /dev/null
    重定向。省略此操作会使进程在创建 session 前挂起,且不产生输出和错误;这是 anomalyco/opencode issue #38723。
  • 不要对审查器施加 token 上限或轮次上限。让它自行完成。
  • 调度后约一分钟运行一次第 2b 步,长时间运行期间也再次运行。NDJSON 为空的 arm 已死亡并会占用 pool 槽位,而 wall time 无法区分它与运行缓慢的审查。
  • 将第 2 步和第 4 步的调度代码块作为后台 Bash 调用运行,绝不要使用
    nohup
    或
    &
    分离。pool 会阻塞至所有 arm 退出,而使用后台运行可避免其阻塞主 session。调用的完成通知是 pool 已完成的唯一信号。
  • 在每个运行
    arm.sh
    的 Bash 调用中,始终导出
    STATE_DIR
    、
    TARGET_PATH
    和
    PROJECT_ROOT
    ,包括单次重新调度。Shell 变量不会跨调用继承,缺少这些变量时
    arm.sh
    会以状态码 255 退出。
  • 在同一个 bash 代码块中启动每个 arm 和 ocr scan。它们是相互独立的进程。
  • 使用普通 message 调用,而不是
    --command
    。
    --command
    flag 存在与 context7 MCP server 相关的已知问题。
  • 运行期间不要清理按 area 划分的文件、NDJSON 或日志。第 1 步会在下一次运行时将其清除。
  • 如果某个 arm 失败,仍须等待并报告其他 arm。
  • opencode arm 只需要
    ~/.config/opencode/opencode.json
    中的操作者 provider,以及位于
    ~/.serena-reviewer
    的 Serena home。其 agent、Serena 上下文和 Serena 手册由插件通过
    opencode_env.py
    提供。
    preflight.py --tier deep
    会在第 1 步清除任何内容之前,按名称检查 provider 和 Serena home。