Skip to content
Merged
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
4 changes: 2 additions & 2 deletions .config/dotnet-tools.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,14 @@
"rollForward": false
},
"dotnet-inspect": {
"version": "0.26.0",
"version": "0.27.0",
"commands": [
"dotnet-inspect"
],
"rollForward": false
},
"purview.build": {
"version": "0.6.1",
"version": "0.8.2",
"commands": [
"purview-build"
],
Expand Down
2 changes: 1 addition & 1 deletion .editorconfig
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Purview.BuildSdk 1.3.0 - managed file, do not edit
# Purview.BuildSdk 1.3.1 - managed file, do not edit
#
# This file is written by Purview.BuildSdk and is replaced whenever the SDK version changes. The version
# on the first line is how the SDK recognises a file it owns: a .editorconfig without that marker is
Expand Down
57 changes: 53 additions & 4 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ The authored set, and what each one answers:
| [`purview-buildsdk-skill-tests`](./skills/purview-buildsdk-skill-tests/SKILL.md) | authoring a skill, and validating it with `Purview.Skills.Testing` |
| [`purview-buildsdk-skill-installation`](./skills/purview-buildsdk-skill-installation/SKILL.md) | installing the skills a package ships, and the `PRSKL*` diagnostics |
| [`purview-buildsdk-repository-review`](./skills/purview-buildsdk-repository-review/SKILL.md) | auditing a repository against the SDK's conventions |
| [`purview-buildsdk-justfile`](./skills/purview-buildsdk-justfile/SKILL.md) | adding or updating `just` recipes that run the skill targets |

After editing one, run `just skills-validate` for the pack-time check and `just test` for the full rule set.

Expand Down Expand Up @@ -128,13 +129,15 @@ dotnet msbuild <project> -t:InstallPurviewSkills # delegates to the skills CLI

That makes version compatibility exact by construction: the resolved ref is the commit the *restored package version* was built from, so an older package reference gets that version's skills rather than whatever is on the default branch. A package whose nuspec has no GitHub repository URL, or no commit, cannot be pinned — it is reported as `PRSKL0002` rather than installed from a guessed branch. `PRSKL0001` is raised when `project.assets.json` is missing, because resolution reads the restore graph and deliberately does not trigger a restore of its own.

Installation itself is delegated to the skills CLI (`npx skills`) rather than reimplemented, because the CLI owns the agent-specific destinations and their discovery conventions — which differ per agent and change independently of this SDK — and owns `skills-lock.json`, so a repository can restore the same set later with `npx skills experimental_install`. `PurviewSkillsAgents` (default `*`) narrows the target agents.
Installation itself is delegated to the skills CLI (`bun x skills` by default) rather than reimplemented, because the CLI owns the agent-specific destinations and their discovery conventions — which differ per agent and change independently of this SDK — and owns `skills-lock.json`, so a repository can restore the same set later with `skills experimental_install`.

`PurviewSkillsAgents` is **empty by default**, which passes no `--agent` at all, so the CLI auto-detects agents. Set, it becomes one `--agent` per name (`_ResolvePurviewSkillsAgents` splits on `,`, `;` and spaces) — the CLI's `--agent` does not split a comma-separated value, so the old `--agent "claude-code,codex"` form named one agent that does not exist. `dotnet msbuild -p:` itself splits on `,` and `;`, so a command line needs `%2C` or a quoted value; tests use `%2C`.

### Package runners and the repository root

Two invariants govern how the CLI is launched, and both are load-bearing.

**Any package runner, defaulting to npx.** The CLI is published to npm and each runner spells "run this npm package" differently. `PurviewSkillsRunner` (default `npx`, because npx ships with Node itself) selects one; the mapping lives in the `PurviewSkillsRunnerLauncher` item group in `Sdk/Sdk.targets`, which carries an `Exec` value (version-pinned, for `InstallPurviewSkills`) and a `Display` value (unpinned, for recommendation text) per runner. A runner with no entry falls through to `<runner> $(PurviewSkillsPackage)`, so an unknown runner still works. Each invocation form was verified against the real tool — `npx -y`, `bunx`, `bun x`, `pnpm dlx`, `yarn dlx` (Yarn 2+ only), `deno run -A npm:` — because a wrong form is a command that simply does not run.
**Any package runner, defaulting to bun.** The CLI is published to npm and each runner spells "run this npm package" differently. `PurviewSkillsRunner` (default `bun`, i.e. `bun x skills@1`) selects one; the mapping lives in the `PurviewSkillsRunnerLauncher` item group in `Sdk/Sdk.targets`, which carries an `Exec` value (version-pinned, for `InstallPurviewSkills`) and a `Display` value (unpinned, for recommendation text) per runner. A runner with no entry falls through to `<runner> $(PurviewSkillsPackage)`, so an unknown runner still works. Each invocation form was verified against the real tool — `npx -y`, `bunx`, `bun x`, `pnpm dlx`, `yarn dlx` (Yarn 2+ only), `deno run -A npm:` — because a wrong form is a command that simply does not run.

`DetectPurviewSkillsCli` (inline task) probes `PATH` for `skills` first, then `$(PurviewSkillsRunner)`, then `$(PurviewSkillsRunnerCandidates)`, honouring `PATHEXT` on Windows where these are `.cmd` shims. `PATH` is read directly rather than by spawning a version check: deterministic, no process, cannot hang on a cold download. The runner actually found wins over the configured preference. Nothing found is `PRSKL0004`; `PurviewSkillsCommand` overrides the launcher entirely and skips detection.

Expand Down Expand Up @@ -204,10 +207,53 @@ cost was pure duplication. The remaining work is now paid only when somebody ask
```bash
dotnet msbuild <project> -t:RecommendPurviewSkills # report, never fail
dotnet msbuild <project> -t:VerifyPurviewSkills # report, and fail on anything unverifiable
dotnet msbuild <project> -t:InstallPurviewSkills # install what is missing
dotnet msbuild <project> -t:InstallPurviewSkills # install every shipped skill at its package's commit
dotnet msbuild <project> -t:UpdatePurviewSkills # re-add every skills-lock.json source, then 'skills update'
dotnet msbuild <project> -t:CheckPurviewSkills # only the PurviewSkillsAgents directories exist
dotnet msbuild <project> -t:FixPurviewSkills # remove the CLI's links from any other agent directory
dotnet msbuild <project> -t:ListPurviewSkills # what the restored packages ship
```

### Once per solution: the leader

Pointed at a solution, MSBuild runs a custom target in **every** project, so each skill target used
to re-add every source and re-run the CLI once per project. Every public skill target is now a thin
wrapper: `_ResolvePurviewSkillsProjects` reads the solution's project list from
`CurrentSolutionConfigurationContents` (the same value in every project, for `.sln` and `.slnx`;
outside a solution it is the project itself), asks each project for its assets file through
`_GetPurviewSkillsProjectInfo`, and elects as leader the first, by sorted path, that answered. The
wrapper then calls `_<Target>Core` on the leader through the `MSBuild` task. MSBuild runs one
project's target once per set of global properties and returns the cached result to every later
caller, so the work runs once — no lock and no stamp file.

Four things are load-bearing:

- **The leader is a consumer project, not a `.proj` shipped in the package.** The skill settings live
in the consumer's `Directory.Build.props`, which a project in the NuGet cache would never import.
- **Every wrapper passes identical global properties** (`_PurviewSkillsLeaderRemoveProperties` strips
`TargetFramework`/`RuntimeIdentifier`). Anything that varies per caller would give each caller its
own cache entry and quietly bring back the fan-out.
- **The query's reply has its own item type** (`_PurviewSkillsProjectInfoReply`). Asking the calling
project runs the query in the caller's own instance, so reusing the caller's item put the caller
first in its own list, and every project elected itself.
- `ResolvePurviewSkillsTarget` resolves across **every** answering project's assets file, so a package
only a test project references is found; packages are de-duplicated by id and version, and one id at
two versions is `PRSKL0017`. The old per-framework condition is gone: it made a multi-targeted
project's outer build resolve nothing.

Tests live in `src/tests/BuildSdk.IntegrationTests/PurviewSkillSolutionTests.cs`, which writes its
own `.slnx` because the harness's lists projects as `<File>` entries.

### Updating, checking and fixing

**Install, update and fix run only when named — never automatically.** Two layers, both tested in `PurviewSkillTargetIsolationTests`: no SDK target hooks or depends on them (a structural scan of every `.props`/`.targets` under `Sdk/`, including `*DependsOn` properties), and `_GuardExplicitPurviewSkillsTarget` — first in each one's `DependsOnTargets` — fails with `PRSKL0009` when `BuildingProject`, `DesignTimeBuild` or `MSBuildIsRestoring` is true, which catches a consumer's own `AfterTargets="Build"` hook. Do not add a `BeforeTargets`/`AfterTargets` to any of them, and do not weaken the guard.

`UpdatePurviewSkills` (`ReadPurviewSkillsLockSources` inline task) re-adds each unique lockfile source once, naming only the skills the lockfile records for it. A source a restored package ships is re-added at **that package's commit**, never unpinned — re-adding it at latest would turn a satisfied entry into `PRSKL0011` drift. Every other source is re-added at latest. Then `skills update -y -p` runs per `PurviewSkillsUpdateMode` (`Auto`: only when the lockfile hash changed; `Always`; `Never`). `skills update` re-fetches each entry at its recorded ref — verified in the CLI source — so it cannot undo the pin. The re-adds use target batching (`_UpdatePurviewSkillsSource`) so each failure is attributed to its source (`PRSKL0008`, `ErrorAndContinue` so all are named); `-p:PurviewSkillsDryRun=true` prints and runs nothing.

`CheckPurviewSkills`/`FixPurviewSkills` (`EnforcePurviewSkillDirectories`) do nothing unless `PurviewSkillsAgents` names agents. Then the only agent skill directories allowed are those agents', per the `PurviewSkillsAgentDirectory` item group (copied from the CLI's agent table — extend it, don't hard-code), plus `.agents/skills`, the canonical store every agent directory links into. The authored `skills/` folder is protected even though openclaw installs there. Fix deletes links only (never their targets — `RemoveDirectory` on Windows, `unlink` on Unix) and folders left empty; a real file is kept and reported as `PRSKL0015`, because it cannot be told from hand-written content. Note the CLI writes copies, not links, for some agent selections, so those are kept too.

Tests live in `src/tests/BuildSdk.IntegrationTests/PurviewSkillUpdateTests.cs`.

`VerifyPurviewSkills` is the gate a CI job calls. It escalates `PRSKL0010` (missing) and `PRSKL0011`
(drifted) from warning to **error** — both are factual, both are fixed by one command, and neither can
fire without a referenced package that really ships skills. `PRSKL0012` stays a warning, because a
Expand All @@ -225,7 +271,7 @@ Drift **never** recommends `npx skills update`: `update` installs the newest pub

Three things shape the output, and all of them are load-bearing:

- missing and drifted are warnings (actionable in one command), while unverifiable and orphaned are messages — and `VerifyPurviewSkills` is what turns them into failures; and
- missing and drifted are warnings (actionable in one command), while unverifiable and orphaned are messages — and `VerifyPurviewSkills` is what turns missing and drifted into failures;
- a correctly installed set produces no output at all.
- there is no deduplication stamp any more (`EnablePurviewSkillRecommendations`, `PurviewSkillRecommendationsInCI` and `PurviewSkillRecommendationStampPath` are retired with it). A stamp existed so a per-build hook could report one state once per repository; a target a person invokes should answer every time.

Expand Down Expand Up @@ -296,6 +342,9 @@ Prefer `just` tasks:
- `just pack`
- `just skills-validate` — the pack-time check over the authored skills
- `just skills-add <source>` — install a skill from another repository into `.agents/skills/`
- `just skills-update` — `UpdatePurviewSkills` then `FixPurviewSkills`
- `just skills-check` / `just skills-fix` — enforce the `PurviewSkillsAgents` layout
- `just skills-recommend` — `RecommendPurviewSkills` (was `skills-check`)
- `just version-sync` — rewrite the `Purview.BuildSdk` pins in the docs, `templates/global.json` and
`.editorconfig` marker after bumping `package.json`
- `just version-sync-check` — the same check without writing; what the pre-commit hook runs
Expand Down
44 changes: 44 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,50 @@ release summarise what the commits recorded rather than what was written at the

## [Unreleased]

### Added

- **`UpdatePurviewSkills`** re-adds every unique source in `skills-lock.json`, naming only the skills
recorded for it, then runs `skills update -y -p`. A source a restored package ships is re-added at
that package's commit, so package skills stay matched to the version referenced; every other source
is re-added at latest. `PurviewSkillsUpdateMode` (`Auto` default, `Always`, `Never`) decides when
`skills update` runs, and `PurviewSkillsDryRun=true` prints every command without running it (also
honoured by `InstallPurviewSkills`). New diagnostics `PRSKL0006`–`PRSKL0008`.
- **`CheckPurviewSkills` and `FixPurviewSkills`** enforce `PurviewSkillsAgents` when it is set: only
those agents' skill directories (plus the canonical `.agents/skills`) may exist at the repository
root (`PRSKL0015`). Fix removes the skills CLI's links — never their targets — and folders left
empty, and keeps and reports real files. The agent-to-directory map is the extensible
`PurviewSkillsAgentDirectory` item group; an unknown agent is `PRSKL0016`.
- **Install, update and fix are explicit-only, enforced.** `InstallPurviewSkills`,
`UpdatePurviewSkills` and `FixPurviewSkills` fail with `PRSKL0009` when pulled into a build, a
restore or a design-time build — including by a consumer's own `AfterTargets="Build"` hook. Run
them by name.
- **Skill targets run once per solution.** Pointed at a solution, every skill target used to run in
every project — re-adding each source and re-running the CLI once per project, racing on
`skills-lock.json`. Each project now hands the work to one elected leader (the first solution
project, by path, that imports the SDK), and MSBuild's result cache runs it once. Resolution reads
every solution project's restore graph, so a package only a test project references is found
without choosing a project for it. Point recipes at the solution; no repository changes are needed
otherwise. Two projects on different versions of one skill-shipping package are `PRSKL0017`.
Package resolution also works in a multi-targeted project now: its outer build used to fail the
per-framework condition and resolve nothing.
- New skill **`purview-buildsdk-justfile`**: adding or updating `just` recipes that run the skill
targets, and replacing hand-written skill scripts with them.
- `just skills-update`, `just skills-fix`; `just skills-check` now runs `CheckPurviewSkills`, and the
old recommendation report moved to `just skills-recommend`.

### Changed (breaking)

- **`PurviewSkillsRunner` defaults to `bun`** (`bun x skills@1`) instead of `npx`, and
`PurviewSkillsRunnerCandidates` to `bun;npx;bunx;pnpm;yarn;deno`. Recommendation text now reads
`bun x skills …`. Set `PurviewSkillsRunner=npx` to keep the previous behaviour.
- **`PurviewSkillsAgents` defaults to empty instead of `*`**: no `--agent` is passed and the skills CLI
auto-detects agents. Set it to choose.

### Fixed

- Several agents in `PurviewSkillsAgents` were passed as one `--agent "a,b"`, which the skills CLI
reads as a single agent named `a,b`. Each agent is now its own `--agent` flag.

## [1.3.0] - 2026-11-08

### Changed (breaking)
Expand Down
41 changes: 2 additions & 39 deletions Justfile
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
set quiet

import "Justfile.skills.just"

solution_file := "src/BuildSdk.slnx"
build_configuration := "Debug"
artifacts_folder := "./artifacts"
Expand Down Expand Up @@ -122,45 +124,6 @@ pack artifact_folder=artifacts_folder *args:
echo "==> Packing {{ BLUE }}{{ solution_file }}{{ NORMAL }} ({{ GREEN }}{{ current_version }}{{ NORMAL }}) to {{ YELLOW }}{{ artifact_folder }}{{ NORMAL }}"
dotnet pack "{{ solution_file }}" --configuration "{{ build_configuration }}" --output "{{ artifact_folder }}" {{ args }}

# Install skills from another repository into .agents/skills/, through the skills CLI
#
# This repository's *own* skills are not installed here. They are authored at skills/<name>/SKILL.md
# for other repositories to install, and are read from that path - AGENTS.md links to it, and
# `npx skills add purview-dev/build-sdk` is how a consumer gets them. .agents/skills/ is reserved for
# skills installed from other sources, and skills-lock.json is the committed record of those.
#
# This goes through the SDK's own InstallPurviewSkills target rather than calling the CLI directly,
# so the repository exercises the path it ships to consumers.
#
# just skills-add purview-dev/engineering-playbook
#
# Install skills from another repository into .agents/skills/ (append '#<ref>' to pin)
[group('Agent Skills')]
skills-add source *args:
echo "==> Installing skills from {{ BLUE }}{{ source }}{{ NORMAL }} into the agent directories"
dotnet msbuild src/src/BuildSdk/BuildSdk.csproj -t:InstallPurviewSkills -p:PurviewSkillsSource={{ source }} {{ args }}

# Validate the authored skills in skills/ against the Agent Skills specification
#
# The same check the pack runs (PRSKL0005). The broader rule set - names, descriptions, optional
# fields, bundled links, body size - runs as RepositorySkillTests in `just test`.
#
# Validate the authored skills in skills/ against the Agent Skills specification
[group('Agent Skills')]
skills-validate *args:
echo "==> Validating authored skills in {{ BLUE }}skills/{{ NORMAL }}"
dotnet msbuild src/src/BuildSdk/BuildSdk.csproj -t:ValidatePurviewSkills -v:n {{ args }}

# Report which skills the restored packages ship, and whether a runner is available
[group('Agent Skills')]
skills-list *args:
dotnet msbuild src/src/BuildSdk/BuildSdk.csproj -t:ListPurviewSkills -v:n {{ args }}

# Report missing, drifted or unverifiable skills without installing anything
[group('Agent Skills')]
skills-check *args:
dotnet msbuild src/src/BuildSdk/BuildSdk.csproj -t:RecommendPurviewSkills -v:n {{ args }}

# Format the code with CSharpier
[group('Utilities')]
lint-fix:
Expand Down
Loading
Loading