在我上次 dev.to 的文章下方,有讀者詢問 merge-check 在工作樹有未提交變動時是否也會拒絕運行。答案是肯定的,審核和測試腳本也是如此。這篇文章將探討什麼情況算作「不乾淨」,檢查在哪裡運行以及為何需要這項檢查。
我在「two-engines-gate」中設定的方案是免費且採用 MIT 授權的版本,它展示了我如何讓 Claude Code 和 Codex 互相檢查。這個方案包含三個腳本:gate.sh 運行你的測試並寫入一個以提交名稱命名的通過記錄;review.sh 將提交發送給另一個引擎進行唯讀審核,並將結果儲存在提交雜湊值下;merge-check.sh 則會拒絕合併,除非你所在的提交同時具備這兩項記錄。
我之前已經寫過關於審核循環和提交雜湊記錄的文章,而這篇文章則專注於這三個腳本共享的同一項檢查。
每個腳本都定義了相同的小型函數 clean(),它會在 .ai/ 資料夾之外有變動或未追蹤的檔案時失敗,或者在 git status 本身失敗時失敗。git status --porcelain 會為每個變動的檔案列印一行,其格式專為腳本設計。--untracked-files=all 會列出所有未追蹤的檔案,包括新資料夾內的檔案。
路徑規範 ':(exclude).ai' 會排除 .ai/ 資料夾,這是閘門寫入其判斷結果、通過記錄和日誌的地方。如果指令列印出任何內容,則表示工作樹不乾淨。如果 git status 本身失敗,該函數也會失敗,這樣損壞的儲存庫就不會意外地看起來是乾淨的。這就是拒絕訊息會說「或 git status 失敗」的原因。
我在 10 月 23 日針對公開儲存庫運行了每個案例,以確切了解腳本的行為:已編輯但未提交的已追蹤檔案會被所有三個腳本拒絕;使用 git add 暫存但未提交的變動會被拒絕;Git 未追蹤的新檔案會被拒絕;已編輯但未提交的 .gitignore 會被拒絕;.ai/ 下的新檔案則被允許;.gitignore 涵蓋的資料夾內的新檔案也被允許。
拒絕訊息會顯示為:「review: REFUSED: uncommitted changes or untracked files (or git status failed); commit or remove them first」。請注意 .gitignore 那一行。
README 要求你將 .ai/ 加入專案的 .gitignore,而這項編輯本身在提交之前就是一個未提交的變動,因此第一次審核會被拒絕。
review.sh 會從提交日誌和 git diff base...HEAD 構建審核者的提示詞,而這兩者都只包含已提交的工作。審核結果隨後會儲存在 HEAD 的雜湊值下。審核提示詞還會告訴審核者,它可以在唯讀沙盒中讀取 checkout 中的任何檔案以獲取上下文,而 Codex 能夠做到這一點。
因此,如果編輯內容存在於工作樹中但沒有提交,審核者可能會打開一個內容與其正在判斷的提交不同的檔案。那麼它的 ACCEPT(接受)將會儲存在一個不包含它所查看程式碼的雜湊值下。當腳本在審核開始前拒絕時,磁碟上的檔案會與正在審核的提交相符。
gate.sh 會在磁碟上的檔案上運行你的測試指令,而磁碟上的檔案正是測試指令所看到的。一個未追蹤的輔助檔案或一個從未提交的本地修復可能會讓測試通過。那麼通過記錄將會攜帶一個提交的雜湊值,而該提交在全新 checkout 時可能會失敗。我的工具包中的指南簡潔地說明了這一點:閘門拒絕不乾淨的工作樹,因為「檢查可能會在一個不屬於提交的檔案上通過」。
review.sh 和 gate.sh 都會運行兩次檢查。在任何操作開始之前,每個腳本都會刪除該提交的早期證據。review.sh 會刪除舊的判斷檔案,而 gate.sh 會刪除舊的通過記錄。只有這樣,乾淨檢查才會運行。因此,被拒絕的運行不會為該提交留下舊的 ACCEPT 或舊的通過記錄。
在審核者或測試完成後,腳本會再次檢查兩件事:HEAD 是否沒有移動,以及工作樹是否仍然乾淨。如果審核者更改了檔案,review.sh 會列印「REVIEW VOID: files changed during the review」並且不保存任何判斷。如果測試指令留下了檔案,gate.sh 會使運行失敗。
我用一個會創建一個雜散檔案的測試指令進行了測試:gate: running 'bash test.sh; touch stray.txt' on 986def16acb8;gate: the run left changed or untracked files (or git status failed);gate: FAIL (exit 1) on 986def16acb8; no pass record; full log .ai/tests/986def16acb8.log。
一個將快照或構建輸出寫入儲存庫的測試套件將會遇到這個問題。解決方法是讓套件寫入 Git 忽略的地方,並提交該 .gitignore 變更。
merge-check.sh 在查看任何通過記錄或判斷之前,會運行相同的 git status 檢查。合併只會處理提交,因此未提交的編輯無論如何都不會被合併。它們仍然意味著 checkout 包含沒有人審核或測試過的工作。在乾淨的 checkout 情況下,merge-check 的「是」表示完全涵蓋了磁碟上的內容。
在依賴這項檢查之前,請了解這些限制。被忽略的檔案對它來說是不可見的。git status 不會列出 .gitignore 涵蓋的檔案,因此讀取被忽略的配置檔案或構建快取的測試在全新 checkout 時仍然可能表現不同。閘門不會捕捉到這一點。
.ai/ 被故意排除在外,這意味著你放在那裡的任何東西都不在檢查範圍內。README 將閘門稱為「防止合併錯誤提交的本地防護,而非安全邊界」,因為任何對儲存庫具有寫入權限的人都可以在 .ai/ 下寫入檔案。它只檢查本地 checkout,不推送、不合併,也不查看遠端儲存庫的內容。
付費的 Two Engines Kit(目前版本 1.0.2)在其腳本中也有相同的規則。它的 cross-review.sh 和 gate.sh 都會拒絕包含未提交變更或未追蹤檔案的工作樹,並且指南中寫道:「不乾淨的工作樹或未追蹤的檔案不會產生記錄,因此你無法對不在提交中的程式碼進行閘門檢查,然後再合併該提交。」
Two Engines Kit 是根據自第一個 Codex 和 Claude CLI 以來收集到的資訊構建的。
你可以嘗試克隆 two-engines-gate 並運行 bash demo.sh,以觀察一個完整的循環,其中包含一個替代審核者,因此不會使用模型或帳戶。然後,在一個使用閘門的專案中編輯一個檔案,保持未提交狀態,並運行 review.sh 以查看拒絕訊息。如果你想從測試端了解這項檢查,tests/run.sh 中的測試套件包含一個不乾淨的工作樹案例和一個「審核期間檔案已更改」的案例。