前言
最近在嘗試把 OpenSpec 導入日常開發,目標是在 Claude Code 寫程式前先做規格對齊
但我發現OpenSpec不適合用在我的開發情境。與其勉強自己用,不如基於 OpenSpec 的核心精神,重新設計一個適合自己的版本。
整個過程從「為什麼現成工具不適合」到「該怎麼設計才適合」,中間踩過不少坑記錄一下。
為什麼 OpenSpec 不適合?
OpenSpec 是個跨工具、跨團隊的規格協議,設計目標是讓不同 AI 工具(Claude Code、Cursor、Aider 等)在做架構規劃時有一致的提案格式。它強迫開發者走完整的 propose → apply → archive 三階段流程,每個階段都有完整的文件產出。
對於有十幾個工程師的大型專案,這種儀式感是合理的。但對於我這種小團隊、單人主導的日常開發,價值密度太低。
舉個具體的例子:有一次我只想改 30 行 Controller 裡的快取邏輯,OpenSpec 卻產出了好幾萬字的提案文件要我審查。我看完那份文件的時間,比我直接改 code 還久。
其實我只需要做到兩件事:
1.動手前讓我看清楚 Claude 的設計意圖
2.對齊我的專案規範
於是我決定動手做一個輕量版本 : specflow。
第一步:用 Skill 讓 Claude 認識我的工作流
在動手寫之前,得先了解 Claude Code 的兩個關鍵機制: Skill 與 Slash Command
Skill 是 Anthropic 推出讓 Claude 能在特定情境自動載入「補充指示」的機制。它的本質是一個資料夾,結構長這樣:
|
1 2 3 4 5 6 7 8 |
.claude/ └── skills/ └── specflow/ ├── SKILL.md ← 主入口 └── templates/ ← 模板檔 ├── issue.md ├── design.md └── task.md |
主要看 SKILL.md 開頭的 YAML:
|
1 2 |
name: specflow description: A lightweight spec-driven development workflow for Laravel projects. Use this skill when the user invokes /spec:new, /spec:design, /spec:task, or /spec:run commands, refers to files in the specflow/ folder... |
這個 description 就是觸發機制 —— Claude 在跟你對話時,會根據你的訊息內容判斷「這個 skill 跟當前對話有沒有關係」。如果有,Claude 會”自動載入” SKILL.md 的內容當作補充指示。
但有個重要觀察:Skill 的觸發是模糊的。如果使用者用自然語言說「幫我規劃一下這個重構」,Skill 可能被觸發、也可能沒有。即使被觸發了,Claude 也只是「參考」這份 SKILL.md,不一定會嚴格照著執行。
對於需要精確控制每一步行為的工作流,光靠 Skill 不夠。所以我加上第二層機制:Slash Command。
第二步:用 Slash Command 取代自然語言觸發
Slash Command 是 Claude Code 的另一個機制,讓使用者可以用 /指令名 觸發特定行為。它的存放位置是 .claude/commands/。
這次我設計了四個指令:
|
1 2 3 4 5 6 7 |
.claude/ └── commands/ └── spec/ ├── new.md → /spec:new ├── design.md → /spec:design ├── task.md → /spec:task └── run.md → /spec:run |
注意檔案結構 —— 子資料夾 spec/ 加上檔名 new.md,Claude Code 就會把它對應成 /spec:new 這個指令(冒號分組)。
每個 .md 檔案的內容就是「這個指令該做什麼」的 prompt。例如 new.md 會明確指示:
1. 驗證 task name 是否符合命名規則(只允許小寫英文 + hyphen)
2. 檢查資料夾是否已存在
3. 建立資料夾、複製 issue.md template
4. 提示使用者填寫
為什麼用Slash Command 比自然語言要好?
我一開始也覺得「不就一個指令名稱嗎,跟自然語言差在哪?」實際做下去才發現差距很大,主要有三點:
1. 意圖明確,而且可以少打很多字
如果用自然語言,「issue.md 寫好了」跟「幫我看一下 issue」對 Claude 來說很可能觸發同一個流程,但你期待的行為不同 —— 前者是「請進入 design 階段」,後者是「請審查 issue」。
用 /spec:design 之後,意圖是強制宣告的,沒有解釋空間。
2. Prompt 內容固定,行為穩定
Slash command 的 prompt 是寫死的檔案。你執行 /spec:design 一百次,Claude 收到的指示一字不差。
對比自然語言:你今天說「幫我設計這個」,明天說「幫我規劃一下」,雖然意思一樣,但 Claude 的解讀可能微妙不同,行為跟著飄。
3. 可以掛 bash 命令做檢查
Slash command 的 prompt 可以用 ! 前綴執行 bash 命令,把輸出注入到指令的執行 context。例如我在 /spec:design 加了:
|
1 |
!`test -f specflow/project.md` |
如果 project.md 不存在,Claude 會看到 exit code != 0,立刻停下來提醒使用者建檔。純自然語言很難做到。
第三步:三個 MD 檔的設計
specflow 的核心是三個 markdown 檔案:issue.md、design.md、task.md,每個檔案職責清楚分離。
issue.md:使用者寫的需求描述
這是唯一由使用者填寫的檔案,template設計已經規範使用者需要填寫哪些資料
講白話文就是把prompt寫在這個檔案內當成開發紀錄
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
# Issue: <一句話標題> ## 想解決的問題 <2~3 句話描述現況的痛點> ## 期望的結果 <改完之後系統應該變成什麼樣> ## 範圍限制(必填) - 只動: - 不動: - 不處理(留待後續): ## 額外提示(選填) |
“範圍限制必填”這條規則。這是治療 Claude「過度設計」的關鍵防線,沒有明確邊界,Claude 永遠會傾向多做一些;有了邊界,它知道該在哪停下來。
design.md:Claude 提出的設計決策
這是 /spec:design 產出的檔案。我要claude強制用 checkbox 形式列出每條設計決策,而且每條決策都要附「理由 + 替代方案」:
|
1 2 3 4 5 |
## 決策清單 - [ ] **資料表命名採 Laravel 複數慣例**:`meeting_rooms`、`schedules` - 理由:Laravel Eloquent 預設以複數表名對應單數 Model - 替代方案:照 issue.md 字面用單數 —— 否決原因為偏離 Laravel 生態慣例 |
開發者只要確認每一項描述都是正確的就在[ ]中打 X 讓claude知道這條沒問題
有問題的部分也可以填寫在下方問題清單持續跟cladue討論
task.md:Claude 寫的、Claude 執行的清單
這是 /spec:task 產出的檔案,顆粒度規定「每項任務 5 分鐘內可完成」:
|
1 2 3 4 5 |
## 執行清單 - [ ] 1. 建立 `app/Contracts/CacheableApiClientContract.php` - 檔案路徑:`app/Contracts/CacheableApiClientContract.php`(新增) - 內容:定義介面,包含 `get(string $key): mixed` 跟 `set(string $key, mixed $value, int $ttl): void` |
顆粒度越細,Claude 越難偏離計畫。一個 5 分鐘的任務就是「在 X 檔案新增 Y method」,沒有自由發揮的空間。
對比 OpenSpec 那種「每個任務 2 小時內完成」的設計,5 分鐘版本看似囉唆,但實際執行時 Claude 幾乎不會跑偏。這是用形式上的繁瑣換實質上的可控性。
整套工作流串起來:從 issue 到 code
現在所有零件都備齊了,完整的工作流長這樣:
|
1 2 3 4 5 6 7 |
/spec:new <task-name> 建立資料夾 + issue.md template ↓ (使用者填寫 issue.md) /spec:design <task-name> 讀 project.md + issue.md → 產出 design.md ↓ (使用者審查並勾選決策清單) /spec:task <task-name> 讀 design.md → 產出 task.md ↓ (使用者審查執行清單) /spec:run <task-name> 逐項執行 + 寫執行後備註 |
每個階段中間都有 checkpoint,Claude 必須等使用者明確下指令才會繼續。
還有一個關鍵設計:討論模式
審查 design.md 的時候,難免會有疑問:「決策 4 為什麼選這個?」、「決策 5 要不要加 is_active 欄位?」
我在 design.md template 加了一個「**待討論問題**」區塊,讓使用者直接寫下疑問。/spec:task 執行時會先檢查這個區塊:
|
1 2 3 4 5 6 |
| 條件 1(決策清單) | 條件 2(待討論問題) | 執行的流程 | |---|---|---| | 全勾選 | 無問題 | 產 task 模式 | | 全勾選 | 有問題 | 討論模式 | | 未全勾選 | 無問題 | 提示勾選 | | 未全勾選 | 有問題 | 討論模式(優先處理討論) | |
進入討論模式後,Claude 會逐題回答、修改受影響的決策、把該決策的 checkbox 重置為 [ ]、清空「待討論問題」區塊、最後把問題摘要搬到「已討論問題」區塊保留脈絡。
這樣設計有幾個優點
1. /spec:task 不需要靠 Claude「記得停下來」,它的閘門條件是兩個檔案狀態:決策清單全勾 + 待討論清空。狀態不滿足就不可能產出 task,這是檔案決定的、不是記憶決定的。
2. 傳統做法是討論散落在對話 history 裡,3 個月後沒人記得。我讓討論摘要自動進到 design.md 的「已討論問題」區塊,跟著 git 一起保存,變成決策演進的歷史。
Read 工具的快取陷阱
實作才知道read的bug
|
1 2 3 |
1. 我跑 `/spec:design`,Claude 產出 design.md 2. 我在編輯器修改 design.md(勾選決策、寫討論問題) 3. 我跑 `/spec:task`,Claude 卻說「沒有變更,issue.md 還是 template 原文」 |
明明我改了檔案,Claude 卻看不到。
找半天原來是Claude Code 的 Read 工具有快取機制,果 session 中讀過某個檔案,再次 Read 會回傳快取版本
解法是強制用 cat 命令繞過快取:
|
1 |
!`cat specflow/changes/$ARGUMENTS/issue.md` |
工具是來服務你的,不是反過來
過去我會覺得「現成工具用就對了,自己做太花時間」。但這次讓我發現,在 LLM 工具普及的時代,造一個小輪子的成本已經低到不該成為阻力。從設計到 npm publish,我跟 Claude 來回討論大約 4 小時就完成了。
如果你也想試試 specflow,可以用一行指令安裝:
|
1 |
npx @virtualorz/specflow init |
附上專案Github : https://github.com/virtualorz/specflow