tools/validate-bundle.ps1
|
# Requires PowerShell 5.1+ compatible syntax (no PS7-only constructs). # Dev-time validator for the Maestro bundle. Run from the repo root or pass -Root. param( [string]$Root ) $ErrorActionPreference = "Stop" if (-not $Root) { $Root = Split-Path -Parent $PSScriptRoot } $script:failures = 0 function Fail([string]$msg) { $script:failures++ Write-Host (" FAIL " + $msg) -ForegroundColor Red } function Pass([string]$msg) { Write-Host (" ok " + $msg) -ForegroundColor DarkGray } $KnownTokens = @("WORKSPACE", "MAESTRO_CONFIG") $PreflightNeedles = @("git rev-parse --show-toplevel", "references/conventions.md") Write-Host "== Maestro bundle validation ==" Write-Host ("root: " + $Root) Write-Host "" function Get-MdFiles([string]$dir) { if (Test-Path -LiteralPath $dir) { return @(Get-ChildItem -LiteralPath $dir -Filter "*.md" -Recurse -File | Where-Object { $_.FullName -notmatch "[\\/]backup[\\/]" }) } return @() } function Remove-FencedBlocks([string]$text) { # Toggle out fenced code blocks so template/example links are not link-checked. $lines = $text -split "`r?`n" $out = New-Object System.Collections.Generic.List[string] $inFence = $false foreach ($line in $lines) { if ($line -match "^````") { $inFence = -not $inFence; continue } if (-not $inFence) { [void]$out.Add($line) } } return ($out -join "`n") } # --- 1. Frontmatter integrity ------------------------------------------------- $skillCount = 0 foreach ($f in @(Get-ChildItem -LiteralPath (Join-Path $Root "skills") -Filter "SKILL.md" -Recurse -File)) { $t = [System.IO.File]::ReadAllText($f.FullName) $name = [regex]::Match($t, "(?m)^name:\s*(\S+)").Groups[1].Value $desc = [regex]::Match($t, "(?m)^description:\s*(.+)$").Groups[1].Value if (-not $name -or -not $desc) { Fail ("skills/" + $f.Directory.Name + "/SKILL.md: missing name/description frontmatter") } else { $skillCount++ } } Pass ("skills: $skillCount SKILL.md files with name+description") $agentOk = $true foreach ($f in @(Get-ChildItem -LiteralPath (Join-Path $Root "agents") -Filter "*.md" -File)) { $t = [System.IO.File]::ReadAllText($f.FullName) $name = [regex]::Match($t, "(?m)^name:\s*(\S+)").Groups[1].Value $desc = [regex]::Match($t, "(?m)^description:\s*(.+)$").Groups[1].Value $mode = [regex]::Match($t, "(?m)^mode:\s*(\S+)").Groups[1].Value if (-not $name -or -not $desc -or $mode -ne "subagent") { Fail ("agents/" + $f.Name + ": missing name/description or mode: subagent") $agentOk = $false } } if ($agentOk) { Pass "agents: all declare name+description+mode: subagent" } # --- 2. Placeholder tokens ----------------------------------------------------- $tokenBad = $false foreach ($f in @(Get-MdFiles (Join-Path $Root "references")) ) { $t = [System.IO.File]::ReadAllText($f.FullName) foreach ($m in [regex]::Matches($t, "\{\{([A-Za-z_]+)\}\}")) { if ($KnownTokens -notcontains $m.Groups[1].Value) { Fail ($f.Name + ": unknown placeholder {{" + $m.Groups[1].Value + "}}") $tokenBad = $true } } } if (-not $tokenBad) { Pass "placeholders: only {{WORKSPACE}} and {{MAESTRO_CONFIG}} appear" } # --- 3. Broken-link typo class: (...md} instead of (...md) -------------------- $typoBad = $false foreach ($f in @(Get-MdFiles $Root)) { $t = [System.IO.File]::ReadAllText($f.FullName) if ([regex]::IsMatch($t, "\]\([^()]*\.[A-Za-z0-9]+\}")) { Fail ($f.FullName.Substring($Root.Length) + ": markdown link ends with '}' instead of ')'") $typoBad = $true } } if (-not $typoBad) { Pass "links: no '.md}' typo-class breakage" } # --- 4. Link targets resolve (outside fences) --------------------------------- $linkBad = 0 foreach ($f in @(Get-MdFiles $Root)) { $t = Remove-FencedBlocks ([System.IO.File]::ReadAllText($f.FullName)) foreach ($m in [regex]::Matches($t, "\[[^\]]*\]\(([^)]+)\)")) { $target = $m.Groups[1].Value.Trim() if ($target -match "^(https?:|mailto:|#)" ) { continue } if ($target -match "[\{\}]") { continue } # placeholder paths resolved at runtime/install if ($target -cmatch "[A-Z]{3,}") { continue } # template placeholders like NNNN-slug.md $target = ($target -split "#")[0] if (-not $target) { continue } $resolved = Join-Path $f.DirectoryName $target if (-not (Test-Path -LiteralPath $resolved)) { Fail ($f.Name + ": broken link target '" + $target + "'") $linkBad++ } } } if ($linkBad -eq 0) { Pass "links: all non-fenced relative targets resolve" } # --- 5. Canonical Pre-flight in every skill and agent ------------------------- $promiseBad = $false foreach ($f in @((Get-ChildItem -LiteralPath (Join-Path $Root "skills") -Filter "SKILL.md" -Recurse -File) + (Get-ChildItem -LiteralPath (Join-Path $Root "agents") -Filter "*.md" -File))) { $t = [System.IO.File]::ReadAllText($f.FullName) foreach ($needle in $PreflightNeedles) { if (-not $t.Contains($needle)) { Fail ($f.Directory.Parent.Name + "/" + $f.Directory.Name + "/" + $f.Name + ": preflight missing '" + $needle + "'") $promiseBad = $true } } } if (-not $promiseBad) { Pass "preflight: every skill/agent resolves workspace + reads conventions.md" } # --- 6. Retry rule consistency ------------------------------------------------ $retryTargets = @( (Join-Path $Root "references/conventions.md"), (Join-Path $Root "references/plan.md"), (Join-Path $Root "skills/orchestrate/SKILL.md") ) $retryBad = $false foreach ($p in $retryTargets) { $t = ([System.IO.File]::ReadAllText($p)) -replace "\*", "" if (-not $t.Contains("Retries") -or -not ($t -match "immediately\s+(BEFORE|before)")) { Fail ((Split-Path -Leaf $p) + ": retry rule wording missing (expected spawn-counting 'immediately BEFORE' language)") $retryBad = $true } } if (-not $retryBad) { Pass "retries: spawn-counting rule present in conventions/plan/orchestrate" } # --- 7. References map completeness ------------------------------------------- $mapBad = $false $mapText = [System.IO.File]::ReadAllText((Join-Path $Root "references/references-map.md")) $listed = @() foreach ($m in [regex]::Matches($mapText, '(?m)^\| `[A-Za-z ]+` \| `([a-z0-9-]+\.md)` \|')) { $listed += $m.Groups[1].Value } foreach ($a in @(Get-ChildItem -LiteralPath (Join-Path $Root "references") -Filter "*.md" -File)) { if ($listed -notcontains $a.Name) { Fail ("references-map.md: missing row for " + $a.Name) $mapBad = $true } } if (-not $mapBad) { Pass "references-map: every reference file listed" } # --- 8. README counts match reality -------------------------------------------- $readme = [System.IO.File]::ReadAllText((Join-Path $Root "README.md")) $sCount = @(Get-ChildItem -LiteralPath (Join-Path $Root "skills") -Directory).Count $aCount = @(Get-ChildItem -LiteralPath (Join-Path $Root "agents") -Filter "*.md" -File).Count $rCount = @(Get-ChildItem -LiteralPath (Join-Path $Root "references") -Filter "*.md" -File).Count $expected = "\*\*$sCount skills\*\* \+ \*\*$aCount agents\*\* \+ \*\*$rCount reference specs\*\*" if ($readme -notmatch $expected) { Fail ("README counts line does not match disk (actual: $sCount skills / $aCount agents / $rCount references)") } else { Pass "counts: README matches disk ($sCount/$aCount/$rCount)" } # --- 9. Vocabulary drift -------------------------------------------------------- $driftBad = $false foreach ($f in @(Get-MdFiles $Root)) { $t = [System.IO.File]::ReadAllText($f.FullName) foreach ($m in [regex]::Matches($t, "(?i)(play|tune)[` ]{0,3}(skill|command)s?")) { Fail ($f.FullName.Substring($Root.Length) + ": '" + $m.Value.Trim() + "' — play/tune are subagents, not skills") $driftBad = $true } } if (-not $driftBad) { Pass "vocabulary: no play/tune-as-skill drift" } # --- Summary ------------------------------------------------------------------- Write-Host "" if ($script:failures -gt 0) { Write-Host ("VALIDATION FAILED: " + $script:failures + " problem(s)") -ForegroundColor Red exit 1 } Write-Host "ALL CHECKS PASSED" -ForegroundColor Green exit 0 |