Lionshead

The reconciler: distributing standards across ten repos without a monorepo

Most dev shops don't have standards. At least not codified and documented standards. They have a Confluence space with someone's 2021 attempt at a style guide buried under three years of half-finished runbooks and obsolete service docs. The rules aren't enforced because nobody reads them. The shops that do enforce standards pay for it: a platform team, a custom lint server, a quarterly review cadence, and someone whose job is to argue with developers about line length.

Lionshead has standards, and they're enforced for free. They live in code, in .claude/rules/ and a BMAD module, and the AI agents doing the keystrokes read them on every story. There's no human in the middle to forget the rule; the agent reads it and writes the code in the same breath. That's not the same trade as "humans who follow the rules better." The AI is doing the writing too; the free enforcement is a side effect of swapping the actor, not a process improvement layered over a human team. That changes the problem. The question stops being "how do we get developers to follow the rules" and starts being "how do we get the same rules into ten unrelated product repos without a monorepo or a platform team."

This post is the answer. A small CLI I call the reconciler: pull-based, hash-compared, idempotent. It ships a manifest from a central repo and lets each consumer adopt the parts it wants. Here is what it does, what it costs to build, and where it ends.

I have operated at the ten-repo scale before, and the reconciler is built for that target. Lionshead today is one product (Vesper); this is the system for where I am going, not where I am.

The shape of the problem

The familiar approaches to keeping many repos in sync all trade the same three axes: friction at edit time, friction at consumer time, and how much shared infrastructure the consumers have to swallow. Pick any two, lose the third.

Copy-paste. Zero infrastructure. The cost shows up at every fix: ten repos, ten patches, and the moment one repo skips a patch the divergence starts. Worse, copy-paste makes the canonical version invisible. No one can grep it without first deciding which repo's copy to trust. I shipped two months on copy-paste before the cost compounded; it compounds fast.

Git submodules. One source of truth, a real link between consumer and producer, and a developer experience that is genuinely awful. Submodules break IDE checkouts, break clone scripts, and force every consumer to handle a sub-checkout step that someone new will get wrong on day one. I tried submodules for two days; the cost showed up faster than copy-paste.

Monorepo. Solves drift completely. Also forces every product onto a shared CI, a shared release cadence, and a shared dependency graph. That cost is paid even by products that have nothing to do with each other. The big-company default; the wrong default for the operator who runs ten small unrelated things.

All three trade the same axes in different proportions. The reconciler trades a fourth axis: it accepts a small per-consumer install cost in exchange for keeping every consumer's repo shape, CI, release cadence, and dependency graph independent. That is the only trade I was willing to make at the ten-repo scale.

What the reconciler actually does

The CLI is published to GitHub Packages under the @lionsheaddigital scope. Consumers configure their npm token once per machine. After that, npx @lionsheaddigital/setup just works.

The CLI reads a single declarative manifest, lionshead.json, from the central ci-standards repo over plain HTTPS to raw.githubusercontent.com. The manifest lists every standards file the central repo owns: BMAD module sources, the Claude Code rule files, GitHub Actions reusable workflows, the GitHub project sync script, and a few others. Each entry carries a sha256 hash of the upstream file.

For every file declared in the manifest, the CLI walks one branch:

The heart of the loop is a hash comparison:

// packages/setup/lib/reconcile.mjs (excerpt)
function computeDelta(remoteStandards, localInstalled) {
  const remoteFiles = remoteStandards?.files ?? [];
  const localFiles = localInstalled?.files ?? [];
 
  const localMap = new Map(localFiles.map((f) => [f.path, f.sha256]));
  // ...
 
  const toDownload = [];
  for (const rf of remoteFiles) {
    const localHash = localMap.get(rf.path);
    if (localHash !== rf.sha256) {
      toDownload.push(rf);
    }
  }
  // ...
}

If a consumer's local copy of a declared file already matches the upstream hash, the CLI does nothing for that file. If the hash differs (the consumer is missing the file, or upstream has moved on, or someone hand-edited the local copy), the CLI downloads the upstream version and overwrites the local one. No prompt. No merge. No three-way confusion. Upstream wins; local edits to managed files are not allowed.

The full run on a clean consumer takes about three seconds. The full run on a clean, up-to-date consumer takes about one second and prints "Standards up to date." That second case is the one that earned the reconciler its name: it is safe to run a hundred times in a row, and the only side effect is that the operator knows the consumer is current.

The hard parts

The interesting design decisions in this kind of CLI are not the visible ones (the manifest shape, the file copy). They are the boring ones underneath.

Idempotency is the first hard part. The hash-compare and the conditional download together give you "do nothing when nothing has drifted." That looks easy until you find the case where BMAD's own installer silently overwrites a managed file with stale bundled content after the main download loop has already updated it. The reconciler now does a second verify-and-restore pass after BMAD runs; without it, a clean run would show "Standards up to date" while the on-disk SHA had quietly diverged from the manifest. That pass cost a day to discover and an afternoon to write. It is the kind of bug that only shows up at the seam between two tools that both think they own the same files.

Partial adoption is the second hard part. Not every consumer wants every standard. A static marketing site does not need the Test Architect workflow; a backend service does not need the design-tokens rule. The manifest supports per-consumer opt-outs via a small lionshead.json in the consumer repo that lists explicit skip paths. The escape hatch is necessary; without it the reconciler is a coupling mechanism dressed up as a distribution one.

Upgrade safety is the third hard part. The reconciler is itself one of the things it ships. Bootstrapping a brand-new consumer is a chicken-and-egg moment: the consumer does not yet have the CLI installed, but the CLI is what installs the CLI. The answer is npx: the CLI runs without a prior install, fetches itself from GitHub Packages, runs once, and exits. The local install only matters for subsequent runs, and those can be npx too. The whole bootstrap is one command.

When NOT to do this

The reconciler is right anywhere above three repos. Below three, copy-paste is genuinely cheaper, and the per-file maintenance cost is small enough that the operator can absorb drift mentally. The architecture has no real upper bound on consumer count: the CLI is stateless, every consumer is independent, and adding one is one npx away.

The real ceiling is governance, not scale. Review doesn't disappear in this model; it shifts upstream to the central standards repo, where PRs are the negotiation surface. That works fine when there's an agreed owner of the central repo. It breaks down when there isn't: the reconciler's "upstream wins" semantics turn a governance question into a technical one consumers can't appeal. A monorepo handles disagreement by making every change visible to all consumers in the same commit; the reconciler doesn't.

And the reconciler is wrong when drift is a feature, not a bug. Research codebases, demo branches, throwaway prototypes: these benefit from divergence. A pull-based "upstream wins" CLI run against a demo branch will quietly undo the demo. The right fix there is to mark the consumer out of scope, not to weaken the reconciler.

Outro: the Building Lionshead thread

The reconciler is one chapter in the Building Lionshead series. Post one was a sketch of why I built a company-specific BMAD module; the post you are reading is what the distribution mechanism for that module looks like in code. The series's spine is "small-company process at small-company cost"; this post is one chapter of it.

The next post in the series will be about the BMAD module itself: what is in it, what the boundary is between Lionshead-shaped behavior and stock BMAD behavior, and why I keep that boundary thin. After that, a post on the rule files in .claude/rules/ and the way they get loaded into agent context. Both posts depend on the reconciler existing; this post is the prerequisite.

  • building-lionshead
  • process
  • ci-cd
  • monorepo-alternatives