Shared Rule Blocks

AGENTS.md is not documentation. It is executable context.

Every repo in the fleet has a CLAUDE.md that is a single line — @AGENTS.md. So AGENTS.md is not reference material an author consults when they remember to; it is the instruction file loaded into every agent's context, in that repo, on every session. Two repos carrying different text for the same rule does not mean the docs are untidy. It means agents behave differently per repo, silently, in a file that no build, no test and no compiler ever reads.

Seven repos carry overlapping copies of the same rules. Until 2026-08-30 the only thing holding them together was prose — the satellites say "everything there applies here unchanged" — and the habit of hand-carrying each edit to every repo. MeshWeaver.Plugins#705 measured what that produced and asked for a decision. This page is the mechanism that came out of it.

The worked example, and why prose could never have caught it

On 2026-08-30 a maintainer directive — always conserve work products — was rolled into all seven repos as seven separate pull requests: core#2732, Plugins#954, Education#236, Reinsurance#122, SocialMedia#111, Manufacturing#34, Memex#149.

That rollout was correct. Nothing about it was checked. Had one repo been missed, or had one drifted the week after, no signal anywhere would have said so — and every agent session in that repo would have run under a different rule without anyone noticing.

Measuring the seven copies afterwards also showed the rollout was less identical than it was believed to be. The intent was "byte-identical apart from a per-repo doc-home clause". In fact there were three variable regions, not one, and core hard-wraps its prose at ~200 columns while the satellites wrap at ~95. Both facts matter for the design below.

The two hubs

#705 asked whether MeshWeaver.Plugins is the hub for everything. It is not, and the files already say so themselves:

Kind of rule Hub How the files declare it
Authoring — the plugin mechanism, node-per-file mapping, Store shape, provisioning MeshWeaver.Plugins Manufacturing, SocialMedia and Reinsurance each open with "The authoritative authoring rules live in MeshWeaver.Plugins' AGENTS.md … Everything there applies here unchanged". Education names the same file for the authoring subset it uses.
Platform / process — conserve work products, CI, deployment topology MeshWeaver (core) The conserve block says so in its own text, in all six satellites: "the platform rule lives in MeshWeaver's AGENTS.md". Core also owns the shared reusable CI and the deployment topology.

So MeshWeaver.Plugins is a hub for authoring and a spoke for platform. The hub is a property of the block, not of the repo — which is why the register records a hub per block rather than one hub per repo.

A third case exists and is deliberately not resolved: rules replicated across satellites with no canonical home at all (#705's "problem B"). Those need a maintainer decision about where they should live. Until that decision is made the register can still hold them to agreeing with each other, which is a claim that needs no decision — see hub: peers below.

The marker syntax

A checker cannot compare regions it cannot find, so shared regions are delimited. Both markers are HTML comments, so every rendered view is unchanged and the raw file stays readable:

<!-- shared-rule:begin conserve-work-products -->
**🗂️ ALWAYS conserve work products — …** The durable form is <!--slot:doc-home-->a doc page under
`src/MeshWeaver.Documentation/Data/`<!--/slot--> — issue comments, PR bodies … Maintainer
directive, 2026-08-30<!-- shared-rule:end conserve-work-products -->. This rule holds in EVERY repo…

Block markers delimit the compared region. They usually take their own line; an end marker placed mid-sentence — as above — is the right move when a sentence's tail is role-specific. The conserve block deliberately stops after the date stamp, because the date is the cheapest way to spot a stale copy and the clause after it correctly differs at each end of the same pointer (core: "this rule holds in EVERY repo of the fleet"; a satellite: "the platform rule lives in MeshWeaver's AGENTS.md").

Slot markers are inline and mark the spans that are legitimately per-repo. The checker replaces each with the token <<name>> before comparing, so a slot's content is free while its name and position are asserted like every other byte. An empty slot is legal and meaningful: it records that this repo says nothing where another says something.

Comparison is byte-for-byte after exactly one normalisation: markdown soft-wrap is collapsed. That is the removal of a loophole rather than the addition of one — core wraps at ~200 columns and the satellites at ~95, so a raw byte compare would encode each repo's wrap width as part of the rule and be red on day one over a difference no reader can see. Everything a reader can see — wording, punctuation, emphasis, links, emoji — is compared exactly.

The register

.github/shared-rules.json in core lists every shared block, its hub, and every repo required to carry it. It lives in the hub, not in the spokes, and that is the anti-defeat property: a satellite cannot opt itself out by deleting its own markers, because the register — which it does not own — still says it must carry the block, and a listed repo with no markers is red.

hub takes a repo, or the literal "peers":

Why the sweep runs in one place

The obvious design is for each repo to check itself against the hub. That design structurally cannot detect the case this gate exists for.

A per-repo self-check only runs when that repo has a pull request. If a directive is rolled to six of seven repos, the seventh has no pull request, so its gate never runs — and "six of the seven were updated" produces evidence identical to "all seven were updated". Only a sweep that reads every repo from one place can tell those apart.

So the gate is one job that reads all seven, and it runs in core — which owns the shared CI and is the platform hub. It runs in two places:

Where When Reads
dotnet-test.yml → job shared-rules every core pull request and push core from the checkout (so a PR is judged on its own diff), the other six from their default branches
shared-rules.yml → job sweep daily at 05:45 UTC, on dispatch, and on a main push touching the gate all seven from their default branches

The scheduled half exists because drift does not need a pull request to appear. In a week when the satellites are busy and core is quiet, the fleet could be inconsistent for days with every check green — because no check ran. That is the gate's own defect one level up.

The coupling this creates, chosen deliberately

The pull-request half reads the satellites' default branches, so a drift merged in a satellite turns core's required check red and blocks core merges until it is fixed there.

That is the point, not a side effect — it is the same coupling the i18n mirror guard already carries, where a Plugins PR stays red until the core catalogue lands. A rule that differs between repos means agents follow different instructions in each of them, which is exactly what must not merge. The remedy is always a one-pull-request fix in the repo the error names. There is no bypass, and adding one would make the gate decorative.

No skip-trapdoor

Six of the seven repos are private, so this gate genuinely needs a credential — precisely the situation "A gate NEVER tests its own inputs" warns about, because GitHub paints a skipped job with the same tick as a passed one. So:

The credential is a GitHub App installation token minted per run (MESHWEAVER_APP_ID + MESHWEAVER_APP_PRIVATE_KEY, the org's meshweaver-cloud App), scoped to contents: read on exactly the repos in the register — never a stored PAT, which has no owner, no expiry anyone watches, and fails indistinguishably from a scope problem.

Adding or changing a shared block

Adding one: wrap the region with markers in every repo the register will list, add the entry, and land it as one change set. The gate is red for a repo that is listed and has no markers, so a half-landed adoption cannot be mistaken for a pass. Because the gate reads the satellites' default branches, merge the satellites first and the core change last — otherwise core's own pull request is red on repos that have not landed their half yet.

Changing the text of one: the hub's copy is authoritative, so the hub's change merges first and every spoke's pull request stays red until it does. Never "fix" that red by reverting the hub.

What a slot is for: a span that is genuinely per-repo — a doc home, a module list, an instance name. A slot is an exemption from the comparison, so it may only be created in the register, never by editing AGENTS.md; the checker refuses a slot the register does not declare, which is what stops "add a slot around the part I changed" from becoming the way around the gate.

What this does not do

It does not decide what the canonical text of any rule should be. It holds copies identical; it has no opinion on which copy is right. #705's comment of 2026-08-27 is the case in point — the older copies of a drifted section turned out to be closer to correct than the one that grew, and a promote-and-copy would have propagated the newer, wrong text into four repos at once with more confidence for being canonical. Enforcing sameness is safe; deciding the wording is a maintainer's call, and a hub: peers entry is how a block waits for one.

Reconnecting…
The server was updated. Reloading the page to pick up the latest version.