---
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 上，不躺在預設分支。** 開的時機是「判定要立案之後、動手之前」——
   一個還沒開工的成功定義直接躺在預設分支上，等於它已經是既成事實。
2. **有一個 PR。** **PR 開出來就是實作完成**：它是那份改動變成可被別人看見、可被判定的
   東西的那一刻。沒有 PR 的改動不管本機多綠都還沒 done。
3. **判定過才進預設分支。** 進去的路徑是那個 PR，不是直接推。
4. **push 之前，本機跑完跑得動的驗證。** type check、lint、單元測試、受影響路徑的冒煙。
   把 CI 當第一道防線等於把 reviewer 當驗證工具。
5. **reviewer 提的每一條，處置回到那條意見上。** 不是「有沒有處理」——是**提出者拿不拿
   得到那個處置**。他看的是他留言的地方，回在別處他收不到。

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

第 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），說明它做什麼、參數與回傳值是什麼。既有沒動的不強制回填——上面
  那句「最可能是舊設計的化石」講的就是它們，補一句猜出來的比空著糟。
- **帶業務意義的 literal 抽成有名字的常數**，名字說不完的在宣告處補一行。名字也是一句
  對行為的主張，同一條規矩適用。純視覺數值（padding、gap）與 `aria-label` 這類不強制。
- **Inline comment 只在「為什麼」不自明時寫，一短行。** 逐行解說「做什麼」是會說謊的
  雜訊：它貼著那一行，那一行改了它不會跟著改。

## 寫腳本的時候

**語言選擇**：串工具、檔案操作、簡單 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` 的時候回「量不到」並拒絕，
不是放行——問不到關卡的狀態跟關卡是綠的，是兩件事。

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

開工條件是一次性的事件，它問「有沒有站在預設分支上」——那是一個對所有單都一樣的判準。
切到**另一張單**的分支照樣通過它，而那正是真的會發生的事故：兩個 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 而且**一行都不印**，核心會說「量不到」
而不是「一致」。印一半比不印糟：那會記下一個比實際涵蓋範圍小的集合，而它之後每次比對都
自洽。

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

`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 互相討論就把作者放行。

## 回覆 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

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

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

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

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

判準一句話：**那個 PR 被 approve 或被 merge，這條流程就走完了。** 是哪一個由那張單自己說，
兩者都算——merge 常常是別人按的、按在別人的排程上，硬等 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 只能
往上加條件，不能拿掉這裡的任何一條。真的需要拿掉時，那是一個要被說出來的例外，不是把
這一條默默改寫。理由跟「載不到就停」是同一句話——一個沒被說出來的合併，跟沒有合併，在
出事的時候長得一樣。
