Stand up a NEW repository in the MeshWeaver fleet — a satellite node repo (packages the mesh compiles live) or, rarely, a deployment repo. Use when creating a new Systemorph repo, when adopting the shared CI into an existing one, or when auditing whether a repo is wired correctly (AGENTS.md, scripts/ gates, reusable-workflow CI, publish-bake + the release poll, branch protection, OIDC, .gitignore). Grounded in the six repos that already exist — MeshWeaver.Plugins, .Education, .Reinsurance, .SocialMedia, .Manufacturing, Memex — and in CiContentBake.md + ContinuousDeliveryContract.md.
🚨 READ FIRST — what a stand-up measured on 2026-09-20 (MeshWeaver.FundReporting)
This section OVERRIDES the sections below wherever they disagree. They were written from the fleet
as it stood in late August; every item here was measured while standing up a private content repo on
2026-09-20, and each names the section it corrects. Where a sentence below contradicts one of these,
this one is the current fleet and that one is history.
The lanes are called @main now, not by SHA. Every adopted caller (Crm, SocialMedia,
Reinsurance) reads uses: Systemorph/MeshWeaver/.github/workflows/node-repo-*.yml@main with
platform-ref: main / scripts-ref: main, and the platform IMAGE is resolved at run time by
preflight (resolve-platform.py, the newest SEALED set) — Nothing pins the platform REFUSES a
pin coming back. §5's "pin every uses: to a 40-char SHA" describes the pre-2026-09-12 shape.
Copy MeshWeaver.Crm@main's ci.yml (the newest content-only caller; SocialMedia's carries a
src/ module lane you do not want).
Core's fleet roster must name the repo, or validate is red on the first PR.node-repo-validate
asserts a row for the calling repo in core's .github/lane-caller-grants.yml (This repo's callers still match the platform's fleet roster → "is not in Systemorph/MeshWeaver's
.github/lane-caller-grants.yml"). Add the row — Crm's ten callers, same grants — in a core PR
BEFORE the first push (core #4975 is the shape). 🔒 Core is PUBLIC and the roster names the repo,
so a client's repo gets a neutral name (what it does, never whose it is); the client is named
only inside the repo.
Four org inputs are selected-visibility and do NOT reach a new repo:CONTROL_WEBHOOK_SECRET,
CONTROL_WEBHOOK_URL, MW_RUNNER_GATE, MW_RUNNER_HEAVY. Add the repo id:
gh api -X PUT orgs/Systemorph/actions/secrets/CONTROL_WEBHOOK_SECRET/repositories/<repo_id>
(and …/actions/variables/<NAME>/repositories/<repo_id> for the three variables). Preflight names
them, so this is a red, not a hole — but it is a step §10 did not list. The runner group is
visibility: all; 🚨 the two org Apps are different things and must not be conflated —
meshweaver-cloud (app_id 4220566) is the write App the lanes mint from, meshweaver-reader is the
read-only App §10a calls fleet-reader, whose INSTALLATION is what core's pinned-digest sweep and
lock discover the fleet from. Measured 2026-09-20, gh api /orgs/Systemorph/installations --jq '.installations[]|"\(.app_slug) \(.repository_selection)"' returned both with
repository_selection: all, which is the only reason that day's new repo needed no install step.
§10a's requirement stands: run that one-liner, and if either has flipped to selected, add the repo
to it — a repo the read-only App does not reach is not red, it is simply absent from the sweep.
The ACR scope map also needs memex-portal-ai content/read (the gate pulls the PORTAL image as
the reference set since core #3022): --repository mw-plugin-test content/read metadata/read --repository memex-portal-ai content/read — §10a lists only the tester.
meshweaver-cloud now holds workflows: write (and issues: write) — §13's "no workflows
permission" is history; verify with the App JWT → GET /app as §10a says.
The OIDC identity github-actions-bake is at 14 of Azure's 20 federated-credential slots after
this repo (two per repo). Three more repos fill it; plan the identity split before then.
validate-repos.py walks EVERY top-level folder, so a non-package folder (client reference
material, a transcription) must be in every script's SKIP set AND in
scripts/gen-manifests.config.json — being package-less (no index.json) keeps it out of the
manifest, the bake and the registry, but not out of the JSON-shape walk.
The mesh gate reads the Tests area's rendered table, so a CASE NAME containing ❌ counts as a
failure (41/41 passed and tests=FAILED). Never put a verdict glyph in a case name.
Running the in-node tests locally on a Mac:run-node-tests.py builds a console runner against
the image's (Linux, arm64) framework and dies at run with exit 134. Build it with --keep, then
execute the kept bin/Debug/net10.0 INSIDE the tester image: docker cp it into a container of
the image, strip the Microsoft.AspNetCore.App entry from NodeTests.runtimeconfig.json, copy the
extracted refs' Microsoft.AspNetCore.App/10.0.12/*.dll beside it, delete NodeTests.deps.json,
docker start -a. The compile gate needs the three registry-served module assemblies
(MeshWeaver.AI, MeshWeaver.Markdown.Collaboration, MeshWeaver.Maps) beside the image's
/app — it REFUSES an /app-only set by design.
A client-private repo is MOUNTED on the instances that should see it, never registered on the
public registry: registration seeds <Source>/* for EVERY configured source to any instance on a
plan (MeshWeaverInstanceService.PlanGrantEntries), and there is no per-source private flag. The
mount is pluginRepos[] {isRegistrySource: true} on the instance's Deployment record (App-
authenticated, no per-source token; SecretName is never rendered), mirrored into the overlay
(Record renders the overlay reds otherwise), and the Store lists it because
StoreManifestSource.Resolve folds the deployment's configured GIT sources (Plugins #2192/#2194)
— the live Store node is system-synced and is never edited for this.
Copy a repo that already works; do not invent a shape. Six exist today and they agree on far
more than they differ. What follows is the agreement, with each divergence flagged as a decision
you have to make rather than a default you can drift into.
Canonical references — read the relevant one before wiring the lane:
CiContentBake.md → "Node repos run the same lane — as reusable workflows" (the CI contract, the pin rule, the required-check rename).
🚨 A deployment repo's directory names are tenant identities. Never copy an environment name,
hostname, customer name or subscription id out of Memex into a public repo, a doc, or a skill.
Everything below is the node-repo lane.
1. Create the repo, and the eight files that make it one
Systemorph/MeshWeaver.<Name>, default branch main, private unless the content is public
(Reinsurance, SocialMedia and Manufacturing are; Plugins and Education are too — check before
assuming). Then the skeleton, all copied:
File
Copy from
Adapt
AGENTS.md
MeshWeaver.SocialMedia/AGENTS.md
§2
CLAUDE.md
any repo — the 11-byte single line @AGENTS.md, uniform across all six
no
README.md
MeshWeaver.SocialMedia/README.md (30 lines: what it is, a module table, a ## Developing block of three commands, a pointer at AGENTS.md)
yes
.gitignore
MeshWeaver.Manufacturing/.gitignore — 32 lines, the cleanest in the fleet. §9
small
.github/workflows/ci.yml
MeshWeaver.SocialMedia/.github/workflows/ci.yml
§5
.github/dependabot.yml
any node repo — they share a verbatim header and the github-actions block
§2 — all five; the table used to name two and §2 five
LICENSE where the content is licensed (Reinsurance, SocialMedia, Manufacturing carry one;
Plugins and Education do not). No node repo has a root docs/ — its documentation lives on the
mesh; only the deployment repo has one.
2. AGENTS.md — take SocialMedia's, and know what you are taking
🚨 The shared rule blocks are the baseline every repo carries — copy them from core's AGENTS.md
for the new repo's class, verify against a temporary copy of the register that adds the repo to repos and to each block's
required-in — check-shared-rules.py --manifest <temporary copy> --local <repo>=<path-to-AGENTS.md> (a
--local naming an unregistered repo is refused — the gate reads only registered repos), then
register the repo once that PR merges: SharedRuleBlocks → "The baseline every repo carries".
Donor: MeshWeaver.SocialMedia/AGENTS.md. A fleet-wide compaction is in flight — one
docs/agents-md-factor-out PR per repo (Education #224, Reinsurance #104, SocialMedia #95,
Manufacturing #26, Memex #142; Plugins uncommitted) moving each file's evidence into on-demand
skills. Take the post-factor-out form, not what main shows today, and check whether the PR has
landed before copying.
repo
AGENTS.md on main
after the factor-out
skills after
Plugins
135 KB / 1957 L
~46 KB
14
Education
34.6 KB / 512 L
16.7 KB
9
Reinsurance
31.5 KB / 445 L
19.1 KB
7
Memex
27.3 KB / 420 L
15.1 KB
4
SocialMedia
20.7 KB / 309 L
13.6 KB
5
Manufacturing
18.7 KB / 272 L
12.8 KB
3
SocialMedia wins on the pair, not on size alone: it lands thinnest-but-one with the complete
skill set — debug, pullrequest, node-files, gates, module-versions — and it is the repo
whose CI is the fully-adopted thin caller you are about to copy in §5, so the two halves match.
Manufacturing is 0.8 KB smaller and has zero unique headings (the purest skeleton), but it has
no .claude/ at all and its PR lands without debug or pullrequest — a gap in the wave, not a
design choice. Take Manufacturing's .gitignore and heading skeleton; take SocialMedia's file.
Its opening move is the thing to copy first: it does not restate the authoring rules, it
points at Plugins' AGENTS.md as authoritative and says "Everything there applies here
unchanged; this file carries only what is specific to this repo." That is the compaction. A new
repo that restates the plugin mechanism has already forked it. Three blockquote callouts sit under
that H1 in Reinsurance, SocialMedia and Manufacturing near-verbatim — copy all three: the
upstream-authority pointer, the skills-delta note, and the system-synced-space access
rule (on a GitSynced space the only Admin is system-security; never hand-create a module's
Space, never grant yourself rights to preview something).
Fleet-universal — keep these, verbatim in shape:
Section
Where
## The instances (which mesh am I talking to?)
6/6, heading identical — which portal each MCP server actually talks to, before any mutation
6/6 in substance, three headings: ## 📚 Docs live on the mesh (Reinsurance, SocialMedia, Manufacturing) · ## 📚 Where the documentation lives — search the memex MCP (Education, Memex) · ## 📚 Read the MeshWeaver docs through the memex MCP (Plugins)
How work ships
6/6, two headings: ## Git workflow (Education, Reinsurance, SocialMedia, Memex) · ## Ship it: merge by DEFAULT … (Plugins, Manufacturing)
Node-repo-universal (the four content repos, exact text shared) — copy these too:
Node FILE formats — .md for agents/skills, .cs for C# source, .json for the rest.
Gates (all of them are hard PR gates) — the §4 list, as a numbered contract.
🚨 A folder is addressable only when a node FILE backs it — X/ with no X.json beside it
is unreachable.
Repo-specific — rewrite these, do not copy the content:## The modules (your table),
the body of ## Gates (yours will match §4), ## Repo-specific scripts, and anything naming a
partition, a product or a price.
Two rules are fleet-universal but do NOT live in a satellite's AGENTS.md — and that is
deliberate. "No band-aids" appears in exactly one of the seven files (core's) and
-warnaserror in two (core, Memex — the only repos with a .NET build). A node repo inherits them
by pointing at core, not by restating them. Do not paste them in; do add the pointer.
🚨 Do not copy SocialMedia's ## Git workflow paragraph. It says "Never commit or push
automatically — wait for the maintainer to ask", which contradicts core's AGENTS.md and the
other three node repos. Take Reinsurance's ## Git workflow ("When the work is finished,
finish it — commit and run /pullrequest without being asked", plus never-push-to-main and
never-merge-red) or Manufacturing's ## Ship it: merge by DEFAULT (the same policy with an
explicit fatal / system-changing / destructive stop table). Both match the platform.
Skills — copy five. Every repo with a .claude/ carries debug and pullrequest today
(Plugins, Reinsurance, SocialMedia; Education adds course-e2e, create-space, markdown-nodes);
the factor-out wave converges the node repos on three more: node-files, gates,
module-versions. debug is effectively byte-shared — its description is identical across four
repos and only allowed-tools varies. pullrequest is the one you must edit: each names its own
repo, its own pre-flight gates and its own deploy target. No repo in the fleet has .claude/agents/.
SocialMedia's own note says why the files are thin: each loads the master node from the mesh
(get Skill/<name> over the mesh MCP) and carries only this repo's delta. The test for a delta:
would this sentence be true in another repo? If yes, it belongs in the master, not here.
🚨 .claude/settings.local.json is per-user state and must not be tracked. Reinsurance tracks
one today (git ls-files returns it) — because no node repo's .gitignore mentions .claude at
all. §9 is the fix.
3. Repo layout — one directory per package, node-per-file
A package is a top-level directory whose root is <Package>/index.json. Verified shape, from
MeshWeaver.Manufacturing/Manufacturing/:
Manufacturing/
index.json ← the package ROOT: nodeType "Store/Plugin", content $type "PluginContent"
manifest.lock ← generated; never hand-edited
Source/ ← PARTITION-level shared C#, pulled in via `shared=@Manufacturing/Source`
Station.json ← a NodeType node (leaf = X.json; node-with-children = X.json BESIDE X/)
Station/
Source/StationLayoutAreas.cs ← compiled by the MESH at runtime, never by CI's dotnet build
Test/StationTests.cs ← singular `Test/`, not `Tests/`
Test/StationTestsArea.cs ← backs WithView("Tests", …) — the area the gate EXECUTES
AnnealingAN1.json … ← instances of the type
Agent/…, Skill/… ← authored as `.md` with front matter, never escaped JSON
content/og-card.png, content/videos/… ← raw assets, never nodes
(MeshWeaver.Plugins/RemoteControl/ is the compact reference for all of it — 19 files, an Agent/
and a Skill/ both .md, two NodeTypes with Source/ + Test/, one UiContribution, and a
manifest.lock short enough to read whole.)
The root (Manufacturing/index.json) — real key set: $type: MeshNode, id, namespace: "",
path, mainNode, name, description, nodeType: "Store/Plugin", category, icon (inline
SVG), state: "Active", and content of $type: "PluginContent" carrying
requires: ["Publish@^1.0.8", "Store@^1.0.0"] (a caret pin accepts compatible updates and refuses
a new major; the bare form ["Store"] stays valid and unconstrained), version (the series,
e.g. "1.2"), description, body (the Store card's marketing page, ending @@("area/Search")),
price + currency + contactEmail, publicSegments, minMeshVersion, installPaths,
marketingPath, tier, preInstalled, and entryPoint. SocialMedia's adds
module: "MeshWeaver.Social" for a mixed package (content + a compiled module).
entryPoint is optional (LinkedIn omits it) and takes three shapes: a plain node path
(RemoteControl/Start), a node path whose default view is the app (Manufacturing/Overview), or an
explicit layout-area route {node}/area/{Area} (Providers/ProvidersApp/area/ProviderSetup).
No leading slash.
A NodeType node (Manufacturing/Station.json) — nodeType: "NodeType" and a content of
$type: "NodeTypeDefinition" with description, includeGlobalTypes, sources
(["namespace:Source scope:subtree", "shared=@Manufacturing/Source"]) and — 🚨 — a
configuration lambda that is C# inside a JSON string:
That string is invisible to grep --include='*.cs' and to every dotnet build. It is why AGENTS.md's
in-mesh-source rule exists and why §4's compile gate is not optional.
manifest.lock — generated, six keys: schema: "mw-manifest/1", module, files (every
file in the folder — .json, .cs under Source/+Test/, .md, and content/ assets — as
repo-path → sha256; 161 entries for Manufacturing), moduleVersion (a content hash — exact but
unordered, so nothing can pin against it), version (SemVer, the number dependents pin),
sourceCommit. You own MAJOR.MINOR via the root's content.version; gen-manifests.py derives
PATCH. Never hand-edit it, and 🚨 never commit a node's version field — that is the owning
hub's persistence counter, and a checked-in one makes MonotonicWriteGuard refuse the install
(found in every node repo in 2026-08, counters as high as 4489).
A menu entry is DATA.nodeType: "UiContribution" — e.g.
MeshWeaver.Plugins/Approvals/RequestApprovalMenu.json: content.$type: "UiContribution" with
context ("Node", "Admin" in the live files; the type declares Node/Mesh/Settings/
AiMenu/SidePanel/TopBar — src/MeshWeaver.Graph/Configuration/UiContributionNodeType.cs),
label + labelKey (i18n — supply it), icon (emoji), area, href ({node} is the only
placeholder the seam substitutes), order, requiredPermission. Installing the package adds the
entry; removing it removes it — no platform build. Visibility is enforced by the compiled
aggregator against a closed gate vocabulary; a contribution can only ever NARROW itself. Only four
exist in the whole fleet, all in Plugins — so copy one rather than deriving it.
4. scripts/ — the validators the CI invokes by path
🚨 The reusable workflows run python3 scripts/<x>.py from the CALLER's checkout, by
repo-relative path (node-repo-validate.yml:50,55,67, node-repo-compile-check.yml:155,
node-repo-tag-modules.yml:59, node-repo-gate.yml:200). There is no shared copy in core — a
find over the platform repo for all fourteen filenames returns zero hits, and
node-repo-tag-modules.yml says why: "scripts/tag-modules.py lives in each node repo, so parsing
its text would couple this shared template to a file it does not own." Every caller carries its own.
Script
Carried by
What it asserts
Key flags
validate-repos.py
all 5
node JSON shape + NodeType Source/ presence, and fails on any module file hidden by .gitignore. Shape only — it does not compile
none (Plugins adds --self-test)
gen-manifests.py
all 5
regenerates manifest.lock; --check = current, --check-versions = the derived SemVer matches the content
--check, --check-versions, --no-fetch
compile-check.py
Plugins, Reins, Social, Manuf
compiles every NodeType's resolved source set — concatenated, usings hoisted, and the configuration lambda — against the framework assemblies exactly as the mesh does on import
--refs <dir>, --image, --gen-allow
tag-modules.py
all 5
cuts <Module>/vX.Y.Z on main; verifies rather than moves a tag that already exists
--dry-run
affected-modules.py
Plugins, Reins, Social, Manuf
the diff → changed modules → transitive dependents → their dependencies, 1:1 with LocalNodeRepo.CollectDependencies
derives the Store checklist: name/description/icon/category/Store/Plugin/content.body/@@("area/Search")/og image, split REQUIRED vs POLISH
--check, --self-test
check-covers.py
Plugins, Edu, Reins, Social
no cover is a wall of text — longest prose paragraph (structure excluded)
--max N (400), --report, --self-test
dep-graph.py
Plugins, Reins, Social, Manuf
the package graph from content.requires, leaves first
--mermaid (Reins: --check-staged)
run-node-tests.py, gen-scope-proxies.py
Plugins, Reins
run a module's in-node tests without a mesh; materialise scope proxies (never hand-written)
--refs, --list / --check
Exactly three are byte-identical across every repo that has them — copy those verbatim:check-covers.py, store-presentation.py, compile-check.allow. Everything else diverges, and
the divergence is almost always one line: the SKIP set of top-level directories that are not
packages (Reinsurance adds "legacy", Plugins adds "app", Education skips dot-directories).
Take the newest copy from MeshWeaver.Plugins — it is the hardened superset for every script
except dep-graph.py --check-staged (Reinsurance-only) — and edit SKIP. Two known-stale copies to
avoid inheriting: SocialMedia's and Manufacturing's dep-graph.py read content.requires raw, so a
pinned edge "Store@^1.0.0" is invisible to them; Manufacturing's gen-manifests.py (14 KB vs
~35 KB) derives the patch from tags only, with no --no-fetch and no two-witness baseline.
Repo-specific — know they exist rather than copying blind: check-test-suites.py (Plugins,
Manufacturing — ratchets tests=skipped, which the gate prints inside a line beginning ok),
check-ci-invariants.py + fetch-registry-package.py (Manufacturing),
check-discriminators.py / check-scripts.py / check-agent-parity.py / check-impersonation.py
(Plugins), check-course.py / check_batteries.py / check-examples.py (Education).
Allow files are one-way ratchets — a NEW failure fails the PR; an entry that now passes is
STALE and also fails ("remove it — it compiles now"). Who consumes what:
file
where
consumed by
compile-check.allow
scripts/
scripts/compile-check.py
plugin-gate.allow
repo root
not a script — passed to mw-plugin-test as --allow, and it is node-repo-gate.yml's allow-filedefault
plugin-tests.allow
repo root
scripts/check-test-suites.py (growth blocked separately via --base-ref origin/main)
cover-prose.allow
repo root
scripts/check-covers.py
layout-area-data-bake.allow
repo root
core's .github/scripts/check-layout-area-data-bake.py, fetched by node-repo-validate.yml. It is REQUIRED: a missing file is red. An empty file is correct for a repository where nothing bakes. On a PR, the tree is compared with the base (--base): the number of baking units may not grow, and a PR that converts an area must lower or delete its line. Doc/GUI/DataBinding has the details.
🚨 Seed every one of them EMPTY. They exist to grandfather debt you inherited; a new repo has
none, and an entry added in the same diff as the hole it excuses is a trapdoor exactly one commit
wide. Where a repo gates additions, the event test lives inside the step, never as an if:.
Every script that can ships --self-test, and CI runs the self-test immediately before the
check — because against a clean tree a gate prints the same green tick whether it measures
correctly, measures the wrong thing, or measures nothing at all.
5. CI — a THIN CALLER of core's reusable workflows. Never a copy.
🚨 This is the load-bearing rule of the whole skill. The shared jobs live in the platform repo
as workflow_call workflows. A satellite that hand-rolls or copy-pastes them forks the contract
and stops receiving its fixes.
Systemorph/MeshWeaver/.github/workflows/
node-repo-validate.yml job: validate name: Validate node repos
node-repo-compile-check.yml job: compile-check name: Compile every NodeType (vs core)
node-repo-gate.yml job: test-repos name: Compile + render node repos (MeshWeaver from ACR)
node-repo-tag-modules.yml job: tag-modules name: Tag module versions
node-repo-publish-bake.yml job: publish-bake name: Bake + publish NodeType assemblies to portal storage
node-repo-module-pack.yml (mixed packages only — builds + packs a compiled module)
Adoption status today, so you copy the right file: SocialMedia and Reinsurance call the full
five — they are the models. Plugins calls only module-pack + publish-bake (its gates carry
repo-specific machinery). Education and Manufacturing call none and hand-roll. Copy
MeshWeaver.SocialMedia/.github/workflows/ci.yml.
🚨 SUPERSEDED — see "READ FIRST" at the top: the fleet calls the lanes @main with run-time
platform resolution, and a pin coming back is REFUSED by Nothing pins the platform. The paragraph
below is the pre-2026-09-12 shape, kept for repos that have not moved. Pin every uses: to a
40-char commit SHA, never @main and never a tag. On @main one edit
to a shared workflow changes every satellite's gates at once and "did my change break this, or
did the workflow move under me?" stops being answerable. A tag is worse: moving it changes all
satellites with no commit anywhere to attribute it to. GitHub forbids an expression in uses:,
so the SHA is literal on each line — bump them all in one commit. SocialMedia's preflight
reads its own uses: lines back and prints the pin's age, shouting past 30 days; copy that step.
Pin the platform IMAGE by digest too (tag@digest), and bump it often. The pin buys
reproducibility, but it also caps what the gates can SEE — a pin lagging the platform by days is
a gate reporting on a framework nobody runs. That is exactly how the 2026-08-09 AddTracking
outage shipped green. An unpinned image is an explicit allow-unpinned: true, never a silent
fallback.
Triggers: push: [main], pull_request, merge_group, workflow_dispatch,
repository_dispatch: types: [meshweaver-framework-released], and schedule (§6).
🚨 merge_group is not optional on a repo with a merge queue — its absence does not look like a
missing trigger, it looks like a PR that queues and then sits there until it is ejected with no
failing check to point at.
cancel-in-progress must be OFF for main pushes:
${{ !(github.event_name == 'push' && github.ref == 'refs/heads/main') }}. Written as
push-AND-main, not github.ref != 'refs/heads/main' — a scheduled or dispatched run also carries
the default-branch ref and those must keep superseding each other. Superseding stays on for PR
branches, which is where the runner saving is. Measured on Plugins with the ref-only form: 18 of
one day's main runs ended cancelled, each killed by the next merge, so the publish jobs never
ran and the registry served bytes older than main.
Keep repo policy in the caller: the digest pin, the gating (if:/needs:), the dispatch
receiver, the module-bundle job, and scripts/.
6. CD — publish, bake, and the poll that actually delivers
The ladder (policy platform-backwards-compatibility). A node repo's publication is sealed
under the platform COMPATIBILITY key c<major>e<epoch>, which every platform build of one epoch
shares — so a new platform build does NOT require this repo to rebuild or re-seal, and installs keep
running its bytes across platform rolls. Its publication rolls independently once its floor (the
producing platform build) is ≤ the platform an install runs. Only a DECLARED break (an epoch bump)
requires a coordinated rebuild and seal, and then platform rolls are held by name until it lands.
Never pin a platform image or a Deployment record to work around one. Full procedure:
Deploying Across Platform Versions.
Publication is node-repo-publish-bake.yml, gated main-only or on a release trigger:
publish-bake:
if: >
(github.event_name == 'push' && github.ref == 'refs/heads/main') ||
github.event_name == 'repository_dispatch' ||
github.event_name == 'schedule'
needs: [preflight, validate, compile-check, test-repos]
permissions: {contents: read, id-token: write} # azure/login OIDC — no long-lived storage secret
uses: Systemorph/MeshWeaver/.github/workflows/node-repo-publish-bake.yml@<sha>
with:
bake-source: <lowercase repo short name> # plugins | reinsurance | socialmedia
test-image: ${{ vars.MW_TEST_IMAGE }}
image-digest: ${{ needs.preflight.outputs.bake-image-digest }}
bake-publish-targets: ${{ vars.BAKE_PUBLISH_TARGETS }}
stage-repo: Systemorph/MeshWeaver.Plugins # only if a package `requires` a module from there
stage-modules: Store # only what you USE; the lane stages their `requires` closure
The digest that the BAKE uses is not the digest the GATES use, and conflating them is the bug.
Two questions: the gates ask "do two runs of identical code agree?" → the pin. The bake asks
"does the RELEASED framework identity have published bundles?" → the currently-released image.
SocialMedia's preflight resolves this in a bake-target step: on repository_dispatch/schedule
it resolves docker manifest inspect -v → .Descriptor.digest and fails loud if it gets no
digest, because silently falling back to the pin would republish an identity that is already
published and leave every instance held, under a green tick.
🚨 repository_dispatch is memex's EVENT; the schedule is the fallback. The registry's
PlatformBuildInboxWatcher dispatches meshweaver-framework-released to every repository the
Hosting/Deployment records name as a registry source — so a new repo is subscribed by being named
on a Deployment record (Systemorph/Memex mesh/Deployments/*.json, pluginRepos[] with
isRegistrySource), never by an edit in core. Core CD dispatches to no repository (its
notify-dependents was withdrawn 2026-09-03; the 2026-08-22 predecessor printed a not-configured
notice for its whole life while the fleet stayed a release behind, every check green). A node repo
without schedule never bakes for a released identity when a dispatch is lost. It bakes its pin,
on its own pushes, forever, and the only symptom is an instance HELD on that repo's bundles.
Stagger the minute; observed today: SocialMedia 7,37, Plugins 17,47, Reinsurance 22,52,
Education 32.
FOLLOW_RELEASE vs the pin, in one line:github.event_name == 'repository_dispatch' || github.event_name == 'schedule' — true ⇒ bake the released image; false ⇒ bake the pin, which
certifies the bits the gates just ran.
Module bundles (mixed packages only) go through node-repo-module-pack.yml with modules (a
JSON array of {package, module, project}), platform-ref, registry-source, and a publish
expression that is trunk-only, not push-only — a release-follow schedule or dispatch MUST
publish or the rebuild it just did is discarded. 🚨 platform-ref is a literal, not
${{ env.MW_PLATFORM_REF }}: the env context is unavailable in a reusable workflow's with:. Keep
the uses: ref and platform-ref the same commit — the lane runs a script from the platform
checkout it makes at platform-ref.
7. The preflight job — a gate NEVER tests its own inputs
One job asserts every value that comes from outside the tree, fails RED naming exactly what to
provision, and every gate needs: it and runs UNCONDITIONALLY.
preflight:
name: Required CI inputs
runs-on: ubuntu-latest
outputs:
# 🚨 TWO outputs, and they are not interchangeable. The GATES read the PIN — an immutable
# digest every job shares, so one run cannot straddle two images. The BAKE reads whatever
# the release tag resolves to RIGHT NOW, because its job is to republish against the
# identity the platform actually released. Defining only `image-digest` leaves the
# publish-bake job in §6 consuming an output that does not exist: it resolves to the empty
# string, and the bake targets nothing while the run still goes green.
image-digest: ${{ steps.pin.outputs.image-digest }}
bake-image-digest: ${{ steps.bake-target.outputs.image-digest }}
if: github.event.pull_request.head.repo.fork != true # the ONLY exemption, on the EVENT
steps:
- name: Assert every externally-provisioned CI input is present
env: {MW_TEST_IMAGE: "${{ vars.MW_TEST_IMAGE }}", ACR_USERNAME: "${{ secrets.ACR_USERNAME }}", …}
run: |
set -euo pipefail
missing=()
[ -n "${MW_TEST_IMAGE:-}" ] || missing+=("vars.MW_TEST_IMAGE — the mw-plugin-test image reference")
…
if [ ${#missing[@]} -gt 0 ]; then
echo "::error::Required CI inputs are not provisioned, so the gates cannot run. A gate that cannot run fails RED."
for m in "${missing[@]}"; do echo "::error::missing: ${m}"; done
exit 1
fi
Why, in three lines: a missing input is a RED job naming what to set, never a skip; gates run
unconditionally behind needs:; anything genuinely exempt is a check on the event
(fork, ref, event_name) — never if: ${{ vars.X != '' }}. At job level those two look
identical and only one is safe, because GitHub renders a skipped job with the same tick as a
passed one, so "the gate never ran" and "the gate passed" become indistinguishable and the
failure is self-concealing. Not theoretical: Plugins' cross-repo gate was built that shape and
therefore never ran once — #683 deleted a live API and put nine plugin partitions on the
compilation-error overlay in production.
Adding an input is one line in missing. A staging device ("stay green until X is wired")
that outlives its staging is a silent hole; delete it the day X is wired.
8. Branch protection + required checks
🚨 Adoption RENAMES the required contexts. A reusable-called workflow's runs report as
<caller job> / <inner job name>. Update branch protection in the same change — a context left
at the old name waits forever. Verified live today:
Validate node repos, Compile every NodeType (vs core), Compile + render node repos (MeshWeaver from ACR)
Plugins true, Manufacturing false
MeshWeaver, Education, Memex
no branch protection (core uses a ruleset instead — main pr protection, id 2128472: pull_request, copilot_code_review, required_status_checks {strict: false, checks: ["Consolidate test results"]}, deletion, non_fast_forward)
—
🔒 A later @refbump renames nothing — neither the caller's job id nor the inner name: — so
the contexts survive bumps. Only renaming a job in the reusable workflow breaks them, which is why
that rename is a breaking change to every caller.
strict is PER-REPO. Measure it; never remember it:
gh api repos/Systemorph/<repo>/branches/main/protection --jq '.required_status_checks.strict' # 404 = unprotected
Choose strict: false. Plugins is the only true in the fleet and it is a merge treadmill:
every merge makes every other branch stale, so each PR needs a fresh CI cycle to catch up. With
false a branch that is merely behind main merges fine; what you still owe is no conflicts and
a green required check. If you want the combination-that-lands tested, that is a merge queue
(merge_group: trigger + queue settings), not strict.
9. .gitignore — two ways to lose files silently
Start from MeshWeaver.Manufacturing/.gitignore (32 lines) and keep its opening banner, which
is the fleet's institutional memory:
🚨 THIS IS A NODE REPO — do NOT add .NET build-output rules here. Every top-level module folder is
a mesh partition of node files, and a node folder may legitimately be named Release/, Debug/,
Bin/ or Log/.
Because git never descends into an ignored directory, a swallowed file is not even reported as
untracked. It has bitten twice: [Rr]elease/ hid 77 UWDeepfield node files during the import
into MeshWeaver.Reinsurance, and [Dd]ebug/ hid the /debug SKILL directory in Plugins — git add
said "nothing to commit". validate-repos.py now fails on any module file hidden by
.gitignore; keep that check. If a tree genuinely needs build-output rules, scope them by path
(legacy/**/[Dd]ebug/, src/**/bin/, test/**/obj/), never bare.
🚨 The second way: .claude/. Two opposite mistakes are live in the fleet right now.
A blanket .claude/ means skills can never be committed — and a doc gate later pointed at the
skills root would then walk an empty directory and report green having checked nothing. Memex is
exactly that (.gitignore:393); its fix, PR #142, has to change the ignore and teach
scripts/check-doc-links.py to walk the skills, in one commit.
No rule at all — the state of all five node repos — leaves per-user state committable, and
Reinsurance has committed .claude/settings.local.json.
The correct form, which today exists only in core's .gitignore (lines 433-440):
# Claude Code per-user local state (settings.local.json, plans/, scheduled_tasks.lock, …)
# — but DO track the shared team skills under .claude/skills/.
.claude/*
!.claude/skills/
Core adds !.claude/skills/debug/ and !.claude/skills/release/only because it also carries
generic [Dd]ebug/ / [Rr]elease/ rules. A node repo following the banner above needs neither —
but if you ever add such a rule, add the matching negation in the same commit. (Memex's fix branch
adds the two-line form without the per-skill negations, and its lines 16/18 still swallow both.)
Also fleet-universal, present in all six: .worktrees/, node_modules/, __pycache__/, *.pyc.
And anchor a nested-repo ignore with a leading slash — an unanchored plugins/ matches at every
depth and, on macOS's case-insensitive filesystem, also matches Plugins/.
10. Secrets, variables and OIDC
Provision under Settings → Secrets and variables → Actions. Verified names across the fleet:
Needed for
Plugins
Reins.
Social
Manuf.
Edu
vars.MW_TEST_IMAGE
the gates (image whose /app assemblies are the reference set)
GitHub App that staging a cross-repo module (stage-repo) reads with
✓
✓
✓
—
—
vars.MW_REGISTRY_URL + secrets.MW_REGISTRY_KEY
the gate fetching the upstream publication over HTTPS (works on pull_request)
—
✓
✓
—
✓
secrets.REGISTRY_PUBLISH_TOKEN
handing a packed module bundle to the registry
✓
—
✓
—
—
Minimum for a content-only repo: MW_TEST_IMAGE + ACR_USERNAME + ACR_PASSWORD. Add the
BAKE_PUBLISH_TARGETS + AZURE_* set the moment you turn on publish-bake — a missing value there
fails the job by design; a grey skip would silently restore every-pod-rebakes-everything.
🚨 Register BOTH OIDC subject formats — GitHub subjects are immutable, and which form a repo
presents varies per repo inside one org at the same moment. Measured 2026-08-17: Education and
SocialMedia presented the immutable form while Plugins presented the classic one, so tidying all
four to immutable broke Plugins with AADSTS700213: No matching federated identity record found.
Expect two rows per repo. When AADSTS700213 appears, copy the presented subject verbatim out
of the error and add a credential for it, keeping the existing one — deleting the other format
is what turns another repo's green lane red. Full text:
ContinuousDeliveryContract.md
→ "Register BOTH OIDC subject formats".
Dependabot: secrets provisioned for Actions are not visible to Dependabot-triggered runs —
provision the pair under Secrets → Dependabot too, or a dependabot PR fails preflight.
10a. Provisioning, end to end — measured 2026-08-28 standing up MeshWeaver.Crm
Everything in §10 is a NAME; this is where each VALUE comes from, in the order that worked, with
the three traps that cost a retry. Nothing here needs a second person — Systemorph issues every
credential itself (registry-credentials-we-issue).
Input
Where the value comes from
Trap
ACR_USERNAME / ACR_PASSWORD
a per-repo ACR token on a per-repo scope map — the fleet convention is <short>-ci-pull on <short>-ci-pull-scope-map (repositories/mw-plugin-test/content/read): az acr scope-map create --registry meshweaver -n <short>-ci-pull-scope-map --repository mw-plugin-test content/read metadata/read then az acr token create --registry meshweaver -n <short>-ci-pull --scope-map <short>-ci-pull-scope-map
🚨 token create ECHOES the password in a docker-login hint on stderr. Treat it as burned: az acr token credential generate … --password1 --query 'passwords[0].value' -o tsv > file 2>/dev/null, then gh secret set … < file. Prove it with docker login … --password-stdin < file
MESHWEAVER_APP_ID / _PRIVATE_KEY
the org GitHub App meshweaver-cloud, id 4220566 (`gh api /orgs/Systemorph/installations --jq '.installations[]
select(.app_slug=="meshweaver-cloud")
AZURE_CLIENT_ID / _TENANT_ID
az identity show -g <resource-group> -n github-actions-bake --query '{clientId:clientId,tenantId:tenantId}'
—
AZURE_SUBSCRIPTION_ID
az account show --query id
—
the two OIDC federated credentials
§10's two az identity federated-credential create calls; REPO_ID = gh api repos/Systemorph/<Repo> --jq .id, org id 77832550
keep BOTH formats
MW_REGISTRY_URL / BAKE_PUBLISH_TARGETS
the same literal every sibling carries — copy it: `gh api repos/Systemorph/MeshWeaver.Reinsurance/actions/variables --jq '.variables[]
"(.name)=(.value)"'`
MW_REGISTRY_KEY
mint a 20-minute mwr_ on the registry (an executable Code node in your home, execute_script, calling RegistrationKeyService.Mint(userId, name, email, description, expiresAt) from MeshWeaver.PluginCatalog; the raw key lands in the activity log a second after Succeeded), then ONE pipeline: curl -X POST "$MW_REGISTRY_URL/api/instances/register" -d '{"bootstrapKey":…,"instanceId":"ci-<short>",…}' > file → `python3 -c "…print(instanceKey,end='')"
gh secret set MW_REGISTRY_KEY --repo …`
Dependabot store too.gh secret set … --app dependabot for ACR_* and MESHWEAVER_APP_* — a
dependabot-triggered run reads the Dependabot store, and preflight fails red on the Actions-only
pair (the same shape MeshWeaver#2249 documents).
Install the read-only fleet-reader App on the new repo too. Core's cross-repo READS — the
shared-rules gate, cross-repo-pair, and the pinned-digest sweep and lock — mint from it, and the
sweep and lock DISCOVER their fleet from its installation: a repo it does not reach is not red, it
is simply absent, and its pinned images stop being locked against the nightly purge. Listing the
repo in core's .github/shared-rules.json without the install turns every core PR red instead.
GitHubAppCredentials.md.
The rest of the repo, from the CLI, in the order that worked:
gh repo create Systemorph/MeshWeaver.<Name> --private --description "…"
gh api -X PATCH repos/Systemorph/MeshWeaver.<Name> -f allow_auto_merge=true
gh api -X PUT repos/Systemorph/MeshWeaver.<Name>/vulnerability-alerts
gh api -X PUT repos/Systemorph/MeshWeaver.<Name>/automated-security-fixes
gh api -X PUT repos/Systemorph/MeshWeaver.<Name>/collaborators/<login> -f permission=push # per maintainer
# the Copilot ruleset is data — copy a sibling's verbatim:
rid=$(gh api repos/Systemorph/MeshWeaver.Reinsurance/rulesets --jq '.[0].id')
gh api repos/Systemorph/MeshWeaver.Reinsurance/rulesets/$rid \
--jq '{name,target,enforcement,conditions,rules,bypass_actors}' > ruleset.json
gh api -X POST repos/Systemorph/MeshWeaver.<Name>/rulesets --input ruleset.json
🚨 gen-manifests.py refuses an EMPTY remote. It reads the trunk's committed lock as the second
version witness (git ls-remote --symref origin HEAD), so on a repo with no main yet it fails
"could not resolve which branch 'origin' publishes" — and validate-repos.py fails on the missing
lock. Push a bootstrap commit (README only — no ci.yml, so nothing runs) FIRST, then generate the
manifest, then push the module. Two commits on a brand-new main is the cost; branch protection
goes on AFTER the first real run has produced the three contexts to require.
🚨 Store/Provision.subdirectory is the PARENT folder, not the package folder.SystemInstall. EffectiveSubdirectory appends the package id, so a request with "subdirectory": "Crm" for package
Crm syncs Crm/Crm and the import refuses an empty snapshot ("No files found under subdirectory
'Crm/Crm'"). For a repo whose package folder sits at the root, omit subdirectory (it derives
Crm). Measured on the control instance, 2026-08-28; a retry is patchsubdirectory: null +
requestedAction: "Provision" — the phases reconcile the _GitSync they wrote.
11. Make the content reachable — register the repo as a catalog source
A repo nobody reads ships nothing. The registry holds the git credential and re-serves per
(source, package); consumers speak only HTTP. Add the source in the deployment's Helm values —
which live in the private deployments repo, never here:
format defaults to node-repo: a folder appears once it carries a <Folder>/index.json root
with a PluginContent. Sources merge in configured order, first wins on an id collision, and a
failing repo contributes nothing (logged) rather than breaking the catalog. Grants, tokens and the
anonymous-registry trap: /plugins.
Deployment repo (§0), in one paragraph: it is not a node repo and none of §3–§6 applies. It
holds per-environment values and its own workflows (Memex: build, helm-release, smoke,
deploy-drift, config-key-coverage). Its dependabot watches nuget + docker + actions and
deliberately does not bump the portal image pin — the in-pod self-updater chooses the running
build, and a Dependabot PR would fight the reconciler. See /new-deployment.
12. Webhooks — one per LIVE portal, or the fleet never sees the repo
Measured 2026-09-07 (MeshWeaver#3582, #3583).MeshWeaver.Crm was created on 08-28 with
everything above and no webhooks. Nothing checked. Its partition on the control instance last
synced on 08-30; ten days later the repo was 54 commits ahead, the portal still held files the repo
had retired, every prebuilt Crm bundle was refused by the source-fingerprint gate (#2813), and
opening any Crm page compiled ten-day-old sources on first use — reported as "why is bake not
working". Every other node repo had two hooks; nobody had noticed the one that had none.
A portal learns about a repo through exactly one channel: a GitHub webhook on the REPO, pointed at
https://<portal>/webhooks/github, signed with THAT portal's GitHub__Webhook__Secret. The push
event only logs ("a build is coming"); the workflow_run completed/success on the default branch
is what triggers the CI-gated import (GitHubWebhookProcessor.ProcessWorkflowRun →
UpdateToLatestFromGitHub at the built commit) and what writes the BuildCompletion the
self-updater and the plugin-update watcher read. No hook ⇒ the partition freezes at its last sync
while the bundles move on ⇒ fingerprint refusals ⇒ compile-from-source (or, since #3583, a named
error on a partition the portal does not track).
One hook per live portal, the portal's own secret, created at the same time as the repo:
# vault object → portal: each portal's GitHub__Webhook__Secret is a Key Vault object named in
# that portal's SecretProviderClass — read the name there, never guess it.
# The value goes from the vault into the request body and nowhere else — never into a variable
# you echo, never onto a command line. scripts/… is deliberately absent: keep it in the scratchpad.
python3 - Systemorph/MeshWeaver.<Name> <portal-host> <webhook-secret-object> push,workflow_run <<'PY'
import json, subprocess, sys, time
repo, host, obj, events = sys.argv[1], sys.argv[2], sys.argv[3], sys.argv[4].split(",")
secret = subprocess.run(["az","keyvault","secret","show","--vault-name","<vault>","--name",obj,
"--query","value","-o","tsv"], check=True, capture_output=True, text=True).stdout.strip()
body = {"name":"web","active":True,"events":events,"config":{"url":f"https://{host}/webhooks/github",
"content_type":"json","insecure_ssl":"0","secret":secret}}
hook = json.loads(subprocess.run(["gh","api","-X","POST",f"repos/{repo}/hooks","--input","-"],
input=json.dumps(body), check=True, capture_output=True, text=True).stdout)
subprocess.run(["gh","api","-X","POST",f"repos/{repo}/hooks/{hook['id']}/pings"], check=True, capture_output=True)
time.sleep(5)
d = json.loads(subprocess.run(["gh","api",f"repos/{repo}/hooks/{hook['id']}/deliveries?per_page=1"],
check=True, capture_output=True, text=True).stdout)[0]
print(f"hook {hook['id']} -> {host}: ping status={d['status']} code={d['status_code']}")
PY
# …and again for every other live portal with ITS webhook-secret object — for the public
# instance with events
# issues,issue_comment,push,workflow_run (the two issue events feed the issue mirror; add them only
# if the repo takes part in it — Plugins, Education, Reinsurance and SocialMedia do).
Prove it, do not assume it: the ping delivery must read status=OK code=200 from the portal.
A hook that exists and delivers non-2xx is the same as no hook — a wrong secret answers 401 and
the repo is exactly as invisible as Crm was. Then, after the first green main run, read the
partition's _GitSync.lastSyncCommitSha on each portal: it must equal that run's head_sha.
The denominator is "every live portal", and it moves. A new portal (a client deployment, a
second staging) needs a hook on EVERY node repo it syncs, so a new-portal checklist carries this
section's mirror image. Audit the fleet in one call and read the count, never the tick:
gh api "orgs/Systemorph/repos?per_page=100" --jq '.[] | select(.archived==false) | .name' \
| while read -r r; do printf '%-28s %s\n' "$r" "$(gh api "repos/Systemorph/$r/hooks" \
--jq '[.[] | "\(.config.url | sub("https://";"") | sub("/webhooks/github";""))"] | join(" ")')"; done
# every node repo (one a portal's GitHubSyncConfig names — `search partitions:all nodeType:GitHubSyncConfig
# repositoryUrl:*<Name>*` on each portal) must list BOTH hosts. 2026-09-07: Crm listed none.
And the portal side is not implied by the hook. The webhook only wakes a sync source that
already exists: each portal needs a <Partition>/_GitSync (a GitHubSyncConfig naming the repo,
branch and subdirectory) for every partition the repo owns — the Store/Provision request in §10a
writes it for a provisioned space; a partition installed any other way (memex's Feedback, four of
the bundle's five files, no _GitSync) has none, and since #3583 its module content ERRORS rather
than compiling from the leftover copy. The Partition Sync administration page adds one; then
trigger one sync and count the partition's source nodes against the repo.
13. There is no pin-bump lane — a node repo carries no platform pin
🚨 RETIRED. Node repos no longer pin the platform: they resolve the newest SEALED set at run time
(MeshWeaver#3842, adopted by MeshWeaver.Plugins in b5414edf7, which deleted the last caller). Core's
node-repo-platform-ref-bump.yml lane is therefore deleted; never add a platform-ref-bump.yml caller
to a new repo. The history — why the lane needed the App's workflows grant, and the empty-room
failure it taught — is kept on Keeping the Platform Source Pin Current.
The checklist
Files and settings:
Repo created, main default, visibility decided.
AGENTS.md from SocialMedia, points at Plugins' AGENTS.md as authoritative, ## The modules
and ## Gates rewritten, git-workflow section taken from Reinsurance or Manufacturing.
CLAUDE.md = @AGENTS.md. README.md ≤ ~40 lines.
.gitignore from Manufacturing, banner kept, no unscoped build-output rules, plus
.claude/* + !.claude/skills/ (neither a blanket .claude/ nor no rule at all).
Prove it: git check-ignore -v .claude/skills/debug/SKILL.md must print nothing.
.claude/skills/ — debug + pullrequest (edit the latter: repo name, gates, deploy
target) + node-files, gates, module-versions; settings.local.json NOT tracked.
scripts/ copied from Plugins with SKIP adjusted (check-covers.py,
store-presentation.py, compile-check.allow verbatim); every --self-test passes;
allow files created empty.
.github/dependabot.yml copied.
At least one package: index.json (Store/Plugin + PluginContent + entryPoint) and a
manifest.lock generated by python3 scripts/gen-manifests.py.
ci.yml copied from MeshWeaver.Crm (see "READ FIRST"; SocialMedia carries a src/ module
lane you do not want); lanes called @main, the platform resolved at run time — not pinned;
triggers include merge_groupandschedule; cancel-in-progress off for main pushes.
Webhooks (§12): one per live portal, that portal's vault secret, ping delivery code=200
— gh api repos/Systemorph/<Repo>/hooks --jq length equals the number of live portals.
A <Partition>/_GitSync on every portal that serves the repo's partitions (§12), and after
the first green main run its lastSyncCommitSha equals that run's head_sha.
preflight names every input, fails red, fork exemption on the event; no gate carries an
input-shaped if: or a continue-on-error:.
Secrets/vars provisioned (§10) for Actions and Dependabot; two OIDC federated credentials
registered.
Branch protection: strict: false, contexts in the <caller job> / <name> form.
Registered as a pluginCatalog.sources entry in the deployment repo.
What proves it worked — three observations, not "the files exist":
# 1. The first PR's required checks went green under their NEW names.
gh pr checks <n> --repo Systemorph/MeshWeaver.<Name>
gh api repos/Systemorph/MeshWeaver.<Name>/branches/main/protection \
--jq '.required_status_checks.contexts' # must MATCH the names above, exactly
# 2. The first main run PUBLISHED — the publish-bake job succeeded and the module tag was cut.
gh run list --repo Systemorph/MeshWeaver.<Name> --branch main --limit 1 --json databaseId,conclusion
gh run view <id> --repo Systemorph/MeshWeaver.<Name> --json jobs \
--jq '.jobs[] | "\(.conclusion)\t\(.name)"' # publish-bake + tag-modules present AND success
git ls-remote --tags origin | grep '<Package>/v' # the version label exists
# 3. A SCHEDULED run has fired and resolved a RELEASE digest, not the pin.
gh run list --repo Systemorph/MeshWeaver.<Name> --event schedule --limit 3 \
--json conclusion,createdAt,databaseId
🚨 A green tick is not a publication. Check the jobs, not the run conclusion — a job marked
continue-on-error: true fails while its workflow still reports success. And a preflight that
went grey is the failure this whole file is built around: it means the gates never ran.
Where the fleet disagrees — decide, don't drift
Question
The fleet
Take
Adopt the reusable workflows?
SocialMedia + Reinsurance yes; Plugins partial; Education + Manufacturing no
Yes. The two that hand-roll predate the extraction
strict branch protection?
Plugins true; the rest false or unprotected
false — true is a measured merge treadmill
Commit/push policy in AGENTS.md
4 repos "finish it / merge by default"; SocialMedia "wait to be asked"
Finish it — matches core
Where the authoring rules live
SocialMedia/Manufacturing/Reinsurance point at Plugins' AGENTS.md; Plugins holds them
Point. A restated rule is a forked rule
.gitignore and .claude/
Memex blanket-ignores it; the five node repos ignore nothing
Neither — .claude/* + !.claude/skills/
Which scripts/ copy
4-5 variants of most files; Plugins is the hardened superset
Plugins, SKIP adjusted — not the nearest neighbour
e2e/ suite?
Plugins/Reinsurance/SocialMedia share 00-bootstrap, login, apps, covers, helpers.ts, packages.ts; Education has 25 specs; Manufacturing has none
Optional. If you add one, arm it in CI — SocialMedia shipped specs nothing ran until an e2e-static job was added
Forwarding AccessContext through async/reactive boundaries, and the no-async rule on the actor-model mesh. Use when writing or reviewing any hub-reachable / Blazor-view / agent-round / IIoPool code that creates, reads, or updates a mesh node, OR when a write "silently does nothing" / "Access denied" appears after a .Subscribe / IIoPool / Observable.Create hop. The async/await/ToTask/.Wait/.Result/.GetAwaiter/FirstAsync family and lost AccessContext are the two ways async boundaries cause wedges and storms. Grounded in AsynchronousCalls.md, SyncedMeshNodeQueries.md, AccessContextPropagation.md.
What CI proves, what it cannot prove, and how to write a gate that cannot pass on no evidence. Use when authoring or changing a GitHub Actions workflow, adding a required check, wiring a satellite repo CI, or reasoning about whether a green run actually covers your change. Covers the absolute no-skip-trapdoor rule (a gate must never carry continue-on-error on its input step nor an if that asks whether a secret is set — GitHub paints a skipped job the same colour as a passed one), the reusable workflow_call jobs satellite repos must call instead of hand-rolling, why runs on main are deliberately never cancelled, and the large body of in-mesh C# that no dotnet build or test ever type-checks.
Debug a deadlock / hang / timeout in the mesh — a test that times out, a hub handler whose response never arrives, a GetRemoteStream / GetMeshNodeStream / GetQuery that never emits, an agent round or per-instance-hub activation that parks forever. Use whenever something times out and you need to find WHERE the message flow breaks down. The timeout is the symptom, never the bug — raising it just hangs longer. Grounded in DebuggingMessageFlow.md.
Remove / reset a user (and their partition) on a running MeshWeaver portal (AKS). Use when you need to kick out a user — e.g. a throwaway test account — so they re-onboard from scratch. The normal delete API REFUSES a user's home ("user partition root … cannot be deleted"), so deletion is a Postgres-schema drop, not an MCP call. Covers where a user's data actually lives (per-user PG schemas named by BOTH id AND email, e.g. `rbuergi` + `rbuergi@systemorph.com`), how to reach PG (the connection string is inline in the portal's `ConnectionStrings__memex` env — NOT the `POSTGRES_PASSWORD` secret), the substring-match footgun (`buergi` also matches admin `rbuergi`), the in-memory resurrection gotcha (a recycle re-saves the node — a portal restart is what actually clears it), and the half-onboarded case where the user has NO schema at all (in-memory only → restart is the whole fix).
Deploy MeshWeaver — the two routes (shared AKS cluster, and Azure Container Apps via the Aspire test/prod modes) and the traps that make a deploy look done when it is not. Use when rolling a code change onto a portal, standing up or patching an environment, or verifying that what is running is what you built. FIRST RULE (maintainer, 2026-09-08): on AKS an operation is a Hosting/InstanceAction on the control instance (Roll, Restart, Suspend, Audit, Reconcile, …) executed by the in-cluster operator — az aks command invoke / kubectl are break-glass, never the procedure. Also covers the bootstrap build-image / set-image / restart sequence the operator runs for you, why an env deploy.sh is first-time setup and not a code-update path, why the database migration is a run-once Job that a helm upgrade runs — never a Deployment to roll — and the DbVersionGate that makes a portal refuse to serve ahead of its schema.
Build or review ANY MeshWeaver UI — layout areas, editable views, forms, tables, content editing, side-panel composers — by wiring the framework's EXISTING data-binding pieces, NEVER hand-rolling. The one rule is ALWAYS DATA-BIND, especially editable views: the backend layout area declares WHAT to render (a UiControl tree + node PATHS), and every value read / write-back happens on the GUI side through the per-node IMeshNodeStreamCache. Hand-woven binding — replicating a node into a /data/{id} copy + a save subscription, emitting HTML strings, hand-built selects/checkboxes, .Take(1) on a live stream, async/await/QueryAsync in a layout area — is the bug class behind "awaiting first data" / "renders empty" / a stuck editable view. Use when writing or reviewing a layout area, control, editor, form, table, or any data-bound view. Grounded in src/MeshWeaver.Layout/DataBinding/, Doc/GUI/DataBinding.md + Editor.md, the Edit extension (EditorExtensions.cs), and the node-bound editor controls.