MonoSpecs 是什麼:為什麼說它是對 OpenSpec 的進一步升級和擴展
MonoSpecs 是什麼:為什麼說它是對 OpenSpec 的進一步升級和擴展
當一個產品體系膨脹到 40+ 個獨立 Git 倉庫時,“規範”該放在哪裡?這篇就聊聊 HagiCode 在多倉庫治理上走過的兩步棋:先把 OpenSpec 上提到主倉庫,再在它之上發展出 MonoSpecs 這套多倉庫管理方案。其實也沒什麼啦,只是走過了一些坑,想記下來罷了。
背景
做過稍微大一點產品的同學,大概都有過這樣的體驗吧——一開始代碼就一個倉庫,規規矩矩,歲月靜好;後來前端、後端、桌面端、文檔站、官網、構建工具各自獨立成倉,倉庫數量蹭蹭往上漲,像野草一樣攔都攔不住。再往後你想給某個跨倉庫的功能寫一份”規範文檔”,突然就不知道該往哪寫了,怎麼說呢,有點像小時候的零花錢,怎麼忽然就沒了。
我們自己的 HagiCode 就是一個由 40+ 個獨立 Git 倉庫組成的產品體系。早期的時候,我們直接把 OpenSpec 的 openspec/ 目錄塞在後端子倉庫 hagicode-core 裡,想著反正後端是核心,放這兒最穩。結果倉庫越拆越多,這套方案就暴露出來一連串讓人頭疼的問題。畢竟代碼世界從來不會因為你”想得穩”就真的穩。
第一個痛點:specs 被困在單一子倉庫裡。一個功能如果同時影響前端 web 和後端 hagicode-core,我得在 hagicode-core 裡寫提案,再跑去其他子倉庫執行代碼改動。提案該歸屬哪個倉庫,本身就成了一個爭議。
第二個痛點:子倉庫不純淨。每個子倉庫都背著自己的 openspec/,規範文檔和產品代碼混在一起。別人 clone 你一個前端倉庫,結果帶回來一堆後端提案文檔,一臉懵。
第三個痛點:AI Agent 難以理解倉庫關係。各個子倉庫彼此獨立,沒有一個機器可讀的”清單”告訴 AI:這個產品由哪些倉庫組成、各自負責什麼、哪個是可編輯的、哪個是只讀參考。
第四個痛點:跨倉庫編輯成本高。要改一份 spec,必須先 cd 進對應子模塊,路徑跳來跳去,協作心智負擔極大。
就是在這樣的背景下,我們先做了一次”OpenSpec Monorepo Migration”,把 specs 從子倉庫上提到 monorepo 根目錄。然後在它之上,又發展出了 MonoSpecs 這套多倉庫管理方案。理解這兩步的遞進關係,是看懂”為什麼說 monospec 是對 openspec 的進一步升級和擴展”的關鍵。
關於 HagiCode
本文分享的方案來自我們在 HagiCode 項目中的實踐經驗。HagiCode 是一個 AI 代碼助手項目,倉庫數量多、跨語言協作頻繁,這種結構複雜度逼著我們必須把”規範”和”倉庫治理”這兩件事都做紮實。MonoSpecs 這套方案,就是在這種多倉庫實戰中一點點打磨出來的,其實也沒什麼神來之筆,無非是多走了幾步罷了。
OpenSpec 解決的是”規範怎麼寫、怎麼演進”
要講清楚兩者的關係,得先拆開看它們各自負責什麼。
OpenSpec 本質上是一套 spec-driven 的變更管理工作流。它核心的產物長這樣:
openspec/├── specs/ # 當前生效的能力規範(每個能力一個 spec.md)├── changes/ # 進行中的提案│ └── archive/ # 已歸檔的歷史提案└── project.md它回答的問題是:一個變更要經過提案(proposal)、設計(design)、任務(tasks)、歸檔(archive)這樣的生命週期,並在歸檔時把 deltas 合併進 specs。這套機制本身和”倉庫有幾個、在哪、歸誰管”是無關的,它只關心 spec 文件怎麼組織。
我們通過一次遷移提案,把原本分散在 hagicode-core/openspec/ 的 82+ 個 spec 文件上提到 monorepo 根目錄的 openspec/,讓所有 spec 在一個地方統一可見、統一版本控制。
但這次遷移說白了只是”把 spec 文件搬家”,並沒有回答更根本的問題:這個 monorepo 到底由哪些子倉庫組成?這些子倉庫之間的關係是什麼?這就是 MonoSpecs 要補上的那一塊。
MonoSpecs 解決的是”多倉庫本身怎麼管”
MonoSpecs 的核心,是一個機器可讀的清單文件:.hagicode/monospecs.yaml。它做了四件 OpenSpec 完全不涉及的事情。
第一件:聲明子倉庫清單。每個倉庫的 path、url、displayName、icon、tags、是否折疊到 “More”,全部寫在一個 YAML 裡,一目瞭然。
第二件:驅動 clone 腳本。scripts/clone-repos.mjs 直接讀取這個 YAML,批量 git clone,不再硬編碼倉庫列表。新增倉庫只要在 YAML 裡加一行,腳本零改動。
第三件:給 AI/IDE 提供項目結構上下文。配合 AGENTS.md,AI Agent 一眼就能看出哪個倉庫可編輯、哪個是 reference-only、技術棧是什麼。
第四件:把 OpenSpec 的產物錨定到主倉庫。specs 不再散落到各子倉庫,而是統一收歸到主倉庫根目錄的 openspec/,子倉庫因此保持純淨。
兩層含義,別搞混了
在 MonoSpecs 的官方 guide 裡,明確點出了一個非常容易混淆的地方:MonoSpecs 其實有兩層含義。
一層是配置系統層,指的就是 .hagicode/monospecs.yaml 這個配置文件本身,以及它配套的加載、校驗、緩存機制。
另一層是倉庫類型層,指的是一種”主倉庫 + 多子倉庫 + 集中 specs”的倉庫組織模式。當我們說一個項目”是 MonoSpecs 項目”,意思是它採用了這種結構。
這兩層疊加在一起,才是完整的 MonoSpecs。很多人第一次接觸容易只看到 YAML 文件這一層,以為 MonoSpecs 就是個配置清單,其實它的價值更多在第二層——一種明確的多倉庫協作範式。其實,美的事物往往不在第一眼,得多看幾眼罷了。
為什麼說是”升級和擴展”
把兩者放在一起對比,關係就清晰了:
| 維度 | OpenSpec | MonoSpecs |
|---|---|---|
| 關注點 | spec 文件的內容與生命週期 | 倉庫的組織結構與清單 |
| 核心產物 | openspec/specs/*/spec.md | .hagicode/monospecs.yaml |
| 是否依賴對方 | 不依賴 MonoSpecs | 依賴 OpenSpec,復用其 openspec/ 做變更管理 |
| 解決的痛點 | 規範怎麼寫、怎麼演進 | 多倉庫怎麼聲明、怎麼 clone、AI 怎麼理解 |
| 作用範圍 | 任何倉庫都能用 | 專為”一主多子”的多倉庫結構設計 |
說白了,MonoSpecs 沒有替代 OpenSpec,而是在它之上加了一層”倉庫治理”。用 monospecs.yaml 描述倉庫拓撲,用集中式 openspec/ 讓 spec 與子倉庫解耦,用 commit_when_archive 讓歸檔自動落盤到主倉庫。
如果用一個類比:OpenSpec 提供了”變更語法”,MonoSpecs 提供了”多倉庫語義”。前者是後者的前提,後者是前者的擴展。條條大路通羅馬,只是這一次,路比想像中更長一點而已。
怎麼落地:四步走
第一步:確立主倉庫與配置文件
在 monorepo 根目錄放好配置文件,聲明所有子倉庫。以我們自己的項目為例,結構大致是這樣:
version: "1.0"commit_when_archive: true
repositories:- path: "repos/web" url: "https://github.com/HagiCode-org/web.git" displayName: "前端" tags: [frontend, react, pcode-client]
- path: "repos/hagicode-core" url: "https://github.com/newbe36524/pcode" displayName: "後端" tags: [backend, dotnet, orleans]
- path: "repos/docs" url: "https://github.com/HagiCode-org/docs.git" displayName: "文檔" tags: [docs, astro, starlight] ui: collapseToMore: true # 在 UI 裡折疊到 "More" 後面有幾個字段要特別注意:
path是相對主倉庫根的本地路徑,也是每條記錄的唯一鍵。url是 Git 遠端地址,clone 腳本就靠它拉代碼。displayName/icon/tags只影響 UI 展示和 AI 上下文,不影響 clone 行為。commit_when_archive: true讓 OpenSpec 提案歸檔時自動 commit 到主倉庫。
第二步:把 OpenSpec 上提到主倉庫根目錄
遷移前後對比如下:
遷移前(specs 困在子倉庫) 遷移後(specs 集中在主倉庫)hagicode-core/ . (主倉庫根)└── openspec/ ├── .hagicode/monospecs.yaml └── specs/ (82+ specs) ├── openspec/ │ ├── specs/ (集中管理) │ └── changes/ └── repos/ ├── hagicode-core/ (純淨,無 openspec) ├── web/ └── docs/子倉庫從此不再背 openspec/,主倉庫成為唯一的 spec 真相源。這一步看起來簡單,但帶來的收益非常實在——任何一個工程師站在主倉庫根目錄,就能看到整個產品體系的全部規範。
第三步:讓 clone 腳本讀配置而非硬編碼
scripts/clone-repos.mjs 的核心邏輯就是讀 YAML、逐條 clone:
const CONFIG_PATH = path.join(__dirname, '..', '.hagicode', 'monospecs.yaml');// 解析 repositories 數組// 對每條執行 git clone <url> <path>// 目標目錄已存在則跳過或 git pull新增倉庫的時候,只需要在 YAML 裡加一條,不用動腳本。這點小小的改動,省下的是無數次”忘記同步倉庫列表”的扯皮。畢竟誰願意重複勞動呢?
第四步:後端提供統一的 MonoSpecs 服務層
如果不抽一層抽象出來,配置解析邏輯很容易散落在 GitAppService、ProjectAppService 各個角落。HagiCode 在 ClaudeHelper 模塊裡抽出了 IMonoSpecsService,對外暴露一組清晰的能力:
public interface IMonoSpecsService{ Task<MonoSpecsConfigDto> GetConfigAsync(string projectPath); Task<List<RepositoryInfoDto>> GetSubRepositoriesAsync(string projectPath); Task<MonoSpecsDataDto> GetMonoSpecsDataAsync(string projectPath); Task<MonoSpecsManagementDto> GetManagementDocumentAsync(string projectPath); Task<MonoSpecsManagementDto> InitializeManagementDocumentAsync(string projectPath); Task ValidateManagementDocumentAsync(string projectPath, UpdateMonoSpecsManagementRequestDto request); Task SaveManagementDocumentAsync(string projectPath, UpdateMonoSpecsManagementRequestDto request);}這套服務負責加載、校驗、緩存配置,並提供”初始化最小模板”的能力——給一個空項目一鍵生成 monospecs.yaml、repos/、openspec/changes/archive/、openspec/specs/ 骨架,並自動補好 .gitignore。緩存這東西,就像記憶一樣,記住了,下次就不必費勁去想了。
實戰中的幾個坑
初始化一個全新的 MonoSpecs 項目
調用 InitializeManagementDocumentAsync 之後,磁盤上會出現這樣的結構:
my-project/├── .gitignore # 新增 repos/ 忽略規則(冪等,不重複追加)├── .hagicode/│ └── monospecs.yaml # 最小模板:version / commit_when_archive / repositories: []├── openspec/│ ├── changes/archive/│ └── specs/└── repos/ # 空目錄,等待 clone這裡有幾個邊界要注意,都是從 spec 裡摳出來的:
- 冪等:已存在的
repos/、openspec/目錄會被保留,不會報錯。 - 不覆蓋:若
monospecs.yaml已存在且能正常解析,初始化不會動它,只補齊缺失的.gitignore規則和 openspec 目錄。 - 拒絕髒配置:已存在但解析不了的
monospecs.yaml會被直接拒絕,返回可診斷的錯誤信息,絕對不覆蓋。 - 不自動掃描:初始化不會自作主張把磁盤目錄掃描成倉庫條目,
repositories默認空,需要你手工或通過 UI 填寫。
配置文件位置的遷移陷阱
歷史上 monospecs.yaml 曾經放在項目根目錄,後來強制遷移到 .hagicode/monospecs.yaml。這一點 spec 裡寫得很明確:
根目錄的
monospecs.yaml不再被檢測,也不作為兼容回退。clone 腳本只認.hagicode/monospecs.yaml。
所以老項目升級的時候,必須手工執行 mv monospecs.yaml .hagicode/monospecs.yaml,沒有任何靜默兼容的路徑。乍看有點不近人情,可仔細想,這是為了徹底消除”兩個位置都可能生效”的歧義——這種歧義一旦存在,排查問題的時候能把人逼瘋,畢竟誰也不想在兩個文件之間來回找答案。
保存校驗:別寫出無效配置
通過 SaveManagementDocumentAsync 寫回之前,服務會做字段級校驗。幾個典型的拒絕場景:
- 兩個倉庫條目
path重複 → 拒絕,返回衝突字段。 - 任一條目缺
path→ 拒絕,返回必填錯誤。 url非空但不是合法的絕對 URL → 拒絕。
校驗通過後才會序列化為 YAML 寫盤,同時失效該項目路徑的配置緩存,保證下次讀取拿到的是最新內容。這一步看起來瑣碎,但能避免無數”為什麼我改了配置沒生效”的工單,畢竟這些工單多了,誰也扛不住。
workspace 模式 vs 手工 repositories 模式
配置文件支持兩種派生倉庫列表的方式。
一種是手工 repositories 模式,直接在 YAML 裡列出每條倉庫,管理文檔標記為可編輯。
另一種是workspace 模式,聲明一個 .code-workspace 文件,由它派生倉庫列表。這種模式下管理文檔被標記為只讀,禁止直接改寫倉庫數組,只能改受支持的頂層字段。
我們自己的 HagiCode Mono 目前註釋掉了 workspace 模式,採用手工模式。原因很簡單:手工模式可以精細地控制每個倉庫的 icon 和 tags,UI 展示效果更可控。怎麼說呢,能掌控的東西,心裡總會踏實一點罷了。
給 AI Agent 的實戰建議
現在 AI 編程越來越普及,MonoSpecs 這套方案其實還有一個隱含價值:它給 AI 提供了一份結構化的項目地圖。
在多倉庫協作時,AGENTS.md 和 monospecs.yaml 是給 AI 的兩份關鍵上下文。建議的工作流是這樣的:
- 先讀
monospecs.yaml拿到倉庫拓撲,搞清楚誰可編輯、誰是 reference-only。 - 再讀根
AGENTS.md的 “Active Edit Scope”,確認當前允許修改的範圍。 - 跨倉庫變更統一在主倉庫根的
openspec/changes/寫提案,不要在各子倉庫裡另起 openspec。
這套約定讓 AI 能穩定地理解”主倉庫管 specs、子倉庫管代碼”的分工,而不會誤把 spec 寫進子倉庫——這種誤操作我們之前踩過好幾次坑。其實也不怪 AI,畢竟子倉庫和主倉庫長得那麼像,誰能一眼分清呢?
總結
一句話總結:OpenSpec 定義了”變更怎麼寫”,MonoSpecs 定義了”倉庫怎麼擺”。
前者是後者的語法基礎,後者把前者從單倉庫語境擴展到多倉庫語境,並用一個 YAML 清單把倉庫拓撲、clone 流程、AI 上下文、specs 歸屬一次性收斂起來。這就是”monospec 是對 openspec 的進一步升級和擴展”的真正含義——不是替代,而是在其之上加了一層多倉庫語義。
如果你也在做類似規模的多倉庫產品,不妨想想這兩層是不是也都鋪好了。規範寫得再漂亮,沒有清晰的倉庫治理撐著,最終還是會亂成一鍋粥…
參考資料
- HagiCode 官網
- HagiCode-org/site GitHub 倉庫
- OpenSpec 工作流文檔
- MonoSpecs 相關 spec:
monospecs-guide、monospecs-repository-config、monospec-config-management
總結
圍繞”MonoSpecs 是什麼:為什麼說它是對 OpenSpec 的進一步升級和擴展”,更穩妥的推進方式是先把關鍵配置、依賴邊界和落地路徑逐步跑通,再補齊優化細節。
當目標、步驟和驗收點都明確之後,這類方案通常就能更順暢地進入實際交付。
开始使用 HagiCode
一次安装,几分钟上手
HagiCode for Windows 在 Microsoft Store 免费提供。打开商店即可安装并保持更新;也可以先对比各版本与定价,再决定从哪个渠道开始。