Files
dotnet__skills/agentic-workflows/dotnet-msbuild/build.ps1
Dan Moseley 698b335ab2 Enforce description size limits and add spec conformance checks (#238)
* Enforce description size limits (per-skill 1024, per-plugin 15K)

Add infrastructure to reject skills with oversized descriptions, per issue #222:

- SkillProfiler: check description length, flag DescriptionTooLong
- ValidateCommand: block oversized descriptions (survives --verdict-warn-only)
- New eng/validate-descriptions.ps1: validates per-skill and per-plugin aggregate limits
- New CI workflow: runs validation on PRs touching plugins/
- build.ps1: adds size checks to local build validation
- Unit tests for all new SkillProfiler behavior

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Update --verdict-warn-only help text to mention description_too_long

Address review feedback: the option description now lists description_too_long
alongside execution errors and --require-evals as conditions that remain
fatal even when --verdict-warn-only is enabled.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Add agentskills.io spec conformance checks to SkillProfiler

Validate name rules (max 64 chars, lowercase+hyphens, no edge/consecutive
hyphens, must match directory), compatibility field max 500 chars, body
max 500 lines, and file reference depth.

Scan of existing skills found 2 violations:
- dotnet-msbuild/msbuild-antipatterns: 582 lines (max 500)
- dotnet-msbuild/directory-build-organization: cross-skill ref 3 levels deep

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Address review feedback on spec conformance checks

- Fix MakeSkill SkillMdPath to derive from resolved path (SkillProfileTests.cs)
- Fix RepoRoot in build.ps1 to be actual repo root (was agentic-workflows/)
- Fix body line count off-by-one: trim trailing newlines before splitting
- Fix file ref depth: normalize paths, handle .. traversals, strip #anchor,
  compute depth from directory segments only
- Add tests for parent traversal, anchor fragments, ./ normalization,
  and trailing newline body count

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Consolidate description validation into skill-validator C#

Move the per-plugin aggregate description limit (15K chars) from the
standalone PowerShell script into ValidateCommand, where it runs after
skill discovery. This eliminates the separate validate-descriptions.ps1,
its CI workflow, its PowerShell tests, and the delegation from build.ps1.

Also rename FailureKind from "description_too_long" to the generic
"spec_conformance_failure" so future spec checks reuse the same kind.

Deleted:
  - eng/validate-descriptions.ps1
  - eng/tests/validate-descriptions.tests.ps1
  - .github/workflows/validate-descriptions.yml

Added:
  - ValidateCommand.CheckAggregateDescriptionLimits()
  - ValidateCommand.DerivePluginName()
  - 6 C# tests for aggregate limit + DerivePluginName

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Upgrade body-lines and file-ref-depth from warnings to blocking errors

Now that #247 has merged and fixed the two existing violations
(msbuild-antipatterns 582 lines, directory-build-organization cross-ref),
these spec conformance checks can be blocking.

Also replaces the DescriptionTooLong bool with a general Errors list on
SkillProfile, so all spec conformance violations (description length,
body lines, file ref depth) flow through the same mechanism.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Revert unrelated lock file changes

The lock file diffs were side effects of running build.ps1 to verify
the validation changes — they picked up upstream content changes
unrelated to this PR.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-03-07 00:16:33 -07:00

171 lines
5.6 KiB
PowerShell

# Build entry point for the agentic-workflows knowledge bundles.
# Validates skills and compiles knowledge lock files.
# Run: pwsh agentic-workflows/dotnet-msbuild/build.ps1
Set-StrictMode -Version Latest
$ErrorActionPreference = 'Stop'
$RepoRoot = Split-Path (Split-Path $PSScriptRoot -Parent) -Parent
$SkillsDir = Join-Path $RepoRoot 'plugins' 'dotnet-msbuild' 'skills'
$DomainGatePattern = 'Only activate in MSBuild/\.NET build context'
# ── Step 1: Validate skills ─────────────────────────────────────────
Write-Host '=== Validating skills ===' -ForegroundColor Cyan
Write-Host ''
$errors = 0
$skillDirs = Get-ChildItem -Path $SkillsDir -Directory |
Where-Object { $_.Name -ne 'shared' }
foreach ($dir in $skillDirs) {
$skillFile = Join-Path $dir.FullName 'SKILL.md'
if (-not (Test-Path $skillFile)) { continue }
$content = Get-Content $skillFile -Raw
if ($content -notmatch '(?s)^---\s*\r?\n(.*?)\r?\n---') {
Write-Host "❌ $($dir.Name): Missing YAML frontmatter" -ForegroundColor Red
$errors++
continue
}
$frontmatter = $Matches[1]
if ($frontmatter -notmatch 'description:\s*"([^"]*)"') {
Write-Host "❌ $($dir.Name): Missing description in frontmatter" -ForegroundColor Red
$errors++
continue
}
$description = $Matches[1]
if ($description -notmatch $DomainGatePattern) {
Write-Host "❌ $($dir.Name): Description missing domain gate. Must include 'Only activate in MSBuild/.NET build context.'" -ForegroundColor Red
$errors++
}
}
if ($errors -gt 0) {
Write-Host "`n$errors validation error(s) found." -ForegroundColor Red
exit 1
} else {
Write-Host "✅ All $($skillDirs.Count) skills pass validation.`n" -ForegroundColor Green
}
# ── Step 2: Compile knowledge bundles ────────────────────────────────
Write-Host '=== Compiling knowledge ===' -ForegroundColor Cyan
Write-Host ''
$KnowledgeGroups = [ordered]@{
'build-errors' = @(
'binlog-failure-analysis'
'binlog-generation'
'check-bin-obj-clash'
'including-generated-files'
)
'performance' = @(
'build-perf-baseline'
'build-perf-diagnostics'
'incremental-build'
'build-parallelism'
'eval-performance'
)
'style-and-modernization' = @(
'msbuild-antipatterns'
'msbuild-modernization'
'directory-build-organization'
)
}
$KnowledgeTargets = @{
'agentic-workflows' = @{
OutputDir = Join-Path $PSScriptRoot 'shared' 'compiled'
MaxChars = 40000
}
}
function Read-Skill([string]$SkillName) {
$skillDir = Join-Path $SkillsDir $SkillName
$skillPath = Join-Path $skillDir 'SKILL.md'
if (-not (Test-Path $skillPath)) {
Write-Host " ⚠ Skill not found: $SkillName ($skillPath)" -ForegroundColor Yellow
return $null
}
$content = Get-Content $skillPath -Raw
# Strip YAML frontmatter (tolerate both LF and CRLF)
if ($content -match '(?s)^---\r?\n.*?\r?\n---\r?\n(.*)$') {
$content = $Matches[1]
}
# Inline linked references: replace [text](references/file.md) with file content
$content = [regex]::Replace($content, '\[([^\]]*)\]\((references/[^\)]+\.md)\)', {
param($m)
$refPath = Join-Path $skillDir $m.Groups[2].Value
if (Test-Path $refPath) {
$refContent = (Get-Content $refPath -Raw).Trim()
return $refContent
}
return $m.Value
})
return $content.Trim()
}
function Compile-KnowledgeFile([string]$OutputName, [string[]]$SkillNames, [string]$OutputDir, [int]$MaxChars) {
$ext = '.lock.md'
Write-Host " Compiling: $OutputName$ext"
$sections = [System.Collections.Generic.List[string]]::new()
$totalChars = 0
$header = "<!-- AUTO-GENERATED — DO NOT EDIT -->`n`n"
$totalChars += $header.Length
foreach ($skillName in $SkillNames) {
$content = Read-Skill $skillName
if ($null -eq $content) { continue }
if ($totalChars + $content.Length -gt $MaxChars) {
Write-Host " ⚠ Truncating $skillName — would exceed $MaxChars char limit" -ForegroundColor Yellow
$remaining = $MaxChars - $totalChars
if ($remaining -gt 500) {
$sections.Add("## $skillName`n`n$($content.Substring(0, $remaining))`n`n[truncated]")
$totalChars += $remaining
}
break
}
$sections.Add($content)
$totalChars += $content.Length
Write-Host " ✓ $skillName ($($content.Length.ToString('N0')) chars)"
}
$output = $header + ($sections -join "`n`n---`n`n")
$outputPath = Join-Path $OutputDir "$OutputName$ext"
[System.IO.File]::WriteAllText($outputPath, $output)
Write-Host " → $OutputName$ext ($($output.Length.ToString('N0')) chars total)"
}
function Compile-Target([string]$TargetName, [hashtable]$Config) {
Write-Host "`n📦 Target: $TargetName"
Write-Host " Output: $($Config.OutputDir)"
New-Item -Path $Config.OutputDir -ItemType Directory -Force | Out-Null
foreach ($entry in $KnowledgeGroups.GetEnumerator()) {
Compile-KnowledgeFile -OutputName $entry.Key -SkillNames $entry.Value -OutputDir $Config.OutputDir -MaxChars $Config.MaxChars
}
}
Write-Host "Skills source: $SkillsDir"
foreach ($entry in $KnowledgeTargets.GetEnumerator()) {
Compile-Target -TargetName $entry.Key -Config $entry.Value
}
Write-Host "`n✅ Build complete." -ForegroundColor Green