當 Codex 在審查 Claude Code 的提交時發生崩潰、中斷或產生兩個判斷時,我的閘門不會為該提交儲存任何判斷。由於沒有儲存的判斷,合併檢查會拒絕該提交,因此唯一有效的審查是包含恰好一行有效「Verdict」的最終訊息。
two-engines-gate 是我用來讓 Claude Code 和 Codex 互相檢查的免費、MIT 授權版本。我之前曾撰寫關於審查迴圈、提交雜湊記錄以及拒絕髒工作樹的文章。緊接在本文之前的文章則涵蓋了閘門如何處理被削弱或刪除的測試。本文則聚焦於那些沒有產生可用答案的審查。
review.sh 腳本以三種結束代碼之一結束:0 代表審查者回答「ACCEPT」,判斷已儲存;2 代表審查者回答「CHANGES REQUIRED」,判斷已儲存;1 代表沒有判斷,且沒有儲存任何內容。
merge-check.sh 僅在儲存的判斷包含單一行「Verdict: ACCEPT」時才通過提交。如果沒有儲存的判斷,它會停止並顯示「no review verdict for ; run review.sh」。
在呼叫審查者之前,review.sh 會刪除該提交已儲存的任何判斷,如果無法刪除則停止。當您重新執行審查且重新執行失敗時,來自先前執行的「ACCEPT」判斷不能在相同的雜湊下保留。
審查者退出後,review.sh 會依序執行以下六項檢查。第一個失敗的檢查會以結束代碼 1 終止執行。
首先,審查者必須以結束代碼 0 退出。否則,它會列印「REVIEW FAILED: codex exited 1; no verdict for 04bf3eea7ffd (log: .ai/reviews/04bf3eea7ffd.log)」。其次,HEAD 必須仍然是正在審查的提交。否則,它會列印「REVIEW VOID: HEAD moved during the review」。
第三,除了 .ai/ 之外,沒有任何檔案被更改。否則,它會列印「REVIEW VOID: files changed during the review (or git status failed)」。第四,最終訊息必須包含內容。否則,它會列印「NO VERDICT: codex wrote no final message」。
第五,最終訊息必須恰好包含一行以「Verdict:」開頭的內容。否則,它會列印「NO VERDICT: the final message needs exactly one Verdict: line, found 2」或「found 0」。
第六,該行必須是「ACCEPT」或「CHANGES REQUIRED」。否則,它會列印「NO VERDICT: the Verdict: line must say ACCEPT or CHANGES REQUIRED」。
只有在所有六項檢查都通過後,腳本才會寫入報告,其中包含完整的提交雜湊、基礎、建構者、審查者、日期和最終訊息。它會先寫入一個 .tmp 檔案,然後再將其移動到正確位置,這樣在儲存過程中發生崩潰就不會留下半寫入的判斷。
我在 10 月 23 日在一個臨時儲存庫中,使用公開腳本執行了每個案例。來自 examples/ 中的替身審查者提供了「ACCEPT」、「CHANGES REQUIRED」、崩潰和雙判斷的答案。而空訊息、中斷和拼寫錯誤的答案則來自一個我為測試編寫的小型假 Codex。
以下是審查者行為與 review.sh 輸出以及判斷是否儲存的對應關係:
- 回答「ACCEPT」:判斷檔案:.ai/reviews/04bf3eea7ffd.md,結束代碼 0,已儲存。
- 回答「CHANGES REQUIRED」:判斷檔案:.ai/reviews/04bf3eea7ffd.md,結束代碼 2,已儲存。
- 因錯誤退出:REVIEW FAILED: codex exited 1; ...,結束代碼 1,未儲存。
- 寫入空最終訊息:NO VERDICT: codex wrote no final message,結束代碼 1,未儲存。
- 在「Verdict」行之前停止:NO VERDICT: ... found 0,結束代碼 1,未儲存。
- 寫入兩行「Verdict」:NO VERDICT: ... found 2,結束代碼 1,未儲存。
- 寫入「Verdict: ACCEPTED」(拼寫錯誤):NO VERDICT: the Verdict: line must say ACCEPT or CHANGES REQUIRED,結束代碼 1,未儲存。
這些替身案例也包含在 tests/run.sh 中,包括檢查失敗執行後不會留下判斷檔案的兩個檢查,因此您可以自行重新執行它們。
為什麼兩個判斷會被視為無效?提示詞要求「Verdict」行「恰好出現一次,且不得在最終訊息的其他地方出現」。一個在頂部附近有「ACCEPT」,底部有「CHANGES REQUIRED」的答案包含兩個判斷,腳本無法知道審查者究竟想表達哪一個。
選擇第一個、最後一個或較友善的判斷都將是猜測,因此它兩者都不選。中斷的答案也會因相同的計數而失敗。提示詞將「Verdict」行放在最後一個部分的開頭,因此在該部分之前停止的回覆計數為 0。
判斷僅來自最終訊息。Codex 執行時帶有 --output-last-message 參數,這會將其最終答案儲存到檔案中。review.sh 從該檔案讀取判斷,而不是其他任何地方。執行的其餘部分會寫入判斷旁邊的日誌供您閱讀,腳本絕不會在日誌中搜尋「Verdict」行。
Codex 獲得的提示詞本身包含「Verdict: ACCEPT」,作為格式範例,因此搜尋任何重複提示詞的內容可能會找到審查者從未給出的「ACCEPT」。
有兩個 Codex 設定需要檢查,兩者都位於儲存庫中的 docs/engines.md。一個在審查後要求 Codex 繼續的「Stop hook」可能會用空訊息替換最終訊息。審查隨後會以「wrote no final message」結束,且沒有任何內容被儲存。
在審查時關閉該 hook,或者在 review-prompt.md 中添加一行,要求 Codex 在任何要求其繼續的情況下重複其完整的審查。~/.codex/config.toml 中的 MCP 伺服器也會在審查時啟動。一個可以在沙盒外部更改內容的伺服器會與唯讀審查衝突,因此在審查時將其 enabled = false。
閘門不做什麼?它不設定時間限制。一個掛起的審查者會讓腳本一直等待,直到您手動停止它。它也不會重試。在出現「NO VERDICT」或「REVIEW FAILED」之後,您需要閱讀日誌並再次執行 review.sh。它不對審查進行評分。一個簡短的「ACCEPT」判斷,只要包含一行「Verdict」,就能通過上述所有檢查,而審查的好壞則由您在儲存的檔案中自行判讀。
付費的 Two Engines Kit(目前版本 1.0.2)也採用相同的規則。其 cross-review.sh 僅從審查者的最終訊息中獲取判斷,其指南將崩潰、中斷的答案和拒絕列為導致結束代碼 1 且沒有判斷檔案的案例。當 REQUIRE_REVIEW=1 時,其合併腳本會拒絕任何該確切提交的判斷檔案不存在或未顯示「ACCEPT」的提交。
我是在 10 月的一個晚上發現四個從未被審查過的僅用於測試的提交後添加了這個功能。其中一個已經合併,三個在合併佇列中,審查者在佇列恢復前確認了所有四個,所以沒有發生任何問題。Two Engines Kit 是根據自第一個 Codex 和 Claude CLI 以來收集的資訊構建的。
試試看:複製 two-engines-gate 並執行 bash demo.sh,以觀察一個完整的迴圈與替身審查者,這樣就不會使用任何模型或帳戶。要親自查看遺失的判斷,請將 examples/stand-in-reviewer.sh 連結為 PATH 前面資料夾中的 codex,然後在臨時儲存庫中執行 review.sh,並將 STANDIN_ANSWER 設定為 two 或 fail。