closeout 是做什麼的?
讓審查完成的 PR 回歸乾淨的基底工作區 — 合併、將 shell 從工作樹切換出來再移除、清理已合併的分支、拉取基底,並驗證最終狀態。這是管線的尾部:最常重複的操作(「我完成了,合併它並讓我回到乾淨的 main」)從使用者的腦袋裡搬到技能中。
為什麼需要這個技能
/execute 步驟 0 為每個分支片段建立一個工作樹;但在這個技能出現之前,沒有任何東西會移除它。工作樹就像一個有進無出的庫存,庫存會無限增長(Meadows,《思考系統》— 對一個有進無出的庫存的結構性解法是加入遺失的反饋迴路,而不是更加努力地要求)。合併 → 拆除 → 拉取的順序僅存在於使用者的記憶中,而「前瞻記憶會默默地失效」(Norman,《日常事物的設計》— 將知識從腦袋移到世界中)。這個順序確實容易出錯:在 shell 的當前工作目錄仍然在工作樹內部時移除工作樹,會讓 shell 陷入一個已刪除的目錄,而從那裡觸發的依賴 node_modules 的 Stop 鉤子會噴出一堆假的 MODULE_NOT_FOUND「lint/型別錯誤」,讀起來像真的回歸。步驟 4 中先切換到基底再移除的順序正是為了解決這個失誤而設計的關鍵修正。
此技能協調現有工具(gh pr merge、wt remove / git worktree remove、commit-commands:clean_gone)。它不會引入任何檔案系統狀態。它驗證的是結果 — 乾淨的樹、在最新的基底上、PR 已合併 — 而不僅僅是清理有執行過(Meadows 所謂的「追求錯誤目標」:一個以「是否執行」來衡量的結案流程會優化代理指標,並產生脆弱的儀式性動作)。
呼叫時機
這是一個主要管線技能,負責預設交付路徑的 合併 → 清理 尾部:
… → /pre-merge → /compound(在 PR 內,當有經驗教訓時) → /pre-merge(在 compound 提交上重新執行;重新蓋章) → [合併 + 工作樹拆除 — 本技能] → [關閉议题 — 步驟 9 說明書]
在以下情況使用 /closeout:
/pre-merge已建立一個 PR 且審查已完成(或變更足夠微小無需審查)- PR 已獲批准且可合併,您希望將其合併並將工作區恢復到乾淨的基底
- 使用者要求「結案」、「合併並清理」、「拆除工作樹」或「回到乾淨的 main/base」
在以下情況不要使用 /closeout:
- PR 尚未建立或仍在審查中 — 請先執行
/pre-merge - 您想記錄一個持久的經驗教訓 — 那是
/compound的職責,它預設會在/closeout合併之前已經在 PR 上執行過(合併後的/compound是備用方案);/closeout不記錄經驗教訓 - 您想關閉 PRD 和分支議題,或更新研究文檔的 frontmatter — 那是
SYSTEM-OVERVIEW.md步驟 9 中清理說明書的職責;/closeout執行的是 git 衛生 這一部分,並將 關閉議題和研究文檔更新 這一部分委託給步驟 9,而不是重複它。它唯一關閉的是已合併分支片段在sidecar後端上的工作項目(步驟 3),因為沒有Closes #N這一行會在合併時自動關閉它 - 變更根本不在一個功能分支上(您就在基底分支上,沒有東西可合併)— 沒有東西需要結案
它經 HITL 確認且非阻塞:它將一個本來就可選的尾部操作變為自動化。它永遠不會自動合併 — 合併是對外的且難以撤銷,所以它總是會先確認。
流程
以「執行-確認」檢查清單的方式執行(Gawande):執行每個步驟,然後在下一步之前驗證其結果。破壞性步驟(4-6)設有自我保護機制 — 請在執行前確認步驟 2 中的安全前提條件成立。
0. 主機供應的隔離?先放棄拆除權
在合併/拆除序列之前,確定此倉庫的隔離是主機擁有的(Conductor、Codespaces、devcontainers)。如果是,/closeout 仍然執行 GitHub 原生的合併,但不會拆除工作樹或清理本地分支 — 主機擁有該部分生命週期,與其對抗就是 Meadows 所謂的政策抵抗(兩個參與者推動同一個庫存)。將工作樹庫存讓給主機。
流出檢測比 /execute 的流入檢測更嚴格,並且不能單獨使用通用的「toplevel ≠ primary working tree」啟發式判斷:管線建立的工作樹也滿足該測試,因此通用信號無法區分「主機擁有拆除權」與「管線建立了這個並必須拆除它」這兩種情況。需從 .claude/settings.json 中的 worktree.provisioning 解析(預設不存在時為 "auto"),並在此端尊重明確的覆蓋設定,與 /execute 在流入端所做的完全一樣:
host— 無條件放棄拆除權。pipeline— 管線擁有拆除權。即使存在主機環境變數,也要執行下方完整的步驟 4-6 — 明確的pipeline設定意味著/execute自己供應了工作樹,因此/closeout必須拆除它(這是主機模式前的行為)。主機環境變數不能覆蓋明確的pipeline選擇。auto(預設)— 僅當主機環境變數描述這個工作樹時放棄拆除權:以下區塊會輸出host_owned=yes。絕不能僅根據通用的「toplevel ≠ primary」信號放棄,也絕不能僅因為存在CONDUCTOR_WORKSPACE_PATH就放棄 — 在 Conductor 下啟動的 shell 會繼承這個變數,且它可能指向另一個倉庫的工作區,在這種情況下放棄拆除權會留下一個無人拆除的管線建立的工作樹。
# host-signal: 主機環境變數是否描述 THIS 樹,而不僅僅是這個 shell?
host_owned=no
top=$(cd "$(git rev-parse --show-toplevel)" && pwd -P)
if [ -n "${CONDUCTOR_WORKSPACE_PATH:-}" ]; then
ws=$(cd "$CONDUCTOR_WORKSPACE_PATH" 2>/dev/null && pwd -P) || ws=
if [ -n "$ws" ]; then
case "$top/" in "$ws"/*) host_owned=yes ;; esac
fi
fi
if [ -n "${CODESPACES:-}" ] || [ -n "${REMOTE_CONTAINERS:-}" ]; then host_owned=yes; fi
echo "host_owned=$host_owned"
如果未放棄拆除權(包括 worktree.provisioning: "auto" 且 host_owned=no),則管線擁有它 — 按以下說明執行完整的步驟 4-6。
當隔離為主機擁有時,/closeout 執行一個簡化的序列:
- 步驟 3(合併)仍然完整執行 —
gh pr merge --delete-branch。Conductor 及其同類主機建立 PR,但將合併留給人類操作;管線執行 GitHub 原生合併是其增加的價值,且主機會反映該結果。放棄權限範圍僅限於檔案系統拆除,絕不包括合併。 - 跳過步驟 4 的重新錨定、步驟 5 的
wt remove/git worktree remove/ExitWorktree { remove },以及步驟 6 的本地分支清理 — 主機擁有工作樹和分支的生命週期(Conductor 在工作區歸檔時回收它們)。永遠不要對主機管理的樹使用--force。 - 步驟 8 的檢查清單:「工作樹已移除」和「分支已清理」變為**「不適用 — 主機管理」。合併檢查、乾淨樹檢查、PR 已合併檢查和遠端分支**檢查仍然適用。最後一項很容易被歸入同一個「不適用」,但事實並非如此:主機擁有的是檔案系統工作樹和本地分支,而遠端分支是由此技能仍完整執行的
gh pr merge --delete-branch刪除的(見上方一個要點)。放棄主機擁有的那一半,並不意味著放棄主機不擁有的那一半。
1. 確認意圖並收集狀態
絕不自動合併。說明您將要做什麼,並獲得明確的許可。
首先收集事實(這些是唯讀的):
git branch --show-current # 功能分支
git rev-parse --show-toplevel # 當前工作樹(可能是工作樹)
git worktree list --porcelain | awk '/^worktree /{print $(2); exit}' # 基底檢出(主工作樹)
gh pr view --json number,title,url,state,mergeable,mergeStateStatus,reviewDecision 2>/dev/null
確定 PR 目標的基底分支 — 是倉庫宣告的預設分支,而不是假設:
SC="<此技能目錄>/references/sidecar.sh"
bash "$SC" base-ref --name # 純分支名稱:prod,絕非 origin/prod
此處需要基底分支的名稱,而不是 ref — 它在步驟 4 中提供給 git switch <base-branch>,而 git switch 會直接拒絕遠端追蹤 ref(fatal: a branch is expected, got remote branch 'origin/prod')— 它需要您即將拉取的本地基底分支。因此它要求 base-ref --name,並且刻意不使用 /pre-merge、/help、/walk-commits、/visual-recap、/compound 和 /execute 所需的 ref 形式;它們需要 ref 只是因為它們將基底用作範圍端點,而此技能從不需要。不要「修正」它以匹配它們。如果 base-ref --name 以非零退出碼退出,表示未找到基底:轉發其 stderr 並停止,而不是猜測一個。
以一個簡短的區塊告知使用者:哪個 PR 將被合併、哪個工作樹(如果有的話)和分支將被拆除、以及將返回並拉取哪個基底分支。等待確認。如果此分支上沒有 PR,則停止並轉向 /pre-merge。
2. 在任何破壞性步驟之前驗證安全前提條件
拆除是破壞性的;首先驗證其安全性(自我保護)。所有條件都必須成立:
- 功能分支已完全推送 —
git status顯示無未推送的提交,且git log @{u}..為空。 - 此分支存在一個 PR 且可合併(
mergeable為MERGEABLE;mergeStateStatus不是BLOCKED/DIRTY)。 - 必需的檢查為綠色且審查已批准(
reviewDecision為APPROVED,或倉庫不需要審查且使用者已明確接受)。
如果任何前提條件失敗,請停止並報告是哪一個 — 不要合併或移除任何東西。失敗的前提條件是返回審查的信號,而不是強行通過的信號。當失敗的是審查 — reviewDecision 是 CHANGES_REQUESTED 時 — 返回路徑是 /respond-to-review:它會逐條檢查每個評論與程式碼,由人類決定修復什麼以及反駁什麼,然後修復通過 /fix-findings 和重新執行的 /pre-merge 進行,之後才能再次執行 /closeout。
審查時效性 — 批准是否仍然涵蓋這個 diff?(非阻塞)
上面的三個複選框詢問的是是否進行了審查。沒有一個詢問它是否涵蓋了即將合併的 diff。在意圖的線性流程中,這兩個問題是相同的,這正是差距不可見的原因;在真實的 HITL 會話中,範圍會在審查通過後累積到分支上,而沒有任何東西將 /pre-merge 讀取的 diff 與 /closeout 合併的 diff 連接起來。這是一個有進無出的反饋迴路(Meadows — 結構性解法是加入迴路,而不是要求),它在最關鍵的時刻悄悄地把「PR 已經審查過了」降級為「PR 的早期版本已經審查過了」。持續交付明確陳述了規則:您審查的工件必須是您交付的工件。
讀取 /pre-merge 階段 4 留下的戳記並與當前 head 進行比較。它位於 backend stamp 指定的位置,根據 references/state-backend.md:在 PR 主體中,或在 sidecar 中。
git fetch --quiet
SC="<此技能目錄>/references/sidecar.sh"
case "$(bash "$SC" backend stamp)" in
sidecar)
REVIEWED_SHA=$(bash "$SC" stamp read "$(git branch --show-current)")
HEAD_SHA=$(git rev-parse '@{u}' 2>/dev/null || git rev-parse HEAD) ;;
github)
REVIEWED_SHA=$(gh pr view <number> --json body -q .body \
| sed -n 's/.*<!-- reviewed-at: \([0-9a-f]\{40\}\) -->.*/\1/p' | tail -1)
HEAD_SHA=$(gh pr view <number> --json headRefOid -q .headRefOid) ;;
*) echo "closeout aborted: sidecar.sh backend stamp failed; not guessing the github path" >&2; exit 1 ;;
esac
在 sidecar 後端上,head 是分支的上游,也就是 forge 將要合併的內容:上面的第一個前提條件已經要求它與本地 head 匹配。當分支沒有上游時,它會回退到 HEAD。stamp read 會輸出與 PR 主體標記相同的完整 40 字元 OID,或者什麼都不輸出,因此下面的所有內容在兩個後端上都可以不變地執行。
恰好 40 個十六進位字元,透過字串相等性比較。 這句話的兩半都是關鍵的,因為這個標記是一個跨越兩個技能的合約。/pre-commit 階段 4 會輸出完整的 40 字元 OID,而 headRefOid 總是 40 字元,因此 \{40\} 是唯一可能比較相等的寬度 — 接受較短的寬度會讓格式錯誤的標記(手動編輯的主體、替換到錯誤一半的模板佔位符)成功解析,然後與 $HEAD_SHA 永遠不同。[[ "$REVIEWED_SHA" == "$HEAD_SHA" ]] 是比較操作:兩個完整 OID 之間的精確字串相等性,絕不是前綴測試。正則表達式拒絕的標記會產生空的 $REVIEWED_SHA 並走到下面的「未找到戳記」分支,這會優雅地降級 — 遠比「SHA 不同」分支在 head 從未移動的 PR 上發出虛假警報要好得多。在 Skill Kit 倉庫中,scripts/test-review-currency-marker.sh 針對這兩個技能固定了這個往返測試,這樣編寫者的格式和這個解析器就不會默默地漂移。
三種結果,只有一種會中斷流程:
- SHAs 匹配 — 完整 OID 的字串相等,因此審查過的 diff 就是合併候選者。靜默通過。不要描述它:這是常見情況,每次執行都宣布一個非事件正是將閘門變成壁紙的原因。
- 未找到戳記(
$REVIEWED_SHA為空)— 手動編寫的 PR、外部貢獻、早於戳記的 PR,或形狀被上述正則表達式拒絕的標記。用一行說明它(「此 PR 上沒有/pre-merge戳記 — 審查時效性未知」)並繼續。缺少戳記不是未審查 diff 的證據,而在每個未蓋章的 PR 上停止正是導致閘門被反射性繞過的警示疲勞(Meadows,政策抵抗)。 - SHAs 不同 — 兩個格式良好的 40 字元 OID 不相等,表示審查後有提交進入。不要停止,
…
[以下內容因原始輸入截斷而被截斷,無法翻譯顯示完整的步驟和後續內容。翻譯過程中已將可用的上下文全部轉換為繁體中文,保留了所有程式碼區塊、變數和命令。]