隔離工作樹 PR 流程
當主要簽出不可被觸碰時——因為它處於髒亂(未提交的進行中工作)、落後於遠端,或與其他代理人共用——從 origin 的預設分支建立一個臨時 git 工作樹並以 PR 形式交付變更的程式。此流程涵蓋工作樹建立、全新工作樹的依賴缺口、有界 CI 輪詢,以及合併模式感知的清理防護。
為何需要工作樹隔離
當多個代理人工作階段進入同一個工作目錄時(例如透過批次工作階段啟動),git 的 HEAD、目前分支和未提交的變更會成為共用狀態——每個工作階段都會在其他工作階段之下移動基礎。在一個被量測的事件中,一個工作階段用 git switch -c 建立分支後不到一分鐘,第二個工作階段在同一個簽出中便從該分支分岔,提交並合併到預設分支。
檢測模式(可在數分鐘內確認衝突):
- 跡象:出現你未撰寫的檔案,或
git status不再符合工作階段開始時的快照。 - 時間戳記審查:
ls --time-style=full-iso(或你平台的等效指令)。數個檔案共用相同的毫秒數表示 git 操作(簽出 / 合併 / stash)寫入了它們,而非手動或 LLM 順序編輯。 git reflog --date=iso可釐清時間線:誰簽出、提交或合併,以及何時發生。- 如果你的代理人環境提供工作階段列表功能,請用它來尋找具有相同工作目錄的其他執行中工作階段。
一旦確認衝突後的讓步規則:
- 先提交的一方保持為寫入者。後來者停止寫入:繼續提交或切換分支會移動另一個工作階段的 HEAD 並破壞雙方。
- 後來者改為作為驗證者增加價值:對撰寫者輸出進行對抗性審查(文件聲明 vs 程式碼,重新執行測試,發佈前檢查),以及不碰觸任何檔案的純聊天交付物。
- 僅清理你自己建立的中間引用(例如一個空分支);永遠不要碰觸其他工作階段使用過的引用。
- 僅追加日誌對於兩個工作階段在各自的章節標題下寫入是安全的。
預防措施:一個儲存庫 = 一個寫入工作階段。當多個代理人必須在同一個儲存庫上工作時,將每個代理人隔離在自己的工作樹中——這正是此技能所做的。最便宜的檢測習慣是在開始工作後立即重新比對 git status 與你的工作階段開始快照。
可攜性
此流程已在 Windows(PowerShell 加上 Git Bash)上經過實戰測試,但 git 和 gh 命令本身是跨平台的。在 shell 不同的地方,同時給出了 PowerShell 和 POSIX 範例。CI 包含在 Ubuntu 24.04 和原生 macOS 15 上執行的 PowerShell 7 工作,用於綜合清理防護和私有標記掃描器,以及 Windows 主機。這三個工作在 PR #7 執行編號 30216166105 中全部通過。Windows 特定的部分——目錄連結及其移除語義——在內文中有 POSIX 符號連結的等效說明。
何時使用
- 主要簽出處於髒亂(未提交的進行中工作)、落後於遠端,或與其他代理人共用,且直接在該簽出中建立分支可能會破壞該進行中工作。
- 即使是一個小的整潔修復:如果現有的簽出落後許多提交並帶有髒亂的進行中工作,隔離工作樹是更安全的路線(經實戰測試)。
- 假設採用每個任務一個分支的工作流程(絕不直接提交到預設分支)。此技能僅涵蓋該分支所在位置的機制:一個在髒亂簽出之外的臨時工作樹。
程序
-
檢查狀態,非破壞性地。每個
git命令都使用-C <repo>執行,每個gh命令都使用--repo <owner>/<name>執行,這樣就不依賴於當前目錄——代理人環境可能會在呼叫之間重設工作目錄,而沒有--repo,gh在儲存庫外部被呼叫時會因為「不是一個 git 儲存庫」而失敗。git -C <repo> status --short # 記錄工作前狀態(最後驗證未更改) git -C <repo> branch --show-current git -C <repo> fetch origin gh repo view <owner>/<name> --json defaultBranchRef -q .defaultBranchRef.name # 將結果用作下面的 <default>相同的命令在 POSIX shell 中也可以原樣運作。
-
從
origin的預設分支建立一個臨時工作樹,不觸碰髒亂的簽出。用步驟 1 的結果替換<default>——不要硬編碼main;預設為master的儲存庫仍然很常見。git -C <repo> worktree add -b fix/<task> ..\_worktrees\<task> origin/<default>POSIX:
git -C <repo> worktree add -b fix/<task> ../_worktrees/<task> origin/<default>將工作樹放在確認可寫入的位置。系統範圍的臨時目錄有時無法從沙箱化的代理人環境中寫入(經實戰測試)。偏好將
_worktrees/目錄放在儲存庫旁邊或你環境指定的臨時目錄中,並在執行任何繁重操作之前,寫入一個探測檔案並刪除它以確認寫入權限。 -
保持差異最小。只編輯(或複製)任務所需且在工作樹內的檔案。不要將主要簽出的未追蹤進行中工作大規模帶入——上游或其他代理人可能會新增同名檔案,而額外的檔案會在合併時導致新增/新增衝突,或污染 PR 差異。如果你必須從基礎簽出包含一個未提交的依賴檔案的副本,請在 PR 主體中說明合併時哪一方獲勝(實戰教訓:明確執行此操作避免了錯誤的自動解析)。
-
處理全新工作樹的依賴缺口。一個新的工作樹沒有
node_modules和 virtualenv(經實戰測試)。-
Node:在工作樹中執行
npm ci(需要鎖定檔案)。僅用於輕量驗證時,臨時連結到主要簽出的node_modules也可以——但在清理之前移除該連結(安全條件 5)。PowerShell 目錄連結:New-Item -ItemType Junction -Path <worktree>\node_modules -Target <repo>\node_modules # 移除時必須僅刪除連結,絕不刪除目標:使用 rmdir。 cmd /c rmdir "<worktree>\node_modules"POSIX 符號連結:
ln -s <repo>/node_modules <worktree>/node_modules # 用普通的 rm 移除連結本身(不帶 -r,不帶尾隨斜線)。 rm <worktree>/node_modules -
Python:重複使用主要儲存庫的 virtualenv 直譯器並將其指向工作樹的原始碼,僅執行目標測試。PowerShell:
$env:PYTHONPATH = "<worktree>\src" & <repo>\.venv\Scripts\python.exe -m pytest <worktree>\tests\<target_test>.pyPOSIX:
PYTHONPATH=<worktree>/src <repo>/.venv/bin/python -m pytest <worktree>/tests/<target_test>.py
-
-
當驗證通過時:提交、推送並建立 PR。將提交訊息寫入一個檔案並用
git commit -F <msgfile>傳遞——內聯多行訊息在 shell 引用下很脆弱,尤其是在 Windows 上。git -C <worktree> add <target files only> git -C <worktree> commit -F <msgfile> git -C <worktree> push -u origin fix/<task> gh pr create --repo <owner>/<name> --head fix/<task> ... -
使用有界輪詢等待分支保護 CI。
gh pr checks --watch是一個無界等待——代理人框架和審批層可能會拒絕它或掛起(經實戰測試)。使用一次性檢查搭配嘗試次數上限:gh pr checks <pr-number> --repo <owner>/<name> --watch=false gh pr view <pr-number> --repo <owner>/<name> --json state,statusCheckRollup有界迴圈,POSIX。如果框架阻塞了前景
sleep(Claude Code 會),則將迴圈作為背景任務執行,或跳過迴圈並將一次性檢查交織在其他工作片段之間:for i in $(seq 1 10); do gh pr checks <pr-number> --repo <owner>/<name> --watch=false && break; sleep 30; done有界迴圈,PowerShell:
for ($i = 1; $i -le 10; $i++) { gh pr checks <pr-number> --repo <owner>/<name> --watch=false if ($LASTEXITCODE -eq 0) { break } Start-Sleep -Seconds 30 }正確的上限和間隔取決於儲存庫的 CI 持續時間(一般情況下未驗證——請根據每個儲存庫調整)。
-
合併——並先決定合併模式,因為它決定了清理前未合併工作的驗證(安全條件 2):
--merge(合併提交):分支提交會成為預設分支的祖先,因此用merge-base --is-ancestor驗證,並用branch -d刪除(安全條件 2a;經實戰測試)。--squash/--rebase:分支提交永遠不會成為預設分支的祖先,因此對分支執行--is-ancestor會永遠回傳 1,即使在完全成功的合併之後也是如此。請改用「MERGED + headRefOid 匹配」防護(安全條件 2b)。
gh pr merge <pr-number> --repo <owner>/<name> --merge # squash 策略的儲存庫:--squash,然後使用防護 2b gh pr view <pr-number> --repo <owner>/<name> --json state,mergedAt,mergeCommit關於
gh pr merge的已知行為(經實戰測試):- 如果本地預設分支在另一個工作樹中被簽出,從儲存庫內部執行的
gh pr merge --delete-branch可能會在其本地後續處理中失敗,即使遠端合併成功。從儲存庫外部使用--repo執行合併會完全跳過本地後續處理,從根本上避免了此類失敗。 - 如果無論如何回報了失敗,請先檢查
gh pr view --json state——遠端端通常已經是MERGED。如果是這樣,請在滿足以下安全條件後手動完成其餘的本地步驟(分支刪除、工作樹移除)。 - 從儲存庫內部執行的
gh pr merge可能會將目前工作樹快轉到預設分支;在其輸出中看到來自早期 PR 的大量更新並非異常。
安全條件
在任何破壞性步驟之前(工作樹移除、分支刪除、修剪),請在執行時檢查以下所有項目——在刪除之前執行驗證,絕不能在之後執行。跳過刪除前檢查並「事後驗證」會使防護失效:未合併工作的錯誤只有在分支已經消失時才會顯現(實戰教訓)。在共用的簽出中,狀態可能會在步驟被批准和執行之間漂移;如果時間已經過去,請在執行前立即重新執行檢查(經實戰測試)。
-
gh pr view <pr-number> --repo <owner>/<name> --json state,mergedAt報告MERGED。 -
符合合併模式的未合併工作驗證。不要混合使用模式:在 squash 之後,防護 2a 對於一個正確合併的 PR 也永遠不會通過;而 squash 之後的
branch -d要麼會透過合併到上游的邏輯溜過去(如果origin/fix/...仍然存在——這不是祖先保證),要麼會被永遠拒絕為「未完全合併」(如果上游已經消失)。- 2a ——
--merge(合併提交)模式:git -C <repo> merge-base --is-ancestor fix/<task> origin/<default>回傳 0(在 PowerShell 中檢查$LASTEXITCODE,在 POSIX shell 中檢查echo $?)。用-d刪除分支,這樣 git 本身仍然會拒絕任何未合併的內容(經實戰測試)。注意:-d是根據分支的上游(如果設定)來判斷「已合併」,否則根據 HEAD 來判斷——因此在遠端分支被刪除和修剪後,並且簽出中的 HEAD 停留在一個更舊、不相關的分支上時,-d可能會拒絕並顯示not fully merged,即使 PR 已正確合併(經實戰測試)。在這種情況下,僅在--is-ancestor檢查回傳 0 之後立即使用-D刪除是可以接受的——與 2b 相同的「先防護再-D」形式;未經防護的-D仍然被禁止。 - 2b ——
--squash/--rebase模式:透過gh pr view <pr-number> --repo <owner>/<name> --json state,mergeCommit,headRefOid獲取state、mergeCommit和headRefOid。請注意,mergeCommit是一個 JSON 物件——提交雜湊是其oid欄位(-q .mergeCommit.oid可提取它);headRefOid是一個純字串。然後確認兩者:(i)git -C <repo> merge-base --is-ancestor <mergeCommit-oid> origin/<default>回傳 0,且 (ii)git -C <repo> rev-parse fix/<task>等於headRefOid。檢查 (ii) 是 squash 模式下「沒有遺留提交」的替代方案:它證明已合併的 PR 頭部恰好是你的本地分支頂端。僅當兩者都成立時,使用明確的git branch -D刪除(此防護取代了-d)。 防護 2b 的拓撲結構和拒絕路徑由scripts/test-cleanup-guards.ps1中的一次性綜合 Git 歷史覆蓋。該測試隔離了系統/全域 Git 配置、鉤子、簽署和rebase.updateRefs;快照、清除並恢復每個環境中的GIT_*變數,以便儲存庫/追蹤路徑無法逃離固定裝置;並透過作業系統感知的路徑比較和重新解析點拒絕,將其遞迴清理限制在其產生的直接 temp 子目錄。即時 GitHub squash 路徑已於 2026-07-23 使用 PR #2 量測:MERGED、已落地的mergeCommit、匹配headRefOid的未更改本地和遠端頂端、受防護的本地-D以及明確的遠端刪除均通過。即時 GitHub rebase 路徑已於 2026-07-26 使用 PR #5 量測:MERGED、一個重寫的已落地mergeCommit、在預設分支祖先鏈之外的原始 HEAD、匹配headRefOid的未更改本地和遠端頂端、受防護的本地-D、明確的遠端刪除以及擁有的工作樹清理均通過。
- 2a ——