Skills that ship with their module

A skill that a package owns travels inside that package's module assembly, and arrives with the code it documents. One artifact, one version, one identity check — and no separate sync that can report success while landing nothing.

The incident this replaces

Hosting/Skill/ held four skills — /deployment, /deployment-activity, /instance, /platform-update — added between 2026-08-09 and 2026-08-18. On 2026-08-31 none of them was on memex.meshweaver.cloud.

The Space's _GitSync recorded lastSyncCommitSha: 1befa1b1, and git ls-tree 1befa1b1 Hosting/Skill/ lists all four files. So the sync had read the commit that carried them and reported success. Thirteen days passed before anyone noticed, and the reason is structural: the only symptom of a missing skill is that nobody types it. A green build, a green validate and a sync that says "up to date" are all compatible with the skill never existing.

Meanwhile the AI engine's own skills — 23 of them in src/MeshWeaver.AI/Data/Skill/ — have never gone missing. Same node type, same discovery, same content shape. The only difference is delivery: they are embedded resources inside MeshWeaver.AI.dll, served by BuiltInSkillProvider.

So: generalise the mechanism that has never failed.

The convention — one csproj line

A module ships its skills as embedded resources under MeshSkills/{Package}/{id}.md:

<ItemGroup>
  <EmbeddedResource Include="MeshSkills/**/*.md" />
</ItemGroup>

and nothing else. There is deliberately no marker attribute and no registration call. A module that had to reference MeshWeaver.AI in order to ship a skill would couple the AKS self-update mechanics — or the collaboration renderer — to the agent runtime for the sake of a markdown file. The engine reads the resource names of the assemblies the mesh installed (InstalledModuleAssembly, what MeshBuilder.InstallAssemblies records), so the entire coupling is a folder name.

The folder under MeshSkills/ names the partition, which is why exactly one level is allowed: MeshSkills/Hosting/deployment.md → the node Hosting/Skill/deployment. A file nested deeper, an id that is not a typeable slash word, or front matter that does not parse is skipped and RECORDED — never skipped silently. Parsing goes through SkillMarkdown, the same reader the platform skills and the sync-back writer use, so read and write cannot drift.

Placement IS targeting

The node lands inside the package's own partition because that is what makes a decentral skill decentral. SkillNodeType.SkillQueries asks path:{partition} scope:descendants nodeType:Skill for the partition of the space in view and of the node TYPE in view. /deployment is therefore offered on Hosting content and nowhere else.

🚨 A module skill is not a platform default and must never become one. The platform defaults have a single master, src/MeshWeaver.AI/Data/Skill/, guarded by validate-repos.pycheck_one_skill_master. Putting a package's private vocabulary in the global Skill partition would put it in every user's slash menu.

Why a static NODE provider and not an IStaticRepoSource

The engine's own skills are materialised into the database by SkillStaticRepoSource, which owns the whole Skill partition. A module's skills live in a subtree of a partition that GitSync already owns, and IStaticRepoSource is whole-partition in three ways that all end badly there:

what a partition-scoped source would do to Hosting
prune mirrors the partition to the source — under FullReplace it would delete every node the module does not carry
PartitionRoot the importer always ensures a partition root, so it would re-write the plugin's own Store/Plugin root
Admin/_SourceOwnedCatalogs the partition is recorded as source-owned; a boot that no longer sees the module DELETES the entire partition subtree as orphaned

Contributing NODES instead composes with GitSync rather than competing with it: nothing is pruned, no root is touched, no ownership is claimed, and the query fan-in unions these with whatever Postgres serves in the same partition.

The cost is the one recorded on StaticNodeProviderExtensions.DescribeStaticServeCollision: a statically served path cannot ALSO hold a durable row — the create path answers "node already exists" from the static entry and every upsert lands on a hub with no persistence backing. That is why a skill delivered this way must not also exist as a node file in the package's repo folder. One authored home: the module. Moving a skill in is a git mv, not a copy.

No _Policy is contributed either. BuiltInSkillProvider ships one because it owns the Skill partition and nothing else would give it a read policy; a module skill lives inside a package partition that already carries its own governance — Hosting is an enterprise-tier gated package — and a second policy landing there as a side effect of a module install would be an access change nobody asked for. A skill is exactly as readable as the package it belongs to.

What ships this way today

package module skills
Hosting MeshWeaver.SelfUpdate.Aks /deployment, /deployment-activity, /instance, /platform-update
Essentials MeshWeaver.Markdown.Collaboration /communicate

What deliberately stays on GitSync

A package with no assembly has no module to ship in, and inventing one would be worse than the problem. Feedback, Providers, RemoteControl, RolePlay and Store are node-only packages; their skills stay where they are. So do the skills authored as .json (Hosting/Skill/setup-environment, Essentials/Skill/models, Edu/Skill/*, DataModelling/Skill/*, Publish/Skill/*, Video/Skill/*, Training/Skill/tour, Observability/Skill/debug) — this mechanism reads .md skill files, which is the authoring form AGENTS.md mandates for a skill; a .json skill is a separate question.

Essentials/Skill/email.md landed while this change was in flight and is a one-line git mv away from riding the module too — the loader globs MeshSkills/{Package}/*.md, so adding a skill needs no code change at all.

The one thing the mechanism needed from the platform

The bytes arrive and the mesh resolves them — IMeshService.Query("path:Essentials scope:descendants nodeType:Skill") returns the node, typed, from the assembly alone. But the agent surface resolves skills through a multi-query UNION (MeshQueryRequest.FromQueries), and StaticNodeQueryProvider read the legacy single Query property, which FromQueries fills with list[0]. The storage provider has always iterated EffectiveQueries; the static one never did, so a static node matched only by query #2 was silently absent.

The skill query set leads with the platform Skill catalog and follows with the package partition — which is exactly query #2. So built-in skills always resolved and a module-shipped one never did. Fixed in MeshWeaver#2942; until that lands in the platform ref this repo builds against, the agent-surface cases of ModuleSkillDeliveryTest fail and the mesh-query case passes, which is the correct split of the evidence.

Proving it, rather than assuming it

The mechanism being replaced had a green everything. So the tests assert at the far end of the chain, and they read the built assembly, never the source file on disk:

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