Skip to content

What is MonoSpecs: Why It's a Further Upgrade and Extension of OpenSpec

Edit page
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

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 HagiCode’s two-step approach to multi-repository governance: first moving OpenSpec to the main repository, then developing MonoSpecs as a multi-repository management solution on top of it. Not really anything special, just documenting some lessons learned from pitfalls encountered.

Background

For those who have worked on slightly larger products, you’ve probably had this experience—at first the code was in one repository, well-organized and peaceful; later frontend, backend, desktop, documentation site, official site, build tools each became independent repositories, and the number of repositories kept growing like weeds that couldn’t be stopped. Later, when you wanted to write a “specification document” for a cross-repository feature, you suddenly didn’t know where to write it. How should I put it, it’s a bit like childhood pocket money that suddenly disappeared.

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 backend is the core, it’s safest there. As a result, as we split more and more repositories, this solution exposed a series of headache-inducing problems. After all, the code world never really becomes stable just because you “think it’s stable.”

First pain point: specs trapped in a single sub-repository. If a feature affects both frontend web and backend hagicode-core, I have to write a proposal in hagicode-core, then run to other sub-repositories to execute code changes. Which repository should the proposal belong to becomes a controversy itself.

Second pain point: sub-repositories are impure. 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 confused.

Third pain point: AI Agents struggle to understand repository relationships. Each sub-repository is independent of each other, without a machine-readable “manifest” telling AI: what repositories this product consists of, what each is responsible for, which is editable, and which is read-only reference.

Fourth pain point: high cost of cross-repository editing. To change a spec, you must first cd into the corresponding submodule, paths jumping around, creating huge cognitive burden for collaboration.

Against this background, we first performed an “OpenSpec Monorepo Migration”, moving specs from sub-repositories up to the monorepo root directory. Then on top of that, we developed MonoSpecs as a multi-repository management solution. Understanding the progressive relationship between these two steps is 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 forces us to do a solid job on both “specifications” and “repository governance.” The MonoSpecs solution was gradually polished in this multi-repository practice. There’s no stroke of genius, just taking a few more steps.

OpenSpec Solves “How to Write and Evolve Specifications”

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 output looks like this:

openspec/
├── specs/ # Currently effective capability specifications (one spec.md per capability)
├── changes/ # Proposals in progress
│ └── archive/ # Archived historical proposals
└── project.md

The question it answers: a change goes through lifecycle stages like proposal (proposal), design (design), tasks (tasks), archive (archive), and merges deltas into specs when archiving. This mechanism itself has nothing to do with “how many repositories there are, where they are, or who manages them.” It only cares about how spec files are organized.

Through a migration proposal, we moved 82+ spec files originally scattered in hagicode-core/openspec/ up to the monorepo root directory’s openspec/, making all specs uniformly visible and version-controlled in one place.

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 is meant to fill in.

MonoSpecs Solves “How to Manage Multi-Repositories Themselves”

The core of MonoSpecs is a machine-readable manifest file: .hagicode/monospecs.yaml. It does four things that OpenSpec doesn’t touch at all.

First: declare sub-repository manifest. Each repository’s path, url, displayName, icon, tags, and whether to collapse to “More” are all written in one YAML, clear at a glance.

Second: drive clone scripts. scripts/clone-repos.mjs directly reads this YAML and batches git clone, no longer hardcoding the repository list. Adding a new repository only requires adding a line in YAML, with zero script changes.

Third: provide project structure context for AI/IDE. Working with AGENTS.md, AI Agents can see at a glance which repository is editable, which is reference-only, and what the tech stack is.

Fourth: anchor OpenSpec artifacts to the main repository. Specs no longer scatter to each sub-repository, but are uniformly collected in the main repository root directory’s openspec/, keeping sub-repositories clean.

Two Meanings, Don’t Confuse Them

In MonoSpecs’ official guide, it explicitly points out a very confusing place: MonoSpecs actually has two meanings.

One is the configuration system layer, referring to the .hagicode/monospecs.yaml configuration file itself, along with its associated loading, validation, and caching mechanisms.

The other 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 superimposed together make up the complete MonoSpecs. Many people first encounter it and only see the YAML file layer, thinking MonoSpecs is just a configuration manifest. In fact, its value lies more in the second layer—a clear multi-repository collaboration paradigm. Actually, beautiful things often aren’t seen at first glance; you need to look a few more times.

Why Call It “Upgrade and Extension”

Put them together for comparison, and the relationship becomes clear:

DimensionOpenSpecMonoSpecs
Focusspec file content and lifecyclerepository organization structure and manifest
Core artifactopenspec/specs/*/spec.md.hagicode/monospecs.yaml
Dependency on the otherDoesn’t depend on MonoSpecsDepends on OpenSpec, reuses its openspec/ for change management
Pain point solvedHow to write and evolve specificationsHow to declare multi-repositories, how to clone, how AI understands
ScopeCan be used in any repositoryDesigned specifically for “one main multiple sub” multi-repository structures

To put it bluntly, MonoSpecs doesn’t replace OpenSpec, but adds a layer of “repository governance” on top of it. Use monospecs.yaml to describe repository topology, use centralized openspec/ to decouple specs from sub-repositories, use commit_when_archive to automatically commit archives to the main repository.

If we use an analogy: OpenSpec provides “change syntax,” MonoSpecs provides “multi-repository semantics.” The former is the prerequisite for the latter, the latter is an extension of the former. All roads lead to Rome, but 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 looks 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: "Documentation"
tags: [docs, astro, starlight]
ui:
collapseToMore: true # Collapse to "More" in UI

Several fields require special attention:

  • path is the local path relative to the main repository root, and also the unique key for each record.
  • url is the Git remote address, which the clone script relies on to pull code.
  • displayName / icon / tags only affect UI display and AI context, not clone behavior.
  • commit_when_archive: true automatically commits to the main repository when OpenSpec proposals are archived.

Step 2: Move OpenSpec to Main Repository Root Directory

Comparison before and after migration:

Before migration (specs trapped in sub-repo) After migration (specs centralized in main repo)
hagicode-core/ . (main repo root)
└── openspec/ ├── .hagicode/monospecs.yaml
└── specs/ (82+ specs) ├── openspec/
│ ├── specs/ (centralized management)
│ └── changes/
└── repos/
├── hagicode-core/ (clean, no openspec)
├── web/
└── docs/

Sub-repositories no longer carry openspec/, and the main repository becomes the single source of truth for specs. This step looks simple, but brings very real benefits—any engineer standing at the main repository root directory 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 entry by entry:

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 exists

When adding a new repository, you only need to add a line in YAML, without touching the script. This small change saves countless “forgot to sync repository list” arguments. After all, who wants to do repetitive work?

Step 4: Backend Provides Unified MonoSpecs Service Layer

If you don’t extract an abstraction layer, configuration parsing logic easily scatters 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, and caching configuration, and provides the ability to “initialize minimal template”—one-click generation of monospecs.yaml, repos/, openspec/changes/archive/, openspec/specs/ skeleton for an empty project, and automatic completion of .gitignore. Caching is like memory; once remembered, you don’t need to work hard to think about it next time.

Several Pitfalls in Practice

Initializing a Brand New MonoSpecs Project

After calling InitializeManagementDocumentAsync, this structure appears on disk:

my-project/
├── .gitignore # Add repos/ ignore rules (idempotent, no duplicate appending)
├── .hagicode/
│ └── monospecs.yaml # Minimal template: version / commit_when_archive / repositories: []
├── openspec/
│ ├── changes/archive/
│ └── specs/
└── repos/ # Empty directory, waiting for clone

There are several boundaries to note, all extracted from the spec:

  • Idempotent: Existing repos/ and openspec/ directories are preserved, no errors.
  • No overwrite: If monospecs.yaml already exists and can be parsed normally, initialization won’t touch it, only supplementing missing .gitignore rules and openspec directories.
  • Reject dirty config: Existing but unparseable monospecs.yaml is directly rejected, returning diagnosable error information, absolutely no overwriting.
  • No auto-scan: Initialization won’t take the initiative to scan disk directories into repository entries, repositories defaults to empty, requiring you to manually fill or fill through UI.

Migration Trap for Configuration File Location

Historically monospecs.yaml was placed in the project root directory, later forcibly migrated to .hagicode/monospecs.yaml. This point is very clear in the spec:

Root directory monospecs.yaml is no longer detected, nor as a compatibility fallback. Clone scripts only recognize .hagicode/monospecs.yaml.

Therefore, 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 on careful thought, this is to completely eliminate the ambiguity of “both locations might take effect”—once this ambiguity exists, troubleshooting problems can drive people crazy. After all, who wants to search for answers back and forth between two files?

Save Validation: Don’t Write Invalid Configurations

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.
  • url non-empty but not a valid absolute URL → Reject.

Only after validation passes will it serialize to YAML and write to disk, while invalidating the configuration cache for that project path, ensuring the next read gets the latest content. This step looks trivial, but can avoid countless “why didn’t my configuration change take effect” tickets. After all, if there are too many such tickets, who can handle 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 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 rewriting of the repository array, only allowing modification of supported top-level fields.

Our own HagiCode Mono currently has the workspace mode commented out, using manual mode. The reason is simple: manual mode allows fine control over each repository’s icon and tags, making UI display effects more controllable. How should I put it, things you can control always feel a bit more reassuring.

Practical Suggestions for AI Agents

As AI programming becomes more 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:

  1. First read monospecs.yaml to get repository topology, understanding which is editable and which is reference-only.
  2. Then read the root AGENTS.md’s “Active Edit Scope” to confirm the currently allowed modification range.
  3. For 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 allows AI to stably understand the division of “main repository manages specs, sub-repositories manage code,” without mistakenly writing specs into sub-repositories—we’ve encountered this mistake several times before. Actually, it’s not AI’s fault; after all, sub-repositories and main repositories look so similar, who can tell at a glance?

Summary

One sentence summary: OpenSpec defines “how to write changes,” MonoSpecs defines “how to place repositories.”

The former is the syntactic 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 YAML manifest. This is the true meaning of “MonoSpecs is a further upgrade and extension of OpenSpec”—not replacement, but adding a multi-repository semantic layer on top of it.

If you’re also working on multi-repository products of similar scale, consider whether both layers are properly set up. No matter how beautifully specifications are written, without clear repository governance support, it will eventually become a mess…

References

Summary

Regarding “What is MonoSpecs: Why It’s a Further Upgrade and Extension of OpenSpec,” a safer approach is to gradually get through key configurations, dependency boundaries, and implementation paths first, then supplement optimization details.

Once goals, steps, and acceptance criteria are clear, such solutions can usually enter actual delivery more smoothly.

开始使用 HagiCode

一次安装,几分钟上手

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