关于 self-review
用于当前分支自审的技能,会运行 Codex 审查、逐项修复并测试,再重复检查直至结果收敛或触发停止条件。另有仅审查模式,只报告发现而不修改代码。
主要功能
- 01在独立进程中运行 Codex 审查
- 02将每项修复单独提交
- 03修复后运行测试
- 04持续审查直到触发停止条件
- 05提供仅审查模式
适用场景
- 01推送前审查已提交的功能分支
- 02在创建 PR 前迭代代码变更
- 03查看发现但不自动修复
- 04在人为审查前发现问题
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ kirkchen/ cadence --skill self-review安装后在 Claude Code 中输入/self-review确认已加载。
详细介绍
自我审查
在当前分支上自动循环执行开发阶段的自我审查。运行 codex(OpenAI GPT),按照 pr-review 的多角色方法进行跨模型审查,根据每条发现应用修复,再次审查,并循环执行,直到发现收敛或触发停止条件。
可选择启用单次模式(review-only)——仅生成发现,不自动修复。
循环语义:
- 每条发现单独原子提交——每项修复都有自己的约定式提交,便于回滚
- 停止条件不可妥协(见下文“停止条件”)——触发任一条件时,循环即停止,剩余工作全部交由用户处理
- 用户可随时按 ctrl-c 中断;主会话会捕获中断并报告部分状态
防止上下文混杂:
- Codex 每轮都在全新进程中运行——与主会话不共享对话上下文
- Codex 输出(冗长的发现详情)写入临时文件,不写入聊天;每轮只有结构化 JSON 摘要会进入主会话记忆
- 主会话实施修复时,必须逐字、机械地执行 codex 发现中的
Mitigation:字段——不得额外推理修复是否正确,不得替换成“更好的方案”,不得扩大范围 - 如果仅凭
Mitigation:字段无法修复某条发现(因为内容含糊、需要设计判断,或涉及该发现范围之外的代码)→ 本轮跳过该发现,并在最后告知用户
禁止判断审查结论:
- 此技能不会做出不予修复的决定。发现只能被修复(自动)或提交给用户处理(升级)
- 不得判断“这条发现无关紧要”→ 除非触发上述“无法仅凭 Mitigation 修复”的门槛,否则必须修复所有发现
- 不得根据主会话记忆填写 Wontfix Template 字段(见
pr-babysit§ 4.6)
质量门槛:
- 每轮修复后都要运行测试——
pnpm test(或检测到的仓库专用命令)。失败 → 停止并升级处理 - 这是防止“修复导致问题”的常规安全网
修复步骤中的作者偏见:
- Codex(跨模型)生成发现 → 在生成发现的阶段隔离偏见
- 主会话编写修复 → 未隔离偏见,但会逐字遵循 codex 的 Mitigation → 偏见攻击面是“机械执行准确性”,而非“审查结论判断”
- 如果发现自己在想“我认为 codex 的判断有误”→ 这属于结论判断,本技能不允许这样做。最后告知用户。
模式
loop(默认)——codex 审查 → 逐条修复并提交 → 测试 → 再次审查 → 循环执行直至停止review-only——codex 单次审查,展示发现后停止。不修复、不提交、不运行测试。适用于希望手动审查且不自动修改的情况。
模式选择:
- 用户说“自我审查”或“审查我的分支”→
loop(默认) - 用户说“只展示发现”或“自我审查但不修复”→
review-only
适用场景
- 刚在功能分支上编写完代码,工作区干净且已提交,希望推送前自动进行跨模型审查并修复问题
- 正在迭代开发某项功能,希望快速收敛到干净状态
- PR 前的合理性检查——在提交给人工审查前,发现 codex 能识别的明显问题
不适用场景
- 实时 PR 审查,需要固定评论和行内讨论串 →
/cadence:pr-review(默认模式) - PR 打开后的跟进工作 →
/cadence:pr-babysit(处理讨论串回复、去重和 CI 门槛) - 工作区有未提交更改——停止并要求用户先提交或暂存。循环假设开始时工作区干净,以确保每条发现的提交都是原子的
- 希望手动控制审查结论 → 使用
mode=review-only并自行决定 - 首次在新分支上使用 → 建议先考虑使用
mode=review-only,了解 codex 会生成哪些内容,再允许其自动修复
设置检查
开始时运行以下检查。任何检查失败都要按照列出的消息停止——不得尝试自动安装。
# Codex CLI
codex --version 2>/dev/null || { echo "STOP: Install codex — npm install -g @openai/codex"; exit 1; }
codex login status >/dev/null 2>&1 || { echo "STOP: Run 'codex login' to authenticate"; exit 1; }
# `codex login status` is a subcommand, not a flag. `--status` is rejected as an unknown
# argument by codex-cli 0.150.x, which makes this check fail for an authenticated user.
# Plugin install root — needed so codex can locate the pr-review methodology prompts
[ -n "${CLAUDE_PLUGIN_ROOT:-}" ] && [ -d "${CLAUDE_PLUGIN_ROOT}/skills/pr-review" ] || {
echo "STOP: CLAUDE_PLUGIN_ROOT must point at cadence's install root (so codex can read ./skills/pr-review/*-prompt.md). Set it explicitly if your harness doesn't export it (e.g. export CLAUDE_PLUGIN_ROOT=~/.claude/plugins/cache/cadence/cadence/<version>)."; exit 1;
}
# Clean working tree
git diff --quiet && git diff --cached --quiet || { echo "STOP: Working tree has uncommitted changes. Commit or stash, then re-invoke."; exit 1; }
# Detect base branch
BASE=$(git symbolic-ref refs/remotes/origin/HEAD 2>/dev/null | sed 's|refs/remotes/origin/||')
[ -z "$BASE" ] && BASE=main
echo "BASE: origin/$BASE"
# Detect test command (best-effort — repo-specific)
if [ -f package.json ] && jq -e '.scripts.test' package.json >/dev/null 2>&1; then
TEST_CMD="pnpm test"
elif [ -f Makefile ] && grep -q '^test:' Makefile; then
TEST_CMD="make test"
else
TEST_CMD=""
echo "WARN: No test command detected — quality gate disabled"
fi
echo "TEST_CMD: ${TEST_CMD:-<none>}"
循环算法(mode=loop)
在主会话内存中维护:
-
ITER— 当前迭代编号(从 1 开始) -
MAX_ITERS = 3— 硬性上限。根据经验,codex 的高价值发现(真正可触发的 bug,即同一模型的审查者会漏掉的那种)出现在第 1–3 轮;第 4–5 轮的发现逐渐变成卫生级的小问题,偶尔还会出现误报,需要控制器花费判断力予以驳回。真正的停止条件是下文的 SC0(严重性门槛),MAX_ITERS是简单粗暴的兜底限制。 -
FINDING_HISTORY— 每轮记录该轮发现的{file, failure_mode, slug}列表。判定依据是file+ 故障模式,而不是file:line:slug。行号和 slug 都会因与发现是否相同无关的原因而变化。每次修复都会让其下方的行号发生偏移,而 codex 在不同轮次间可以随意重命名。在一个以散文为主的代码库上进行的真实 4 轮测试中,精确指纹检查(SC3)从未触发,尽管同一发现连续 3 轮出现,且每轮的 slug 和行号都不同;而零重叠检查(SC4b)误触发了两次——由于上一轮的发现已修复,且所有剩余行号都已移动,重叠度按设计接近于零。该轮中所有依赖指纹的信号都出错了,而且正反两个方向都有。
你是模型,不是
grep:要比较故障模式,判断它们描述的是否是同一个缺陷。同一文件中的author-self-accepts-security-exceptions和spoofable-accepted-by-identity是同一个反复出现的发现,不是两个。
第 1 步:运行 codex 审查
提示词的构造方式与 mode=review-only 的第 2 步相同(见下文的 Codex 提示词部分)。输出写入 $WORK_DIR/iter-$ITER.md。不要将冗长的 codex 输出内联到主会话中——只解析 JSON 摘要块。
Codex 提示词必须以摘要 JSON 块结尾(如下文的 Codex 提示词部分所述),这样主会话无需解析散文即可驱动循环。
WORK_DIR=${WORK_DIR:-$(mktemp -d "${TMPDIR:-/tmp}/self-review-XXXXXX")}
PROMPT_FILE="$WORK_DIR/prompt-iter-$ITER.md"
OUTPUT_FILE="$WORK_DIR/iter-$ITER.md"
JSON_FILE="$WORK_DIR/iter-$ITER.json"
# Write prompt (see Codex Prompt section)
write_codex_prompt > "$PROMPT_FILE"
# Run codex (5-min timeout)
_REPO_ROOT=$(git rev-parse --show-toplevel)
codex exec "$(cat "$PROMPT_FILE")" \
-C "$_REPO_ROOT" \
-s read-only \
-c 'model_reasoning_effort="high"' \
--enable web_search_cached \
> "$OUTPUT_FILE" 2>/tmp/self-review-err
# Extract JSON summary block between unique sentinels
# awk, not sed: the multi-command `/range/{...}` form is a GNU extension. BSD sed (macOS)
# rejects it with `extra characters at the end of p command`, writes an empty $JSON_FILE,
# and the guard below then blames codex for output codex actually produced.
awk '//{f=1;next} //{f=0} f' \
"$OUTPUT_FILE" > "$JSON_FILE"
# Guard: codex must emit a parseable JSON block — empty or malformed is NOT zero findings
if [ ! -s "$JSON_FILE" ]; then
# Distinguish "codex emitted nothing" from "extraction failed" — they need different fixes,
# and the second one masquerading as the first sends the user to debug the wrong process.
if grep -q 'SELF-REVIEW-JSON-START' "$OUTPUT_FILE"; then
echo "STOP: the JSON block IS present in $OUTPUT_FILE but extraction produced nothing — this is a bug in this skill's extraction step, not in codex."
else
echo "STOP: codex did not emit a JSON summary block. Output saved to $OUTPUT_FILE for manual review."
fi
break
fi
if ! jq -e '.findings | type == "array"' "$JSON_FILE" >/dev/null 2>&1; then
echo "STOP: codex JSON summary malformed. Output: $OUTPUT_FILE, JSON: $JSON_FILE"
break
fi
# Sanity: confirm line_source field is "source" on every finding (catches diff-line confusion)
BAD_LINE_SOURCE=$(jq -r '[.findings[] | select(.line_source != "source")] | length' "$JSON_FILE")
if [ "$BAD_LINE_SOURCE" != "0" ]; then
echo "STOP: codex emitted findings with line_source != 'source' (likely diff-line numbers, not source-file lines). Review $OUTPUT_FILE manually."
break
fi
第 2 步:收敛检查
从 $JSON_FILE 读取发现。按顺序应用停止条件——匹配到第一个条件即停止:
FINDINGS_COUNT=$(jq '.findings | length' "$JSON_FILE")
SC1 — 成功:FINDINGS_COUNT == 0 → 停止,报告成功。
SC0 — 严重性门槛(收敛到足够程度):如果某轮的发现全部属于卫生级,则停止并报告已收敛。“卫生级” = 不存在任何发现同时满足(severity 属于 Blocker|Factual)且(justification 属于 Reachable|Asymmetric)。也就是说,不再有真正可触发的 bug——只剩 Suggestion/Question,或基于 Precedent/Historical 推测的 Factual 发现。
REAL_BUGS=$(jq -r '[.findings[]
| select((.severity == "Blocker" or .severity == "Factual")
and (.justification == "Reachable" or .justification == "Asymmetric"))]
| length' "$JSON_FILE")
REAL_BUGS == 0(且 FINDINGS_COUNT > 0)→ 停止。将剩余的卫生级发现呈现给用户,不要自动修复——它们达不到再进行一轮 codex 检查的门槛。这是健康运行中预期的停止状态,通常出现在第 2–3 轮。MAX_ITERS 只用于兜底:当 codex 直到那么多轮之后仍不断发现真正的 bug 时才会触发(这本身就表明改动范围过大,应拆分处理)。
为什么 SC0 在 SC2 之前、但在 SC1 之后检查:零发现明确表示成功;只有卫生级发现的迭代表示已收敛到足够程度;
MAX_ITERS上限则是不理想情况下的兜底。将其命名为 SC0,可使它在视觉上与 SC1 相邻(两者都属于成功类),而无需重新编号 SC2–SC6。
SC2 — 达到上限:ITER > MAX_ITERS → 停止,升级处理当前发现。不要再应用修复。
SC3 — 同一发现出现 3 次(竞态中的竞态信号):
对于本轮的每项发现,回看 FINDING_HISTORY,统计其中包含同一文件中的同一缺陷(故障模式相同,不论 slug 和行号如何)的迭代次数。达到 3 次或以上 → 停止,升级处理。
# Mechanical pre-filter only. It narrows the candidates; it does not decide.
jq -r '.findings[] | "\(.file)\t\(.slug)\t\(.failure_mode)"' "$JSON_FILE"
然后针对每项候选结果进行判断:同一缺陷是否已在之前两轮中报告过? 根据故障模式文本判断,而不是比较字符串是否相同。如果某项发现涉及的文件出现在历史记录中,且故障模式措辞不同但含义等同,也算。这项检查能捕捉到一种发现:你的修复总是无法彻底解决它——每一轮审查者都会重新描述同一个漏洞,因为你的补丁只是把它挪了位置,而没有将其堵上。
参见 pr-babysit § 4.5 Gate B,了解等效的收敛失败模式。
SC3.5 — 热点文件(警告,不停止):跟踪每轮产生发现的文件。如果某个文件连续 3 轮或以上都有发现,在报告中注明并继续。
jq -r '.findings[].file' "$JSON_FILE" | sort -u > "$WORK_DIR/iter-$ITER.files"
if [ "$ITER" -ge 3 ]; then
HOT=$(grep -Fxf "$WORK_DIR/iter-$((ITER-1)).files" "$WORK_DIR/iter-$ITER.files" \
| grep -Fxf "$WORK_DIR/iter-$((ITER-2)).files")
fi
这以前是一项停止条件,用来弥补 SC3 无法识别 slug 漂移的问题。现在 SC3 会根据故障模式进行匹配,因此这项任务已完成——而作为一项停止条件,这个信号实际上有害:在产品就是一个大型文件的仓库中,每项发现都必然位于该文件中,所以每次运行满 3 轮就会触发。本次改动所基于的那次运行正是如此,它停止了一个还有实际工作待完成的循环。
值得保留并报告这个信号,但不应据此采取行动:如果某个文件轮轮都有发现,说明其中的改动可能太大,不适合增量审查。说明这一点;由人来决定。
SC4 — 发现结果发散:任一以下信号触发即生效(检测竞态中的竞态,参见 pr-babysit § 4.5 Gate B):
(a) 数量增加:FINDINGS_COUNT 严格大于上一轮的数量 → 停止并升级处理。修复步骤引入新问题的速度超过了解决问题的速度。
(b) 集合替换——本轮发现与上一轮的发现没有任何重叠,并且新发现出现在上一轮修复批次触及的代码中(prior_fix_range)。两项条件必须同时满足。
没有任何重叠本身是健康迭代的常见情况:你修复了上一轮提出的所有问题,因此没有任何问题再次出现。仅凭这一点触发,就会把成功报告成失败——在一次 4 轮运行中测得两次,而且都是误报。发散的区别在于新发现位于何处:如果它们落在上次修复所改动的代码行内,说明这些修复是在扩大问题表面,而非将其收敛。
# Which files did the previous fix batch touch?
git diff --name-only $prior_fix_range > "$WORK_DIR/prior-fix-files.txt"
# How many of this iteration's findings are in those files?
IN_FIX=$(jq -r '.findings[].file' "$JSON_FILE" | grep -Fxf "$WORK_DIR/prior-fix-files.txt" | wc -l | tr -d ' ')
TOTAL=$(jq '.findings | length' "$JSON_FILE")
触发条件:TOTAL >= 3,且与上一轮的语义重叠为零,且IN_FIX 至少占 TOTAL 的一半。否则循环继续——这些发现是新的工作,而非自行引发的反复折腾。
请注意,这与 pr-review 中的丢弃信号 (B) 不对称,后者会丢弃自引入表面上的发现。这里同一观察结果是一个停止信号,而非过滤条件:允许自我审查修复自己造成的问题,但不能无休止地这样做,也不能不告知你。
第 3 步:逐项修复发现的问题
如果没有触发停止条件,就遍历所有发现的问题,并将每项修复作为一个独立提交:
jq -c '.findings[]' "$JSON_FILE" | while read -r FINDING; do
ID=$(echo "$FINDING" | jq -r '.id')
PERSONA=$(echo "$FINDING" | jq -r '.persona')
CATEGORY=$(echo "$FINDING" | jq -r '.category')
SLUG=$(echo "$FINDING" | jq -r '.slug')
FILE=$(echo "$FINDING" | jq -r '.file')
LINE=$(echo "$FINDING" | jq -r '.line')
FAILURE_MODE=$(echo "$FINDING" | jq -r '.failure_mode')
MITIGATION=$(echo "$FINDING" | jq -r '.mitigation')
# Main session reads the full finding from OUTPUT_FILE for context
# then implements MITIGATION literally
apply_minimal_fix_from_mitigation "$FILE" "$LINE" "$MITIGATION"
# Verify file changed
if git diff --quiet "$FILE"; then
SKIPPED_FINDINGS+=("$ID: no diff produced — mitigation may need design judgment")
continue
fi
# Atomic commit. The body goes through a FILE, never `-m "...$MITIGATION..."`:
# $FAILURE_MODE / $MITIGATION / $SLUG are codex's output, and inside a double-quoted
# shell string both `backticks` and $(...) are command substitution. Best case a cited
# `file.py:42` silently becomes empty; worst case model output executes.
MSG_FILE="$WORK_DIR/commit-$ID.txt"
{
printf 'fix(%s): %s — self-review iter %s #%s\n\n' "$PERSONA" "$SLUG" "$ITER" "$ID"
printf 'Failure mode: %s\n' "$FAILURE_MODE"
printf 'Mitigation: %s\n\n' "$MITIGATION"
printf 'Source: codex review following pr-review methodology\nCategory: %s\n' "$CATEGORY"
} > "$MSG_FILE"
git add "$FILE"
git commit -F "$MSG_FILE"
done
逐项修复规则(强化 HARD-GATE):
- 阅读该问题对应的 codex
Mitigation:字段,并以最小改动实现。 - 不要扩大范围(不要重构相邻代码、不要添加未要求的测试、不要添加注释)。
- 不要加入 Claude 的推理(“我还发现了 X,也顺手修好了”——不行)。
- 如果
Mitigation:含糊不清,或需要设计判断才能实现 → 跳过,将其添加到SKIPPED_FINDINGS,并在最后告知用户。 - 一个问题对应一个提交。如果单项缓解措施确实涉及 3 个文件,可以用一个提交涵盖这 3 个文件。但两个不同的问题 = 两个提交。
模式泛化(必须执行——不属于扩大范围):
如果某项发现的 Failure mode 描述的是一类错误,而不是单个位置的缺陷(例如“Slack 帖子发送失败会导致该行无法进入终态”、“未经验证的输入传入 X”或“对 Y 形态的调用缺少 await”),那么只有修复同一模式下的所有并列位置,才算完成:
- 提交前,在代码库中搜索相同模式(搜索故障形态,而非逐字匹配代码行)。在该问题对应的同一轮迭代中修复所有匹配位置。
- 这明确不属于上文所述的“扩大范围”违规。扩大范围是修复不相关的问题。修复同一已标记模式在其他位置的问题,才算完成该项发现——把其他位置留到下一轮 codex 检查,会浪费一次迭代,而且在此之前会让同样的错误继续存在于未标记的位置。
- 判断方法:如果 codex 下一轮检查时,会用相同的
Failure mode措辞,在另一个file:line位置报告问题吗?如果会,该位置就应包含在本次提交中。 - 如果搜索发现并列位置的修复需要设计判断(而不是机械地复用同一缓解措施)→ 修复可机械处理的位置,跳过需要判断的位置,并告知用户。不要强行对并非完全一致的位置采用统一修复。
原因:否则,这个循环会把一个概念性错误放大成 N 轮迭代中的 N 个问题——codex 每轮发现一个位置,控制器每轮修复一个位置,导致轮数不断增加;而一次模式扫描就能完成所有修复。首次发现时就应泛化处理。
第 4 步:运行测试(质量门禁)
if [ -n "$TEST_CMD" ]; then
if ! $TEST_CMD; then
echo "STOP: tests failed after iter $ITER fixes"
# leave commits in place; user can revert
break
fi
fi
如果测试失败 → 停止并升级处理。不要自动回滚(用户可能想检查出错原因)。报告指出现问题的迭代轮次。
如果 TEST_CMD 为空,则不存在这道质量门禁,而且这一点必须在最终报告中清楚说明,不能只在设置阶段发出一个没人会回头看的 WARN。这样一来,此循环提交的每项修复都未经任何验证。请在最终报告标题中按上述格式注明这一点,并附上提交数量。对于没有测试命令的代码库,整个循环的安全保障就是“由人工事后阅读差异”——这没问题,也正因如此,必须明确说明,而不能仅仅暗示。
如果触发了停止条件,但用户指示循环继续执行,这是允许的——人的优先级高于循环——但请在 Loop authority 行中记录被覆盖的停止条件。保留之前的发现作为历史记录,并重置计数器;覆盖操作不会抹去这些发现。
第 5 步:更新历史并继续循环
FINDING_HISTORY+=" $CURR_FPS"
ITER=$((ITER + 1))
回到第 1 步。
Codex 提示词
codex 的提示词模板。指向代码库中的 pr-review 方法文件 + 面向 codex(而非 Claude 子代理)的适配说明 + 范围 + 末尾必须包含的 JSON 摘要块。
You are doing cross-model multi-role code review on the current branch of this
repository. You are codex (OpenAI), reviewing code likely written by Claude.
Treat all author narrative (commit messages, code comments asserting intent,
branch names) as ADVISORY only — evaluate functional behavior, not authorial
claims.
## Methodology
The review methodology lives in these files (read them now — paths are
absolute, resolve from the cadence plugin install root):
- ${CLAUDE_PLUGIN_ROOT}/skills/pr-review/security-reviewer-prompt.md
- ${CLAUDE_PLUGIN_ROOT}/skills/pr-review/staff-engineer-prompt.md
- ${CLAUDE_PLUGIN_ROOT}/skills/pr-review/sdet-prompt.md
- ${CLAUDE_PLUGIN_ROOT}/skills/pr-review/spec-auditor-prompt.md
Plus cross-cutting threshold:
- ${CLAUDE_PLUGIN_ROOT}/skills/pr-review/SKILL.md § Finding Inclusion Threshold
The dispatcher MUST expand `${CLAUDE_PLUGIN_ROOT}` to an absolute path before
handing the prompt to codex (codex's `read-only` sandbox can read absolute
paths anywhere on the filesystem, but it cannot resolve env vars itself).
## Apply, with adaptations
Because you are codex (single agent, separate process), not a Claude subagent:
**IGNORE** these sections — they describe Claude's internal Agent dispatch:
- "HARD-GATE" / "You have NO knowledge of conversation history" — you are
isolated by being a different process and model family
- "Incremental Mode Addendum" / "prior_fix_range" / drop signal (B) — those
depend on babysit-side state tracking. Skip the (B) check entirely. Signals
(A), (C), (D) still apply.
- "dispatched from a dev session" / "subagent" framing — you are codex
executing this prompt directly
**APPLY** in full:
- Per-persona category tables: Security (S1-S5), Staff Engineer (E1-E9),
SDET (T1-T4), Spec Auditor (C1-C4)
- Finding Inclusion Threshold: Justification class (Reachable / Precedent /
Asymmetric / Historical)
- Drop signals (A), (C), (D)
- Race-class Finding Metadata: Mitigation MUST end with
`[window=<ms|s|min|hr>, damage=<data-loss|deadlock|inconsistency|latency|marginal>, recovery=<has|no>]`
- Per-prompt Output Schema (Severity / Confidence / Blast / Justification /
Evidence / Failure mode / Mitigation)
## Execution
Execute all 4 personas sequentially. Output a combined finding list grouped
by persona.
## Scope
Review `git diff origin/<BASE>..HEAD` where BASE is below. Use `git diff` and
`git log --oneline` to understand the change. Read source files as needed.
## Output format (REQUIRED)
First emit per-persona findings in the per-prompt format from the prompt files.
Group by persona.
Then at the END emit a structured JSON summary block — this is REQUIRED for the
calling skill to drive its loop. The JSON block MUST be valid and parseable. Wrap
it in unique sentinel markers (NOT a generic markdown fenced block — those collide
with code examples in Evidence fields):
{
"findings": [
{
"id": "1",
"persona": "Security|Staff|SDET|Spec",
"category": "S2|E5|T1|C4|...",
"slug": "kebab-case-slug-from-finding",
"file": "path/to/file (relative to repo root)",
"line": 42,
"line_source": "source",
"severity": "Blocker|Factual|Suggestion|Question",
"justification": "Reachable|Precedent|Asymmetric|Historical",
"confidence": "high|medium|low",
"blast": "Local|Module|Cross-service|Data layer",
"failure_mode": "one-line",
"mitigation": "one-line, ending with race-meta tag if applicable"
}
]
}
Rules:
- `findings: []` (empty array) is VALID output meaning no findings. Still emit the
block with `{"findings": []}` between the sentinels — do NOT omit the JSON block.
- `line` MUST be the source file line number (the line in the file as written on
disk after your reading), NOT the diff hunk line number. If the diff shifted lines,
use the post-shift source file line.
- `line_source` MUST be `"source"` literal — this confirms you used source file
lines, not diff lines. Any other value → caller treats as malformed and escalates.
- All listed fields are REQUIRED. Do not emit findings with missing fields.
## Important
- Do NOT modify any files
- Race-class findings without meta tag → drop the finding
- You are codex, not Claude — your prose can be your own
- Stay focused on the diff
BASE branch: origin/<substitute BASE from skill caller>
停止条件摘要
| 条件 | 触发时机 | 操作 |
|---|---|---|
| SC1 Success | findings == 0 | 报告迭代次数 + 已应用的修复总数 |
| SC0 Severity floor | 本轮迭代有发现,但没有真正的 bug(没有 Blocker/Factual × Reachable/Asymmetric) | STOP,视为已基本收敛;列出卫生问题,不要自动修复 |
| SC2 Cap reached | iter > 3(MAX_ITERS) | 升级处理;向用户列出剩余发现 |
| SC3 Repeat 3x | 同一缺陷(文件 + 故障模式,基于判断,而非字符串匹配)连续出现 3 轮 | 升级处理;发出 race-of-race 信号(参见 pr-babysit § 4.5 Gate B) |
| SC3.5 Slug drift | 同一文件连续 3 轮或以上都有发现(仅按文件匹配的回退方式) | 升级处理;可能存在 slug drift,掩盖了卡住的发现 |
| SC4 Findings diverging | (a) 发现数量逐轮增加,或 (b) 有 ≥3 个发现且语义重叠为零,并且至少一半出现在上一批修复涉及的文件中 | 升级处理;自动修复正在引入新的问题面 |
| SC5 Test failure | $TEST_CMD 在本轮修复后以非零状态退出 | 升级处理;保留提交,供用户检查 |
| SC6 Skip backlog | 本轮有 ≥3 个发现被跳过(仅凭 Mitigation 无法修复) | 继续循环,但在最后列出跳过项 |
| User ctrl-c | 用户中断 | 报告部分状态、最后提交的迭代以及当时正在进行的操作 |
Mode=review-only
当用户明确要求只列出发现、不自动修复时:
- 运行 codex(使用与循环模式步骤 1 相同的提示词)
- 原样呈现
$OUTPUT_FILE中的输出(完整的逐角色发现文本) - 停止。不要提交。不要修改文件。
这是 L0+ 的“建议性发现”路径——由用户作出判断。
最终报告
循环退出后(无论触发哪项停止条件),生成报告:
SELF-REVIEW LOOP REPORT
═════════════════════════════════════════════════════════════
Iterations: <N>
Stop reason: <SC code + brief>
Commits made: <count> (atomic, one per finding)
- <sha> fix(<persona>): <slug>
- ...
Tests: <pass | fail | ⚠️ NOT RUN — no TEST_CMD detected, N commits unverified>
Loop authority: <stop condition honoured | ⚠️ human override — <which SC fired, and that the user directed the loop to continue>>
Findings still open (if escalation):
- <persona> / <category> @ <file>:<line>: <slug>
Mitigation: <one-line>
Why surfaced: <which SC fired>
Skipped findings (Mitigation needed design judgment):
- <persona> @ <file>:<line>: <slug>
Mitigation: <verbatim>
Reason: <why main session couldn't fix mechanically>
Suggested next steps:
- Review the atomic commits — revert any you disagree with
- For surfaced findings: read $WORK_DIR/iter-<N>.md for full context, decide modify/wontfix/defer manually
- Consider /cadence:pr-review mode=local for Claude-side multi-role view + comparison
- Push when satisfied
═════════════════════════════════════════════════════════════
说明
-
跨模型隔离的理由:codex(OpenAI GPT)审查 Claude 生成的代码 → 避免同模型自我偏好偏差(Wataoka 等人,基于困惑度)。每次调用 codex 都会启动一个全新进程,不会继承对话上下文。
-
防止上下文混杂的设计:codex 的输出写入
$WORK_DIR/iter-$ITER.md(文件,而非聊天)。主会话只将 JSON 摘要块解析到对话记忆中。实现每项修复时,主会话通过读取文件来访问完整的发现文本,不会自动注入。这能让主会话不断增长的对话在多轮迭代中保持精简。 -
方法论单一来源:codex 直接读取
${CLAUDE_PLUGIN_ROOT}/skills/pr-review/*-prompt.md(调度器会先将环境变量展开为绝对路径,再把提示词交给 codex)。更新 pr-review 提示词时,此技能会自动采用新方法论。无需在 pr-review 和自审之间维护同步。 -
适配层需保持同步:codex 提示词中的“忽略以下部分”列表与 pr-review 提示词中的 Claude 专属部分对应。如果 pr-review 增添了新的 Claude 专属机制,请更新 IGNORE 列表。此项已标记为维护事项,不会自动检测。
-
每项发现单独提交:
git revert <sha>可干净地撤销针对某项发现所做的修复。历史记录会保留 codex 标记了什么,以及如何修复的审计轨迹。 -
测试关卡是天然的安全网:检测“自动修复导致问题”的最廉价信号,就是测试失败。它能捕捉回归问题,无需复杂的语义验证。
-
循环次数上限(3)的依据:根据经验(在真实功能分支上运行 5 轮),codex 的高价值发现——真正可触达的漏洞,即同模型盲点这一类——都出现在第 1–3 轮。第 4–5 轮产生的都是卫生级小问题、已识别模式的尾部问题,以及一个控制器不得不驳回的明显误报。上限原为 5,现已改为 3。_有原则的_停止条件是 SC0(严重性下限);
MAX_ITERS是简单粗暴的兜底。如果第 3 轮仍然发现真实漏洞,这本身就说明改动过大,应该拆分。 -
SC0 与 MAX_ITERS——为何两者都需要:SC0(严重性下限)是预期的停止条件——当某轮没有产生真正可触达的漏洞时就会触发,也就是说,继续迭代只会发现小问题。
MAX_ITERS=3是针对 codex 在多轮后仍不断发现真实漏洞这一异常情况的兜底。健康的运行会在第 2–3 轮因 SC0 停止;只有不健康(差异过大)的运行才会达到上限。 -
泛化模式优于增加轮数:循环在结构上的弱点是将一个概念性漏洞放大为 N 轮中的 N 项发现(codex 每轮发现一个位置;控制器每轮修复一个位置)。第 3 步的“模式泛化”规则正是为了解决这一问题——首次发现某种模式的问题时,使用 grep 搜索并在同一轮修复所有相关位置。执行得当,循环无需依赖上限也能自行收敛。
-
作者偏见仍会影响 FIX 步骤,但不会影响 VERDICT 步骤:codex 的发现生成过程经过跨模型隔离。主会话编写修复时并未经过隔离,但 HARD-GATE 限制主会话只能机械执行
Mitigation:字段,从而消除判断推理这一攻击面。如果发现主会话在“推理 codex 是否正确”→ 这就违反了 HARD-GATE,应当直接呈现该发现,而不是争辩。 -
工作树假设:此技能预期在功能分支上运行(用户已处于工作树中,或位于非主分支)。不会自行创建工作树。如果用户位于
main,应发出警告,但不要阻止运行——用户可能知道自己在做什么。 -
调用之间不持久化状态:每次调用都会从头开始。
FINDING_HISTORY仅在单次调用中有效。如果手动编辑后重新调用,循环不会记住之前的运行情况——这是有意设计的(让技能保持无状态,无需维护.claude/state/文件)。