pre-merge 是做什麼的?
建立一個連結回PRD與子任務的GitHub PR,然後根據專案的架構原則審查完整差異。產生建議性發現結果 — 不封鎖合併、不自動修復程式碼、也不建立議題。
調用位置
此技能是主要管線技能,用於實作已驗證後、合併至 main 分支之前,或在接手他人的PR進行審查時使用。
當分支準備好建立PR、進行架構審查以及最終的計畫與程式碼對照時,使用 /pre-merge。當你要審查非自己建立的PR時,使用 /pre-merge --pr <number>。
請勿將其用作實作驗證、QA接收或重構規劃的替代品。它假設工作已經完成並準備好進行審查。
模式
/pre-merge 以三種模式之一執行。所有三種模式都重複使用 review-checklist.md 中定義的架構審查維度;它們在消費內容和產出結果上有所不同。
- 作者模式(預設) — 在自己的分支上呼叫,不帶
--pr參數。技能會建立PR(階段二),並將發現結果以建議形式輸出至終端機(階段四),然後交回控制權 — 它不會自己進行任何修復提交,也不會開啟第二輪審查。 這是/execute步驟六在HITL路徑上自動調用的模式。它讀取的內容由分支狀態決定,而非由旗標決定: 首次審查會讀取整個子任務,也就是整個分支,除非該子任務的工作項目記錄了起始點;若針對已帶有審查貨幣戳記、且頭部後續有變動的PR重新執行,則只會取戳記後的差異 — 戳記SHA之後的提交 — 作為審查的 主體,而不會重新讀取先前審查已涵蓋的內容。階段一步驟四會做出此決定並輸出。差異是主體,而非審查者可能閱讀範圍的限制:review-checklist.md規定了審查者應讀取的內容,而這包含差異周圍的目錄樹結構。 - 審查者模式 — 使用
/pre-merge --pr <number>呼叫,針對非你建立的PR。技能會跳過PR建立(PR已存在),並產生 草稿評論文字(階段四)供你審查與發布,這些文字根據references/comment-craft.md(5P閘門、三重R、評論訊號、MMG交換)的格式組織。 - 循環模式 — 使用
/pre-merge --loop呼叫,或當/execute在AFK執行中交回控制權時自動進入。此模式執行階段一至四,如同作者模式,但階段一步驟四的範圍決策不同:循環模式總是讀取整個分支,從不採用戳記後的差異,然後繼續進入階段五:它將每個發現記錄在持久化記錄簿中 — 在PR主體或工作項目上,根據 references/state-backend.md § 審查記錄的儲存位置所述 — 並附上支持或反駁每個發現的證據,然後交回給操作者。在此模式下,發現結果不再是終端機輸出;它們變成了有負責人的持久化記錄行。循環模式不修復任何東西,也不進行任何提交 — 操作者決定每個發現的後續處理方式(參見階段五 § 為什麼循環不進行修復)。
循環模式的退出條件是每個發現都有負責人 — 絕不是審查結果為零。 「零發現」是禁止的終止訊號,與階段四現有的最低發現數量保護機制一致,此機制的設立是因為對於一個非平凡的差異,近乎空白的審查結果代表審查過早結束,而非程式碼完美無瑕。一個以乾淨審查結果終止的循環只會可靠地產出乾淨的審查結果,而沒有其他價值(Meadows, Seeking the Wrong Goal; Leveson: 永遠不要獎勵低事故數量 — 獎勵報告行為本身)。
如果你在使用者工作分支以外的分支上執行,且未提供 --pr,則詢問使用者一次是否要針對特定PR號碼使用審查者模式,而非自行猜測 — 自動偵測可以省下按鍵次數,但錯誤分類模式會產生本應是本地建議的草稿評論,反之亦然。
使用時機
- 作者模式: QA通過後、將功能分支合併至 main 之前;Ralph完成AFK執行且你已驗證行為後;或任何希望在合併前進行審查的分支,即使沒有完整的管線執行。
- 審查者模式: 當團隊成員或外部貢獻者開啟PR,且你想對他們的差異套用
review-checklist.md中的完整架構審查,並產出具建設性的評論文字時。 - 循環模式: 當發現結果必須在產生它們的 session 之外留存時 — 例如來自
/execute的AFK交接,或任何你將分多次審查、且不希望每次重新推導發現結果的分支。當你付出的代價是遺漏追蹤發現結果,而非進行修復時使用它。
執行流程
階段一:收集背景資訊
作者模式:
-
詢問PRD議題編號。 如果此變更未經過完整管線,則接受「無」。在來自AFK
/execute交接的循環模式中,不要詢問 — 從交接中取得議題編號(/execute步驟六傳遞它),如果沒有,則視為「無」。此時沒有人可以回答,因此在此處提出無條件問題會使執行在第一步就卡住。在側車後端,答案是一個W-<n>工作項目ID(步驟二)。此處的「無」會使階段二沒有地方記錄PR,且審查註記和循環模式記錄簿沒有歸宿:參見 references/state-backend.md § 審查記錄的儲存位置以了解它們的處理方式。 -
如果提供了PRD:
gh issue view <number> gh issue list --search "in:body #<prd-number>" --state all --json number,title,state,body --limit 100從每個子任務議題主體中解析邊界對應(產出/消費區段)。
在側車後端(
bash "$SC" backend slices回答sidecar,根據 references/state-backend.md;以下區塊會詢問它,並在拒絕時停止,而非回退到gh issue),PRD及其子任務是工作項目,步驟一中詢問的編號是W-<n>ID:此分支實作的子任務,由/execute傳遞,或其PRD。不會執行任何gh issue命令;血緣關係和邊界對應來自工作項目:SC="<this skill's directory>/references/sidecar.sh" W="W-<n>" case "$(bash "$SC" backend slices)" in sidecar) ;; github) echo "backend is github: read the lineage with the issue block above"; exit 0 ;; *) echo "lineage aborted: sidecar.sh backend slices failed; not guessing the github path" >&2; exit 1 ;; esac J="$(bash "$SC" work view "$W" --json)" || { echo "lineage aborted: cannot read $W" >&2; exit 1; } case "$(jq -r '.kind // empty' <<<"$J")" in slice) PRD="$(jq -r '.parent // empty' <<<"$J")" ;; prd) PRD="$W" ;; *) echo "lineage aborted: $W has no kind" >&2; exit 1 ;; esac SLICES="$W" if [ -n "$PRD" ]; then bash "$SC" work view "$PRD" --json | jq -r '"# \(.id) (prd, \(.state))\n\(.body)"' SLICES="$(bash "$SC" work list --kind slice --parent "$PRD" --state all)" \ || { echo "lineage aborted: cannot list the slices of $PRD" >&2; exit 1; } fi while IFS= read -r S; do [ -n "$S" ] || continue bash "$SC" work view "$S" --json | jq -r '"# \(.id) (slice, \(.state))\n\(.body)"' done <<<"$SLICES"從那些主體中解析邊界對應,就像從議題主體中解析一樣。保留
$W:階段二會在其上記錄PR。 -
偵測基礎分支並評估差異:
SC="<this skill's directory>/references/sidecar.sh" # $BASE_REF 是範圍端點;$BASE_BRANCH 是 `--base` 所接受的名稱。 BASE_REF="$(bash "$SC" base-ref)" || { echo "base-ref failed: not guessing a base" >&2; exit 1; } BASE_BRANCH="$(bash "$SC" base-ref --name)" || { echo "base-ref --name failed: not guessing a base" >&2; exit 1; } git diff "$BASE_REF...HEAD" --stat git log --oneline "$BASE_REF..HEAD"base-ref在有宣告的側車base_ref時列印它,否則列印origin/HEAD預設值。在非零退出時,它不列印任何 ref:停止並轉發其 stderr,而不是猜測基礎。對於堆疊式PR子任務,此處設定的變數不會到達後續區塊:將兄弟子任務的分支名稱(PR將目標的上游)放入每個消費該變數的區塊(你的後端的階段二區塊和階段四的戳記區塊)的BASE_BRANCH=佔位符中。如果與基礎沒有差異,告知使用者沒有東西需要審查並停止。殘留:
$BASE_REF只有在上次git fetch之後才是最新的。當origin/<base>無法解析時,base-ref會針對本地分支進行測量並在 stderr 上說明;如果計數看起來錯誤,請執行git fetch並重新執行。 -
決定範圍 — 整個子任務,或戳記後的差異。 此決定僅適用於作者模式。循環模式會跳過它,並無條件地將
SCOPE_FROM設定為"$BASE_REF",每次執行都讀取整個分支,根據階段三的循環模式項目。首次審查讀取整個子任務:sidecar.sh range給出其左端點,即當步驟二命名的工作項目在此分支上仍有記錄的起始點時為該記錄的起始點,否則為$BASE_REF,因此在github後端上,子任務就是整個分支。針對已由本技能戳記、且其頭部後續有變動的PR重新執行 — 例如/fix-findings回合,或在故意刪除鎖定後的手動修復 — 會將戳記後的差異作為其主體。戳記存在於backend stamp所指定的位置,根據 references/state-backend.md:PR主體或側車。使用/closeout步驟二所使用的相同運算式讀取PR主體;scripts/test-review-currency-marker.sh確認兩者字節相同,而scripts/test-delta-review-scope.sh針對其命名的每個分支狀態、在兩個後端上針對 fixture 執行此區塊:SC="<this skill's directory>/references/sidecar.sh" case "$(bash "$SC" backend stamp)" in sidecar) REVIEWED_SHA=$(bash "$SC" stamp read "$(git branch --show-current)") ;; github) REVIEWED_SHA=$(gh pr list --head "$(git branch --show-current)" --json body -q '.[0].body // ""' \ | sed -n 's/.*<!-- reviewed-at: \([0-9a-f]\{40\}\) -->.*/\1/p' | tail -1) ;; *) echo "scope aborted: sidecar.sh backend stamp failed; not guessing the github path" >&2; exit 1 ;; esac W="<步驟二命名的 W-<n>,在 github 後端上為空>"; BASE_REF="$(bash "$SC" base-ref)" || { echo "scope aborted: sidecar.sh base-ref failed" >&2; exit 1; } RANGE="$(bash "$SC" range ${W:+"$W"})" || { echo "scope aborted: sidecar.sh range failed" >&2; exit 1; } SLICE_FROM="${RANGE%%..*}" SCOPE_FROM="$SLICE_FROM" if [ -n "$REVIEWED_SHA" ] \ && git merge-base --is-ancestor "$REVIEWED_SHA" HEAD 2>/dev/null \ && [ "$REVIEWED_SHA" != "$(git rev-parse HEAD)" ] \ && [ -z "$(git log --merges --format=%H "$REVIEWED_SHA..HEAD")" ]; then SCOPE_FROM="$REVIEWED_SHA" elif [ -n "$REVIEWED_SHA" ] \ && ! git merge-base --is-ancestor "$REVIEWED_SHA" HEAD 2>/dev/null \ && [ "$(git rev-parse -q --verify "$REVIEWED_SHA^{tree}")" = "$(git rev-parse 'HEAD^{tree}')" ]; then SCOPE_FROM="HEAD" fi if [ "$SCOPE_FROM" = HEAD ]; then echo "review scope: none, history-only rewrite — HEAD carries the tree reviewed at $REVIEWED_SHA" elif [ "$SCOPE_FROM" = "$BASE_REF" ]; then echo "review scope: whole branch, from $BASE_REF" elif [ "$SCOPE_FROM" = "$SLICE_FROM" ]; then echo "review scope: whole slice, from $SLICE_FROM ($W's recorded start)" else echo "review scope: post-stamp delta, from $SCOPE_FROM" fi git diff "$SCOPE_FROM...HEAD" --stat git log --oneline "$SCOPE_FROM..HEAD"四種狀態會回退到整個子任務,每種都是故意的:沒有戳記(首次審查或手動編寫的PR);戳記不是
HEAD的祖先且HEAD不再承載其目錄樹(分支被重寫且內容隨之改變,或審查過的提交已消失 —/closeout稱此為更強的分歧訊號,且此非較小的審查);戳記等於HEAD(沒有新的東西可以設定範圍);以及戳記之後的合併提交(戳記後的差異將包含基礎分支本身的變更,這些變更並非此分支上的任何人所寫)。整個子任務就是整個分支,除非range找到記錄的起始點,而當它找到一個因摺疊或變基而被重寫的起始點時,它會在 stderr 上說明並回答$BASE_REF,絕不重新錨定起始點。會列印此決定,以便終端機輸出和PR對已讀取的內容表述一致。保留了已審查目錄樹的重寫不是分歧。 當已戳記的提交不是
HEAD的祖先,但仍存在於本地且承載與HEAD相同的目錄樹時,歷史改變了但內容沒有:階段四交接提供的摺疊(git rebase -i --autosquash)正好產生這種情況。區塊會列印review scope: none並將SCOPE_FROM設定為"HEAD"。跳過階段三以及階段四中除了戳記(在HEAD重新戳記)和交接選單之外的所有內容。戳記認證了已審查的內容,而目錄樹雜湊正是該內容的精確表示。如果已戳記的提交在本地不可用,則無法比較目錄樹,並且重寫會採用上述的整個分支回退。在差異重新執行時,差異是主體,而目錄樹仍然可讀。 向每個階段三的審查者簡報戳記後提交聲稱要關閉的發現 — 此 session 中先前的輸出、如果循環模式寫入了記錄簿則有記錄簿(根據 references/state-backend.md § 審查記錄的儲存位置),或提交訊息本身 — 並附上
/fix-findings的判定結果(如果有的話),以及bash "$SC" profile的輸出(逐字逐句),這是階段三合約提供給每個審查者的內容。每個提交的第一個問題是其修復是否關閉了其發現在其命名的所有位置上的問題,這是「修復完整性」維度;第二個問題是修復是否引入了新的問題,這是「回歸」維度。
…