A .md skill imports as a real Skill with its instructions (#1984); banner visible-in-GUI E2E (#1809); EA calendar docs (#1690) - #2284
Conversation
…banner E2E (#1809) + EA calendar docs (#1690) #1984 — the second half. Step 1 (695b721) made MarkdownFileParser bind front matter case-insensitively, so `nodeType: Skill` finally landed the node TYPE. What it could not do is put the body where a skill's procedure lives: the catch-all parser produces MarkdownContent, SkillNodeType reads SkillDefinition, and ContentAs<T> recovers only a same-short-named type — so every such skill read Instructions == null. That intermediate state is WORSE than the bug it replaced, which is why the new guard asserts on the CONTENT and not the node type. Before, a broken skill was visible in a listing as a Markdown page; after, the node claims to be a Skill, appears in the slash menu, and does nothing. A guard keyed on NodeType would have passed the whole time — so SkillFileParserTest leads with `WithoutTheContributedParser_TheSkillIsTypedButEMPTY`, which states exactly that. The fix is a contributed SkillFileParser in MeshWeaver.AI, registered by AddAI alongside AgentFileParser and therefore tried before the catch-all — the same shape and the same reason ContributedParserPriorityTest already pins for agents. It delegates the FORMAT to SkillMarkdown, the one place a Skill node ↔ its .md is defined, and supplies only what the chain knows and SkillMarkdown cannot: the id/namespace derived from the file's path (SkillMarkdown places every skill in the platform Skill partition, correct for content/ai/Skill, wrong for a plugin shipping Hosting/Skill/deployment). SkillMarkdown's reader becomes case-insensitive too, and that is required rather than tidy: MarkdownFileParser.Serialize writes any Skill node it still owns with a PascalCase `NodeType:`, so a camelCase-only reader would refuse the file it had just written and the skill would degrade a second time. Case-insensitivity is strictly wider than the convention it replaces; the WRITER keeps camelCase, so SkillMarkdownRoundTripTest still pins the emitted bytes. The write side refuses rather than clobbers: SkillDefinition has no required members, so deserializing a not-yet-retyped node's MarkdownContent into it succeeds and yields an all-null definition. Claiming that write would emit a skill file with front matter and NO BODY. The parser declines unless the content is a typed SkillDefinition or an untyped payload whose $type says so, which leaves that node exactly where it is today — with the Markdown parser. ## The backfill needs no migration, and that is measured, not assumed ~11 skills are stored in the broken shape. The obvious reading is that something has to rewrite them — a LogonAction, or a SQL migration looping partition schemas. Neither is needed: CreateOrUpdateNodeRequest's UpdateAccordingToSourceNode already applies `NodeType = source.NodeType ?? state.NodeType` AND `Content = source.Content ?? state.Content`, so a re-import through the fixed parser retypes and refills the node as ordinary behaviour. The only open question was whether PackageInstaller's unchanged-check would SKIP that write; it does not, for either stored shape, and SkillRetypeOnReinstallTest pins both — plus the already-correct case, so the pin cannot be satisfied by an installer that rewrites everything every time. A CLAIMED node (SyncBehavior != Include) is deliberately not reached: claiming is what decouples a node from its package. Also folded in, both platform-side halves of the same cluster: #1809 — a browser-level assertion that the stale-build banner is VISIBLE, not merely written into the $Banner slot. Negative control first (an always-on banner would put a "newer build available" notice above every page), then a different latestAssemblyPath written through /api/mesh/patch — no recompile, no sleep, because the state is a pure function of two strings — then the banner, then the instance's own content still rendered underneath. Verified against a real launched portal: the run discriminated, the slot holding the empty StackControl of the no-offer path before the publish and the offer after it. Screenshots are written either way; the one on FAILURE is the point, since a Playwright timeout says only "the locator never matched" and the image says which of four very different defects it was. 🚨 That run also surfaced something worth knowing: since db552ff the Blazor view packs have no Debug-only reference in Memex.Portal.Monolith, so a dev `dotnet run` registers no MarkdownControl view and every control renders as its model's ToString(). The test's assertions are on the offer TEXT inside the banner's own container, which holds in both worlds; its doc comment records precisely what that does and does not prove, so nobody later reads a text match as evidence of styled markdown. #1690 — the documentation half. The code fix is Systemorph/MeshWeaver.Plugins#677 (the plugin moved out in 1145451); this repo still documented the three-tool calendar surface, and now documents GetEvent / UpdateEvent and the read-then-patch flow that replaces cancel-and-recreate — the shape that cost a user an eight-item checklist on 2026-08-16. Verified: AI, GitSync, PluginCatalog and the three touched test projects build -c Release -warnaserror with 0 Error(s) / 0 Warning(s); SkillFileParserTest 10/10, SkillRetypeOnReinstallTest 3/3, the existing parser suites 63/63, SkillMarkdownRoundTripTest + BuiltInSkillCatalogTest 10/10, DocumentationLinkIntegrityTest green, and the new E2E green against a launched portal. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
This PR addresses three tracked items: ensuring .md-authored Skills import as real Skill nodes with usable Instructions (#1984), adding a browser-level E2E assertion that the stale-build banner is actually visible in the portal UI (#1809), and updating Executive Assistant calendar documentation to reflect the non-destructive read-then-patch flow (#1690).
Changes:
- Add a contributed
SkillFileParser(registered byAddAI) plus shared path-derivation logic so.mdskills land asSkillDefinitionwith body →Instructions. - Add regression coverage for both parsing and reinstall/retype behavior of previously-broken skills.
- Add a Playwright E2E test asserting the stale-build banner is visible in a real browser, plus update EA calendar docs and add two “What’s New” entries.
Reviewed changes
Copilot reviewed 11 out of 11 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
| test/MeshWeaver.Portal.E2E.Test/StaleBuildBannerE2ETest.cs | New Playwright E2E test asserting stale-build banner visibility in-browser. |
| test/MeshWeaver.PluginCatalog.Test/SkillRetypeOnReinstallTest.cs | Pins that reinstall detects both broken stored shapes and rewrites as needed (no migration). |
| test/MeshWeaver.Content.Test/SkillFileParserTest.cs | Parser-chain regression tests: body → SkillDefinition.Instructions, casing, priority, and write-side refusal. |
| src/MeshWeaver.Documentation/Data/WhatsNew/2026-08-25-skill-markdown-instructions.md | What’s New entry for markdown-authored skills now importing correctly. |
| src/MeshWeaver.Documentation/Data/WhatsNew/2026-08-25-executive-assistant-calendar-edits.md | What’s New entry for EA calendar non-destructive edits + improved error behavior. |
| src/MeshWeaver.Documentation/Data/AI/ExecutiveAssistant.md | Updates EA calendar tool surface and documents read-then-patch edit flow. |
| src/MeshWeaver.AI/SkillMarkdown.cs | Makes front matter binding case-insensitive and adds IsSkillMarkdown gate for contributed parsing. |
| src/MeshWeaver.AI/Persistence/SkillFileParser.cs | New contributed .md Skill parser + safe write-claim rules to avoid clobbering. |
| src/MeshWeaver.AI/Persistence/MarkdownNodePath.cs | Shared relative-path → (id, namespace) logic for AI markdown parsers. |
| src/MeshWeaver.AI/Persistence/AgentFileParser.cs | Refactor to use shared MarkdownNodePath derivation. |
| src/MeshWeaver.AI/AIExtensions.cs | Registers SkillFileParser in DI alongside AgentFileParser so it wins over the catch-all markdown parser. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
Copilot review, correctly: `DeclaresSkillDefinition`'s JsonObject arm read the discriminator with `GetValue<string>()`, which THROWS whenever `$type` is present but is not a JSON string. `FileFormatParserRegistry.GetSerializerFor` calls `CanSerialize` with no catch around it, so that throw would take down a sync-back for the whole node rather than handing the file to the next parser — strictly worse than the clobber this method exists to prevent, and reached by exactly the malformed content it is supposed to guard against. Both arms now read the discriminator with a TRY (`JsonValue.TryGetValue<string>` beside the JsonElement arm's existing ValueKind check), and a theory covers the shapes that reach it from a real mesh — a numeric `$type`, an object `$type`, a null `$type`, and no `$type` at all — asserted against BOTH the JsonElement and the JsonObject shapes, since the two arms fail differently. SkillFileParserTest 14/14; Content.Test builds -c Release -warnaserror, 0 Error(s). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…thout it
Not part of this PR's subject, and included only because nothing merges until it is
fixed: `AiContentPackDriftTest.EveryFile_StillHashesToItsPinnedReconciliationPoint`
fails on `origin/main` itself, so every branch cut from it inherits the red and the
green-merge gate stops there.
`123f6734c` ("Deployment correctness…") edited `content/ai/Skill/group.md` — adding
the Email:Enabled-without-the-module caveat to the invitation section — without
re-pinning its ledger entry. The drift guard exists for exactly this and printed the
replacement hash; verified by measurement, not inference:
git show origin/main:content/ai/Skill/group.md is byte-identical to the working
tree, and this branch's diff touches neither content/ai/** nor the ledger, so the
drift is attributable to main alone.
Only the `core` hash moves. `state` stays `PackAbsent`, which was checked rather
than assumed: the MeshWeaver.Plugins master pack ships `Skill/group.json`, not a
`group.md`, so there is no counterpart to reconcile and nothing to carry over — the
entry keeps `pack: null`, which is the coherence rule `EveryEntry_DeclaresACoherentState`
enforces for that state. The 22 other PackAbsent entries carry no note, so a bare
hash re-pin is the shape the ledger already uses.
AiContentPackDriftTest 15/15.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Added: a red on
|
|
Whichever merges first, the other side of the merge makes the same edit to the same line, so git resolves it without a conflict and the second one simply becomes a no-op for that hunk. Prefer #2288 — it is the single-purpose PR and the right home for this. I have kept the commit here only because dropping it would leave this PR red on a defect it did not cause, for as long as #2288 takes to land. If you merge #2288 first I am happy to drop mine and re-push; say the word. |
Test Results (shard 4)2 260 tests 2 258 ✅ 14m 43s ⏱️ Results for commit d29d12a. ♻️ This comment has been updated with latest results. |
Test Results (shard 3) 9 files 9 suites 4m 51s ⏱️ Results for commit d29d12a. ♻️ This comment has been updated with latest results. |
Test Results (shard 5) 9 files 9 suites 7m 1s ⏱️ Results for commit d29d12a. ♻️ This comment has been updated with latest results. |
Test Results (shard 0)624 tests 620 ✅ 17m 7s ⏱️ Results for commit d29d12a. ♻️ This comment has been updated with latest results. |
Test Results (shard 1)2 038 tests 1 844 ✅ 9m 25s ⏱️ Results for commit d29d12a. ♻️ This comment has been updated with latest results. |
Test Results (shard 2)2 900 tests 2 797 ✅ 8m 14s ⏱️ Results for commit d29d12a. ♻️ This comment has been updated with latest results. |
Test Results 53 files + 53 53 suites +53 1h 1m 24s ⏱️ + 1h 1m 24s Results for commit d29d12a. ± Comparison against base commit 1486d26. ♻️ This comment has been updated with latest results. |
|
This PR is It states Cause: #2293 ("Delete Blazor and the portal GUI from core", I attempted the non-destructive unblock ( So it needs a manual resolution, and I deliberately have not attempted one. 🚨 Why, and it is worth care from whoever does: the base change is a 135k-line DELETION. Resolving conflicts against a delete is exactly where a branch-favoured hunk silently RESURRECTS files that were intentionally removed — and the result compiles, so nothing catches it. The session that owns #2293 hit seven such conflicts and reported that two of them would have undone a fix had the delete side been taken. After resolving, run the revert-check before pushing: git diff origin/main...HEAD --stat # THREE dotsIt must show only the files this PR intends to change. Any |
…tructions # Conflicts: # src/MeshWeaver.AI/AIExtensions.cs
…into fix/1984-skill-md-instructions
…sees AI CI: "The type or namespace name 'AI' does not exist in the namespace 'MeshWeaver'" at SkillFileParserTest.cs:2-3. Not merge damage and not a missing reference to restore. Main deliberately removed MeshWeaver.AI from MeshWeaver.Content.Test as part of #2276 (AI leaves the platform) — the same direction as #2363 and #2368, which moved AI-dependent suites out rather than re-adding the reference. This branch added a NEW test to the project that had just been cut loose. SkillFileParser is AI code (src/MeshWeaver.AI/Persistence/SkillFileParser.cs), so its test belongs in MeshWeaver.AI.Test. Moved, namespace updated to match. Re-adding the ProjectReference would have been the smaller diff and the wrong one: it reverses deliberate carve-out work to accommodate a test that was in the wrong project to begin with. Verified: MeshWeaver.AI.Test and MeshWeaver.Content.Test both build clean under -c Release -warnaserror; SkillFileParserTest 14/14 green in its new home. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…into fix/1984-skill-md-instructions
…g.Test no longer sees AI The same carve-out that moved SkillFileParserTest also took MeshWeaver.AI off MeshWeaver.PluginCatalog.Test's reference list (#2276), so this PR's second new test broke `Build solution (once)` the same way: test/MeshWeaver.PluginCatalog.Test/SkillRetypeOnReinstallTest.cs(3,18): error CS0234: The type or namespace name 'AI' does not exist in the namespace 'MeshWeaver' Re-adding the ProjectReference would re-couple exactly what the carve-out separated. The established destination already exists: MeshWeaver.AI.Test carries a ProjectReference to MeshWeaver.PluginCatalog for precisely this case ("the plugin-catalog install tests that exercise AI node types travel with the engine", maintainer decision 2026-08-26), and seven such tests already live there. This test asserts PackageInstaller.IsUnchanged over SkillDefinition / SkillNodeType — AI node types — so it belongs with them. Move only: namespace MeshWeaver.PluginCatalog.Test -> MeshWeaver.AI.Test, and the now-implicit `using MeshWeaver.AI` swapped for the explicit `using MeshWeaver.PluginCatalog`, mirroring StaticShadowedInstallTest's migration comment. No assertion changed. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Clears three issues in this repo. The largest is #1984; the other two are the platform-side halves of
#1809 and #1690.
Fixes #1984 — a
.mdskill imports as a real Skill, carrying its instructionsThis was half done, and the symptom had CHANGED rather than gone away. Step 1 (
695b72176,#1998) made
MarkdownFileParserbind front matter case-insensitively, so anodeType: Skillfilefinally landed the node type. What that could not do is put the body where a skill's procedure
lives: the catch-all parser produces
MarkdownContent,SkillNodeTypeisWithContentType<SkillDefinition>(), andContentAs<T>recovers only a same-short-named type — soSkillNodeType.cs:132readInstructions == nulland every such skill was silently empty.🚨 That intermediate state is worse than the bug it replaced, which is why the new guard asserts on
the CONTENT and not the node type. Before, a broken skill was visible in a listing as a Markdown
page. After, the node claims to be a Skill, appears in the slash-command list, and does nothing —
nothing is red, nothing is logged, and it looks completely normal. A guard keyed on
NodeTypewouldhave passed the whole time, so the test class leads with the failure stated as a fact:
The fix
A contributed
SkillFileParserinMeshWeaver.AI, registered byAddAIbesideAgentFileParserand therefore tried before the catch-all — the same shape, and the same reason,that
ContributedParserPriorityTestalready pins for agents. It delegates the FORMAT toSkillMarkdown(the one place a Skill node ↔ its.mdis defined, whose round tripSkillMarkdownRoundTripTestpins) and supplies only what the chain knows andSkillMarkdowncannot:the id/namespace derived from the file's path.
SkillMarkdownhard-codes the platformSkillpartition — correct for
content/ai/Skill, wrong for a plugin shippingHosting/Skill/deployment.SkillMarkdown's reader becomes case-insensitive too, and that is required, not tidiness:MarkdownFileParser.Serializewrites any Skill node it still owns with a PascalCaseNodeType:, soa camelCase-only reader would refuse the file it had just written and the skill would degrade a
second time. Case-insensitive matching is strictly wider than the convention it replaces (it
ignores internal capitalisation too, so
launchesSubThreadstill binds); the writer keepscamelCase, so the emitted bytes are unchanged.
The write side refuses rather than clobbers.
SkillDefinitionhas no required members, sodeserializing a not-yet-retyped node's
MarkdownContentinto it succeeds and yields an all-nulldefinition — claiming that write would emit a skill file with front matter and no body. The parser
declines unless the content is a typed
SkillDefinitionor an untyped payload whose$typesays so,which leaves that node exactly where it is today.
The backfill needs no migration — measured, not assumed
~11 skills are stored in the broken shape, and the obvious reading is that something must rewrite
them: a
LogonAction, or (worse) a SQL migration looping partition schemas. Neither is needed.UpdateAccordingToSourceNodealready applies bothNodeType = source.NodeType ?? state.NodeTypeandContent = source.Content ?? state.Content, so are-import through the fixed parser retypes and refills the node as its ordinary behaviour. The only
open question was whether
PackageInstaller.IsUnchangedwould SKIP that write and leave the nodebroken forever. It does not — for either stored shape — and
SkillRetypeOnReinstallTestpinsboth, plus the already-correct case so the pin cannot be satisfied by an installer that rewrites
everything every time.
The one node this deliberately does not reach is a claimed one (
SyncBehavior != Include), whichDecideAndWriteskips before asking whether anything changed. That is correct: claiming is thedeliberate act that decouples a node from its package.
Fixes #1809 — the stale-build banner is asserted VISIBLE in a browser
Its stated blocker (#1807) is closed, so this was unblocked.
StaleBuildBannerTestasserts therendered control tree through a real mesh, which proves the server writes into
$Banner; it does notprove a user sees anything.
StaleBuildBannerE2ETestdrives a real portal, in the shape the issuespecified and without weakening it:
anywhere (an always-on banner would put "newer build available" above every page in the portal);
latestAssemblyPathwritten through/api/mesh/patch(an RFC 7396 merge routedthrough
GetMeshNodeStream(typePath).Update, so the collection / framework version / Ok statusare preserved and the build stays usable) — no recompile and no sleep, because the state is a
pure function of two strings;
lost its content (or recycled itself) is the regression this feature replaced;
only "the locator never matched", while the image says whether the page was blank, still spinning,
showing a compile overlay, or rendering fine with no banner.
Verified against a real launched portal, and it discriminated: before the publish the slot held
the empty
StackControlthe no-offer path writes; after it, the offer, with the content marker stillbelow. Both screenshots are attached to the run.
🚨 A finding from that run, reported not fixed
Since
db552ffbf(merged today)Memex.Portal.Monolith.csprojhas an emptyCondition="'$(Configuration)' == 'Debug'"ItemGroup where the Blazor view-pack references used tobe — while the comment above it still says "Debug-conditioned ships-the-bits keeps local dev
whole". It does not: a dev
dotnet runnow registers noMarkdownControlview, so every controlrenders as its model's
ToString(). That is what the screenshots show, and it silently degradesevery E2E test driven by
E2E_LAUNCH=1— a test asserting on text still passes, because the dumpcontains the text.
The banner test's assertions are on the offer text inside the banner's own container, which holds in
both worlds, and its doc comment records exactly what that does and does not prove so nobody later
reads a text match as evidence of styled markdown. The dev-portal regression itself belongs to the
modularization programme, not to this PR — flagging it rather than papering over it.
#1690 — the documentation half
The code fix is Systemorph/MeshWeaver.Plugins#677 (the plugin moved out in
11454517a, so theissue is filed on the wrong repo). What stays here is
src/MeshWeaver.Documentation/Data/AI/ExecutiveAssistant.md, which still documented the three-toolcalendar surface. It now documents
GetEvent/UpdateEventand the read-then-patch flow thatreplaces cancel-and-recreate — the shape that cost a user an eight-item checklist on 2026-08-16 — and
corrects the stale claim that the plugin lives in
Memex.Portal.Shared.What's New
Two entries, both
Category: Fix: the skills fix, and the Executive Assistant calendar fix (whosecode ships from the plugins repo but whose user-visible change is real, and whose docs live here).
The E2E test is internal and gets none.
Verification
dotnet build -c Release -warnaserror:MeshWeaver.AI(full--no-incrementalrebuild),MeshWeaver.GitSync,MeshWeaver.PluginCatalog, and the three touched test projects — all0 Error(s) / 0 Warning(s).
SkillFileParserTest10/10,SkillRetypeOnReinstallTest3/3.MarkdownFileParserTest+AgentFileParserTest+ContributedParserPriorityTest63/63;SkillMarkdownRoundTripTest+BuiltInSkillCatalogTest10/10 (the reader change is the one that could have broken these).DocumentationLinkIntegrityTestgreen after rebuilding the embedded doc resources.StaleBuildBannerE2ETestgreen against a launched portal (skips in CI like the rest of the suite).🤖 Generated with Claude Code