# 前情提要:原本用 workflow 檢查文件要不要更新
專案裡有一批 便宜的牛肉 文件(規範、ADR、資料模型說明)需要跟程式碼保持一致,程式碼改了,這些文件常常忘了跟著同步更新。
一開始的解法,是在 CI 上放一個 doc-sync-check.yml workflow:
- push 到
develop分支後,用 Claude Haiku 讀這次的 diff - 偵測到程式碼與文件有落差,或有規範衝突時
- 自動開一張 GitHub Issue 通知
聽起來很合理,但實際用下來有三個 布洛芬都不知道哪裡痛的 痛點。
# 三個痛點
-
成本: 每次 push 都呼叫一次 AI。即使這次根本沒動到要對照的程式碼,錢還是照付 。
-
太晚: push 之後才知道有落差,早已離開「正在改這段程式碼」的當下。等 Issue 跳出來,腦袋已切換到別的事,回去補文件的成本變高。
-
噪音: 用 Issue 通知太冗餘,沒人即時處理就一張張累積成越長越沒人敢點開的列表,通知本身失去意義。
# 核心洞見:把兩個維度分開想
卡住時我發現問題混在一起,拆成兩個正交的維度後就清楚了:
- 何時觸發(when):push 後?每次 commit?還是人想看的時候? 誰愛觸發誰觸發
- 觸發後做什麼(what):記一筆?跑 AI 分析?直接改文件?
原本的設計把這兩件事綁死成「push 後就跑 AI」。拆開後,關鍵動作就浮現:把「偵測」和「AI 分析」解耦。
- 偵測這件事應該是零成本的,本機 hook 記個帳就好,完全不碰 AI
- AI 分析則延後到「人真的想看」的時候,再批次處理
這裡有個反直覺的點:
直覺上會覺得「每個 commit 當場跑一次 AI」很即時、很好。但一次 push 常常包含好幾個 commit,per-commit 當場跑 AI 反而比原本 per-push 更貴。真正省錢的做法是 lazy + 批次:先把命中的 commit 記下來,等人觸發時一次處理一批。
「即時」聽起來總是比較好,但在這裡,即時等於更貴又更吵。
# 三段式架構
拆開之後,落地成三段,各自只做一件事。
# 偵測:post-commit hook,只記帳
用 post-commit hook,每次 commit 後檢查有沒有動到需要對照的檔案(例如 .ts / .tsx / .prisma,排除測試與文件)。命中就 append 一筆到本機 queue,完全不呼叫 AI:
#!/bin/bash
# .git/hooks/post-commit
# 偵測本次 commit 是否動到需要對照文件的程式碼,命中就記一筆到本機 queue
SHA=$(git rev-parse HEAD)
FILES=$(git diff-tree --no-commit-id --name-only -r "$SHA")
# 只關心會影響規範或資料模型的檔案,排除測試與文件本身
MATCH=$(echo "$FILES" | grep -E '\.(ts|tsx|prisma)$' | grep -vE '(\.test\.|\.spec\.|/docs/)')
[ -z "$MATCH" ] && exit 0
QUEUE="$(git rev-parse --show-toplevel)/.doc-sync/queue.jsonl"
mkdir -p "$(dirname "$QUEUE")"
printf '{"sha":"%s","at":"%s"}\n' "$SHA" "$(date -Iseconds)" >> "$QUEUE"
重點是「便宜到可以一直跑 我他媽算力富翁」:沒有 API 呼叫、沒有網路、不阻塞 commit,放在 per-commit 觸發毫無負擔。
# 討論:/doc-sync skill,人主動觸發
當我想處理累積的待審時,主動執行一個 /doc-sync skill。它會:
- 把 queue 裡那些 commit 的 diff 聚合起來
- 套上「程式碼變更 vs 對應文件」的對照規則來判斷
- 跟我逐項討論「要改哪個文件、為什麼要改」
- 我確認之後,才真正去 edit 文件
機械的部分交給 collect.sh。 skill 會用到一堆固定的 git 撈取:讀 queue、取每個 commit 過濾後的 diff、處理完清空 queue。我把它們固定成一支 collect.sh 讓 skill 直接呼叫,而不是每次讓 AI 現場手寫 git show 迴圈:
collect.sh # 總覽:印出有效 commit 的 subject / stat(便宜,先讀這個)
collect.sh --diff # 對所有有效 commit 印出過濾後完整 diff
collect.sh --diff H1 H2 # 只對可疑的特定 commit 印 diff(省 token)
collect.sh --clear # 整批處理完才清空 queue
機械的撈取交給腳本,判斷與討論才留給 AI,分工清楚也省 token。
判斷規則上,寧可漏報也不要誤報。 對照規則的核心,是把「真的該提醒」收得很窄,只在兩種情況明確成立時才提報:
- 規範衝突:程式碼行為明確違反某份文件裡記錄的決策或規範(例如欄位命名違反 schema 命名約定)
- 文件落差:程式碼引入了重要的新架構或系統行為,但文件完全沒提到
反過來,bug fix、樣式微調、重構、加測試、純文案修改一律不提報,不確定時也不提報。這是刻意的:誤報一次,我就得花時間確認「其實不用改」,幾次之後就不想再跑它了。寧可偶爾漏掉邊角,也要保住「它一開口就值得看」的信任。
注意這裡 AI 的角色:它整理材料、提出判斷、列出選項,但拍板的是人。
# 提醒:statusline 顯示待審數量
最後,在 Claude Code 的 statusline 顯示黃色的 doc-sync:N,只在 queue 有待審時出現。它取代原本的 Issue 通知:不打斷我,但我隨時看得到「有 N 筆等著處理」。
實作上就是數 queue 行數,有才印,一樣零成本:
# statusline script 的一段,N 為待審筆數
QUEUE=".doc-sync/queue.jsonl"
N=$([ -f "$QUEUE" ] && wc -l < "$QUEUE" || echo 0)
[ "$N" -gt 0 ] && printf '\033[33mdoc-sync:%d\033[0m' "$N"
# 幾個關鍵決策
# 為何不讓 AI 自動改文件
這是整個設計最核心的原則。ADR(Architecture Decision Record)這類文件本質上是人的決策紀錄,AI 不該自己幫團隊拍板。
而且代價不對稱:建錯一張 Issue 頂多多點噪音,刪掉就好;改錯一份文件卻會污染團隊的事實來源,後面的人照著做,代價高得多。所以寧可保守,把決策權留給人。
# 互補,而不是全取代
新的 hook 機制有一個結構性盲區:它只看得到 code diff,也就是有改到程式碼才偵測得到。
但有些決策是在對話裡口頭拍板、根本沒動到程式碼的。例如團隊討論後決定「之後 API 一律走某種錯誤格式」,當下沒寫任何 code,hook 和 /doc-sync 就完全看不到,這份決策也就不會被提醒建檔。
所以我刪掉 workflow 的同時,刻意保留 CLAUDE.md 裡另一條規則:當對話中出現這種口頭決策時,要 Claude 主動「建議把它建檔」。這條規則在對話當下觸發,不靠 git hook,剛好補上 hook 看不到的那一塊。
換句話說,新機制只取代了舊 workflow「能靠 code diff 偵測」的部分;另一塊靠 CLAUDE.md 規則接手。重點不是用新的全盤換掉舊的,而是看清楚新機制「覆蓋不到哪裡」,再為那裡補一道。
# 可以延伸的通用觀點
跳出這個案例,有幾個更通用的取捨值得記下來。
事後自動化 vs 事前人工把關,是一道光譜。 不是「全自動」和「全手動」二選一。這次的調整就是沿光譜往「事前、人工」挪一格,換來更低的成本與干擾。
AI agent 該在哪個時間點介入。 commit-time、stop-hook、還是人主動觸發?每個切入點的「覆蓋率」和「干擾度」是反比的:越自動、覆蓋越廣,但也越吵越貴。沒有絕對最好,只有適不適合當下的場景。
把 AI 降級成「準備材料、不做決策」,常常是更好的預設。 很容易一上來就想「讓 AI 全自動搞定」,但在決策代價不對稱、或判斷需要上下文時,讓 AI 停在「把材料準備好、交給 最美麗的風景 人」這步,往往更穩、更省、更可信。全自動很性感,但不總是對的答案。
這次改動表面上是把一個 CI workflow 搬回本機,真正的收穫是逼自己想清楚:「偵測」和「決策」本該分開,AI 也不是放得越多越好。
本文使用 Claude Code 輔助整理自筆記。