What is MonoSpecs: Why It's a Further Upgrade and Extension of OpenSpec
When a product system expands to 40+ independent Git repositories, where should "specifications" be placed? This article discusses the two steps HagiCode took in multi-repository governance: first lifting OpenSpec to the main repository, then developing MonoSpecs as a multi-repository management solution on top of it. Really nothing special, just stumbled through some pitfalls and wanted to document them.
Background
Anyone who's worked on a slightly larger product has probably had this experience—initially the code was in a single repository, well-organized and peaceful; later the frontend, backend, desktop app, documentation site, official website, and build tools each became independent repositories, and the number of repositories kept growing like wild grass, unstoppable. Then when you wanted to write a "specification document" for some cross-repository functionality, suddenly you didn't know where to write it—it's a bit like childhood pocket money, suddenly gone without a trace.
Our own HagiCode is such a product system composed of 40+ independent Git repositories. In the early days, we directly stuffed OpenSpec's openspec/ directory into the backend sub-repository hagicode-core, thinking that since the backend is the core, placing it there would be most stable. As the number of repositories grew, this approach exposed a series of headache-inducing problems. After all, the code world never actually becomes stable just because you "thought it would be stable."
The first pain point: specs are trapped in a single sub-repository. If a functionality affects both the frontend web and the backend hagicode-core, I have to write proposals in hagicode-core, then go to other sub-repositories to execute code changes. Which repository a proposal belongs to itself becomes a controversy.
The second pain point: sub-repositories are not pure. Each sub-repository carries its own openspec/, mixing specification documents with product code. Someone clones your frontend repository and ends up bringing back a bunch of backend proposal documents, completely bewildered.
The third pain point: AI Agents struggle to understand repository relationships. Each sub-repository is independent, with no machine-readable "manifest" telling the AI: what repositories this product consists of, what each is responsible for, which is editable, which is read-only reference.
The fourth pain point: high cost of cross-repository editing. To modify a spec, you must first cd into the corresponding sub-module, paths jump around, creating significant cognitive burden for collaboration.
Against this background, we first performed an "OpenSpec Monorepo Migration," lifting specs from sub-repositories to the monorepo root directory. Then on top of it, we developed MonoSpecs as a multi-repository management solution. Understanding the progressive relationship between these two steps is the key to understanding "why MonoSpecs is a further upgrade and extension of OpenSpec."
About HagiCode
The solution shared in this article comes from our practical experience in the HagiCode project. HagiCode is an AI code assistant project with many repositories and frequent cross-language collaboration. This structural complexity forced us to make both "specifications" and "repository governance" solid. The MonoSpecs solution was gradually polished through multi-repository practice—no brilliant strokes, just a few more steps taken.
OpenSpec Solves "How Specifications Are Written and Evolved"
To clarify the relationship between the two, we need to first look at what each is responsible for.
OpenSpec is essentially a spec-driven change management workflow. Its core artifacts look like this:
openspec/
├── specs/ # Currently active capability specifications (one spec.md per capability)
├── changes/ # In-progress proposals
│ └── archive/ # Archived historical proposals
└── project.md
It answers the question: a change goes through a lifecycle of proposal, design, tasks, archive, and merges deltas into specs when archiving. This mechanism itself is unrelated to "how many repositories there are, where they are, who manages them"—it only cares about how spec files are organized.
Through a migration proposal, we lifted 82+ spec files originally scattered in hagicode-core/openspec/ to the monorepo root's openspec/, making all specs visible in one place with unified version control.
But this migration was essentially just "moving spec files," and didn't answer the more fundamental question: what sub-repositories does this monorepo actually consist of? What are the relationships between these sub-repositories? This is what MonoSpecs fills in.
MonoSpecs Solves "How Multi-Repositories Themselves Are Managed"
The core of MonoSpecs is a machine-readable manifest file: .hagicode/monospecs.yaml. It does four things that OpenSpec doesn't address at all.
The first: declare sub-repository manifest. Each repository's path, url, displayName, icon, tags, whether collapsed to "More", all written in a single YAML, clear at a glance.
The second: drive clone scripts. scripts/clone-repos.mjs reads this YAML directly, batch git clone, no longer hardcoding repository lists. Adding a new repository just requires one line in the YAML, the script needs zero changes.
The third: provide project structure context for AI/IDE. Paired with AGENTS.md, AI Agents can instantly see which repository is editable, which is reference-only, and what the tech stack is.
The fourth: anchor OpenSpec artifacts to the main repository. Specs are no longer scattered across sub-repositories, but unified in the main repository root's openspec/, keeping sub-repositories pure.
Two Layers of Meaning, Don't Get Confused
In MonoSpecs' official guide, a very confusing point is clearly pointed out: MonoSpecs actually has two layers of meaning.
One layer is the configuration system layer, referring to the .hagicode/monospecs.yaml configuration file itself, along with its accompanying loading, validation, and caching mechanisms.
The other layer is the repository type layer, referring to a "main repository + multiple sub-repositories + centralized specs" repository organization pattern. When we say a project "is a MonoSpecs project," it means it adopts this structure.
These two layers叠加 together form the complete MonoSpecs. Many people encountering it for the first time only see the YAML file layer, thinking MonoSpecs is just a configuration manifest, but its value is actually more in the second layer—a clear multi-repository collaboration paradigm. Beautiful things often aren't visible at first glance; you need to look a few more times.
Why "Upgrade and Extension"
Putting them side by side, the relationship becomes clear:
| Dimension | OpenSpec | MonoSpecs |
|---|---|---|
| Focus | spec file content and lifecycle | repository organization structure and manifest |
| Core artifact | openspec/specs/*/spec.md |
.hagicode/monospecs.yaml |
| Depends on the other | Doesn't depend on MonoSpecs | Depends on OpenSpec, reuses its openspec/ for change management |
| Pain point solved | How specifications are written and evolved | How multi-repositories are declared, cloned, and understood by AI |
| Scope | Can be used in any repository | Designed specifically for "one main multiple sub" multi-repository structure |
Fundamentally, MonoSpecs doesn't replace OpenSpec, but adds a layer of "repository governance" on top of it. Using monospecs.yaml to describe repository topology, using centralized openspec/ to decouple specs from sub-repositories, using commit_when_archive to automatically archive commits to the main repository.
If using an analogy: OpenSpec provides "change syntax," MonoSpecs provides "multi-repository semantics." The former is the premise of the latter, the latter is an extension of the former. All roads lead to Rome, just this time, the road is a bit longer than imagined.
How to Implement: Four Steps
Step 1: Establish Main Repository and Configuration File
Place the configuration file in the monorepo root directory, declaring all sub-repositories. Taking our own project as an example, the structure is roughly like this:
# .hagicode/monospecs.yaml
version: "1.0"
commit_when_archive: true
repositories:
- path: "repos/web"
url: "https://github.com/HagiCode-org/web.git"
displayName: "Frontend"
tags: [frontend, react, pcode-client]
- path: "repos/hagicode-core"
url: "https://github.com/newbe36524/pcode"
displayName: "Backend"
tags: [backend, dotnet, orleans]
- path: "repos/docs"
url: "https://github.com/HagiCode-org/docs.git"
displayName: "Docs"
tags: [docs, astro, starlight]
ui:
collapseToMore: true # Collapse to "More" in the UI
There are a few fields to pay special attention to:
-
pathis the local path relative to the main repository root, and is also the unique key for each record. -
urlis the Git remote address, which the clone script relies on to pull code. -
displayName/icon/tagsonly affect UI display and AI context, not clone behavior. -
commit_when_archive: trueautomatically commits to the main repository when OpenSpec proposals are archived.
Step 2: Lift OpenSpec to Main Repository Root Directory
Before and after migration comparison:
Before migration (specs trapped in sub-repository) After migration (specs centralized in main repository)
hagicode-core/ . (main repository root)
└── openspec/ ├── .hagicode/monospecs.yaml
└── specs/ (82+ specs) ├── openspec/
│ ├── specs/ (centralized management)
│ └── changes/
└── repos/
├── hagicode-core/ (pure, no openspec)
├── web/
└── docs/
From then on, sub-repositories no longer carry openspec/, and the main repository becomes the sole source of truth for specs. This step looks simple, but the benefits it brings are very practical—any engineer standing at the main repository root can see all specifications for the entire product system.
Step 3: Make Clone Scripts Read Configuration Instead of Hardcoding
The core logic of scripts/clone-repos.mjs is to read YAML and clone one by one:
const CONFIG_PATH = path.join(__dirname, '..', '.hagicode', 'monospecs.yaml');
// Parse repositories array
// Execute git clone <url> <path> for each entry
// Skip or git pull if target directory already exists
When adding a new repository, you only need to add one line in the YAML, without touching the script. This small change saves countless "forgot to sync repository list" disputes. After all, who wants to repeat labor?
Step 4: Backend Provides Unified MonoSpecs Service Layer
If you don't extract an abstraction layer, configuration parsing logic can easily scatter across GitAppService, ProjectAppService and other corners. HagiCode extracted IMonoSpecsService in the ClaudeHelper module, exposing a set of clear capabilities:
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);
}
This service is responsible for loading, validating, caching configuration, and providing "initialize minimal template" capability—one-click generation of monospecs.yaml, repos/, openspec/changes/archive/, openspec/specs/ skeleton for an empty project, and automatically completing .gitignore. Caching, like memory, once remembered, next time you don't have to think hard.
Several Pitfalls in Practice
Initializing a Brand New MonoSpecs Project
After calling InitializeManagementDocumentAsync, this structure will appear on disk:
my-project/
├── .gitignore # Added repos/ ignore rules (idempotent, won't duplicate append)
├── .hagicode/
│ └── monospecs.yaml # Minimal template: version / commit_when_archive / repositories: []
├── openspec/
│ ├── changes/archive/
│ └── specs/
└── repos/ # Empty directory, waiting for clone
There are a few boundaries to pay attention to here, all extracted from specs:
-
Idempotent: Existing
repos/,openspec/directories will be preserved and won't error. -
No overwrite: If
monospecs.yamlalready exists and can be parsed normally, initialization won't touch it, only supplements missing.gitignorerules and openspec directories. -
Reject dirty config: Existing but unparsable
monospecs.yamlwill be directly rejected, returning diagnosable error information, absolutely no overwrite. -
No auto-scan: Initialization won't scan disk directories into repository entries on its own,
repositoriesdefaults to empty, requiring manual or UI filling.
Configuration File Location Migration Trap
Historically, monospecs.yaml was once placed in the project root, later forcibly migrated to .hagicode/monospecs.yaml. This is clearly stated in the spec:
Root directory's
monospecs.yamlis no longer detected, nor as a compatibility fallback. Clone scripts only recognize.hagicode/monospecs.yaml.
So when upgrading old projects, you must manually execute mv monospecs.yaml .hagicode/monospecs.yaml, with no silent compatibility path. At first glance it seems unreasonable, but thinking carefully, this is to completely eliminate the ambiguity of "both locations might take effect"—once this ambiguity exists, troubleshooting can drive people crazy. After all, who wants to look for answers between two files?
Save Validation: Don't Write Invalid Configuration
Before writing back through SaveManagementDocumentAsync, the service performs field-level validation. Several typical rejection scenarios:
- Two repository entries with duplicate
path→ reject, return conflicting fields. - Any entry missing
path→ reject, return required field error. -
urlis non-empty but not a valid absolute URL → reject.
Only after validation passes will it serialize to YAML and write to disk, while invalidating that project path's configuration cache, ensuring the next read gets the latest content. This step looks trivial, but can avoid countless "why didn't my configuration changes take effect" tickets. After all, when these tickets accumulate, who can bear it?
Workspace Mode vs Manual Repositories Mode
The configuration file supports two ways to derive repository lists.
One is manual repositories mode, directly listing each repository in the YAML, management document marked as editable.
The other is workspace mode, declaring a .code-workspace file, which derives the repository list. In this mode the management document is marked as read-only, prohibiting direct modification of the repository array, only modifying supported top-level fields.
Our own HagiCode Mono currently has workspace mode commented out, using manual mode. The reason is simple: manual mode allows fine-grained control of each repository's icon and tags, making UI display effects more controllable. As they say, things you can control always make you feel more at ease.
Practical Advice for AI Agents
Now with AI programming becoming increasingly popular, the MonoSpecs solution actually has an implicit value: it provides AI with a structured project map.
In multi-repository collaboration, AGENTS.md and monospecs.yaml are two key contexts for AI. The recommended workflow is:
- First read
monospecs.yamlto get repository topology, understanding who is editable, who is reference-only. - Then read root
AGENTS.md's "Active Edit Scope" to confirm the currently allowed modification scope. - Cross-repository changes uniformly write proposals in the main repository root's
openspec/changes/, don't start separate openspec in each sub-repository.
This convention lets AI stably understand the division of "main repository manages specs, sub-repositories manage code," and won't mistakenly write specs into sub-repositories—we've stumbled into this pit several times before. Actually can't blame AI, after all sub-repositories and main repositories look so similar, who can tell at a glance?
Summary
One sentence summary: OpenSpec defines "how changes are written," MonoSpecs defines "how repositories are placed."
The former is the grammatical foundation of the latter, the latter extends the former from single-repository context to multi-repository context, and converges repository topology, clone process, AI context, and specs ownership in one go with a YAML manifest. This is the true meaning of "MonoSpecs is a further upgrade and extension of OpenSpec"—not replacement, but adding a layer of multi-repository semantics on top of it.
If you're also working on a multi-repository product of similar scale, consider whether these two layers are both laid out properly. No matter how beautifully specifications are written, without clear repository governance supporting them, eventually it will all become a chaotic mess......
References
- HagiCode Official Website
- HagiCode-org/site GitHub Repository
- OpenSpec Workflow Documentation
- MonoSpecs related specs:
monospecs-guide,monospecs-repository-config,monospec-config-management
Summary
Surrounding "What is MonoSpecs: Why It's a Further Upgrade and Extension of OpenSpec," a more prudent advancement approach is to first gradually run through key configurations, dependency boundaries, and implementation paths, then supplement optimization details.
Once goals, steps, and acceptance criteria are clear, such solutions can usually more smoothly enter actual delivery.
Original Article & License
Thanks for reading. If this article helped, consider liking, bookmarking, or sharing it.
This article was created with AI assistance and reviewed by the author before publication.
- Author: newbe36524
- Original URL: https://docs.hagicode.com/go?platform=devto&target=%2Fblog%2F2026-06-20-monospecs-openspec-upgrade-and-extension%2F
- License: Unless otherwise stated, this article is licensed under CC BY-NC-SA. Please retain attribution when sharing.
Top comments (0)