跳转到内容

MonoSpecs 是什麼:為什麼說它是對 OpenSpec 的進一步升級和擴展

编辑此页
HagiCode for Windows Microsoft Store artwork
HagiCode for Windows is now on Microsoft Store
HagiCode for Windows is officially live on Microsoft Store. Windows users can install it directly from the storefront and stay on the store-managed update path. Open the listing and take a look.
Open Microsoft Store

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 就是個配置清單,其實它的價值更多在第二層——一種明確的多倉庫協作範式。其實,美的事物往往不在第一眼,得多看幾眼罷了。

為什麼說是”升級和擴展”

把兩者放在一起對比,關係就清晰了:

維度OpenSpecMonoSpecs
關注點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 根目錄放好配置文件,聲明所有子倉庫。以我們自己的項目為例,結構大致是這樣:

.hagicode/monospecs.yaml
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 服務層

如果不抽一層抽象出來,配置解析邏輯很容易散落在 GitAppServiceProjectAppService 各個角落。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.yamlrepos/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.mdmonospecs.yaml 是給 AI 的兩份關鍵上下文。建議的工作流是這樣的:

  1. 先讀 monospecs.yaml 拿到倉庫拓撲,搞清楚誰可編輯、誰是 reference-only。
  2. 再讀根 AGENTS.md 的 “Active Edit Scope”,確認當前允許修改的範圍。
  3. 跨倉庫變更統一在主倉庫根的 openspec/changes/ 寫提案,不要在各子倉庫裡另起 openspec。

這套約定讓 AI 能穩定地理解”主倉庫管 specs、子倉庫管代碼”的分工,而不會誤把 spec 寫進子倉庫——這種誤操作我們之前踩過好幾次坑。其實也不怪 AI,畢竟子倉庫和主倉庫長得那麼像,誰能一眼分清呢?

總結

一句話總結:OpenSpec 定義了”變更怎麼寫”,MonoSpecs 定義了”倉庫怎麼擺”

前者是後者的語法基礎,後者把前者從單倉庫語境擴展到多倉庫語境,並用一個 YAML 清單把倉庫拓撲、clone 流程、AI 上下文、specs 歸屬一次性收斂起來。這就是”monospec 是對 openspec 的進一步升級和擴展”的真正含義——不是替代,而是在其之上加了一層多倉庫語義。

如果你也在做類似規模的多倉庫產品,不妨想想這兩層是不是也都鋪好了。規範寫得再漂亮,沒有清晰的倉庫治理撐著,最終還是會亂成一鍋粥…

參考資料

總結

圍繞”MonoSpecs 是什麼:為什麼說它是對 OpenSpec 的進一步升級和擴展”,更穩妥的推進方式是先把關鍵配置、依賴邊界和落地路徑逐步跑通,再補齊優化細節。

當目標、步驟和驗收點都明確之後,這類方案通常就能更順暢地進入實際交付。

开始使用 HagiCode

一次安装,几分钟上手

HagiCode for Windows 在 Microsoft Store 免费提供。打开商店即可安装并保持更新;也可以先对比各版本与定价,再决定从哪个渠道开始。