---
name: swe-knowledge
description: |
  軟體工程這一類工作「怎麼算 done」的通識：改動住在一條 branch 上、有一個 PR、判定過才
  進預設分支、push 之前本機跑完跑得動的驗證。由 driving-work-to-done 在判定一件工作會改到
  程式碼時載入。很少、扁平、不含任何一家公司或一個專案特有的東西。

  driving-work-to-done 判定這件工作會改到程式碼、要進版控時載入。

  不用於：不會產生程式碼變更的工作（報告、調查、文件、資料分析）——那些沒有這裡的
  完成條件，走 `--pack none`。

  不用於：某一家公司或某一個專案特有的規則（codecov 門檻、stage 部署流程、ticket 命名）。
  那些在各自的公司 pack 裡，見〈跟公司 pack 的關係〉。
metadata:
  version: 1.0.0
scope: universal
tools:
  - name: gh
    provision: manual
    why: 開 PR、讀 review、建 release、查 CI
    fix: 裝 GitHub CLI 並完成 `gh auth login`——二進位檔裝得起來，登入只有人做得到
---

# swe-knowledge — 軟體工程的 Definition of Done

這裡放的是**所有增量共用**的完成條件。跟每張單獨有的驗收條件（acceptance criteria）分得很
開：AC 寫在那張單凍結的 assertion 裡，DoD 寫在這裡。**一張單可以通過全部 AC 卻沒有 done**——
assertion 全綠但改動還躺在預設分支上、沒有任何人看得到它，就是那個情況。

所以這幾條不要抄進任何一張單的凍結區。抄進去等於每張單都重簽一次同樣幾行不承載新資訊的
東西，而漏抄的那一張就靜默地少了一條完成條件。

## 五條

1. **改動住在一條 branch 上，不躺在預設分支。** 開的時機是「判定要立案之後、動手之前」——
   一個還沒開工的成功定義直接躺在預設分支上，等於它已經是既成事實。

   **那條 branch 是自己建的。** 改動只推到自己建的 branch。別人建的、或有別人的 PR 在用的
   branch，任何情況都不推——推不推得動、用不用 force 都一樣。要在別人還沒併的 branch 之上
   做事，從它切一條自己的，PR 的 base 指向它。

   **所以別人的 branch 推不動的時候，那不是要解決的問題。** 它從一開始就不是推的對象，
   沒有「換個方法推上去」這一題。2026-09-10 的實例：一顆 PR 的 base 是同事還沒併的
   branch，遠端規則讓兩條都推不動，討論裡出現了「那條也一樣推不動」——那條本來就不是我們
   會去推的東西。
2. **有一個 PR。** **PR 開出來就是實作完成**：它是那份改動變成可被別人看見、可被判定的
   東西的那一刻。沒有 PR 的改動不管本機多綠都還沒 done。

   **開之前，在改動落下去的那棵樹上跑過 `check-your-own-work`，它列出的每一項都處置完。**
   這是這一條的一部分，不是開完之後的選配：review 抓到的問題，多半是作者自己對一次就看得到
   的那幾類。2026-09-17～10-01 兩週裡，有人 review 的 PR 只有一半一次過，被抓到的五類問題
   有四類那支 skill 本來就在問，而開 PR 的那些 session 幾乎沒有一個跑過它。怎麼跑、為什麼要
   指名那棵樹，在〈開 PR〉。

   **一個功能跨好幾個 repo 的時候，只做了一半的那顆 PR 開 draft，描述寫出它依賴哪一顆。**
   單獨看它是綠的，併進去卻不成立；全部做完、端到端在本機量過真值，才轉成可以 review。
   2026-09-24 的標本：讀取那一端的 PR 回了一個新欄位，寫入那一端還沒把它寫進索引，前者單獨
   上線之後那個欄位全部是 null。
3. **判定過才進預設分支。** 進去的路徑是那個 PR，不是直接推。

   **這一條有東西在守**（DP-865）：`scripts/check-ship-point.sh` 在併 PR、建或推 release tag、送
   部署請求之前，問那個動作對到的本地單判過了沒——站別要是 `delivered`，併的時候 head 還要是
   交付紀錄記的那一個。對不到任何本地單的不擋：別人的 PR、別人的發佈不是這一條管的。開 PR
   不擋，PR 開出來是實作完成，判定在它之後。任何 agent 都能從命令列叫它；Claude Code 的
   `.claude/hooks/gate-ship-point.sh` 只是轉呼叫它。

   **只上 stage 的 tag 不是進預設分支的出貨**（DP-907）。上 prod 之前要先在 stage 驗過，而
   stage 驗收在判定之前，所以那種 tag 站別到 `engineering` 就放行。認法看名字：有一段是
   `beta` 或 `rc`（前面是開頭或 `_`、`.`、`-`，後面是結尾、分隔字元或數字），例如
   `searches_beta_3`、`v3.2.26_rc`、`v1.0.0-rc.1`。`searches_v3.2.26` 這種正式 tag 照舊要
   `delivered`。一次推好幾個 tag 而有一個是正式的，或叫它的人沒給 tag 名（`--tag`），都照
   正式的判。

   **命名空間自己的梯子把出貨動作排在判定之前時，由它宣告**（DP-908）。例如上 stage 的部署、
   併進整合分支：那個環境的驗收要先發生，單才走得到 `delivered`，只認 `delivered` 的閘會跟它
   互等。宣告的形狀跟收斂條件同一套：

   ```
   <!-- {任意前綴}-SHIP-POINT-CONDITION-{命名空間}: {命令} -->
   ```

   對到的單站別還不夠時，出貨點在 repo 根跑那條命令，後面接單的目錄與 `--action`、`--branch`、
   `--base`、`--repo`、`--tag`、`--head`、`--text-file`。回 0 就放行那一張，非 0 照舊擋，並帶上
   命令自己印的缺口。沒宣告的命名空間照舊。
4. **push 之前，本機跑完跑得動的驗證。** type check、lint、單元測試、受影響路徑的冒煙。
   把 CI 當第一道防線等於把 reviewer 當驗證工具。
5. **reviewer 提的每一條，處置回到那條意見上。** 不是「有沒有處理」——是**提出者拿不拿
   得到那個處置**。他看的是他留言的地方，回在別處他收不到。

   **處置有四種：照做、回覆說明為什麼不照做、拒絕、另外開單。照做不是預設。**「處置」這個
   字是中性的，而它常被讀成「照做」。一條意見可以是對的、而仍然不該在這一輪做；也可以是
   這個 repo 根本不做那件事。讀成「照做」的話，這一條會把每一則意見都變成一次施工。

   **每一串都算，nit 也算。** 「妥善處理」的量法是：每一串的最後一則是作者，而且那一則說出了
   四種處置裡的哪一種。標成 nit 的意見只是提出者在說「這條不擋你」，不是在說「這條不用回」。

   **回覆承認對方說得對、卻沒有改，不算四種處置的任何一種。** 那是第五種，而它讀起來像照做。
   回覆與 merge 之間也要留給提出者重看的機會，不在同一分鐘內回完就合併。

   **照做的那一次推進，PR 描述跟著對齊**（見〈開 PR〉尾端）。修正改到的行為若還寫著舊的，
   那一條意見在程式碼裡處置了，在描述裡沒有。

第 2 條有一個這個 repo 自己的教訓：2026-08-03，「開 PR」這個能力在腳本歸位時被刪掉，因為
它在三站裡沒有主人；43 分鐘後有人寫了一句跟腳本矛盾的散文把洞蓋住，於是「PR 算不算完成」
有兩個互相矛盾的答案在流通。現在它有主人了，就是這一條。

第 5 條是同一個形狀又發生了一次。2026-08-11：一個 PR 收到三位 reviewer 的意見，全部查證、
修掉、assertion 重跑全綠，也回了另一個系統的訊息串——**但那個 PR 上一則回覆都沒有**，是人自己
發現的（「我好像沒有在 PR 上看到你怎麼處理的」）。偵測有主人（誰欠我回覆、誰欠我審查，
都有東西在算），**動作沒有主人**。而「還沒被 approve」這個狀態，跟「我還沒動手」長得
一模一樣。

## 在程式碼裡，「你寫下的話」長成什麼樣子

`engineering` 那條「送審之前把自己寫下的話跟行為對一遍」在這一類工作裡有具體形狀。這幾樣
都是對行為的主張，都會跟實作分開演化，而編譯器與測試都不會抓：

- **doc-comment 的第一句**——通常是最早寫下的，也最可能是舊設計的化石。
- **型別宣告**（參數型別、回傳型別、介面欄位）——宣告成數字就不能送出字串。它是契約，
  不是提示；改了寫入路徑要回頭看宣告。
- **名字**——一個叫「處理中」的狀態要真的在事情處理中的時候是真的。
- **形狀**——把等待用的結構套在一個不會等待的呼叫上（例如包住一個同步呼叫），會讓一段
  沒有等待的程式碼長得像在等待，而讀的人依樣相信它。

第 4 條那句「本機跑完跑得動的驗證」的另一半就是這個：**跑得動的用跑的，跑不動的用讀的。**
一句說謊的 doc-comment 跑不出紅燈，它只會在下一個人依它行事的時候生效。

所以寫的時候就要讓它值得被讀：

- **新增或修改的 function 要有 doc-comment**（TSDoc / JSDoc / Google-style docstring /
  shell function header），說明它做什麼、參數與回傳值是什麼。既有沒動的不強制回填——上面
  那句「最可能是舊設計的化石」講的就是它們，補一句猜出來的比空著糟。前因後果同樣不寫進
  doc-comment，理由見下面那一條。
- **帶業務意義的 literal 抽成有名字的常數**，名字說不完的在宣告處補一行。名字也是一句
  對行為的主張，同一條規矩適用。純視覺數值（padding、gap）與 `aria-label` 這類不強制。
- **Inline comment 只在「為什麼」不自明時寫，一短行。** 逐行解說「做什麼」是會說謊的
  雜訊：它貼著那一行，那一行改了它不會跟著改。那一行只寫 code 讀不出來的那一件事，
  「為什麼」用單號指回去，例如 `// 不帶 lastmod，見 <單號>`。前因後果——經過、上游欄位
  反映什麼、別處怎麼做——留在單與 PR 描述，不進 code。

  理由有兩個。前因後果會過期：那張單關了、上游欄位拿掉了，註解沒有人回來改，而過期的
  前因後果讀起來跟真的一樣。它本來也有家：單與 PR 描述記的就是這件事，而且帶著日期。

  這一條是通識，不是某一家公司的規矩。哪一個 repo 都有單或 PR 可以指回去，而註解會過期
  這件事跟是哪一家公司無關。

## 一個輸出寫得下幾種情況

上一節問的是「寫下的話跟行為對不對得上」。這一節問另一半：**寫下的話的詞彙夠不夠用。**

離場碼、給人看的訊息、記在檔案裡的欄位、報告的一行——每一種都是一組有限的值，而會走到
那一行的情況常常比那組值多。**多出來的那一種不會消失，它會被折進寫得下的那幾種。**

被折掉的幾乎總是同一種：**這一趟沒量到、問不到、走不下去。** 它跟「量到了，好的」與
「量到了，壞的」是三件事，而多數輸出只有兩格。

**折向哪一邊不是設計選擇，是意外**，由旁邊那幾行剛好怎麼寫決定：上一趟的結果檔還在，
於是比對照樣跑完，折成**通過**；一句 `|| die` 把「工具不存在」的 127 接住，折成**失敗**；
查詢那條路在印任何東西之前就 `exit 1`，折成**什麼都沒有**。

三種都不是真話，但壽命不一樣。**折向通過的沒有人會抱怨**，它活到有人回頭核對為止；
**折向失敗的會有人立刻去查，而他查的是錯的東西**——訊息說這份交付壞了，真相在環境。

所以動手之前問一次：

> **會走到這一行的情況有幾種？這一行寫得下幾種？**

一樣就沒事。不一樣的時候，差額已經在那裡了，只是還沒有人撞到。修法通常很小：多一個離場
碼、多一句話說出「這一趟沒量到」，或者在折進去之前先把原因印出來。

這份知識裡好幾處各自寫過它的一半——**問不到不得當成沒問題**。那些是這一句的實例，各自
貼著自己那一支腳本；反過來那一半同樣算數：**問不到也不得當成你錯了。**

**這件事擋不住。** 一道檢查得先知道「這一行本來應該寫得下幾種」，而那正是上面那個問題
要回答的東西——它只能在寫的時候被問一次。

## 寫腳本的時候

**語言選擇**：串工具、檔案操作、簡單 control flow → bash。結構化資料、複雜 regex、需要
在記憶體裡建資料結構 → python。只有裝了套件才跑得動 → 那個套件生態的語言。純檔案掃描不要
拉 Node，純 bash 能解的不要拉 Python——啟動成本會在整批一起跑的時候累積起來。

**關卡門與對外寫入之間要有 fail-stop 邊界**：`set -euo pipefail`，或者把檢查與寫入拆成兩次
呼叫，寫入只在檢查回 0 之後跑。一個檢查失敗了還繼續往下寫的腳本，比沒有那個檢查糟——它
會印出一行綠的。

## 開工前要成立的條件

第 1 條有一個時機問題：它必須在**動手之前**成立，事後才發現改動躺在預設分支上，那些
commit 已經在那裡了。所以它不是驗收時才量的東西，是開輪次那一刻的前置條件。

下面這一行是機器讀的。核心不認得「branch」是什麼——它只知道去找這一行、跑它指名的命令、
非 0 就拒絕開工。條件寫在這裡，不寫在核心裡：

<!-- SWE-PRECONDITION: bash .claude/skills/swe-knowledge/scripts/check-swe-precondition.sh -->

**有幾個落腳處就判幾次。** 核心把這張單開輪次時宣告的那幾個地方原樣接在上面那行命令後面
（跟下一節的身分那一行同一條路），所以這支腳本判的是**這張單的改動會落在哪**，不是「跑這
個命令的人現在站在哪」。全部成立才成立：放行一個「三個地方裡有兩個對」的開工，等於第三個
地方的改動從第一個 commit 起就沒有被任何條件管過。

一個地方都沒被指名時它回非 0——那是「量不到」，不是「通過」。2026-08-08 之前核心跑這一行
不帶參數，於是一張改三個產品 repo 的單，開工條件是拿 workspace 自己的分支去判的；那個判定
跟改動落在哪完全無關，綠或紅都不代表任何事（同事在一張跨 repo 的單上撞到）。

2026-08-03 那次，三張單的 commit 全部混在預設分支上，而寫下這條規矩的 commit 本身也在
預設分支上——規矩在寫下的一小時內失效四次。靠人記得的規則的失效率就是那個樣子，所以
它現在有一道關卡。

### 第二條：那個工作區的 hook 真的被 git 在用

第 4 條「push 之前本機跑完跑得動的驗證」在很多 repo 裡是掛在 git hook 上的，而 hook 有一個
只有開工前擋得住的失效方式：**沒接上的 checkout，它的每個 commit 與每次 push 一道關卡都不會
跑，而且不會有任何東西說。** 發現的時候那些 commit 已經在歷史裡了。

所以開工條件多問一句。**它不認得任何一個 hook 目錄的位置，也不認得誰負責裝它**——它掃
宣告：

```
<!-- {任意前綴}-GIT-HOOKS: {相對於 repo 根的路徑} | {接上它的命令} -->
```

**掃兩棵 skill 樹**：那個工作區底下的 `.claude/skills`，以及使用者家目錄的
`~/.claude/skills`。個人的 skill 住在後面那一棵，而它們一樣宣告得出東西。

**為什麼修法是多掃一棵樹，不是把宣告搬進工作區**：一支宣告自己是個人的 skill，按分類規矩
**本來就不會**出現在工作區那一棵樹裡。這個框架自己的 workspace 就是這樣——那份唯一真的
GIT-HOOKS 宣告住在一支個人的 skill 裡，而個人的東西不進公司 repo。搬過去等於違反那條規矩，
所以只掃工作區那一棵的掃描器，對這一類宣告是**結構上**看不見，不是這一次剛好不在（DP-627）。

**一份都沒找到時它說的是「這一次沒有問到」，而且說出掃了哪裡**——不是「這一條不適用」。
兩句話對一個真的接上了 hook 的工作區給出相反的意思，而分辨得出來的只有讀到那句話的人。

**一份宣告適不適用於這個工作區，看它指的目錄在不在這裡。** 那是事實，不是推測：宣告裡的
路徑相對於 repo 根，而家目錄那一棵 skill 樹被這台機器上每一個工作區共用，所以住在那裡的
一份宣告不可能是「每一個 repo 都要有這個目錄」的意思。**但住在工作區自己那一棵樹裡的宣告
不一樣**：它只可能在講這個工作區，所以它指向空氣的時候仍然判紅。

**一段講「宣告長什麼樣」的文字不是一份宣告。** 分開它們的是位置：宣告是散文裡的一行 HTML
註解，格式說明住在 fenced code block 裡，掃描器跳過 code block。以前分開它們的是**佔位符
剛好用了哪個字元**——上面那個 `{任意前綴}` 的 `{` 不在 `[A-Za-z0-9_-]` 裡，所以正則對不上
它；把佔位符換成一個 ASCII 前綴，這段格式說明就會被當成一份活的宣告撿走。一個沒有人說出口
的分界，下一次換個寫法就沒了。

宣告了而且適用，就要三件都成立：目錄存在、裡面的檔案都有執行位元（沒有的話 git 是**安靜地**
跳過）、而且 `core.hooksPath` 真的指著它。問不出 `core.hooksPath` 的時候回「量不到」並拒絕，
不是放行——問不到關卡的狀態跟關卡是綠的，是兩件事。

**別的 repo 的 hook 把你擋下來時，不跳過它。** 先分清楚那是紅（你的產出不合它的規矩）還是
量不到（它自己跑不起來）。紅就改產出；改不了就停下來回報，不用 `--no-verify`。原因看起來跟這次
的改動無關時，先查本機的 git 設定——那種紅常常不是 repo 造成的。

## 施工期間：這個工作區還是不是原來那個

**動一個共用的 checkout 之前，先問它現在站在誰的分支上**（`git branch --show-current`）。不是
自己的就開一棵 worktree，不在別人的分支上 commit。

開工條件是一次性的事件，它問「有沒有站在預設分支上」——那是一個對所有單都一樣的判準。
切到**另一張單**的分支照樣通過它，而那正是真的會發生的事故：兩個 session 互搶同一個
checkout，commit 落到別人的 feature 分支上。

所以除了「開工前成立什麼」，還要有「這張單當初落在哪」。下面這一行也是機器讀的，它印出
的每一行核心當作**不透明的識別**存起來，之後任何一刻拿現況比對。核心不知道那是分支名，
比對相不相等不需要知道：

<!-- SWE-WORKSPACE-IDENTITY: bash .claude/skills/swe-knowledge/scripts/workspace-identity.sh -->

**印幾行就是幾個地方。** 一件工作牽涉多個 repo 是常態，所以給幾個路徑就印幾行，每一行是
`<repo 目錄名>:<分支名>`——帶目錄名是因為兩個 repo 上同名的分支（`main`）到處都是，只印
分支名會讓兩個地方塌成一個。

**要量哪些地方是被告知的，不是猜的。** 核心把這張單開輪次時宣告的落腳處原樣接在上面那行
命令後面，所以這支腳本不看自己站在哪裡；一個地方都沒被指名就回非 0，不印任何東西。
DP-482 之前它沒拿到參數就量 `pwd`，於是一張「單住在 A、程式碼落在 B」的單記下的是 A——
之後每一次比對都拿 A 跟 A 比，永遠自洽，而 B 被別的 session 切走時完全安靜。

求不出來（detached HEAD、不在 repo 裡）一律回非 0 而且**一行都不印**，核心會說「量不到」
而不是「一致」。印一半比不印糟：那會記下一個比實際涵蓋範圍小的集合，而它之後每次比對都
自洽。

## 動手之前：先讀那個 repo 自己的規矩

**改動落在的每個 repo，根目錄的 `CLAUDE.md`、`AGENTS.md` 與 `.claude/rules/` 是那個 repo 的
權威。** 動手之前三份全部讀完，不只讀改到的那個面向——規定每顆 PR 要帶什麼、哪些檔案不能動的
那一條，常常不在你改的那一區。

**跟框架衝突時以 repo 為準，並把衝突回報給派工的人。** 框架講的是所有 repo 共用的東西，repo
講的是它自己；兩者說法不同，通常是框架不知道那個 repo 的事。照 repo 做，回報時說出是哪一條、
框架那一條在哪。

**三份都不存在時，說出「這個 repo 沒有自己的規矩檔」，然後照常工作。** 沒找到跟讀過是兩件事：
不說出來的話，讀報告的人會以為那一步做了、而且沒有東西要遵守。

改動落在另一棵工作樹時，讀的是那棵樹根目錄的那三份，不是 session 站的那一棵。理由跟〈開 PR〉
那一段同一個：站的地方不一定是改動落下去的地方。

## 改動的原則

這裡不另外發明原則，用外面已經在用的：[SOLID](https://en.wikipedia.org/wiki/SOLID)、
[YAGNI](https://martinfowler.com/bliki/Yagni.html)、[KISS](https://en.wikipedia.org/wiki/KISS_principle)、
[DRY](https://en.wikipedia.org/wiki/Don%27t_repeat_yourself)、
[boy scout rule](https://www.oreilly.com/library/view/97-things-every/9780596809515/ch08.html)。
下面三條是它們落在一次改動上的樣子。

**不留 legacy。** 一張單換掉做法之後，被換掉那版的實作在同一顆 PR 全部拿掉。只為舊做法存在的
東西每一類都算：程式、設定與 toggle、env 變數、DB 欄位（drop 的 migration 進同一顆）、Swagger
與 API 說明、測試。下面四種說法都不是留下來的理由：

- 「留著不動」：沒人用的程式留在 repo 裡，下一個讀的人要先查它還有沒有人用。toggle 留著，就會有人打開它。
- 「之後的班再清」：排到之後的班是在估時程，那不是這一層能判的事，見本節最後一段。
- 「縮小這一顆的 diff」：拿掉舊實作是這次改動的一部分。
- 「drop 要 migration」：migration 是拿掉的一部分，部署說明寫一句要跑 migration。

判準是新做法還讀不讀它，不看它是不是舊那版加的。新做法仍在輸出、仍有消費者在讀的留著，別的 repo
在讀也算。動手之前逐項列「拿掉／保留＋理由」，保留的每一項指名誰在讀。拿掉的東西原本做的事由誰做，
用 `check-your-own-work` 第七問對一次。

**盡量復用。** 要做的事講得出名字，先在這個專案裡找，見下一節〈動手刻之前〉。

**做減法。** 一個問題先問拿掉什麼就夠，再問要加什麼。加的機制只做被要求的那件事。碰到的那一段
離開時比來的時候乾淨，範圍限於這次碰到的地方。

**LLM 只判爆炸半徑，不判改動範圍大小與實作時間。** 爆炸半徑是這次改動壞了會波及誰：哪些消費者、
哪些環境、壞了能不能 rollback。「hotfix 要小所以留著」「這顆先做一半」「下一班再做」都是在判
範圍與時程，那是使用者的決定。要壓風險就把爆炸半徑量出來，寫進 PR 描述的限制段，不靠少拿掉東西。
使用者原話：「LLM 不要去判斷改動範圍，實作時間，只判斷爆炸半徑就好」。

2026-10-27 的標本：一顆 PR 把鎖改成當場算之後，裁決寫的是「toggle 與批次鎖 API 留著不動、之後的班
再清」，DB 欄位的 drop 也排到之後的班。使用者原話：「這修沒用到，且是〔那個 epic〕過去的錯誤實作，不是應該拿掉嗎？你為什麼會做出 “留一個沒用到的環境變數” 這種 code」。
最後全部拿掉，drop 的 migration 進同一顆。

## 動手刻之前：這件事這裡大概已經有人做過

`engineering` 列的三件要浮出來的事，第二件是「重造既有組件」。那條規矩在軟體工程現場最
常見的失效方式不是有人明知故犯，是**判準被讀成「重寫一個大東西」**——手刻一小段控制流在
寫的當下感覺像在寫 code，不像在造組件，於是什麼都不會觸發。

**判準是行為重疊，不是行數。** 八行也算。想做的事如果講得出名字，那個名字通常已經在這個
專案裡了，動手之前先找一次。

這幾種形狀特別容易被重造，因為每一個都小到不值得停下來想：

- 等上層或另一個元件把一件事做完
- 防抖、節流
- 輪詢、重試、退避
- 開了再關的載入／禁用狀態
- 分頁、無限捲動
- 表單驗證與錯誤訊息
- 彈窗的開關與生命週期

先找一次的成本是一次搜尋；沒找的成本是一個測試照不到的缺陷。

**標本 2026-08-07**：一顆按鈕要在上層做完之前轉圈，於是手刻了一段 `try/finally` 包住一個
同步的事件送出——狀態開了又關，中間沒有任何會離開這個 tick 的點，按鈕從來沒有轉過。那個
專案早就有一支八行的共用函式在解「把事件丟出去、等對方做完」這件事；掃過去，正確用法
18 處，手刻的那個形狀 0 處。八行對八行，而且沒有任何東西會紅——測試量的是「按下去有沒有
丟出事件」，加不加那層殼都一樣綠。最後是人在審查時抓到的。

## 量它

```bash
bash .claude/skills/swe-knowledge/scripts/check-swe-precondition.sh --repo <path>   # 開工前
bash .claude/skills/swe-knowledge/scripts/workspace-identity.sh --repo <path>       # 施工期間比對用
bash .claude/skills/swe-knowledge/scripts/check-swe-done.sh --repo <path>           # 交付前
```

它查四件事：現在不站在預設分支上、這條 branch 有 open 的 PR、工作區是乾淨的、以及那個 PR
上 reviewer 提的意見都有處置回到那條意見上。查不到的（沒裝 `gh`、沒有 remote、遠端回非 0）
**會說出來並回非 0**，不會當成通過——量不到跟量到「沒問題」是兩件事。第 4 條它不查，那要
看是哪個專案的哪一套指令，屬公司 pack。

第 5 條算的是「串頭有沒有被作者接過話」：一串意見的所有回覆都指向那一串的第一則，所以問的
是「有沒有一則回覆的作者是 PR 作者」。**另一位 reviewer 在同一串裡接話不算**——把「這串有
人回過」當成成立的那一版，會讓兩個 reviewer 互相討論就把作者放行。

**宣告一支腳本「回 0 筆」之前，先看它輸出了幾個位元組，並確認輸出是什麼格式。** 拿 JSON 解析器
去讀一份純文字輸出，那個解析例外不是空集合。2026-09-17 的標本：一個解析例外被讀成「0 筆」，
接著開了一張不存在的單，還替它編了一個模式。

## 本機驗過，是走完整條請求鏈

第 4 條「本機跑完跑得動的驗證」對三種改動有一個下限：**新 route、新寫入點、新中介層行為**。
這三種改動，交付前至少一次用真實狀態的資料走完整條請求鏈。跨 repo 就本機起兩邊。
**只有 Feature／Unit 測試綠不算本機驗過。**

真實狀態指那份資料在線上會長的樣子：有登入、有權限、被既有中介層看過。手寫的測資與 mock
掉的那一層不算，理由跟〈注一刀〉最後一段相同：測資自帶答案的時候，取值那條路一行都沒有被走到。

2026-10-27 的標本：一個新寫入點的 Feature 與 Unit 測試本機綠了兩次。正式環境 14 天 27,752 筆
請求全部回 403——那條 route 前面有一層既有的驗證中介層，六年來一直擋著這一類請求，而測試沒有
經過它。部測試環境之前查 log 才發現。使用者原話：「我以為開發 local 要成功是基本的，不成功怎麼上 sit？」

## 推到開著的 PR：派一個看不到施工對話的驗證者

「本機驗過」由施工的那一條線自己說，就跟它自己挑的量測命令一樣，沒有第二雙眼睛。所以每次推到
一顆開著的 PR，由 git 的 pre-push hook 在背景派一個驗證者：

```bash
bash .claude/skills/swe-knowledge/scripts/push-verifier.sh install --repo <checkout> --issues <單的根目錄> [--agent-cmd '<命令>']
bash .claude/skills/swe-knowledge/scripts/push-verifier.sh status  --repo <checkout>
bash .claude/skills/swe-knowledge/scripts/push-verifier.sh remove  --repo <checkout>
```

- **觸發點是 git hook，不是哪一個 agent 的 hook**，所以誰推都會觸發。啟動命令預設 Claude Code，
  `--agent-cmd 'codex exec'` 這一類換掉它，prompt 接在最後一個參數。
- **不取代既有的 hook。** 接上之後每一支 hook 先照樣跑原本那一支，輸出與 exit code 原樣透傳，
  只有 pre-push 成功時才派。不改 repo 裡被追蹤的檔，`remove` 還原原本的 `core.hooksPath`。
  指向轉呼叫的那一行經 `.git/config` 尾端的 `[include]` 帶進來，所以 husky 在 `npm install` 時
  把 `core.hooksPath` 寫回 `[core]`，轉呼叫照樣在，原本的 hook 跟著那一格轉。
- **它不擋 push，也不等。** 判有沒有 open PR 用的是推到遠端的那個分支名，`HEAD:<branch>` 也認得。
  刪分支、推 tag 不派，各說一行；推沒有 open PR 的分支不派，也不說；問不到有沒有 PR 時說一行，照樣不派。
- **驗證者看到的**只有凍結塊、那個 head 相對 base 的 diff、repo 的規矩檔、本機環境的宣告，在自己
  的 worktree 裡。它跑 `check-your-own-work` 九問，並照上一節走請求鏈。
- **判定寫回那張單**：一段行首 `check-your-own-work 九問，head <head>（驗證者）` 的紀錄，加
  `.spine/independent/<head>.json`。紅了那張單回 `engineering`；起不起來、拿不到資料、鏈走不完的
  寫「跑不到」與缺什麼，不判綠。它不擋 merge，出貨點不讀它。
- **進度看 log**：驗證者每做完一問或一條鏈就 append 一行到 `.spine/independent/<head 前 12 碼>.log`，
  跑的期間 `tail -f` 那個檔就看得到走到哪。一趟有 3600 秒的時限；逾時被切掉時，逾時前做完的鏈與
  finding 照樣寫回，判定記成 `PARTIAL`（單上寫「沒跑完」），逾時前一個字都沒寫出來才是「跑不到」。

**擋不到的，明講。** `git push --no-verify` 跳過所有 hook。有人把那一行 `[include]` 拿掉，或在它
後面再設一次 `core.hooksPath`，轉呼叫就不在了，`status` 會說出來。驗證者碰得到什麼只寫在
指示裡：預設命令不載 MCP、不讀個人設定，但叫得到 Bash——那是一份會被自願遵守的界線，不是一道閘。
每推一次多一個 agent 的 token 與時間。

## 注一刀進去驗那條斷言真的會紅

**要證明一條斷言真的在守，就要讓它紅一次。** 而最直覺的注入方式——真的 commit 一顆帶著
問題的 commit——會撞上 commit 前的那道檢查，於是「繞過去」變成路上唯一看得見的那一步。
**它不是唯一的一步。** 這一節只回答一件事：不繞，怎麼做得到。**一次紅控要成立需要什麼**
是另一族問題，不在這裡——那一族的每一條各自已經有家，這裡不搬也不重講。

**不要注在共用的那棵樹上，而理由是後果不是紀律。** 那一刀讓那道檢查暫時壞掉幾秒，而那
幾秒裡別的人可能正在跑自己的交付。**注在共用樹上的紅控會擋掉別人的交付，而那個擋看起來
會像他們自己的實作有問題**——他們看到的是自己被擋下來，於是去查自己剛寫的東西，而正確的
下一步是等隔壁那一刀還原。同一個顯示對得上兩種真相，而兩邊的下一步相反。「開一棵新的
比較乾淨」不是理由，這才是。

**兩種拋棄式的做法，各自換到什麼：**

| 做法 | 換到什麼 |
|---|---|
| 一棵拋棄式的工作樹 | 便宜、幾秒就開得起來，但它跟原本那棵**共用同一份 git 目錄** |
| 一棵完整複製出來的樹 | 完全隔離，代價是**只在本機、沒有版控的設定不會跟過去**——依賴那種設定的掃描類檢查會回「量不到」，而那個紅不是那一刀造成的 |

**所以在一棵全新的樹上，正向控制多了第二個用途。** 它原本回答的是「這條斷言不是恆紅」；
在新樹上它還回答**這個紅是環境造成的，還是那一刀造成的**——做法是拿掉注入、改動一個無關
的地方，再跑一次。同樣紅就是環境。少了這一步，一棵新樹自己的紅會被讀成注入的成果，
而結論剛好反過來。

**反過來也一樣：把一個紅歸因成環境，要兩個條件都成立。** 一是換過執行位置（另一個工作目錄、
另一棵樹）再跑一次，還是同樣紅；二是紅在載入或解析那一層（模組找不到、設定讀不到、路徑
解析失敗）。斷言層的失敗（`AssertionError`、期望值與實際值對不上）一律是真的紅，不歸給
環境。2026-09-18 的標本：一組測試被判成「環境問題跑不起來」，換到那個套件自己的目錄再跑，
跑得起來，而且其中一條是斷言失敗。

**一刀連帶紅了兩條斷言時，不要為了讓它只紅一條而去挑刀。** 那通常是它們真的耦合：其中
一條吃的幾種形狀裡，有一部分走的是另一條的路。挑開之後那兩條看起來互相獨立，而它們不是。
這跟「一刀紅了整批、看不出哪一條在守」是相鄰的兩件事，方向相反——收窄過頭跟放寬過頭
各有一個坑。

**還原要跟注入同粒度。** 量完把那一刀拿掉，拿掉的範圍要剛好是那一刀——不是把那個檔案
整個還原，那會連帶丟掉同一個檔裡還沒送出去的東西。

**所以順序是：先把這一輪的實作 commit 掉，再注那一刀。** 反過來做的話，還原那一步會連
同還沒 commit 的實作一起丟掉——而那不是一個會報錯的動作，它成功地把東西刪乾淨。

**手寫的測資或 stub 自己帶著要驗的那個欄位時，取值那條 code 一行都沒有被走到。** 斷言是
綠的，但它驗的是測資，不是程式。所以至少一條量測要走真的取值路徑，stub 查不到就回空；出貨
之前把新加的資料來源拔掉，確認那條斷言會紅。2026-09-22 的標本：測資自帶答案讓一個造出來的
PASS 過關，線上那條取值的 code 從沒被量過。

## 回覆 reviewer

**三步，順序不得顛倒**：落地成檔案 → 過對外寫入的檢查 → 才送出去。送出去是不可逆的，
而一段沒有被任何東西看過的文字送到別人面前，收不回來。

**第一步，把回覆寫成一個檔案。** 直接把文字塞進命令列的那一種，沒有任何東西看得到它，
事後也指不出送了什麼。

**第二步，過那道檢查。** 它**不住在這裡**，由認領它的那支 skill 自己宣告：

```bash
bash .claude/skills/swe-knowledge/scripts/resolve-external-write-gate.sh
```

它回一行命令；**沒有人宣告的時候它說出來並回非 0**——那個離場碼的意思是「這個環境沒有那
一層，落地成檔案這件事你自己要做」，不是「不用檢查了」。這一支 skill 會被單獨匯進別人的
環境，而寫死一條指向別支 skill 的路徑，在那些環境裡是一個安靜讀不到的東西。

**第三步才是送。** 一行，跟〈開 PR〉同一個待遇：

```bash
gh api --method POST "repos/<owner>/<name>/pulls/<n>/comments/<comment_id>/replies" -F body=@<file>
```

**這裡不帶包住那個 API 呼叫的腳本**，理由跟〈開 PR〉那一段一模一樣。這份知識裡往別人看得到
的地方送東西的動作只有兩個：這一個，與〈開 PR〉裡改描述的那一個，兩個都走這三步——多開
第三個，這三步就會有一條繞得過去的路。

**對一顆正在收票的 PR 推任何 commit 之前，當下重查 head 上的有效核准。** 空 commit、修一個
nit 都算——任何一推都會讓已經投的票失效。已經達到門檻就不推，nit 的回覆留到下一顆處理。CI
因為基礎設施逾時變紅、而 code 沒有問題時，先請有權限的人 restart；空 commit 只在零票時當最後
手段。2026-10-01 的標本：兩顆 PR 各被一次推送推掉兩票，一顆是空 commit，一顆是修 nit。

## 開 PR

**開之前，先在改動真的落下去的那棵樹上跑一次 `check-your-own-work`**——這是〈五條〉第 2 條
的完成條件，這裡講怎麼跑。指名那棵樹（它的蒐集腳本收 `--repo`），並在同一輪把它列出的東西
處置完，再開 PR。

要指名，是因為 session 站的地方不一定是那棵樹：改動在另一個工作樹、另一個 repo 的時候，
那個 repo 自己寫下的規範不保證在手上。而 reviewer 最常引用的，正是 repo 自己的規範檔。

2026-09-29 的標本：一顆 PR 沒帶那個 repo 規定每顆 PR 都要有的一份檔案。那條規則在三個地方
都寫了：repo 的說明檔、repo 的規則目錄、以及那家公司的知識。施工在另一棵工作樹，session 站
在別的工作區，流程裡沒有一步去讀它們。必過的檢查紅了，補上之後，已經拿到的核准全部作廢，
要重新找人看。

```bash
gh pr create --repo <owner/name> --base <base> --head <branch> --title '<一行>' --body '<內容>'
```

命令就這一行。這裡不帶自製的 PR 建立腳本：上一支長到 866 行，帶著 7 支 selftest，最後在
一次搬家裡整支消失而沒有人發現。

**描述只寫這顆 PR 做了什麼**：改了什麼、併掉之後的行為、代價或限制。看 PR 的人要知道併掉
之後會發生什麼，不需要知道這個決定翻過幾次。**限制那一段包含同型處清單**：同一個 bug 在這顆
PR 的範圍裡還有哪幾處、每一處改或不改、不改的理由（`check-your-own-work` 第三問的三種之一）；
範圍以外的合成一段，指向母單上那則留言。

**決策經過不寫進描述**：誰說了什麼、哪顆 PR 撤了、為什麼重開，這些留在單上。寫進描述的話，
review 的人會回頭去找那段脈絡。引別的 PR 只在併掉之後跟它有關係的時候（stack、依賴），被撤
掉的那幾顆不引。

**之後每一次推進這顆 PR，描述在同一個動作裡對齊實作。** 回 review 的修正、補測試、換做法都算：
推上去的那一刻，描述裡寫的行為要還是真的。描述是對外的，而 diff 看不出它過期了——reviewer 讀
到的是舊的承諾，卻以為那是現在的行為。2026-09-29 的標本：一顆 PR 回 review 時把手寫的字元
對照表換成套件，併進去之後，描述仍寫著表是手建的、沒有用那個套件。

**改描述跟回 review 走同樣三步**：寫成一個檔案 → 過對外寫入的檢查 → 才送出去。檢查用
`resolve-external-write-gate.sh` 回的那一行，身分是 `swe-knowledge:pr-body`、surface 是
`pr-body`：

```bash
<gate 那一行> --writer-token swe-knowledge:pr-body --surface pr-body --body-file <file>
gh pr edit <n> --repo <owner/name> --body-file <file>
```

檢查不過就不送，改好檔案再跑一次。直接 `gh pr edit` 沒有任何東西擋，所以這一步靠的是照著做。
2026-10-12 的標本：這個身分登錄之前，pr-body 一律被擋，一顆聚合過的 PR 描述裡的 commit 表
停在舊 SHA，只能補一則留言繞過。

**一顆 PR 要改的東西只改一次。** 已經知道還缺什麼，就補齊再送，不要先併一版佔位、之後再推
一次。拆成兩趟的話，reviewer 要看兩次，而第一次核准的是一個已知不完整的東西。2026-09-30 的
標本：一顆設定 PR 已經拿到核准，有人提議先併一份佔位的清單，之後再推真的值——那等於讓同一個
reviewer 為同一件事看兩次。

## 之後呢：它什麼時候算出去了

上面五條講的是 done——改動變成可被別人看見、可被判定的東西。**再往後一步是「它出去了」，
而那一步在別人的 repo 裡不歸這條流程管。**

判準一句話：**那個 PR 被 approve 或被 merge，這條流程就走完了。** 是哪一個由那張單自己說，
兩者都算——merge 常常是別人按的、按在別人的排程上，硬等 merge 等於讓一張已經被接受的單卡
在一個不歸我們管的動作上。

**但「誰按」是公司 pack 要回答的問題，這裡不預設是別人。** 公司 pack 說幾票算看過了、到了
誰按；它說作者自己按的，那麼票到了、第 5 條成立、CI 綠，按 merge 就是這條流程
最後一個動作，**不是一個要問人的待決**。2026-08-30 到 09-03 之間，同一個人把「達成的就 merge」
講了四次，每一次都是因為讀這一段的人只帶走「approve 或 merge 都算」、把行為者那一格留給
「不可逆先問人」的預設去補。

**之後那個 repo 怎麼釋出，不由這裡管、不由這裡偵測、不由這裡記錄。** 每個 repo 有自己的
節奏，而我們不是它的 owner。猜別人的節奏比不做更危險：一個猜錯的釋出狀態，讀起來跟一個
量到的釋出狀態一模一樣。要有設計的話，由那個 repo 的擁有者自己帶進來。

**上線更不在這裡。** 交付之後的上線要人介入，回填的做法是人主動說，然後動手改該改的地方。
這裡不加欄位、不加狀態、不加輪詢——**沒有欄位就沒有東西會把「還沒回填」讀成「沒上線」或
「上線了」。**

下面這一行是機器讀的。核心不認得 PR 是什麼，它只知道去找這一行、跑它、讀它印出來的狀態：

<!-- SWE-DELIVERED: bash .claude/skills/swe-knowledge/scripts/check-swe-delivered.sh -->

跟開工條件同一條路：核心把這張單記下的落腳處與身分原樣接在後面，所以判的是**這張單自己**
落在哪、當初開在哪條分支上，不是「跑這個命令的人現在站在哪」。**有幾個落腳處就判幾次**，
全部走完才算走完——放行一個「兩個 repo 裡有一個出去了」的終局，等於另一個永遠不會有人再看。

問不到不是走完，也不是沒走完：它自己一種狀態，印出來、非 0。一個因為 API 逾時而被讀成
「還沒出去」的單，會安靜地停在待辦裡；被讀成「出去了」的那一種更糟。

## 不做了之後：收拾自己留下的痕跡

一張單決定不做了，它在版控上留下的東西不會自己消失。2026-08-08 DP-440 關單並寫下理由，
它的 branch 與 PR 一路活到 2026-08-10 被人工看見才處理——**「這張單不做了」與「還有一條
branch 在等著被合」同時成立了兩天**，而沒有任何東西回報。狀態改了，痕跡沒改。

判準一條：**收得掉的只有這張單自己的東西。**

- 這張單開的那個 PR 關掉，遠端那條分支一起刪。commit 留在 `refs/pull/<n>/head` 上，所以
  關掉不等於丟掉——復原是 `git fetch origin refs/pull/<n>/head`。
- 本地那條 branch **只有已經併進預設分支的才刪**。還沒併的留著並且列出來：那裡面有沒有人
  看過的工作，而刪掉跟丟掉之間只差一個 reflog 到期。
- 別人開的 PR、不是這張單的 branch，一律不動。

**留下來的不得安靜。** 收不掉的逐個列出來並且非 0——一個沒有被列出來的殘留，下一次就會被
當成沒有殘留，而那正是那兩天發生的事。

<!-- SWE-CLOSE-CLEANUP: bash .claude/skills/swe-knowledge/scripts/swe-close-cleanup.sh -->

## 跟公司 pack 的關係

這裡是**通識**，只有上面五條；一家公司或一個專案特有的東西（codecov 門檻、stage 部署、
ticket 命名、review 人數）在它自己的 pack 裡。

**兩者疊起來的時候要說出順序，不得靜默疊加**：先這裡的五條，再疊公司 pack；公司 pack 只能
往上加條件，不能拿掉這裡的任何一條。真的需要拿掉時，那是一個要被說出來的例外，不是把
這一條默默改寫。理由跟「載不到就停」是同一句話——一個沒被說出來的合併，跟沒有合併，在
出事的時候長得一樣。
