ADR 0004 — BMO lives in this repository#
| Status | Accepted |
| Date | 2026-09-21 |
| Authors | Ruben Knuijver |
| Supersedes | — |
| Superseded by | — |
| Issue | #286 |
Context#
BMO (Benefits.CommandCenter, Benefits.Agent, the bmo CLI) installs and manages
BenefitManager versions on IIS and Windows Services. It is in production, and its API and CLI
tooling are more advanced than CommandCenter's equivalents — the Fleet manager, stage removal and
deploys in this repository already proxy to it or are slated to replace their legacy equivalents
with it.
It lived in github.com/bcs-bv/benefits-mgmt-agent. The next BMO releases depend on CommandCenter
changes, which means the two repositories would have had to be released in lockstep across two
hosts with two CI systems. They also already share a SQL schema without declaring it: BMO reads and
writes TblCustomerStage, TblServerLog, TblDeployLog and TblToolSchedule with raw Dapper,
while CommandCenter.WebApi's EF migrations are what create those tables.
Decision#
Import the whole BMO repository under the top-level prefix bmo/, with full history, using
git subtree add without --squash. Deployments from benefits-mgmt-agent are frozen; there is
no two-way sync to maintain.
Why the whole repository, not a subtree split of src/#
BMO's build files live at its root, and Benefits.CommandCenter.csproj declares no
TargetFramework — it inherits net10.0 from BMO's Directory.Build.props. src/ without that
file does not build. Importing everything keeps history for everything; deleting what has no job
here afterwards is an ordinary commit.
Why a top-level prefix#
MSBuild resolves the nearest Directory.Build.props / Directory.Packages.props walking up —
nearest wins outright, with no merging. With BMO's own files at bmo/, they shadow ours completely
and BMO keeps its target frameworks, its AnalysisModeSecurity=Recommended (ours is All), its
MinVer versioning (ours is Nerdbank.GitVersioning), its own 129-entry package list and its own
nuget.config. Nesting under backend/ would have put a second repository inside CommandCenter's
project tree and inside .claude/rules/backend.md's scope, whose content would then be wrong for
every BMO file.
Verified after the import: Benefits.CommandCenter.csproj evaluates to
TargetFramework=net10.0, AnalysisModeSecurity=Recommended, and no Nerdbank assembly appears in
BMO's build output.
Why BMO is not in CommandCenter.sln#
Benefits.Agent, Benefits.Agent.ServiceBus and three of the four test projects target
net10.0-windows; Benefits.Agent and Benefits.Orchestrator pin RuntimeIdentifier=win-x64.
Our only .NET CI job runs dotnet test CommandCenter.sln on mcr.microsoft.com/dotnet/sdk:10.0,
where Windows test executables cannot run — the same limit already carved out for
CommandCenter.SystemStatsAPI. BMO builds as bmo/benefits-mgmt.slnx and is gated separately by
bmo:build.
Versioning#
BMO's 18 release tags were fetched renamed, v* → bmo-v*, and MinVerTagPrefix in
bmo/Directory.Build.props was changed to match. Because a non-squash git subtree add merges
BMO's real commits, those tags point at commits that genuinely are in this repository's history, so
version continuity is preserved without adopting Nerdbank for BMO. Verified: MinVer reports
2.3.1-alpha.0.15, derived from bmo-v2.3.0, not from our v2.1.
What was pruned, moved and kept#
Removed (ordinary deletions; history is preserved and git blame still reaches them):
bmo/frontend (a scratch react-example Vite app in no solution and no CI), bmo/CLAUDE.md
(cross-contaminated from this repository — it described two EF DbContexts, a MassTransit hook and
a vite proxy, none of which BMO has), bmo/.claude, bmo/.husky, bmo/mkdocs.yml,
bmo/Dockerfile.mkdocs, bmo/repomix.config.json, bmo/.repomixignore, and the seven
bmo/.ai/skills directories that were byte-identical to ours.
Moved: bmo/docs/{index.md,guides,architecture} → docs/bmo/; bmo/.plans, bmo/.compliance
and bmo/docs/superpowers → docs/bmo/history/ (design record, and the compliance notes are the
only written statement of the AI Act / CRA / GDPR / NIS2 obligations); bmo/readme.md →
bmo/README.md; BMO's six unique .ai/skills up to .ai/skills/.
Kept: bmo/AGENTS.md, bmo/CHANGELOG.md, bmo/README.md, bmo/scripts, bmo/deploy.ps1,
bmo/Register-BMO.ps1, bmo/local-dev, bmo/docker-compose.dev.yml, bmo/.gitleaks.toml,
bmo/.dockerignore, bmo/.gitignore. bmo/.github is kept for now so that A-2's port of those
workflows to GitLab CI reads as a move rather than a deletion plus an unrelated addition.
Consequences#
Two pre-existing BMO defects surfaced immediately#
Both were invisible while the image was pulled ready-made rather than built. They are the clearest evidence the import was worth doing.
- The Dockerfile could not restore.
bmo/src/Benefits.CommandCenter/DockerfilecopiesDirectory.Build.propsbut neverDirectory.Packages.props, while BMO setsManagePackageVersionsCentrallyand everyPackageReferenceis versionless. Restore failed withNU1015across every project. Fixed with oneCOPYline. - The healthcheck could never have passed.
compose.yamlprobedhttps://localhost:8090/healthzwithwget. Theaspnet:10.0base image ships neitherwgetnorcurl, and with no server certificate configured BMO logs "binding plain HTTP on :8090" — so the URL scheme and the tool were both wrong. It now usesbashand/dev/tcp(CMD, notCMD-SHELL:CMD-SHELLruns dash, which has no/dev/tcp).
History access is asymmetric#
git subtree add grafts BMO's tree under the prefix at the merge commit but does not rewrite
paths in the 148 imported commits — they still say src/…, and pathspec filtering cannot cross the
graft. Therefore:
git blame bmo/<file>works, and follows BMO's ownsrc/BenefitManager.*→src/Benefits.*rename.git log -- bmo/<file>shows only the import merge. Path history comes from querying the BMO side with its original root-relative path:git log --follow -- src/Benefits.CommandCenter/Services/StageRemovalExecutor.cs.
Rewriting paths with git filter-repo --to-subdirectory-filter would have fixed git log at the
cost of changing every SHA, which would have broken the tag-to-commit identity that the MinVer
continuity above depends on. Blame is the archaeology that actually gets used; the trade was
accepted.
The import merge commit must never be squashed or rebased. This project has
squash_option: default_on, so the squash checkbox is pre-checked on every merge request and has
to be unchecked for the import.
Tag hygiene#
Git's automatic tag following brings BMO's original v* tag names along even when the fetch
refspec renames them. After the import, 18 unprefixed v* tags had to be deleted locally, leaving
only CommandCenter's own v2.0 and v2.1. remote.bmo.tagOpt is set to --no-tags. Do not
git push --tags after fetching from a BMO remote.
global.json does not isolate#
Unlike Directory.*.props, global.json resolves from the invocation directory upward. A build
started at the repository root uses our 10.0.201 pin with rollForward: latestFeature, and BMO's
{"test": {"runner": "Microsoft.Testing.Platform"}} is ignored. Harmless for the compile gate;
it matters when BMO's test jobs land in A-2.
CI and compose#
bmo:build runs dotnet build bmo/benefits-mgmt.slnx -c Release on the existing Linux SDK image
when anything under bmo/** changes, with GIT_DEPTH: 0 because MinVer needs the tags. Verified by
running the job's own commands inside mcr.microsoft.com/dotnet/sdk:10.0 against a git archive of
the branch: 16 projects, 0 errors — every net10.0-windows and win-x64 project compiles on Linux.
commandcenter-bmo now builds from ./bmo and the BFF's cluster-bmo destination plus
BMO_BASE_URL default to http://commandcenter-bmo:8090/. Verified: the container builds and
reaches healthy. The end-to-end leg — the SPA Fleet dashboard rendering against it — is not
verified, because BMO needs ConnectionStrings__CommandCenter_* keys the dev stack does not
provide; that is why the bmo profile stays opt-in.
What is deliberately not done here#
- The release pipeline (A-2). BMO's three GitHub Actions workflows still sit in
bmo/.githuband are dead where they are. Porting them means:bmo:test:linux(Testcontainers + Azurite on dind),bmo:test:windowson theBEN-APP01runner (whether it has the .NET 10 SDK is unverified — it is taggedvs2019),bmo:packforBenefits.Contracts+Benefits.Agent.Client,bmo:releaseon tagbmo-v*with cross-publishedwin-x64self-contained zips (which need no Windows host), andrelease:image:bmo. The compliance gate is not ported one-to-one: this repository already includes GitLab's Secret-Detection and SAST templates.
Note for that work: bmo/scripts/build-commandcenter-image.ps1 derives its default version from
git describe --tags --dirty with a leading v stripped. With the prefix now bmo-v, that
produces a wrong version string.
Who consumes the two NuGet packages is not established — no consumer exists in any local repository. Until one is identified the publish target stays GitHub Packages.
-
Unification (A-3). Folding BMO into
CommandCenter.slnand this repository's MSBuild roots is a project-by-project job (Recommended→Allanalyzer fixes, merging the package lists, an explicitTargetFrameworkonBenefits.CommandCenter, MinVer → Nerdbank withpathFilters). Also deferred: moving BMO into the committed-spec OpenAPI pipeline and retiringgen:bmo's live URL; ~~retiring theBmoPackageReaderbridge in favour of a direct reference toBenefits.PackageStore~~ (done —CommandCenter.WebApireferencesBenefits.PackageStoreandBenefits.Agent.Clientdirectly; seebackend/CLAUDE.md→ BMO libraries in WebApi); replacing BMO's hand-rolled Serilog and auth duplicates withShared.*; makingbmo stage removecall CommandCenter's endpoint so there is one stage-removal implementation again. -
Rotating the committed API key.
bmo/src/Benefits.CommandCenter/appsettings.Production.jsoncarriesClientAuthentication.ApiKey: "123-abcd"andAgentApiKey: "123-abcd". It is already in BMO's history and GitLab Secret Detection will now scan it. Rotation is tracked separately. -
BMO's unmerged branches. Seven branches were unmerged into BMO
mainat import time.feature/installed-versions-cleanuphad no commits not already inmain.feature/cli-interactive-config(10 commits, thebmo configwizard) had had its upstream deleted and was pushed back tooriginbefore the import so it is not machine-local. The remaining five have live upstreams and must be recreated here if wanted; they are lost whenbenefits-mgmt-agentis archived.stash@{0}was a three-line edit torepomix.config.json, a file this import deletes.
Related#
- ADR 0003 — BMO deploy history is a separate view. Its premise, that changing BMO's contract is "BMO-repo work, out of scope", no longer holds and it should be revisited.
.claude/rules/bmo.md— the working rules forbmo/.docs/bmo/— BMO's operator guides and design history.