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.py →
check_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:
HostingModuleSkillsTest— the four skills are insideMeshWeaver.SelfUpdate.Aks.dll, atHosting/Skill/*, as main nodes, with descriptions and bodies, and nothing was skipped.ModuleSkillProviderTest— the contract, over this test assembly's own embedded fixtures, including three files that must be REFUSED and an invariant that every resource is either a node or a recorded failure, so nothing can fall between.ModuleSkillDeliveryTest— a real mesh:MeshAgentSkillsSource, the source the Microsoft Agent Framework resolves an agent round's skills through, returnsEssentials/Skill/communicatewith its body, scoped to its own package.ModuleSkillAbsentWithoutTheModuleTest— the CONTROL. Same mesh, no installed module, skill gone. Without it the delivery test could not tell "the module delivered it" from "it was already there".