Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions .github/workflows/ci-cli.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@ on:
push:
branches: [main]
paths:
- '.claude/skills/**'
- 'plugins/dotnet-maui/skills/maui-devflow-onboard/**'
- 'plugins/dotnet-maui/skills/maui-devflow-debug/**'
- 'src/Cli/**'
- 'src/DevFlow/Microsoft.Maui.DevFlow.Driver/**'
- 'eng/**'
Expand All @@ -17,7 +18,8 @@ on:
types: [opened, synchronize, reopened, edited]
branches: [main]
paths:
- '.claude/skills/**'
- 'plugins/dotnet-maui/skills/maui-devflow-onboard/**'
- 'plugins/dotnet-maui/skills/maui-devflow-debug/**'
- 'src/Cli/**'
- 'src/DevFlow/Microsoft.Maui.DevFlow.Driver/**'
- 'eng/**'
Expand Down
1 change: 1 addition & 0 deletions MauiLabs.slnx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
<Folder Name="/src/Cli/">
<Project Path="src/Cli/Microsoft.Maui.Cli.UnitTests/Microsoft.Maui.Cli.UnitTests.csproj" />
<Project Path="src/Cli/Microsoft.Maui.Cli/Microsoft.Maui.Cli.csproj" />
<Project Path="src/Cli/Microsoft.Maui.Cli.Skills/Microsoft.Maui.Cli.Skills.csproj" />
<Project Path="src/Cli/Microsoft.Maui.StartupProfiling/Microsoft.Maui.StartupProfiling.csproj" />
</Folder>
<Folder Name="/src/DevFlow/">
Expand Down
2 changes: 1 addition & 1 deletion plugins/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

Distributable agent skills for .NET MAUI development. Installable via the Copilot CLI, Claude Code, or VS Code plugin system.

DevFlow runtime skills (`maui-devflow-onboard`, `maui-devflow-debug`) are bundled with the `maui` CLI from `plugins/dotnet-maui/skills/`, installed with `maui devflow init`, and exposed through the plugin manifest.
DevFlow runtime skills (`maui-devflow-onboard`, `maui-devflow-debug`) are sourced from `plugins/dotnet-maui/skills/`, bundled into the `maui` CLI through the `Microsoft.Maui.Cli.Skills` project, installed with `maui devflow init`, and exposed through the plugin manifest.

## Plugin

Expand Down
1 change: 1 addition & 0 deletions src/Cli/Cli.slnf
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
"path": "../../MauiLabs.slnx",
"projects": [
"src\\Cli\\Microsoft.Maui.Cli\\Microsoft.Maui.Cli.csproj",
"src\\Cli\\Microsoft.Maui.Cli.Skills\\Microsoft.Maui.Cli.Skills.csproj",
"src\\Cli\\Microsoft.Maui.Cli.UnitTests\\Microsoft.Maui.Cli.UnitTests.csproj",
"src\\Cli\\Microsoft.Maui.StartupProfiling\\Microsoft.Maui.StartupProfiling.csproj",
"src\\DevFlow\\Microsoft.Maui.DevFlow.Driver\\Microsoft.Maui.DevFlow.Driver.csproj"
Expand Down
18 changes: 18 additions & 0 deletions src/Cli/Microsoft.Maui.Cli.Skills/MauiCliSkillResources.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
using System.Reflection;

namespace Microsoft.Maui.Cli.Skills;

public static class MauiCliSkillResources
{
public const string ResourceRoot = "devflow.skills";

public static Assembly Assembly => typeof(MauiCliSkillResources).Assembly;

public static IReadOnlyList<MauiCliSkillDefinition> BundledSkills { get; } =
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 MODERATE · consensus 3/3

Silent failure if BundledSkills list and .csproj embedded resource globs diverge.

These two manually-maintained lists must stay in sync: the BundledSkills entries here and the EmbeddedResource includes in the .csproj. If a future skill directory is added to the .csproj glob but not to this list (or vice versa):

  • Missing from BundledSkills → resources are embedded but never discovered (silent dead weight)
  • Missing from .csprojLoadSkillBundleAsync finds zero resources and throws InvalidOperationException("No embedded DevFlow skill resources found...") at runtime

The existing unit test hardcodes the two known skill names and wouldn't catch divergence when a third skill is added.

Recommendation: Add a test (or adjust the existing one) that dynamically iterates MauiCliSkillResources.BundledSkills and asserts each entry has at least one embedded resource with prefix {ResourceRoot}/{skill.Id}/, and conversely that every embedded resource prefix maps to a BundledSkills entry.

[
new("maui-devflow-onboard", "MAUI DevFlow Onboard", "Guides first-time MAUI DevFlow project integration.", Recommended: true),
new("maui-devflow-debug", "MAUI DevFlow Debug", "Guides build, deploy, connection recovery, inspect, and debug loops with MAUI DevFlow.", Recommended: true)
];
}

public sealed record MauiCliSkillDefinition(string Id, string DisplayName, string Description, bool Recommended);
21 changes: 21 additions & 0 deletions src/Cli/Microsoft.Maui.Cli.Skills/Microsoft.Maui.Cli.Skills.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFrameworks>net9.0;net10.0</TargetFrameworks>
<RootNamespace>Microsoft.Maui.Cli.Skills</RootNamespace>
<AssemblyName>Microsoft.Maui.Cli.Skills</AssemblyName>
<Description>Bundled agent skills for the .NET MAUI CLI</Description>
<IsPackable>false</IsPackable>
<IsAotCompatible>true</IsAotCompatible>
</PropertyGroup>

<ItemGroup>
<EmbeddedResource Include="..\..\..\plugins\dotnet-maui\skills\maui-devflow-onboard\**\*" LogicalName="devflow.skills/maui-devflow-onboard/%(RecursiveDir)%(Filename)%(Extension)">
<WithCulture>false</WithCulture>
</EmbeddedResource>
<EmbeddedResource Include="..\..\..\plugins\dotnet-maui\skills\maui-devflow-debug\**\*" LogicalName="devflow.skills/maui-devflow-debug/%(RecursiveDir)%(Filename)%(Extension)">
<WithCulture>false</WithCulture>
</EmbeddedResource>
</ItemGroup>

</Project>
15 changes: 15 additions & 0 deletions src/Cli/Microsoft.Maui.Cli.UnitTests/DevFlowSkillManagerTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,28 @@
using System.Text.Json.Nodes;
using Microsoft.Maui.Cli.DevFlow;
using Microsoft.Maui.Cli.DevFlow.Skills;
using Microsoft.Maui.Cli.Skills;
using Xunit;

namespace Microsoft.Maui.Cli.UnitTests;

[Collection("CLI")]
public sealed class DevFlowSkillManagerTests
{
[Fact]
public void BundledSkillResources_AreLoadedFromSkillsAssembly()
{
var skillsAssembly = MauiCliSkillResources.Assembly;
var cliAssembly = typeof(DevFlowSkillManager).Assembly;
var skillsResources = skillsAssembly.GetManifestResourceNames();
var cliResources = cliAssembly.GetManifestResourceNames();

Assert.NotSame(cliAssembly, skillsAssembly);
Assert.Contains($"{MauiCliSkillResources.ResourceRoot}/maui-devflow-onboard/SKILL.md", skillsResources);
Assert.Contains($"{MauiCliSkillResources.ResourceRoot}/maui-devflow-debug/SKILL.md", skillsResources);
Assert.DoesNotContain(cliResources, resource => resource.StartsWith($"{MauiCliSkillResources.ResourceRoot}/maui-devflow-", StringComparison.Ordinal));
}

[Fact]
public async Task InstallRecommended_ProjectScope_WritesBundledSkillsAndUserLevelState()
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\Microsoft.Maui.Cli.Skills\Microsoft.Maui.Cli.Skills.csproj" />
<ProjectReference Include="..\Microsoft.Maui.Cli\Microsoft.Maui.Cli.csproj" />
</ItemGroup>

Expand Down
21 changes: 13 additions & 8 deletions src/Cli/Microsoft.Maui.Cli/DevFlow/Skills/DevFlowSkillManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,13 @@
using System.Text;
using System.Text.Json;
using System.Text.Json.Nodes;
using Microsoft.Maui.Cli.Skills;

namespace Microsoft.Maui.Cli.DevFlow.Skills;

internal static class DevFlowSkillManager
{
const string ResourceRoot = "devflow.skills";
const string ResourceRoot = MauiCliSkillResources.ResourceRoot;
const string PackageId = "Microsoft.Maui.Cli";
const string StateRootEnvironmentVariable = "MAUIDEVFLOW_STATE_ROOT";
const string AutoTarget = "auto";
Expand All @@ -19,11 +20,9 @@ internal static class DevFlowSkillManager
static readonly TimeSpan FreshnessCheckInterval = TimeSpan.FromDays(7);
static readonly TimeSpan FreshnessPromptInterval = TimeSpan.FromDays(1);

static readonly DevFlowSkillDefinition[] s_skills =
[
new("maui-devflow-onboard", "MAUI DevFlow Onboard", "Guides first-time MAUI DevFlow project integration.", Recommended: true),
new("maui-devflow-debug", "MAUI DevFlow Debug", "Guides build, deploy, connection recovery, inspect, and debug loops with MAUI DevFlow.", Recommended: true)
];
static readonly DevFlowSkillDefinition[] s_skills = MauiCliSkillResources.BundledSkills
.Select(skill => new DevFlowSkillDefinition(skill.Id, skill.DisplayName, skill.Description, skill.Recommended))
.ToArray();

static readonly string[] s_legacySkillIds =
[
Expand Down Expand Up @@ -463,7 +462,7 @@ static bool MigrateLegacySkills(InstallTarget installTarget, JsonObject skillSta

static async Task<SkillBundle> LoadSkillBundleAsync(DevFlowSkillDefinition skill, CancellationToken cancellationToken)
{
var assembly = typeof(DevFlowSkillManager).Assembly;
var assembly = MauiCliSkillResources.Assembly;
var prefix = $"{ResourceRoot}/{skill.Id}/";
var resources = assembly.GetManifestResourceNames()
.Where(name => name.StartsWith(prefix, StringComparison.Ordinal))
Expand All @@ -488,7 +487,7 @@ static async Task<SkillBundle> LoadSkillBundleAsync(DevFlowSkillDefinition skill
files.Add(new SkillAssetFile(relativePath, content, HashContent(content)));
}

return new SkillBundle(skill.Id, GetCurrentCliVersion(), files, HashBundle(files));
return new SkillBundle(skill.Id, GetBundledSkillVersion(), files, HashBundle(files));
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 MODERATE · consensus 2/3

Version decoupling incomplete — update safety logic still coupled to CLI version.

This line correctly records GetBundledSkillVersion() in the bundle, but the update/downgrade safety logic elsewhere (UpsertStateEntry at ~line 809) still writes installedByCliVersion = GetCurrentCliVersion(), and the comparison at ~line 360 evaluates against CLI version. If the Skills assembly is ever versioned independently (a stated goal of this extraction), a skills-only hotfix (e.g., Skills v1.0.1 with CLI still at v1.0) would record installedByCliVersion = "1.0" even though the installed content is from v1.0.1. A subsequent older Skills bundle would bypass downgrade protection since installedByCliVersion == GetCurrentCliVersion().

Recommendation: Write installedByBundleVersion = GetBundledSkillVersion() separately in UpsertStateEntry, and use it (not installedByCliVersion) for the newer/older classification when deciding whether to allow overwrites. Keep installedByCliVersion as diagnostic metadata only.

}

static bool ShouldExcludeSkillAsset(string relativePath)
Expand Down Expand Up @@ -1144,6 +1143,12 @@ static string GetCurrentCliVersion()
?? typeof(DevFlowSkillManager).Assembly.GetName().Version?.ToString()
?? "unknown";

static string GetBundledSkillVersion()
=> MauiCliSkillResources.Assembly
.GetCustomAttribute<AssemblyInformationalVersionAttribute>()?.InformationalVersion
?? MauiCliSkillResources.Assembly.GetName().Version?.ToString()
?? GetCurrentCliVersion();
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟢 MINOR · consensus 2/3

Fallback to GetCurrentCliVersion() silently re-couples the assemblies.

If the Skills assembly has no AssemblyInformationalVersionAttribute and no AssemblyVersion (e.g., a malformed build), this silently falls back to the CLI's version. The skillVersion recorded in state would then be the CLI version rather than signaling something is wrong. Future diagnostics comparing bundledVersion vs installedVersion would appear identical, masking the issue.

Recommendation: Consider using "unknown" as the final fallback (consistent with GetCurrentCliVersion()'s own ultimate fallback) to make the failure mode visible rather than silently aliased to the CLI version. This is low-risk given IsPackable=false keeps versions in lockstep today.


static string? GetString(JsonObject node, string propertyName)
=> node.TryGetPropertyValue(propertyName, out var value) ? value?.GetValue<string>() : null;

Expand Down
10 changes: 1 addition & 9 deletions src/Cli/Microsoft.Maui.Cli/Microsoft.Maui.Cli.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@
</Target>

<ItemGroup>
<ProjectReference Include="..\Microsoft.Maui.Cli.Skills\Microsoft.Maui.Cli.Skills.csproj" />
<ProjectReference Include="..\Microsoft.Maui.StartupProfiling\Microsoft.Maui.StartupProfiling.csproj" />
<ProjectReference Include="..\..\DevFlow\Microsoft.Maui.DevFlow.Driver\Microsoft.Maui.DevFlow.Driver.csproj" />
</ItemGroup>
Expand All @@ -70,13 +71,4 @@
<Content Include="Build\MauiStartupProfiling.AutoInitialize.cs" CopyToOutputDirectory="PreserveNewest" CopyToPublishDirectory="PreserveNewest" />
</ItemGroup>

<ItemGroup>
<EmbeddedResource Include="..\..\..\plugins\dotnet-maui\skills\maui-devflow-onboard\**\*" LogicalName="devflow.skills/maui-devflow-onboard/%(RecursiveDir)%(Filename)%(Extension)">
<WithCulture>false</WithCulture>
</EmbeddedResource>
<EmbeddedResource Include="..\..\..\plugins\dotnet-maui\skills\maui-devflow-debug\**\*" LogicalName="devflow.skills/maui-devflow-debug/%(RecursiveDir)%(Filename)%(Extension)">
<WithCulture>false</WithCulture>
</EmbeddedResource>
</ItemGroup>

</Project>
Loading