十個開發者,一個 docs 資料夾
Superpowers 是一個 Claude Code plugin(GitHub 286,000 顆星),它的 skill 會把 spec 和 implementation plan 寫成 markdown 檔案,並 commit 進 repository。預設情況下,這些檔案全部落在同一個資料夾:docs/superpowers/。六月時,Python 金融函式庫 Okama 的維護者發現,agent 在那裡寫下的八份 implementation plan——完全依照 plugin 的預期方式 commit——已經被 Read the Docs 渲染成公開網頁。他們是在發布之後才注意到的。
現在把規模放大:一個十人的前端團隊、四個小組、一個 repository,而每個開發者都在產生這些檔案。三個問題會落到 lead 桌上。我們到底該對什麼做版本控管?當一份 spec 提到的路徑在兩個 sprint 前就被刪掉時,drift 算不算問題?資料夾又該由誰治理?Okama 的外洩事件,問題不在於 commit,而在於 commit 的時候沒有規則。本文提供五條規則。
每個工具都會 commit 這些檔案
Superpowers 從不詢問是否該 commit。它的 planning skill 第 18 行,會把每份 plan 存進 docs/superpowers/plans/,附上日期,每個功能一個檔案。brainstorming skill 則在同一步驟寫下設計文件並 commit,你甚至還沒看過那份 plan。
這不是 Superpowers 獨有的怪癖。每一個主流的 spec-driven 工具都做出同樣的選擇:
| Tool | Publisher | Where specs live |
|---|---|---|
| Superpowers (286,000 stars) | obra | docs/superpowers/,所有東西共用一個資料夾 |
| Spec Kit (136,000 stars) | GitHub | 每個功能一個編號資料夾,放在該功能的 branch 上 |
| Kiro | Amazon | .kiro/specs/,每個功能一個資料夾 |
| AI-native SDLC playbook | Anthropic | 每個階段 commit 一份產出物:intent、spec、plan、diff、review findings、incident record |
Kiro 把「每個功能一個資料夾」當作賣點,讓組員可以同時協作不同功能。Anthropic 八月發布的 playbook 走得更遠:每個階段都會 commit 一份下一階段可以讀取的產出物。所以問題從來不是「要不要 commit」,而「放在哪裡」每個工具都給了答案。誰擁有這個檔案,以及它何時算失效,卻沒有答案。這正是五條規則要填補的空缺。
出錯的三種方式
已 commit 的 plan,會用三種不同的方式出錯。
失誤一:docs 產生器。 一套文件建置流程,會把 docs/ 底下每一份 markdown 檔案渲染出來,不管目錄表有沒有列出它,於是計畫資料夾就跟著手冊一起發布出去。Okama 遇到的正是這個狀況。
失誤二:branch。 Superpowers issue 1246 回報,brainstorm 會把 plan 直接 commit 到 main。有使用者算出,每次 session 有 10 到 15 次 commit,每改幾下就多一個。那則討論串的訴求只有一句話:main branch 上不該有 plan 和 spec。
失誤三:drift。 spec drift 是最安靜的一種失誤:程式碼演進了,spec 卻沒有。Thoughtworks 的 Birgitta Böckeler 把 spec-driven development 分成三個層級:
| Level | Meaning |
|---|---|
| Spec-first | 寫過就用一次 |
| Spec-anchored | 在功能的整個生命週期裡持續維護 |
| Spec-as-source | 人類只編輯 spec |
Superpowers 寫出的是 spec-first 文件,但永久保留。預設下你得到的是「錨定式儲存,配上初稿等級的維護」:沒有人更新那份檔案,而 agent 卻在下一季把它當成事實讀進去。TrueFoundry 說得很直接:drift 在 spec-first 做法裡無法避免,在 spec-anchored 做法裡可以管理,但前提是分歧要能被偵測到。
一個持有 41 份 spec 與 9 個 agent 檔案的 repository,不是有整潔問題。它有 41 個未受版本控管的行為輸入。Böckeler 自己的反應是:她寧可審查程式碼,也不想審查這些 markdown 檔案。一份沒人再讀的 plan 是雜物。一份 agent 會讀的 plan 是指令,而過時的指令就是錯的指令。
規則一:每個小組一個家,配一個負責人
規則一:資料夾要有負責人,而負責人是小組,不是 plugin。從第 5 版開始,Superpowers 會尊重你專案 CLAUDE.md 裡的指示,優先於它自己的預設值。你說 plan 該放哪裡,它就會放在哪裡。
但光有一張路徑表還不夠。在 issue 939 裡,有使用者寫了一張輸出路徑表,結果模型還是照著 skill 裡的具體路徑走,跳過了那張表。skill 自身的覆寫說明只是個括號註記。第一次就成功的方式,是一行加粗、祈使句的指示:spec 必須存在這裡,不是那裡,並把預設路徑直接點名為該避免的東西。
版面配置是每個小組一個資料夾(checkout、search、accounts、design-system),各自擁有自己的 spec 和 plan。在 monorepo 裡,就把這行寫進該套件自己的 CLAUDE.md。Claude Code 會在讀取某個子目錄時,依需要載入該目錄的檔案,而官方文件也指出,每個目錄的檔案通常由該目錄的負責人維護。從未安裝這個 plugin 的工程師完全不受影響:這行指示只有在 skill 詢問該存到哪裡時才會生效。
接著讓平台去落實所有權。每個小組資料夾放一行 CODEOWNERS,每份 spec 的審查就會自動落到會與它共存的人手上。有一個要留意的地方:這個覆寫只是一段 prompt,不是設定。每次 plugin 更新後,都要檢查第一份 spec。
規則二:個人與共用要分開
規則二:不是每份 plan 都是團隊資產。第一個要求 Superpowers 提供可設定位置的人(issue 337)說得最好:plan 檔案可以是個人的工作文件,而不是專案資產。
個人這一側,已經有專為它設計的檔案。CLAUDE.local.md 放在共用檔案旁邊,在它之後載入,而官方文件也告訴你要自己把它加進 git ignore。你重新導向到私人資料夾的那行指示就放在這裡,別人看不到。至於資料夾本身,Git 有一種 ignore 檔案完全不會碰到共用的樹狀結構:.git/info/exclude 是每個 clone 各自獨立的,從不被 commit,也不需要開 pull request。plugin 自己的暫存空間就是這麼做的:從 6.0.3 版開始,它的工作目錄裡會放一個內容為 * 的 .gitignore,自己把自己忽略掉,不需要動到任何被追蹤的檔案。
共用那一側,是任何通過 brainstorm 本身就會執行的審查關卡的東西:寫好 spec、提出審查、由第二個人核准。核准之後,才移進小組資料夾。沒核准,就留在本機。兩邊都寫在 branch 上,絕不寫在 main 上。共用的 CLAUDE.md 裡兩行就能做到:把 spec 和 plan 當成一項功能的起點,而且要先建立 worktree,再動筆寫它們。
有一個限制:被 git ignore 的 plan 不會跟著你走。Okama 的維護者一開始試過把整個資料夾 ignore 掉,後來又改回來,因為 plan 不再在不同機器和 agent 之間同步。個人的意思就是只留給個人。
規則三:像 ADR 一樣的生命週期
規則三:一份 spec 要有狀態,失效的 spec 要說清楚自己失效了。這個想法已經有 15 年歷史。Michael Nygard 在 2011 年提出,把 architecture decision record(ADR)保存在 repository 裡,一個編號檔案對應一項決策。一項決策先是「proposed」,然後是「accepted」。當後來的紀錄改變了它,舊的那份就標記為 deprecated 或 superseded,並附上取代它的紀錄。如果一項決策被推翻,舊紀錄會保留下來,但標記為 superseded。
把這套規則套用到 spec 上,每份共用的 spec 都以一段五行的標頭開場,而 agent 本來就會讀到它:
| Field | Purpose |
|---|---|
| status | proposed、accepted、superseded |
| superseded-by | 取代它的那份 spec |
| owner | 負責的小組 |
| touches | 這份 spec 所主張、涉及的路徑 |
| review date | 上次檢查的時間 |
絕不要把一份已 accepted 的 spec 改寫成另一項決策。寫下一份新的,回頭指向它,歷史紀錄就能保持誠實。Anthropic 的 playbook 裡提到一個例外:當實作偏離了 plan,就在同一次 commit 裡更新 plan。plan 要跟著讓它失準的那個 diff 一起走,而這一點可以靠一個 hook 來強制執行。
Spec Kit 列出三種 spec 可能延續下去的方式。Flow-forward:每個功能一個新資料夾,舊的留下來當歷史。Living spec:spec 就是合約,其餘一切都靠它重新生成。Flow-back:build 的結果反過來重塑 spec。它們的規則是一致的:如果 spec 說的和 task 或程式碼不一樣,就不該留著這個落差。這就是刻意選擇 spec-anchored。
限制在於:status 是需要人來設定的 metadata。除非你的 CLAUDE.md 交代清楚,agent 不會主動把自己寫的 spec 標記為 superseded,而即便交代了,審查也得有人注意到才行。
規則四:CI 裡的 drift gate
規則四:一份對現有樹狀結構說謊的 spec,應該讓建置失敗。六月發表的那篇論文把這個問題命名為 silent spec-code drift:程式碼演進了,規格文件卻沒有,而這個分歧會一直隱形,直到修復的代價變得高昂。它給出的答案是 drift gate,一個會擋下 merge 的檢查條件。
這個檢查比聽起來簡單。對每一份標頭寫著 accepted 的 spec,抓出它提到的路徑——不管是寫在反引號裡,還是寫在 touches 那一行——然後檢查每個路徑是否還存在。路徑不見了,建置就失敗,而且日誌裡會寫出是哪份 spec 出的問題。superseded 和 proposed 的檔案會被跳過:只有 accepted 的 spec 對現有樹狀結構有主張,所以也只有它們需要被檢查。TrueFoundry 把這件事描述成一個排定時程的 diff 檢查,而不是事故發生後的考古工作:spec 是一種 scaffold,而 spec 的變動就是一種政策變動。
已經有人在手動做這件事的簡化版。有留言者會請 Claude 比對 spec 檔案和程式碼,把缺漏的部分開成 ticket。這支腳本只是把同一件事,免費地套用在每一次 pull request 上。
第二道防線,是 Okama 最終採取的做法:把資料夾從 docs 建置流程中排除。在 Sphinx 設定裡加一行,檔案依然留在 Git 裡,但不會出現在 HTML 裡。
限制在於:路徑檢查只能抓到被刪除的檔案,抓不到改變的行為。一份 spec 可以列出的每個檔案都還存在,卻描述著一個早已消失的 API。這正是審查和「同一次 commit」規則要負責的地方。
規則五:大小預算,以及代價
規則五:大多數工作根本不值得寫一份 spec。關於 spec-driven 額外成本的證據相當一致:
| Experiment | Result |
|---|---|
| Marmelab,用 Spec Kit 做一個顯示目前日期的功能 | 8 個檔案、1,300 行規格文字 |
| OpenSpec bake-off,同樣的需求,spec 工具對比純用 Claude Code | 多寫 50% 程式碼、多 50% 循環複雜度、多花一倍時間、多花三倍成本 |
| 一個已經跑了幾個月 spec-driven development 的團隊 | 多花 2 到 3 倍 token、大約多花一倍時間、為了本不需要協調的變動付出巨大的協調成本;沒有證據顯示程式碼品質有所改善 |
這次 bake-off 只是一次實驗,不是一個 benchmark,作者本人也這麼說。但方向是一致的。所以規則是:當工作跨越小組邊界,或是 90 天內還會被再次讀取,才寫 spec。其他一切都只是一個 prompt。
Superpowers 本身就會把每個請求分成三條路:spike、bounded、architectural。只有 architectural 這條路才會寫 spec。讓 bounded 這條路保持在 bounded 的規模,並讓 spec 維持在接近 300 行。實務工作者還會補上兩句:別把 spec 寫得太大,而且要讀你產生出來的那些 spec。
這個限制是雙向的。同一次 bake-off 也發現了三個純手動流程漏掉的缺口。spec 買到的是覆蓋率,不是速度。把錢花在覆蓋率真正重要的地方。
結論:先 commit,再治理
回到 lead 的那三個問題。要對什麼做版本控管:核准過的 spec 和 plan,放在 branch 上,存在小組的資料夾裡。Drift:一個狀態標頭,加一道 CI 檢查。治理:負責人,加一條大小規則。
五條規則,而且沒有一條在今天是靠工具強制執行的。那個覆寫的 issue 還開著。main branch 的 issue 還開著。那個重新排序指示的 pull request 被關閉,沒有 merge。被燙過一次的那個團隊,還是保留了這個做法:8 份 plan、6 份 spec,commit 進 repository,並從 docs 建置流程中排除。
Böckeler 的質疑依然站得住腳。有了狀態標頭和大小預算,你只需要讀 accepted 的 spec,而且數量會更少。你換回來的,就是 Anthropic 的 playbook 所說的稽核軌跡:誰要求了什麼,agent 產出了什麼,又是誰核准的。
這是流程問題,不是工具問題:一個 CLAUDE.md 檔案、一個 CODEOWNERS 檔案、一段標頭,加一支十五行的腳本。如果這個團隊連 CODEOWNERS 的 request 都不願意審查,他們也不會去審查一份 spec。到了那個地步,.gitignore 才是最誠實的選擇。
AIDive